From dec0a67e4fa77470be7a7d0b26694f31f0c6187f Mon Sep 17 00:00:00 2001 From: Anton A S Date: Wed, 11 Mar 2026 16:46:01 +0300 Subject: [PATCH] Batch insert_icon and search_icons, fetch_icons prefetch tool --- packages/core/src/layout.ts | 3 +- packages/core/src/tools/create.ts | 144 +++++++++++++++++------------- src/ai/system-prompt.md | 15 ++-- 3 files changed, 91 insertions(+), 71 deletions(-) diff --git a/packages/core/src/layout.ts b/packages/core/src/layout.ts index 7a626654d..af1eeef09 100644 --- a/packages/core/src/layout.ts +++ b/packages/core/src/layout.ts @@ -388,9 +388,10 @@ function configureChildAsLeaf(yogaChild: YogaNode, child: SceneNode, parent: Sce function configureTextLeaf( yogaChild: YogaNode, child: SceneNode, - _parent: SceneNode + parent: SceneNode ): void { const autoResize = child.textAutoResize + const isRow = parent.layoutMode === 'HORIZONTAL' if (child.layoutGrow > 0) { yogaChild.setFlexGrow(child.layoutGrow) diff --git a/packages/core/src/tools/create.ts b/packages/core/src/tools/create.ts index 0acea1690..c47ed376d 100644 --- a/packages/core/src/tools/create.ts +++ b/packages/core/src/tools/create.ts @@ -1,10 +1,11 @@ import { parseColor } from '../color' -import { fetchIcon, fetchIcons, searchIconsBatch } from '../iconify' +import { fetchIcons, searchIconsBatch } from '../iconify' import type { SceneNode } from '../scene-graph' import { defineTool, nodeSummary } from './schema' -import type { FigmaNodeProxy } from '../figma-api' +import type { FigmaAPI, FigmaNodeProxy } from '../figma-api' +import type { Color } from '../types' import type { Stroke, StrokeCap, StrokeJoin } from '../scene-graph' export const createShape = defineTool({ @@ -229,89 +230,108 @@ export const fetchIconsTool = defineTool({ } }) +import type { IconData } from '../iconify' + +function createIconFrame( + figma: FigmaAPI, + icon: IconData, + name: string, + size: number, + parsedColor: Color, + parentId?: string +): FigmaNodeProxy { + const frame = figma.createFrame() + frame.name = `Icon / ${name}` + frame.resize(size, size) + frame.fills = [] + + if (parentId) { + const parent = figma.getNodeById(parentId) + if (parent) parent.appendChild(frame) + } + + for (const path of icon.paths) { + const vector = figma.createVector() + vector.name = 'path' + vector.resize(size, size) + frame.appendChild(vector) + + figma.graph.updateNode(vector.id, { + vectorNetwork: path.vectorNetwork + } as Record) + + if (path.fill) { + vector.fills = [{ + type: 'SOLID', + color: path.fill === 'currentColor' ? parsedColor : parseColor(path.fill), + opacity: 1, + visible: true + }] + } else { + vector.fills = [] + } + + if (path.stroke) { + const strokeColor = path.stroke === 'currentColor' ? parsedColor : parseColor(path.stroke) + const stroke: Stroke = { + color: strokeColor, + weight: path.strokeWidth, + opacity: 1, + visible: true, + align: 'CENTER', + cap: STROKE_CAP_MAP[path.strokeCap] ?? 'NONE', + join: STROKE_JOIN_MAP[path.strokeJoin] ?? 'MITER' + } + vector.strokes = [stroke] + } + } + + return frame +} + export const insertIcon = defineTool({ name: 'insert_icon', mutates: true, description: - 'Insert a vector icon onto the canvas. If already cached by fetch_icons — instant, no network request. Otherwise fetches on demand.', + 'Insert one or more vector icons onto the canvas. Pass a single name or multiple names to batch-insert into the same parent. If already cached by fetch_icons — instant, no network request.', params: { - name: { - type: 'string', - description: 'Icon name as prefix:name (e.g. lucide:heart, mdi:home)', + names: { + type: 'string[]', + description: 'Icon names as prefix:name (e.g. ["lucide:heart"] or ["lucide:heart","lucide:home","lucide:star"])', required: true }, size: { type: 'number', description: 'Icon size in pixels (default: 24)' }, color: { type: 'color', description: 'Icon color hex (replaces currentColor). Default: #000000' }, - x: { type: 'number', description: 'X position' }, - y: { type: 'number', description: 'Y position' }, - parent_id: { type: 'string', description: 'Parent node ID' } + parent_id: { type: 'string', description: 'Parent node ID for all icons' } }, execute: async (figma, args) => { const size = args.size ?? 24 const color = args.color ?? '#000000' + const parsedColor = parseColor(color) - let icon + let icons try { - icon = await fetchIcon(args.name, size) + icons = await fetchIcons(args.names, size) } catch (e) { return { error: (e as Error).message } } - if (icon.paths.length === 0) { - return { error: `Icon "${args.name}" has no path data` } - } + const inserted: { id: string; name: string; icon: string }[] = [] + const notFound: string[] = [] - const frame = figma.createFrame() - frame.name = `Icon / ${args.name}` - frame.resize(size, size) - frame.fills = [] - if (args.x !== undefined) frame.x = args.x - if (args.y !== undefined) frame.y = args.y - - if (args.parent_id) { - const parent = figma.getNodeById(args.parent_id) - if (parent) parent.appendChild(frame) - } - - const parsedColor = parseColor(color) - - for (const path of icon.paths) { - const vector = figma.createVector() - vector.name = 'path' - vector.resize(size, size) - frame.appendChild(vector) - - figma.graph.updateNode(vector.id, { - vectorNetwork: path.vectorNetwork - } as Record) - - if (path.fill) { - vector.fills = [{ - type: 'SOLID', - color: path.fill === 'currentColor' ? parsedColor : parseColor(path.fill), - opacity: 1, - visible: true - }] - } else { - vector.fills = [] - } - - if (path.stroke) { - const strokeColor = path.stroke === 'currentColor' ? parsedColor : parseColor(path.stroke) - const stroke: Stroke = { - color: strokeColor, - weight: path.strokeWidth, - opacity: 1, - visible: true, - align: 'CENTER', - cap: STROKE_CAP_MAP[path.strokeCap] ?? 'NONE', - join: STROKE_JOIN_MAP[path.strokeJoin] ?? 'MITER' - } - vector.strokes = [stroke] + for (const name of args.names) { + const icon = icons.get(name) + if (!icon || icon.paths.length === 0) { + notFound.push(name) + continue } + const frame = createIconFrame(figma, icon, name, size, parsedColor, args.parent_id) + inserted.push({ id: frame.id, name: frame.name, icon: name }) } - return nodeSummary(frame) + const result: Record = { inserted } + if (notFound.length > 0) result.not_found = notFound + return result } }) diff --git a/src/ai/system-prompt.md b/src/ai/system-prompt.md index 6638ca8f4..ebb669e32 100644 --- a/src/ai/system-prompt.md +++ b/src/ai/system-prompt.md @@ -189,21 +189,20 @@ Never place vertical dividers outside their row container. Horizontal dividers ( ## Icons -Three tools for Iconify icons (200k+ icons): +Three tools for Iconify icons (200k+ icons). All batch-friendly — minimize LLM steps: - `search_icons` — find icons by keyword. Pass multiple queries at once: `queries=["heart","arrow","settings"]`. -- `fetch_icons` — pre-download icons into cache. Batches by prefix (one HTTP request per set). Pass all names you'll need: `names=["lucide:heart","lucide:home","lucide:star"]`. -- `insert_icon` — place one icon on the canvas. Instant if already cached by `fetch_icons`. +- `fetch_icons` — pre-download icons into cache. Batches by prefix (one HTTP request per set). Pass all names you'll need. +- `insert_icon` — place icons on the canvas. Pass one or many `names`. When icons share the same parent/color/size, batch them: `names=["lucide:heart","lucide:home","lucide:star"]`. Instant if cached by `fetch_icons`. -**Workflow:** search → fetch batch → insert one by one as you build. +**Workflow:** search → fetch → insert. Each step handles multiple icons in one call. ``` 1. search_icons(queries=["heart","home","settings","star"]) 2. fetch_icons(names=["lucide:heart","lucide:home","lucide:settings","lucide:star"], size=20) -3. render skeleton... -4. insert_icon(name="lucide:heart", parent_id=..., color="#FFF") -5. insert_icon(name="lucide:home", parent_id=..., color="#FFF") - ... (each insert_icon is instant — no network wait) +3. render skeleton with empty containers... +4. insert_icon(names=["lucide:heart","lucide:home","lucide:settings"], parent_id=navId, color="#FFF") +5. insert_icon(names=["lucide:star"], parent_id=footerId, color="#999") ``` **Popular icon sets:**