Commit Graph

128 Commits

Author SHA1 Message Date
David Montero Crespo c9fe6f199d feat(editor): folders - create/delete folder trees in the file explorer
File names may now carry '/' paths; the explorer renders each board group
as a collapsible folder tree (folders first, files in group order, CSS-var
indentation because .fe-file-item pins padding with !important). New-folder
buttons on the board header and per-folder hover actions (new file here,
new subfolder, delete with confirm when files are inside; a folder holding
every file of the group cannot be deleted - groups keep >= 1 file).

Model: names are the source of truth; only EMPTY folders are tracked
(folderGroups per group) so they survive until a file lands in them, then
materialise into the name prefix. Round-trips through .vlx (optional
folderGroups field, laxly validated), the auth stash draft, and
loadProjectState. Examples gain boards[].files so a single board can ship
a full multi-file tree (first entry = main sketch).

Backend: the arduino-cli sketch writer now creates subdirectories for
'/'-pathed names and REJECTS names that resolve outside the sketch dir
('..' traversal) - previously it crashed on the former and wrote outside
the tmp dir on the latter.
2026-08-05 06:48:38 +02:00
David Montero Crespo 36c414df8f feat(sd): source files stay off the card - only project DATA files auto-copy
On real hardware your code lives in flash, not on the microSD; a sketch
listing the card should see its data (csv, json, txt, wav...), not its
own source. buildProjectSdImage now skips .ino/.h/.hpp/.c/.cpp/.py from
the workspace auto-copy. Explicit uploads are exempt - an upload always
lands whatever its extension. Panel hint updated; tests cover both the
exclusion and the upload exemption (15 green).
2026-08-04 19:58:31 +02:00
David Montero Crespo baa5925f95 feat(sd): live Card contents in the SD panel - list and download what the sketch wrote
The SD panel only knew about uploads; everything the GUEST wrote to the
FAT card (photos, logs) stayed invisible inside the bridge. New
readFat16Image() parses the card image back (BPB-driven, real cluster
chains, VFAT long names, root dir - the inverse of buildFat16Image but
general enough for FatFs-written cards), and the panel gains a live
'Card contents' section reading the bridge's readSdImage() with a
per-file Save (download) button and a Refresh. Round-trip covered in
fat-image.test.ts (16 green).
2026-08-04 16:29:54 +02:00
David Montero Crespo cfa03aeaf9 feat(sd): boards with a built-in microSD slot take user uploads
A ProBoardDef can declare builtInSdCsPin, and the store already built a FAT
card from the project files for it - but there was no way to put YOUR files
on that card: the upload panel only existed for the microsd-card component
and for parts with metadata.sdSlot, and boards have no properties bag.

Boards now carry sdFiles (same shape the SD panel persists everywhere else):

- BoardInstance.sdFiles, consumed by the store's Run path (card component
  still wins when one is on the canvas);
- the board inspector shows the SD Card panel whenever the board kind
  declares builtInSdCsPin, and routes the panel's onChange to updateBoard;
- .vlx round-trips it (serialisableBoard + payload type), and gallery
  examples can pre-load the slot via ExampleBoard.sdFiles, applied by
  loadExample the way the panel would.

First user: the XIAO ESP32S3 Sense (CS=GPIO21), whose board def and example
land in the pro overlay.
2026-08-04 05:55:21 +02:00
David Montero Crespo 7dd9b90cf9 fix(ui): a column is a RUN of pads, not two opposite ends of the board
The Arduino Uno came out wrong: 18 pins on the top header, 13 on the bottom,
and SIX x values shared between the two. The column detector accepted any two
pads sharing an x, so those pairs were read as columns and most of the board
was thrown into the side gutters with leader lines fanning across the art.

A column now needs three pads AND small gaps between them (no more than 40%
of the body), which is what a real header looks like; rows get the same guard
on x. The Uno lands as two headers, top and bottom, with no side gutters at
all - covered by a test using its real 31-pin table.
2026-08-04 03:01:21 +02:00
David Montero Crespo 4be8ddd6c3 fix(ui): a pin stack taller than the part was clipped by the preview
The ReSpeaker's left side carries 13 labels: at the minimum spacing that is
208px against a 190px body, so the column ran past both ends and the first
and last labels were cut off by the preview's clip. The gutters now grow to
whatever the labels actually occupy - the box follows the content.
2026-08-03 23:20:49 +02:00
David Montero Crespo cbc74d8409 fix(ui): the inspector dropped labels for inset pin columns, and flickered
Three things the dialog got wrong in use:

