From 2d011580f0377ecc5253c2b9976b58b21832416a Mon Sep 17 00:00:00 2001 From: Danila Poyarkov Date: Mon, 18 May 2026 02:44:11 +0300 Subject: [PATCH] fix(fig): scale instance stroke weights --- .../kiwi/instance-overrides/constraints.ts | 16 ++++++ .../derived-symbol-data/layout.ts | 11 +++- .../core/src/kiwi/instance-overrides/types.ts | 1 + .../import/scaled-instance-strokes.test.ts | 56 +++++++++++++++++++ 4 files changed, 82 insertions(+), 2 deletions(-) create mode 100644 tests/engine/io/fig/import/scaled-instance-strokes.test.ts diff --git a/packages/core/src/kiwi/instance-overrides/constraints.ts b/packages/core/src/kiwi/instance-overrides/constraints.ts index 7b8ac2042..689788498 100644 --- a/packages/core/src/kiwi/instance-overrides/constraints.ts +++ b/packages/core/src/kiwi/instance-overrides/constraints.ts @@ -91,6 +91,15 @@ function scaleVectorNetwork( } } +function scaledStrokes(source: SceneNode, child: SceneNode, shapeScaleX: number, shapeScaleY: number) { + if (source.strokes.length !== child.strokes.length) return undefined + if (Math.abs(shapeScaleX - shapeScaleY) >= 0.001) return undefined + return child.strokes.map((stroke, strokeIndex) => ({ + ...stroke, + weight: source.strokes[strokeIndex].weight + })) +} + function scaleChildren( graph: SceneGraph, instance: SceneNode, @@ -131,6 +140,7 @@ function scaleChildren( if (source.vectorNetwork) { updates.vectorNetwork = scaleVectorNetwork(source.vectorNetwork, shapeScaleX, shapeScaleY) } + updates.strokes = scaledStrokes(source, child, shapeScaleX, shapeScaleY) graph.updateNode(child.id, updates) scaled.add(child.id) @@ -202,6 +212,12 @@ function propagateScaling(ctx: OverrideContext, scaled: Set): void { cu.strokeGeometry = copyGeometryPaths(source.strokeGeometry) if (source.vectorNetwork) cu.vectorNetwork = structuredClone(source.vectorNetwork) } + if (source.strokes.length === clone.strokes.length) { + cu.strokes = clone.strokes.map((stroke, strokeIndex) => ({ + ...stroke, + weight: source.strokes[strokeIndex].weight + })) + } if (Object.keys(cu).length > 0) graph.updateNode(cloneId, cu) queue.push(cloneId) } diff --git a/packages/core/src/kiwi/instance-overrides/derived-symbol-data/layout.ts b/packages/core/src/kiwi/instance-overrides/derived-symbol-data/layout.ts index b388db816..ff95f2575 100644 --- a/packages/core/src/kiwi/instance-overrides/derived-symbol-data/layout.ts +++ b/packages/core/src/kiwi/instance-overrides/derived-symbol-data/layout.ts @@ -27,11 +27,18 @@ function resolveSizeOnlyPosition( return withinParent ? { x: source.x, y: source.y } : { x: 0, y: 0 } } -function buildDsdTextUpdates(d: DerivedSymbolOverride, blobs: Uint8Array[]): Partial { +function buildDsdTextUpdates( + d: DerivedSymbolOverride, + blobs: Uint8Array[], + target: SceneNode +): Partial { const updates: Partial = {} if (d.fontSize !== undefined) updates.fontSize = d.fontSize if (d.lineHeight !== undefined) updates.lineHeight = convertLineHeight(d.lineHeight, d.fontSize) if (d.letterSpacing !== undefined) updates.letterSpacing = convertLetterSpacing(d.letterSpacing, d.fontSize) + if (d.strokeWeight !== undefined && target.strokes.length > 0) { + updates.strokes = target.strokes.map((stroke) => ({ ...stroke, weight: d.strokeWeight as number })) + } const figmaDerivedTextGlyphs = convertFigmaDerivedTextGlyphs(d.derivedTextData, blobs) if (figmaDerivedTextGlyphs.length > 0) updates.figmaDerivedTextGlyphs = figmaDerivedTextGlyphs return updates @@ -43,7 +50,7 @@ export function buildDsdLayoutUpdates( d: DerivedSymbolOverride, target: SceneNode ): { updates: Partial; hasSize: boolean } { - const updates: Partial = buildDsdTextUpdates(d, ctx.blobs) + const updates: Partial = buildDsdTextUpdates(d, ctx.blobs, target) const figmaDerivedLayout: NonNullable = {} if (d.size) { diff --git a/packages/core/src/kiwi/instance-overrides/types.ts b/packages/core/src/kiwi/instance-overrides/types.ts index c972ce322..8638a1d01 100644 --- a/packages/core/src/kiwi/instance-overrides/types.ts +++ b/packages/core/src/kiwi/instance-overrides/types.ts @@ -50,6 +50,7 @@ export interface DerivedSymbolOverride { fontSize?: number lineHeight?: NodeChange['lineHeight'] letterSpacing?: NodeChange['letterSpacing'] + strokeWeight?: number derivedTextData?: NodeChange['derivedTextData'] fillGeometry?: Array<{ windingRule?: string; commandsBlob?: number }> strokeGeometry?: Array<{ windingRule?: string; commandsBlob?: number }> diff --git a/tests/engine/io/fig/import/scaled-instance-strokes.test.ts b/tests/engine/io/fig/import/scaled-instance-strokes.test.ts new file mode 100644 index 000000000..76fd68c8f --- /dev/null +++ b/tests/engine/io/fig/import/scaled-instance-strokes.test.ts @@ -0,0 +1,56 @@ +import { describe, expect, test } from 'bun:test' + +import { importNodeChanges } from '@open-pencil/core' +import type { NodeChange } from '#core/kiwi/binary/codec' + +import { canvas, doc, node } from './legacy/helpers' + +describe('fig import scaled instance strokes', () => { + test('preserves vector stroke weight while scaling icon geometry', () => { + const componentGuid = { sessionID: 1, localID: 10 } + const vectorGuid = { sessionID: 1, localID: 11 } + const instanceGuid = { sessionID: 1, localID: 20 } + + const graph = importNodeChanges( + [ + doc(), + canvas(), + node('SYMBOL', 10, 1, { + guid: componentGuid, + size: { x: 24, y: 24 } + } as Partial), + node('VECTOR', 11, 1, { + guid: vectorGuid, + parentIndex: { guid: componentGuid, position: '!' }, + size: { x: 12, y: 12 }, + horizontalConstraint: 'SCALE', + verticalConstraint: 'SCALE', + strokeWeight: 2, + strokePaints: [ + { + type: 'SOLID', + color: { r: 0.2, g: 0.25, b: 0.33, a: 1 }, + opacity: 1, + visible: true, + blendMode: 'NORMAL' + } + ] + } as Partial), + node('INSTANCE', 20, 1, { + guid: instanceGuid, + size: { x: 16, y: 16 }, + symbolData: { symbolID: componentGuid } + } as Partial) + ], + [], + undefined, + { populate: 'all' } + ) + + const instance = Array.from(graph.getAllNodes()).find((sceneNode) => sceneNode.name === 'INSTANCE_20') + const vector = instance?.childIds.map((id) => graph.getNode(id)).find(Boolean) + + expect(vector?.strokes[0]?.weight).toBe(2) + expect(vector?.strokes[0]?.color).toEqual({ r: 0.2, g: 0.25, b: 0.33, a: 1 }) + }) +})