openpencil/tests/engine/vue/input/resize-geometry.test.ts
Danila Poyarkov cb2c4aa3d3 fix(vector): edit vectors from opened files
- Resolve the active graph at action time and map edit geometry through full world transforms
- Regenerate live path fills and discard stale imported stroke outlines after edits
- Preserve fill and stroke geometry through resize previews, undo, and redo
- Cover graph replacement, nested transforms, edit history, resizing, and CanvasKit output

Co-authored-by: Rob Coenen <753704+rcoenen@users.noreply.github.com>
2026-07-28 12:30:21 +03:00

140 lines
4.6 KiB
TypeScript

import { describe, expect, test } from 'bun:test'
import { createEditor } from '@open-pencil/core/editor'
import type { Fill, GeometryPath, SceneNode, VectorNetwork } from '@open-pencil/scene-graph'
import { cloneVectorNetwork } from '@open-pencil/scene-graph'
import { copyGeometryPaths } from '@open-pencil/scene-graph/copy'
import { collectResizeDescendants } from '@open-pencil/scene-graph/resize'
import { applyResize, commitResizePreview } from '#vue/shared/input/resize'
import type { DragResize } from '#vue/shared/input/types'
import { getNodeOrThrow } from '#tests/helpers/assert'
const RED: Fill = {
type: 'SOLID',
color: { r: 1, g: 0, b: 0, a: 1 },
opacity: 1,
visible: true,
blendMode: 'NORMAL'
}
const NETWORK: VectorNetwork = {
vertices: [
{ x: 0, y: 0 },
{ x: 10, y: 0 },
{ x: 10, y: 10 },
{ x: 0, y: 10 }
],
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 } }
],
regions: [{ windingRule: 'NONZERO', loops: [[0, 1, 2, 3]] }]
}
function movePath(x: number, y: number, fills?: Fill[]): GeometryPath {
const commandsBlob = new Uint8Array(9)
const view = new DataView(commandsBlob.buffer)
commandsBlob[0] = 1
view.setFloat32(1, x, true)
view.setFloat32(5, y, true)
return { windingRule: 'NONZERO', commandsBlob, fills }
}
function pathPoint(path: GeometryPath | undefined) {
if (!path) return null
const view = new DataView(
path.commandsBlob.buffer,
path.commandsBlob.byteOffset,
path.commandsBlob.byteLength
)
return [view.getFloat32(1, true), view.getFloat32(5, true)]
}
function vectorGeometry(node: SceneNode) {
return {
network: node.vectorNetwork ? cloneVectorNetwork(node.vectorNetwork) : null,
fills: copyGeometryPaths(node.fillGeometry),
strokes: copyGeometryPaths(node.strokeGeometry)
}
}
function dragFor(node: SceneNode, origChildren: DragResize['origChildren'] = null): DragResize {
const geometry = vectorGeometry(node)
return {
type: 'resize',
handle: 'se',
startX: 10,
startY: 10,
origRect: { x: node.x, y: node.y, width: node.width, height: node.height },
nodeId: node.id,
origVectorNetwork: geometry.network,
origFillGeometry: geometry.fills,
origStrokeGeometry: geometry.strokes,
origChildren
}
}
function expectGeometry(node: SceneNode, size: number) {
expect(node.width).toBe(size)
expect(node.height).toBe(size)
expect(node.vectorNetwork?.vertices[2]).toMatchObject({ x: size, y: size })
expect(pathPoint(node.fillGeometry[0])).toEqual([size, size])
expect(pathPoint(node.strokeGeometry[0])).toEqual([size / 2, size / 2])
expect(node.fillGeometry[0]?.fills?.[0]?.color.r).toBe(1)
}
describe('vector resize geometry', () => {
test('keeps fill and stroke paths aligned through resize, undo, and redo', () => {
const editor = createEditor()
const page = editor.graph.getPages()[0]
const vector = editor.graph.createNode('VECTOR', page.id, {
width: 10,
height: 10,
vectorNetwork: cloneVectorNetwork(NETWORK),
fillGeometry: [movePath(10, 10, [RED])],
strokeGeometry: [movePath(5, 5)]
})
const drag = dragFor(vector)
applyResize(drag, 20, 20, false, editor)
commitResizePreview(drag, editor)
expectGeometry(getNodeOrThrow(editor.graph, vector.id), 20)
editor.undo.undo()
expectGeometry(getNodeOrThrow(editor.graph, vector.id), 10)
editor.undo.redo()
expectGeometry(getNodeOrThrow(editor.graph, vector.id), 20)
})
test('keeps descendant geometry aligned through group resize history', () => {
const editor = createEditor()
const page = editor.graph.getPages()[0]
const group = editor.graph.createNode('GROUP', page.id, { width: 10, height: 10 })
const vector = editor.graph.createNode('VECTOR', group.id, {
width: 10,
height: 10,
vectorNetwork: cloneVectorNetwork(NETWORK),
fillGeometry: [movePath(10, 10, [RED])],
strokeGeometry: [movePath(5, 5)]
})
const descendants = collectResizeDescendants(editor.graph, group.id)
expect(descendants).not.toBeNull()
const drag = dragFor(group, descendants)
applyResize(drag, 20, 20, false, editor)
commitResizePreview(drag, editor)
expectGeometry(getNodeOrThrow(editor.graph, vector.id), 20)
editor.undo.undo()
expectGeometry(getNodeOrThrow(editor.graph, vector.id), 10)
editor.undo.redo()
expectGeometry(getNodeOrThrow(editor.graph, vector.id), 20)
})
})