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) + }) +})