- Snap vector points, moved layers, and resized edges to geometry, objects, guides, and pixels - Add persistent snapping preferences with browser and native menu controls - Normalize canvas and layout guides across Scene Graph and .fig conversion - Clear transient snapping feedback across interrupted interactions
209 lines
5.7 KiB
TypeScript
209 lines
5.7 KiB
TypeScript
import { describe, expect, test } from 'bun:test'
|
|
|
|
import { DEFAULT_SNAPPING_PREFERENCES, createEditor } from '@open-pencil/core/editor'
|
|
|
|
import { applyNodeEditSnap } from '#vue/shared/input/node-edit/snap'
|
|
import type { DragEditNode } from '#vue/shared/input/types'
|
|
|
|
function createDrag(startX: number, startY: number): DragEditNode {
|
|
return {
|
|
type: 'edit-node',
|
|
startX,
|
|
startY,
|
|
origPositions: new Map([[0, { x: startX, y: startY }]])
|
|
}
|
|
}
|
|
|
|
describe('vector edit snapping', () => {
|
|
test('snaps a dragged vertex to sibling bounds and publishes a guide', () => {
|
|
const editor = createEditor()
|
|
const pageId = editor.state.currentPageId
|
|
const vector = editor.graph.createNode('VECTOR', pageId, {
|
|
x: 0,
|
|
y: 0,
|
|
width: 50,
|
|
height: 50,
|
|
vectorNetwork: {
|
|
vertices: [{ x: 10, y: 20 }],
|
|
segments: [],
|
|
regions: []
|
|
}
|
|
})
|
|
editor.graph.createNode('RECTANGLE', pageId, {
|
|
x: 100,
|
|
y: 0,
|
|
width: 100,
|
|
height: 100
|
|
})
|
|
const editState = {
|
|
nodeId: vector.id,
|
|
vertices: [{ x: 10, y: 20 }],
|
|
segments: [],
|
|
selectedVertexIndices: new Set([0]),
|
|
selectedHandles: new Set<string>(),
|
|
hoveredHandleInfo: null
|
|
}
|
|
|
|
const snapped = applyNodeEditSnap(createDrag(10, 20), 87, 0, editor, editState)
|
|
|
|
expect(snapped).toEqual({ x: 90, y: 0 })
|
|
expect(editor.state.snapGuides).toContainEqual({
|
|
axis: 'x',
|
|
position: 100,
|
|
from: 0,
|
|
to: 100
|
|
})
|
|
})
|
|
|
|
test('snaps a dragged vertex to another vertex in the edited network', () => {
|
|
const editor = createEditor()
|
|
const pageId = editor.state.currentPageId
|
|
const vector = editor.graph.createNode('VECTOR', pageId, {
|
|
x: 0,
|
|
y: 0,
|
|
width: 100,
|
|
height: 100,
|
|
vectorNetwork: {
|
|
vertices: [
|
|
{ x: 10, y: 20 },
|
|
{ x: 80, y: 60 }
|
|
],
|
|
segments: [],
|
|
regions: []
|
|
}
|
|
})
|
|
const editState = {
|
|
nodeId: vector.id,
|
|
vertices: [
|
|
{ x: 10, y: 20 },
|
|
{ x: 80, y: 60 }
|
|
],
|
|
segments: [],
|
|
selectedVertexIndices: new Set([0]),
|
|
selectedHandles: new Set<string>(),
|
|
hoveredHandleInfo: null
|
|
}
|
|
|
|
const snapped = applyNodeEditSnap(createDrag(10, 20), 67, 37, editor, editState)
|
|
|
|
expect(snapped).toEqual({ x: 70, y: 40 })
|
|
expect(editor.state.snapGuides).toEqual(
|
|
expect.arrayContaining([
|
|
{ kind: 'geometry', axis: 'x', position: 80, from: 60, to: 60 },
|
|
{ kind: 'geometry', axis: 'y', position: 60, from: 80, to: 80 }
|
|
])
|
|
)
|
|
})
|
|
|
|
test('keeps the snap threshold constant in screen pixels across zoom levels', () => {
|
|
const editor = createEditor()
|
|
editor.state.zoom = 2
|
|
const pageId = editor.state.currentPageId
|
|
const vector = editor.graph.createNode('VECTOR', pageId, {
|
|
x: 0,
|
|
y: 0,
|
|
width: 100,
|
|
height: 100,
|
|
vectorNetwork: {
|
|
vertices: [
|
|
{ x: 10, y: 20 },
|
|
{ x: 80, y: 60 }
|
|
],
|
|
segments: [],
|
|
regions: []
|
|
}
|
|
})
|
|
const editState = {
|
|
nodeId: vector.id,
|
|
vertices: [
|
|
{ x: 10, y: 20 },
|
|
{ x: 80, y: 60 }
|
|
],
|
|
segments: [],
|
|
selectedVertexIndices: new Set([0]),
|
|
selectedHandles: new Set<string>(),
|
|
hoveredHandleInfo: null
|
|
}
|
|
|
|
const outsideScreenThreshold = applyNodeEditSnap(createDrag(10, 20), 67, 0, editor, editState)
|
|
const insideScreenThreshold = applyNodeEditSnap(createDrag(10, 20), 68, 0, editor, editState)
|
|
|
|
expect(outsideScreenThreshold.x).toBe(67)
|
|
expect(insideScreenThreshold.x).toBe(70)
|
|
})
|
|
|
|
test('respects independent geometry and object preferences', () => {
|
|
const editor = createEditor()
|
|
const pageId = editor.state.currentPageId
|
|
const vector = editor.graph.createNode('VECTOR', pageId, {
|
|
x: 0,
|
|
y: 0,
|
|
width: 100,
|
|
height: 100,
|
|
vectorNetwork: {
|
|
vertices: [
|
|
{ x: 10, y: 20 },
|
|
{ x: 80, y: 60 }
|
|
],
|
|
segments: [],
|
|
regions: []
|
|
}
|
|
})
|
|
editor.graph.createNode('RECTANGLE', pageId, { x: 100, y: 0, width: 100, height: 100 })
|
|
const editState = {
|
|
nodeId: vector.id,
|
|
vertices: [
|
|
{ x: 10, y: 20 },
|
|
{ x: 80, y: 60 }
|
|
],
|
|
segments: [],
|
|
selectedVertexIndices: new Set([0]),
|
|
selectedHandles: new Set<string>(),
|
|
hoveredHandleInfo: null
|
|
}
|
|
|
|
editor.state.snappingPreferences = {
|
|
...DEFAULT_SNAPPING_PREFERENCES,
|
|
objects: false
|
|
}
|
|
expect(applyNodeEditSnap(createDrag(10, 20), 87, 0, editor, editState).x).toBe(87)
|
|
|
|
editor.state.snappingPreferences = {
|
|
geometry: false,
|
|
objects: true,
|
|
pixelGrid: false
|
|
}
|
|
expect(applyNodeEditSnap(createDrag(10, 20), 67, 37, editor, editState)).toEqual({
|
|
x: 67,
|
|
y: 37
|
|
})
|
|
})
|
|
|
|
test('snaps vector points to whole-pixel coordinates', () => {
|
|
const editor = createEditor()
|
|
editor.state.snappingPreferences = { geometry: false, objects: false, pixelGrid: true }
|
|
const pageId = editor.state.currentPageId
|
|
const vector = editor.graph.createNode('VECTOR', pageId, {
|
|
x: 0,
|
|
y: 0,
|
|
width: 100,
|
|
height: 100,
|
|
vectorNetwork: { vertices: [{ x: 10.2, y: 20.4 }], segments: [], regions: [] }
|
|
})
|
|
const editState = {
|
|
nodeId: vector.id,
|
|
vertices: [{ x: 10.2, y: 20.4 }],
|
|
segments: [],
|
|
selectedVertexIndices: new Set([0]),
|
|
selectedHandles: new Set<string>(),
|
|
hoveredHandleInfo: null
|
|
}
|
|
|
|
const snapped = applyNodeEditSnap(createDrag(10.2, 20.4), 2.2, 3.3, editor, editState)
|
|
|
|
expect(snapped.x).toBeCloseTo(1.8)
|
|
expect(snapped.y).toBeCloseTo(3.6)
|
|
expect(editor.state.snapGuides.map((guide) => guide.axis).sort()).toEqual(['x', 'y'])
|
|
})
|
|
})
|