2026-03-07 10:14:35 +07:00
/ * *
* 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 {
2026-03-09 20:08:14 +07:00
boardType : 'arduino-uno' | 'arduino-nano' | 'arduino-mega' | 'raspberry-pi-pico' ;
2026-03-08 05:29:23 +07:00
boardPosition : { x : number ; y : number } ;
2026-03-07 10:14:35 +07:00
components : VelxioComponent [ ] ;
wires : Wire [ ] ;
files : Array < { name : string ; content : string } > ;
2026-03-10 11:25:49 +07:00
/** Library names parsed from libraries.txt. Includes both standard Arduino Library Manager names and Wokwi-hosted entries in the form "LibName@wokwi:hash". */
2026-03-09 22:53:24 +07:00
libraries : string [ ] ;
2026-03-07 10:14:35 +07:00
}
// ── Board mappings ────────────────────────────────────────────────────────────
// Wokwi board type → Velxio boardType
2026-03-09 20:08:14 +07:00
const WOKWI_TYPE_TO_BOARD : Record < string , ' arduino - uno ' | ' arduino - nano ' | ' arduino - mega ' | ' raspberry - pi - pico ' > = {
2026-03-07 10:14:35 +07:00
'wokwi-arduino-uno' : 'arduino-uno' ,
2026-03-08 09:14:33 +07:00
'wokwi-arduino-nano' : 'arduino-nano' ,
2026-03-09 20:08:14 +07:00
'wokwi-arduino-mega' : 'arduino-mega' ,
2026-03-07 10:14:35 +07:00
'wokwi-raspberry-pi-pico' : 'raspberry-pi-pico' ,
} ;
// Velxio boardType → Wokwi type
const BOARD_TO_WOKWI_TYPE : Record < string , string > = {
'arduino-uno' : 'wokwi-arduino-uno' ,
2026-03-08 09:14:33 +07:00
'arduino-nano' : 'wokwi-arduino-nano' ,
2026-03-09 20:08:14 +07:00
'arduino-mega' : 'wokwi-arduino-mega' ,
2026-03-07 10:14:35 +07:00
'raspberry-pi-pico' : 'wokwi-raspberry-pi-pico' ,
} ;
// Velxio boardType → default Wokwi part id
const BOARD_TO_WOKWI_ID : Record < string , string > = {
'arduino-uno' : 'uno' ,
2026-03-08 09:14:33 +07:00
'arduino-nano' : 'nano' ,
2026-03-09 20:08:14 +07:00
'arduino-mega' : 'mega' ,
2026-03-07 10:14:35 +07:00
'raspberry-pi-pico' : 'pico' ,
} ;
2026-03-09 02:30:49 +07:00
// ── Pin name aliases ─────────────────────────────────────────────────────────
// Maps Wokwi connection "signal" pin names to wokwi-element physical pin names.
// Wokwi boards (e.g. board-ssd1306) use different naming than the bare elements.
const COMPONENT_PIN_ALIASES : Record < string , Record < string , string > > = {
'ssd1306' : {
'SDA' : 'DATA' ,
'SCL' : 'CLK' ,
'VCC' : 'VIN' ,
} ,
} ;
function normalizePinName ( metadataId : string , pinName : string ) : string {
return COMPONENT_PIN_ALIASES [ metadataId ] ? . [ pinName ] ? ? pinName ;
}
2026-03-07 10:14:35 +07:00
// ── 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 } ` ;
}
2026-03-09 23:32:04 +07:00
// ── Library list parser ───────────────────────────────────────────────────────
/ * *
* Parse the contents of a Wokwi libraries . txt file .
* - Strips blank lines and # comments
2026-03-10 11:25:49 +07:00
* - Includes Wokwi - hosted entries in the form name @wokwi : hash
* so the backend can download and install them from wokwi . com
2026-03-09 23:32:04 +07:00
* /
export function parseLibrariesTxt ( content : string ) : string [ ] {
const libs : string [ ] = [ ] ;
for ( const raw of content . split ( '\n' ) ) {
const line = raw . trim ( ) ;
2026-03-10 11:25:49 +07:00
if ( ! line || line . startsWith ( '#' ) ) continue ;
2026-03-09 23:32:04 +07:00
libs . push ( line ) ;
}
return libs ;
}
2026-03-07 10:14:35 +07:00
// ── Export ────────────────────────────────────────────────────────────────────
export async function exportToWokwiZip (
files : Array < { name : string ; content : string } > ,
components : VelxioComponent [ ] ,
wires : Wire [ ] ,
boardType : string ,
projectName : string ,
2026-03-08 05:29:23 +07:00
boardPosition : { x : number ; y : number } = { x : 50 , y : 50 } ,
2026-03-07 10:14:35 +07:00
) : 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
2026-03-08 05:29:23 +07:00
// Subtract boardPosition so coords are relative to the board
2026-03-07 10:14:35 +07:00
const parts : WokwiPart [ ] = [
{ type : boardWokwiType , id : boardId , top : 0 , left : 0 , attrs : { } } ,
. . . components . map ( ( c ) = > ( {
type : metadataIdToWokwiType ( c . metadataId ) ,
id : c.id ,
2026-03-08 05:29:23 +07:00
top : Math.round ( c . y - boardPosition . y ) ,
left : Math.round ( c . x - boardPosition . x ) ,
2026-03-07 10:14:35 +07:00
attrs : c.properties as Record < string , unknown > ,
} ) ) ,
] ;
// Build connections
const connections : [ string , string , string , string [ ] ] [ ] = wires . map ( ( w ) = > {
2026-03-08 09:14:33 +07:00
const isBoardStart = w . start . componentId === 'arduino-uno' || w . start . componentId === 'arduino-nano' || w . start . componentId === 'nano-rp2040' ;
const isBoardEnd = w . end . componentId === 'arduino-uno' || w . end . componentId === 'arduino-nano' || w . end . componentId === 'nano-rp2040' ;
const startId = isBoardStart ? boardId : w.start.componentId ;
const endId = isBoardEnd ? boardId : w.end.componentId ;
2026-03-07 10:14:35 +07:00
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' ;
2026-03-08 09:14:33 +07:00
// Velxio internal component ID for the board element (must match DOM element id)
const VELXIO_BOARD_ID : Record < string , string > = {
'arduino-uno' : 'arduino-uno' ,
'arduino-nano' : 'arduino-nano' ,
2026-03-09 20:08:14 +07:00
'arduino-mega' : 'arduino-mega' ,
2026-03-08 09:14:33 +07:00
'raspberry-pi-pico' : 'nano-rp2040' ,
} ;
const velxioBoardId = VELXIO_BOARD_ID [ boardType ] ? ? 'arduino-uno' ;
2026-03-09 02:30:49 +07:00
// Board position from diagram. Apply a minimum offset so the board is never
// crammed against the canvas top-left corner (Wokwi diagrams often use 0,0).
const MIN_OFFSET = 50 ;
const rawBoardX = boardPart ? . left ? ? MIN_OFFSET ;
const rawBoardY = boardPart ? . top ? ? MIN_OFFSET ;
const offsetX = rawBoardX < MIN_OFFSET ? MIN_OFFSET - rawBoardX : 0 ;
const offsetY = rawBoardY < MIN_OFFSET ? MIN_OFFSET - rawBoardY : 0 ;
2026-03-08 05:29:23 +07:00
const boardPosition = {
2026-03-09 02:30:49 +07:00
x : rawBoardX + offsetX ,
y : rawBoardY + offsetY ,
2026-03-08 05:29:23 +07:00
} ;
2026-03-08 04:26:47 +07:00
2026-03-09 02:30:49 +07:00
// Convert non-board parts to Velxio components.
// Apply the same offset so components keep their relative position to the board.
2026-03-07 10:14:35 +07:00
const components : VelxioComponent [ ] = diagram . parts
. filter ( ( p ) = > ! WOKWI_TYPE_TO_BOARD [ p . type ] )
. map ( ( p ) = > ( {
id : p.id ,
metadataId : wokwiTypeToMetadataId ( p . type ) ,
2026-03-09 02:30:49 +07:00
x : p.left + offsetX ,
y : p.top + offsetY ,
2026-03-07 10:14:35 +07:00
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
2026-03-08 09:14:33 +07:00
const startId = startCompRaw === boardId ? velxioBoardId : startCompRaw ;
const endId = endCompRaw === boardId ? velxioBoardId : endCompRaw ;
2026-03-07 10:14:35 +07:00
2026-03-09 02:30:49 +07:00
// Normalize pin names: Wokwi uses signal names (SDA, SCL, VCC) while
// wokwi-elements use physical/board pin names (DATA, CLK, VIN).
const startMetadataId = components . find ( ( c ) = > c . id === startId ) ? . metadataId ? ? '' ;
const endMetadataId = components . find ( ( c ) = > c . id === endId ) ? . metadataId ? ? '' ;
const normalizedStartPin = normalizePinName ( startMetadataId , startPin ) ;
const normalizedEndPin = normalizePinName ( endMetadataId , endPin ) ;
2026-03-07 10:14:35 +07:00
return {
id : ` wire- ${ i } - ${ Date . now ( ) } ` ,
2026-03-09 02:30:49 +07:00
start : { componentId : startId , pinName : normalizedStartPin , x : 0 , y : 0 } ,
end : { componentId : endId , pinName : normalizedEndPin , x : 0 , y : 0 } ,
2026-03-07 10:14:35 +07:00
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 ) ;
} ) ;
2026-03-09 23:32:04 +07:00
// Parse libraries.txt
2026-03-09 22:53:24 +07:00
const libraries : string [ ] = [ ] ;
const libEntry = zip . file ( 'libraries.txt' ) ;
if ( libEntry ) {
2026-03-09 23:32:04 +07:00
libraries . push ( . . . parseLibrariesTxt ( await libEntry . async ( 'string' ) ) ) ;
2026-03-09 22:53:24 +07:00
}
return { boardType , boardPosition , components , wires , files , libraries } ;
2026-03-07 10:14:35 +07:00
}