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) { 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)