openpencil/tests/engine/vector/path-rendering.test.ts
Danila Poyarkov 0332b062c5
refactor(canvas): adopt immutable CanvasKit paths (#554)
* 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
2026-08-18 23:09:45 +03:00

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