Commit Graph

1357 Commits

Author SHA1 Message Date
David Montero Crespo bbd025d1c4 fix(breadboard): land agent-seated parts exactly under rotation
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>
2026-07-19 22:01:41 +02:00
David Montero Crespo 66c6c7d813 feat(breadboard): hover-gated labels + full-footprint seating solver
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>
2026-07-19 05:28:40 +02:00
David Montero 206cda78af fix(components): type-coerce string properties + reseat on pininfo-change
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.
2026-07-18 19:18:27 +02:00
David Montero aab5e1be08 feat(canvas): resistors default to vertical on add
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.
2026-07-18 18:02:41 +02:00
David Montero f98c3268fa 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 08:47:25 +02:00
David Montero 8e33088752 fix(editor): sync URL after New workspace + sever project identity on .vlx import
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.
2026-07-18 08:24:06 +02:00
David Montero 30882f3930 refactor(verify): extract store-driven pre-flight verification into verifyFromStore
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.
2026-07-18 06:44:38 +02:00
David Montero 7ed9c51bd3 feat(wires): first-time auto-routing around components
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
2026-07-18 05:59:45 +02:00
David Montero abbbbad559 feat(wires): fuse sub-pixel jogs + snap segment drags to the wire's own runs
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.
2026-07-18 05:39:37 +02:00
David Montero 152f9e4ce0 feat(wires): wokwi-style rounded bends + degenerate path cleanup
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.
2026-07-18 05:02:47 +02:00
David Montero 2e5ac20eba fix(simulator): don't fire key-bound buttons while typing in Monaco
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.
2026-07-18 04:02:01 +02:00
David Montero 5218f7314b feat(simulator): map pushbuttons to keyboard keys
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.
2026-07-18 04:02:01 +02:00
David Montero Crespo e72413a13f feat(ui): replace native confirm() dialogs with reusable modal
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.
2026-07-18 01:59:18 +02:00
David Montero Crespo d1d06a44d3 feat(canvas): breadboards always sit behind everything
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.
2026-07-17 20:50:59 +02:00
David Montero Crespo cbdabb730d feat(canvas): only the pin under the cursor lights up, on every component
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.
2026-07-17 20:37:57 +02:00
David Montero Crespo f6dec9ace2 fix(canvas): board pins stay clickable — hover handlers on the wrapper
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.
2026-07-17 20:23:35 +02:00
David Montero Crespo 5b472c7929 feat(canvas): calmer pin overlays + pin presses never pan the canvas
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.
2026-07-17 20:04:23 +02:00
David Montero Crespo b6ce131b03 fix(canvas): stack pins with their component + pickers above floating panels
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>
2026-07-17 18:44:58 +02:00
David Montero a2df942d74 fix(docker): vendor drazzy.com board index + seed missing indexes at boot
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.
2026-07-17 06:41:44 +02:00
David Montero Crespo 108d330efd feat(editor): preserve in-progress workspace across a login redirect
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.
2026-07-17 06:27:28 +02:00
David Montero Crespo 9826737831 feat(ui): global message dialog to replace window.alert()
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.
2026-07-17 06:06:28 +02:00
David Montero Crespo 17e9e5a8c2 refactor(custom-chips): move the AI entry point out of OSS into an overlay slot
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.
2026-07-17 04:36:14 +02:00
David Montero Crespo cd22335838 feat(custom-chips): AI entry point + friendly auth error
- '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.
2026-07-16 20:34:42 +02:00
David Montero Crespo d85e4ac14a feat(elements): side-effect registrar for all custom elements
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).
2026-07-16 18:31:20 +02:00
David Montero Crespo 4cdbf9c89e seo: add SiteNavigationElement structured data (Editor first)
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.
2026-07-16 06:21:50 +02:00
David Montero Crespo fcfe47eab5 redesign(examples): compact toolbar + denser grid + fix card black bars
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.
2026-07-16 06:09:02 +02:00
David Montero Crespo 5fc57c264d fix(minimap): live-track pan/zoom + real board footprints + bigger map
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.
2026-07-16 05:51:29 +02:00
David Montero Crespo 0d7dad16dd feat(projects): duplicateProject accepts name/description/visibility overrides
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)".
2026-07-16 04:51:24 +02:00
David Montero Crespo 6eba0d4850 feat(projects): duplicateProject client for POST /projects/{id}/duplicate
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.
2026-07-16 04:27:32 +02:00
David Montero Crespo cb14af7b4f ci: stop docker/build-push-action from uploading .dockerbuild artifacts
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.
2026-07-16 03:02:55 +02:00
David Montero Crespo 57015212ab feat(picker): featured components sort first — breadboards lead the list
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.
2026-07-16 01:04:29 +02:00
David Montero Crespo 56686da95f feat(breadboard): use the Fritzing parts-library artwork (CC-BY-SA 3.0)
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).
2026-07-15 22:12:00 +02:00
David Montero Crespo 4d0c30e12c style(breadboard): whiter body tone, closer to the familiar breadboard look 2026-07-15 22:01:28 +02:00
David Montero Crespo 6307aed007 feat: breadboard (full 830-point) + mini breadboard (170-point) parts
Two velxio-native passive parts, rendered as web components with
programmatic SVG + precomputed pinInfo (velxio-breadboard 830 holes,
velxio-breadboard-mini 170). Pin names follow the Wokwi convention
(holes `18t.d` / `17b.i`, rails `tp/tn/bp/bn.N`) and the metadata ids
are `breadboard` / `breadboard-mini`, so wokwi diagram.json zips
import/export with no aliasing.

