- Add mixed-selection cap, join, and miter-limit controls with one-step undo - Apply node miter limits across CanvasKit stroke paths and vector outline caches - Preserve imported and edited stroke geometry through Plugin API and .fig roundtrips
104 lines
3.6 KiB
TypeScript
104 lines
3.6 KiB
TypeScript
import { describe, expect, test } from 'bun:test'
|
|
|
|
import { computed, ref } from 'vue'
|
|
|
|
import { createEditor } from '@open-pencil/core/editor'
|
|
import type { SceneNode } from '@open-pencil/scene-graph'
|
|
|
|
import { MIXED, type MixedValue } from '#vue/controls/node-props/use'
|
|
import {
|
|
DEFAULT_STROKE,
|
|
createStrokeGeometryActions,
|
|
createStrokeGeometryState
|
|
} from '#vue/controls/stroke/helpers'
|
|
|
|
import { firstPageId, makeSceneGraph } from '#tests/helpers/scene'
|
|
|
|
function strokedRect(overrides: Partial<SceneNode> = {}) {
|
|
const graph = makeSceneGraph()
|
|
const node = graph.createNode('RECTANGLE', firstPageId(graph), {
|
|
strokes: [{ ...DEFAULT_STROKE }],
|
|
...overrides
|
|
})
|
|
return { graph, node }
|
|
}
|
|
|
|
function merged(nodes: SceneNode[]) {
|
|
return <K extends keyof SceneNode>(key: K): MixedValue<SceneNode[K]> => {
|
|
const first = nodes[0]?.[key]
|
|
if (first === undefined || nodes.some((node) => node[key] !== first)) return MIXED
|
|
return first
|
|
}
|
|
}
|
|
|
|
describe('stroke geometry controls', () => {
|
|
test('is active only when every selected node has a stroke and reports mixed values', () => {
|
|
const { node } = strokedRect()
|
|
const other = structuredClone(node)
|
|
other.id = 'other'
|
|
other.strokeJoin = 'BEVEL'
|
|
const nodes = ref([node, other])
|
|
const state = createStrokeGeometryState({
|
|
nodes: computed(() => nodes.value),
|
|
merged: (key) => merged(nodes.value)(key)
|
|
})
|
|
|
|
expect(state.advancedActive.value).toBe(true)
|
|
expect(state.cap.value).toBe('NONE')
|
|
expect(state.join.value).toBe(MIXED)
|
|
|
|
nodes.value[1].strokes = []
|
|
expect(state.advancedActive.value).toBe(false)
|
|
})
|
|
|
|
test('synchronizes node and paint cap and join with one multi-selection undo', () => {
|
|
const graph = makeSceneGraph()
|
|
const pageId = firstPageId(graph)
|
|
const first = graph.createNode('RECTANGLE', pageId, { strokes: [{ ...DEFAULT_STROKE }] })
|
|
const second = graph.createNode('RECTANGLE', pageId, { strokes: [{ ...DEFAULT_STROKE }] })
|
|
const editor = createEditor({ graph })
|
|
const nodes = computed(() =>
|
|
[graph.getNode(first.id), graph.getNode(second.id)].filter(Boolean)
|
|
)
|
|
const actions = createStrokeGeometryActions(editor, nodes)
|
|
|
|
actions.setCap('ROUND')
|
|
actions.setJoin('BEVEL')
|
|
expect(graph.getNode(first.id)).toMatchObject({
|
|
strokeCap: 'ROUND',
|
|
strokeJoin: 'BEVEL',
|
|
strokes: [{ cap: 'ROUND', join: 'BEVEL' }]
|
|
})
|
|
expect(graph.getNode(second.id)?.strokes[0]).toMatchObject({ cap: 'ROUND', join: 'BEVEL' })
|
|
|
|
editor.undo.undo()
|
|
expect(graph.getNode(first.id)?.strokeJoin).toBe('MITER')
|
|
expect(graph.getNode(second.id)?.strokeJoin).toBe('MITER')
|
|
expect(graph.getNode(first.id)?.strokeCap).toBe('ROUND')
|
|
})
|
|
|
|
test('previews and commits miter limit as one multi-selection undo step', () => {
|
|
const graph = makeSceneGraph()
|
|
const pageId = firstPageId(graph)
|
|
const first = graph.createNode('RECTANGLE', pageId, { strokes: [{ ...DEFAULT_STROKE }] })
|
|
const second = graph.createNode('RECTANGLE', pageId, {
|
|
strokes: [{ ...DEFAULT_STROKE }],
|
|
strokeMiterLimit: 8
|
|
})
|
|
const editor = createEditor({ graph })
|
|
const nodes = computed(() =>
|
|
[graph.getNode(first.id), graph.getNode(second.id)].filter(Boolean)
|
|
)
|
|
const actions = createStrokeGeometryActions(editor, nodes)
|
|
|
|
actions.updateMiterLimit(12)
|
|
expect(graph.getNode(first.id)?.strokeMiterLimit).toBe(12)
|
|
expect(graph.getNode(second.id)?.strokeMiterLimit).toBe(12)
|
|
actions.commitMiterLimit(12)
|
|
|
|
editor.undo.undo()
|
|
expect(graph.getNode(first.id)?.strokeMiterLimit).toBe(4)
|
|
expect(graph.getNode(second.id)?.strokeMiterLimit).toBe(8)
|
|
})
|
|
})
|