* feat(scene-graph)!: make a stroke a paint Stroke extends Fill, so a stroke carries the same paint vocabulary a fill does instead of a lone color. Every construction site now states a solid paint type, which keeps today's behaviour exactly; rendering, .fig conversion, and the stroke panel still read solid strokes only. copyFill is generic over the paint shape so copyStroke reuses it rather than repeating the deep copy of gradient stops, transforms and pattern fields. Groundwork for the gradient and image strokes in #797. * test(scene-graph): cover a stroke's nested paint data in copyStroke A stroke is a paint now, so its gradient stops and transform must copy as deeply as a fill's; the fixture was solid and proved only the color and dash pattern.
158 lines
5.5 KiB
TypeScript
158 lines
5.5 KiB
TypeScript
import { describe, test, expect } from 'bun:test'
|
|
|
|
import type { Fill, Stroke, Effect, StyleRun, GeometryPath } from '@open-pencil/scene-graph'
|
|
import {
|
|
copyFill,
|
|
copyFills,
|
|
copyStroke,
|
|
copyEffect,
|
|
copyStyleRun,
|
|
copyGeometryPaths,
|
|
scaleGeometryPaths
|
|
} from '@open-pencil/scene-graph/copy'
|
|
|
|
import { expectDefined } from './helpers/assert'
|
|
|
|
describe('copy helpers — mutation isolation', () => {
|
|
test('copyFill: mutating copy does not affect original', () => {
|
|
const original: Fill = {
|
|
type: 'SOLID',
|
|
color: { r: 1, g: 0, b: 0, a: 1 },
|
|
opacity: 1,
|
|
visible: true
|
|
}
|
|
const copy = copyFill(original)
|
|
copy.color.r = 0
|
|
copy.opacity = 0.5
|
|
expect(original.color.r).toBe(1)
|
|
expect(original.opacity).toBe(1)
|
|
})
|
|
|
|
test('copyFill: gradient stops are deep copied', () => {
|
|
const original: Fill = {
|
|
type: 'GRADIENT_LINEAR',
|
|
color: { r: 0, g: 0, b: 0, a: 1 },
|
|
opacity: 1,
|
|
visible: true,
|
|
gradientStops: [
|
|
{ color: { r: 1, g: 0, b: 0, a: 1 }, position: 0 },
|
|
{ color: { r: 0, g: 0, b: 1, a: 1 }, position: 1 }
|
|
],
|
|
gradientTransform: { m00: 1, m01: 0, m02: 0, m10: 0, m11: 1, m12: 0 }
|
|
}
|
|
const copy = copyFill(original)
|
|
expectDefined(copy.gradientStops?.[0], 'copied gradient stop').color.r = 0
|
|
expectDefined(copy.gradientTransform, 'copied gradient transform').m00 = 99
|
|
expect(expectDefined(original.gradientStops?.[0], 'original gradient stop').color.r).toBe(1)
|
|
expect(expectDefined(original.gradientTransform, 'original gradient transform').m00).toBe(1)
|
|
})
|
|
|
|
/** A stroke is a paint, so its nested paint data copies as deeply as a fill's does. */
|
|
test('copyStroke: dash pattern and nested paint data are independent', () => {
|
|
const original: Stroke = {
|
|
type: 'GRADIENT_LINEAR',
|
|
color: { r: 0, g: 0, b: 0, a: 1 },
|
|
weight: 1,
|
|
opacity: 1,
|
|
visible: true,
|
|
align: 'CENTER',
|
|
dashPattern: [5, 3],
|
|
gradientStops: [{ color: { r: 1, g: 0, b: 0, a: 1 }, position: 0 }],
|
|
gradientTransform: { m00: 1, m01: 0, m02: 0, m10: 0, m11: 1, m12: 0 }
|
|
}
|
|
const copy = copyStroke(original)
|
|
expectDefined(copy.dashPattern, 'copied dash pattern').push(99)
|
|
copy.color.g = 1
|
|
expectDefined(copy.gradientStops?.[0], 'copied gradient stop').color.r = 0
|
|
expectDefined(copy.gradientTransform, 'copied gradient transform').m00 = 99
|
|
|
|
expect(original.dashPattern).toEqual([5, 3])
|
|
expect(original.color.g).toBe(0)
|
|
expect(expectDefined(original.gradientStops?.[0], 'original gradient stop').color.r).toBe(1)
|
|
expect(expectDefined(original.gradientTransform, 'original gradient transform').m00).toBe(1)
|
|
})
|
|
|
|
test('copyEffect: offset and color are independent', () => {
|
|
const original: Effect = {
|
|
type: 'DROP_SHADOW',
|
|
color: { r: 0, g: 0, b: 0, a: 0.5 },
|
|
offset: { x: 4, y: 4 },
|
|
radius: 8,
|
|
spread: 0,
|
|
visible: true
|
|
}
|
|
const copy = copyEffect(original)
|
|
copy.offset.x = 100
|
|
copy.color.a = 1
|
|
expect(original.offset.x).toBe(4)
|
|
expect(original.color.a).toBe(0.5)
|
|
})
|
|
|
|
test('copyStyleRun: style object is independent', () => {
|
|
const original: StyleRun = {
|
|
start: 0,
|
|
length: 5,
|
|
style: { fontWeight: 700, fontSize: 24 }
|
|
}
|
|
const copy = copyStyleRun(original)
|
|
copy.style.fontWeight = 400
|
|
expect(original.style.fontWeight).toBe(700)
|
|
})
|
|
|
|
test('copyGeometryPaths: blobs and path fills are independent', () => {
|
|
const original: GeometryPath[] = [
|
|
{
|
|
windingRule: 'NONZERO',
|
|
commandsBlob: new Uint8Array([1, 2, 3]),
|
|
fills: [{ type: 'SOLID', color: { r: 1, g: 0, b: 0, a: 1 }, opacity: 1, visible: true }]
|
|
}
|
|
]
|
|
const copy = copyGeometryPaths(original)
|
|
copy[0].commandsBlob[0] = 99
|
|
if (copy[0].fills?.[0]) copy[0].fills[0].color.r = 0
|
|
expect(original[0].commandsBlob[0]).toBe(1)
|
|
expect(original[0].fills?.[0]?.color.r).toBe(1)
|
|
})
|
|
|
|
test('copyGeometryPaths and scaleGeometryPaths keep path-level fills', () => {
|
|
// Resize snapshots use these helpers; dropping fills collapses multi-color
|
|
// vectors to a single node fill (gray blob).
|
|
const orange: Fill = {
|
|
type: 'SOLID',
|
|
color: { r: 1, g: 0.32, b: 0, a: 1 },
|
|
opacity: 1,
|
|
visible: true
|
|
}
|
|
const original: GeometryPath[] = [
|
|
{
|
|
windingRule: 'NONZERO',
|
|
commandsBlob: new Uint8Array([1, 0, 0, 0, 0, 0, 0, 0, 0]),
|
|
fills: [orange]
|
|
},
|
|
{ windingRule: 'EVENODD', commandsBlob: new Uint8Array([2, 3, 4]) }
|
|
]
|
|
|
|
const copied = copyGeometryPaths(original)
|
|
expect(copied[0]?.fills?.[0]?.color.r).toBeCloseTo(1, 2)
|
|
expect(copied[1]?.fills).toBeUndefined()
|
|
expectDefined(copied[0]?.fills?.[0], 'copied path fill').color.r = 0
|
|
expect(original[0]?.fills?.[0]?.color.r).toBe(1)
|
|
|
|
const scaled = scaleGeometryPaths(original, 0.5, 0.5)
|
|
expect(scaled[0]?.fills?.[0]?.color.g).toBeCloseTo(0.32, 2)
|
|
expect(scaled[0]?.commandsBlob).not.toBe(original[0]?.commandsBlob)
|
|
})
|
|
|
|
test('copyFills: array independence', () => {
|
|
const originals: Fill[] = [
|
|
{ type: 'SOLID', color: { r: 1, g: 0, b: 0, a: 1 }, opacity: 1, visible: true },
|
|
{ type: 'SOLID', color: { r: 0, g: 1, b: 0, a: 1 }, opacity: 0.5, visible: true }
|
|
]
|
|
const copies = copyFills(originals)
|
|
copies.push({ type: 'SOLID', color: { r: 0, g: 0, b: 1, a: 1 }, opacity: 1, visible: true })
|
|
copies[0].color.r = 0
|
|
expect(originals).toHaveLength(2)
|
|
expect(originals[0].color.r).toBe(1)
|
|
})
|
|
})
|