import { useEffect, useState, useMemo, useCallback } from 'react'; import { useRackStore } from '../../store/useRackStore'; import { cn } from '../../lib/utils'; export function ConnectionLayer() { const { racks, cablingFromPortId, setActiveConfigConnectionId } = useRackStore(); const [coords, setCoords] = useState>({}); const [mousePos, setMousePos] = useState({ x: 0, y: 0 }); // Update port coordinates const updateCoords = useCallback(() => { const newCoords: Record = {}; const dots = document.querySelectorAll('[data-port-id]'); // Find the closest scrollable parent that defines our coordinate system // RackPlanner has overflow-auto on the canvas wrapper const canvas = document.querySelector('.rack-planner-canvas'); if (!canvas) return; const canvasRect = canvas.getBoundingClientRect(); dots.forEach((dot) => { const portId = (dot as HTMLElement).dataset.portId; if (!portId) return; const rect = dot.getBoundingClientRect(); // Coordinate is relative to the canvas origin, including its scroll position newCoords[portId] = { x: rect.left + rect.width / 2 - canvasRect.left + canvas.scrollLeft, y: rect.top + rect.height / 2 - canvasRect.top + canvas.scrollTop, }; }); setCoords(newCoords); }, []); useEffect(() => { updateCoords(); // Re-calculate on window resize or when racks change (modules move) window.addEventListener('resize', updateCoords); // Also re-calculate if the user scrolls (though ideally lines are pinned to the canvas) // Actually, if SVG is INSIDE the scrollable container, we don't need scroll adjustment. // Use a MutationObserver to detect DOM changes (like modules being added/moved) const observer = new MutationObserver(() => { // Small debounce or check if it was our OWN SVG that changed updateCoords(); }); const canvas = document.querySelector('.rack-planner-canvas'); if (canvas) { // DO NOT observe the entire subtree with attributes if it includes the ConnectionLayer // Instead, just watch for module layout changes observer.observe(canvas, { childList: true, subtree: true }); } return () => { window.removeEventListener('resize', updateCoords); observer.disconnect(); }; }, [racks, updateCoords]); // Track mouse for "draft" connection (only while actively cabling) useEffect(() => { if (!cablingFromPortId) return; const onMouseMove = (e: MouseEvent) => { const canvas = document.querySelector('.rack-planner-canvas'); if (!canvas) return; const rect = canvas.getBoundingClientRect(); setMousePos({ x: e.clientX - rect.left + canvas.scrollLeft, y: e.clientY - rect.top + canvas.scrollTop, }); }; window.addEventListener('mousemove', onMouseMove); return () => window.removeEventListener('mousemove', onMouseMove); }, [cablingFromPortId]); const connections = useMemo(() => { const conns: { id: string; from: string; to: string; color?: string; edgeType?: string }[] = []; racks.forEach((rack) => { rack.modules.forEach((mod) => { mod.ports.forEach((port) => { port.sourceConnections?.forEach((c) => { conns.push({ id: c.id, from: c.fromPortId, to: c.toPortId, color: c.color, edgeType: c.edgeType, }); }); }); }); }); return conns; }, [racks]); // Decide if we should show draft line const draftStart = cablingFromPortId ? coords[cablingFromPortId] : null; return ( {/* Existing connections */} {connections.map((conn) => { const start = coords[conn.from]; const end = coords[conn.to]; if (!start || !end) return null; let d = ''; if (conn.edgeType === 'straight') { d = `M ${start.x} ${start.y} L ${end.x} ${end.y}`; } else if (conn.edgeType === 'step') { const midX = start.x + (end.x - start.x) / 2; d = `M ${start.x} ${start.y} L ${midX} ${start.y} L ${midX} ${end.y} L ${end.x} ${end.y}`; } else { // default bezier const dx = Math.abs(end.x - start.x); const dy = Math.abs(end.y - start.y); const distance = Math.sqrt(dx*dx + dy*dy); const curvature = Math.min(100, distance / 3); d = `M ${start.x} ${start.y} C ${start.x + curvature} ${start.y}, ${end.x - curvature} ${end.y}, ${end.x} ${end.y}`; } return ( {/* Thicker transparent helper for easier identification if we ever add hover interactions */} { if (e.shiftKey) { e.stopPropagation(); setActiveConfigConnectionId(conn.id); } }} /> ); })} {/* Draft connection line (dashed) */} {draftStart && ( )} ); }