import { useEffect, useState, useMemo, useCallback } from 'react'; import { useRackStore } from '../../store/useRackStore'; export function ConnectionLayer() { const { racks, cablingFromPortId } = 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; fromRackId: string; toRackId: 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, fromRackId: rack.id, toRackId: '' // We don't easily know the destination rack without searching }); }); }); }); }); 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; // Calculate a slight curve. If ports are close, use a tighter curve. 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); return ( {/* Thicker transparent helper for easier identification if we ever add hover interactions */} ); })} {/* Draft connection line (dashed) */} {draftStart && ( )} ); }