openpencil/tests/engine/render/canvas/visual/boolean-matrix.ts
2026-06-30 10:54:32 +03:00

139 lines
4.6 KiB
TypeScript

import { writeFile } from 'node:fs/promises'
import type { SceneNode } from '@open-pencil/scene-graph'
import { initCanvasKit } from '#cli/headless'
import { SkiaRenderer } from '#core/canvas'
import { parseSVGPath } from '#core/io/formats/svg/parse-path'
import { createAPI } from '#tests/engine/figma/api/helpers'
import { expectDefined } from '#tests/helpers/assert'
const CELL_W = 180
const CELL_H = 150
const COLUMNS = 4
const OUT = '/tmp/open-pencil-boolean-matrix.png'
type Operation = 'union' | 'subtract' | 'intersect' | 'exclude'
type CaseName = 'rect' | 'rounded' | 'ellipse' | 'arc' | 'line' | 'polygon' | 'star' | 'vector'
const cases: CaseName[] = ['rect', 'rounded', 'ellipse', 'arc', 'line', 'polygon', 'star', 'vector']
const operations: Operation[] = ['union', 'subtract', 'intersect', 'exclude']
function color(index: number) {
const palette = [
{ r: 0.12, g: 0.45, b: 1, a: 1 },
{ r: 0.98, g: 0.22, b: 0.45, a: 1 },
{ r: 0.18, g: 0.74, b: 0.42, a: 1 },
{ r: 0.98, g: 0.64, b: 0.1, a: 1 }
]
return palette[index % palette.length]
}
function setSolidFill(node: SceneNode, index: number) {
node.fills = [{ type: 'SOLID', color: color(index), opacity: 1, visible: true }]
}
function makePair(name: CaseName, x: number, y: number) {
const api = createAPI()
const first = api.createRectangle()
const second = api.createRectangle()
first.x = x + 30
first.y = y + 36
second.x = x + 72
second.y = y + 48
first.resize(82, 72)
second.resize(82, 72)
const firstNode = expectDefined(api.graph.getNode(first.id), 'first node')
const secondNode = expectDefined(api.graph.getNode(second.id), 'second node')
if (name === 'rounded') {
first.cornerRadius = 18
second.cornerRadius = 18
} else if (name === 'ellipse' || name === 'arc') {
firstNode.type = 'ELLIPSE'
secondNode.type = 'ELLIPSE'
if (name === 'arc') {
firstNode.arcData = { startingAngle: 0, endingAngle: Math.PI * 1.5, innerRadius: 0.35 }
secondNode.arcData = {
startingAngle: Math.PI * 0.25,
endingAngle: Math.PI * 1.75,
innerRadius: 0.2
}
}
} else if (name === 'line') {
firstNode.type = 'LINE'
secondNode.type = 'LINE'
firstNode.height = 54
secondNode.height = 54
firstNode.strokes = [
{ type: 'SOLID', color: color(1), opacity: 1, visible: true, weight: 14, align: 'CENTER' }
]
secondNode.strokes = [
{ type: 'SOLID', color: color(2), opacity: 1, visible: true, weight: 14, align: 'CENTER' }
]
} else if (name === 'polygon' || name === 'star') {
firstNode.type = name === 'polygon' ? 'POLYGON' : 'STAR'
secondNode.type = name === 'polygon' ? 'POLYGON' : 'STAR'
firstNode.pointCount = name === 'polygon' ? 6 : 5
secondNode.pointCount = name === 'polygon' ? 6 : 5
} else if (name === 'vector') {
firstNode.type = 'VECTOR'
secondNode.type = 'VECTOR'
firstNode.vectorNetwork = parseSVGPath('M 0 72 L 41 0 L 82 72 Z')
secondNode.vectorNetwork = parseSVGPath('M 0 0 L 82 0 L 41 72 Z')
}
setSolidFill(firstNode, 0)
setSolidFill(secondNode, 1)
return { api, first, second }
}
function applyOperation(operation: Operation, pair: ReturnType<typeof makePair>) {
switch (operation) {
case 'union':
return pair.api.union([pair.first, pair.second], pair.api.currentPage)
case 'subtract':
return pair.api.subtract([pair.first, pair.second], pair.api.currentPage)
case 'intersect':
return pair.api.intersect([pair.first, pair.second], pair.api.currentPage)
case 'exclude':
return pair.api.exclude([pair.first, pair.second], pair.api.currentPage)
}
}
const ck = await initCanvasKit()
const width = CELL_W * COLUMNS
const rows = cases.length
const height = CELL_H * rows
const surface = expectDefined(ck.MakeSurface(width, height), 'surface')
const renderer = new SkiaRenderer(ck, surface)
renderer.viewportWidth = width
renderer.viewportHeight = height
renderer.dpr = 1
renderer.worldViewport = { x: -1e9, y: -1e9, w: 2e9, h: 2e9 }
const canvas = surface.getCanvas()
canvas.clear(ck.WHITE)
for (let row = 0; row < cases.length; row++) {
for (let column = 0; column < operations.length; column++) {
const pair = makePair(cases[row], column * CELL_W, row * CELL_H)
const booleanNode = applyOperation(operations[column], pair)
const node = expectDefined(pair.api.graph.getNode(booleanNode.id), 'boolean node')
setSolidFill(node, column)
renderer.renderNode(canvas, pair.api.graph, node.id, {})
}
}
surface.flush()
const image = surface.makeImageSnapshot()
const png = expectDefined(image.encodeToBytes(ck.ImageFormat.PNG, 100), 'encoded image')
await writeFile(OUT, png)
image.delete()
surface.delete()
console.warn(OUT)