openpencil/tests/engine/vue/controls/constraints.test.ts
2026-08-17 12:00:01 +03:00

189 lines
5.7 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 {
collectResizeDescendants,
constrainedChildRect,
type ResizeSnapshot
} from '@open-pencil/scene-graph/resize'
import { constraintPins, isConstraintEligible, toggleConstraintPin } from '@open-pencil/vue'
import { applyResize } from '#vue/shared/input/resize'
import type { DragResize } 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: ResizeSnapshot['vectorNetwork']
}): ResizeSnapshot {
return {
x: node.x,
y: node.y,
width: node.width,
height: node.height,
vectorNetwork: node.vectorNetwork,
fillGeometry: [],
strokeGeometry: [],
derivedTextGlyphs: null,
strokes: [],
textPathBox: null
}
}
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: ResizeSnapshot = {
x: 20,
y: 30,
width: 40,
height: 20,
vectorNetwork: null,
fillGeometry: [],
strokeGeometry: [],
derivedTextGlyphs: null,
strokes: [],
textPathBox: 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,
origFillGeometry: [],
origStrokeGeometry: [],
origDerivedTextGlyphs: null,
origStrokes: [],
origTextPathBox: 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 })
})
test('repositions constrained descendants after an in-flow child grows in resize preview', () => {
const graph = makeSceneGraph()
const pageId = firstPageId(graph)
const root = graph.createNode('FRAME', pageId, {
width: 100,
height: 100,
layoutMode: 'HORIZONTAL',
primaryAxisSizing: 'FIXED',
counterAxisSizing: 'FIXED'
})
const nested = graph.createNode('FRAME', root.id, {
width: 100,
height: 100,
layoutGrow: 1
})
const grandchild = graph.createNode('RECTANGLE', nested.id, {
x: 80,
y: 0,
width: 10,
height: 10,
horizontalConstraint: 'MAX'
})
const editor = createEditor({ graph })
const drag: DragResize = {
type: 'resize',
handle: 'e',
startX: 100,
startY: 50,
origRect: { x: root.x, y: root.y, width: root.width, height: root.height },
nodeId: root.id,
origVectorNetwork: null,
origFillGeometry: [],
origStrokeGeometry: [],
origDerivedTextGlyphs: null,
origStrokes: [],
origTextPathBox: null,
origChildren: collectResizeDescendants(graph, root.id)
}
applyResize(drag, 200, 50, false, editor)
expect(graph.getNode(nested.id)?.width).toBe(200)
expect(graph.getNode(grandchild.id)?.x).toBe(180)
})
})