feat: add pin hover tooltip during wire aiming and implement null-safety checks for wire rendering
parent
1973b0adbc
commit
a11dd87d5a
|
|
@ -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<string | null>(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 = () => {
|
|||
<circle cx={aimPosition.x} cy={aimPosition.y} r="2" fill="rgba(0, 200, 255, 0.9)" />
|
||||
</svg>
|
||||
)}
|
||||
|
||||
{/* Aim Hover Tooltip for Touch Devices */}
|
||||
{wireAiming && aimPosition && aimHoveredPinName && (
|
||||
<div style={{
|
||||
position: 'absolute',
|
||||
left: aimPosition.x + 15,
|
||||
top: aimPosition.y - 20,
|
||||
backgroundColor: 'rgba(0, 0, 0, 0.8)',
|
||||
color: 'white',
|
||||
padding: '4px 8px',
|
||||
borderRadius: '4px',
|
||||
fontSize: '12px',
|
||||
pointerEvents: 'none',
|
||||
zIndex: 45,
|
||||
whiteSpace: 'nowrap',
|
||||
boxShadow: '0 2px 4px rgba(0,0,0,0.3)',
|
||||
border: '1px solid rgba(255,255,255,0.1)'
|
||||
}}>
|
||||
{aimHoveredPinName}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Wire creation mode banner — visible on both desktop and mobile */}
|
||||
|
|
|
|||
|
|
@ -55,19 +55,23 @@ export const WireLayer: React.FC<WireLayerProps> = ({
|
|||
zIndex: 35,
|
||||
}}
|
||||
>
|
||||
{wires.map((wire) => (
|
||||
<WireRenderer
|
||||
key={wire.id}
|
||||
wire={wire}
|
||||
isSelected={wire.id === selectedWireId}
|
||||
isHovered={wire.id === hoveredWireId}
|
||||
overridePath={
|
||||
segmentDragPreview?.wireId === wire.id
|
||||
? segmentDragPreview.overridePath
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
))}
|
||||
{wires.map((wire) => {
|
||||
// Skip null/undefined wires (can happen during circuit loading)
|
||||
if (!wire) return null;
|
||||
return (
|
||||
<WireRenderer
|
||||
key={wire.id}
|
||||
wire={wire}
|
||||
isSelected={wire.id === selectedWireId}
|
||||
isHovered={wire.id === hoveredWireId}
|
||||
overridePath={
|
||||
segmentDragPreview?.wireId === wire.id
|
||||
? segmentDragPreview.overridePath
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
|
||||
{/* Segment handles for the selected wire — scaled inversely to zoom for constant screen size */}
|
||||
{segmentHandles.map((handle) => {
|
||||
|
|
|
|||
|
|
@ -24,6 +24,9 @@ export const WireRenderer: React.FC<WireRendererProps> = ({
|
|||
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);
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue