diff --git a/frontend/src/components/simulator/SimulatorCanvas.tsx b/frontend/src/components/simulator/SimulatorCanvas.tsx index 3e33bd9..8d81f9d 100644 --- a/frontend/src/components/simulator/SimulatorCanvas.tsx +++ b/frontend/src/components/simulator/SimulatorCanvas.tsx @@ -230,6 +230,7 @@ export const SimulatorCanvas = () => { const currentTouchRef = useRef({ x: 0, y: 0 }); // latest screen position for timeout callback const [wireAiming, setWireAiming] = useState(false); // React state for rendering (crosshair visible) const [aimPosition, setAimPosition] = useState<{ x: number; y: number } | null>(null); // world coords of crosshair + const [aimHoveredPinName, setAimHoveredPinName] = useState(null); // Name of pin hovered by crosshair // Convert viewport coords to world (canvas) coords const toWorld = useCallback((screenX: number, screenY: number) => { @@ -430,6 +431,11 @@ export const SimulatorCanvas = () => { const aimX = world.x; const aimY = world.y + AIMING_OFFSET_Y; setAimPosition({ x: aimX, y: aimY }); + + const snapDist = AIMING_PIN_SNAP_DIST / zoomRef.current; + const nearPin = findNearestPin(aimX, aimY, snapDist); + setAimHoveredPinName(nearPin ? nearPin.pinName : null); + useSimulatorStore.getState().updateWireInProgress(aimX, aimY); }, AIMING_LONG_PRESS_MS); } @@ -495,7 +501,13 @@ export const SimulatorCanvas = () => { setWireAiming(true); if (navigator.vibrate) navigator.vibrate(30); const world = toWorld(currentTouchRef.current.x, currentTouchRef.current.y); - setAimPosition({ x: world.x, y: world.y + AIMING_OFFSET_Y }); + const aimX = world.x; + const aimY = world.y + AIMING_OFFSET_Y; + setAimPosition({ x: aimX, y: aimY }); + + const snapDist = AIMING_PIN_SNAP_DIST / zoomRef.current; + const nearPin = findNearestPin(aimX, aimY, snapDist); + setAimHoveredPinName(nearPin ? nearPin.pinName : null); }, AIMING_LONG_PRESS_MS); } isPanningRef.current = true; @@ -572,6 +584,11 @@ export const SimulatorCanvas = () => { const aimX = world.x; const aimY = world.y + AIMING_OFFSET_Y; setAimPosition({ x: aimX, y: aimY }); + + const snapDist = AIMING_PIN_SNAP_DIST / zoomRef.current; + const nearPin = findNearestPin(aimX, aimY, snapDist); + setAimHoveredPinName(nearPin ? nearPin.pinName : null); + if (aimPhase === 'aiming_end') { useSimulatorStore.getState().updateWireInProgress(aimX, aimY); } @@ -687,7 +704,7 @@ export const SimulatorCanvas = () => { const snapDist = AIMING_PIN_SNAP_DIST / zoomRef.current; const nearPin = findNearestPin(aimX, aimY, snapDist); - setWireAiming(false); setAimPosition(null); + setWireAiming(false); setAimPosition(null); setAimHoveredPinName(null); if (nearPin) { // Start wire from this pin startWireCreation( @@ -711,7 +728,7 @@ export const SimulatorCanvas = () => { const snapDist = AIMING_PIN_SNAP_DIST / zoomRef.current; const nearPin = findNearestPin(aimX, aimY, snapDist); - setWireAiming(false); setAimPosition(null); + setWireAiming(false); setAimPosition(null); setAimHoveredPinName(null); if (nearPin) { // Finish wire at this pin finishWireCreation({ componentId: nearPin.componentId, pinName: nearPin.pinName, x: nearPin.x, y: nearPin.y }); @@ -1723,6 +1740,27 @@ export const SimulatorCanvas = () => { )} + + {/* Aim Hover Tooltip for Touch Devices */} + {wireAiming && aimPosition && aimHoveredPinName && ( +
+ {aimHoveredPinName} +
+ )} {/* Wire creation mode banner — visible on both desktop and mobile */} diff --git a/frontend/src/components/simulator/WireLayer.tsx b/frontend/src/components/simulator/WireLayer.tsx index 55b199c..370c689 100644 --- a/frontend/src/components/simulator/WireLayer.tsx +++ b/frontend/src/components/simulator/WireLayer.tsx @@ -55,19 +55,23 @@ export const WireLayer: React.FC = ({ zIndex: 35, }} > - {wires.map((wire) => ( - - ))} + {wires.map((wire) => { + // Skip null/undefined wires (can happen during circuit loading) + if (!wire) return null; + return ( + + ); + })} {/* Segment handles for the selected wire — scaled inversely to zoom for constant screen size */} {segmentHandles.map((handle) => { diff --git a/frontend/src/components/simulator/WireRenderer.tsx b/frontend/src/components/simulator/WireRenderer.tsx index 2ee8865..9e75151 100644 --- a/frontend/src/components/simulator/WireRenderer.tsx +++ b/frontend/src/components/simulator/WireRenderer.tsx @@ -24,6 +24,9 @@ export const WireRenderer: React.FC = ({ previewWaypoints, overridePath, }) => { + // Guard against null/undefined wire + if (!wire || !wire.start || !wire.end) return null; + const waypoints = previewWaypoints ?? wire.waypoints; const path = overridePath ?? generateOrthogonalPath(wire.start, waypoints, wire.end); diff --git a/frontend/src/services/EmbedBridge.ts b/frontend/src/services/EmbedBridge.ts index d6e62bf..13ed86a 100644 --- a/frontend/src/services/EmbedBridge.ts +++ b/frontend/src/services/EmbedBridge.ts @@ -92,7 +92,9 @@ class EmbedBridge { // Set wires if provided if (data.wires && Array.isArray(data.wires)) { - store.setWires(data.wires as never[]); + // Filter out null/undefined wires to prevent runtime errors + const validWires = data.wires.filter((w) => w && w.start && w.end && w.id) as never[]; + store.setWires(validWires); } break; }