fix(tools): describe visible strokes (#447)

- Include stroke color and rounded weight in node and child summaries
- Select the first visible stroke when hidden paints precede it
- Cover both describe output paths with mixed stroke visibility

Co-authored-by: Rob Coenen <753704+rcoenen@users.noreply.github.com>
This commit is contained in:
rcoenen 2026-08-01 10:40:30 -04:00 committed by GitHub
parent 90d1d37113
commit 9f80c35838
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 79 additions and 1 deletions

View file

@ -4,6 +4,7 @@
### Fixed
- Show stroke colors and weights in AI visual descriptions. (#447)
- Stop warning AI agents that supported inline SVG attributes were ignored. (#445)
- Help AI agents discover every shape supported by `create_shape`. (#448)
- Keep `fill="none"` and `stroke="none"` SVG paths transparent when rendering inline artwork. (#446)

View file

@ -16,6 +16,13 @@ function findSolidFillIndex(node: SceneNode): number {
return node.fills.findIndex((candidate) => candidate.type === 'SOLID' && candidate.visible)
}
function strokeSummary(node: SceneNode): string | null {
const stroke = node.strokes.find((candidate) => candidate.visible)
if (!stroke) return null
const weight = Math.round(stroke.weight * 100) / 100
return `${colorToHex(stroke.color)} ${weight}px stroke`
}
export function describeVisual(node: SceneNode, graph?: SceneGraph): string {
const parts: string[] = []
const fillIndex = findSolidFillIndex(node)
@ -23,7 +30,8 @@ export function describeVisual(node: SceneNode, graph?: SceneGraph): string {
const fill = node.fills[fillIndex]
parts.push(`${colorToHex(fill.color)}${boundFillSuffix(node, fillIndex, graph)} fill`)
}
if (node.strokes.length > 0 && node.strokes[0]?.visible) parts.push('bordered')
const stroke = strokeSummary(node)
if (stroke) parts.push(stroke)
if (node.cornerRadius > 0) parts.push('rounded')
if (node.clipsContent) parts.push('clipped')
for (const effect of node.effects) {
@ -83,6 +91,8 @@ export function summarizeContainer(node: SceneNode, graph?: SceneGraph): string
const fill = node.fills[fillIndex]
parts.push(`${colorToHex(fill.color)}${boundFillSuffix(node, fillIndex, graph)}`)
}
const stroke = strokeSummary(node)
if (stroke) parts.push(stroke)
if (node.cornerRadius > 0) parts.push('rounded')
const layout = describeLayout(node)
if (layout) parts.push(layout)

View file

@ -0,0 +1,67 @@
import { describe, expect, test } from 'bun:test'
import type { Color, Stroke } from '@open-pencil/scene-graph'
import { expectDefined } from '#tests/helpers/assert'
import { getTool, setupToolTest, type ToolResult } from '#tests/helpers/tools'
const NAVY: Color = { r: 2 / 255, g: 26 / 255, b: 59 / 255, a: 1 }
const HIDDEN_STROKE: Stroke = {
color: { r: 1, g: 0, b: 0, a: 1 },
weight: 4,
opacity: 1,
visible: false,
align: 'CENTER',
cap: 'NONE',
join: 'MITER'
}
const VISIBLE_STROKE: Stroke = {
color: NAVY,
weight: 10.126,
opacity: 1,
visible: true,
align: 'CENTER',
cap: 'NONE',
join: 'MITER'
}
interface ChildSummary {
summary: string
}
function setupStrokedChild() {
const { figma, graph } = setupToolTest()
const frame = figma.createFrame()
frame.name = 'Outline'
frame.resize(200, 200)
const outline = figma.createRectangle()
outline.resize(100, 4)
frame.appendChild(outline)
graph.updateNode(outline.id, {
fills: [],
strokes: [HIDDEN_STROKE, VISIBLE_STROKE]
})
return { figma, frameId: frame.id, outlineId: outline.id }
}
describe('describe stroke summaries', () => {
test('reports the first visible stroke in a child summary', () => {
const { figma, frameId } = setupStrokedChild()
const result = getTool('describe').execute(figma, { id: frameId }) as ToolResult
const children = result.children as ChildSummary[]
const summary = expectDefined(children[0], 'stroked child summary').summary
expect(summary).toContain('#021A3B 10.13px stroke')
})
test('reports the first visible stroke in the node visual summary', () => {
const { figma, outlineId } = setupStrokedChild()
const result = getTool('describe').execute(figma, { id: outlineId }) as ToolResult
expect(result.visual).toContain('#021A3B 10.13px stroke')
})
})