138 lines
4.6 KiB
TypeScript
138 lines
4.6 KiB
TypeScript
import { writeFile } from 'node:fs/promises'
|
|
|
|
import { initCanvasKit } from '#cli/headless'
|
|
import { SkiaRenderer } from '#core/canvas'
|
|
import { parseSVGPath } from '#core/io/formats/svg/parse-path'
|
|
import type { SceneNode } from '#core/scene-graph'
|
|
|
|
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)
|