From 5139836816632e761bac68cbbd5d9b176516e9a6 Mon Sep 17 00:00:00 2001 From: Shai Rubinstein Date: Mon, 16 Mar 2026 20:15:02 +0200 Subject: [PATCH] fix: default TEXT nodes to solid black fill TEXT nodes created via SceneGraph.createNode() defaulted to empty fills, causing text to be invisible when .fig files are opened in Figma. Now defaults to a solid black fill matching Figma's behavior. Closes #133 --- CHANGELOG.md | 1 + packages/core/src/scene-graph.ts | 4 +- tests/engine/font-normalize-export.test.ts | 67 ++++++++++++++++++++++ 3 files changed, 71 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ad4031375..21cba7547 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,7 @@ - Fix browser/headless rendering mismatch for imported toolbar/instance content by correcting runtime imported layout recomputation instead of diverging browser rendering behavior - Fix `set_layout` tool not defaulting to HUG sizing when enabling auto-layout — frames now shrink/grow to fit children instead of keeping fixed dimensions - Normalize font family names on `.fig` export — strip optical size suffixes (e.g. "DM Sans 9pt" → "DM Sans") so Figma recognizes the font +- TEXT nodes now default to a solid black fill — previously exported with no fill, making text invisible when opened in Figma - Fix save crash when COLOR variable is missing alpha channel - Fix console error spam on deployed web app from automation WebSocket reconnect loop - Fix headless CLI font fallback — bundled Inter font now ships with `@open-pencil/core` and loads without a web server diff --git a/packages/core/src/scene-graph.ts b/packages/core/src/scene-graph.ts index 4c2f4bb79..fa0fc1bb5 100644 --- a/packages/core/src/scene-graph.ts +++ b/packages/core/src/scene-graph.ts @@ -371,7 +371,9 @@ function createDefaultNode(type: NodeType, overrides: Partial = {}): width: 100, height: 100, rotation: 0, - fills: [], + fills: type === 'TEXT' + ? [{ type: 'SOLID' as const, color: { r: 0, g: 0, b: 0, a: 1 }, opacity: 1, visible: true }] + : [], strokes: [], effects: [], opacity: 1, diff --git a/tests/engine/font-normalize-export.test.ts b/tests/engine/font-normalize-export.test.ts index d222bc7e1..b2fc76f58 100644 --- a/tests/engine/font-normalize-export.test.ts +++ b/tests/engine/font-normalize-export.test.ts @@ -82,3 +82,70 @@ describe('Font family normalization on .fig export', () => { expect(textNode.fontFamily).toBe('Roboto') }) }) + +describe('TEXT node default fill', () => { + test('TEXT node without explicit fills exports with solid black fill', async () => { + const graph = new SceneGraph() + graph.createNode('TEXT', pageId(graph), { + name: 'No Fill Specified', + x: 0, + y: 0, + width: 100, + height: 20, + text: 'Hello', + fontFamily: 'Inter', + fontWeight: 400, + fontSize: 14, + }) + + const exported = await exportFigFile(graph) + const reimported = await parseFigFile(exported.buffer as ArrayBuffer) + + const nodes = [...reimported.nodes.values()] + const textNode = nodes.find((n) => n.type === 'TEXT')! + expect(textNode.fills.length).toBe(1) + expect(textNode.fills[0].type).toBe('SOLID') + expect(textNode.fills[0].color.r).toBe(0) + expect(textNode.fills[0].color.g).toBe(0) + expect(textNode.fills[0].color.b).toBe(0) + expect(textNode.fills[0].color.a).toBe(1) + }) + + test('TEXT node with explicit fill preserves it', async () => { + const graph = new SceneGraph() + graph.createNode('TEXT', pageId(graph), { + name: 'Gold Text', + x: 0, + y: 0, + width: 100, + height: 20, + text: 'Hello', + fontFamily: 'Inter', + fontWeight: 400, + fontSize: 14, + fills: [{ type: 'SOLID', color: { r: 0.96, g: 0.72, b: 0, a: 1 }, opacity: 1, visible: true }], + }) + + const exported = await exportFigFile(graph) + const reimported = await parseFigFile(exported.buffer as ArrayBuffer) + + const nodes = [...reimported.nodes.values()] + const textNode = nodes.find((n) => n.type === 'TEXT')! + expect(textNode.fills.length).toBe(1) + expect(textNode.fills[0].color.r).toBeCloseTo(0.96, 1) + expect(textNode.fills[0].color.g).toBeCloseTo(0.72, 1) + expect(textNode.fills[0].color.b).toBeCloseTo(0, 1) + }) + + test('non-TEXT node still defaults to empty fills', async () => { + const graph = new SceneGraph() + const node = graph.createNode('RECTANGLE', pageId(graph), { + name: 'Rect', + x: 0, + y: 0, + width: 100, + height: 100, + }) + expect(graph.getNode(node.id)!.fills.length).toBe(0) + }) +})