feat(oss): portable .vlx project export/import for self-hosters
Phase 4 of the OSS / pro split. The OSS image has no auth and no
server-side persistence — without this commit, the user's workspace
was ephemeral (lost on tab refresh). `.vlx` is a single-file JSON
snapshot of the entire workspace (boards, file groups, components,
wires, active board id) that the user can save to disk and reload
later.
New: utils/vlxFile.ts
- buildVlxPayload() / buildVlxBlob() — pure snapshot of the current
editor + simulator stores.
- triggerDownloadVlx({ name? }) — anchor-click download with a safe
filename. Returns the filename actually used.
- parseVlxFile(File) — async reader + validator. Checks
format === "velxio-project", version <= 1, and the required
arrays/objects are present. Throws VlxParseError with a human-
readable message on any issue.
- importVlxFile(File) — convenience wrapper that parses AND calls
useSimulatorStore.loadProjectState() with the result.
Format intentionally mirrors the server's POST /api/projects body so
a Pro user can export-from-pro and import-into-OSS losslessly (and
vice-versa once Pro adds an Export button — out of scope here).
lib/proSaveAction.ts: the default (no-overlay) implementation now
calls triggerDownloadVlx() instead of console.info'ing about the
missing handler. The Pro overlay still wins via installSaveActionImpl()
— Save in Pro keeps opening SaveProjectModal. The Save button in OSS
now actually saves.
components/editor/FileExplorer.tsx: new "Open .vlx" button next to
New + Save. Opens a hidden file input; confirms with the user before
replacing the workspace (loadProjectState is destructive); surfaces
VlxParseError messages via window.alert.
Verified with both builds:
- OSS-only: triggerSaveAction → download .vlx; FileExplorer shows
3 buttons (New, Open, Save).
- OSS + overlay: Pro's installSaveActionImpl overrides — Save opens
SaveProjectModal as before. Open .vlx still works (independent
button, not part of the save flow).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 02:33:00 +07:00
|
|
|
/**
|
|
|
|
|
* .vlx file format — portable project export/import for OSS Velxio.
|
|
|
|
|
*
|
|
|
|
|
* Phase 4 of the OSS / pro split. The OSS image has no auth, no DB, no
|
|
|
|
|
* server-side project persistence. The user's work is otherwise ephemeral
|
|
|
|
|
* (lost on tab refresh). `.vlx` is a single-file JSON snapshot that
|
|
|
|
|
* round-trips everything the server-side Save flow captures:
|
|
|
|
|
*
|
|
|
|
|
* {
|
|
|
|
|
* "format": "velxio-project",
|
|
|
|
|
* "version": 1,
|
|
|
|
|
* "exportedAt": "ISO timestamp",
|
|
|
|
|
* "name": "project name (optional)",
|
|
|
|
|
* "boards": [...],
|
|
|
|
|
* "fileGroups": { "<groupId>": [{ name, content }, ...] },
|
|
|
|
|
* "components": [...],
|
|
|
|
|
* "wires": [...],
|
|
|
|
|
* "activeBoardId": "..." | null
|
|
|
|
|
* }
|
|
|
|
|
*
|
|
|
|
|
* Reading: `parseVlxFile(File) → payload` validates and returns a shape
|
|
|
|
|
* directly consumable by `useSimulatorStore.loadProjectState(...)`.
|
|
|
|
|
*
|
|
|
|
|
* Writing: `buildVlxBlob()` snapshots the current store state into a
|
|
|
|
|
* `Blob`, ready to feed an `<a download>` link.
|
|
|
|
|
*
|
|
|
|
|
* The format is INTENTIONALLY identical to the server's POST/PUT body
|
|
|
|
|
* for `/api/projects/`, so a pro user can export-from-Pro / import-into-
|
|
|
|
|
* OSS (and vice-versa) without surprises.
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
import type { BoardInstance } from '../types/board';
|
|
|
|
|
import type { Component } from '../types/component';
|
|
|
|
|
import type { Wire } from '../types/wire';
|
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 } from '../store/useEditorStore';
|
feat(oss): portable .vlx project export/import for self-hosters
Phase 4 of the OSS / pro split. The OSS image has no auth and no
server-side persistence — without this commit, the user's workspace
was ephemeral (lost on tab refresh). `.vlx` is a single-file JSON
snapshot of the entire workspace (boards, file groups, components,
wires, active board id) that the user can save to disk and reload
later.
New: utils/vlxFile.ts
- buildVlxPayload() / buildVlxBlob() — pure snapshot of the current
editor + simulator stores.
- triggerDownloadVlx({ name? }) — anchor-click download with a safe
filename. Returns the filename actually used.
- parseVlxFile(File) — async reader + validator. Checks
format === "velxio-project", version <= 1, and the required
arrays/objects are present. Throws VlxParseError with a human-
readable message on any issue.
- importVlxFile(File) — convenience wrapper that parses AND calls
useSimulatorStore.loadProjectState() with the result.
Format intentionally mirrors the server's POST /api/projects body so
a Pro user can export-from-pro and import-into-OSS losslessly (and
vice-versa once Pro adds an Export button — out of scope here).
lib/proSaveAction.ts: the default (no-overlay) implementation now
calls triggerDownloadVlx() instead of console.info'ing about the
missing handler. The Pro overlay still wins via installSaveActionImpl()
— Save in Pro keeps opening SaveProjectModal. The Save button in OSS
now actually saves.
components/editor/FileExplorer.tsx: new "Open .vlx" button next to
New + Save. Opens a hidden file input; confirms with the user before
replacing the workspace (loadProjectState is destructive); surfaces
VlxParseError messages via window.alert.
Verified with both builds:
- OSS-only: triggerSaveAction → download .vlx; FileExplorer shows
3 buttons (New, Open, Save).
- OSS + overlay: Pro's installSaveActionImpl overrides — Save opens
SaveProjectModal as before. Open .vlx still works (independent
button, not part of the save flow).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 02:33:00 +07:00
|
|
|
import { useSimulatorStore } from '../store/useSimulatorStore';
|
|
|
|
|
|
|
|
|
|
const VLX_FORMAT = 'velxio-project';
|
|
|
|
|
const VLX_VERSION = 1;
|
|
|
|
|
|
|
|
|
|
export interface VlxPayload {
|
|
|
|
|
format: typeof VLX_FORMAT;
|
|
|
|
|
version: number;
|
|
|
|
|
exportedAt: string;
|
|
|
|
|
name?: string;
|
|
|
|
|
boards: Array<{
|
|
|
|
|
id: string;
|
|
|
|
|
boardKind: string;
|
|
|
|
|
x: number;
|
|
|
|
|
y: number;
|
|
|
|
|
activeFileGroupId: string;
|
|
|
|
|
languageMode?: string;
|
|
|
|
|
serialBaudRate?: number;
|
|
|
|
|
}>;
|
|
|
|
|
fileGroups: Record<string, Array<{ name: string; content: string }>>;
|
|
|
|
|
components: Component[];
|
|
|
|
|
wires: Wire[];
|
|
|
|
|
activeBoardId: string | null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function serialisableBoard(b: BoardInstance) {
|
|
|
|
|
return {
|
|
|
|
|
id: b.id,
|
|
|
|
|
boardKind: b.boardKind,
|
|
|
|
|
x: b.x,
|
|
|
|
|
y: b.y,
|
|
|
|
|
activeFileGroupId: b.activeFileGroupId,
|
|
|
|
|
languageMode: b.languageMode,
|
|
|
|
|
serialBaudRate: b.serialBaudRate,
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Snapshot the current editor + simulator state into a VlxPayload object.
|
|
|
|
|
* Pure function: no side effects.
|
|
|
|
|
*/
|
|
|
|
|
export function buildVlxPayload(opts: { name?: string } = {}): VlxPayload {
|
|
|
|
|
const sim = useSimulatorStore.getState();
|
|
|
|
|
const editor = useEditorStore.getState();
|
|
|
|
|
|
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
|
|
|
// Persist file groups referenced by a board, plus each programmable chip's
|
|
|
|
|
// own program group (group-chip-<id>) — otherwise the chip's program would
|
|
|
|
|
// be dropped on export. Stray groups from deleted boards don't round-trip.
|
feat(oss): portable .vlx project export/import for self-hosters
Phase 4 of the OSS / pro split. The OSS image has no auth and no
server-side persistence — without this commit, the user's workspace
was ephemeral (lost on tab refresh). `.vlx` is a single-file JSON
snapshot of the entire workspace (boards, file groups, components,
wires, active board id) that the user can save to disk and reload
later.
New: utils/vlxFile.ts
- buildVlxPayload() / buildVlxBlob() — pure snapshot of the current
editor + simulator stores.
- triggerDownloadVlx({ name? }) — anchor-click download with a safe
filename. Returns the filename actually used.
- parseVlxFile(File) — async reader + validator. Checks
format === "velxio-project", version <= 1, and the required
arrays/objects are present. Throws VlxParseError with a human-
readable message on any issue.
- importVlxFile(File) — convenience wrapper that parses AND calls
useSimulatorStore.loadProjectState() with the result.
Format intentionally mirrors the server's POST /api/projects body so
a Pro user can export-from-pro and import-into-OSS losslessly (and
vice-versa once Pro adds an Export button — out of scope here).
lib/proSaveAction.ts: the default (no-overlay) implementation now
calls triggerDownloadVlx() instead of console.info'ing about the
missing handler. The Pro overlay still wins via installSaveActionImpl()
— Save in Pro keeps opening SaveProjectModal. The Save button in OSS
now actually saves.
components/editor/FileExplorer.tsx: new "Open .vlx" button next to
New + Save. Opens a hidden file input; confirms with the user before
replacing the workspace (loadProjectState is destructive); surfaces
VlxParseError messages via window.alert.
Verified with both builds:
- OSS-only: triggerSaveAction → download .vlx; FileExplorer shows
3 buttons (New, Open, Save).
- OSS + overlay: Pro's installSaveActionImpl overrides — Save opens
SaveProjectModal as before. Open .vlx still works (independent
button, not part of the save flow).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 02:33:00 +07:00
|
|
|
const referencedGroupIds = new Set(sim.boards.map((b) => b.activeFileGroupId));
|
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
|
|
|
for (const c of sim.components) {
|
|
|
|
|
if (c.metadataId !== 'custom-chip') continue;
|
|
|
|
|
const gid = chipFileGroupId(c.id);
|
|
|
|
|
if (editor.fileGroups[gid]?.length) referencedGroupIds.add(gid);
|
|
|
|
|
}
|
feat(oss): portable .vlx project export/import for self-hosters
Phase 4 of the OSS / pro split. The OSS image has no auth and no
server-side persistence — without this commit, the user's workspace
was ephemeral (lost on tab refresh). `.vlx` is a single-file JSON
snapshot of the entire workspace (boards, file groups, components,
wires, active board id) that the user can save to disk and reload
later.
New: utils/vlxFile.ts
- buildVlxPayload() / buildVlxBlob() — pure snapshot of the current
editor + simulator stores.
- triggerDownloadVlx({ name? }) — anchor-click download with a safe
filename. Returns the filename actually used.
- parseVlxFile(File) — async reader + validator. Checks
format === "velxio-project", version <= 1, and the required
arrays/objects are present. Throws VlxParseError with a human-
readable message on any issue.
- importVlxFile(File) — convenience wrapper that parses AND calls
useSimulatorStore.loadProjectState() with the result.
Format intentionally mirrors the server's POST /api/projects body so
a Pro user can export-from-pro and import-into-OSS losslessly (and
vice-versa once Pro adds an Export button — out of scope here).
lib/proSaveAction.ts: the default (no-overlay) implementation now
calls triggerDownloadVlx() instead of console.info'ing about the
missing handler. The Pro overlay still wins via installSaveActionImpl()
— Save in Pro keeps opening SaveProjectModal. The Save button in OSS
now actually saves.
components/editor/FileExplorer.tsx: new "Open .vlx" button next to
New + Save. Opens a hidden file input; confirms with the user before
replacing the workspace (loadProjectState is destructive); surfaces
VlxParseError messages via window.alert.
Verified with both builds:
- OSS-only: triggerSaveAction → download .vlx; FileExplorer shows
3 buttons (New, Open, Save).
- OSS + overlay: Pro's installSaveActionImpl overrides — Save opens
SaveProjectModal as before. Open .vlx still works (independent
button, not part of the save flow).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 02:33:00 +07:00
|
|
|
const fileGroups: VlxPayload['fileGroups'] = {};
|
|
|
|
|
for (const gid of referencedGroupIds) {
|
|
|
|
|
fileGroups[gid] = (editor.fileGroups[gid] ?? []).map((f) => ({
|
|
|
|
|
name: f.name,
|
|
|
|
|
content: f.content,
|
|
|
|
|
}));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
format: VLX_FORMAT,
|
|
|
|
|
version: VLX_VERSION,
|
|
|
|
|
exportedAt: new Date().toISOString(),
|
|
|
|
|
name: opts.name,
|
|
|
|
|
boards: sim.boards.map(serialisableBoard),
|
|
|
|
|
fileGroups,
|
|
|
|
|
components: sim.components,
|
|
|
|
|
wires: sim.wires,
|
|
|
|
|
activeBoardId: sim.activeBoardId,
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** Build a Blob (MIME: application/json) carrying the current state. */
|
|
|
|
|
export function buildVlxBlob(opts: { name?: string } = {}): Blob {
|
|
|
|
|
const payload = buildVlxPayload(opts);
|
|
|
|
|
const json = JSON.stringify(payload, null, 2);
|
|
|
|
|
return new Blob([json], { type: 'application/json' });
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** Sanitise a filename: keep letters, digits, dashes, dots, underscores. */
|
|
|
|
|
function safeFilename(name?: string): string {
|
|
|
|
|
const base = (name ?? 'velxio-project').trim() || 'velxio-project';
|
|
|
|
|
const cleaned = base
|
|
|
|
|
.replace(/[^a-zA-Z0-9._-]+/g, '-')
|
|
|
|
|
.replace(/^-+|-+$/g, '')
|
|
|
|
|
.slice(0, 80);
|
|
|
|
|
return `${cleaned || 'velxio-project'}.vlx`;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Trigger a browser download of the current state as `<name>.vlx`.
|
|
|
|
|
* Returns the filename actually used (for UI feedback).
|
|
|
|
|
*/
|
|
|
|
|
export function triggerDownloadVlx(opts: { name?: string } = {}): string {
|
|
|
|
|
const blob = buildVlxBlob(opts);
|
|
|
|
|
const filename = safeFilename(opts.name);
|
|
|
|
|
const url = URL.createObjectURL(blob);
|
|
|
|
|
const a = document.createElement('a');
|
|
|
|
|
a.href = url;
|
|
|
|
|
a.download = filename;
|
|
|
|
|
document.body.appendChild(a);
|
|
|
|
|
a.click();
|
|
|
|
|
// The browser starts the download immediately. Revoke the URL on the
|
|
|
|
|
// next tick so the click handler has time to settle before GC.
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
document.body.removeChild(a);
|
|
|
|
|
URL.revokeObjectURL(url);
|
|
|
|
|
}, 0);
|
|
|
|
|
return filename;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** Thrown by parseVlxFile when the file isn't a valid .vlx payload. */
|
|
|
|
|
export class VlxParseError extends Error {
|
|
|
|
|
constructor(message: string) {
|
|
|
|
|
super(message);
|
|
|
|
|
this.name = 'VlxParseError';
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function isPlainObject(value: unknown): value is Record<string, unknown> {
|
|
|
|
|
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Validate that `data` is shaped like a VlxPayload. Throws VlxParseError
|
|
|
|
|
* on mismatch with a human-readable reason. Keep the checks defensive —
|
|
|
|
|
* users may edit .vlx files by hand or feed us a wrong file by accident.
|
|
|
|
|
*/
|
|
|
|
|
function validatePayload(data: unknown): VlxPayload {
|
|
|
|
|
if (!isPlainObject(data)) {
|
|
|
|
|
throw new VlxParseError('File is not a JSON object.');
|
|
|
|
|
}
|
|
|
|
|
if (data.format !== VLX_FORMAT) {
|
|
|
|
|
throw new VlxParseError(
|
|
|
|
|
`Not a Velxio project file (expected format="${VLX_FORMAT}", got ${JSON.stringify(
|
|
|
|
|
data.format,
|
|
|
|
|
)}).`,
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
if (typeof data.version !== 'number') {
|
|
|
|
|
throw new VlxParseError('Missing or invalid "version" field.');
|
|
|
|
|
}
|
|
|
|
|
if (data.version > VLX_VERSION) {
|
|
|
|
|
throw new VlxParseError(
|
|
|
|
|
`This file uses .vlx format version ${data.version}, but this Velxio supports up to v${VLX_VERSION}. Update Velxio to open it.`,
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
if (!Array.isArray(data.boards)) {
|
|
|
|
|
throw new VlxParseError('Missing or invalid "boards" array.');
|
|
|
|
|
}
|
|
|
|
|
if (!isPlainObject(data.fileGroups)) {
|
|
|
|
|
throw new VlxParseError('Missing or invalid "fileGroups" object.');
|
|
|
|
|
}
|
|
|
|
|
if (!Array.isArray(data.components)) {
|
|
|
|
|
throw new VlxParseError('Missing or invalid "components" array.');
|
|
|
|
|
}
|
|
|
|
|
if (!Array.isArray(data.wires)) {
|
|
|
|
|
throw new VlxParseError('Missing or invalid "wires" array.');
|
|
|
|
|
}
|
|
|
|
|
return data as unknown as VlxPayload;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Read a File object (from a `<input type="file">` change event or a
|
|
|
|
|
* drop), parse it as JSON, validate, and return the payload. Throws
|
|
|
|
|
* VlxParseError on any failure.
|
|
|
|
|
*/
|
|
|
|
|
export async function parseVlxFile(file: File): Promise<VlxPayload> {
|
|
|
|
|
let text: string;
|
|
|
|
|
try {
|
|
|
|
|
text = await file.text();
|
|
|
|
|
} catch (err) {
|
|
|
|
|
throw new VlxParseError(`Could not read file: ${(err as Error).message}`);
|
|
|
|
|
}
|
|
|
|
|
let parsed: unknown;
|
|
|
|
|
try {
|
|
|
|
|
parsed = JSON.parse(text);
|
|
|
|
|
} catch (err) {
|
|
|
|
|
throw new VlxParseError(`Invalid JSON: ${(err as Error).message}`);
|
|
|
|
|
}
|
|
|
|
|
return validatePayload(parsed);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Convenience wrapper: parse the file AND load its contents into the
|
|
|
|
|
* simulator stores via `loadProjectState`. Returns the parsed payload
|
|
|
|
|
* so the caller can show a confirmation toast or similar.
|
|
|
|
|
*/
|
|
|
|
|
export async function importVlxFile(file: File): Promise<VlxPayload> {
|
|
|
|
|
const payload = await parseVlxFile(file);
|
|
|
|
|
useSimulatorStore.getState().loadProjectState({
|
|
|
|
|
boards: payload.boards as unknown as BoardInstance[],
|
|
|
|
|
fileGroups: payload.fileGroups,
|
|
|
|
|
components: payload.components,
|
|
|
|
|
wires: payload.wires,
|
|
|
|
|
activeBoardId: payload.activeBoardId,
|
|
|
|
|
});
|
|
|
|
|
return payload;
|
|
|
|
|
}
|