1. Missing pins. Classification was nearest-edge with an "interior" bucket,
   and interior pins printed no label - only a dot with a tooltip. The
   ReSpeaker Lite showed it: its XIAO socket is two INSET columns (x=50.5 and
   126.5 on a 178-wide body) plus two breakout columns, so most of its pins
   were called interior and the part looked like it was missing them. The
   classifier now finds the LINES first - pins sharing an x are a column,
   pins sharing a y a row - assigns each line to the side of the body it sits
   on however far inset it is, and joins label to dot with a leader. Only a
   lone pad falls back to nearest-edge. Every pin gets a printed label; that
   is now the contract, covered by a ReSpeaker test with its real table.

2. The panel was cut off on the Datasheet tab until some unrelated event
   re-rendered it. The datasheet loads asynchronously, so the one-shot
   measure placed the dialog while it was still short and it then grew off
   screen. A ResizeObserver repositions on any size change, and the tab body
   caps its own height so long markdown scrolls inside the tab.

3. Flicker when the pointer left and re-entered. The preview effect depended
   on componentProperties and pinInfo by identity - both are rebuilt on every
   parent render (pinInfo is a getter) - so the element was torn down and
   remounted constantly. It now depends on stable digests of them.
2026-08-03 23:04:37 +02:00
David Montero Crespo b02aab9a47 fix(ui): crowded pin headers escaped the inspector - draw those labels vertically
An SSD1306 has 8 pins along a 240px header. Horizontal labels need ~36px
each = 288px, so they overflowed the art box and rendered outside the dialog,
over the header and the canvas. Top/bottom labels are now vertical text - what
printed pinout diagrams do - which needs only the side spacing, and the gutter
grows to fit. The preview also clips, so nothing can escape again.
2026-08-03 22:04:43 +02:00
David Montero Crespo d4a42a4e75 feat(ui): the Part Inspector - right-click dialog merged with the picker card
The right-click dialog was a 250px column: every pin as a text row (an ESP32
means 30 rows of scrolling), a few editors, Rotate/Delete. The picker's info
card (datasheet, brand, Buy) was a separate surface you could only reach by
hovering the catalogue. One dialog now serves both:

- ~560px, two panes: live preview + actions on the left, Properties/Datasheet
  tabs on the right. The Datasheet tab IS the picker card's body -
  ComponentInfoPanel was split into an embeddable ComponentInfoBody (+
  useComponentDoc, and its .cip-* styles moved to their own stylesheet) so
  both surfaces render one implementation.
- "Show pins" (off by default): the preview is the REAL web component,
  mounted and scaled, with pin markers at their true pinInfo positions and
  labels flanking the side each pin lives on. pinInspectorLayout is pure
  geometry - nearest-edge classification, per-side label stacking with a
  minimum spacing and leader lines when nudged, interior pins as
  dot+tooltip - driven only by pinInfo plus the measured element size, so
  every part and board, present or future, works with no per-part data.
  Dots are tinted by the pin's declared signal (i2c/spi/power/...).
- Clicking a pin keeps the old contract (onPinSelect -> handlePinClick), so
  wires start with the same undo, colors and running-state gating.
- The two open gestures finally agree on coordinates: right-click always
  passed viewport coords while touch passed canvas-local ones; the dialog is
  now portaled to body in viewport space and the touch site converts.
- Keybind capture, the SD Card panel, the two-step delete, Escape and
  click-outside all migrate unchanged. Mobile keeps the full-sheet layout.

Geometry covered by pin-inspector-layout.test.ts with coordinate tables
copied verbatim from the shipped parts (ESP32 two-column board, LED, an
interior-pin case).
2026-08-03 21:44:46 +02:00
David Montero Crespo 13a8403d6d fix(sockets): a board a hair off the seat is still plugged in 2026-08-01 06:14:43 +02:00
David Montero Crespo fdcf0d19d2 feat(canvas): ver donde cae lo que anades, y clicks con sentido
Tres cambios que van juntos porque atacan la misma queja: "anado un
elemento y a veces ni veo donde se anadio".

1. Donde cae. Ya se anclaba a la esquina visible, pero la cascada que evita
   que se apilen iba indexada por components.length: seguia avanzando
   aunque movieras o borraras piezas, asi que las caidas se alejaban cada
   vez mas de donde estabas mirando. Ahora toma el primer hueco LIBRE desde
   la esquina, bajando en diagonal; si apartas la ultima, la siguiente
   recupera su sitio. Extraido a utils/dropSlot con 8 tests, incluido el
   caso de "la apartaron" y el tope para no salirse de la vista.

2. Que se vea. El recien anadido queda seleccionado, y la seleccion pasa de
   un borde discontinuo quieto a un caminito de hormigas. El movimiento es
   lo que capta el ojo en un canvas lleno; un borde fijo se pierde. Va en
   un pseudo-elemento por fuera del cuerpo, sin robar clicks ni tapar el
   dibujo, y se queda quieto si el sistema pide menos animacion.

