feat: add Wokwi zip import/export functionality and update dependencies
parent
44293ba000
commit
88c5f3b19f
|
|
@ -76,4 +76,5 @@ wokwi-libs/*/.cache/
|
|||
!wokwi-libs/wokwi-features/
|
||||
.claude/settings.local.json
|
||||
.history/*
|
||||
.daveagent/*
|
||||
.daveagent/*
|
||||
example_zip/*
|
||||
|
|
|
|||
|
|
@ -1,17 +1,18 @@
|
|||
{
|
||||
"name": "frontend",
|
||||
"version": "0.0.0",
|
||||
"name": "velxio",
|
||||
"version": "1.0.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "frontend",
|
||||
"version": "0.0.0",
|
||||
"name": "velxio",
|
||||
"version": "1.0.0",
|
||||
"dependencies": {
|
||||
"@monaco-editor/react": "^4.7.0",
|
||||
"@wokwi/elements": "file:../wokwi-libs/wokwi-elements",
|
||||
"avr8js": "file:../wokwi-libs/avr8js",
|
||||
"axios": "^1.13.6",
|
||||
"jszip": "^3.10.1",
|
||||
"react": "^19.2.0",
|
||||
"react-dom": "^19.2.0",
|
||||
"react-router-dom": "^7.13.1",
|
||||
|
|
@ -19,6 +20,7 @@
|
|||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.39.1",
|
||||
"@types/jszip": "^3.4.0",
|
||||
"@types/node": "^24.11.0",
|
||||
"@types/react": "^19.2.7",
|
||||
"@types/react-dom": "^19.2.3",
|
||||
|
|
@ -1796,6 +1798,16 @@
|
|||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/jszip": {
|
||||
"version": "3.4.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/jszip/-/jszip-3.4.0.tgz",
|
||||
"integrity": "sha512-GFHqtQQP3R4NNuvZH3hNCYD0NbyBZ42bkN7kO3NDrU/SnvIZWMS8Bp38XCsRKBT5BXvgm0y1zqpZWp/ZkRzBzg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"jszip": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "24.11.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.11.0.tgz",
|
||||
|
|
@ -2638,6 +2650,12 @@
|
|||
"url": "https://opencollective.com/express"
|
||||
}
|
||||
},
|
||||
"node_modules/core-util-is": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz",
|
||||
"integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/cross-spawn": {
|
||||
"version": "7.0.6",
|
||||
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
|
||||
|
|
@ -3486,6 +3504,12 @@
|
|||
"node": ">= 4"
|
||||
}
|
||||
},
|
||||
"node_modules/immediate": {
|
||||
"version": "3.0.6",
|
||||
"resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz",
|
||||
"integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/import-fresh": {
|
||||
"version": "3.3.1",
|
||||
"resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz",
|
||||
|
|
@ -3513,6 +3537,12 @@
|
|||
"node": ">=0.8.19"
|
||||
}
|
||||
},
|
||||
"node_modules/inherits": {
|
||||
"version": "2.0.4",
|
||||
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
|
||||
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/is-extglob": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
|
||||
|
|
@ -3543,6 +3573,12 @@
|
|||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/isarray": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
|
||||
"integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/isexe": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
|
||||
|
|
@ -3697,6 +3733,18 @@
|
|||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/jszip": {
|
||||
"version": "3.10.1",
|
||||
"resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz",
|
||||
"integrity": "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==",
|
||||
"license": "(MIT OR GPL-3.0-or-later)",
|
||||
"dependencies": {
|
||||
"lie": "~3.3.0",
|
||||
"pako": "~1.0.2",
|
||||
"readable-stream": "~2.3.6",
|
||||
"setimmediate": "^1.0.5"
|
||||
}
|
||||
},
|
||||
"node_modules/keyv": {
|
||||
"version": "4.5.4",
|
||||
"resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
|
||||
|
|
@ -3721,6 +3769,15 @@
|
|||
"node": ">= 0.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/lie": {
|
||||
"version": "3.3.0",
|
||||
"resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz",
|
||||
"integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"immediate": "~3.0.5"
|
||||
}
|
||||
},
|
||||
"node_modules/locate-path": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz",
|
||||
|
|
@ -3990,6 +4047,12 @@
|
|||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/pako": {
|
||||
"version": "1.0.11",
|
||||
"resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz",
|
||||
"integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==",
|
||||
"license": "(MIT AND Zlib)"
|
||||
},
|
||||
"node_modules/parent-module": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
|
||||
|
|
@ -4102,6 +4165,12 @@
|
|||
"node": ">= 0.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/process-nextick-args": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
|
||||
"integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/proxy-from-env": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
|
||||
|
|
@ -4187,6 +4256,21 @@
|
|||
"react-dom": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/readable-stream": {
|
||||
"version": "2.3.8",
|
||||
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz",
|
||||
"integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"core-util-is": "~1.0.0",
|
||||
"inherits": "~2.0.3",
|
||||
"isarray": "~1.0.0",
|
||||
"process-nextick-args": "~2.0.0",
|
||||
"safe-buffer": "~5.1.1",
|
||||
"string_decoder": "~1.1.1",
|
||||
"util-deprecate": "~1.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/require-from-string": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz",
|
||||
|
|
@ -4207,6 +4291,12 @@
|
|||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/safe-buffer": {
|
||||
"version": "5.1.2",
|
||||
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
|
||||
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/saxes": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz",
|
||||
|
|
@ -4242,6 +4332,12 @@
|
|||
"integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/setimmediate": {
|
||||
"version": "1.0.5",
|
||||
"resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz",
|
||||
"integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/shebang-command": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
|
||||
|
|
@ -4317,6 +4413,15 @@
|
|||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/string_decoder": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
|
||||
"integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"safe-buffer": "~5.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/strip-json-comments": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz",
|
||||
|
|
@ -4572,6 +4677,12 @@
|
|||
"punycode": "^2.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/util-deprecate": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
|
||||
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/vite": {
|
||||
"version": "7.3.1",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-7.3.1.tgz",
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@
|
|||
"@wokwi/elements": "file:../wokwi-libs/wokwi-elements",
|
||||
"avr8js": "file:../wokwi-libs/avr8js",
|
||||
"axios": "^1.13.6",
|
||||
"jszip": "^3.10.1",
|
||||
"react": "^19.2.0",
|
||||
"react-dom": "^19.2.0",
|
||||
"react-router-dom": "^7.13.1",
|
||||
|
|
@ -30,6 +31,7 @@
|
|||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.39.1",
|
||||
"@types/jszip": "^3.4.0",
|
||||
"@types/node": "^24.11.0",
|
||||
"@types/react": "^19.2.7",
|
||||
"@types/react-dom": "^19.2.3",
|
||||
|
|
|
|||
|
|
@ -1,10 +1,11 @@
|
|||
import { useState, useCallback } from 'react';
|
||||
import { useState, useCallback, useRef } from 'react';
|
||||
import { useEditorStore } from '../../store/useEditorStore';
|
||||
import { useSimulatorStore, BOARD_FQBN, BOARD_LABELS } from '../../store/useSimulatorStore';
|
||||
import { compileCode } from '../../services/compilation';
|
||||
import { LibraryManagerModal } from '../simulator/LibraryManagerModal';
|
||||
import { parseCompileResult } from '../../utils/compilationLogger';
|
||||
import type { CompilationLog } from '../../utils/compilationLogger';
|
||||
import { exportToWokwiZip, importFromWokwiZip } from '../../utils/wokwiZip';
|
||||
import './EditorToolbar.css';
|
||||
|
||||
interface EditorToolbarProps {
|
||||
|
|
@ -29,6 +30,7 @@ export const EditorToolbar = ({ consoleOpen, setConsoleOpen, compileLogs: _compi
|
|||
const [compiling, setCompiling] = useState(false);
|
||||
const [message, setMessage] = useState<{ type: 'success' | 'error'; text: string } | null>(null);
|
||||
const [libManagerOpen, setLibManagerOpen] = useState(false);
|
||||
const importInputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
const addLog = useCallback((log: CompilationLog) => {
|
||||
setCompileLogs((prev: CompilationLog[]) => [...prev, log]);
|
||||
|
|
@ -93,6 +95,36 @@ export const EditorToolbar = ({ consoleOpen, setConsoleOpen, compileLogs: _compi
|
|||
setMessage(null);
|
||||
};
|
||||
|
||||
const handleExport = async () => {
|
||||
try {
|
||||
const { components, wires } = useSimulatorStore.getState();
|
||||
const projectName = files.find((f) => f.name.endsWith('.ino'))?.name.replace('.ino', '') || 'velxio-project';
|
||||
await exportToWokwiZip(files, components, wires, boardType, projectName);
|
||||
} catch (err) {
|
||||
setMessage({ type: 'error', text: 'Export failed.' });
|
||||
}
|
||||
};
|
||||
|
||||
const handleImportFile = async (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const file = e.target.files?.[0];
|
||||
if (!importInputRef.current) return;
|
||||
importInputRef.current.value = '';
|
||||
if (!file) return;
|
||||
try {
|
||||
const result = await importFromWokwiZip(file);
|
||||
const { loadFiles } = useEditorStore.getState();
|
||||
const { setComponents, setWires, setBoardType, stopSimulation } = useSimulatorStore.getState();
|
||||
stopSimulation();
|
||||
if (result.boardType) setBoardType(result.boardType);
|
||||
setComponents(result.components);
|
||||
setWires(result.wires);
|
||||
if (result.files.length > 0) loadFiles(result.files);
|
||||
setMessage({ type: 'success', text: `Imported ${file.name}` });
|
||||
} catch (err: any) {
|
||||
setMessage({ type: 'error', text: err?.message || 'Import failed.' });
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="editor-toolbar">
|
||||
|
|
@ -174,6 +206,39 @@ export const EditorToolbar = ({ consoleOpen, setConsoleOpen, compileLogs: _compi
|
|||
</span>
|
||||
)}
|
||||
|
||||
{/* Import Wokwi zip */}
|
||||
<input
|
||||
ref={importInputRef}
|
||||
type="file"
|
||||
accept=".zip"
|
||||
style={{ display: 'none' }}
|
||||
onChange={handleImportFile}
|
||||
/>
|
||||
<button
|
||||
onClick={() => importInputRef.current?.click()}
|
||||
className="tb-btn tb-btn-lib"
|
||||
title="Import Wokwi zip"
|
||||
>
|
||||
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" />
|
||||
<polyline points="7 10 12 15 17 10" />
|
||||
<line x1="12" y1="15" x2="12" y2="3" />
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
{/* Export Wokwi zip */}
|
||||
<button
|
||||
onClick={handleExport}
|
||||
className="tb-btn tb-btn-lib"
|
||||
title="Export as Wokwi zip"
|
||||
>
|
||||
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" />
|
||||
<polyline points="17 8 12 3 7 8" />
|
||||
<line x1="12" y1="3" x2="12" y2="15" />
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
{/* Libraries */}
|
||||
<button
|
||||
onClick={() => setLibManagerOpen(true)}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,253 @@
|
|||
/**
|
||||
* Wokwi zip import/export
|
||||
*
|
||||
* Converts between Wokwi's diagram.json format and Velxio's internal
|
||||
* component/wire format, bundling everything into a .zip file.
|
||||
*
|
||||
* Wokwi zip structure:
|
||||
* diagram.json — parts + connections
|
||||
* sketch.ino — main sketch (or projectname.ino)
|
||||
* *.h / *.cpp — additional files
|
||||
* libraries.txt — optional library list
|
||||
* wokwi-project.txt — optional metadata
|
||||
*/
|
||||
|
||||
import JSZip from 'jszip';
|
||||
import type { Wire } from '../types/wire';
|
||||
|
||||
// ── Type definitions ──────────────────────────────────────────────────────────
|
||||
|
||||
interface WokwiPart {
|
||||
type: string;
|
||||
id: string;
|
||||
top: number;
|
||||
left: number;
|
||||
rotate?: number;
|
||||
attrs: Record<string, unknown>;
|
||||
}
|
||||
|
||||
interface WokwiDiagram {
|
||||
version: number;
|
||||
author: string;
|
||||
editor: string;
|
||||
parts: WokwiPart[];
|
||||
connections: [string, string, string, string[]][];
|
||||
}
|
||||
|
||||
export interface VelxioComponent {
|
||||
id: string;
|
||||
metadataId: string;
|
||||
x: number;
|
||||
y: number;
|
||||
properties: Record<string, unknown>;
|
||||
}
|
||||
|
||||
export interface ImportResult {
|
||||
boardType: 'arduino-uno' | 'raspberry-pi-pico';
|
||||
components: VelxioComponent[];
|
||||
wires: Wire[];
|
||||
files: Array<{ name: string; content: string }>;
|
||||
}
|
||||
|
||||
// ── Board mappings ────────────────────────────────────────────────────────────
|
||||
|
||||
// Wokwi board type → Velxio boardType
|
||||
const WOKWI_TYPE_TO_BOARD: Record<string, 'arduino-uno' | 'raspberry-pi-pico'> = {
|
||||
'wokwi-arduino-uno': 'arduino-uno',
|
||||
'wokwi-arduino-nano': 'arduino-uno',
|
||||
'wokwi-arduino-mega': 'arduino-uno',
|
||||
'wokwi-raspberry-pi-pico': 'raspberry-pi-pico',
|
||||
};
|
||||
|
||||
// Velxio boardType → Wokwi type
|
||||
const BOARD_TO_WOKWI_TYPE: Record<string, string> = {
|
||||
'arduino-uno': 'wokwi-arduino-uno',
|
||||
'raspberry-pi-pico': 'wokwi-raspberry-pi-pico',
|
||||
};
|
||||
|
||||
// Velxio boardType → default Wokwi part id
|
||||
const BOARD_TO_WOKWI_ID: Record<string, string> = {
|
||||
'arduino-uno': 'uno',
|
||||
'raspberry-pi-pico': 'pico',
|
||||
};
|
||||
|
||||
// ── Color helpers ─────────────────────────────────────────────────────────────
|
||||
|
||||
const COLOR_NAME_TO_HEX: Record<string, string> = {
|
||||
red: '#ff0000', black: '#000000', green: '#00c800', blue: '#0000ff',
|
||||
yellow: '#ffff00', orange: '#ff8800', white: '#ffffff', gray: '#808080',
|
||||
grey: '#808080', purple: '#800080', pink: '#ff69b4', cyan: '#00ffff',
|
||||
gold: '#ffd700', brown: '#8b4513', magenta: '#ff00ff', lime: '#00ff00',
|
||||
violet: '#ee82ee', maroon: '#800000', navy: '#000080', teal: '#008080',
|
||||
};
|
||||
|
||||
const HEX_TO_COLOR_NAME: Record<string, string> = {
|
||||
'#ff0000': 'red', '#000000': 'black', '#00ff00': 'green', '#00c800': 'green',
|
||||
'#0000ff': 'blue', '#ffff00': 'yellow', '#ff8800': 'orange', '#ffffff': 'white',
|
||||
'#808080': 'gray', '#800080': 'purple', '#00ffff': 'cyan', '#ffd700': 'gold',
|
||||
};
|
||||
|
||||
function colorToHex(color: string): string {
|
||||
if (!color) return '#888888';
|
||||
if (color.startsWith('#')) return color.toLowerCase();
|
||||
return COLOR_NAME_TO_HEX[color.toLowerCase()] ?? '#888888';
|
||||
}
|
||||
|
||||
function hexToColorName(hex: string): string {
|
||||
return HEX_TO_COLOR_NAME[hex.toLowerCase()] ?? hex;
|
||||
}
|
||||
|
||||
// ── Type conversion ───────────────────────────────────────────────────────────
|
||||
|
||||
function wokwiTypeToMetadataId(type: string): string {
|
||||
if (type.startsWith('wokwi-')) return type.slice(6);
|
||||
if (type.startsWith('board-')) return type.slice(6);
|
||||
return type;
|
||||
}
|
||||
|
||||
function metadataIdToWokwiType(metadataId: string): string {
|
||||
return `wokwi-${metadataId}`;
|
||||
}
|
||||
|
||||
// ── Export ────────────────────────────────────────────────────────────────────
|
||||
|
||||
export async function exportToWokwiZip(
|
||||
files: Array<{ name: string; content: string }>,
|
||||
components: VelxioComponent[],
|
||||
wires: Wire[],
|
||||
boardType: string,
|
||||
projectName: string,
|
||||
): Promise<void> {
|
||||
const zip = new JSZip();
|
||||
|
||||
const boardWokwiType = BOARD_TO_WOKWI_TYPE[boardType] ?? 'wokwi-arduino-uno';
|
||||
const boardId = BOARD_TO_WOKWI_ID[boardType] ?? 'uno';
|
||||
|
||||
// Build parts — board first, then user components
|
||||
const parts: WokwiPart[] = [
|
||||
{ type: boardWokwiType, id: boardId, top: 0, left: 0, attrs: {} },
|
||||
...components.map((c) => ({
|
||||
type: metadataIdToWokwiType(c.metadataId),
|
||||
id: c.id,
|
||||
top: Math.round(c.y),
|
||||
left: Math.round(c.x),
|
||||
attrs: c.properties as Record<string, unknown>,
|
||||
})),
|
||||
];
|
||||
|
||||
// Build connections
|
||||
const connections: [string, string, string, string[]][] = wires.map((w) => {
|
||||
const startId = w.start.componentId === 'arduino-uno' ? boardId : w.start.componentId;
|
||||
const endId = w.end.componentId === 'arduino-uno' ? boardId : w.end.componentId;
|
||||
return [
|
||||
`${startId}:${w.start.pinName}`,
|
||||
`${endId}:${w.end.pinName}`,
|
||||
hexToColorName(w.color ?? '#888888'),
|
||||
[],
|
||||
];
|
||||
});
|
||||
|
||||
const diagram: WokwiDiagram = {
|
||||
version: 1,
|
||||
author: 'Velxio',
|
||||
editor: 'wokwi',
|
||||
parts,
|
||||
connections,
|
||||
};
|
||||
|
||||
zip.file('diagram.json', JSON.stringify(diagram, null, 2));
|
||||
zip.file('wokwi-project.txt', `Exported from Velxio\n\nSimulate this project on https://velxio.dev\n`);
|
||||
|
||||
for (const f of files) {
|
||||
zip.file(f.name, f.content);
|
||||
}
|
||||
|
||||
const blob = await zip.generateAsync({ type: 'blob' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = `${(projectName || 'velxio-project').replace(/[^a-z0-9_-]/gi, '-')}.zip`;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
setTimeout(() => URL.revokeObjectURL(url), 1000);
|
||||
}
|
||||
|
||||
// ── Import ────────────────────────────────────────────────────────────────────
|
||||
|
||||
export async function importFromWokwiZip(file: File): Promise<ImportResult> {
|
||||
const zip = await JSZip.loadAsync(file);
|
||||
|
||||
// diagram.json is required
|
||||
const diagramEntry = zip.file('diagram.json');
|
||||
if (!diagramEntry) throw new Error('No diagram.json found in the zip file.');
|
||||
|
||||
const diagramText = await diagramEntry.async('string');
|
||||
const diagram: WokwiDiagram = JSON.parse(diagramText);
|
||||
|
||||
// Detect board
|
||||
const boardPart = diagram.parts.find((p) => WOKWI_TYPE_TO_BOARD[p.type]);
|
||||
const boardType = boardPart ? WOKWI_TYPE_TO_BOARD[boardPart.type] : 'arduino-uno';
|
||||
const boardId = boardPart?.id ?? 'uno';
|
||||
|
||||
// Convert non-board parts to Velxio components
|
||||
const components: VelxioComponent[] = diagram.parts
|
||||
.filter((p) => !WOKWI_TYPE_TO_BOARD[p.type])
|
||||
.map((p) => ({
|
||||
id: p.id,
|
||||
metadataId: wokwiTypeToMetadataId(p.type),
|
||||
x: p.left,
|
||||
y: p.top,
|
||||
properties: { ...p.attrs },
|
||||
}));
|
||||
|
||||
// Convert connections to Velxio wires
|
||||
const wires: Wire[] = diagram.connections.map((conn, i) => {
|
||||
const [startStr, endStr, color] = conn;
|
||||
const colonA = startStr.indexOf(':');
|
||||
const colonB = endStr.indexOf(':');
|
||||
const startCompRaw = colonA >= 0 ? startStr.slice(0, colonA) : startStr;
|
||||
const startPin = colonA >= 0 ? startStr.slice(colonA + 1) : '';
|
||||
const endCompRaw = colonB >= 0 ? endStr.slice(0, colonB) : endStr;
|
||||
const endPin = colonB >= 0 ? endStr.slice(colonB + 1) : '';
|
||||
|
||||
// Remap board part id → Velxio internal board id
|
||||
const startId = startCompRaw === boardId ? 'arduino-uno' : startCompRaw;
|
||||
const endId = endCompRaw === boardId ? 'arduino-uno' : endCompRaw;
|
||||
|
||||
return {
|
||||
id: `wire-${i}-${Date.now()}`,
|
||||
start: { componentId: startId, pinName: startPin, x: 0, y: 0 },
|
||||
end: { componentId: endId, pinName: endPin, x: 0, y: 0 },
|
||||
controlPoints: [],
|
||||
color: colorToHex(color),
|
||||
signalType: 'digital' as const,
|
||||
isValid: true,
|
||||
};
|
||||
});
|
||||
|
||||
// Read code files (.ino, .h, .cpp, .c)
|
||||
const CODE_EXTS = new Set(['.ino', '.h', '.cpp', '.c']);
|
||||
const files: Array<{ name: string; content: string }> = [];
|
||||
|
||||
for (const [filename, entry] of Object.entries(zip.files)) {
|
||||
if (entry.dir) continue;
|
||||
const basename = filename.split('/').pop() ?? filename;
|
||||
const ext = '.' + basename.split('.').pop()!.toLowerCase();
|
||||
if (CODE_EXTS.has(ext)) {
|
||||
const content = await entry.async('string');
|
||||
files.push({ name: basename, content });
|
||||
}
|
||||
}
|
||||
|
||||
// Sort: .ino first, then alphabetically
|
||||
files.sort((a, b) => {
|
||||
const aIno = a.name.endsWith('.ino');
|
||||
const bIno = b.name.endsWith('.ino');
|
||||
if (aIno && !bIno) return -1;
|
||||
if (!aIno && bIno) return 1;
|
||||
return a.name.localeCompare(b.name);
|
||||
});
|
||||
|
||||
return { boardType, components, wires, files };
|
||||
}
|
||||
Loading…
Reference in New Issue