Internal connectivity (5-hole column strips, full-length power rails)
is centralized in utils/breadboardNets.ts and wired into every net
consumer:
- NetlistBuilder: unionBreadboardGroups joins wired holes per group at
  the union-find level in buildNetlist, buildWireNetMap and
  buildBoardPinNetMap — SPICE, the circuit verifier and the voltage
  overlay all see one net per strip/rail with no extra cards.
- DynamicComponent.traceDetailed: the digital trace hops through every
  other wired hole of the entered group, so parts wired through a
  breadboard still resolve their board pin (2-terminal
  PASSIVE_PIN_PAIRS could not express N-hole groups).

Verified end-to-end in the app: Uno pin 8 -> full-board column ->
resistor -> mini-board column -> LED -> ground rail -> GND lights the
LED, and the HUD shows the 3 collapsed SPICE nets. 8 new unit tests
(breadboard-nets.test.ts); netlist-builder + circuit-verifier suites
stay green.
2026-07-15 21:50:36 +02:00
velxio-deploy 814755b249 chore(examples): refresh 1 thumb file(s) [auto] 2026-07-15 21:07:04 +02:00
David Montero Crespo 2a3935f682 fix(esp32-worker): never block QEMU callbacks on stdout (_emit queue)
_emit() wrote to stdout synchronously from QEMU callback context — the
iothread fires _on_uart_tx per UART byte, each becoming ~45 bytes of
JSON. When the parent's pipe reader stalled, the 64 KB pipe filled and
the write blocked INSIDE the QEMU iothread, freezing the entire guest.
Observed as an intermittent (~1 in 10) boot hang: serial output stops
right after the ROM log / rtcinit line — exactly where the accumulated
boot events cross the pipe capacity — and never recovers. Direct worker
harness runs (fast reader) never reproduced it; the browser/WS path did.

Route _emit() through a bounded queue drained by a dedicated writer
thread (opportunistic batching, one write per drain). Under extreme
backpressure events are dropped and counted on stderr — losing telemetry
beats freezing the emulated CPU. The shutdown path posts a sentinel and
joins the writer so crash/system events still flush before os._exit.

Verified on staging: 10/10 UI run/stop cycles + 8/8 direct harness boots
with identical serial latency (~0.95s to first app output).
2026-07-15 20:00:15 +02:00
David Montero Crespo 7da7dc8844 fix(esp32-worker): size the IOMUX pull scan by chip GPIO count
_refresh_pin_pulls read a fixed 40 registers from get_internals(3), but
the exposed array is per-chip: classic muxgpios[40], ESP32-S3 49
(GPIO0..48). The fixed bound silently missed S3 pulls on GPIO40-48.
Read _GPIO_COUNT entries instead (set from the machine at startup).

