* refactor(canvas): adopt immutable CanvasKit paths - Upgrade CanvasKit to 0.41.1 across app, Core, CLI, and Vue peers - Build mutable geometry with PathBuilder and detach immutable paths for drawing - Replace in-place strokes, transforms, and boolean operations with immutable results - Update renderer mocks and path construction tests for the new API * test(canvas): model immutable path ownership - Return detached path instances from CanvasKit builder mocks - Assert shadow ring and combined path disposal - Preserve recorded vector operations across ownership transfer * fix(build): align CanvasKit workspace versions
123 lines
3.5 KiB
TypeScript
123 lines
3.5 KiB
TypeScript
import { describe, expect, mock, test } from 'bun:test'
|
|
|
|
import type { Canvas } from 'canvaskit-wasm'
|
|
|
|
import { SceneGraph } from '@open-pencil/scene-graph'
|
|
|
|
import { drawNodeFill } from '#core/canvas/fills'
|
|
import type { SkiaRenderer } from '#core/canvas/renderer'
|
|
import { makeSmoothRRectPath } from '#core/canvas/shapes'
|
|
|
|
function pageId(graph: SceneGraph) {
|
|
return graph.getPages()[0].id
|
|
}
|
|
|
|
function createRenderer() {
|
|
const paths: Array<{
|
|
addRect: ReturnType<typeof mock>
|
|
moveTo: ReturnType<typeof mock>
|
|
lineTo: ReturnType<typeof mock>
|
|
cubicTo: ReturnType<typeof mock>
|
|
arcToRotated: ReturnType<typeof mock>
|
|
close: ReturnType<typeof mock>
|
|
delete: ReturnType<typeof mock>
|
|
}> = []
|
|
|
|
class MockPathBuilder {
|
|
addRect = mock(() => this)
|
|
moveTo = mock(() => this)
|
|
lineTo = mock(() => this)
|
|
cubicTo = mock(() => this)
|
|
arcToRotated = mock(() => this)
|
|
close = mock(() => this)
|
|
delete = mock(() => undefined)
|
|
detachAndDelete = mock(() => ({ delete: mock(() => undefined) }))
|
|
|
|
constructor() {
|
|
paths.push(this)
|
|
}
|
|
}
|
|
|
|
const renderer = {
|
|
ck: {
|
|
PathBuilder: MockPathBuilder,
|
|
LTRBRect: mock((l, t, r, b) => new Float32Array([l, t, r, b]))
|
|
},
|
|
fillPaint: {}
|
|
} as SkiaRenderer
|
|
|
|
return { renderer, paths }
|
|
}
|
|
|
|
function createCanvas() {
|
|
return {
|
|
drawPath: mock(() => undefined),
|
|
drawRRect: mock(() => undefined),
|
|
drawRect: mock(() => undefined)
|
|
}
|
|
}
|
|
|
|
describe('canvas corner smoothing', () => {
|
|
test('builds cubic paths for smoothed rectangular corners', () => {
|
|
const graph = new SceneGraph()
|
|
const node = graph.createNode('RECTANGLE', pageId(graph), {
|
|
width: 120,
|
|
height: 80,
|
|
cornerRadius: 24,
|
|
cornerSmoothing: 0.75
|
|
})
|
|
const { renderer, paths } = createRenderer()
|
|
|
|
makeSmoothRRectPath(renderer, node).delete()
|
|
|
|
expect(paths).toHaveLength(1)
|
|
expect(paths[0].moveTo).toHaveBeenCalledWith(80, 0)
|
|
expect(paths[0].lineTo).toHaveBeenCalledWith(120, 40)
|
|
expect(paths[0].arcToRotated).toHaveBeenCalledTimes(4)
|
|
expect(paths[0].cubicTo).toHaveBeenCalledTimes(8)
|
|
expect(paths[0].lineTo).toHaveBeenCalledTimes(3)
|
|
expect(paths[0].close).toHaveBeenCalled()
|
|
})
|
|
|
|
test('supports independent smoothed corner radii', () => {
|
|
const graph = new SceneGraph()
|
|
const node = graph.createNode('RECTANGLE', pageId(graph), {
|
|
width: 120,
|
|
height: 80,
|
|
independentCorners: true,
|
|
topLeftRadius: 28,
|
|
topRightRadius: 12,
|
|
bottomRightRadius: 32,
|
|
bottomLeftRadius: 0,
|
|
cornerSmoothing: 1
|
|
})
|
|
const { renderer, paths } = createRenderer()
|
|
|
|
makeSmoothRRectPath(renderer, node).delete()
|
|
|
|
expect(paths).toHaveLength(1)
|
|
expect(paths[0].moveTo).toHaveBeenCalledWith(98.18181818181819, 0)
|
|
expect(paths[0].arcToRotated).toHaveBeenCalledTimes(3)
|
|
expect(paths[0].cubicTo).toHaveBeenCalledTimes(6)
|
|
expect(paths[0].lineTo).toHaveBeenCalledWith(0, 80)
|
|
})
|
|
|
|
test('draws smoothed rectangle fills as paths instead of regular rrects', () => {
|
|
const graph = new SceneGraph()
|
|
const node = graph.createNode('RECTANGLE', pageId(graph), {
|
|
width: 120,
|
|
height: 80,
|
|
cornerRadius: 24,
|
|
cornerSmoothing: 0.75
|
|
})
|
|
const { renderer } = createRenderer()
|
|
const canvas = createCanvas()
|
|
|
|
drawNodeFill(renderer, canvas as Canvas, node, new Float32Array([0, 0, 120, 80]), true)
|
|
|
|
expect(canvas.drawPath).toHaveBeenCalled()
|
|
expect(canvas.drawRRect).not.toHaveBeenCalled()
|
|
expect(canvas.drawRect).not.toHaveBeenCalled()
|
|
})
|
|
})
|