fix(fig): preserve auto-layout child transforms

- Stop zeroing transforms for auto-layout children during .fig export
- Match real Figma files, which keep child transform offsets alongside layout data
- Update transform regression coverage to prevent collapsed roundtrips
This commit is contained in:
Danila Poyarkov 2026-05-20 18:50:00 +03:00
parent 4fe2b75dc8
commit 1c655f34ce
3 changed files with 11 additions and 20 deletions

View file

@ -23,7 +23,7 @@ interface SceneNodeToKiwiContext {
mapToFigmaType: (type: SceneNode['type']) => string
fillToKiwiPaint: (fill: SceneNode['fills'][number]) => Paint
safeColor: (color: Color) => Color
computeExportTransform: (node: SceneNode, graph: SceneGraph) => Matrix
computeExportTransform: (node: SceneNode) => Matrix
serializeCornerRadii: (node: SceneNode, nc: KiwiNodeChange) => void
serializeTextProps: (
node: SceneNode,
@ -250,7 +250,7 @@ export function sceneNodeToKiwiWithContext(
opacity: node.opacity,
phase: 'CREATED',
size: { x: node.width, y: node.height },
transform: context.computeExportTransform(node, context.graph),
transform: context.computeExportTransform(node),
strokeWeight: node.strokes[0]?.weight ?? DEFAULT_STROKE_WEIGHT,
strokeAlign: node.strokes[0]?.align ?? 'INSIDE'
}

View file

@ -410,20 +410,11 @@ function serializeVariableBindings(
if (entries.length > 0) nc.variableConsumptionMap = { entries }
}
function computeExportTransform(node: SceneNode, graph: SceneGraph): Matrix {
function computeExportTransform(node: SceneNode): Matrix {
const sx = node.flipX ? -1 : 1
const cos = Math.cos((node.rotation * Math.PI) / 180)
const sin = Math.sin((node.rotation * Math.PI) / 180)
// Auto-layout children should have (0,0) transform — Figma computes
// their positions from the layout engine at render time.
const parent = node.parentId ? graph.getNode(node.parentId) : undefined
const isAutoLayoutChild =
parent &&
parent.layoutMode !== 'NONE' &&
parent.layoutMode !== 'GRID' &&
node.layoutPositioning !== 'ABSOLUTE'
const m00 = cos * sx
const m01 = -sin
const m10 = sin * sx
@ -443,10 +434,10 @@ function computeExportTransform(node: SceneNode, graph: SceneGraph): Matrix {
return {
m00,
m01,
m02: isAutoLayoutChild ? 0 : node.x - offsetX,
m02: node.x - offsetX,
m10,
m11,
m12: isAutoLayoutChild ? 0 : node.y - offsetY
m12: node.y - offsetY
}
}

View file

@ -7,7 +7,7 @@ import { expectDefined, getNodeOrThrow } from '#tests/helpers/assert'
import { ROOT_GUID, pageId } from '../helpers'
describe('Fix 1: auto-layout child transforms', () => {
test('auto-layout child gets zero transform regardless of its x/y', () => {
test('auto-layout child preserves its stored transform offsets', () => {
const graph = new SceneGraph()
const parent = graph.createNode('FRAME', pageId(graph), {
name: 'AutoLayout',
@ -48,8 +48,8 @@ describe('Fix 1: auto-layout child transforms', () => {
'child node change'
)
expect(childNc).toBeDefined()
expect(childNc.transform.m02).toBe(0)
expect(childNc.transform.m12).toBe(0)
expect(childNc.transform.m02).toBe(50)
expect(childNc.transform.m12).toBe(100)
})
test('absolute-positioned child inside auto-layout keeps its real x/y', () => {
@ -130,7 +130,7 @@ describe('Fix 1: auto-layout child transforms', () => {
expect(childNc.transform.m12).toBe(45)
})
test('horizontal auto-layout child also gets zero transform', () => {
test('horizontal auto-layout child also preserves its stored transform offsets', () => {
const graph = new SceneGraph()
const parent = graph.createNode('FRAME', pageId(graph), {
name: 'HorizontalLayout',
@ -164,8 +164,8 @@ describe('Fix 1: auto-layout child transforms', () => {
changes.find((nc) => nc.name === 'Item'),
'item node change'
)
expect(itemNc.transform.m02).toBe(0)
expect(itemNc.transform.m12).toBe(0)
expect(itemNc.transform.m02).toBe(200)
expect(itemNc.transform.m12).toBe(50)
})
})