/** * WireRenderer Component * * Renders wires with segment-based editing: * - Click to select wire * - Hover over segments to see drag handles * - Drag horizontal segments up/down * - Drag vertical segments left/right */ import React, { useMemo, useCallback, useState, useRef, useEffect } from 'react'; import type { Wire } from '../../types/wire'; import { useSimulatorStore } from '../../store/useSimulatorStore'; import { generateWirePath } from '../../utils/wirePathGenerator'; import { computeSegments, findSegmentUnderCursor, getPathPoints, generateOrthogonalPoints, updateOrthogonalPointsForSegmentDrag, orthogonalPointsToControlPoints, type WireSegment, } from '../../utils/wireSegments'; interface WireRendererProps { wire: Wire; isSelected: boolean; } export const WireRenderer: React.FC = ({ wire, isSelected }) => { const { setSelectedWire, updateWire } = useSimulatorStore(); const [hoveredSegment, setHoveredSegment] = useState(null); const [dragState, setDragState] = useState<{ segment: WireSegment; startMousePos: { x: number; y: number }; originalOrthoPoints: Array<{ x: number; y: number }>; } | null>(null); // Local preview path during drag (for smooth performance) const [previewOrthoPoints, setPreviewOrthoPoints] = useState | null>(null); const svgRef = useRef(null); const rafRef = useRef(null); // For requestAnimationFrame // Generate SVG path (memoized for performance) // Use preview points during drag, actual wire points otherwise const path = useMemo(() => { if (previewOrthoPoints) { // Generate path from preview points during drag let pathD = `M ${previewOrthoPoints[0].x} ${previewOrthoPoints[0].y}`; for (let i = 1; i < previewOrthoPoints.length; i++) { pathD += ` L ${previewOrthoPoints[i].x} ${previewOrthoPoints[i].y}`; } return pathD; } return generateWirePath(wire); }, [wire, previewOrthoPoints]); // Compute segments (memoized) // Use preview points during drag for accurate segment positions const segments = useMemo(() => { if (previewOrthoPoints) { // During drag, compute segments from preview points const previewSegments: WireSegment[] = []; for (let i = 0; i < previewOrthoPoints.length - 1; i++) { const start = previewOrthoPoints[i]; const end = previewOrthoPoints[i + 1]; if (start.x === end.x && start.y === end.y) continue; const orientation = start.y === end.y ? 'horizontal' : 'vertical'; const length = orientation === 'horizontal' ? Math.abs(end.x - start.x) : Math.abs(end.y - start.y); previewSegments.push({ id: `${wire.id}-seg-${i}`, startPoint: start, endPoint: end, orientation, midPoint: { x: (start.x + end.x) / 2, y: (start.y + end.y) / 2, }, length, startIndex: i, endIndex: i + 1, }); } return previewSegments; } return computeSegments(wire); }, [wire, previewOrthoPoints]); // Handle wire selection const handleWireClick = useCallback( (e: React.MouseEvent) => { e.stopPropagation(); setSelectedWire(wire.id); }, [wire.id, setSelectedWire] ); // Handle segment hover const handleMouseMove = useCallback( (e: React.MouseEvent) => { if (dragState) { // Cancel previous animation frame if (rafRef.current !== null) { cancelAnimationFrame(rafRef.current); } // Handle dragging - use requestAnimationFrame for smooth updates rafRef.current = requestAnimationFrame(() => { const svg = svgRef.current?.ownerSVGElement; if (!svg) return; const svgRect = svg.getBoundingClientRect(); const mouseX = e.clientX - svgRect.left; const mouseY = e.clientY - svgRect.top; const { segment, startMousePos, originalOrthoPoints } = dragState; // Calculate offset perpendicular to segment let offset = 0; if (segment.orientation === 'horizontal') { offset = mouseY - startMousePos.y; } else { offset = mouseX - startMousePos.x; } console.log('🎯 Drag Update:', { segmentId: segment.id, orientation: segment.orientation, offset, originalPointsCount: originalOrthoPoints.length, mousePos: { x: mouseX, y: mouseY }, }); // No grid snapping during drag for smooth movement // Grid snapping will be applied on mouse up // Update orthogonal points (local preview) const newOrthoPoints = updateOrthogonalPointsForSegmentDrag( originalOrthoPoints, segment, offset ); console.log('📍 New Ortho Points:', newOrthoPoints); // Update preview state (doesn't touch the store) setPreviewOrthoPoints(newOrthoPoints); rafRef.current = null; }); } else if (isSelected) { // Update hovered segment const svg = svgRef.current?.ownerSVGElement; if (!svg) return; const svgRect = svg.getBoundingClientRect(); const mouseX = e.clientX - svgRect.left; const mouseY = e.clientY - svgRect.top; const segment = findSegmentUnderCursor(segments, mouseX, mouseY); setHoveredSegment(segment); } }, [dragState, isSelected, segments, wire, updateWire] ); const handleSegmentMouseDown = useCallback( (segment: WireSegment, e: React.MouseEvent) => { e.stopPropagation(); const svg = svgRef.current?.ownerSVGElement; if (!svg) return; const svgRect = svg.getBoundingClientRect(); const mouseX = e.clientX - svgRect.left; const mouseY = e.clientY - svgRect.top; // Get current orthogonal points const pathPoints = getPathPoints(wire); const orthoPoints = generateOrthogonalPoints(pathPoints); console.log('🚀 Start Dragging Segment:', { segmentId: segment.id, orientation: segment.orientation, segmentStart: segment.startPoint, segmentEnd: segment.endPoint, pathPointsCount: pathPoints.length, orthoPointsCount: orthoPoints.length, wireStart: wire.start, wireEnd: wire.end, wireControlPoints: wire.controlPoints, }); setDragState({ segment, startMousePos: { x: mouseX, y: mouseY }, originalOrthoPoints: orthoPoints, }); }, [wire] ); const handleMouseUp = useCallback(() => { console.log('🖱️ Mouse Up - Drag State:', { hasDragState: !!dragState, hasPreviewPoints: !!previewOrthoPoints, previewPointsCount: previewOrthoPoints?.length, wireStart: wire.start, wireEnd: wire.end, }); if (dragState && previewOrthoPoints) { // Apply grid snapping to final position const GRID_SIZE = 20; const snappedPoints = previewOrthoPoints.map((p) => ({ x: Math.round(p.x / GRID_SIZE) * GRID_SIZE, y: Math.round(p.y / GRID_SIZE) * GRID_SIZE, })); console.log('📐 Snapped Points:', snappedPoints); // Convert back to control points const newControlPoints = orthogonalPointsToControlPoints( snappedPoints, wire.start, wire.end ); console.log('🔧 New Control Points:', newControlPoints); console.log('🔌 Wire Endpoints:', { start: wire.start, end: wire.end, }); // Update store only once at the end updateWire(wire.id, { controlPoints: newControlPoints }); } // Clear drag state and preview setDragState(null); setPreviewOrthoPoints(null); }, [dragState, previewOrthoPoints, wire, updateWire]); const handleMouseLeave = useCallback(() => { if (!dragState) { setHoveredSegment(null); } }, [dragState]); // Update cursor based on hovered segment useEffect(() => { const svg = svgRef.current?.ownerSVGElement; if (!svg) return; if (dragState) { svg.style.cursor = dragState.segment.orientation === 'horizontal' ? 'ns-resize' : 'ew-resize'; } else if (hoveredSegment) { svg.style.cursor = hoveredSegment.orientation === 'horizontal' ? 'ns-resize' : 'ew-resize'; } else { svg.style.cursor = 'pointer'; } }, [hoveredSegment, dragState]); // Cleanup animation frame on unmount useEffect(() => { return () => { if (rafRef.current !== null) { cancelAnimationFrame(rafRef.current); } }; }, []); return ( {/* Invisible thick path for easier clicking */} {/* Visible wire path */} {/* Endpoint markers */} {/* Selection indicator */} {isSelected && ( )} {/* Segment interaction overlays - only when selected */} {isSelected && segments.map((segment) => ( {/* Invisible thick hitbox for easier interaction */} handleSegmentMouseDown(segment, e)} /> {/* Visual drag handle at midpoint when hovering */} {(hoveredSegment?.id === segment.id || dragState?.segment.id === segment.id) && ( <> {/* Highlight the segment */} {/* Drag handle circle */} )} ))} ); };