refactor(sim): single-board example load rebuilds from scratch

Cleaner follow-up to the multi-board residue fix. Instead of removing the
extra boards and retyping the surviving one (which left a stale id such as
"stm32-bluepill" on what was now an Arduino Uno), the single-board path now
tears every board down and adds exactly one fresh board of the target kind.
This mirrors the multi-board and board-less paths and guarantees the
surviving board's id matches its kind.

Drops the now-unused setBoardType/activeBoardId destructures and tightens
the boardFilter cast off `any`. Strengthens the regression test to assert
the surviving board's id and kind.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
David Montero 2026-05-31 23:23:34 +02:00
parent 0d5a1d5838
commit af39225688
2 changed files with 34 additions and 40 deletions

View File

@ -121,14 +121,20 @@ describe('loadExample — multi-board to single-board leaves no residue (bug 3)'
resetStores(); resetStores();
}); });
it('a single-board example after a multi-board example ends with exactly one board', async () => { it('a single-board example after a multi-board example ends with exactly one clean board', async () => {
// Multi-board example: STM32 Blue Pill + Arduino Uno. // Multi-board example: STM32 Blue Pill + Arduino Uno.
await loadExample(findExample('stm32-uno-gpio-mirror')); await loadExample(findExample('stm32-uno-gpio-mirror'));
expect(useSimulatorStore.getState().boards.length).toBe(2); expect(useSimulatorStore.getState().boards.length).toBe(2);
// Single-board example must reduce the canvas back to one board. // Single-board example must reduce the canvas back to one board, freshly
// built — its id must match its kind (no stale "stm32-bluepill" id left
// on what is now an Arduino Uno).
await loadExample(findExample('blink-led')); await loadExample(findExample('blink-led'));
expect(useSimulatorStore.getState().boards.length).toBe(1); const after = useSimulatorStore.getState();
expect(after.boards.length).toBe(1);
expect(after.boards[0].boardKind).toBe('arduino-uno');
expect(after.boards[0].id).toBe('arduino-uno');
expect(after.activeBoardId).toBe('arduino-uno');
}); });
it('residue cleanup also applies when extra boards were added manually', async () => { it('residue cleanup also applies when extra boards were added manually', async () => {

View File

@ -88,9 +88,7 @@ export async function loadExample(
const { const {
setComponents, setComponents,
setWires, setWires,
setBoardType,
setBoardLanguageMode, setBoardLanguageMode,
activeBoardId,
boards, boards,
addBoard, addBoard,
removeBoard, removeBoard,
@ -186,43 +184,33 @@ export async function loadExample(
recalculateAllWirePositions(); recalculateAllWirePositions();
} else { } else {
// ── Single-board loading ───────────────────────────────────────────── // ── Single-board loading ─────────────────────────────────────────────
// Analog-only and digital-only SPICE examples are board-less. Remove every // Tear the canvas down to nothing first, then (unless the example is
// existing board so the canvas opens with just the circuit (boards are now // board-less) add exactly one fresh board of the target kind.
// optional — you can have 0, 1, or many at any time). //
const filter = (example as any).boardFilter; // Analog-only and digital-only SPICE examples are board-less — they open
// with just the circuit (boards are optional: 0, 1, or many at any time).
//
// Rebuilding from scratch — rather than reusing and retyping a leftover
// board from a previous (possibly multi-board) example — is what keeps
// this prolijo: a single-board example always ends with exactly one
// board whose id matches its kind. The old reuse path left a stale id
// (e.g. "stm32-bluepill" on what was now an Arduino Uno) and any extra
// boards as residue. This mirrors the multi-board path above; the
// setComponents/setWires calls below replace components and wires
// wholesale.
const filter = (example as { boardFilter?: string }).boardFilter;
const isBoardless = filter === 'analog' || filter === 'digital'; const isBoardless = filter === 'analog' || filter === 'digital';
if (isBoardless) {
const currentIds = boards.map((b) => b.id); boards.forEach((b) => removeBoard(b.id));
currentIds.forEach((id) => removeBoard(id));
} else { if (!isBoardless) {
const targetBoard = example.boardType || 'arduino-uno'; const targetBoard = example.boardType || 'arduino-uno';
// A previous MULTI-board example may have left several boards on the
// canvas. A single-board example must end with exactly one board, so
// drop every board past the first before retyping it. Without this
// the extra boards (and their editor file groups) linger as residue
// from the previous example. setComponents/setWires below already
// replace the components and wires wholesale; boards were the one
// piece of state this path never reset.
const existing = useSimulatorStore.getState().boards;
if (existing.length > 1) {
existing.slice(1).forEach((b) => removeBoard(b.id));
}
// If boards[] is empty (e.g. a previous analog example removed every
// board), setBoardType can't work — it only maps over existing entries.
// Add a fresh board instead.
if (useSimulatorStore.getState().boards.length === 0) {
const newId = addBoard( const newId = addBoard(
targetBoard as BoardKind, targetBoard as BoardKind,
DEFAULT_BOARD_POSITION.x, DEFAULT_BOARD_POSITION.x,
DEFAULT_BOARD_POSITION.y, DEFAULT_BOARD_POSITION.y,
); );
setActiveBoardId(newId); setActiveBoardId(newId);
} else {
// Reuse the surviving board, but make sure it's the active one
// first — setBoardType retypes whatever board is active.
setActiveBoardId(useSimulatorStore.getState().boards[0].id);
setBoardType(targetBoard);
}
} }
// ── MicroPython + multi-file payloads ──────────────────────────────── // ── MicroPython + multi-file payloads ────────────────────────────────