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.
71 lines
1.8 KiB
TypeScript
71 lines
1.8 KiB
TypeScript
import { useViewport } from '@xyflow/react'
|
|
import type { Guide } from '@/utils/alignment'
|
|
|
|
interface AlignmentGuidesProps {
|
|
guides: Guide[]
|
|
color?: string
|
|
}
|
|
|
|
/**
|
|
* SVG overlay that draws alignment guide lines on top of the React Flow canvas.
|
|
* Coordinates are in canvas (flow) space; we read the viewport transform to
|
|
* project them into screen space so lines stay locked to nodes when the user
|
|
* pans or zooms.
|
|
*/
|
|
export function AlignmentGuides({ guides, color = '#00d4ff' }: AlignmentGuidesProps) {
|
|
const { x: vx, y: vy, zoom } = useViewport()
|
|
|
|
if (guides.length === 0) return null
|
|
|
|
return (
|
|
<svg
|
|
style={{
|
|
position: 'absolute',
|
|
inset: 0,
|
|
width: '100%',
|
|
height: '100%',
|
|
pointerEvents: 'none',
|
|
zIndex: 5,
|
|
overflow: 'visible',
|
|
}}
|
|
>
|
|
{guides.map((g, i) => {
|
|
if (g.axis === 'x') {
|
|
const x = g.position * zoom + vx
|
|
const y1 = g.start * zoom + vy
|
|
const y2 = g.end * zoom + vy
|
|
return (
|
|
<line
|
|
key={`x-${i}-${g.position}`}
|
|
x1={x}
|
|
y1={y1}
|
|
x2={x}
|
|
y2={y2}
|
|
stroke={color}
|
|
strokeWidth={1}
|
|
strokeDasharray="4 3"
|
|
shapeRendering="crispEdges"
|
|
/>
|
|
)
|
|
}
|
|
const y = g.position * zoom + vy
|
|
const x1 = g.start * zoom + vx
|
|
const x2 = g.end * zoom + vx
|
|
return (
|
|
<line
|
|
key={`y-${i}-${g.position}`}
|
|
x1={x1}
|
|
y1={y}
|
|
x2={x2}
|
|
y2={y}
|
|
stroke={color}
|
|
strokeWidth={1}
|
|
strokeDasharray="4 3"
|
|
shapeRendering="crispEdges"
|
|
/>
|
|
)
|
|
})}
|
|
</svg>
|
|
)
|
|
}
|