3. Clicks. El izquierdo SELECCIONA y ya esta; antes abria el panel de
   propiedades, o sea que no podias ni senalar una pieza sin comerte un
   popup que luego habia que cerrar. Propiedades y pines pasan al click
   derecho, que es donde va lo deliberado. En tactil se mantiene tocar ->
   panel, que ahi no hay boton derecho.
2026-07-30 15:40:52 +02:00
David Montero Crespo 93388c675a fix(pi): perifericos completos en la familia Pi — entradas, buses y pines
Auditoria de "la Pi tiene todo lo de la placa real" con cuatro huecos
encontrados y cerrados:

1) GPIO de entrada en modo Linux: GPIO_IN respondia VAL 0 fijo (stub de
   la fase 2), asi que GPIO.input() leia 0 eternamente aunque el canvas
   empujara el nivel. El backend guarda ahora el ultimo nivel por pin
   (set_pin_state lo escribe) y GPIO_IN contesta de ahi. Los flancos
   (SET) siguen llegando al guest como antes.

2) UART del header hacia otra placa: el shim del rootfs ya hablaba
   `UART <port> TX <hex>` / RX_REQ, pero sin modelo de esclavo el
   backend tragaba los bytes. Ahora TX sin esclavo se emite al canvas
   (uart_tx) y RX_REQ sin esclavo drena la cola que llena pi_uart_rx —
   el mismo protocolo de siempre, sin ops nuevas.

3) El escaner de esclavos I2C/SPI/UART estaba doblemente muerto:
   clasificaba por numero fisico de pin ('3','5','19'...) cuando el
   elemento expone GPIOxx, y su unico llamador era RaspberryPiWorkspace,
   que el terminal unificado reemplazo. Acepta ambos nombres y corre en
   onBooted del store.

4) boardPinToNumber solo mapeaba los pines de la 3/4/5; la Zero, 1B+ y
   2B (mismo header de 40 pines, mismo elemento) se quedaban sin mapa.
2026-07-29 17:05:39 +02:00
David Montero Crespo 9160624da4 fix(uart): la Pi puede hablar por serie con la placa de al lado
Dos piezas que faltaban para que pi-to-arduino-led-control fuera algo
mas que un guion imprimiendo lo que "habria enviado".

1) classifyPin no reconocia los pads del header por su nombre. Se llaman
   GPIO14 / GPIO15 en el dibujo de la placa y en todos los cables de los
   ejemplos, pero solo se aceptaban numeros fisicos: parseInt('GPIO14')
   daba NaN, el pin no clasificaba como nada y el Interconnect nunca
   construia la ruta. Ahora se acepta el prefijo GPIO/BCM y la numeracion
   fisica sigue funcionando.

2) Seam de serie para placas que no tienen ni simulador ni bridge: el
   motor de navegador corre el Python de la Pi en la propia pestana.
   registerSerialSink(placa, fn) recibe los bytes que le llegan y
   feedBoardSerialOut(placa, ch) anuncia los que envia, que es lo que el
   enrutado por cables ya sabia repartir.
