* 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
131 lines
3.4 KiB
TypeScript
131 lines
3.4 KiB
TypeScript
import { describe, expect, test } from 'bun:test'
|
|
|
|
import type { CanvasKit } from 'canvaskit-wasm'
|
|
|
|
import { vectorNetworkToPath } from '@open-pencil/core/vector'
|
|
import type { VectorNetwork } from '@open-pencil/scene-graph'
|
|
|
|
interface RecordedPath {
|
|
operations: string[]
|
|
fillType: number | null
|
|
}
|
|
|
|
function createCanvasKit() {
|
|
const paths: RecordedPath[] = []
|
|
class MockPath {
|
|
operations: string[] = []
|
|
fillType: number | null = null
|
|
|
|
constructor() {
|
|
paths.push(this)
|
|
}
|
|
|
|
moveTo(x: number, y: number): void {
|
|
this.operations.push(`M${x},${y}`)
|
|
}
|
|
|
|
lineTo(x: number, y: number): void {
|
|
this.operations.push(`L${x},${y}`)
|
|
}
|
|
|
|
cubicTo(x1: number, y1: number, x2: number, y2: number, x3: number, y3: number): void {
|
|
this.operations.push(`C${x1},${y1},${x2},${y2},${x3},${y3}`)
|
|
}
|
|
|
|
close(): void {
|
|
this.operations.push('Z')
|
|
}
|
|
|
|
setFillType(value: number): void {
|
|
this.fillType = value
|
|
}
|
|
}
|
|
|
|
class MockPathBuilder {
|
|
operations: string[] = []
|
|
fillType: number | null = null
|
|
|
|
moveTo(x: number, y: number): void {
|
|
this.operations.push(`M${x},${y}`)
|
|
}
|
|
|
|
lineTo(x: number, y: number): void {
|
|
this.operations.push(`L${x},${y}`)
|
|
}
|
|
|
|
cubicTo(x1: number, y1: number, x2: number, y2: number, x3: number, y3: number): void {
|
|
this.operations.push(`C${x1},${y1},${x2},${y2},${x3},${y3}`)
|
|
}
|
|
|
|
close(): void {
|
|
this.operations.push('Z')
|
|
}
|
|
|
|
setFillType(value: number): void {
|
|
this.fillType = value
|
|
}
|
|
|
|
detachAndDelete(): MockPath {
|
|
const path = new MockPath()
|
|
path.operations = [...this.operations]
|
|
path.fillType = this.fillType
|
|
return path
|
|
}
|
|
}
|
|
|
|
return {
|
|
ck: {
|
|
PathBuilder: MockPathBuilder,
|
|
FillType: { Winding: 0, EvenOdd: 1 }
|
|
} as CanvasKit,
|
|
paths
|
|
}
|
|
}
|
|
|
|
function mixedNetwork(): VectorNetwork {
|
|
return {
|
|
vertices: [
|
|
{ x: 0, y: 0 },
|
|
{ x: 10, y: 0 },
|
|
{ x: 10, y: 10 },
|
|
{ x: 0, y: 10 },
|
|
{ x: 50, y: 50 },
|
|
{ x: 90, y: 50 }
|
|
],
|
|
segments: [
|
|
{ start: 0, end: 1, tangentStart: { x: 0, y: 0 }, tangentEnd: { x: 0, y: 0 } },
|
|
{ start: 1, end: 2, tangentStart: { x: 0, y: 0 }, tangentEnd: { x: 0, y: 0 } },
|
|
{ start: 2, end: 3, tangentStart: { x: 0, y: 0 }, tangentEnd: { x: 0, y: 0 } },
|
|
{ start: 3, end: 0, tangentStart: { x: 0, y: 0 }, tangentEnd: { x: 0, y: 0 } },
|
|
{ start: 4, end: 5, tangentStart: { x: 0, y: 0 }, tangentEnd: { x: 0, y: 0 } }
|
|
],
|
|
regions: [{ windingRule: 'NONZERO', loops: [[0, 1, 2, 3]] }]
|
|
}
|
|
}
|
|
|
|
describe('vectorNetworkToPath', () => {
|
|
test('preserves open segments alongside filled regions', () => {
|
|
const { ck, paths } = createCanvasKit()
|
|
|
|
const result = vectorNetworkToPath(ck, mixedNetwork())
|
|
|
|
expect(result).toHaveLength(2)
|
|
expect(paths[0]).toEqual({
|
|
operations: ['M0,0', 'L10,0', 'L10,10', 'L0,10', 'L0,0', 'Z'],
|
|
fillType: 0
|
|
})
|
|
expect(paths[1]).toEqual({ operations: ['M50,50', 'L90,50'], fillType: null })
|
|
})
|
|
|
|
test('does not duplicate segments shared by multiple region loops', () => {
|
|
const { ck, paths } = createCanvasKit()
|
|
const network = mixedNetwork()
|
|
network.regions.push({ windingRule: 'EVENODD', loops: [[0, 1, 2, 3]] })
|
|
|
|
const result = vectorNetworkToPath(ck, network)
|
|
|
|
expect(result).toHaveLength(3)
|
|
expect(paths.filter((path) => path.operations.includes('M50,50'))).toHaveLength(1)
|
|
})
|
|
})
|