velxio/docs/wiki/component-interaction.md

5.6 KiB

Component Interaction Model

How the canvas decides whether a mouse click on a component opens its property dialog, starts a drag-to-rearrange, or is forwarded to the wokwi web component's own pointer logic (knob rotation, button press, joystick stick drag, etc.).

TL;DR

When a user clicks on a component on the simulator canvas, exactly one of three things happens:

Surface clicked Result
Wrapper border / padding / non-interactive area of any component Canvas owns the click. Single click = open property dialog; mousedown + threshold movement = drag the component to reposition it.
A wokwi web component whose tag is in the pointer-owning whitelist below Wokwi component owns the click. Rotates the knob, presses the button, slides the switch, etc.
A wokwi web component NOT in the whitelist (sensor, display, LED, resistor, etc.) Same as the wrapper case — canvas owns the click.

The whitelist

The exhaustive list lives in frontend/src/components/DynamicComponent.tsx inside handleMouseDown. As of this writing it covers:

const ownsPointer =
  interactionRunning &&
  (tag === 'wokwi-pushbutton'
   || tag === 'wokwi-pushbutton-6mm'
   || tag === 'wokwi-potentiometer'
   || tag === 'wokwi-slide-potentiometer'
   || tag === 'wokwi-slide-switch'
   || tag === 'wokwi-dip-switch-8'
   || tag === 'wokwi-analog-joystick'
   || tag === 'wokwi-ky-040'
   || tag === 'wokwi-membrane-keypad'
   || tag === 'wokwi-rotary-dialer');

If you wire a NEW wokwi component that depends on its own internal pointerdown / pointermove / pointerup handlers, append its custom element tag here. Otherwise the canvas's e.stopPropagation() in the capture phase will swallow the pointer event before the wokwi component ever sees it — the knob will not rotate, the button won't report a press, the joystick stick won't move.

Why the gate is opt-in (whitelist) rather than opt-out

The gate used to be heuristic: "if the component has a PartSimulationRegistry handler (isInteractive), let the wokwi component own the pointerdown." That heuristic was too broad — DHT22, HC-SR04, NTC, photoresistor, LED all register attachEvents to bridge the SPICE / sensor-update layers, but visually they have NO internal pointer logic. The heuristic caused clicks on those components to be silently swallowed by the wokwi shadow DOM (which did nothing with them) → property dialog never opened → users couldn't change sensor values.

A tight whitelist is verifiable in one read and trivial to extend.

Property dialog flow

For non-pointer-owning components, the mousedown bubbles to SimulatorCanvas which:

  1. Tracks the down position and starts a movement-threshold timer.
  2. If the user releases the button without moving past ~3 px ⇒ treat as click ⇒ open <ComponentPropertyDialog> for that component.
  3. If the user moves past the threshold ⇒ treat as drag ⇒ reposition the component on the canvas grid.

The property dialog renders a SensorControlPanel (for sensors that have SENSOR_CONTROL_DEFS) or a generic property editor. The slider dispatches dispatchSensorUpdate(componentId, { … }) which routes via SensorUpdateRegistry to the part's attachEvents callback. That callback updates the AVR ADC (setAdcVoltage) AND emits a PROPERTY_CHANGE_EVENT so the SPICE netlist gets rebuilt with the new value (e.g. NTC resistance recalculated from the new temperature).

Live-simulation vs. edit-mode

The whitelist is only consulted when interactionRunning is true — either the MCU sim is running or this is a board-less SPICE-only circuit with the electrical simulation un-paused. While the user is in "edit mode" (sim stopped) the canvas always owns clicks so the user can arrange components without accidentally rotating knobs.

Test infrastructure note

CDP-synthesized PointerEvent carries isTrusted = false. Lit's gesture machinery (used by wokwi-elements) requires real OS-issued trusted events. Automated tests that need to rotate a potentiometer or press a button cannot do it via dispatchEvent. Two workarounds:

  • For pushbuttons: dispatch the custom button-press / button-release events directly on the wokwi-pushbutton DOM element. The Velxio simulation layer reacts to those.
  • For rotary / slider components: reach into the wokwi shadow root, find the hidden <input type="range">, set its value via the native HTMLInputElement setter, then dispatch a bubbling input event. Example:
const slider = document.querySelector('wokwi-potentiometer')
  .shadowRoot.querySelector('input[type="range"]');
const set = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, 'value').set;
set.call(slider, '512');
slider.dispatchEvent(new Event('input', { bubbles: true }));

Real user mouse drag works correctly without any of this — the whitelist fix is all that was needed for the runtime path.

  • frontend/src/components/DynamicComponent.tsx — the per-component wrapper, handleMouseDown lives here.
  • frontend/src/components/simulator/SimulatorCanvas.tsx — owns the canvas drag-threshold logic and dialog open dispatch.
  • frontend/src/components/simulator/SensorControlPanel.tsx — renders the slider/button UI for sensor property editing.
  • frontend/src/simulation/SensorUpdateRegistry.ts — module-level singleton bridging the dialog → part attachEvents callbacks.
  • frontend/src/simulation/parts/SensorParts.ts, BasicParts.ts, ComplexParts.ts — the actual per-part handlers that consume slider updates.