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.
44 lines
1.5 KiB
TypeScript
44 lines
1.5 KiB
TypeScript
// Persisted client-side settings for alignment guides.
|
|
// Kept in localStorage (per-user UI preference, not canvas data).
|
|
// Same-tab updates propagate via a CustomEvent so the drag hook and the
|
|
// settings panel can stay in sync without a global store.
|
|
|
|
export interface AlignmentSettings {
|
|
enabled: boolean
|
|
threshold: number
|
|
}
|
|
|
|
export const DEFAULT_ALIGNMENT_SETTINGS: AlignmentSettings = { enabled: true, threshold: 6 }
|
|
|
|
const KEY = 'homelable.alignmentGuides'
|
|
const EVENT = 'homelable:alignment-settings-changed'
|
|
|
|
export function readAlignmentSettings(): AlignmentSettings {
|
|
try {
|
|
const raw = localStorage.getItem(KEY)
|
|
if (!raw) return DEFAULT_ALIGNMENT_SETTINGS
|
|
const parsed = JSON.parse(raw) as Partial<AlignmentSettings>
|
|
return {
|
|
enabled: parsed.enabled ?? DEFAULT_ALIGNMENT_SETTINGS.enabled,
|
|
threshold: typeof parsed.threshold === 'number' ? parsed.threshold : DEFAULT_ALIGNMENT_SETTINGS.threshold,
|
|
}
|
|
} catch {
|
|
return DEFAULT_ALIGNMENT_SETTINGS
|
|
}
|
|
}
|
|
|
|
export function writeAlignmentSettings(s: AlignmentSettings): void {
|
|
try {
|
|
localStorage.setItem(KEY, JSON.stringify(s))
|
|
window.dispatchEvent(new CustomEvent<AlignmentSettings>(EVENT, { detail: s }))
|
|
} catch {
|
|
/* quota / SSR */
|
|
}
|
|
}
|
|
|
|
export function subscribeAlignmentSettings(listener: (s: AlignmentSettings) => void): () => void {
|
|
const handler = (e: Event) => listener((e as CustomEvent<AlignmentSettings>).detail)
|
|
window.addEventListener(EVENT, handler)
|
|
return () => window.removeEventListener(EVENT, handler)
|
|
}
|