Pairs with qemu-lcgamboa 547e989, which models the S3 IO_MUX pull bits
and exposes them via the get_internals override — together they make
INPUT_PULLUP buttons work on the emulated S3 exactly like the classic
ESP32 (verified on staging with GPIO4 and GPIO40).
2026-07-15 19:39:17 +02:00
David Montero Crespo 7734eca564
Update README.md 2026-07-13 00:26:28 -03:00
David Montero Crespo daf6000a11
Update README.md 2026-07-13 00:25:16 -03:00
David Montero Crespo 90285cdda7
Update Star History section with responsive chart 2026-07-13 00:24:34 -03:00
David Montero 2fa5d801ec feat(esp32s3): use a 49-pin GPIO pinmap for the ESP32-S3 worker
The S3 has GPIO0..48; the identity pinmap length drives how many pins
picsimlab_wire_gpio connects to the host. Default 40 left GPIO40..48 unwired,
so digitalWrite/Read on those pins never reached the frontend. Build a 49-entry
pinmap when the machine is esp32s3 (mirrors the c3->22 special case). Requires
the GPIO-model widening in libqemu (esp32_gpio bank-1 8->17).
2026-07-11 05:11:10 +02:00
David Montero 305e37561e feat(examples): add ESP32-S3 ILI9341 TFT example
Hardware-SPI (FSPI/GPSPI2) ILI9341 draw from an ESP32-S3: fill + rounded
rect + text via Adafruit_GFX/ILI9341. Exercises the S3 GPSPI2 controller
end to end (worker SPI stream + DC/CS/RST -> the canvas TFT decoder).
2026-07-11 04:18:07 +02:00
David Montero e5f43c07c6 fix(store): sync simulator with activeBoardId in addBoard
When addBoard promotes a board to active (first board, or the previously
active one was removed) it set activeBoardId without syncing s.simulator,
unlike setActiveBoardId which sets both. Parts that read s.simulator - SPI
displays (ILI9341) attach spi.onByte to the active simulator - then wired
onto the previous board's bus and never received data, so a boards[] ESP32
example with a TFT rendered black. Sync simulator to the promoted board
(no-op when the active board is unchanged).
2026-07-11 04:18:07 +02:00
David Montero Crespo 4a158fc40b fix(espidf): compile ESP32-S3 for the esp32s3 target (not esp32)
_idf_target had no S3 case, so every S3 FQBN (esp32s3 / XIAO_ESP32S3 /
nano_nora) compiled as plain esp32 (LX6) and could not boot the S3 QEMU
machine. Add _is_esp32s3 (covers all three FQBNs; nano_nora has no 's3'
token) + return 'esp32s3'; add the xtensa-esp32s3-elf toolchain to the
build PATH; place the S3 second-stage bootloader at flash offset 0x0
(like C3) instead of 0x1000 via 'is_c3 or idf_target==esp32s3' at the merge.

