fix(canvas): while running, the canvas is interact-only (no wire/pin/edit)

Reported: on a running circuit, clicking a pushbutton SELECTED the wire under it
instead of pressing the button — and you could still move wires / pick pins to
make connections during a run.

Root cause: component dragging was already locked during a run, but the
canvas-level onClick (wire selection via findWireNearPoint) wasn't — so a click
on a button bubbled to the canvas and selected the wire. The button press itself
fired (shadow DOM), but the wire-select made it feel broken.

Gate every EDIT interaction on the existing interactionRunning predicate while
keeping part interaction (buttons/switches/pots) and pan/zoom:
- canvas onClick wire-selection + onDoubleClick waypoint-insert
- wire segment / waypoint drag handles (mouse + touch)
- pin-click wire creation
- touch tap wire-selection
- hide the PinOverlay (was gated on !running, so board-less runs still showed
  clickable pins) and skip wire-hover highlighting while running
- clear any wire/component selection when a run starts so leftover handles don't
  linger over the live circuit

Component drag + property dialog were already gated on interactionRunning; this
extends the same 'freeze to edit, run to interact' model to wires and pins.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
David Montero 2026-06-04 19:14:51 +02:00
parent dd322b27e3
commit 5f04b42bdd
1 changed files with 30 additions and 8 deletions

View File

@ -286,6 +286,16 @@ export const SimulatorCanvas = ({ headerSlot }: SimulatorCanvasProps = {}) => {
runningRef.current = running;
const interactionRunningRef = useRef(interactionRunning);
interactionRunningRef.current = interactionRunning;
// When a run starts the canvas becomes interact-only — drop any edit
// selection so leftover wire/segment handles don't linger over the circuit.
useEffect(() => {
if (interactionRunning) {
setSelectedWire(null);
setSelectedComponentId(null);
}
}, [interactionRunning, setSelectedWire]);
const componentsRef = useRef(components);
componentsRef.current = components;
const boardPositionRef = useRef(boardPosition);
@ -941,7 +951,8 @@ export const SimulatorCanvas = ({ headerSlot }: SimulatorCanvasProps = {}) => {
}
// ── Short tap on empty canvas: wire selection + double-tap inserts waypoint ──
if (isShortTap) {
// Disabled while running — the canvas is interact-only then.
if (isShortTap && !interactionRunningRef.current) {
const now = Date.now();
const world = toWorld(changed.clientX, changed.clientY);
const baseThreshold = isTouchDeviceRef.current ? 20 : 8;
@ -1434,8 +1445,9 @@ export const SimulatorCanvas = ({ headerSlot }: SimulatorCanvasProps = {}) => {
return;
}
// Wire hover detection (when not dragging anything)
if (!draggedComponentId) {
// Wire hover detection (when not dragging anything). Skipped while running —
// wires aren't selectable then, so they shouldn't highlight as hoverable.
if (!draggedComponentId && !interactionRunningRef.current) {
const world = toWorld(e.clientX, e.clientY);
const threshold = 8 / zoomRef.current;
const wire = findWireNearPoint(wiresRef.current, world.x, world.y, threshold);
@ -1614,6 +1626,7 @@ export const SimulatorCanvas = ({ headerSlot }: SimulatorCanvasProps = {}) => {
(e: React.MouseEvent, segIndex: number) => {
e.stopPropagation();
e.preventDefault();
if (interactionRunningRef.current) return; // interact-only while running
if (!selectedWireId) return;
const wire = wiresRef.current.find((w) => w.id === selectedWireId);
if (!wire) return;
@ -1636,6 +1649,7 @@ export const SimulatorCanvas = ({ headerSlot }: SimulatorCanvasProps = {}) => {
const handleHandleTouchStart = useCallback(
(e: React.TouchEvent, segIndex: number) => {
e.stopPropagation();
if (interactionRunningRef.current) return;
if (!selectedWireId) return;
const wire = wiresRef.current.find((w) => w.id === selectedWireId);
if (!wire) return;
@ -1659,6 +1673,7 @@ export const SimulatorCanvas = ({ headerSlot }: SimulatorCanvasProps = {}) => {
(e: React.MouseEvent, waypointIndex: number) => {
e.stopPropagation();
e.preventDefault();
if (interactionRunningRef.current) return; // interact-only while running
if (!selectedWireId) return;
const wire = wiresRef.current.find((w) => w.id === selectedWireId);
if (!wire) return;
@ -1676,6 +1691,7 @@ export const SimulatorCanvas = ({ headerSlot }: SimulatorCanvasProps = {}) => {
const handleWaypointTouchStart = useCallback(
(e: React.TouchEvent, waypointIndex: number) => {
e.stopPropagation();
if (interactionRunningRef.current) return;
if (!selectedWireId) return;
const wire = wiresRef.current.find((w) => w.id === selectedWireId);
if (!wire) return;
@ -1723,6 +1739,8 @@ export const SimulatorCanvas = ({ headerSlot }: SimulatorCanvasProps = {}) => {
// Wire creation via pin clicks
const handlePinClick = (componentId: string, pinName: string, x: number, y: number) => {
// No making connections while the simulation runs — interact-only.
if (interactionRunningRef.current) return;
// Close property dialog when starting wire creation
if (showPropertyDialog) {
setShowPropertyDialog(false);
@ -1928,7 +1946,7 @@ export const SimulatorCanvas = ({ headerSlot }: SimulatorCanvasProps = {}) => {
isSelected={isSelected}
onMouseDown={(e) => handleComponentMouseDown(component.id, e)}
/>
{!running && (
{!interactionRunning && (
<PinOverlay
componentId={component.id}
componentX={component.x}
@ -1984,8 +2002,8 @@ export const SimulatorCanvas = ({ headerSlot }: SimulatorCanvasProps = {}) => {
}}
/>
{/* Pin overlay for wire creation - hide when running */}
{!running && (
{/* Pin overlay for wire creation - hide while interacting/running */}
{!interactionRunning && (
<PinOverlay
componentId={component.id}
componentX={component.x}
@ -2383,6 +2401,10 @@ export const SimulatorCanvas = ({ headerSlot }: SimulatorCanvasProps = {}) => {
segmentDragJustCommittedRef.current = false;
return;
}
// While the simulation runs the canvas is interact-only: a click on
// a button must press it (its own handler), not select the wire
// underneath it for editing.
if (interactionRunning) return;
// Wire selection via canvas-level hit detection
const world = toWorld(e.clientX, e.clientY);
const threshold = 8 / zoomRef.current;
@ -2395,7 +2417,7 @@ export const SimulatorCanvas = ({ headerSlot }: SimulatorCanvasProps = {}) => {
}
}}
onDoubleClick={(e) => {
if (wireInProgress) return;
if (wireInProgress || interactionRunning) return;
const world = toWorld(e.clientX, e.clientY);
const threshold = 8 / zoomRef.current;
const wire = findWireNearPoint(wiresRef.current, world.x, world.y, threshold);
@ -2533,7 +2555,7 @@ export const SimulatorCanvas = ({ headerSlot }: SimulatorCanvasProps = {}) => {
the right-click menu the floating bar covered nearby pins
and intercepted clicks on buttons during simulation)
- while the simulator is running (canvas is read-only). */}
{!wireInProgress && isTouchDevice && !running &&
{!wireInProgress && isTouchDevice && !interactionRunning &&
(() => {
if (selectedWireId) {
const wire = wires.find((w) => w.id === selectedWireId);