From 9a3d7ca702ee2e04370b3b2a6c0a3999b5d692dc Mon Sep 17 00:00:00 2001 From: Danila Poyarkov Date: Thu, 3 Sep 2026 08:38:06 +0300 Subject: [PATCH] fix(ai): require closed stock photo vectors - Reject open vector networks before contacting the provider\n- Cover closed and open vector targets explicitly\n- Label the stock photo prompt example code fence --- packages/core/src/tools/stock-photo/apply.ts | 7 +++ src/app/ai/chat/system-prompt.md | 2 +- tests/engine/tools/stock-photo/apply.test.ts | 64 +++++++++++++++++++- 3 files changed, 71 insertions(+), 2 deletions(-) diff --git a/packages/core/src/tools/stock-photo/apply.ts b/packages/core/src/tools/stock-photo/apply.ts index 32722482a..8bf4825b4 100644 --- a/packages/core/src/tools/stock-photo/apply.ts +++ b/packages/core/src/tools/stock-photo/apply.ts @@ -51,6 +51,13 @@ export async function applyPhoto( } } + if (node.type === 'VECTOR' && (node.vectorNetwork.regions?.length ?? 0) === 0) { + return { + id: req.id, + error: `"${node.name}" has no closed vector regions — use closed area geometry` + } + } + if (node.type !== 'BOOLEAN_OPERATION' && node.children.length > 0) { return { id: req.id, error: `"${node.name}" has children — use a leaf image placeholder` } } diff --git a/src/app/ai/chat/system-prompt.md b/src/app/ai/chat/system-prompt.md index eea90698d..210509cd1 100644 --- a/src/app/ai/chat/system-prompt.md +++ b/src/app/ai/chat/system-prompt.md @@ -98,7 +98,7 @@ No style={{}}, className, CSS. No named colors or rgb(). No percentage values. N `stock_photo` places real Pexels images on leaf image placeholders. Prefer Rectangle, Rounded Rectangle, or Ellipse placeholders; closed vector and Boolean geometry are also supported. Pass a JSON array — **all photos fetched in parallel**: -``` +```text stock_photo({ requests: '[{"id":"0:30","query":"wall street trading floor"},{"id":"0:58","query":"AI chip semiconductor"},{"id":"0:65","query":"bank finance credit card"}]' }) ``` diff --git a/tests/engine/tools/stock-photo/apply.test.ts b/tests/engine/tools/stock-photo/apply.test.ts index 4260ea0df..e4e3708ff 100644 --- a/tests/engine/tools/stock-photo/apply.test.ts +++ b/tests/engine/tools/stock-photo/apply.test.ts @@ -51,7 +51,6 @@ const AREA_TARGET_TYPES = [ 'ELLIPSE', 'STAR', 'POLYGON', - 'VECTOR', 'COMPONENT', 'INSTANCE' ] as const satisfies readonly NodeType[] @@ -105,6 +104,69 @@ describe('applyPhoto', () => { }) } + test('applies a photo to closed vector geometry', async () => { + const { graph, page, figma } = setup() + const vector = graph.createNode('VECTOR', page.id, { + name: 'Closed vector', + width: 100, + height: 100, + vectorNetwork: { + vertices: [ + { x: 0, y: 0 }, + { x: 100, y: 0 }, + { x: 100, y: 100 }, + { x: 0, y: 100 } + ], + segments: [ + { start: 0, end: 1, tangentStart: { x: 0, y: 0 }, tangentEnd: { x: 0, y: 0 } }, + { start: 1, end: 2, tangentStart: { x: 0, y: 0 }, tangentEnd: { x: 0, y: 0 } }, + { start: 2, end: 3, tangentStart: { x: 0, y: 0 }, tangentEnd: { x: 0, y: 0 } }, + { start: 3, end: 0, tangentStart: { x: 0, y: 0 }, tangentEnd: { x: 0, y: 0 } } + ], + regions: [{ windingRule: 'NONZERO', loops: [[0, 1, 2, 3]] }] + } + }) + const calls: ProviderCall[] = [] + + const result = await applyPhoto(figma, createProvider(calls), { + id: vector.id, + query: 'marble texture' + }) + + expect(result.error).toBeUndefined() + expect(calls).toHaveLength(1) + expect(vector.fills[0]).toMatchObject({ type: 'IMAGE', imageScaleMode: 'FILL' }) + }) + + test('rejects open vector geometry before searching', async () => { + const { graph, page, figma } = setup() + const vector = graph.createNode('VECTOR', page.id, { + name: 'Open vector', + vectorNetwork: { + vertices: [ + { x: 0, y: 0 }, + { x: 100, y: 100 } + ], + segments: [{ start: 0, end: 1, tangentStart: { x: 0, y: 0 }, tangentEnd: { x: 0, y: 0 } }], + regions: [] + } + }) + const originalFills = copyFills(vector.fills) + const calls: ProviderCall[] = [] + + const result = await applyPhoto(figma, createProvider(calls), { + id: vector.id, + query: 'should not run' + }) + + expect(result.error).toBe( + `"${vector.name}" has no closed vector regions — use closed area geometry` + ) + expect(calls).toHaveLength(0) + expect(vector.fills).toEqual(originalFills) + expect(graph.images.size).toBe(0) + }) + test('applies a photo to Boolean geometry with operand children', async () => { const { graph, page, figma } = setup() const operation = graph.createNode('BOOLEAN_OPERATION', page.id, {