fix(sim): clean restart on Run after agent + display body occludes crossing wires

Two issues from a real ESP32 7-segment clock the agent built.

Run after the agent didn't work until a page reload
---------------------------------------------------
The agent's run_simulation leaves the ESP32 board RUNNING (live QEMU
WebSocket). Esp32Bridge.connect() is a no-op while the socket is non-CLOSED,
so the user's subsequent Run click called startBoard() → connect() → did
NOTHING. And if the backend QEMU session had since died while the frontend
socket lingered (CONNECTING/OPEN/CLOSING), the user saw a dead sim that only
a reload cleared — exactly the "di Run y no funcionó; recargué y sí" report.
The Arduino/C++ QEMU path now stops a running board first (closing the WS),
waits for it to settle, then boots fresh — the MicroPython path already did
this for the same reason.

Wires painted over the 7-segment digits
----------------------------------------
The agent bridges each segment strip to its resistor from a breadboard hole
that is physically UNDER the seated display; on the flat canvas those wires
(wire layer z 35) painted over the digits (component z 1) — "casi ni se ven
los dígitos". A large-bodied display seated on a breadboard now renders
ABOVE the wire layer, so its face occludes the wires crossing it exactly as
the real part's body would (the wire passes behind it to reach the hole).
Scoped to display bodies (7segment, matrix, oled, lcd, ili9341, led-ring…)
and only when actually seated; thin parts and free-floating displays are
untouched. The pin overlay + seated-pin markers share the display's stacking
group, so they rise with it and wiring still works.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
David Montero Crespo 2026-07-21 06:46:57 +02:00
parent d726139946
commit 9c26174c93
2 changed files with 43 additions and 1 deletions

View File

@ -811,6 +811,19 @@ export const EditorToolbar = ({
// QEMU boards: auto-compile if no firmware available yet
if (isQemuBoard) {
console.log('[handleRun] QEMU path');
// Clean restart when the board is already running. Esp32Bridge.connect()
// is a no-op while the socket is non-CLOSED, so startBoard() on a live
// session does NOTHING — and if the backend QEMU session has since died
// but the frontend socket is still zombie (CONNECTING/OPEN/CLOSING), the
// user sees a dead sim that only a page reload fixes. This is the exact
// "el agente terminó, di Run y no funcionó; recargué y sí" report: the
// agent's run_simulation left the board running, so the user's Run
// no-op'd. Stop first (closes the WS), let it settle, then boot fresh —
// mirrors what the MicroPython branch above already does.
if (board?.running) {
stopBoard(activeBoardId);
await new Promise((resolve) => setTimeout(resolve, 300));
}
if (!board?.compiledProgram || codeChangedSinceLastCompile) {
console.log('[handleRun] auto-compile + run');
autoRunAfterCompile.current = true;

View File

@ -83,6 +83,16 @@ import './SimulatorCanvas.css';
/** World-units of tolerance for alignment snap (scales with zoom). */
const ALIGN_SNAP_PX = 6;
/**
* Large-bodied display parts whose face should occlude wires when the part is
* seated on a breadboard the flat canvas has no depth, so bridge wires
* routed to holes UNDER the body would otherwise paint over the readout.
* Matches both the metadata ids (`7segment`, `ssd1306`, ) and the `wokwi-`/
* `velxio-` element-name variants. Thin two-pin parts are deliberately absent.
*/
const DISPLAY_BODY_METADATA_RE =
/(7segment|led-matrix|max7219|neopixel-matrix|ssd1306|oled|ili9341|lcd1602|lcd2004|led-ring)/i;
/** Long-press duration for touch context menu (ms). */
const LONG_PRESS_MS = 500;
@ -2240,7 +2250,26 @@ export const SimulatorCanvas = ({ headerSlot }: SimulatorCanvasProps = {}) => {
// them — so they always sit at the very back: below boards (z 0), other
// components (z 1/2) and wires (z 35). Selection doesn't raise them.
const isBreadboard = String(component.metadataId).startsWith('breadboard');
const groupZIndex = isBreadboard ? -1 : isSelected ? 2 : 1;
// A large-bodied display seated on a breadboard physically sits ON the
// board, so wires routed to holes UNDERNEATH it pass behind its body in
// real life. On the flat canvas those bridge wires (segment strips whose
// holes are literally under the display) otherwise paint OVER the digits
// — the reported "los cables se ven superpuestos y casi ni se ven los
// dígitos". Raise a seated display above the wire layer (z 35) so its
// face occludes the wires crossing it, exactly as the real part would.
// Scoped to occluding display bodies + only when actually seated, so
// thin parts (resistors, LEDs) and free-floating displays are untouched.
const isSeated = (seatedPinsByComponent.get(component.id)?.length ?? 0) > 0;
const occludesWhenSeated = DISPLAY_BODY_METADATA_RE.test(String(component.metadataId));
const groupZIndex = isBreadboard
? -1
: isSeated && occludesWhenSeated
? isSelected
? 37
: 36
: isSelected
? 2
: 1;
return (
<React.Fragment key={component.id}>