004a8f19c1
Draw.io / Figma style: while dragging a node, show dashed cyan guide lines when its edges (left / center / right / top / middle / bottom) align with another node's, and snap the position to the matched line within a configurable threshold. - utils/alignment.ts: pure snap math, returns delta + guide segments. Same-size boxes show all aligned guides simultaneously. - canvas/AlignmentGuides.tsx: SVG overlay locked to the React Flow viewport (panned/zoomed correctly). - hooks/useAlignmentGuides.ts: wires onNodeDrag/onNodeDragStop, applies snap via setNodes, listens for Alt to temporarily disable. - utils/alignmentSettings.ts: localStorage-backed prefs (enabled, threshold 2-16px) with a tiny CustomEvent pub-sub so the SettingsPanel and the drag hook stay in sync without a global store. - Sidebar settings panel: toggle + threshold slider. Multi-selection drag uses the union bounding box. Children with parentId are skipped for v1 to avoid mixing absolute and parent-relative coordinates. Hold Alt to escape snap.
48 lines
1.9 KiB
TypeScript
48 lines
1.9 KiB
TypeScript
import { describe, it, expect, vi } from 'vitest'
|
|
import { render } from '@testing-library/react'
|
|
import { AlignmentGuides } from '../AlignmentGuides'
|
|
import type { Guide } from '@/utils/alignment'
|
|
|
|
vi.mock('@xyflow/react', () => ({
|
|
useViewport: () => ({ x: 50, y: 100, zoom: 2 }),
|
|
}))
|
|
|
|
describe('AlignmentGuides', () => {
|
|
it('renders nothing when no guides', () => {
|
|
const { container } = render(<AlignmentGuides guides={[]} />)
|
|
expect(container.querySelector('svg')).toBeNull()
|
|
})
|
|
|
|
it('projects an x-axis guide through the viewport transform', () => {
|
|
const guides: Guide[] = [{ axis: 'x', position: 100, start: 0, end: 200 }]
|
|
const { container } = render(<AlignmentGuides guides={guides} />)
|
|
const line = container.querySelector('line')!
|
|
// x = position * zoom + vx → 100*2 + 50 = 250
|
|
expect(line.getAttribute('x1')).toBe('250')
|
|
expect(line.getAttribute('x2')).toBe('250')
|
|
// y1 = start * zoom + vy → 0*2 + 100 = 100; y2 = 200*2 + 100 = 500
|
|
expect(line.getAttribute('y1')).toBe('100')
|
|
expect(line.getAttribute('y2')).toBe('500')
|
|
})
|
|
|
|
it('projects a y-axis guide horizontally', () => {
|
|
const guides: Guide[] = [{ axis: 'y', position: 50, start: 10, end: 60 }]
|
|
const { container } = render(<AlignmentGuides guides={guides} />)
|
|
const line = container.querySelector('line')!
|
|
// y = 50*2 + 100 = 200; x1 = 10*2 + 50 = 70; x2 = 60*2 + 50 = 170
|
|
expect(line.getAttribute('y1')).toBe('200')
|
|
expect(line.getAttribute('y2')).toBe('200')
|
|
expect(line.getAttribute('x1')).toBe('70')
|
|
expect(line.getAttribute('x2')).toBe('170')
|
|
})
|
|
|
|
it('renders one line per guide', () => {
|
|
const guides: Guide[] = [
|
|
{ axis: 'x', position: 100, start: 0, end: 200 },
|
|
{ axis: 'y', position: 50, start: 10, end: 60 },
|
|
]
|
|
const { container } = render(<AlignmentGuides guides={guides} />)
|
|
expect(container.querySelectorAll('line')).toHaveLength(2)
|
|
})
|
|
})
|