From 61a86ad09d913c5920a0a363ffde65f8f90510f1 Mon Sep 17 00:00:00 2001 From: Julio Servan Date: Fri, 14 Aug 2026 22:15:47 +0000 Subject: [PATCH] feat: add set_solid_fills for multi-node paint updates MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Theming a component's variants currently costs one WebSocket round-trip per node, which does not scale past a handful of layers. set_solid_fills takes an `items` array — the same field name and per-item shape as set_node_visibility, including the fillHex/fillOpacity aliases from #39 — and applies them all in a single round-trip. Items are independent: a failing node is reported in its own result entry and does not abort the rest, so a partially-stale node list still does useful work. --- README.md | 2 ++ plugin/src/main/code.ts | 63 +++++++++++++++++++++++++++++++++++++++++ server/src/schema.ts | 57 +++++++++++++++++++++++++++++++++++++ server/src/tools.ts | 14 +++++++++ 4 files changed, 136 insertions(+) diff --git a/README.md b/README.md index 879f4e8..10b1b6b 100644 --- a/README.md +++ b/README.md @@ -87,6 +87,7 @@ If you want to know more about how it works, read the [How it works](#how-it-wor | `set_text_properties` | Patch font, size, alignment, auto-resize, color, and bounds on a text node | | `set_node_properties` | Patch common node properties: name, position, size, visibility, opacity, corner radius | | `set_solid_fill` | Replace a node's fill or stroke with a single solid paint | +| `set_solid_fills` | Replace fills or strokes on many nodes in a single round-trip | | `set_gradient_fill` | Replace a node's fill or stroke with a linear/radial/angular/diamond gradient | | `set_effects` | Replace a node's effects list (drop/inner shadows, layer/background blurs) | | `set_stroke_properties` | Patch stroke weight, align, dash pattern, cap, and join | @@ -118,6 +119,7 @@ All tools accept an optional `fileKey` parameter when multiple Figma files are c With the current write surface, an agent can build a basic slide deck in a new empty Figma file: create slide frames, style titles and body copy, lay out rectangles/ellipses/lines for cards and dividers, duplicate slide templates, reparent content into the right frame, and adjust common geometry/visual properties — including solid/gradient paints, shadows and blurs, stroke geometry, and auto-layout configuration. + The current version is intentionally limited — no components/instances, no variables/styles authoring, no per-segment text styling, and no vector boolean operations yet. ## Local development diff --git a/plugin/src/main/code.ts b/plugin/src/main/code.ts index 896e85d..76ae6a4 100644 --- a/plugin/src/main/code.ts +++ b/plugin/src/main/code.ts @@ -14,6 +14,7 @@ type RequestType = | "set_text_properties" | "set_node_properties" | "set_solid_fill" + | "set_solid_fills" | "set_gradient_fill" | "set_effects" | "set_stroke_properties" @@ -221,6 +222,13 @@ const setSolidFill = ( (node as GeometryMixin & { fills: ReadonlyArray }).fills = [paint]; }; + + + + + + + type GradientStopInput = { position: number; hex: string; opacity?: number }; type GradientPaintType = | "GRADIENT_LINEAR" @@ -350,6 +358,7 @@ const EDIT_REQUEST_TYPES = new Set([ "set_text_properties", "set_node_properties", "set_solid_fill", + "set_solid_fills", "set_gradient_fill", "set_effects", "set_stroke_properties", @@ -933,6 +942,60 @@ const handleRequest = async ( }, }; } + case "set_solid_fills": { + const rawItems = request.params?.items; + if (!Array.isArray(rawItems) || rawItems.length === 0) { + throw new Error("items is required for set_solid_fills"); + } + const items = rawItems as Array>; + const results: Array< + | { nodeId: string; target: "fill" | "stroke" } + | { nodeId: string | null; error: string } + > = []; + + // Sequential on purpose: these all mutate the document, and the + // round-trip being saved is the WebSocket one, not the local work. + for (const item of items) { + const nodeId = typeof item.nodeId === "string" ? item.nodeId : null; + try { + if (!nodeId) { + throw new Error("nodeId is required"); + } + // fillHex/fillOpacity are accepted here too, so a caller can lift a + // set_solid_fill call into items without renaming its fields (#39). + const hex = typeof item.hex === "string" ? item.hex : item.fillHex; + if (typeof hex !== "string") { + throw new Error( + "hex is required (fillHex is accepted as an alias)" + ); + } + const rawOpacity = + typeof item.opacity === "number" + ? item.opacity + : item.fillOpacity; + const node = await getSceneNodeById(nodeId); + const target = item.target === "stroke" ? "stroke" : "fill"; + setSolidFill( + node, + hex, + typeof rawOpacity === "number" ? rawOpacity : undefined, + target + ); + results.push({ nodeId, target }); + } catch (error) { + results.push({ + nodeId, + error: error instanceof Error ? error.message : String(error), + }); + } + } + + return { + type: request.type, + requestId: request.requestId, + data: { results }, + }; + } case "set_gradient_fill": { const nodeId = request.nodeIds && request.nodeIds[0]; if (!nodeId) { diff --git a/server/src/schema.ts b/server/src/schema.ts index 7bf07c7..eaf23ac 100644 --- a/server/src/schema.ts +++ b/server/src/schema.ts @@ -179,6 +179,47 @@ export const setSolidFillInput = setSolidFillShape.transform( } ); +/** + * One node's worth of work for `set_solid_fills`. Mirrors `set_solid_fill` + * field for field, including the fillHex/fillOpacity aliases, so a caller can + * lift a single-node call straight into `items` without renaming anything. + */ +const createSolidFillItemSchema = () => + z + .object({ + nodeId: createFigmaNodeIdSchema().describe("The node ID to update"), + hex: createHexColorSchema() + .optional() + .describe("Solid color as hex (e.g. '#FFAA00')"), + fillHex: createHexColorSchema().optional().describe("Alias for hex"), + opacity: z + .number() + .min(0) + .max(1) + .optional() + .describe("Optional paint opacity from 0 to 1 (default 1)"), + fillOpacity: z + .number() + .min(0) + .max(1) + .optional() + .describe("Alias for opacity"), + target: solidFillTarget, + }) + .refine((item) => item.hex !== undefined || item.fillHex !== undefined, { + message: "hex is required (fillHex is accepted as an alias)", + }); + +export const setSolidFillsInput = z.object({ + items: z + .array(createSolidFillItemSchema()) + .min(1) + .describe( + "Fill/stroke updates, one entry per node, applied in a single round-trip" + ), + fileKey: fileKeyField, +}); + const blendMode = z.enum([ "PASS_THROUGH", "NORMAL", @@ -720,6 +761,21 @@ export const toolInputSchemas = { set_solid_fill: setSolidFillInput, + set_solid_fills: setSolidFillsInput, + + + + + + + + + + + + + + set_effects: setEffectsInput, set_stroke_properties: setStrokePropertiesInput.refine( @@ -959,6 +1015,7 @@ const rpcToArgs: Record< }), set_gradient_fill: (nodeIds, params) => ({ ...params, nodeId: nodeIds?.[0] }), set_solid_fill: (nodeIds, params) => ({ ...params, nodeId: nodeIds?.[0] }), + set_solid_fills: (_nodeIds, params) => ({ ...params }), set_effects: (nodeIds, params) => ({ ...params, nodeId: nodeIds?.[0] }), set_stroke_properties: (nodeIds, params) => ({ ...params, diff --git a/server/src/tools.ts b/server/src/tools.ts index 54e1be8..9099c6b 100644 --- a/server/src/tools.ts +++ b/server/src/tools.ts @@ -294,6 +294,20 @@ export function registerTools( } ); + server.tool( + "set_solid_fills", + "Replace the fill (or stroke) of many nodes with solid paints in a single round-trip. Each item takes the same fields as set_solid_fill. Items are independent: a bad nodeId fails only its own entry and the rest still apply. When multiple files are connected, specify fileKey.", + toolInputSchemas.set_solid_fills.shape, + async (args): Promise => { + const parsed = parseToolInput(toolInputSchemas.set_solid_fills, args); + if (!parsed.success) return parsed.error; + const { items, fileKey } = parsed.data; + return renderResponse(() => + node.sendWithParams("set_solid_fills", undefined, { items }, fileKey) + ); + } + ); + server.tool( "set_gradient_fill", "Replace a node's fill (or stroke) with a gradient paint. Provide ordered stops (position 0..1, hex color, optional alpha) and an optional 2x3 gradientTransform matching Figma's gradientTransform format. Useful for setting linear/radial/angular/diamond gradients programmatically.",