2026-03-31 05:00:33 +07:00
|
|
|
/**
|
|
|
|
|
* Shared utility to load an example project into the editor and simulator stores.
|
|
|
|
|
* Used by both ExamplesPage (gallery click) and ExampleLoaderPage (direct URL).
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
import type { ExampleProject } from '../data/examples';
|
|
|
|
|
import type { BoardKind } from '../types/board';
|
2026-05-19 04:23:48 +07:00
|
|
|
import { isPiBoardKind } from '../types/board';
|
feat(custom-chip): program lives in its own editor group, not the board sketch
A programmable custom-chip (a CPU emulator that runs a ROM/program, e.g. the
Z80 or 8080) now keeps its program (larson.s, chaser.c, ...) in a dedicated
editor file group — group-chip-<chipId> — rendered as its own collapsible
section in the file explorer, exactly like each board owns its sketch group.
Behaviour/driver chips and predefined chips carry no programFile and get no
group; they stay editable only in the chip designer.
Fixes two reported issues on the Z80 examples:
- /example/z80-larson-no-board: the board-less chip example now opens its
program (larson.s) as the active group, editable on the left — previously
the editor showed but no file appeared.
- /example/z80-led-chaser-c: the chip program (chaser.c) no longer shows as
a sibling tab inside the Arduino sketch group; it sits in its own chip
section instead. The board group shows only sketch.ino.
Details:
- useEditorStore: chipFileGroupId()/CHIP_GROUP_PREFIX helpers.
- loadExample: seedChipProgramGroups() routes each chip's programFile into its
own group (seeded from the example files), sweeps stale chip groups, keeps
the program OUT of the board group, and for a board-less chip example makes
the chip group active so the program is the editable file shown.
- EditorToolbar.prepareCustomChips: resolves the program from the chip's own
group (falls back to board files for older projects) before assembling ROM.
- FileExplorer: renders one collapsible section per programmable chip with an
IC icon; clicking switches the editor to the chip group. Lazy-creates a
group for chips dropped on the canvas.
- projectPayload + vlxFile: serialise chip groups alongside board groups and
include them in the dirty-check hash, so chip-program edits persist on
save / autosave / .vlx export and round-trip via replaceFileGroups on load.
- Regression tests for board-less + board+chip routing and stale-group sweep.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-04 03:07:56 +07:00
|
|
|
import { useEditorStore, chipFileGroupId, CHIP_GROUP_PREFIX } from '../store/useEditorStore';
|
2026-04-23 10:22:35 +07:00
|
|
|
import { useSimulatorStore, DEFAULT_BOARD_POSITION } from '../store/useSimulatorStore';
|
2026-05-13 00:26:33 +07:00
|
|
|
import { useElectricalStore } from '../store/useElectricalStore';
|
fix(loadExample): clear currentProject before mutating stores
Critical data-loss bug. Repro:
1. User opens a saved project at /<username>/<slug>. The page sets
useProjectStore.currentProject = { id, slug, ownerUsername, ... }.
Auto-save kicks in and starts watching simulator/editor stores.
2. User clicks the "Examples" link, picks an example, hits Run.
3. loadExample mutates useSimulatorStore (setComponents, setWires,
addBoard, removeBoard) and useEditorStore (loadFiles).
4. Auto-save sees the change. Its eligibility check finds
currentProject still pointing at the user's saved project (we
never touched useProjectStore). It debounces a
PUT /api/projects/<old-id> with the EXAMPLE's components/wires/
files. The user's saved project is overwritten with the example
contents.
The URL changing to /editor isn't enough — useProjectStore is store
state, not router state. ProjectPage / ProjectByIdPage set it on
mount; nothing clears it when the user navigates away.
Fix: loadExample calls useProjectStore.getState().clearCurrentProject()
BEFORE the simulator/editor mutations. autoSaveImpl is subscribed to
useProjectStore via subscribe((s, prev) => ... reset() if id changed),
and Zustand notifies subscribers synchronously inside set(), so the
reset (projectId=null, baseline hash=null) runs in the same tick.
Every subsequent setComponents/setWires/loadFiles fires onChange in
the hook, which now sees projectId=null and returns early. No PUT
ever goes out.
The reset is order-sensitive: it must run BEFORE the store mutations
or the hook would already have queued a save with the old projectId
before we cleared. Comment in the source spells this out so it
doesn't get reordered in a future refactor.
In-flight saves are not affected: buildSavePayload() snapshots state
before its `await updateProject(...)`, so a save that started right
before the example load still sends the user's pre-example state to
the right project. Worst case: the save completes after clear, and
the hook quietly returns idle.
Build verified (vite OSS+pro, 285 SEO pages).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 10:02:05 +07:00
|
|
|
import { useProjectStore } from '../store/useProjectStore';
|
2026-03-31 05:00:33 +07:00
|
|
|
import { useVfsStore } from '../store/useVfsStore';
|
|
|
|
|
import { isBoardComponent } from './boardPinMapping';
|
|
|
|
|
import { getInstalledLibraries, installLibrary } from '../services/libraryService';
|
|
|
|
|
import { trackOpenExample } from './analytics';
|
2026-05-16 03:11:28 +07:00
|
|
|
import { stripBrandPrefix } from './exampleToBuildNetlistInput';
|
2026-03-31 05:00:33 +07:00
|
|
|
|
|
|
|
|
export interface LibraryInstallProgress {
|
|
|
|
|
total: number;
|
|
|
|
|
done: number;
|
|
|
|
|
current: string;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Install any missing Arduino libraries required by an example.
|
|
|
|
|
* Calls onProgress for UI updates; silently continues on failure.
|
|
|
|
|
*/
|
|
|
|
|
export async function ensureLibraries(
|
|
|
|
|
libs: string[],
|
|
|
|
|
onProgress?: (progress: LibraryInstallProgress | null) => void,
|
|
|
|
|
): Promise<void> {
|
|
|
|
|
if (libs.length === 0) return;
|
|
|
|
|
try {
|
|
|
|
|
const installed = await getInstalledLibraries();
|
|
|
|
|
const installedNames = new Set(
|
|
|
|
|
installed.map((l) => (l.library?.name ?? l.name ?? '').toLowerCase()),
|
|
|
|
|
);
|
|
|
|
|
const missing = libs.filter((l) => !installedNames.has(l.toLowerCase()));
|
|
|
|
|
if (missing.length === 0) return;
|
|
|
|
|
|
|
|
|
|
onProgress?.({ total: missing.length, done: 0, current: missing[0] });
|
|
|
|
|
for (let i = 0; i < missing.length; i++) {
|
|
|
|
|
onProgress?.({ total: missing.length, done: i, current: missing[i] });
|
|
|
|
|
await installLibrary(missing[i]);
|
|
|
|
|
}
|
|
|
|
|
onProgress?.(null);
|
|
|
|
|
} catch {
|
|
|
|
|
onProgress?.(null);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
feat(custom-chip): program lives in its own editor group, not the board sketch
A programmable custom-chip (a CPU emulator that runs a ROM/program, e.g. the
Z80 or 8080) now keeps its program (larson.s, chaser.c, ...) in a dedicated
editor file group — group-chip-<chipId> — rendered as its own collapsible
section in the file explorer, exactly like each board owns its sketch group.
Behaviour/driver chips and predefined chips carry no programFile and get no
group; they stay editable only in the chip designer.
Fixes two reported issues on the Z80 examples:
- /example/z80-larson-no-board: the board-less chip example now opens its
program (larson.s) as the active group, editable on the left — previously
the editor showed but no file appeared.
- /example/z80-led-chaser-c: the chip program (chaser.c) no longer shows as
a sibling tab inside the Arduino sketch group; it sits in its own chip
section instead. The board group shows only sketch.ino.
Details:
- useEditorStore: chipFileGroupId()/CHIP_GROUP_PREFIX helpers.
- loadExample: seedChipProgramGroups() routes each chip's programFile into its
own group (seeded from the example files), sweeps stale chip groups, keeps
the program OUT of the board group, and for a board-less chip example makes
the chip group active so the program is the editable file shown.
- EditorToolbar.prepareCustomChips: resolves the program from the chip's own
group (falls back to board files for older projects) before assembling ROM.
- FileExplorer: renders one collapsible section per programmable chip with an
IC icon; clicking switches the editor to the chip group. Lazy-creates a
group for chips dropped on the canvas.
- projectPayload + vlxFile: serialise chip groups alongside board groups and
include them in the dirty-check hash, so chip-program edits persist on
save / autosave / .vlx export and round-trip via replaceFileGroups on load.
- Regression tests for board-less + board+chip routing and stale-group sweep.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-04 03:07:56 +07:00
|
|
|
/**
|
|
|
|
|
* Programmable custom-chips (those with a `programFile`) keep their program
|
|
|
|
|
* (ROM source / C) in their OWN editor file group — group-chip-<chipId> — so
|
|
|
|
|
* it shows as a separate collapsible section in the file explorer, never mixed
|
|
|
|
|
* into the board's sketch. This mirrors how every board owns a group.
|
|
|
|
|
*
|
|
|
|
|
* Seeds those groups from the example's `files[]` (matched by programFile
|
|
|
|
|
* name), clearing any chip groups left over from a previously-loaded example.
|
|
|
|
|
* Returns the set of program filenames (so the caller keeps them OUT of the
|
|
|
|
|
* board's own group) and the created group ids (so a board-less example can
|
|
|
|
|
* open the program as the active group).
|
|
|
|
|
*/
|
|
|
|
|
function seedChipProgramGroups(example: ExampleProject): {
|
|
|
|
|
programFileNames: Set<string>;
|
|
|
|
|
chipGroupIds: string[];
|
|
|
|
|
} {
|
|
|
|
|
const editor = useEditorStore.getState();
|
|
|
|
|
// Drop chip groups from a previously-loaded example so they don't linger.
|
|
|
|
|
Object.keys(editor.fileGroups)
|
|
|
|
|
.filter((g) => g.startsWith(CHIP_GROUP_PREFIX))
|
|
|
|
|
.forEach((g) => editor.deleteFileGroup(g));
|
|
|
|
|
|
|
|
|
|
const programFileNames = new Set<string>();
|
|
|
|
|
const chipGroupIds: string[] = [];
|
|
|
|
|
for (const comp of example.components) {
|
|
|
|
|
if (stripBrandPrefix(comp.type) !== 'custom-chip') continue;
|
|
|
|
|
const pf = String(
|
|
|
|
|
(comp.properties as Record<string, unknown>)?.programFile ?? '',
|
|
|
|
|
).trim();
|
|
|
|
|
if (!pf) continue; // behaviour / predefined chips have no editable program
|
|
|
|
|
programFileNames.add(pf);
|
|
|
|
|
const content = example.files?.find((f) => f.name === pf)?.content ?? '';
|
|
|
|
|
const gid = chipFileGroupId(comp.id);
|
|
|
|
|
editor.createFileGroup(gid, [{ name: pf, content }]);
|
|
|
|
|
chipGroupIds.push(gid);
|
|
|
|
|
}
|
|
|
|
|
return { programFileNames, chipGroupIds };
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-31 05:00:33 +07:00
|
|
|
/**
|
|
|
|
|
* Load an example project into the editor + simulator stores.
|
|
|
|
|
* Does NOT navigate — the caller is responsible for navigation.
|
|
|
|
|
*/
|
|
|
|
|
export async function loadExample(
|
|
|
|
|
example: ExampleProject,
|
|
|
|
|
onLibraryProgress?: (progress: LibraryInstallProgress | null) => void,
|
|
|
|
|
): Promise<void> {
|
|
|
|
|
trackOpenExample(example.title);
|
|
|
|
|
|
fix(loadExample): clear currentProject before mutating stores
Critical data-loss bug. Repro:
1. User opens a saved project at /<username>/<slug>. The page sets
useProjectStore.currentProject = { id, slug, ownerUsername, ... }.
Auto-save kicks in and starts watching simulator/editor stores.
2. User clicks the "Examples" link, picks an example, hits Run.
3. loadExample mutates useSimulatorStore (setComponents, setWires,
addBoard, removeBoard) and useEditorStore (loadFiles).
4. Auto-save sees the change. Its eligibility check finds
currentProject still pointing at the user's saved project (we
never touched useProjectStore). It debounces a
PUT /api/projects/<old-id> with the EXAMPLE's components/wires/
files. The user's saved project is overwritten with the example
contents.
The URL changing to /editor isn't enough — useProjectStore is store
state, not router state. ProjectPage / ProjectByIdPage set it on
mount; nothing clears it when the user navigates away.
Fix: loadExample calls useProjectStore.getState().clearCurrentProject()
BEFORE the simulator/editor mutations. autoSaveImpl is subscribed to
useProjectStore via subscribe((s, prev) => ... reset() if id changed),
and Zustand notifies subscribers synchronously inside set(), so the
reset (projectId=null, baseline hash=null) runs in the same tick.
Every subsequent setComponents/setWires/loadFiles fires onChange in
the hook, which now sees projectId=null and returns early. No PUT
ever goes out.
The reset is order-sensitive: it must run BEFORE the store mutations
or the hook would already have queued a save with the old projectId
before we cleared. Comment in the source spells this out so it
doesn't get reordered in a future refactor.
In-flight saves are not affected: buildSavePayload() snapshots state
before its `await updateProject(...)`, so a save that started right
before the example load still sends the user's pre-example state to
the right project. Worst case: the save completes after clear, and
the hook quietly returns idle.
Build verified (vite OSS+pro, 285 SEO pages).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 10:02:05 +07:00
|
|
|
// CRITICAL — clear currentProject FIRST, before touching any other store.
|
|
|
|
|
//
|
|
|
|
|
// Otherwise: user has a saved project open (currentProject = { id, slug, …}),
|
|
|
|
|
// navigates to /examples, clicks an example. We mutate the simulator +
|
|
|
|
|
// editor stores below; the auto-save hook is still subscribed and still
|
|
|
|
|
// thinks the active project is the user's saved one. It debounces a
|
|
|
|
|
// PUT /api/projects/<old-id> with the example's components/wires/files
|
|
|
|
|
// and OVERWRITES the user's saved project with the example contents.
|
|
|
|
|
//
|
|
|
|
|
// The auto-save hook is subscribed to useProjectStore and resets its
|
|
|
|
|
// baseline (projectId=null, lastSavedHash=null) whenever currentProject?.id
|
|
|
|
|
// changes. Clearing here BEFORE the mutations below guarantees the hook
|
|
|
|
|
// sees null as projectId during every subsequent simulator/editor change,
|
|
|
|
|
// so no PUT goes out.
|
|
|
|
|
useProjectStore.getState().clearCurrentProject();
|
|
|
|
|
|
2026-06-07 11:07:48 +07:00
|
|
|
// P2.4 — this example's declared manifest (compile scope) is assigned to each
|
|
|
|
|
// board it creates at the END of this function (the boards don't exist yet).
|
2026-06-07 05:10:15 +07:00
|
|
|
|
2026-05-13 00:26:33 +07:00
|
|
|
// Loading a new example always starts unpaused — otherwise the canvas
|
|
|
|
|
// would open with every LED frozen at the previous example's state.
|
|
|
|
|
useElectricalStore.getState().setPaused(false);
|
|
|
|
|
|
2026-03-31 05:00:33 +07:00
|
|
|
// Auto-install required libraries
|
|
|
|
|
if (example.libraries && example.libraries.length > 0) {
|
|
|
|
|
await ensureLibraries(example.libraries, onLibraryProgress);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const {
|
2026-04-22 02:45:45 +07:00
|
|
|
setComponents,
|
|
|
|
|
setWires,
|
2026-04-29 05:24:39 +07:00
|
|
|
setBoardLanguageMode,
|
2026-04-22 02:45:45 +07:00
|
|
|
boards,
|
|
|
|
|
addBoard,
|
|
|
|
|
removeBoard,
|
|
|
|
|
setActiveBoardId,
|
2026-04-16 18:39:47 +07:00
|
|
|
recalculateAllWirePositions,
|
2026-03-31 05:00:33 +07:00
|
|
|
} = useSimulatorStore.getState();
|
|
|
|
|
|
|
|
|
|
if (example.boards && example.boards.length > 0) {
|
|
|
|
|
// ── Multi-board loading ───────────────────────────────────────────────
|
|
|
|
|
const currentIds = boards.map((b) => b.id);
|
|
|
|
|
currentIds.forEach((id) => removeBoard(id));
|
|
|
|
|
|
|
|
|
|
example.boards.forEach((eb) => {
|
|
|
|
|
addBoard(eb.boardKind as BoardKind, eb.x, eb.y);
|
|
|
|
|
});
|
|
|
|
|
|
2026-04-29 05:24:39 +07:00
|
|
|
// Match addBoard's deterministic ID rule (useSimulatorStore.addBoard):
|
|
|
|
|
// 1st board of a kind → id = boardKind
|
|
|
|
|
// 2nd board of a kind → id = `${boardKind}-2`
|
|
|
|
|
// Nth board of a kind → id = `${boardKind}-N`
|
|
|
|
|
// This is what wires reference, so the loader must compute the same IDs
|
|
|
|
|
// when loading per-board code/vfs.
|
|
|
|
|
const kindCount = new Map<string, number>();
|
|
|
|
|
const boardIds: string[] = example.boards.map((eb) => {
|
|
|
|
|
const n = (kindCount.get(eb.boardKind) ?? 0) + 1;
|
|
|
|
|
kindCount.set(eb.boardKind, n);
|
|
|
|
|
return n === 1 ? eb.boardKind : `${eb.boardKind}-${n}`;
|
|
|
|
|
});
|
|
|
|
|
|
2026-03-31 05:00:33 +07:00
|
|
|
const { boards: newBoards } = useSimulatorStore.getState();
|
2026-04-29 05:24:39 +07:00
|
|
|
example.boards.forEach((eb, idx) => {
|
|
|
|
|
const boardId = boardIds[idx];
|
2026-03-31 05:00:33 +07:00
|
|
|
const board = newBoards.find((b) => b.id === boardId);
|
|
|
|
|
if (!board) return;
|
|
|
|
|
|
|
|
|
|
if (eb.code) {
|
2026-04-29 06:40:48 +07:00
|
|
|
// Arduino-style boards (AVR, RP2040, ESP32, …) all need the `.ino`
|
|
|
|
|
// extension so arduino-cli auto-includes <Arduino.h>. Only the Pi 3B
|
|
|
|
|
// uses a different toolchain (Python via VFS or g++ for `.cpp`).
|
2026-05-19 04:23:48 +07:00
|
|
|
const filename = isPiBoardKind(eb.boardKind) ? 'main.cpp' : 'sketch.ino';
|
2026-03-31 05:00:33 +07:00
|
|
|
useEditorStore.getState().setActiveGroup(board.activeFileGroupId);
|
|
|
|
|
useEditorStore.getState().loadFiles([{ name: filename, content: eb.code }]);
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-19 04:23:48 +07:00
|
|
|
if (eb.vfsFiles && isPiBoardKind(eb.boardKind)) {
|
2026-03-31 05:00:33 +07:00
|
|
|
const vfsState = useVfsStore.getState();
|
|
|
|
|
const tree = vfsState.getTree(boardId);
|
|
|
|
|
for (const [nodeId, node] of Object.entries(tree)) {
|
|
|
|
|
if (node.type === 'file' && eb.vfsFiles[node.name] !== undefined) {
|
|
|
|
|
vfsState.setContent(boardId, nodeId, eb.vfsFiles[node.name]);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
2026-04-29 05:24:39 +07:00
|
|
|
const firstArduinoIdx = example.boards.findIndex(
|
2026-03-31 05:00:33 +07:00
|
|
|
(eb) =>
|
2026-05-19 04:23:48 +07:00
|
|
|
!isPiBoardKind(eb.boardKind) &&
|
2026-03-31 05:00:33 +07:00
|
|
|
eb.boardKind !== 'esp32' &&
|
|
|
|
|
eb.boardKind !== 'esp32-s3' &&
|
|
|
|
|
eb.boardKind !== 'esp32-c3',
|
|
|
|
|
);
|
2026-04-29 05:24:39 +07:00
|
|
|
if (firstArduinoIdx !== -1) {
|
|
|
|
|
setActiveBoardId(boardIds[firstArduinoIdx]);
|
2026-03-31 05:00:33 +07:00
|
|
|
}
|
|
|
|
|
|
feat(custom-chip): program lives in its own editor group, not the board sketch
A programmable custom-chip (a CPU emulator that runs a ROM/program, e.g. the
Z80 or 8080) now keeps its program (larson.s, chaser.c, ...) in a dedicated
editor file group — group-chip-<chipId> — rendered as its own collapsible
section in the file explorer, exactly like each board owns its sketch group.
Behaviour/driver chips and predefined chips carry no programFile and get no
group; they stay editable only in the chip designer.
Fixes two reported issues on the Z80 examples:
- /example/z80-larson-no-board: the board-less chip example now opens its
program (larson.s) as the active group, editable on the left — previously
the editor showed but no file appeared.
- /example/z80-led-chaser-c: the chip program (chaser.c) no longer shows as
a sibling tab inside the Arduino sketch group; it sits in its own chip
section instead. The board group shows only sketch.ino.
Details:
- useEditorStore: chipFileGroupId()/CHIP_GROUP_PREFIX helpers.
- loadExample: seedChipProgramGroups() routes each chip's programFile into its
own group (seeded from the example files), sweeps stale chip groups, keeps
the program OUT of the board group, and for a board-less chip example makes
the chip group active so the program is the editable file shown.
- EditorToolbar.prepareCustomChips: resolves the program from the chip's own
group (falls back to board files for older projects) before assembling ROM.
- FileExplorer: renders one collapsible section per programmable chip with an
IC icon; clicking switches the editor to the chip group. Lazy-creates a
group for chips dropped on the canvas.
- projectPayload + vlxFile: serialise chip groups alongside board groups and
include them in the dirty-check hash, so chip-program edits persist on
save / autosave / .vlx export and round-trip via replaceFileGroups on load.
- Regression tests for board-less + board+chip routing and stale-group sweep.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-04 03:07:56 +07:00
|
|
|
// Programmable chips own their program in a dedicated editor group so it
|
|
|
|
|
// shows as its own section (the per-board code came from eb.code above).
|
|
|
|
|
seedChipProgramGroups(example);
|
|
|
|
|
|
2026-03-31 05:00:33 +07:00
|
|
|
const componentsWithoutBoard = example.components.filter(
|
|
|
|
|
(comp) =>
|
|
|
|
|
!comp.type.includes('arduino') &&
|
|
|
|
|
!comp.type.includes('pico') &&
|
|
|
|
|
!comp.type.includes('raspberry') &&
|
|
|
|
|
!comp.type.includes('esp32'),
|
|
|
|
|
);
|
|
|
|
|
setComponents(
|
|
|
|
|
componentsWithoutBoard.map((comp) => ({
|
|
|
|
|
id: comp.id,
|
2026-05-16 03:11:28 +07:00
|
|
|
metadataId: stripBrandPrefix(comp.type),
|
2026-03-31 05:00:33 +07:00
|
|
|
x: comp.x,
|
|
|
|
|
y: comp.y,
|
|
|
|
|
properties: comp.properties,
|
|
|
|
|
})),
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
setWires(
|
|
|
|
|
example.wires.map((wire) => ({
|
|
|
|
|
id: wire.id,
|
|
|
|
|
start: { componentId: wire.start.componentId, pinName: wire.start.pinName, x: 0, y: 0 },
|
|
|
|
|
end: { componentId: wire.end.componentId, pinName: wire.end.pinName, x: 0, y: 0 },
|
|
|
|
|
color: wire.color,
|
|
|
|
|
waypoints: [],
|
|
|
|
|
})),
|
|
|
|
|
);
|
2026-04-16 18:39:47 +07:00
|
|
|
recalculateAllWirePositions();
|
2026-03-31 05:00:33 +07:00
|
|
|
} else {
|
|
|
|
|
// ── Single-board loading ─────────────────────────────────────────────
|
2026-06-01 04:23:34 +07:00
|
|
|
// Tear the canvas down to nothing first, then (unless the example is
|
|
|
|
|
// board-less) add exactly one fresh board of the target kind.
|
|
|
|
|
//
|
|
|
|
|
// 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;
|
2026-05-13 00:26:33 +07:00
|
|
|
const isBoardless = filter === 'analog' || filter === 'digital';
|
2026-06-01 04:23:34 +07:00
|
|
|
|
|
|
|
|
boards.forEach((b) => removeBoard(b.id));
|
|
|
|
|
|
|
|
|
|
if (!isBoardless) {
|
2026-04-21 22:59:59 +07:00
|
|
|
const targetBoard = example.boardType || 'arduino-uno';
|
2026-06-01 04:23:34 +07:00
|
|
|
const newId = addBoard(
|
|
|
|
|
targetBoard as BoardKind,
|
|
|
|
|
DEFAULT_BOARD_POSITION.x,
|
|
|
|
|
DEFAULT_BOARD_POSITION.y,
|
|
|
|
|
);
|
|
|
|
|
setActiveBoardId(newId);
|
2026-04-21 22:59:59 +07:00
|
|
|
}
|
2026-04-29 05:24:39 +07:00
|
|
|
|
feat(custom-chip): program lives in its own editor group, not the board sketch
A programmable custom-chip (a CPU emulator that runs a ROM/program, e.g. the
Z80 or 8080) now keeps its program (larson.s, chaser.c, ...) in a dedicated
editor file group — group-chip-<chipId> — rendered as its own collapsible
section in the file explorer, exactly like each board owns its sketch group.
Behaviour/driver chips and predefined chips carry no programFile and get no
group; they stay editable only in the chip designer.
Fixes two reported issues on the Z80 examples:
- /example/z80-larson-no-board: the board-less chip example now opens its
program (larson.s) as the active group, editable on the left — previously
the editor showed but no file appeared.
- /example/z80-led-chaser-c: the chip program (chaser.c) no longer shows as
a sibling tab inside the Arduino sketch group; it sits in its own chip
section instead. The board group shows only sketch.ino.
Details:
- useEditorStore: chipFileGroupId()/CHIP_GROUP_PREFIX helpers.
- loadExample: seedChipProgramGroups() routes each chip's programFile into its
own group (seeded from the example files), sweeps stale chip groups, keeps
the program OUT of the board group, and for a board-less chip example makes
the chip group active so the program is the editable file shown.
- EditorToolbar.prepareCustomChips: resolves the program from the chip's own
group (falls back to board files for older projects) before assembling ROM.
- FileExplorer: renders one collapsible section per programmable chip with an
IC icon; clicking switches the editor to the chip group. Lazy-creates a
group for chips dropped on the canvas.
- projectPayload + vlxFile: serialise chip groups alongside board groups and
include them in the dirty-check hash, so chip-program edits persist on
save / autosave / .vlx export and round-trip via replaceFileGroups on load.
- Regression tests for board-less + board+chip routing and stale-group sweep.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-04 03:07:56 +07:00
|
|
|
// ── Program / file routing ───────────────────────────────────────────
|
|
|
|
|
// A programmable chip's program (larson.s, chaser.c, …) goes into the
|
|
|
|
|
// chip's OWN editor group — its own collapsible section — never into the
|
|
|
|
|
// board's sketch group. Everything else (sketch.ino) stays with the board.
|
|
|
|
|
const { programFileNames, chipGroupIds } = seedChipProgramGroups(example);
|
|
|
|
|
const boardOwnedFiles = (example.files ?? []).filter(
|
|
|
|
|
(f) => !programFileNames.has(f.name),
|
|
|
|
|
);
|
|
|
|
|
|
2026-04-29 05:24:39 +07:00
|
|
|
// When the example specifies languageMode='micropython' or ships a
|
|
|
|
|
// files[] array, we go through setBoardLanguageMode + loadFiles instead
|
|
|
|
|
// of the legacy setCode() path so the editor opens the right file
|
|
|
|
|
// (main.py) with the right language mode.
|
|
|
|
|
const liveBoardId = useSimulatorStore.getState().activeBoardId;
|
|
|
|
|
const liveBoard = useSimulatorStore
|
|
|
|
|
.getState()
|
|
|
|
|
.boards.find((b) => b.id === liveBoardId);
|
|
|
|
|
|
feat(esp32): pure ESP-IDF language mode for the ESP32 family (#139)
Adds a third entry to the board language selector next to Arduino C++
and MicroPython: ESP-IDF. In this mode the user writes a plain ESP-IDF
project — app_main() entry point, FreeRTOS + driver APIs — and the
backend compiles it through the same ESP-IDF toolchain it already uses
for ESP32 Arduino sketches, just without the arduino-esp32 component.
Backend:
- CompileRequest.language ('espidf') threaded through the sync + async
compile paths and folded into the dedup job key (language='arduino'
and omitted hash identically so old clients keep dedupping).
- espidf_compiler: pure_idf flag. User files are written into main/
as-is (no Arduino.h wrap, no velxio_compat.h, Arduino library
resolution skipped), ARDUINO_ESP32_PATH is dropped from the build env
and VELXIO_PURE_SKETCH raised so the template CMake compiles the
user's own sources via a glob branch. Pure builds get their own
persistent build-dir variant through the eff_hash fold.
- QEMU WiFi compat for IDF-style code: esp_wifi.h/esp_wifi_init
detection sets has_wifi, and literal #define SSID/PASS plus
wifi_config_t designated initializers are normalized to the QEMU AP.
- CONFIG_ARDUINO_* lines are stripped from sdkconfig.defaults in pure
mode (the symbols don't exist without the arduino component).
Frontend:
- LanguageMode gains 'espidf'; BOARD_SUPPORTS_ESPIDF covers the ESP32
family (Xtensa, S3, C3). Toolbar shows the option only for those.
- Switching modes seeds a main.c blink skeleton (app_main + gpio
driver), mirroring the MicroPython main.py flow.
- compileCode sends language='espidf'; run/stop paths are unchanged
(the QEMU worker consumes the same merged flash image).
- New gallery example: esp32-idf-blink (LED + resistor on GPIO 2).
Tests: unit coverage for the build-env switch, IDF wifi normalization,
job-key variance, file-group seeding and the new example; verified
end-to-end in a container from the prod image (pure build produces a
bootable flash image; Arduino-mode build unchanged, same variant hash).
2026-07-24 11:37:01 +07:00
|
|
|
if (
|
|
|
|
|
(example.languageMode === 'micropython' || example.languageMode === 'espidf') &&
|
|
|
|
|
liveBoard
|
|
|
|
|
) {
|
|
|
|
|
setBoardLanguageMode(liveBoard.id, example.languageMode);
|
2026-04-29 05:24:39 +07:00
|
|
|
}
|
|
|
|
|
|
feat(custom-chip): program lives in its own editor group, not the board sketch
A programmable custom-chip (a CPU emulator that runs a ROM/program, e.g. the
Z80 or 8080) now keeps its program (larson.s, chaser.c, ...) in a dedicated
editor file group — group-chip-<chipId> — rendered as its own collapsible
section in the file explorer, exactly like each board owns its sketch group.
Behaviour/driver chips and predefined chips carry no programFile and get no
group; they stay editable only in the chip designer.
Fixes two reported issues on the Z80 examples:
- /example/z80-larson-no-board: the board-less chip example now opens its
program (larson.s) as the active group, editable on the left — previously
the editor showed but no file appeared.
- /example/z80-led-chaser-c: the chip program (chaser.c) no longer shows as
a sibling tab inside the Arduino sketch group; it sits in its own chip
section instead. The board group shows only sketch.ino.
Details:
- useEditorStore: chipFileGroupId()/CHIP_GROUP_PREFIX helpers.
- loadExample: seedChipProgramGroups() routes each chip's programFile into its
own group (seeded from the example files), sweeps stale chip groups, keeps
the program OUT of the board group, and for a board-less chip example makes
the chip group active so the program is the editable file shown.
- EditorToolbar.prepareCustomChips: resolves the program from the chip's own
group (falls back to board files for older projects) before assembling ROM.
- FileExplorer: renders one collapsible section per programmable chip with an
IC icon; clicking switches the editor to the chip group. Lazy-creates a
group for chips dropped on the canvas.
- projectPayload + vlxFile: serialise chip groups alongside board groups and
include them in the dirty-check hash, so chip-program edits persist on
save / autosave / .vlx export and round-trip via replaceFileGroups on load.
- Regression tests for board-less + board+chip routing and stale-group sweep.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-04 03:07:56 +07:00
|
|
|
const editorStore = useEditorStore.getState();
|
|
|
|
|
if (liveBoard) {
|
|
|
|
|
// Board present: the board group shows the sketch; chip programs sit in
|
|
|
|
|
// their own sections. Re-resolve the group ID — setBoardLanguageMode
|
|
|
|
|
// replaces it. We use `loadFiles` (not legacy `setCode`) and switch the
|
|
|
|
|
// editor to the board's group first: after a board-less → board
|
|
|
|
|
// transition the editor's `activeFileId` still points at an orphan ID
|
|
|
|
|
// from the deleted group, so `setCode` would silently no-op and the
|
|
|
|
|
// editor would appear blank. (Regression: load-example-transitions.test.ts.)
|
2026-04-29 05:24:39 +07:00
|
|
|
const updatedBoard = useSimulatorStore
|
|
|
|
|
.getState()
|
|
|
|
|
.boards.find((b) => b.id === liveBoard.id);
|
|
|
|
|
const groupId = updatedBoard?.activeFileGroupId ?? liveBoard.activeFileGroupId;
|
|
|
|
|
editorStore.setActiveGroup(groupId);
|
feat(custom-chip): program lives in its own editor group, not the board sketch
A programmable custom-chip (a CPU emulator that runs a ROM/program, e.g. the
Z80 or 8080) now keeps its program (larson.s, chaser.c, ...) in a dedicated
editor file group — group-chip-<chipId> — rendered as its own collapsible
section in the file explorer, exactly like each board owns its sketch group.
Behaviour/driver chips and predefined chips carry no programFile and get no
group; they stay editable only in the chip designer.
Fixes two reported issues on the Z80 examples:
- /example/z80-larson-no-board: the board-less chip example now opens its
program (larson.s) as the active group, editable on the left — previously
the editor showed but no file appeared.
- /example/z80-led-chaser-c: the chip program (chaser.c) no longer shows as
a sibling tab inside the Arduino sketch group; it sits in its own chip
section instead. The board group shows only sketch.ino.
Details:
- useEditorStore: chipFileGroupId()/CHIP_GROUP_PREFIX helpers.
- loadExample: seedChipProgramGroups() routes each chip's programFile into its
own group (seeded from the example files), sweeps stale chip groups, keeps
the program OUT of the board group, and for a board-less chip example makes
the chip group active so the program is the editable file shown.
- EditorToolbar.prepareCustomChips: resolves the program from the chip's own
group (falls back to board files for older projects) before assembling ROM.
- FileExplorer: renders one collapsible section per programmable chip with an
IC icon; clicking switches the editor to the chip group. Lazy-creates a
group for chips dropped on the canvas.
- projectPayload + vlxFile: serialise chip groups alongside board groups and
include them in the dirty-check hash, so chip-program edits persist on
save / autosave / .vlx export and round-trip via replaceFileGroups on load.
- Regression tests for board-less + board+chip routing and stale-group sweep.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-04 03:07:56 +07:00
|
|
|
if (boardOwnedFiles.length > 0) {
|
|
|
|
|
editorStore.loadFiles(boardOwnedFiles);
|
2026-06-04 02:30:42 +07:00
|
|
|
} else {
|
feat(custom-chip): program lives in its own editor group, not the board sketch
A programmable custom-chip (a CPU emulator that runs a ROM/program, e.g. the
Z80 or 8080) now keeps its program (larson.s, chaser.c, ...) in a dedicated
editor file group — group-chip-<chipId> — rendered as its own collapsible
section in the file explorer, exactly like each board owns its sketch group.
Behaviour/driver chips and predefined chips carry no programFile and get no
group; they stay editable only in the chip designer.
Fixes two reported issues on the Z80 examples:
- /example/z80-larson-no-board: the board-less chip example now opens its
program (larson.s) as the active group, editable on the left — previously
the editor showed but no file appeared.
- /example/z80-led-chaser-c: the chip program (chaser.c) no longer shows as
a sibling tab inside the Arduino sketch group; it sits in its own chip
section instead. The board group shows only sketch.ino.
Details:
- useEditorStore: chipFileGroupId()/CHIP_GROUP_PREFIX helpers.
- loadExample: seedChipProgramGroups() routes each chip's programFile into its
own group (seeded from the example files), sweeps stale chip groups, keeps
the program OUT of the board group, and for a board-less chip example makes
the chip group active so the program is the editable file shown.
- EditorToolbar.prepareCustomChips: resolves the program from the chip's own
group (falls back to board files for older projects) before assembling ROM.
- FileExplorer: renders one collapsible section per programmable chip with an
IC icon; clicking switches the editor to the chip group. Lazy-creates a
group for chips dropped on the canvas.
- projectPayload + vlxFile: serialise chip groups alongside board groups and
include them in the dirty-check hash, so chip-program edits persist on
save / autosave / .vlx export and round-trip via replaceFileGroups on load.
- Regression tests for board-less + board+chip routing and stale-group sweep.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-04 03:07:56 +07:00
|
|
|
const filename = isPiBoardKind(liveBoard.boardKind) ? 'main.cpp' : 'sketch.ino';
|
|
|
|
|
editorStore.loadFiles([{ name: filename, content: example.code }]);
|
2026-06-04 02:30:42 +07:00
|
|
|
}
|
feat(custom-chip): program lives in its own editor group, not the board sketch
A programmable custom-chip (a CPU emulator that runs a ROM/program, e.g. the
Z80 or 8080) now keeps its program (larson.s, chaser.c, ...) in a dedicated
editor file group — group-chip-<chipId> — rendered as its own collapsible
section in the file explorer, exactly like each board owns its sketch group.
Behaviour/driver chips and predefined chips carry no programFile and get no
group; they stay editable only in the chip designer.
Fixes two reported issues on the Z80 examples:
- /example/z80-larson-no-board: the board-less chip example now opens its
program (larson.s) as the active group, editable on the left — previously
the editor showed but no file appeared.
- /example/z80-led-chaser-c: the chip program (chaser.c) no longer shows as
a sibling tab inside the Arduino sketch group; it sits in its own chip
section instead. The board group shows only sketch.ino.
Details:
- useEditorStore: chipFileGroupId()/CHIP_GROUP_PREFIX helpers.
- loadExample: seedChipProgramGroups() routes each chip's programFile into its
own group (seeded from the example files), sweeps stale chip groups, keeps
the program OUT of the board group, and for a board-less chip example makes
the chip group active so the program is the editable file shown.
- EditorToolbar.prepareCustomChips: resolves the program from the chip's own
group (falls back to board files for older projects) before assembling ROM.
- FileExplorer: renders one collapsible section per programmable chip with an
IC icon; clicking switches the editor to the chip group. Lazy-creates a
group for chips dropped on the canvas.
- projectPayload + vlxFile: serialise chip groups alongside board groups and
include them in the dirty-check hash, so chip-program edits persist on
save / autosave / .vlx export and round-trip via replaceFileGroups on load.
- Regression tests for board-less + board+chip routing and stale-group sweep.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-04 03:07:56 +07:00
|
|
|
} else if (chipGroupIds.length > 0) {
|
|
|
|
|
// Board-less custom-chip example. The chip's program IS the only code
|
|
|
|
|
// here, so open its group on the left, editable — just like an Arduino
|
|
|
|
|
// sketch. (This is what makes /example/z80-larson-no-board show larson.s.)
|
|
|
|
|
editorStore.setActiveGroup(chipGroupIds[0]);
|
|
|
|
|
} else if (boardOwnedFiles.length > 0) {
|
|
|
|
|
// Board-less but ships plain files (rare) — point the editor at the
|
|
|
|
|
// default group and load them there so it isn't blank/uneditable.
|
|
|
|
|
editorStore.setActiveGroup('group-arduino-uno'); // = DEFAULT_GROUP_ID
|
|
|
|
|
editorStore.loadFiles(boardOwnedFiles);
|
|
|
|
|
} else {
|
|
|
|
|
// Pure analog/digital circuit, no editable program — keep the legacy
|
|
|
|
|
// behaviour (write the placeholder to the current file).
|
|
|
|
|
editorStore.setCode(example.code);
|
2026-04-29 05:24:39 +07:00
|
|
|
}
|
2026-03-31 05:00:33 +07:00
|
|
|
|
|
|
|
|
const componentsWithoutBoard = example.components.filter(
|
|
|
|
|
(comp) =>
|
|
|
|
|
!comp.type.includes('arduino') &&
|
|
|
|
|
!comp.type.includes('pico') &&
|
|
|
|
|
!comp.type.includes('esp32'),
|
|
|
|
|
);
|
|
|
|
|
setComponents(
|
|
|
|
|
componentsWithoutBoard.map((comp) => ({
|
|
|
|
|
id: comp.id,
|
2026-05-16 03:11:28 +07:00
|
|
|
metadataId: stripBrandPrefix(comp.type),
|
2026-03-31 05:00:33 +07:00
|
|
|
x: comp.x,
|
|
|
|
|
y: comp.y,
|
|
|
|
|
properties: comp.properties,
|
|
|
|
|
})),
|
|
|
|
|
);
|
|
|
|
|
|
2026-06-04 02:30:42 +07:00
|
|
|
// A board-less example with a custom chip must START STOPPED so the Run
|
|
|
|
|
// button is enabled: the chip needs an explicit Run to compile its
|
|
|
|
|
// WASM/ROM and begin executing. Pure analog/digital circuits stay live
|
|
|
|
|
// (paused=false) as before.
|
|
|
|
|
if (isBoardless) {
|
|
|
|
|
const hasCustomChip = componentsWithoutBoard.some(
|
|
|
|
|
(c) => stripBrandPrefix(c.type) === 'custom-chip',
|
|
|
|
|
);
|
|
|
|
|
useElectricalStore.getState().setPaused(hasCustomChip);
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-21 22:59:59 +07:00
|
|
|
// After possibly removing every board, re-read activeBoardId.
|
|
|
|
|
const liveActiveBoardId = useSimulatorStore.getState().activeBoardId;
|
|
|
|
|
// For analog (board-less) examples we leave any 'arduino-uno' references
|
|
|
|
|
// in wires untouched — there shouldn't be any, but if there are we'd
|
|
|
|
|
// rather emit a dangling endpoint than silently graft them onto a board
|
|
|
|
|
// that no longer exists.
|
|
|
|
|
const remapBoardId = (id: string) =>
|
|
|
|
|
isBoardComponent(id) && liveActiveBoardId ? liveActiveBoardId : id;
|
2026-03-31 05:00:33 +07:00
|
|
|
|
|
|
|
|
setWires(
|
|
|
|
|
example.wires.map((wire) => ({
|
|
|
|
|
id: wire.id,
|
2026-04-22 02:45:45 +07:00
|
|
|
start: {
|
|
|
|
|
componentId: remapBoardId(wire.start.componentId),
|
|
|
|
|
pinName: wire.start.pinName,
|
|
|
|
|
x: 0,
|
|
|
|
|
y: 0,
|
|
|
|
|
},
|
|
|
|
|
end: {
|
|
|
|
|
componentId: remapBoardId(wire.end.componentId),
|
|
|
|
|
pinName: wire.end.pinName,
|
|
|
|
|
x: 0,
|
|
|
|
|
y: 0,
|
|
|
|
|
},
|
2026-03-31 05:00:33 +07:00
|
|
|
color: wire.color,
|
|
|
|
|
waypoints: [],
|
|
|
|
|
})),
|
|
|
|
|
);
|
2026-04-16 18:39:47 +07:00
|
|
|
recalculateAllWirePositions();
|
2026-03-31 05:00:33 +07:00
|
|
|
}
|
2026-06-07 11:07:48 +07:00
|
|
|
|
|
|
|
|
// P2.4 — assign this example's declared manifest to every board it created
|
|
|
|
|
// (the per-board compile scope). Examples declare one library set today, so
|
|
|
|
|
// each board gets it; the user can refine per board via velxio.json.
|
|
|
|
|
{
|
|
|
|
|
const sim = useSimulatorStore.getState();
|
|
|
|
|
const libs =
|
|
|
|
|
example.libraries && example.libraries.length ? example.libraries : undefined;
|
|
|
|
|
for (const b of sim.boards) sim.updateBoard(b.id, { libraries: libs });
|
|
|
|
|
}
|
2026-03-31 05:00:33 +07:00
|
|
|
}
|