fix(canvas): support stretched layout grids

This commit is contained in:
Danila Poyarkov 2026-05-28 05:39:40 +03:00
parent 64e8b21949
commit 5fcd8e0e9c
5 changed files with 53 additions and 8 deletions

View file

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

View file

@ -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

View file

@ -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',

View file

@ -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')

View file

@ -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()