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 <noreply@anthropic.com>
This commit is contained in:
David Montero Crespo 2026-05-19 15:10:42 -03:00
parent 2b668d882c
commit dd22bcfe50
3 changed files with 46 additions and 6 deletions

View File

@ -276,12 +276,37 @@ export const DynamicComponent: React.FC<DynamicComponentProps> = ({
*/
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(

View File

@ -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;
// SteinhartHart 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;
}

View File

@ -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