2026-07-29 07:40:13 +02:00
David Montero Crespo bbafe66ee7 feat(pi-family): unified editor UX — Monaco + explorer + bottom xterm, one file surface
QEMU-Linux boards used three competing file surfaces (workspace group
with a meaningless sketch.ino/libraries.json, the VFS panel with its
Upload button, and the Pi workspace's own editor). Now they behave like
every other board:

- the editor file group defaults to script.py for ANY Pi-family kind
  (kind-based check via isPiBoardKind, not the old raspberry-pi- string)
- the libraries.json manifest row is hidden for Pi boards
- EditorPage always renders Monaco; the RaspberryPiWorkspace swap is
  gone
- the bottom serial panel renders the interactive xterm (PiTerminal,
  now seeded with session history) for running Pi boards
- example vfsFiles load into the editor group (single source of truth);
  the run path uploads the group into the guest home
- Run on a booted guest re-runs without the 45 s reboot (Ctrl-C +
  re-upload + run); starting a Pi board pops the terminal open
- piSyncAndRunScript/piRerunScript exported from the store; auto-run
  now applies to the whole family (guestHome/autoRun overridable)
2026-07-28 19:43:20 +02:00
David Montero Crespo dd86020343 feat(pi-family): one-click run UX — autoRun + guestHome + clean serial mirror
- ProBoardDef.autoRun: after boot (+guestSetup) the VFS uploads itself
  and the command runs, so a single Run click boots, uploads and starts
  the user's script (same UX as compiled boards)
- ProBoardDef.guestHome: VFS home dir override ('/root' for guests that
  log in as root); those boards drop the historic hello.sh sample
- upload sequence extracted to utils/piUpload (shared by the VFS panel
  button and autoRun)
- serial monitor strips DEL/C0 control echoes (backspace showed tofu)
2026-07-28 16:28:02 +02:00
David Montero Crespo 98df5134cb fix(canvas): las placas solo pintan encima cuando estan POSADAS en un zocalo
El bump global a zIndex 3 (efecto iman del Round Display) puso TODAS las
placas por encima de TODOS los componentes: una resistencia al lado de un
Arduino quedaba oculta tras la placa en cualquier ejemplo normal (se veian
los cables, no el cuerpo). Ahora isBoardSeated (misma matematica del snap,
umbral 0.5px) decide: posada -> z3 encima de su zocalo, como el XIAO fisico
apilado en el shield; libre -> z0 debajo de los componentes, como siempre.
El iman coloca la placa exactamente en el asiento, asi que al capturarla
salta al frente y al arrancarla vuelve abajo.
2026-07-28 15:22:56 +02:00
David Montero Crespo 24d25966a8 feat(canvas): iman de zocalo — una placa se posa sobre un shield que la acepta
Como el iman de la breadboard pero para PLACAS: un componente puede declarar en
su elemento (estilo regla 6a, igual que pinInfo) que lleva un zocalo:

    get boardSocket(): { anchorPin, accepts }

y una placa cuyo boardKind case con accepts, arrastrada cerca, se posa de golpe
con su pad anchorPin sobre el homonimo del zocalo. Un solo ancla basta porque
ambas rejillas comparten paso — esa es la gracia de un zocalo. Arrastrarla mas
alla de la tolerancia la suelta, sin estado que recordar.

Enganchado en los dos caminos de arrastre de placas (raton y tactil). El
overlay privado declara el zocalo sin que este repo sepa que existe.
2026-07-27 17:36:52 +02:00
David Montero Crespo a698534b7b fix(pins): la cabecera del M5 Cardputer ya resuelve a numeros de GPIO
boardPinToNumber no tenia rama para 'cardputer-adv', asi que caia al return
null del final: cada cable a su cabecera EXT o al Grove Port A se conectaba a
nada. La pieza quedaba en el lienzo, el sketch leia un pin muerto y nadie
avisaba de nada.

La rama compartida de esp32 tampoco habria servido: recorta en 39 y este board
saca G40 en la cabecera. El S3 tiene 48 GPIOs.
2026-07-26 02:47:05 +02:00
David Montero Crespo fd6829f645 merge: master (produccion) dentro de v3.2
Las dos ramas habian divergido: master llevaba el modo lenguaje ESP-IDF puro
(#139) y v3.2 la ruta de compilacion IDF v5.5 para toda la familia ESP32 mas
los arreglos de venv/toolchain. Ambas tocaban espidf_compiler.py.

Los dos lados son ejes ORTOGONALES y se conservan enteros:

  - use_idf5 / arduino_mode (v3.2): que arbol IDF usa el build (5.5 vs 4.4) y
    si cabe Arduino-como-componente.
  - pure_idf (master): el modo LENGUAJE que elige el usuario; sus ficheros son
    las fuentes del componente main con su propio app_main().

Resolucion:
  - _build_env acepta los tres. Un build IDF puro fuerza arduino_mode a falso:
    la plantilla CMake mete el componente arduino-esp32 en cuanto existe
    ARDUINO_ESP32_PATH, asi que dejarlo puesto compilaba el core de Arduino en
    un build que no tiene sketch. Lo cazaron los tests de master.
  - VELXIO_PURE_SKETCH solo con pure_idf, nunca con arduino_mode a falso a
    secas: un target sin core arduino-esp32 sigue entregando un SKETCH al
    traductor legacy y no debe tomar la rama del glob puro.
  - La identidad del build-dir suma los dos tokens (|idf:N|ard:N y |lang:pure):
    ningun par de esas combinaciones puede compartir un build/ configurado.
  - La cadena de escritura de fuentes queda pure_idf -> arduino_mode -> legacy.
  - sdkconfig: render de v3.2 (con target/use_idf5) mas el filtrado de simbolos
    CONFIG_ARDUINO* de master cuando el build es puro.

test/backend/unit/test_espidf_compiler.py: los 7 tests que ya estaban rotos en
v3.2 (AttributeError: idf5_path, fixture sin actualizar desde que se anadio la
seleccion de IDF) vuelven a pasar.

Verificado: backend 293 pasan / 0 fallan (v3.2 traia 7 rotos); frontend 2268
pasan / 0 fallan en los dos shards.
2026-07-25 21:52:00 +02:00
David Montero 734b7d0487 feat(esp32): pure ESP-IDF language mode for the ESP32 family (#139)
Adds a third entry to the board language selector next to Arduino C++
and MicroPython: ESP-IDF. In this mode the user writes a plain ESP-IDF
project — app_main() entry point, FreeRTOS + driver APIs — and the
backend compiles it through the same ESP-IDF toolchain it already uses
for ESP32 Arduino sketches, just without the arduino-esp32 component.

Backend:
- CompileRequest.language ('espidf') threaded through the sync + async
  compile paths and folded into the dedup job key (language='arduino'
  and omitted hash identically so old clients keep dedupping).
- espidf_compiler: pure_idf flag. User files are written into main/
  as-is (no Arduino.h wrap, no velxio_compat.h, Arduino library
  resolution skipped), ARDUINO_ESP32_PATH is dropped from the build env
  and VELXIO_PURE_SKETCH raised so the template CMake compiles the
  user's own sources via a glob branch. Pure builds get their own
  persistent build-dir variant through the eff_hash fold.
- QEMU WiFi compat for IDF-style code: esp_wifi.h/esp_wifi_init
  detection sets has_wifi, and literal #define SSID/PASS plus
  wifi_config_t designated initializers are normalized to the QEMU AP.
- CONFIG_ARDUINO_* lines are stripped from sdkconfig.defaults in pure
  mode (the symbols don't exist without the arduino component).

Frontend:
- LanguageMode gains 'espidf'; BOARD_SUPPORTS_ESPIDF covers the ESP32
  family (Xtensa, S3, C3). Toolbar shows the option only for those.
- Switching modes seeds a main.c blink skeleton (app_main + gpio
  driver), mirroring the MicroPython main.py flow.
- compileCode sends language='espidf'; run/stop paths are unchanged
  (the QEMU worker consumes the same merged flash image).
- New gallery example: esp32-idf-blink (LED + resistor on GPIO 2).

Tests: unit coverage for the build-env switch, IDF wifi normalization,
job-key variance, file-group seeding and the new example; verified
end-to-end in a container from the prod image (pure build produces a
bootable flash image; Arduino-mode build unchanged, same variant hash).
2026-07-24 06:37:01 +02:00
David Montero Crespo 4392a91539 fix(boards): isBoardComponent recognizes overlay-registered boards (CP3)
A wire to a pro board's Dx pad (XIAO 'D2', etc.) did nothing because
isBoardComponent only knew the OSS board-id list, so the electrical/sensor
chain skipped the board endpoint. It now also returns true for any
proBoardRegistry kind, so boardPinToNumber (which already consults the pro
def's pinToNumber) resolves the Dx name. Fixes the numeric-alias-only
workaround in the XIAO examples.
2026-07-24 01:43:15 +02:00
David Montero Crespo 32958640a3 feat(boards): runtime board-registration seam for private overlays
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.
2026-07-22 21:21:21 +02:00
David Montero Crespo d726139946 feat(breadboard): one-hole-one-wire selection rules + jumper colors
Fixes the reported breadboard wiring UX ("requerimos un vocabulario"):

Vocabulary implemented (breadboardOccupancy.ts, pure + unit-tested):
  - 1 hole = 1 wire. A hole already holding a visible wire can't start a
    new one — clicking it SELECTS that wire. This is the core fix: wires
    running hole-to-hole across the board were impossible to select
    because the pin overlays swallowed every click and silently started a
    new wire (so the top horizontal rail wire was un-deletable).
  - Same 5-hole strip / rail = one net. When a new wire end lands in an
    occupied hole (a seated leg or another wire), it shifts to the
    NEAREST FREE hole of the same group — electrically identical, the
    real-world "bridge to the next hole in the row". Never crosses strips.

Two selection bugs behind the symptom:
  - Click on a wire lying over the breadboard BODY now selects the wire
    instead of opening the breadboard's 830-hole property dialog (that
    list popping over everything was the "se sobrepone la lista de todos
    los puntos" report). Guarded so the bubbled canvas click doesn't
    re-toggle the fresh selection.
  - Click on a hole occupied by a wire selects the wire (handlePinClick),
    so wires anchored in holes are reachable at all.

Jumper colors (like a real kit — a board of identical green wires is
unreadable, "se ven todos verdes"):
  - Power-rail holes mandate red (tp./bp. = +) / black (tn./bn. = −).
  - Other breadboard holes get a random jumper-palette color on manual
    draw; red and black are reserved for rails.
  - jumperColorForId gives agent/deterministic callers a stable per-wire
    color across reloads.

Tests: breadboard-occupancy.test.ts (12) — findWireAtHole (skips seating
wires, topmost wins), resolveFreeHole (same-strip shift, no cross-strip,
rail shift, passthrough), color policy (rails, palette determinism,
red/black reserved).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-21 05:49:23 +02:00
David Montero Crespo 0635e15e7a fix(router): escape corridors for endpoint-in-obstacle + checked-elbow parity
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>
2026-07-20 21:35:58 +02:00
David Montero Crespo 651161559a feat(wires): avoid other wires + live routed preview + system-owned shapes
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>
2026-07-20 20:08:25 +02:00
David Montero Crespo d612c3bae7 feat(breadboard): green dots on pins plugged into a breadboard
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>
2026-07-20 04:48:16 +02:00
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 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 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 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 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 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
David Montero 07a0bfef6e seo: emit trailing-slash canonical + sitemap URLs
Static/docs/example routes are served as <route>/index.html and nginx
301-redirects the slash-less form to add the trailing slash. The sitemap
generator, the prerender canonical/og:url, and the client useSEO canonical
all emitted the slash-LESS form, so every sitemap URL was fetched as a
redirect (filed under 'Page with redirect' in Search Console) and each
canonical pointed at a redirecting URL.

Align all three to the trailing-slash form so sitemap URL == canonical ==
served URL == 200, with no redirect hop.

- generate-sitemap.mjs: append '/' to every <loc> (root stays '/')
- prerender-seo.mjs: withSlash() on canonical + og:url (routes + examples)
- useSEO.ts: withTrailingSlash() on canonical + og:url (covers dynamic
  project pages too)
2026-06-15 06:10:32 +02:00
David Montero fddc03aa60 fix(interconnect): classify Arduino Mega UART/I2C function-label pins
Follow-up audit after the ESP32 fix: classifyPin() was run for every board
against the protocol pin labels its element actually exposes. One real gap
remained -- Arduino Mega. Its dedicated SDA/SCL pins are only labelled (not
numbered), so I2C links drawn on them came back 'digital' and never bridged.
Map every Mega function label (TX/RX, TX0-3/RX0-3, SDA/SCL) to its pin number.

Audit result for the rest (added as board-protocols-audit.test.ts):
- Arduino Uno/Nano, Pico/Pico-W, STM32 Blue Pill: already OK.
- ESP32 / ESP32-C3: fixed earlier (esp32-uart-pin-classify).
- Raspberry Pi 3/4/5: OK -- the element labels pins by physical number (1..40)
  which normalize to BCM, so no function-label gap exists there.
2026-06-12 08:29:03 +02:00
David Montero 8ef730b9f7 fix(interconnect): classify ESP32 UART pin names so multi-board Serial works
Wiring two ESP32s TX2->RX2 (Serial2) or TX->RX for board-to-board serial
produced no data on the receiver: classifyPin() returned 'digital' for the
UART pins, so the Interconnect never installed the byte-level UART bridge.

Two causes in boardProtocols.ts normalizePinName:
- TX/RX aliases only matched boardKind === 'esp32' exactly, missing every
  variant (esp32-devkit-c-v4, esp32-cam, esp32-s3), and TX2/RX2 were not
  handled at all. Resolve them via startsWith('esp32') (esp32-c3 kept
  separate) and map TX2/RX2 -> GPIO17/16.
- 'GPIO17'-style labels fell into the 'GP' (RP2040) branch first, where
  parseInt('IO17') = NaN swallowed them to null. Exclude 'GPIO' from the
  'GP' branch so the ESP32 GPIO-prefix handling runs.

Adds esp32-uart-classify.test.ts (6 cases, green).
2026-06-12 07:21:47 +02:00
David Montero 22de488de2 feat(microsd): SD-over-SPI card storage for AVR, RP2040 and ESP32
Add a working microSD card part backed by a FAT16 image, following the
Wokwi storage model: the project's own workspace files are auto-copied
onto the card (free), and an optional "SD Card" panel uploads extra
files (gated as a paid feature by the velxio.dev overlay; OSS default
allows it).

Frontend (in-browser AVR / RP2040):
- ProtocolParts.ts: rewrite the microsd-card part from a handshake stub
  into a real SD-over-SPI device (reply-first Ncr timing, SDSC byte
  addressing, single/multi-block read+write, CSD/CID, full CMD set).
- utils/fatImage.ts: dependency-free FAT16 super-floppy builder (8.3 + LFN).
- utils/sdCardFiles.ts: assemble the card image from workspace files plus
  uploaded files; base64 helpers.
- components/simulator/SdCardPanel.tsx + ComponentPropertyDialog: upload UI.
- DynamicComponent + useSimulatorStore: build and inject the image on run.
- lib/proSdCardGate.ts: overlay-installable gate for the upload action.
- data/examples-storage-microsd.ts: Arduino Uno + ESP32 gallery examples.

Backend (ESP32 via QEMU):
- services/esp32_sd_slave.py: synchronous SD-over-SPI slave (Python port of
  the browser part) with a sparse backing store, idle-state R1 tracking and
  real CRC16 on data blocks when the host enables CRC (CMD59) -- both
  required by ESP-IDF's sdspi driver.
- esp32_worker.py: route SPI bytes to the slave (returns MISO synchronously)
  and feed write-only bulk transfers.
- esp32_lib_manager.py + routes/simulation.py: forward the FAT image
  (sd_card.image_b64) from the start config into the worker.

Tested:
- frontend: protocol-parts, fat-image, sd-card-gate and microsd-real-firmware
  (real Arduino SD.h on avr8js) -- 86 passing.
- backend: test_esp32_sd_slave (10) covering the ESP-IDF init sequence and
  CRC16; validated end to end by running a real SD.h sketch in libqemu-xtensa
  (mount, directory listing, read and write-readback).
2026-06-11 03:59:53 +02:00
David Montero 8617d3b224 feat(library-manifest): per-board manifests + autocomplete
Library manifests are now PER-BOARD (each board carries its own velxio.json),
so two boards in one project can use different (even conflicting) libraries
without clashing — the multi-board extension of the no-clash guarantee.

- board.libraries on BoardInstance + serialisableBoard: rides in boards_json,
  so it round-trips, dirty-checks, autosaves and restores natively. This also
  removes the load-restore hacks (useLibraryManifestStore + applyProjectManifest
  deleted): the manifest is plain board state.
- loadProjectState now restores per-board boardOptions/spiffsFiles/libraries
  (it previously dropped them).
- EditorToolbar single + compile-all send the COMPILING board's libraries.
- Backend compile.py prefers the client's per-board request.libraries; the
  project-level libraries_json (now the union of all boards) is the fallback.
- buildLoadPayload migrates pre-per-board projects: seed each board with the
  project union so they keep compiling scoped.
- Library Manager 'In project' tab edits the ACTIVE board's velxio.json (shows
  the board name) and the add field is now an autocomplete (installed libs +
  index search) so users pick from a list instead of typing names.

Deletes useLibraryManifestStore.ts + applyProjectManifest.ts.
2026-06-07 06:07:48 +02:00
David Montero c93924541c feat(library-manifest): user-facing velxio.json config + load-restore
End users can now configure a project's declared libraries (the compile scope):
- Library Manager gains an 'In project' tab = the project's velxio.json:
  declared libs as removable rows, quick add-by-name, and a raw velxio.json
  editor. Installing a library auto-adds it to the project. Installed-tab rows
  get an 'Add to project' toggle.
- FileExplorer shows a velxio.json entry (with declared count) that opens the
  Library Manager via a window event the toolbar listens for.
- applyProjectManifest(): restore a saved project's manifest into the store on
  load so the editor/toolbar/Library Manager/velxio.json reflect it.
- computeProjectStateHash() includes the manifest so declaring a library marks
  the project dirty and autosaves.

Note: the OSS ProjectByIdPage also calls applyProjectManifest for parity, but
velxio.dev routes the pro-overlay ProjectByIdPage (wired separately).
2026-06-07 05:08:34 +02:00
David Montero ee41f361b6 fix(frontend): don't clobber a project's saved library manifest on save
buildSavePayload omitted libraries_json=[] whenever the manifest store was empty
— so an autosave right after loading a project (whose manifest the store hadn't
restored) wiped the saved manifest. Now omit libraries_json entirely when the
store value is null (unknown), so the backend preserves the saved manifest. The
compiler reads it server-side regardless (get_project_libraries hook).
2026-06-07 02:13:58 +02:00
David Montero 288ab46521 feat(frontend): persist + restore project library manifest (P2.4 projects)
Saved projects now round-trip their declared library manifest (compile scope):
buildSavePayload includes libraries_json from useLibraryManifestStore; loading a
project restores it (and clears any stale example manifest). Existing projects
load with an empty manifest -> legacy scan-all (unchanged); new saves capture
whatever manifest is active. Pairs with the backend libraries_json column.
2026-06-07 00:47:39 +02:00
David Montero e947f1e600 feat(frontend): send the example library manifest as the compile scope (P2.3)
Activates manifest-scoped ESP-IDF resolution for the gallery. loadExample now
records the example's declared libraries in useLibraryManifestStore; EditorToolbar
passes them to compileCode, which sends them as `libraries` in the compile
request. The backend then merges exactly those libraries (P2.0 scope) instead of
picking a stray same-named lib from the shared dir.

Safe: a core-only example sends null (legacy scan-all); a stale/incomplete
manifest degrades to scan-all via the backend graceful fallback, never a wrong
build. Ignored by the backend for non-ESP32 (arduino-cli) boards. Example
manifests were completed (incl. transitive deps) in c671c9b.
2026-06-07 00:10:15 +02:00
David Montero dd322b27e3 feat(editor): per-target compilation console — a section per board/chip
Phase 4 of the run-system work. The compile console now groups output into a
section per run target (board or chip) with a status glyph and label, the way
multiple Arduinos already stream — instead of one flat list.

- CompilationLog gains an optional target { id, label, kind: 'board'|'chip' }.
  message/type are unchanged so the pro overlay (diagnose-with-AI prompt +
  errorCount slot) and the console's length-based clear/auto-error heuristics
  are untouched. parseCompileResult stamps the target on every produced line.
- Producers stamp their lines: compileAllBoards (per-board, dropping the old
  '<label>: ' string prefix the header now carries), prepareCustomChips
  (per-chip, WASM + ROM), handleCompile + handleRun MicroPython (single board) —
  including the Pi / MicroPython / FQBN / error paths so a target's lines never
  fragment across sections.
- CompilationConsole groups filteredLogs into consecutive-run sections at RENDER
  time only (the flat array is unchanged); each target section shows ✓/✕/▸ +
  name + kind tag, with no-target lines ('Compiling all targets', 'Done') as
  plain narration around them.

Reviewed by an adversarial pass; the flagged un-stamped edge paths (Pi /
MicroPython / single-board errors) are now stamped.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-04 07:56:54 +02:00
David Montero a68e7f8e94 feat(editor): rename boards & custom chips; show which target owns each file
Phase 2 of the run-system/UX work.

- BoardInstance gains an optional user ; boardDisplayName(board) resolver
  (name || kind label) routes every INSTANCE-label surface: file-explorer
  section header, compile console (EditorToolbar), canvas selector/tooltip/
  context-menu, Serial Monitor tabs, Oscilloscope board picker, Board Options
  subtitle. Board/component pickers keep the KIND label (they pick new boards).
- Inline rename on board AND chip section headers (double-click the name, or a
  hover pencil button). Board -> updateBoard(id,{name}); chip -> chipName in
  properties. Enter commits, Escape cancels (cancel-flag ref guards the
  unmount-fires-onBlur footgun), empty clears to the kind / 'Custom Chip'.
- FileTabs shows an owner badge naming the board/chip whose files are shown
  (resolved as a selector so it doesn't re-render on every sim pin toggle).
- CustomChipDialog no longer clobbers a user-given chipName: chip.json's name
  only seeds the blank defaults (My Chip / Custom Chip); loading an example
  relabels explicitly.
- Persistence: board name round-trips via projectPayload (+ dirty hash),
  vlxFile, ProjectByIdPage load + loadProjectState; chipName rides components_json.
- Drive-by: fixed a pre-existing rules-of-hooks violation in BoardOptionsModal
  (early return before a useCallback).

Reviewed by a 3-agent adversarial pass (completeness / persistence / correctness);
all major findings folded in.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-04 06:22:25 +02:00
David Montero 5a23e89eb5 feat(custom-chip): program lives in its own editor group, not the board sketch
A programmable custom-chip (a CPU emulator that runs a ROM/program, e.g. the
Z80 or 8080) now keeps its program (larson.s, chaser.c, ...) in a dedicated
editor file group — group-chip-<chipId> — rendered as its own collapsible
section in the file explorer, exactly like each board owns its sketch group.
Behaviour/driver chips and predefined chips carry no programFile and get no
group; they stay editable only in the chip designer.

Fixes two reported issues on the Z80 examples:
- /example/z80-larson-no-board: the board-less chip example now opens its
  program (larson.s) as the active group, editable on the left — previously
  the editor showed but no file appeared.
- /example/z80-led-chaser-c: the chip program (chaser.c) no longer shows as
  a sibling tab inside the Arduino sketch group; it sits in its own chip
  section instead. The board group shows only sketch.ino.

Details:
- useEditorStore: chipFileGroupId()/CHIP_GROUP_PREFIX helpers.
- loadExample: seedChipProgramGroups() routes each chip's programFile into its
  own group (seeded from the example files), sweeps stale chip groups, keeps
  the program OUT of the board group, and for a board-less chip example makes
  the chip group active so the program is the editable file shown.
- EditorToolbar.prepareCustomChips: resolves the program from the chip's own
  group (falls back to board files for older projects) before assembling ROM.
- FileExplorer: renders one collapsible section per programmable chip with an
  IC icon; clicking switches the editor to the chip group. Lazy-creates a
  group for chips dropped on the canvas.
- projectPayload + vlxFile: serialise chip groups alongside board groups and
  include them in the dirty-check hash, so chip-program edits persist on
  save / autosave / .vlx export and round-trip via replaceFileGroups on load.
- Regression tests for board-less + board+chip routing and stale-group sweep.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-03 22:07:56 +02:00
David Montero fe001d728c fix(examples): board-less chip example — Run enabled on load + editable program
Two UX bugs in the board-less "Z80 Larson Scanner (no board)" example:

- It loaded "running" (electrical sim defaults to paused=false), so Run was
  disabled and Stop enabled even though the chip hadn't started — the user had
  to Stop then Run. loadExample now starts a board-less example that contains a
  custom chip in the STOPPED state (paused=true) so Run is enabled; pure
  analog/digital circuits stay live.
- The chip's program wasn't editable: it shipped a pre-baked ROM and the
  board-less loader only setCode'd into an orphan file group (no-op → blank
  editor). The example now ships larson.s as a real file (programFile), and
  the board-less loader points the editor at the default group and loadFiles()
  the example's files, so the program shows on the left and is editable, like
  the board-backed examples. Run compiles it.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-03 21:30:42 +02:00