fix(canvas): render imported layout grids
This commit is contained in:
parent
711106518e
commit
42fcb37f4d
120
packages/core/src/canvas/layout-grids.ts
Normal file
120
packages/core/src/canvas/layout-grids.ts
Normal file
|
|
@ -0,0 +1,120 @@
|
|||
import type { Canvas } from 'canvaskit-wasm'
|
||||
|
||||
import { SELECTION_COLOR } from '#core/constants'
|
||||
import type { SceneNode } from '#core/scene-graph'
|
||||
import type { Color } from '#core/types'
|
||||
|
||||
import type { SkiaRenderer } from './renderer'
|
||||
|
||||
interface RawLayoutGrid {
|
||||
visible?: boolean
|
||||
color?: Color
|
||||
pattern?: string
|
||||
axis?: string
|
||||
type?: string
|
||||
alignment?: string
|
||||
numSections?: number
|
||||
count?: number
|
||||
offset?: number
|
||||
sectionSize?: number
|
||||
gutterSize?: number
|
||||
}
|
||||
|
||||
interface GridGeometry {
|
||||
pattern: 'COLUMNS' | 'ROWS' | 'GRID'
|
||||
alignment: 'MIN' | 'CENTER' | 'MAX' | 'STRETCH'
|
||||
count: number
|
||||
offset: number
|
||||
sectionSize: number
|
||||
gutterSize: number
|
||||
color: Color
|
||||
}
|
||||
|
||||
function rawLayoutGrids(node: SceneNode): RawLayoutGrid[] {
|
||||
const grids = node.source.fig.rawNodeFields.layoutGrids
|
||||
if (!Array.isArray(grids)) return []
|
||||
return grids.filter((grid): grid is RawLayoutGrid => grid !== null && typeof grid === 'object')
|
||||
}
|
||||
|
||||
function rawGridPattern(grid: RawLayoutGrid): GridGeometry['pattern'] {
|
||||
if (grid.pattern === 'GRID') return 'GRID'
|
||||
if (grid.pattern === 'ROWS') return 'ROWS'
|
||||
if (grid.pattern === 'COLUMNS') return 'COLUMNS'
|
||||
if (grid.axis === 'Y') return 'ROWS'
|
||||
return 'COLUMNS'
|
||||
}
|
||||
|
||||
function rawGridAlignment(grid: RawLayoutGrid): GridGeometry['alignment'] {
|
||||
const value = grid.alignment ?? grid.type
|
||||
if (value === 'CENTER' || value === 'MAX' || value === 'STRETCH') return value
|
||||
return 'MIN'
|
||||
}
|
||||
|
||||
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
|
||||
return {
|
||||
pattern: rawGridPattern(grid),
|
||||
alignment: rawGridAlignment(grid),
|
||||
count,
|
||||
offset: grid.offset ?? 0,
|
||||
sectionSize,
|
||||
gutterSize: grid.gutterSize ?? 0,
|
||||
color: grid.color ?? { ...SELECTION_COLOR, a: 0.1 }
|
||||
}
|
||||
}
|
||||
|
||||
function gridStart(nodeSize: number, grid: GridGeometry): number {
|
||||
const span = grid.count * grid.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
|
||||
}
|
||||
|
||||
function drawColumnGrid(
|
||||
r: SkiaRenderer,
|
||||
canvas: Canvas,
|
||||
node: SceneNode,
|
||||
grid: GridGeometry
|
||||
): void {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
function drawRowGrid(r: SkiaRenderer, canvas: Canvas, node: SceneNode, grid: GridGeometry): void {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
function drawSquareGrid(
|
||||
r: SkiaRenderer,
|
||||
canvas: Canvas,
|
||||
node: SceneNode,
|
||||
grid: GridGeometry
|
||||
): void {
|
||||
for (let x = grid.offset; x < node.width; x += grid.sectionSize) {
|
||||
canvas.drawRect(r.ck.LTRBRect(x, 0, x + 1, node.height), r.auxFill)
|
||||
}
|
||||
for (let y = grid.offset; y < node.height; y += grid.sectionSize) {
|
||||
canvas.drawRect(r.ck.LTRBRect(0, y, node.width, y + 1), r.auxFill)
|
||||
}
|
||||
}
|
||||
|
||||
export function drawLayoutGrids(r: SkiaRenderer, canvas: Canvas, node: SceneNode): void {
|
||||
for (const rawGrid of rawLayoutGrids(node)) {
|
||||
const grid = gridGeometry(rawGrid)
|
||||
if (!grid) continue
|
||||
r.auxFill.setColor(r.ck.Color4f(grid.color.r, grid.color.g, grid.color.b, grid.color.a))
|
||||
if (grid.pattern === 'GRID') drawSquareGrid(r, canvas, node, grid)
|
||||
else if (grid.pattern === 'ROWS') drawRowGrid(r, canvas, node, grid)
|
||||
else drawColumnGrid(r, canvas, node, grid)
|
||||
}
|
||||
}
|
||||
|
|
@ -9,6 +9,7 @@ import { vectorNetworkToCenterlinePath } from '#core/vector'
|
|||
|
||||
import { figmaBlendModeToSkia, needsIsolatedBlendLayer } from './blend'
|
||||
import { renderBooleanOperation } from './boolean'
|
||||
import { drawLayoutGrids } from './layout-grids'
|
||||
import { renderMaskedChildIds } from './masks'
|
||||
import type { SkiaRenderer, RenderOverlays } from './renderer'
|
||||
import { makeSmoothRRectPath, nodeHasRadius, nodeHasSmoothCorners } from './shapes'
|
||||
|
|
@ -250,6 +251,7 @@ export function renderNode(
|
|||
|
||||
applyNodeTransforms(r, canvas, node, nodeId, overlays)
|
||||
renderNodeContent(r, canvas, graph, node, nodeId, overlays)
|
||||
drawLayoutGrids(r, canvas, node)
|
||||
renderChildren(r, canvas, graph, node, overlays, absX, absY)
|
||||
|
||||
if (layerBlur) {
|
||||
|
|
|
|||
82
tests/engine/render/canvas/layout-grids.test.ts
Normal file
82
tests/engine/render/canvas/layout-grids.test.ts
Normal file
|
|
@ -0,0 +1,82 @@
|
|||
import { describe, expect, test } from 'bun:test'
|
||||
|
||||
import type { Canvas } from 'canvaskit-wasm'
|
||||
|
||||
import { drawLayoutGrids } from '#core/canvas/layout-grids'
|
||||
import type { SceneNode } from '#core/scene-graph'
|
||||
|
||||
import { createMockCanvas, createMockRenderer, mockCalls } from './effects/helpers'
|
||||
|
||||
function nodeWithLayoutGrids(layoutGrids: unknown[]): SceneNode {
|
||||
return {
|
||||
type: 'FRAME',
|
||||
width: 240,
|
||||
height: 160,
|
||||
source: {
|
||||
fig: {
|
||||
rawNodeFields: { layoutGrids }
|
||||
}
|
||||
}
|
||||
} as SceneNode
|
||||
}
|
||||
|
||||
describe('layout grid rendering', () => {
|
||||
test('renders Figma plugin 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 },
|
||||
sectionSize: 48,
|
||||
gutterSize: 16,
|
||||
alignment: 'MIN',
|
||||
count: 4,
|
||||
offset: 16
|
||||
}
|
||||
])
|
||||
|
||||
drawLayoutGrids(r, canvas as Canvas, node)
|
||||
|
||||
expect(mockCalls(canvas.drawRect)).toHaveLength(4)
|
||||
expect(mockCalls(r.ck.LTRBRect)[0]).toEqual([16, 0, 64, 160])
|
||||
expect(mockCalls(r.ck.LTRBRect)[3]).toEqual([208, 0, 256, 160])
|
||||
})
|
||||
|
||||
test('renders Kiwi row grids', () => {
|
||||
const r = createMockRenderer()
|
||||
const canvas = createMockCanvas()
|
||||
const node = nodeWithLayoutGrids([
|
||||
{
|
||||
pattern: 'STRIPES',
|
||||
axis: 'Y',
|
||||
visible: true,
|
||||
color: { r: 0, g: 0, b: 1, a: 0.2 },
|
||||
sectionSize: 20,
|
||||
gutterSize: 10,
|
||||
type: 'MIN',
|
||||
numSections: 2,
|
||||
offset: 5
|
||||
}
|
||||
])
|
||||
|
||||
drawLayoutGrids(r, canvas as Canvas, node)
|
||||
|
||||
expect(mockCalls(canvas.drawRect)).toHaveLength(2)
|
||||
expect(mockCalls(r.ck.LTRBRect)).toEqual([
|
||||
[0, 5, 240, 25],
|
||||
[0, 35, 240, 55]
|
||||
])
|
||||
})
|
||||
|
||||
test('skips hidden grids', () => {
|
||||
const r = createMockRenderer()
|
||||
const canvas = createMockCanvas()
|
||||
const node = nodeWithLayoutGrids([{ visible: false, sectionSize: 20, count: 2 }])
|
||||
|
||||
drawLayoutGrids(r, canvas as Canvas, node)
|
||||
|
||||
expect(canvas.drawRect).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
Loading…
Reference in a new issue