From dd22bcfe5017ce9eb433fa1adda22e86e144ff69 Mon Sep 17 00:00:00 2001 From: David Montero Crespo Date: Tue, 19 May 2026 15:10:42 -0300 Subject: [PATCH] fix(ui+spice+example): interactive wokwi components, NTC formula, photoresistor alias Three independent fixes uncovered during a systematic example-by-example audit (plan/full_test_plan/): 1. DynamicComponent.handleMouseDown was calling e.stopPropagation() unconditionally in the capture phase. That swallowed pointerdown BEFORE wokwi-potentiometer / pushbutton / slide-switch / joystick could see it, so the rotary knob would not rotate and buttons wouldn't press even with a real OS mouse. Now we skip the swallow when the click target is an inner wokwi-* element during a live simulation, letting the wokwi component own its own pointerdown while still allowing the canvas drag-to-rearrange flow on the wrapper / non-interactive surface. 2. examples.ts uno-ntc (and pico-ntc) sketch had the NTC divider formula inverted relative to both the SPICE mapper topology (VCC -> R_NTC -> A1 -> R_pull -> GND, the standard module wiring) and real wokwi-ntc-temperature-sensor modules. Moving the slider to 60 C made the firmware print -3.42 C. Flipped the formula to r = SERIES_R * (VCC - v) / v. Now slider 60 C -> Serial reports 60.12 C and A1 voltmeter shows 4.00 V. 3. componentToSpice.ts photoresistor mapper was only registered under the bare key `photoresistor`, but example components use the metadataId `photoresistor-sensor`. Added an alias so the LDR + pull-down divider gets emitted for the real component instance. All three reproduce visually in seconds; documented per-example in plan/full_test_plan/examples/. Co-Authored-By: Claude Opus 4.7 --- frontend/src/components/DynamicComponent.tsx | 33 ++++++++++++++++--- frontend/src/data/examples.ts | 11 +++++-- .../src/simulation/spice/componentToSpice.ts | 8 +++++ 3 files changed, 46 insertions(+), 6 deletions(-) diff --git a/frontend/src/components/DynamicComponent.tsx b/frontend/src/components/DynamicComponent.tsx index 7c6e8e3c..a3c8eb23 100644 --- a/frontend/src/components/DynamicComponent.tsx +++ b/frontend/src/components/DynamicComponent.tsx @@ -276,12 +276,37 @@ export const DynamicComponent: React.FC = ({ */ const handleMouseDown = useCallback( (e: React.MouseEvent) => { - if (onMouseDown) { - e.stopPropagation(); - onMouseDown(e); + if (!onMouseDown) return; + // Don't swallow the pointerdown when the user is interacting with a + // live wokwi component (rotating a potentiometer knob, pressing a + // pushbutton, toggling a slide-switch, dragging the joystick stick, + // etc.). The wokwi-elements internally use pointerdown/move/up on + // their shadow-DOM SVGs; if we call stopPropagation() in the capture + // phase here, that internal logic NEVER receives the event and the + // knob can't rotate, the button never reports pressed, etc. + // + // The canvas's drag-to-rearrange flow still works: the user can grab + // the component WRAPPER (its padding/border) or any non-interactive + // part of the body. For interactive parts the wokwi component owns + // the click — same UX as Wokwi/Tinkercad. + // + // We additionally allow the canvas drag when the simulation is NOT + // running (interactionRunning=false): the user is in "edit mode" + // arranging the board, so capturing the click into a canvas drag + // is the correct behaviour even on interactive components. + const target = e.target as HTMLElement; + const targetIsInteractive = + isInteractive && + interactionRunning && + target.tagName.toLowerCase().startsWith('wokwi-'); + if (targetIsInteractive) { + // Let the wokwi component own this pointerdown. + return; } + e.stopPropagation(); + onMouseDown(e); }, - [onMouseDown], + [onMouseDown, isInteractive, interactionRunning], ); const handleDoubleClick = useCallback( diff --git a/frontend/src/data/examples.ts b/frontend/src/data/examples.ts index e807dc81..d82ef668 100644 --- a/frontend/src/data/examples.ts +++ b/frontend/src/data/examples.ts @@ -5185,7 +5185,13 @@ const float NOM_TEMP_K = 298.15; // 25 °C in Kelvin float readTempC() { int raw = analogRead(NTC_PIN); float v = raw * (VCC / 1023.0); - float r = SERIES_R * v / (VCC - v); // voltage divider + // Voltage divider topology used by standard NTC modules and by Velxio's + // wokwi-ntc-temperature-sensor: VCC → R_NTC → A1 → R_pull (10k) → GND. + // Higher temperature → R_NTC drops → V rises, so the NTC resistance is + // r = R_pull * (VCC - v) / v + // Using the inverted form (r = R_pull * v / (VCC - v)) gives the wrong + // sign and Steinhart-Hart returns negative temperatures for hot inputs. + float r = SERIES_R * (VCC - v) / v; // Steinhart–Hart simplified equation float st = log(r / NOM_R) / B_COEFF + 1.0 / NOM_TEMP_K; return (1.0 / st) - 273.15; @@ -5514,7 +5520,8 @@ const float NOM_TEMP_K = 298.15; // 25 °C float readTempC() { int raw = analogRead(NTC_PIN); float v = raw * (VCC / 1023.0); - float r = SERIES_R * v / (VCC - v); + // VCC → R_NTC → A1 → R_pull → GND (standard NTC module topology) + float r = SERIES_R * (VCC - v) / v; float st = log(r / NOM_R) / B_COEFF + 1.0 / NOM_TEMP_K; return (1.0 / st) - 273.15; } diff --git a/frontend/src/simulation/spice/componentToSpice.ts b/frontend/src/simulation/spice/componentToSpice.ts index 8face639..6fd97c83 100644 --- a/frontend/src/simulation/spice/componentToSpice.ts +++ b/frontend/src/simulation/spice/componentToSpice.ts @@ -1220,6 +1220,14 @@ for (const [presetId, baseId] of Object.entries(PASSIVE_PRESETS)) { MAPPERS[presetId] = MAPPERS[baseId]; } +// Alias: metadata id for the wokwi-photoresistor-sensor breakout is +// `photoresistor-sensor`, but the mapper is registered under the short +// name `photoresistor` (matches the bare LDR/discrete element). Without +// this alias, any example that drops a photoresistor sensor on the +// canvas gets a null mapping → no R_ldr / R_pull emitted → A0 net +// floats → analogRead returns 0 even though the divider should solve. +MAPPERS['photoresistor-sensor'] = MAPPERS['photoresistor']; + /** * Public entry: map one Velxio component to SPICE cards. * Returns null if we have no mapping for this metadataId (caller should