From 5fcd8e0e9cb4b129e997f03cd96d58fd1389d97d Mon Sep 17 00:00:00 2001 From: Danila Poyarkov Date: Thu, 28 May 2026 05:39:40 +0300 Subject: [PATCH] fix(canvas): support stretched layout grids --- packages/core/src/canvas/layout-grids.ts | 26 ++++++++++++++----- packages/core/src/io/formats/fig/export.ts | 3 +++ .../io/fig/import/raw-field-coverage.test.ts | 2 +- .../io/fig/roundtrip/source-metadata.test.ts | 5 ++++ .../engine/render/canvas/layout-grids.test.ts | 25 ++++++++++++++++++ 5 files changed, 53 insertions(+), 8 deletions(-) diff --git a/packages/core/src/canvas/layout-grids.ts b/packages/core/src/canvas/layout-grids.ts index 2082ab516..47288be97 100644 --- a/packages/core/src/canvas/layout-grids.ts +++ b/packages/core/src/canvas/layout-grids.ts @@ -54,10 +54,12 @@ function gridGeometry(grid: RawLayoutGrid): GridGeometry | null { if (grid.visible === false) return null const count = grid.count ?? grid.numSections ?? 1 const sectionSize = grid.sectionSize ?? 0 - if (count <= 0 || sectionSize <= 0) return null + const alignment = rawGridAlignment(grid) + if (!Number.isFinite(count) || count <= 0) return null + if (alignment !== 'STRETCH' && sectionSize <= 0) return null return { pattern: rawGridPattern(grid), - alignment: rawGridAlignment(grid), + alignment, count, offset: grid.offset ?? 0, sectionSize, @@ -66,8 +68,14 @@ function gridGeometry(grid: RawLayoutGrid): GridGeometry | null { } } +function gridSectionSize(nodeSize: number, grid: GridGeometry): number { + if (grid.alignment !== 'STRETCH') return grid.sectionSize + return (nodeSize - grid.offset * 2 - Math.max(0, grid.count - 1) * grid.gutterSize) / grid.count +} + function gridStart(nodeSize: number, grid: GridGeometry): number { - const span = grid.count * grid.sectionSize + Math.max(0, grid.count - 1) * grid.gutterSize + const sectionSize = gridSectionSize(nodeSize, grid) + const span = grid.count * sectionSize + Math.max(0, grid.count - 1) * grid.gutterSize if (grid.alignment === 'CENTER') return (nodeSize - span) / 2 + grid.offset if (grid.alignment === 'MAX') return nodeSize - span - grid.offset return grid.offset @@ -79,18 +87,22 @@ function drawColumnGrid( node: SceneNode, grid: GridGeometry ): void { + const sectionSize = gridSectionSize(node.width, grid) + if (sectionSize <= 0) return const x0 = gridStart(node.width, grid) for (let index = 0; index < grid.count; index++) { - const x = x0 + index * (grid.sectionSize + grid.gutterSize) - canvas.drawRect(r.ck.LTRBRect(x, 0, x + grid.sectionSize, node.height), r.auxFill) + const x = x0 + index * (sectionSize + grid.gutterSize) + canvas.drawRect(r.ck.LTRBRect(x, 0, x + sectionSize, node.height), r.auxFill) } } function drawRowGrid(r: SkiaRenderer, canvas: Canvas, node: SceneNode, grid: GridGeometry): void { + const sectionSize = gridSectionSize(node.height, grid) + if (sectionSize <= 0) return const y0 = gridStart(node.height, grid) for (let index = 0; index < grid.count; index++) { - const y = y0 + index * (grid.sectionSize + grid.gutterSize) - canvas.drawRect(r.ck.LTRBRect(0, y, node.width, y + grid.sectionSize), r.auxFill) + const y = y0 + index * (sectionSize + grid.gutterSize) + canvas.drawRect(r.ck.LTRBRect(0, y, node.width, y + sectionSize), r.auxFill) } } diff --git a/packages/core/src/io/formats/fig/export.ts b/packages/core/src/io/formats/fig/export.ts index e3d038e36..79e28cfd0 100644 --- a/packages/core/src/io/formats/fig/export.ts +++ b/packages/core/src/io/formats/fig/export.ts @@ -216,6 +216,9 @@ function applyImportedCanvasFields(page: FigExportPage, canvasNc: KiwiNodeChange page.source.fig.rawNodeFields.backgroundPaints ) as NodeChange['backgroundPaints'] } + if ('guides' in page.source.fig.rawNodeFields) { + canvasNc.guides = structuredClone(page.source.fig.rawNodeFields.guides) + } const strokeJoin = page.source.fig.rawNodeFields.strokeJoin if (typeof strokeJoin === 'string') canvasNc.strokeJoin = strokeJoin const strokeWeight = page.source.fig.rawNodeFields.strokeWeight diff --git a/tests/engine/io/fig/import/raw-field-coverage.test.ts b/tests/engine/io/fig/import/raw-field-coverage.test.ts index 06547167f..84b92d5fa 100644 --- a/tests/engine/io/fig/import/raw-field-coverage.test.ts +++ b/tests/engine/io/fig/import/raw-field-coverage.test.ts @@ -20,6 +20,7 @@ const RAW_FIELD_COVERAGE = { 'fontVariantCommonLigatures', 'fontVariantContextualLigatures', 'fontVariations', + 'guides', 'layoutGrids', 'leadingTrim', 'letterSpacing', @@ -88,7 +89,6 @@ const RAW_FIELD_COVERAGE = { 'gridRowAnchor', 'gridRows', 'gridRowsSizing', - 'guides', 'handoffStatusMap', 'isPageDivider', 'isSoftDeleted', diff --git a/tests/engine/io/fig/roundtrip/source-metadata.test.ts b/tests/engine/io/fig/roundtrip/source-metadata.test.ts index ba7d9c432..baa6ea0b9 100644 --- a/tests/engine/io/fig/roundtrip/source-metadata.test.ts +++ b/tests/engine/io/fig/roundtrip/source-metadata.test.ts @@ -43,6 +43,10 @@ describe('fig roundtrip source metadata', () => { blendMode: 'NORMAL' } ] + page.source.fig.rawNodeFields.guides = [ + { axis: 'X', offset: 42 }, + { axis: 'Y', offset: 84 } + ] page.source.fig.rawNodeFields.strokeJoin = 'BEVEL' page.source.fig.rawNodeFields.strokeWeight = 0 @@ -72,6 +76,7 @@ describe('fig roundtrip source metadata', () => { expect(canvas?.strokeWeight).toBe(0) expect(canvas?.backgroundColor).toEqual(page.source.fig.rawNodeFields.backgroundColor) expect(canvas?.backgroundPaints).toEqual(page.source.fig.rawNodeFields.backgroundPaints) + expect(canvas?.guides).toEqual(page.source.fig.rawNodeFields.guides) expect(changes.get('4:4812')?.parentIndex?.position).toBe('~~~~~~~~~~1') expect(changes.get('4:4813')?.parentIndex?.position).toBe('~~~~~~~~~~3') diff --git a/tests/engine/render/canvas/layout-grids.test.ts b/tests/engine/render/canvas/layout-grids.test.ts index c657b0b3f..b10ef0389 100644 --- a/tests/engine/render/canvas/layout-grids.test.ts +++ b/tests/engine/render/canvas/layout-grids.test.ts @@ -44,6 +44,31 @@ describe('layout grid rendering', () => { expect(mockCalls(r.ck.LTRBRect)[3]).toEqual([208, 0, 256, 160]) }) + test('renders stretched column grids', () => { + const r = createMockRenderer() + const canvas = createMockCanvas() + const node = nodeWithLayoutGrids([ + { + pattern: 'COLUMNS', + visible: true, + color: { r: 1, g: 0, b: 0, a: 0.1 }, + gutterSize: 10, + alignment: 'STRETCH', + count: 3, + offset: 15 + } + ]) + + drawLayoutGrids(r, canvas as Canvas, node) + + expect(mockCalls(canvas.drawRect)).toHaveLength(3) + expect(mockCalls(r.ck.LTRBRect)).toEqual([ + [15, 0, 78.33333333333334, 160], + [88.33333333333334, 0, 151.66666666666669, 160], + [161.66666666666669, 0, 225.00000000000003, 160] + ]) + }) + test('renders Kiwi row grids', () => { const r = createMockRenderer() const canvas = createMockCanvas()