- Add SDK-owned mixed constraint state, pin controls, and undo-aware updates - Apply Figma constraint modes recursively during frame resize previews and commits - Document and test the control, multi-selection behavior, and resize geometry
125 lines
4 KiB
TypeScript
125 lines
4 KiB
TypeScript
import { describe, expect, test } from 'bun:test'
|
|
|
|
import { createEditor } from '@open-pencil/core/editor'
|
|
import type { ConstraintType } from '@open-pencil/scene-graph'
|
|
import { constraintPins, isConstraintEligible, toggleConstraintPin } from '@open-pencil/vue'
|
|
|
|
import { applyResize } from '#vue/shared/input/resize'
|
|
import { constrainedChildRect } from '#vue/shared/input/resize/constraints'
|
|
import type { DragResize, OrigChildState } from '#vue/shared/input/types'
|
|
|
|
import { createRect, firstPageId, makeSceneGraph } from '#tests/helpers/scene'
|
|
|
|
function original(node: {
|
|
x: number
|
|
y: number
|
|
width: number
|
|
height: number
|
|
vectorNetwork: OrigChildState['vectorNetwork']
|
|
}): OrigChildState {
|
|
return {
|
|
x: node.x,
|
|
y: node.y,
|
|
width: node.width,
|
|
height: node.height,
|
|
vectorNetwork: node.vectorNetwork
|
|
}
|
|
}
|
|
|
|
describe('constraint control model', () => {
|
|
test('limits constraints to eligible frame children', () => {
|
|
const graph = makeSceneGraph()
|
|
const pageId = firstPageId(graph)
|
|
const topLevel = createRect(graph, pageId)
|
|
const frame = graph.createNode('FRAME', pageId, { layoutMode: 'NONE' })
|
|
const child = createRect(graph, frame.id)
|
|
const group = graph.createNode('GROUP', frame.id)
|
|
expect(isConstraintEligible(graph, topLevel)).toBe(false)
|
|
expect(isConstraintEligible(graph, child)).toBe(true)
|
|
expect(isConstraintEligible(graph, group)).toBe(false)
|
|
|
|
graph.updateNode(frame.id, { layoutMode: 'HORIZONTAL' })
|
|
expect(isConstraintEligible(graph, child)).toBe(false)
|
|
graph.updateNode(child.id, { layoutPositioning: 'ABSOLUTE' })
|
|
expect(isConstraintEligible(graph, child)).toBe(true)
|
|
})
|
|
|
|
test('maps edge pins and additive pin toggles', () => {
|
|
expect(constraintPins('STRETCH')).toMatchObject({ leading: true, trailing: true })
|
|
expect(toggleConstraintPin('MAX', 'leading', false)).toBe('MIN')
|
|
expect(toggleConstraintPin('MAX', 'leading', true)).toBe('STRETCH')
|
|
expect(toggleConstraintPin('STRETCH', 'leading', true)).toBe('MAX')
|
|
expect(toggleConstraintPin('MIN', 'leading', true)).toBe('CENTER')
|
|
})
|
|
})
|
|
|
|
describe('constraint resize geometry', () => {
|
|
const child: OrigChildState = {
|
|
x: 20,
|
|
y: 30,
|
|
width: 40,
|
|
height: 20,
|
|
vectorNetwork: null
|
|
}
|
|
const expectedX: Record<ConstraintType, [number, number]> = {
|
|
MIN: [20, 40],
|
|
CENTER: [70, 40],
|
|
MAX: [120, 40],
|
|
STRETCH: [20, 140],
|
|
SCALE: [40, 80]
|
|
}
|
|
|
|
for (const constraint of Object.keys(expectedX) as ConstraintType[]) {
|
|
test(`applies ${constraint} on one axis`, () => {
|
|
const rect = constrainedChildRect(
|
|
child,
|
|
{ width: 100, height: 100 },
|
|
{ width: 200, height: 100 },
|
|
constraint,
|
|
'MIN'
|
|
)
|
|
expect([rect.x, rect.width]).toEqual(expectedX[constraint])
|
|
expect([rect.y, rect.height]).toEqual([30, 20])
|
|
})
|
|
}
|
|
|
|
test('recurses through constrained nested frames during resize preview', () => {
|
|
const graph = makeSceneGraph()
|
|
const pageId = firstPageId(graph)
|
|
const root = graph.createNode('FRAME', pageId, { width: 100, height: 100 })
|
|
const nested = graph.createNode('FRAME', root.id, {
|
|
x: 10,
|
|
y: 10,
|
|
width: 50,
|
|
height: 50,
|
|
horizontalConstraint: 'STRETCH'
|
|
})
|
|
const grandchild = graph.createNode('RECTANGLE', nested.id, {
|
|
x: 30,
|
|
y: 10,
|
|
width: 10,
|
|
height: 10,
|
|
horizontalConstraint: 'MAX'
|
|
})
|
|
const editor = createEditor({ graph })
|
|
const drag: DragResize = {
|
|
type: 'resize',
|
|
handle: 'se',
|
|
startX: 100,
|
|
startY: 100,
|
|
origRect: { x: root.x, y: root.y, width: root.width, height: root.height },
|
|
nodeId: root.id,
|
|
origVectorNetwork: null,
|
|
origChildren: new Map([
|
|
[nested.id, original(nested)],
|
|
[grandchild.id, original(grandchild)]
|
|
])
|
|
}
|
|
|
|
applyResize(drag, 200, 100, false, editor)
|
|
|
|
expect(graph.getNode(nested.id)).toMatchObject({ x: 10, width: 150 })
|
|
expect(graph.getNode(grandchild.id)).toMatchObject({ x: 130, width: 10 })
|
|
})
|
|
})
|