Requires the xtensa-esp32s3-elf toolchain in the image
(install.sh esp32,esp32c3,esp32s3) and a libqemu-xtensa with an
esp32s3(-picsimlab) machine.
2026-07-11 01:51:02 +02:00
velxio-deploy 8958fe7c88 chore(examples): refresh 4 thumb file(s) [auto] 2026-07-09 23:23:09 +02:00
David Montero d7f4e8e966 feat(ssd1306): add the 4-pin I2C OLED module + examples on Uno/ESP32/Pico/STM32
Adds `velxio-ssd1306-i2c-4pin`, a native 4-pin SSD1306 OLED module
(GND/VCC/SCL/SDA) — the cheap 0.96" I2C board most beginners actually have,
matching Wokwi's board-ssd1306. The 8-pin `wokwi-ssd1306` breakout stays; this
is the distinct 4-pin part (issue #215). Same SSD1306Core render pipeline
(imageData/redraw) so the display paints identically; I2C-only, address via the
i2cAddress property (default 0x3C). Styled after the existing 8-pin element
(blue PCB, dark screen, corner holes, star).

Ships four "SSD1306 OLED (4-pin I2C)" gallery examples wiring it over I2C on
Arduino Uno (A4/A5), ESP32 (21/22), Raspberry Pi Pico (GP4/GP5) and STM32 Blue
Pill (PB7/PB6).
2026-07-09 23:13:50 +02:00
David Montero 95e9fe9716 fix(routing): redirect /en/* to the prefix-free path instead of a blank page
English is the default locale and is served at the root with no prefix, so
/en/project/x (a natural guess by analogy with /es/, /zh-cn/, ...) matched no
route and rendered blank. Redirect /en/* -> /* (and /en -> /), preserving query
and hash, so those URLs land on the right page while the canonical prefix-free
English URLs stay put for SEO. The other 8 locales already work under their
/<locale>/ prefixes.
2026-07-09 22:33:56 +02:00
David Montero b51bbcf06d refactor(ssd1306): drop the i2c/spi aliases; CS-only auto-detect + protocol pin
Follow-up to the SSD1306 picker consolidation. All 68 saved projects that used
the retired ssd1306-i2c / ssd1306-spi ids have been migrated to the single
`ssd1306` (metadataId rewritten, protocol pinned), so the simulation aliases
are no longer needed and are removed.

- Auto-detect refined to CS-only: chip-select is the SPI-exclusive signal;
  DC does NOT imply SPI (on the 8-pin module DC doubles as the I2C address /
  SA0 line, so many I2C circuits wire it). Fixes false-SPI on those circuits.
- The `ssd1306` part honors an explicit `protocol` property when present
  (migrated legacy projects carry it) and auto-detects otherwise.
- loadProjectState normalizes any lingering ssd1306-i2c/spi ids (old .vlx
  files, pre-migration snapshots) to `ssd1306` + the matching protocol, so
  removing the aliases can never blank an old import.
2026-07-09 22:13:31 +02:00
David Montero b6dd2f5201 feat(ssd1306): one auto-detecting OLED part (merge the I2C/SPI picker entries)
The SSD1306 was three picker entries — a generic `ssd1306` with a protocol
selector plus `ssd1306-i2c` / `ssd1306-spi` shortcuts (issue #101) — all the
same 8-pin wokwi-ssd1306 element. That is confusing for one physical module
(issue #215). Wokwi ships a single I2C-only part; this goes one better: a
single part that auto-detects the protocol from the wiring, like a real
breadboard — CS or DC wired to a GPIO means SPI, otherwise I2C. No protocol
switch to set, just wire it up.

Works on every board with an I2C/SPI bus (AVR, RP2040, ESP32 Xtensa, STM32).
The ssd1306-i2c / ssd1306-spi ids stay as backward-compat simulation aliases
for projects saved before the merge, but are removed from the picker. Adds an
i2cAddress property (0x3c/0x3d) matching the real module and Wokwi.

Note: ESP32-C3, Raspberry Pi 3 and the bare RISC-V board do not emulate I2C/SPI
peripherals, so no I2C/SPI device (this or any other) attaches there yet.
2026-07-09 21:46:40 +02:00
David Montero 061bb91da1 fix(esp32-c3): drive digital inputs from the SPICE solve (spiceDrivenInputs)
Esp32C3Simulator already had the GPIO_IN plumbing (setPinState -> gpioIn ->
GPIO_IN_REG read) but never opted into connectDigitalInputsToMcu, so a pin
wired to a switch/button was never fed the solved circuit voltage and
digitalRead() ignored the real wiring. Enabling the flag (as AVRSimulator and
RP2040Simulator already do) completes the issue #247 fix: the ESP32-C3 now
reads GPIO2 from the SPICE solve, so toggling the slide switch flips the LED.
BasicParts' button/slide-switch seed already yields to spiceDriven(), so there
is no double-drive.
2026-07-09 19:16:37 +02:00