fix(canvas): board-less SPICE switches toggle on click instead of opening property dialog
In digital / analog board-less examples the user clicks a slide-switch
or pushbutton expecting it to flip its state. Until this commit the
component property dialog opened instead and the click never reached
the wokwi-element underneath, so:
- The user couldn't change switch state through the canvas at all.
- With no state change the SPICE solver kept the old netlist, and
every downstream LED stayed dark — the symptom that read as
"voltages change but no LED lights".
Root cause was the gating: SimulatorCanvas only suppressed the
property dialog when `useSimulatorStore.running` was true, but that
flag is bound to an MCU's start/stop. Board-less circuits have no MCU
to start so `running` is permanently false, even when the SPICE engine
has been live since the example loaded.
New derived flag `interactionRunning = running || (boards.length === 0
&& !electricalPaused)` — true whenever the user is in an "interactive"
session, MCU or SPICE-only. Used in three click-handling paths:
- SimulatorCanvas mouse-up handler: dialog is suppressed and the
click falls through to the wokwi-element (line 1395).
- SimulatorCanvas touch-start passthrough: same for touch (line 474).
- SimulatorCanvas touch-end short-tap: same for tap (line 774).
Also propagated to DynamicComponent so the cursor becomes pointer (not
move) for interactive parts in board-less mode — visual cue that the
user can click instead of just drag.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
55581690f9
commit
083e0df732
|
|
@ -14,6 +14,7 @@
|
|||
import React, { useRef, useEffect, useCallback } from 'react';
|
||||
import type { ComponentMetadata } from '../types/component-metadata';
|
||||
import { useSimulatorStore } from '../store/useSimulatorStore';
|
||||
import { useElectricalStore } from '../store/useElectricalStore';
|
||||
import { PartSimulationRegistry } from '../simulation/parts';
|
||||
import { isBoardComponent, boardPinToNumber } from '../utils/boardPinMapping';
|
||||
|
||||
|
|
@ -58,6 +59,14 @@ export const DynamicComponent: React.FC<DynamicComponentProps> = ({
|
|||
const handleComponentEvent = useSimulatorStore((s) => s.handleComponentEvent);
|
||||
const running = useSimulatorStore((s) => s.running);
|
||||
const simulator = useSimulatorStore((s) => s.simulator);
|
||||
// Board-less SPICE circuits (digital / analog gallery) have no MCU to
|
||||
// run, so `running` is always false — but interactive parts like
|
||||
// slide-switches and pushbuttons should still show a pointer cursor
|
||||
// and let the user click them. We treat board-less + un-paused as
|
||||
// "interactive" so the cursor + dialog gating mirror the MCU mode.
|
||||
const boardCount = useSimulatorStore((s) => s.boards.length);
|
||||
const electricalPaused = useElectricalStore((s) => s.paused);
|
||||
const interactionRunning = running || (boardCount === 0 && !electricalPaused);
|
||||
// hexEpoch increments each time a new hex is loaded, triggering a fresh
|
||||
// attachEvents call (and re-registration of I2C devices on the new bus).
|
||||
// We intentionally do NOT depend on `running` so that I2C displays and
|
||||
|
|
@ -350,7 +359,7 @@ export const DynamicComponent: React.FC<DynamicComponentProps> = ({
|
|||
position: 'absolute',
|
||||
left: `${x}px`,
|
||||
top: `${y}px`,
|
||||
cursor: running && isInteractive ? 'pointer' : 'move',
|
||||
cursor: interactionRunning && isInteractive ? 'pointer' : 'move',
|
||||
border: isSelected ? '2px dashed #007acc' : '2px solid transparent',
|
||||
borderRadius: '4px',
|
||||
padding: '4px',
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { useSimulatorStore, getEsp32Bridge } from '../../store/useSimulatorStore';
|
||||
import { useElectricalStore } from '../../store/useElectricalStore';
|
||||
import React, { useEffect, useState, useRef, useCallback } from 'react';
|
||||
import { createPortal } from 'react-dom';
|
||||
import { Undo2, Redo2 } from 'lucide-react';
|
||||
|
|
@ -238,10 +239,23 @@ export const SimulatorCanvas = ({ headerSlot }: SimulatorCanvasProps = {}) => {
|
|||
const panRef = useRef({ x: 0, y: 0 });
|
||||
const zoomRef = useRef(1);
|
||||
|
||||
// Board-less SPICE circuits (analog / digital examples with no MCU on
|
||||
// the canvas) have no concept of a board to "start", so `running` is
|
||||
// always false. But the simulation IS effectively live the moment the
|
||||
// engine has solved at least once — switches and buttons should toggle
|
||||
// their own state on click instead of opening the property dialog.
|
||||
// We treat board-less + un-paused as "interaction-running" so the same
|
||||
// gating logic that suppresses the dialog for MCU-running mode also
|
||||
// covers board-less circuits.
|
||||
const electricalPaused = useElectricalStore((s) => s.paused);
|
||||
const interactionRunning = running || (boards.length === 0 && !electricalPaused);
|
||||
|
||||
// Refs that mirror state/props for use inside touch event closures
|
||||
// (touch listeners are added imperatively and can't access current React state)
|
||||
const runningRef = useRef(running);
|
||||
runningRef.current = running;
|
||||
const interactionRunningRef = useRef(interactionRunning);
|
||||
interactionRunningRef.current = interactionRunning;
|
||||
const componentsRef = useRef(components);
|
||||
componentsRef.current = components;
|
||||
const boardPositionRef = useRef(boardPosition);
|
||||
|
|
@ -471,7 +485,9 @@ export const SimulatorCanvas = ({ headerSlot }: SimulatorCanvasProps = {}) => {
|
|||
// ── 2. Interactive web component during simulation → let browser synthesize mouse events ──
|
||||
// (potentiometer knobs, button presses, etc. need mousedown/mouseup synthesis)
|
||||
// touch-action:none on .canvas-content already prevents browser scroll/zoom.
|
||||
if (runningRef.current) {
|
||||
// Board-less SPICE circuits piggy-back on the same path so a tap on
|
||||
// a slide-switch reaches the wokwi element and triggers its toggle.
|
||||
if (interactionRunningRef.current) {
|
||||
const webComp = target?.closest('.web-component-container');
|
||||
if (webComp) {
|
||||
touchPassthroughRef.current = true;
|
||||
|
|
@ -767,11 +783,13 @@ export const SimulatorCanvas = ({ headerSlot }: SimulatorCanvasProps = {}) => {
|
|||
}
|
||||
} else if (touchId !== '__board__') {
|
||||
// Short tap on component → open property dialog or sensor panel.
|
||||
// While the simulator is running, components stay interactive —
|
||||
// only sensor panels may open; property editing is disabled.
|
||||
// While the simulator is running (MCU or board-less SPICE),
|
||||
// components stay interactive — only sensor panels may open;
|
||||
// property editing is disabled so the tap reaches the
|
||||
// wokwi-element underneath and toggles it.
|
||||
const component = componentsRef.current.find((c) => c.id === touchId);
|
||||
if (component) {
|
||||
if (runningRef.current) {
|
||||
if (interactionRunningRef.current) {
|
||||
if (SENSOR_CONTROLS[component.metadataId] !== undefined) {
|
||||
setSensorControlComponentId(touchId);
|
||||
setSensorControlMetadataId(component.metadataId);
|
||||
|
|
@ -1392,11 +1410,13 @@ export const SimulatorCanvas = ({ headerSlot }: SimulatorCanvasProps = {}) => {
|
|||
} else if (draggedComponentId !== '__board__') {
|
||||
const component = components.find((c) => c.id === draggedComponentId);
|
||||
if (component) {
|
||||
if (running) {
|
||||
// During simulation only sensor panels open on click — every
|
||||
// other component is interactive (pushbutton, switch, pot, …)
|
||||
// and must handle its own clicks, so we suppress the property
|
||||
// dialog entirely.
|
||||
if (interactionRunning) {
|
||||
// During simulation (MCU running OR board-less SPICE active)
|
||||
// only sensor panels open on click — every other component
|
||||
// is interactive (pushbutton, switch, pot, …) and must
|
||||
// handle its own clicks, so we suppress the property
|
||||
// dialog entirely. This is the path that also unblocks
|
||||
// wokwi-slide-switch toggling in the digital examples.
|
||||
if (SENSOR_CONTROLS[component.metadataId] !== undefined) {
|
||||
setSensorControlComponentId(draggedComponentId);
|
||||
setSensorControlMetadataId(component.metadataId);
|
||||
|
|
|
|||
Loading…
Reference in New Issue