Boards with built-in hardware (LCD on the element's own canvas, speaker,
on-board buttons/keyboard) need run-time wiring between the DOM element and
the board's simulator shim / ESP32 bridge. One generic SimulatorCanvas effect
now hands those handles to the overlay's attachBuiltins shortly after run
start and runs its cleanup on stop — no board names in the OSS tree.
registerProExamples() now notifies useSyncExternalStore subscribers (same
contract as proRoutes): a direct URL to an overlay-registered example resolved
before the dynamic @pro import landed and stuck on the 404 branch. The three
example pages subscribe to the version counter.
registerProBoards() lets a hosted overlay ship boards outside the OSS tree as
data-only definitions: registration patches the exported BoardKind maps
(labels / FQBN / MicroPython) so every existing read site keeps working, and
the sites a map can't cover consult the registry — canvas render (custom
element or overlay render fn), BOARD_SIZE, pin-name mapping, picker list +
descriptions + tag, ESP32 family routing, in-browser simulator construction
and firmware load (structural ProBoardSimulator contract, duck-typed PIO
attach/detach), built-in bridge sensors, and a CS-gated built-in microSD
(sdCsPin -> sd_card.cs_pin worker config). Esp32Bridge additionally gains the
esp32-c6 machine type + TX pin (public chip knowledge — the C6 compile path
already ships) and a generic sendKey() for built-in matrix keyboards.
registerProExamples() appends gallery examples at runtime; the board ONLINE
ads recompute at render so registration hides them. OSS behavior without an
overlay is unchanged — the registry is dead code, same as the other seams.
Components that only exist in the hosted editor now advertise themselves in
the picker exactly like the online-only boards do: ONLINE_ONLY_COMPONENT_ADS
(same module as the board ads) renders an ONLINE-badged card that links to
velxio.com, auto-hidden in any build whose ComponentRegistry has the real
component (the hosted overlay merges it in — no per-build switches). First
entries: the M5Stack Chain RGB/Mono 8x8 matrices. componentDocs gains
registerComponentDoc(id, raw) so an overlay can supply the hover datasheet
for components it injects at runtime.
Ports the v5 compiler layer: /opt/esp-idf-v5 + arduino-esp32 3.x
discovery (IDF5_PATH / ARDUINO_ESP32_5_PATH overridable), per-compile IDF
selection routing the family to the v5 tree, esp32c6 target support, and
the docker-runtime hardening (version-matched python venv pinned on PATH,
toolchain ordering by the tree's tools.json, user-libs component REQUIRES
for the IDF components Arduino libraries include directly). The LED_BUILTIN
fallback in the project template becomes a whitelist so non-esp32 variants
(C6 first) never hit the redefinition trap. Includes the option tests.
The newest-env-wins heuristic pinned a 5.x env for a 4.4 tree — each
generation's dependency check only accepts its own requirement set. The
version is read from tools/cmake/version.cmake of the active IDF tree,
falling back to the newest env when unreadable.
Two failure modes seen on a fresh docker deploy where the final image's
system python differs from the one the IDF venv was created with:
- IDF's cmake derives the venv dir name from the SYSTEM python version
and fails with 'python doesn't exist' before configure; and when cmake
is invoked directly (not via idf.py) its PYTHON property falls back to
the bare 'python' from PATH. The Linux branch of _build_env now pins
IDF_PYTHON_ENV_PATH to the newest env under the tools root and puts its
bin first on PATH (mirroring what the Windows branch already did).
- When several toolchain generations share one tools root, glob order
decided which won the PATH race. Toolchain dirs are now ordered by the
versions the IDF tree declares in tools/tools.json.
Generic platform work ported from the internal line:
- Esp32BridgeFactory seam + rebuildEsp32Bridge + sync-I2C seam: a
substitute simulation bridge (e.g. the hosted editor's in-browser JS
emulators) can be installed without touching OSS code
- Component datasheets: hover popover (ComponentInfoPanel) + markdown
docs for common parts
- Per-chip S3/C3 basics examples for the gallery
- .gitignore: never allow pro emulator mask ROMs into the OSS repo
New: online-only board showcase. Boards implemented by the hosted editor
(ESP32-C6, M5Stack Core, Cardputer ADV, Pimoroni RP2350 family) appear
in the picker as advertisement cards with an ONLINE badge linking to
velxio.com, where they are free to use. Ads auto-hide in any build that
registers the real BoardKind.
Running a multiplexed 4-digit 7-segment clock on ESP32/QEMU froze the
browser for minutes after Run — evaluate probes waited 40-90 s, and before
the first fixes the sim WebSocket eventually died (code 1006) with the page
never recovering. CPU-profiled on staging; four compounding per-GPIO-edge
costs, in profile order:
updateComponentState minted a new components array per edge
------------------------------------------------------------
The store setter rebuilt `components` (and one properties object) on EVERY
edge even when the state didn't change. The breadboard is direct-wired to
13 board pins, so segment toggles produced thousands of store sets per
second; every subscriber re-rendered each time, and the canvas subscription
effect (deps: [components, ...]) re-subscribed all pin listeners in a loop.
Now a no-op guard returns prevState unchanged, and breadboards are treated
as self-managed (they have no visual on/off state to echo).
CompilationConsole re-rendered every log line per editor render
----------------------------------------------------------------
The post-compile console holds hundreds of lines; each render called
Date.toLocaleTimeString per line (~0.2 ms each — it builds a fresh Intl
formatter every call). Profile: 162 s of self time in LogLine over a 337 s
window, in ~150 ms tasks. LogLine is now memoized (entries are immutable),
timestamps go through one shared Intl.DateTimeFormat, and the console
itself is React.memo'd against parent re-renders.
Per-edge full SPICE re-solves
------------------------------
PinManager requested a FULL netlist rebuild+solve on every 'mcu' edge.
Now only the edge that newly classifies a pin as MCU-output triggers the
rebuild (that's what emits the pin's V-source); steady-state updates flow
through connectMcuEdgesToService's per-pin coalesced alterSource path.
The start.ts resolve hook is trailing-throttled (33 ms) for the other
per-edge callers (RP2040, custom chips), the service's pending-edge queue
drains on a 33 ms gap timer instead of replaying back-to-back, and new
edges arriving inside the gap queue instead of soloing a solve.
STM32 / Pi reverse pin-name mappings added to connectMcuEdgesToService so
those boards keep fine-grained updates now that the full-tick storm is
gone (PA0/PC13-style and GPIO-style names never matched before).
wokwi-7segment re-rendered per segment write
---------------------------------------------
element.values now flushes at most every 8 ms per display (trailing write
guaranteed), instead of re-rendering the 32-shape SVG per edge.
Also: CLN (colon) pin support for 7-segment clock faces — wired CLN now
drives colon/colonValue in both the attachEvents path and the QEMU
onPinStateChange path; it was silently ignored, so clock colons never lit.
Verified on staging with the failing project: main-thread probes drop from
40-90 s waits (324 long tasks, 52.6 s blocked in 150 s) to 5-11 ms
(2 long tasks, 179 ms), display shows 12:00 with the colon blinking at
1 Hz from the first seconds after Run.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The registry's Pi Zero/1/2/3/4/5 component entries rendered the live
velxio-raspberry-pi-* element clipped to a sliver and carried no PRO
marker. Reuse the board illustrations keyed by tagName (Zero/1/2
intentionally share the Pi 3 art) and show the shared gold PRO pill on
any component whose id is a pro board kind (Pi Linux family + STM32).
The Pi 4/5 cards instantiated their live custom element at natural size
with a CSS scale; the transform keeps the unscaled layout box, so the
100px thumbnail clipped the board to a narrow vertical sliver. Use the
existing board illustration PNGs with objectFit contain, same as Pi 3.
The PRO badge on gated boards grows to a readable pill with a drop
shadow.
Three router bugs found by replaying a real agent session (reloj_3333) where
wires ran straight across a seated 4-digit display. Each fix is covered by a
regression test built from the failing geometry.
Endpoint inside an obstacle no longer drops the whole obstacle
--------------------------------------------------------------
Breadboard strips under a seated display start INSIDE its inflated bbox, so
the "rects containing an endpoint are dropped" rule deleted the display as
an obstacle for every wire leaving those strips — 15 wires crossed it end to
end. The rect is now carved instead: an escape corridor (ROUTE_MARGIN wide)
from the endpoint to the chosen edge, with the rest of the body still
blocking. Side blocks overlap the endpoint's row by 1px, or the strict
segment-hit test leaves the row as a free seam straight across the body.
Overlapping rects escape in ONE shared direction
------------------------------------------------
Seated resistors overlap heavily (19px pitch, ~66px inflated boxes). When
each containing rect picked its own nearest edge, the corridors pointed
different ways and walled each other off — A* found no exit, fell back to
the direct elbow, and the wire crossed the display anyway. The escape
direction is now chosen once against the UNION of containing rects and
every carve uses it, so the corridors chain into a continuous exit.
Null route materialises the CHECKED elbow
------------------------------------------
routeAroundObstacles returns null when the PREVIEW elbow (longer-axis-first)
is clear — but the re-route pass stored empty waypoints, which the renderer
expands as the horizontal-first corner: a DIFFERENT elbow the router never
validated. Three wires shipped crossing a display whose checked route was
clean. The pass now materialises previewElbow explicitly, exactly like
finishWireCreation always did.
Verified E2E: the same agent prompt that produced 15 crossings now builds
the ESP32 clock with ZERO wire segments crossing the display body.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Extends the existing component-avoiding A* (wireAutoRoute.ts) into the full
auto-router the canvas was missing. Three pieces:
Wire avoidance with soft costs
------------------------------
Component bodies stay hard-blocked, but wires get graded costs: running
parallel on top of another wire (within an 8px corridor) is charged per px,
a perpendicular crossing costs a small fixed amount, and bends keep their
existing penalty. Crossings must stay possible — hard-blocking wires makes
dense boards unroutable and everything would degrade to the default elbow.
The compressed grid gains "corridor" coordinates 8px to each side of every
wire segment, so the router actually has a lane to run BESIDE a wire; that
is also what lays multi-wire runs out as a tidy side-by-side bus, since
each new wire routes seeing the previous ones. Wires sharing an endpoint
with the route are exempt (wires meeting on a pin must touch there), and
only wires within 120px of the route's bbox participate, keeping the grid
under the coordinate cap on dense canvases.
autoRouted: the system owns the shape until the user takes it
-------------------------------------------------------------
New Wire flag, set by pin-to-pin creation and by agent add_wire. Every
shape-editing gesture (segment drag, waypoint drag, waypoint insert — five
call sites) clears it: from that moment the wire is hand-authored and is
NEVER re-shaped, exactly where the user put it. Wires from older projects
have no flag and are treated as hand-authored.
recalculateAllWirePositions re-routes flagged wires after endpoints move
(component drag end, agent batches, mount settle — never per drag frame).
This is also what routes agent wires at all: they are created before their
elements mount and before pin coords are final, so creation-time routing
is impossible; the settle-timer recalc routes them once geometry is real.
Live routed preview
-------------------
updateWireInProgress routes start->cursor (throttled to 40ms) and the
preview renders that path, so the wire dodges components and wires AS THE
MOUSE MOVES instead of snapping into shape on the final click. Hand-guided
previews (user-placed waypoints) keep the classic path untouched.
Verified in the live app: an agent-built breadboard circuit shows 0 wire
overlap px and 0 body crossings across all wires, and a hand-started wire
aimed collinear with an existing run previews 21px beside it, overlap 0.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
An ESP32 clock built by the agent stayed dark while QEMU was verifiably
emitting hundreds of GPIO edges per second (437/pin measured on the live
websocket). Reload did not help — this was not the seating race. Two
independent tracing bugs, reproduced from the real project circuit (fixture
included) and each sufficient to kill the display:
Boards added at runtime were invisible
--------------------------------------
isBoardComponent matches static id prefixes ('arduino-uno', ...), which only
covers the default board. Every board added at runtime gets a minted UUID id
— the agent's add_board always does — so traceDetailed treated the board
endpoint as an unknown component and resolved null, and SimulatorCanvas's
direct-wire subscription path skipped it entirely. Every Uno project happened
to work because they reuse the default board whose instance id IS the literal
'arduino-uno'. Both sites now consult the live boards list first, keeping
isBoardComponent as the legacy-id fallback.
Strip walking missed wires stacked on one hole
----------------------------------------------
The breadboard group walk continued the trace from every OTHER wired hole of
the strip, excluding the arrival hole by name. But two wires may legitimately
share one hole — the agent bridges strips straight into the seat hole (8 of
this circuit's 9 bridges land exactly on a resistor's own hole), which is
electrically identical to using a free hole of the strip. The name exclusion
made those junctions dead ends. Exclusion is now by incoming WIRE id, so
same-hole connections resolve; the depth bound already prevents ping-ponging
between two wires of one net.
With both fixes the exact saved circuit resolves every display pin to its
GPIO (A..DP -> 32,33,25,26,27,14,12,13; DIG1..4 -> 15,2,4,5; COM -> GND) and
the live project now shows 12:00 on the real QEMU simulation. traceDetailed
is exported for the regression test, which drives the real store with the
real circuit.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The ESP-IDF compile path stages exactly the libraries declared in the
example (no transitive resolution), so Adafruit_GFX.h failing to find
Adafruit_I2CDevice.h broke esp32s3-ili9341-hello and esp32-oled-4pin-i2c
with 'Compilation produced no firmware'. The other ESP32 GFX examples
(esp32-oled, esp32-bmp280) already declare BusIO explicitly.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A part can land in the store at its FINAL position before its element
mounts: the agent streams add_component and the seating move in one batch,
and updateComponent's reseat then finds no DOM (computeSeating null) and
keeps the empty seating. Nothing re-derived it afterwards — the agent-side
seat correction skips when the position needs no nudge, and 'pininfo-change'
only fires on pin-SET swaps, not on plain init. Meanwhile run_simulation
executes right after the SSE round, before the correction's animation frame.
Net effect, reported by a user as a suspicion that turned out exactly right:
a clock the agent built and ran in one turn showed a dead display, while
reloading the project and running it worked — bb seating wires are persisted,
so on reload they exist before Run is pressed.
DynamicComponent now reseats once the element's pinInfo first becomes
measurable (same polling cadence as the pinInfo-ready effect), which closes
the hole for every path that stores a final position before mount: agent
batches, project load, undo. To keep that free on load,
reseatComponentOnBreadboard skips the store write when there is nothing
seated and nothing to clear — otherwise every off-board part would churn the
wires array identity once per mount.
Verified live end-to-end: agent adds + seats + wires + compiles + RUNS in a
single turn; the seated LED blinks immediately (4 transitions sampled), with
all 4 seated-pin markers present — no reload needed.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Seating is otherwise invisible — a seated pin connects to its hole through a
zero-length `bb` wire that never renders — so a user couldn't tell a part
that merely sits ON the board from one whose pins are actually connected.
This was reported after placing parts that looked seated but gave no signal
they were wired in.
SeatedPinMarkers draws a small always-on green dot (Wokwi-style) on each pin
that has a `bb` wire, derived once per render from the store's wires
(component pin = wire start). Non-interactive layer below the wire-target
hit boxes; only breadboard-seated pins light up, so board-wired builtins stay
unmarked — exactly the "seated vs connected" distinction that was missing.
The per-pin rotation math (rotate about the wrapper centre, which the overlay
layers live outside of) is extracted from PinOverlay into a shared
`rotatePinLocal`, so the dots and the wire-target boxes can never drift apart
under rotation. A test asserts rotatePinLocal agrees with calculatePinPosition
at 0/90/180/270°.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The agent computes exact hole assignments server-side but can only send an
approximate canvas x/y, because the rotation pivot is the DOM wrapper centre
and the wrapper includes a text label the server cannot measure. Under
rotation that left seated parts off by up to ~4 px — enough that a diode
(pins 7.5 pitches apart) half-seated: computeSeating found no hole for the
far pin and it went electrically dead.
resolveSeatPosition corrects it in the browser by pure translation: read
where the anchor pin actually is from live DOM geometry (real pivot), read
where the solver put it, shift the whole part by the difference. Every other
pin follows because pin-to-pin offsets are pivot-free. It never re-solves, so
it cannot slide the part to different holes and the validated netlist holds.
The anchor target is the solver's anchor position in breadboard-element
space, WITH its sub-pitch centroid translation — not the hole centre.
Targeting the centre would re-break the diode (far pin 4.8 px out). Verified
against real rendered geometry in a browser: resistor and diode at 90° both
seat within the intrinsic lattice residual (0.6 / 2.4 px).
Applied via a `seat` payload on the move_component effect (velxio-prod
overlay); this commit is the resolver + tests.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Three changes, all driven by a real project where a 4-digit 7-segment clock
was unreadable and half its parts were not actually seated.
Labels on hover only
--------------------
Eight vertical resistors at 19 px pitch rendered eight 93 px "Resistor 220 Ω"
labels on top of each other, hiding the parts and the breadboard holes; the
SPICE overlay added ~40 more `0uV` pills. Both are now revealed on hover:
hovering a part also lights up the voltages of every wire touching it.
The label is hidden with OPACITY and stays in flow. pinPositionCalculator
derives the rotation pivot from wrapper.offsetHeight, so taking it out of
flow would move the pins of every rotated component in every saved project.
Seat-on-drop
------------
The drag-time magnet only aligned the anchor pin and assumed the rest
followed, which is how parts ended up HALF-seated: some pins in holes, the
rest dead in the air. It looks mounted in a screenshot and silently breaks
the circuit. On release we now re-solve properly — nearest position where
EVERY pin is in a free hole, sliding past occupied columns — via the new
solvePlacement/seatOnDrop. Geometry comes from the element's own pinInfo,
so there is no part whitelist.
Sub-pitch translation
---------------------
solvePlacement first assigned pins to holes at half-pitch, then translates
by the centroid of the residuals before judging fit. Pinning the anchor dead
centre refused every off-lattice footprint: a diode spans 7.5 pitches, so
one leg landed 4.8 px out. Shifted 2.4 px, BOTH legs sit inside tolerance —
what bending the leads does on a real board. Measured over the catalog this
takes seatable parts from 87 to 125 of 152; diodes, transistors, regulators,
optocouplers and flip-flops are rescued with no artwork change.
Staying under SEAT_TOLERANCE (< half pitch) keeps each pin's nearest hole
unambiguous, so computeSeating resolves the same holes and the netlist is
unaffected by the small offset.
Also: refuse a placement that would put two of a part's own pins in one
strip. A column strip — and far worse, a power rail — is a single net, so
such a seating shorts the part to itself. Without it a 7-segment happily
lays its pins across a rail. And deduplicate pin names before solving:
calculatePinPosition resolves by name and returns the first match, so a
board carrying GND x5 collided with itself and was refused outright.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Root cause of the 'digits=4 display seated with the 1-digit COM pinout'
bug: property values arrive as STRINGS (agent set_component_property,
the property dialog's text inputs) and were assigned to the web
component verbatim — wokwi's 7segment does switch(this.digits) with
numeric cases, so el.digits='4' silently fell back to the 1-digit
pinout (and 'false' stayed truthy for boolean props like colon).
- DynamicComponent now coerces string values to the TYPE of the
metadata default for that key (number/boolean) before assigning.
- New pininfo-change listener: when a property swaps the element's pin
set (digits, flip, pins edge), the elements announce it — re-derive
the breadboard seating then, with the fresh pinout, instead of never.
Every resistor variant ('resistor' + 'resistor-<value>') now lands on
the canvas rotated 90 degrees: reads better, takes less horizontal
space, and drops straight into breadboard columns. Explicit rotations
in metadata defaults are respected. The breadboard auto-vertical drag
check widens from the two-entry set to the same prefix predicate, so
preconfigured variants (resistor-330 etc.) rotate on the board too.
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.
Three stale-project-identity fixes from reviewing the New-workspace flow:
- New workspace (web): handleNewClick cleared the workspace and the
current project but left the browser on the old /user/slug URL — a
refresh (or back-button pop) silently reloaded the OLD project over
the fresh unsaved workspace. Now replaceState's to the localized
/editor (replace, not push, so no back-entry points at the stale
project route).
- New workspace (desktop menu): same URL fix for the newProject menu
action, which cleared identity but never left the project route.
- .vlx import: importVlxFile mutated the stores WITHOUT clearing
currentProject — with a saved project open, autosave saw the
imported content as dirty edits on the old projectId and silently
PUT the .vlx contents over the user's saved project (and pushed the
clobber to GitHub on linked projects). Now severs identity first,
same guard loadExample.ts already documents.
verifyCircuitFromStore() builds the worst-case snapshot (every wired
digital pin driven HIGH) and solves it — extracted verbatim from
EditorToolbar's runVerification so programmatic runners (editor
extensions, agents) can gate their own run paths on the same rules.
No behavior change for the Run button.
Creating a wire with a direct pin-to-pin click (no user waypoints) now
routes around other components' bounding boxes instead of crossing
them. Routing happens exactly once, at creation: the routed corners are
stored as ordinary waypoints, so every later manual edit stays where
the user puts it — never re-routed.
Router (utils/wireAutoRoute.ts):
- tries the preview elbow first (clear -> keep existing behavior and
the WYSIWYG shape), then the opposite elbow, then A* over the
compressed grid spanned by pin coordinates and obstacle edges
inflated by an 8 px clearance, with a 40 px per-bend penalty so
straighter routes win
- obstacles are component boxes only (never boards — pins sit on both
board edges and detouring around a board produces absurd routes),
excluding the wire's own endpoint components, measured from the
rendered DOM; rects containing an endpoint are dropped
- any failure (walled-off target, oversized grid, no DOM) falls back
to the previous direct-elbow behavior
Hand-aligning a dragged segment could leave two parallel runs a pixel
or two apart, joined by a tiny perpendicular step, because alignment
snapping only ever targeted OTHER wires' geometry.
- Segment and bend-point drags now also snap (6 px threshold) against
the dragged wire's own points — excluding the ones being dragged —
so a run clicks into line with its neighbour and the exact
simplification fuses them into one segment on commit.
- fuseMicroJogs: parallel runs offset by under 2 px joined by a tiny
step are aligned automatically (the run not anchored to a wire
endpoint moves; shorter run yields when both are free). Applied at
render time and in renderedToWaypoints/normalizeWireWaypoints, so
already-saved crooked wires display straight without touching data.
Three wiring quality fixes:
- Rounded corners: every bend now renders as a quadratic curve
(radius 7, clamped to half the shorter adjacent segment), with
round line caps/joins. Segment/waypoint drag previews and the
in-progress preview use the same path builder so the look is
consistent everywhere.
- Degenerate geometry cleanup at render time: the expanded polyline
is simplified (duplicates, collinear runs, U-turns) before the
path is emitted, so wires saved with junk waypoints no longer
render on top of themselves. Stored data is untouched until the
user edits the wire.
- WYSIWYG commit: finishWireCreation materialises the final-leg
elbow exactly as the live preview drew it (longer axis first) and
normalises the stored waypoints. Previously the committed wire
fell back to horizontal-first and visibly changed shape on click.
simplifyOrthogonalPath moved to wireUtils (re-exported from
wireHitDetection for existing imports); the duplicated inline
expansions in SimulatorCanvas now use the shared helper. Waypoint
dots on idle wires removed (visual noise); endpoint dots stay.
Monaco's focus sink is a plain div (.native-edit-context under the
EditContext API), neither an input tag nor contentEditable, so the
typing guard missed it and a mapped letter typed into the code editor
pressed the button. Treat any keydown originating inside .monaco-editor
as typing.
Any pushbutton (pushbutton / pushbutton-6mm) can now be driven from the
keyboard. Assign a key from the component property dialog — a keycap
control captures the next keypress (Escape cancels, modifiers alone are
rejected) — and a keycap badge next to the component label shows the
mapping on the canvas. Several buttons may share one key on purpose;
the dialog shows a hint when that happens.
At runtime a global bridge translates keydown/keyup into the same
button-press / button-release DOM events the mouse fires on the wokwi
element, so every simulation path (avr8js pin logic, SPICE-driven
inputs, the QEMU GPIO bridge, the pressed visual) behaves identically
to a mouse click. Guards: ignored while typing in inputs or the code
editor, ignored with Ctrl/Alt/Meta held, auto-repeat collapses into one
long press, and window blur releases everything so no button sticks
after Alt-Tab.
The binding is stored as the component's 'key' property, so it
round-trips through project saves and .vlx exports and is undoable like
any other property edit. Strings added to all 9 locales.
Convert the remaining window.confirm() call sites to the in-app
MessageDialogHost, extended with a new confirm mode (Cancel + Confirm
buttons, optional danger styling) via showConfirmDialog().
Sites converted:
- New workspace (EditorPage)
- Load project / delete file (FileExplorer)
- Overwrite SPIFFS file (BoardOptionsModal)
- Delete VFS node (VirtualFileSystem)
All dialog strings are internationalized across the 9 supported locales
(en, es, pt-br, it, fr, zh-cn, de, ja, ru); the two previously
English-only modals now pull from i18n too.
A breadboard is the physical base of a circuit — boards, components and
wires all plug into it — so it should never cover them. Pin its group at
z-index -1 (below boards z 0, components z 1/2, wires z 35), ignoring
selection. Detected by metadataId prefix 'breadboard' (breadboard,
breadboard-mini). Its own pins stay wireable wherever it's not covered.
The dense-component threshold (>60 pins) meant boards like the Arduino
(31 pins) still painted every pin blue on hover — a wall of squares. Drop
the threshold: every component/board now keeps its squares invisible and
lights up only the ONE under the cursor (matching the breadboard, which
users already liked). Wiring mode still paints them all — all valid targets.
Removing isActive from board showPins (prev commit) exposed a latent bug:
BoardOnCanvas put onMouseEnter/onMouseLeave on the drag overlay, a SIBLING
of PinOverlay. Moving the cursor from the overlay onto a pin square fired
the overlay's mouseleave, cleared hoveredBoardId, and hid every pin right
as you reached one — so a board pin could never be clicked to start a wire
(breadboards were fine: their group wrapper owns both body and pins).
Move the hover handlers to the wrapper div that contains the board body,
the drag overlay AND the pin squares, so moving among them never fires
mouseleave. Board dragging (onMouseDown on the overlay) is unaffected.
Three UX fixes to the pin squares:
- Pins show on hover or while wiring only. The active board and the
selected component used to light every pin permanently.
- Dense components (>60 pins — breadboards) don't paint a wall of blue
on hover: squares stay invisible and light up individually under the
cursor. While a wire is in progress every square paints again since
they're all valid targets (new `wiring` prop threaded to PinOverlay).
- mousedown on a pin square stops propagation, so press-and-drag from a
pin no longer pans the canvas.
Two stacking bugs:
1. Pin overlays used a global z-index 30 while component bodies sit at
z 0-5, all in .canvas-world's single stacking context — so a covered
component's pins painted on top of whatever covered it (arduino pins
showing through a breadboard). Each component/board group is now a
zero-size positioned wrapper that forms its own stacking context
(boards z 0, components z 1, selected z 2): pins stay above their own
body but are hidden together with it. .components-area becomes
pointer-events: none so board pins/drag overlays (now trapped at z 0)
keep receiving clicks through it; component groups re-enable their own.
2. The Add Component / board picker overlays (z 1000/2000) rendered
behind the pro AI chat panel (z 8000). Both now portal to <body> at
z 9000.
Verified in-app: board drag, component drag, wire creation from board
pin to LED, covered pins hidden (0/14 leak), covering component's pins
still clickable.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
drazzy.com (ATTinyCore index host) has had an expired TLS certificate
since 2026-06-22 and now 301-redirects http to https, defeating the
plain-http URL pinned to sidestep its TLS issues. Two failure modes:
1. A /root/.arduino15 volume from an older image can reference the index
in config while lacking the file; arduino-cli then fails instance
init outright, breaking EVERY compile, not just ATtiny (issue #254).
2. backend/Dockerfile chained update-index with &&, so any uncached
image build fails hard while the host is broken (exit 1 verified).
A stale index is harmless (ATTinyCore 1.4.1's platform archive and its
micronucleus 2.0a4 both download from github.com); a missing one is
fatal. So: vendor the index under backend/board-indexes/, copy it to
/opt/arduino15-seed in Dockerfile.standalone, and teach entrypoint.sh
to seed any missing package_*.json into /root/.arduino15 at boot,
healing stale volumes. backend/Dockerfile seeds the index directly and
makes update-index best-effort; core install lines stay strict.
Verified: removing the index reproduces the reporter's exact
'Error initializing instance' brick; after seeding, instance init
exits 0 with the host still broken.
The Sign-in links navigate with a full page load (they mount in a
separate React root without Router context), which wipes the in-memory
Zustand workspace. New utils/workspaceDraft stashes the whole workspace
(reusing the lossless .vlx serialisation) to sessionStorage before that
navigation and restores it once when the editor remounts after login —
so a user who was building a circuit and signs in lands back on their
work instead of the empty starter board.
Strictly scoped to the login round-trip by a one-shot restore flag (not
a general autosave), and skipped when a named project is already loaded
so it never clobbers one. EditorPage calls restoreStashedWorkspace() on
mount; the pro overlay's auth links call stashWorkspaceForAuth() before
navigating.
useMessageDialogStore + <MessageDialogHost /> (mounted once in App.tsx)
give a themed in-app dialog callable from anywhere — React components
and plain .ts modules alike via showMessageDialog(msg, {kind}). Swaps
the native alert() calls in FileExplorer (import errors) and the
desktop menu (.vlx open errors, updater status) for it; the pro overlay
can reuse the same store.
The 'Create with AI' button referenced the Pro AI agent (hardcoded
prompt, agent event) inside the anonymous OSS dialog — chat/agent logic
must live in the velxio-prod overlay, not here. CustomChipDialog now
just exposes a generic `data-velxio-slot="custom-chip-actions"`
extension point (empty in OSS) and hangs its close handler on the slot
element so the overlay can dismiss the dialog after acting. The button
itself, its prompt and the Pro entitlement gate move to the overlay.
- 'Create with AI' button in CustomChipDialog dispatches the generic
velxio:agent-prompt window event (no-op without a listener — the pro
overlay's chat panel picks it up and prefills the composer).
- chipCompileService maps the hosted deployment's 401 gate to a human
'sign in to compile custom chips' message instead of a raw status
line. Self-hosted OSS keeps the route open and never sees either.
Registers upstream @wokwi/elements, velxio-elements/ and the element
classes living next to their React wrappers in velxio-components/ —
without pulling any React component graph. Used by pin-metadata
introspection in tests/generators (the pro agent's metadata export).
Google's auto-generated sitelinks for "velxio" surfaced docs/blog pages
but not the Editor (the primary app) or Home. Sitelinks can't be forced,
but an explicit SiteNavigationElement ItemList naming the primary nav —
Editor, Examples, Documentation, Pricing, About, in that order — is the
recognized structured-data hint for what the site's main sections are.
The examples page showed only ~3 cards per row (the grid was capped at
max-width 1200px with minmax(300px) columns), a tall header, and three
stacked filter rows (16 board tabs + category + difficulty) that pushed
the actual examples far down the page. Card thumbnails also letterboxed
with black bars on the sides.
- Grid: widen to 1680px + minmax(232px) columns → ~6 cols on a 1600px
screen (was 3). Cards smaller/denser (radius, info padding, title).
- Thumbnails: fixed 5:3 aspect-ratio container + object-fit:cover so the
preview fills edge to edge — no more black side bars.
- Filters: replace the search row + 16 board tabs + two button rows with
ONE compact toolbar (search + Board/Category/Difficulty dropdowns +
live count) and removable filter chips (badges with ×) + Clear all.
- Header trimmed (smaller title, less margin) so cards start high.
The red viewport rectangle read the React `pan` prop, but the canvas
pans by mutating panRef + the .canvas-world transform directly (no
setState until pointer-up, for zero-lag dragging). So while you dragged
the canvas the rectangle sat frozen and only jumped at the end. The
minimap now also receives panRef/zoomRef and mirrors them via a
requestAnimationFrame loop that setStates only on change, so the rect
follows the canvas every frame while keeping the canvas render-free
during the gesture.
Also: boards were all drawn as one fixed 120x90 world rectangle
regardless of the actual board, misrepresenting the layout. Use the real
per-board BOARD_SIZE (now exported from BoardOnCanvas) so each footprint
is proportional. And enlarge the map 100x75 -> 160x120 (same 4:3 as the
4000x3000 world) so it's usable.
The profile Duplicate dialog passes the chosen name, description and
visibility to POST /projects/{id}/duplicate. Options are optional — an
empty call still clones with the source name + " (copy)".
Pairs with the pro-overlay endpoint that clones a project (row + file
groups) into the caller's account. Lives next to deleteProject — the
OSS client already fronts the pro-only projects API when overlaid.
build-push-action v6 uploads a build-record artifact on every run with
no expiry; 768 of them had silently exhausted the account-wide Actions
artifact-storage quota, which then failed artifact uploads in every
other repo (qemu-lcgamboa's libqemu release CI among them). Disable the
upload via DOCKER_BUILD_RECORD_UPLOAD=false — the provenance records
were never consumed.
New optional `featured` metadata flag: ComponentRegistry stable-sorts
featured components to the front after loading (and indexes categories
from the sorted list, so per-category views keep the same order). The
two breadboards are marked featured in component-overrides.json — they
are everyday parts and now open the component grid instead of sitting
at the bottom below every diode.
Swap the programmatic SVGs for the real Fritzing breadboard art
(breadboard2.svg / miniBreadboard.svg from fritzing/fritzing-parts),
served from /component-svgs/fritzing/ and scaled x4/3 so the hole pitch
is the wokwi-standard 9.6 CSS px. pinInfo is computed from the measured
Fritzing hole grid (terminal col 1 at x=10.92, rails at x=25.33 in
5-hole groups; wokwi rows a-e map onto fritzing J..F on the full board,
1:1 on the mini; the red stripe marks the + row of each rail pair), so
wire endpoints land exactly on the drawn holes. The element reserves its
final size immediately and falls back to a light programmatic SVG with
identical geometry if the asset can't load. ATTRIBUTION.md records the
CC-BY-SA 3.0 license of the two SVG files.
Verified in the app: fritzing art renders for both boards, the LED
circuit through full-board column + mini column + ground rail still
lights, SPICE overlay shows the merged nets (5.00V on the pin-8 net).