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-04-22 02:45:45 +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 > > = {
2026-04-22 02:45:45 +07:00
ssd1306 : {
SDA : 'DATA' ,
SCL : 'CLK' ,
VCC : 'VIN' ,
2026-03-09 02:30:49 +07:00
} ,
} ;
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 > = {
2026-04-22 02:45:45 +07:00
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' ,
2026-03-07 10:14:35 +07:00
} ;
const HEX_TO_COLOR_NAME : Record < string , string > = {
2026-04-22 02:45:45 +07:00
'#ff0000' : 'red' ,
'#000000' : 'black' ,
'#00ff00' : 'green' ,
'#00c800' : 'green' ,
'#0000ff' : 'blue' ,
'#ffff00' : 'yellow' ,
'#ff8800' : 'orange' ,
'#ffffff' : 'white' ,
'#808080' : 'gray' ,
'#800080' : 'purple' ,
'#00ffff' : 'cyan' ,
'#ffd700' : 'gold' ,
2026-03-07 10:14:35 +07:00
} ;
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 {
feat(breadboard): Wokwi-style parts-on-breadboard — hole snapping + invisible seating wires
Parts now plug INTO the breadboard instead of using it as a junction box:
- Drag magnetism: while dragging, the part's anchor pin snaps to the
nearest hole center (9 px range, 9.6 px grid) so parts land perfectly
aligned, like Wokwi.
- Seating: every pin within 4 px of a hole gets an invisible zero-length
wire (Wire.bb) from pin to hole — the exact model Wokwi persists as
["r1:1","bb1:6t.b","",["$bb"]]. Electrically they are ordinary
wires, so the netlist builder, digital trace and SPICE need zero
changes; they are simply not rendered and not hit-testable. Seating
re-computes on every move/rotation (updateComponent), and moving the
breadboard carries its seated parts along.
- Resistors auto-rotate to vertical when dragged over a breadboard
(their 58.8 px pin span bridges the center trench rows b-f exactly).
- Seat tolerance 4 px: absorbs the worst element pin-spacing residual
(~1.6 px) while staying under half the hole pitch, so a pin is never
ambiguous between holes.
Wokwi interchange fixes that fell out of the diagram.json research:
- import maps the top-level rotate attr onto properties.rotation
(previously every rotated part imported flat) and export emits it
back as rotate instead of leaking it into attrs;
- $bb / empty-color connections import as bb seating wires and export
back as ["$bb"] entries, so parts-on-breadboard projects round-trip;
- wokwi-breadboard-half aliases to the full breadboard (hole names are
a strict superset, so every connection stays valid).
Breadboard elements now export their pure hole grids and import cleanly
without a DOM (node tests); geometry + store seating covered by
breadboard-snap.test.ts and breadboard-seating.test.ts.
2026-07-18 13:47:25 +07:00
// Velxio has no half-size breadboard element; the full board is a strict
// superset of its hole names (columns 1-30 exist on both), so every
// connection stays valid — the part just renders longer.
if ( type === 'wokwi-breadboard-half' ) return 'breadboard' ;
2026-03-07 10:14:35 +07:00
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 : { } } ,
feat(breadboard): Wokwi-style parts-on-breadboard — hole snapping + invisible seating wires
Parts now plug INTO the breadboard instead of using it as a junction box:
- Drag magnetism: while dragging, the part's anchor pin snaps to the
nearest hole center (9 px range, 9.6 px grid) so parts land perfectly
aligned, like Wokwi.
- Seating: every pin within 4 px of a hole gets an invisible zero-length
wire (Wire.bb) from pin to hole — the exact model Wokwi persists as
["r1:1","bb1:6t.b","",["$bb"]]. Electrically they are ordinary
wires, so the netlist builder, digital trace and SPICE need zero
changes; they are simply not rendered and not hit-testable. Seating
re-computes on every move/rotation (updateComponent), and moving the
breadboard carries its seated parts along.
- Resistors auto-rotate to vertical when dragged over a breadboard
(their 58.8 px pin span bridges the center trench rows b-f exactly).
- Seat tolerance 4 px: absorbs the worst element pin-spacing residual
(~1.6 px) while staying under half the hole pitch, so a pin is never
ambiguous between holes.
Wokwi interchange fixes that fell out of the diagram.json research:
- import maps the top-level rotate attr onto properties.rotation
(previously every rotated part imported flat) and export emits it
back as rotate instead of leaking it into attrs;
- $bb / empty-color connections import as bb seating wires and export
back as ["$bb"] entries, so parts-on-breadboard projects round-trip;
- wokwi-breadboard-half aliases to the full breadboard (hole names are
a strict superset, so every connection stays valid).
Breadboard elements now export their pure hole grids and import cleanly
without a DOM (node tests); geometry + store seating covered by
breadboard-snap.test.ts and breadboard-seating.test.ts.
2026-07-18 13:47:25 +07:00
. . . components . map ( ( c ) = > {
// Rotation travels as Wokwi's top-level `rotate`, never as an attr.
const { rotation , . . . attrs } = ( c . properties ? ? { } ) as Record < string , unknown > ;
const rotate = Number ( rotation ) || 0 ;
return {
type : metadataIdToWokwiType ( c . metadataId ) ,
id : c.id ,
top : Math.round ( c . y - boardPosition . y ) ,
left : Math.round ( c . x - boardPosition . x ) ,
. . . ( rotate ? { rotate } : { } ) ,
attrs ,
} ;
} ) ,
2026-03-07 10:14:35 +07:00
] ;
// Build connections
const connections : [ string , string , string , string [ ] ] [ ] = wires . map ( ( w ) = > {
2026-04-22 02:45:45 +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' ;
2026-03-08 09:14:33 +07:00
const startId = isBoardStart ? boardId : w.start.componentId ;
const endId = isBoardEnd ? boardId : w.end.componentId ;
feat(breadboard): Wokwi-style parts-on-breadboard — hole snapping + invisible seating wires
Parts now plug INTO the breadboard instead of using it as a junction box:
- Drag magnetism: while dragging, the part's anchor pin snaps to the
nearest hole center (9 px range, 9.6 px grid) so parts land perfectly
aligned, like Wokwi.
- Seating: every pin within 4 px of a hole gets an invisible zero-length
wire (Wire.bb) from pin to hole — the exact model Wokwi persists as
["r1:1","bb1:6t.b","",["$bb"]]. Electrically they are ordinary
wires, so the netlist builder, digital trace and SPICE need zero
changes; they are simply not rendered and not hit-testable. Seating
re-computes on every move/rotation (updateComponent), and moving the
breadboard carries its seated parts along.
- Resistors auto-rotate to vertical when dragged over a breadboard
(their 58.8 px pin span bridges the center trench rows b-f exactly).
- Seat tolerance 4 px: absorbs the worst element pin-spacing residual
(~1.6 px) while staying under half the hole pitch, so a pin is never
ambiguous between holes.
Wokwi interchange fixes that fell out of the diagram.json research:
- import maps the top-level rotate attr onto properties.rotation
(previously every rotated part imported flat) and export emits it
back as rotate instead of leaking it into attrs;
- $bb / empty-color connections import as bb seating wires and export
back as ["$bb"] entries, so parts-on-breadboard projects round-trip;
- wokwi-breadboard-half aliases to the full breadboard (hole names are
a strict superset, so every connection stays valid).
Breadboard elements now export their pure hole grids and import cleanly
without a DOM (node tests); geometry + store seating covered by
breadboard-snap.test.ts and breadboard-seating.test.ts.
2026-07-18 13:47:25 +07:00
// Breadboard seating wires round-trip as Wokwi's hidden `["$bb"]` entries.
if ( w . bb ) {
return [ ` ${ startId } : ${ w . start . pinName } ` , ` ${ endId } : ${ w . end . pinName } ` , '' , [ '$bb' ] ] ;
}
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 ) ) ;
2026-04-22 02:45:45 +07:00
zip . file (
'wokwi-project.txt' ,
` Exported from Velxio \ n \ nSimulate this project on https://velxio.dev \ n ` ,
) ;
2026-03-07 10:14:35 +07:00
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 ,
feat(breadboard): Wokwi-style parts-on-breadboard — hole snapping + invisible seating wires
Parts now plug INTO the breadboard instead of using it as a junction box:
- Drag magnetism: while dragging, the part's anchor pin snaps to the
nearest hole center (9 px range, 9.6 px grid) so parts land perfectly
aligned, like Wokwi.
- Seating: every pin within 4 px of a hole gets an invisible zero-length
wire (Wire.bb) from pin to hole — the exact model Wokwi persists as
["r1:1","bb1:6t.b","",["$bb"]]. Electrically they are ordinary
wires, so the netlist builder, digital trace and SPICE need zero
changes; they are simply not rendered and not hit-testable. Seating
re-computes on every move/rotation (updateComponent), and moving the
breadboard carries its seated parts along.
- Resistors auto-rotate to vertical when dragged over a breadboard
(their 58.8 px pin span bridges the center trench rows b-f exactly).
- Seat tolerance 4 px: absorbs the worst element pin-spacing residual
(~1.6 px) while staying under half the hole pitch, so a pin is never
ambiguous between holes.
Wokwi interchange fixes that fell out of the diagram.json research:
- import maps the top-level rotate attr onto properties.rotation
(previously every rotated part imported flat) and export emits it
back as rotate instead of leaking it into attrs;
- $bb / empty-color connections import as bb seating wires and export
back as ["$bb"] entries, so parts-on-breadboard projects round-trip;
- wokwi-breadboard-half aliases to the full breadboard (hole names are
a strict superset, so every connection stays valid).
Breadboard elements now export their pure hole grids and import cleanly
without a DOM (node tests); geometry + store seating covered by
breadboard-snap.test.ts and breadboard-seating.test.ts.
2026-07-18 13:47:25 +07:00
// Wokwi stores rotation as a top-level `rotate` (degrees), not an
// attr — map it onto properties.rotation or every rotated part
// imports lying flat.
properties : {
. . . p . attrs ,
. . . ( p . rotate ? { rotation : p.rotate } : { } ) ,
} ,
2026-03-07 10:14:35 +07:00
} ) ) ;
// Convert connections to Velxio wires
const wires : Wire [ ] = diagram . connections . map ( ( conn , i ) = > {
feat(breadboard): Wokwi-style parts-on-breadboard — hole snapping + invisible seating wires
Parts now plug INTO the breadboard instead of using it as a junction box:
- Drag magnetism: while dragging, the part's anchor pin snaps to the
nearest hole center (9 px range, 9.6 px grid) so parts land perfectly
aligned, like Wokwi.
- Seating: every pin within 4 px of a hole gets an invisible zero-length
wire (Wire.bb) from pin to hole — the exact model Wokwi persists as
["r1:1","bb1:6t.b","",["$bb"]]. Electrically they are ordinary
wires, so the netlist builder, digital trace and SPICE need zero
changes; they are simply not rendered and not hit-testable. Seating
re-computes on every move/rotation (updateComponent), and moving the
breadboard carries its seated parts along.
- Resistors auto-rotate to vertical when dragged over a breadboard
(their 58.8 px pin span bridges the center trench rows b-f exactly).
- Seat tolerance 4 px: absorbs the worst element pin-spacing residual
(~1.6 px) while staying under half the hole pitch, so a pin is never
ambiguous between holes.
Wokwi interchange fixes that fell out of the diagram.json research:
- import maps the top-level rotate attr onto properties.rotation
(previously every rotated part imported flat) and export emits it
back as rotate instead of leaking it into attrs;
- $bb / empty-color connections import as bb seating wires and export
back as ["$bb"] entries, so parts-on-breadboard projects round-trip;
- wokwi-breadboard-half aliases to the full breadboard (hole names are
a strict superset, so every connection stays valid).
Breadboard elements now export their pure hole grids and import cleanly
without a DOM (node tests); geometry + store seating covered by
breadboard-snap.test.ts and breadboard-seating.test.ts.
2026-07-18 13:47:25 +07:00
const [ startStr , endStr , color , path ] = conn ;
// Wokwi persists parts seated ON a breadboard as one hidden connection
// per pin: empty color + the special "$bb" token in the path slot.
// Import them as velxio breadboard-seating wires (bb: true) so they
// stay invisible and re-seat when the part moves.
const isSeating = ( Array . isArray ( path ) && path [ 0 ] === '$bb' ) || color === '' ;
2026-03-07 10:14:35 +07:00
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-15 08:55:13 +07:00
waypoints : [ ] ,
feat(breadboard): Wokwi-style parts-on-breadboard — hole snapping + invisible seating wires
Parts now plug INTO the breadboard instead of using it as a junction box:
- Drag magnetism: while dragging, the part's anchor pin snaps to the
nearest hole center (9 px range, 9.6 px grid) so parts land perfectly
aligned, like Wokwi.
- Seating: every pin within 4 px of a hole gets an invisible zero-length
wire (Wire.bb) from pin to hole — the exact model Wokwi persists as
["r1:1","bb1:6t.b","",["$bb"]]. Electrically they are ordinary
wires, so the netlist builder, digital trace and SPICE need zero
changes; they are simply not rendered and not hit-testable. Seating
re-computes on every move/rotation (updateComponent), and moving the
breadboard carries its seated parts along.
- Resistors auto-rotate to vertical when dragged over a breadboard
(their 58.8 px pin span bridges the center trench rows b-f exactly).
- Seat tolerance 4 px: absorbs the worst element pin-spacing residual
(~1.6 px) while staying under half the hole pitch, so a pin is never
ambiguous between holes.
Wokwi interchange fixes that fell out of the diagram.json research:
- import maps the top-level rotate attr onto properties.rotation
(previously every rotated part imported flat) and export emits it
back as rotate instead of leaking it into attrs;
- $bb / empty-color connections import as bb seating wires and export
back as ["$bb"] entries, so parts-on-breadboard projects round-trip;
- wokwi-breadboard-half aliases to the full breadboard (hole names are
a strict superset, so every connection stays valid).
Breadboard elements now export their pure hole grids and import cleanly
without a DOM (node tests); geometry + store seating covered by
breadboard-snap.test.ts and breadboard-seating.test.ts.
2026-07-18 13:47:25 +07:00
color : isSeating ? '#000000' : colorToHex ( color ) ,
. . . ( isSeating ? { bb : true as const } : { } ) ,
2026-03-07 10:14:35 +07:00
} ;
} ) ;
// 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
}