app
Adom TSCI - Tscircuit Board Viewer
Public Made by Adomby adom
Interactive tscircuit preview in a Hydrogen webview — first-class 3D / PCB / Schematic tabs, Components & Nets HUDs, an auto-glow x-ray view, Inspect, Measure, a Walkthrough Demo, and a one-click `exp
← Commit history
Publish 1.4.13
12 files changed
+2354−136
LICENSEadded+21@@ -0,0 +1,21 @@+MIT License++Copyright (c) 2026 Adom Inc.++Permission is hereby granted, free of charge, to any person obtaining a copy+of this software and associated documentation files (the "Software"), to deal+in the Software without restriction, including without limitation the rights+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell+copies of the Software, and to permit persons to whom the Software is+furnished to do so, subject to the following conditions:++The above copyright notice and this permission notice shall be included in all+copies or substantial portions of the Software.++THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE+SOFTWARE.
README.md+435−64@@ -1,75 +1,446 @@-# adom-tsci — Tscircuit Board Viewer--An Adom-Docker CLI that gives you **first-class 3D / PCB / Schematic-tabs** for a tscircuit project inside a Hydrogen webview, plus the-canonical "ship this molecule to the wiki" flow.--## What it does--- **3D tab** — full Babylon-based viewer (orbit, zoom, ortho/perspective)- with the tscircuit GLB loaded, ground shadow, IBL environment.-- **Components HUD** — every chip / passive / testpoint / machine pin- the GLB ships, grouped by kind. Click a row to hide that component- and see the traces underneath.-- **Nets HUD** — every routed pcb_trace lifts off the baked surface- as a live polygon mesh on top of an x-ray-transparent substrate. The- `✦` button auto-selects the biggest power/ground net the moment you- open the panel — that's the gorgeous GlowLayer x-ray shot the- walkthrough demos.-- **Walkthrough Demo** — narrated tour of the board with cinematic- camera flyovers. Drives by `walkthrough.json` you can hand-edit per- project. Auto-plays in static-export mode (wiki embed).-- **Inspect / Measure** — Fusion 360-style smart picking. Hover any- pad/hole/chip/silkscreen/board for an info card; pin two clicks for- a measurement.-- **Re-run autorouter** — one-click hot reload, regenerates traces- + GLB without restarting tsci dev.-- **export-wiki** — bundle the running viewer + source + plan.md and- publish to the Adom Wiki as a molecule page.--## Install--Auto-discover-friendly, single command:+# Tscircuit Board Viewer (`adom-tsci`) -```bash-curl -fsSL https://wiki-ufypy5dpx93o.adom.cloud/static/apps/adom-tsci/adom-tsci -o /tmp/adom-tsci \- && chmod +x /tmp/adom-tsci \- && sudo install -m 0755 /tmp/adom-tsci /usr/local/bin/adom-tsci \- && adom-tsci install+Interactive tscircuit preview inside a Hydrogen webview panel.+First-class **3D / PCB / Schematic** tabs rendered from the tscircuit+build output, plus:++- **🔍 Inspect tool** (key `I`) — hover any feature for a labelled info+ card: chip refdes + footprint + pin count, pad → net routing+ ("Connects to MC1.pin1, TP1.pin1 via 2 direct traces"), machine-+ contact role (mechanical-only vs wired), board stackup + baked-+ texture resolution, silkscreen text.+- **📏 Measure tool** (key `M`) — Fusion 360-style picking (pad / hole+ / chip / pin / edge), precision + mm/inches/mils.+- **🎬 Walkthrough Demo** — narrated tour generated deterministically from+ `circuit.json` by `adom-tsci walkthrough-gen` (connectors → ICs → grouped+ passives → nets x-ray), with 3D flyover animations. Connector edges are+ computed (not hardcoded), long ref lists are capped, and it **finishes on+ the ViewCube Home view with the Nets/x-ray panel turned off**. Don't+ hand-edit `walkthrough.json` — it auto-regenerates on `circuit.json` drift.+- **⟳ Re-run autorouter** — one click touches `lib/index.tsx` + spawns+ `bunx tsci build --glbs --svgs`.+- **`--texture-resolution N`** — re-bakes the board-surface texture+ (silkscreen + copper pads + annular rings + solder mask) up to+ 8192×8192 via `circuit-json-to-gltf`, cached by `circuit.json`+ content hash. Essential for readable silkscreen on big boards.+- **Stale-deps check** against `@tscircuit/cli` on startup.+- **Secondary tabs:** `tsci live` (raw RunFrame) and `3d compare`.++**Recent (1.4.x):**+- **Canonical Adom app-header** — one-row `brand · subject · actions` with a+ hover app-info popover (version / engine / wiki / GitHub), brand fonts+ (Familjen Grotesk / Satoshi / JetBrains Mono), Adom-teal accent.+- **Parts inspector lists EVERY component, grouped by kind** (`Resistors 92`,+ `LEDs 90`, …) with collapsible groups — passives are no longer hidden.+- **Ground plane is 65% transparent and self-heals** — the bottom layer /+ bottom silkscreen show through; a footer readout reports the live+ transparency so a regression to opaque is caught instantly.+- **Panel deep-linking** — `#3d` / `#pcb` / `#schematic` / `#parts` /+ `#tsci-live` URL hashes select a panel (shareable + AI-drivable).+- **`view home`** — CLI/HTTP preset for the ViewCube Home view.+- **TTY-gated CLI color** so `OK:`/`ERROR:`/`Hint:` never leak ANSI into pipes.++As of **v0.3.0** the 3D tab is fully **AI-drivable**: every toolbar+button has a CLI subcommand + HTTP endpoint, plus there's a `/eval`+channel for hot-patching the running JS and a `/console` forwarder so+Claude can read uncaught errors without clicking anything. **No Adom+Viewer service dependency** — the Adom 3D viewer library is bundled+into the binary.++As of **v0.4.1**, `adom-tsci start` does a pre-flight `/proc` sweep+for stale `bun tsci dev` processes and cleans them up before spawning,+plus holds an atomic PID lock at `/tmp/adom-tsci-<port>.pid`. No more+orphan tsci devs compounding across crashed restarts.++++## Credit to Ray++This app is an **onboarding layer over Ray's authoritative tscircuit+workflow guide** at+`~/.claude/skills/adom/guides/adom-tscircuit-skill.md`. Everything+it does with tscircuit — the `<Molecule>` component,+`MachineContactMedium` placement rules, the sizing grid, the DRC+notes, the `bunx tsci build --glbs --svgs` flags, the project+skeleton — traces back to Ray's guide. Go read it for connector+families, molecule generators, review servers, and DRC tooling.+`adom-tsci` just makes the "show me what I'm building" step trivial.++## The UI++**Primary tabs:**++- **3D (default).** Full-bleed canvas rendered by the Adom 3D+ Viewer (Babylon-based engine, bundled into the binary — no AV).+ Contact shadow + **65%-transparent ground plane** (the board's+ bottom layer / silkscreen show through; a footer readout reports+ the live transparency). The **ViewCube** (top-right) handles Home /+ Fit / view presets; a floating draggable toolbar over the canvas+ carries ▭ ground, ⎚ wireframe, axes, 📏 measure, inspect, and the+ walkthrough. Floating component panel lists every chip / passive /+ testpoint / contact from `circuit.json` — click a row to hide that+ component and see the traces routed under it.+- **PCB.** Inline `dist/lib/index/pcb.svg` in a panzoom container.+- **Schematic.** Inline `dist/lib/index/schematic.svg`, same panzoom.+- **Parts.** Inspector listing **every** component grouped by kind+ (Connectors / LEDs / Resistors / Capacitors / Machine pins …) —+ click any to verify its footprint ↔ 3D-body alignment.++++++++**Secondary tab:**++- **tsci live.** The raw tscircuit RunFrame through a slingshot+ proxy. Escape hatch for features upstream tscircuit ships that+ we haven't wrapped yet. Dimmed 0.78 because RunFrame is+ hardcoded-light-theme.++## The Re-run autorouter button++Top-right of the header, right next to the GLB timestamp pill.++++Click it to:++1. Touch `lib/index.tsx` via the slingshot → `tsci dev` file server.+ Fires a `FILE_UPDATED` event, RunFrame's in-memory view refreshes.+2. Spawn `bunx tsci build lib/index.tsx --glbs --svgs` in the+ background. Regenerates the on-disk `3d.glb`, `pcb.svg`, and+ `schematic.svg`, which the first-class tabs poll for (`/glb/meta`+ every 3 s). Within ~30 s the 3D tab re-renders with the new+ layout and the PCB + Schematic tabs re-fetch on next click.+3. **Shift-click for a clean re-run** — also deletes+ `manual-edits.json` first so any interactive trace edits made in+ RunFrame's PCB editor are discarded.++Same from the CLI: `adom-tsci rerun` or `adom-tsci rerun --clean`.++## Stale-deps check++`adom-tsci start` fires a background query against+`https://registry.npmjs.org/@tscircuit/cli/latest` on launch and+compares to the version in+`<project>/node_modules/@tscircuit/cli/package.json`. If you're+behind, you get one `HINT:` line:++```+HINT: @tscircuit/cli is out of date (installed 0.1.1226, latest 0.1.1234).+Run `adom-tsci upgrade` to refresh. ``` -`adom-tsci install` deploys SKILL.md + the `adom-usbc`,-`demo-recording`, and `export-wiki` guides to `~/.claude/skills/`.+Non-fatal, 3-second timeout, never blocks startup. tscircuit does+daily releases; this nudges you when you drift behind.++`adom-tsci upgrade [project-dir]` shells out to `bun update --latest`+in the target project. ## Quick start ```bash-# Boot the preview against any tscircuit project (lib/index.tsx convention):-cd ~/project/adom-tsci-projects/My-Board-adom-tsci start . --port 8861 --tsci-port 3061 --texture-resolution 4096--# Drive the running viewer programmatically:-adom-tsci view top-adom-tsci net --on # x-ray view, auto-glow on biggest net-adom-tsci walkthrough start # narrated tour-adom-tsci toggle-component U1 --hide # see traces under the chip--# Publish to the wiki as a molecule:-adom-tsci export-wiki . \- --slug my-board \- --page-type molecules \- --label "Live 3D viewer"+# Install (one-time)+curl -fsSL https://wiki-ufypy5dpx93o.adom.cloud/static/apps/adom-tsci/adom-tsci \+ -o /usr/local/bin/adom-tsci && \+ chmod +x /usr/local/bin/adom-tsci && \+ adom-tsci install++# From inside a tscircuit project directory:+adom-tsci start .++# Edit lib/index.tsx, click ⟳ Re-run autorouter in the header,+# watch the 3D / PCB / Schematic tabs re-render.++# When done:+adom-tsci stop+```++If you don't have a tscircuit project yet, see the `adom-tscircuit`+skill for the 4-file skeleton.++## Demo video++A 4-minute feature tour of everything in 1.2.0 is published at+[`apps/adom-tsci` on the wiki](https://wiki-ufypy5dpx93o.adom.cloud/apps/adom-tsci)+— 34 scenes with TTS narration, recorded against the RP2040 dev-board+molecule.++The full demo pipeline (scene script, TTS generation, per-scene+recording, mux + concat) lives under [`demo/`](demo/) and is+reproducible — see [`demo/README.md`](demo/README.md). Run it against+any tscircuit project to re-cut the tour.++## Examples++Ready-to-build example molecules live under [`examples/`](examples/)+— clone, `bun install`, and `adom-tsci start <example-dir>` to preview.++### `examples/SN65HVD230-Molecule/`++Small reference molecule: a **Texas Instruments SN65HVD230 CAN transceiver**+in SOIC-8, wrapped in a 16×16 mm molecule with power + CAN bus broken out+to six `MachineContactMedium` pins. Includes a 100 nF decoupling cap, the+120 Ω CAN bus termination resistor, and per-pin silkscreen labels.++++### `examples/BME680-Breakout-Molecule/`++Environmental-sensor breakout: a **Bosch BME680** four-in-one sensor+(temperature, humidity, barometric pressure, and a heated metal-oxide+gas-resistance plate that tracks VOC concentration) in a 3×3 mm LGA-8+package, wrapped in a 24×16 mm molecule. Power + I²C bus + the two+SPI-only pins (SDO, CSB) are broken out on six `MachineContactMedium`+pins. Internally: 100 nF decoupling on each supply rail (VDD and VDDIO),+10 kΩ pull-ups on SDA and SCL, and a third 10 kΩ from CSB→VDDIO so the+chip boots into I²C mode (address `0x77` by default; tie SDO low at the+host to switch to `0x76` and run two BME680s on one bus). One `<testpoint>`+per broken-out signal for probing-workcell access, and a bottom-silk ID+card (part number, function, I²C address, supply range, revision).++++### `examples/BHI360-Breakout-Molecule/`++Motion-sensor breakout: a **Bosch BHI360** 6-axis IMU (accelerometer ++gyroscope) with a built-in Fuser2 DSP core that runs orientation-fusion+firmware on-chip, so the host MCU can stay asleep while the sensor+tracks motion. LGA-20 (3×2.5 mm) package, wrapped in a 24×24 mm+molecule. Eight `MachineContactMedium` pins break out the SPI/I²C+interface (CSB / SCK / SDI / SDO, left edge) and power + interrupt+(INT / VDD / VDDIO / GND, right edge) — tie CSB high at the host to+select I²C, leave it driven for SPI. Internally: 100 nF decoupling on+each supply rail (VDD core + VDDIO I/O), silkscreen section headers+(`SPI/I2C` / `PWR`), a bottom-silk ID card, and one 0.8 mm+`<testpoint>` per broken-out signal for probing-workcell access.++Demonstrates an LGA-20 part on a footprinter that only ships+`qfn`/`son`/`dfn`/`bga` primitives — `qfn20_w4mm_h4mm_p0.5mm` is used+as the closest spatial match (QFN pads sit where the LGA pads land+on the bottom of the package).++++++### `examples/iCE40HX1K-VQ100-Molecule/`++Big FPGA breakout: a **Lattice iCE40HX1K** in a 14×14 mm TQFP-100 (VQ100)+package, wrapped in a 56×56 mm molecule that breaks out **all 100 chip+pins** to `MachineContactMedium` contacts on the matching edge. Each pin+also gets a dedicated `<testpoint>` for probing-workcell access.++Silkscreen sections group the pins functionally:+`SPI` (programming), `PROG` (CRESET/CDONE), `IO BANK 0/1/2/3`, plus a+`NO USB / NO UART / NO SWD` reminder — the iCE40 is an FPGA, not an MCU,+so there's no debug port in the ARM sense; you program it through the+SPI pins (pins 67–71) via an iCELink/FT232 adapter and boot from a+downstream SPI flash.++++++### `examples/iCE40HX1K-USB-Molecule/`++Same iCE40HX1K FPGA, now with **USB-C programming built in** — an+"easy to use FPGA dev board." Plug a USB-C cable from your host into+the board and load a bitstream with `iceprog` or `fujprog`. No external+programmer, no JTAG dongle, no manual pin wiring.++Onboard stack:+- USB-C receptacle with ESD protection (USBLC6-2P6)+- **FT2232H** — dual-channel USB→SPI/UART bridge. Channel A in MPSSE+ mode talks to the iCE40's SPI programming pins (`iceprog`-compatible).+ Channel B is a USB-UART console you can attach to a soft-UART running+ inside your FPGA bitstream.+- **W25Q16JV** 2 MB SPI boot flash — the FT2232H writes your bitstream+ here, and the iCE40 reads from it on power-up so the board boots+ standalone without a host.+- **AP7333-33** (3.3 V) and **AP2112-1V2** (1.2 V) LDOs from USB 5 V+ for FPGA VCCIO + VCC core rails+- 12 MHz crystal for the FT2232H, 93LC56B EEPROM for its USB VID/PID+- 32 GPIO + 3 power contacts broken out on the molecule edge+- **Programming pass-through** — 6 edge contacts on the left side+ (`SCK / SS / SO / SI / CRESET / CDONE`, marked with a `→ VQ100 PROG`+ silkscreen callout) that carry the FT2232H's SPI bus out to mate with+ a downstream `iCE40HX1K-VQ100-Molecule`. Dock that molecule here and a+ single `iceprog` invocation reaches the bare FPGA through this board's+ onboard USB bridge.+- **16 test points** on the critical nets (VBUS, 3V3, 1V2, GND,+ USB D+/- on both sides of the ESD chip, the SPI programming bus,+ CRESET/CDONE, UART TX/RX) as SMT copper pads for a probing workcell's+ pogo pins — `footprintVariant="pad"`, 1 mm diameter, top layer++++++## Architecture++```++---------------------------+ +---------------------------++| Hydrogen webview tab | | adom-tsci (Rust) |+| /proxy/8850/ |<------>| tiny_http on 8850 |+| | | |+| +----------+ | | / shell.html+| | 3D |<------------+------->| /glb file stream+| +----------+ | | /pcb.svg file stream+| | PCB | | | /schematic.svg file stream+| +----------+ | | |+| | Schematic| | | /runframe/ slingshot+| +----------+ | | /runframe/api/* (proxy)+| | | /runframe/standalone.min.js+| +----------+ | | |+| | tsci live|<------------+------->| /runframe/… slingshot+| +----------+ | | |+| | | /basic-3d.html (legacy)+| | | /js/adom-3d-viewer.min.js+| | | /js/environmentSpecular.env+| | | |+| | | /console /eval /eval/* (AI bridges)+| | | /api/set-view /api/set-camera+| | | /api/toggle /api/toggle-component+| | | /api/components /api/tour+| | | /rerun /reload /shutdown /health+| ⟳ Re-run autorouter -----+------->| |++---------------------------+ +---------------------------++ |+ v+ +--------------------++ | bun tsci dev |+ | -p 3040 |+ | (process group) |+ +--------------------++```++### The slingshot fix (why this app exists)++`tsci dev` emits a hardcoded absolute URL in its HTML shell:++```js+window.TSCIRCUIT_FILESERVER_API_BASE_URL = "http://127.0.0.1:3040/api";+```++Inside a Hydrogen webview iframe, `127.0.0.1` is the user's machine,+not the container — so every fetch fails and RunFrame hangs at+`loading...` forever. `adom-tsci`'s slingshot rewrites that line (and+the absolute `/standalone.min.js` script tag) to **relative paths**,+then transparently proxies `/runframe/api/*` to tsci dev on port 3040.++**v0.2 note:** the v0.1 proxy handler had a bug where route-match+query-string stripping also dropped the query string on forward,+breaking `files/get?file_path=...`. v0.2 ships a full method/body/+query-aware proxy handler. If you're upgrading from v0.1, this is+why your tabs suddenly work.++tsci dev uses HTTP polling (`/api/events/list?since=<id>`), no+WebSockets, so the slingshot is a plain HTTP forwarder.++## CLI commands++See [`SKILL.md`](SKILL.md) for the full reference. Summary:++| Command | Purpose |+|---|---|+| `start <dir>` | Spawn tsci dev, start slingshot, open webview tab |+| `stop` | Clean shutdown (kills whole tsci process group) |+| `status [--json]` | Query running instance state |+| `open` | Re-add or navigate webview tab |+| `reload` | Force first-class tabs to re-poll build outputs |+| `rerun [--clean] [--fast]` | Re-run the autorouter (touches .tsx + spawns build) |+| `upgrade [dir]` | `bun update --latest` in target project |+| `health` | Probe tsci_reachable |+| `install` | Drop `SKILL.md` into `~/.claude/skills/adom-tsci/` |+| **AI-drive the 3D viewer** | |+| `view <preset>` | home (ViewCube Home)/front/back/left/right/top/bottom/iso |+| `camera [--alpha A] [--beta B] [--radius R]` | Raw camera position |+| `tour start \| stop` | Cinematic slow-orbit |+| `toggle <flag>` | Flip ground/wireframe/axes |+| `toggle-component <NAME> [--hide \| --show]` | Show/hide a component (e.g. `U1` to see traces) |+| `list-chips` | List components the UI discovered in the current GLB |+| `eval "<js>"` | Run JS inside the webview, print the result |+| `console [--follow]` | Tail the forwarded JS console (logs + uncaught errors) |++## HTTP API++Every state-mutating CLI command is a thin wrapper over an HTTP+endpoint. AI agents and shell scripts can `curl` them directly:++```bash+curl http://127.0.0.1:8850/health+curl http://127.0.0.1:8850/glb/meta+curl -X POST http://127.0.0.1:8850/rerun+curl -X POST "http://127.0.0.1:8850/rerun?clean=1"+curl -X POST http://127.0.0.1:8850/shutdown+```++See [`SKILL.md`](SKILL.md) for the full reference.++## Development++```bash+git clone https://github.com/adom-inc/adom-tsci+cd adom-tsci+cargo build --release+cp target/release/adom-tsci ~/.local/bin/+adom-tsci install+```++Rust 1.70+, standard `cargo` toolchain. Unit tests cover the+slingshot HTML rewrite (the load-bearing regex):++```bash+cargo test ``` -`export-wiki` bundles the static viewer + a tarball of the project-source (lib/, package.json, walkthrough.json, **plan.md**) + uploads-the plan.md standalone so a future agent picking up your molecule-sees both the brief and the source in one place.+## Known v0.3 limitations++- **Component mapping coverage is partial.** The UI groups GLB+ meshes by nearest pcb_component centroid; some multi-mesh+ components (especially SOIC/SOT bodies with many primitives) can+ end up split across neighbouring names or fall into the `Board`+ catch-all. Kind-aware tolerances (chip=10mm, testpoint=1.2mm,+ contact=2mm, …) get most of the obvious ones right; edge cases+ tracked for v0.4.+- **The tsci live tab is dimmed** (`filter: brightness(0.78)`)+ because tscircuit's RunFrame is hardcoded light-theme. When they+ ship a dark mode, the filter comes out.+- **Re-run autorouter rebuilds the whole circuit, not just the+ autorouter.** tscircuit doesn't expose a standalone "run just the+ autorouter" entry point — the only way to refresh route layouts+ is to re-evaluate the circuit, which runs the whole pipeline. On+ a single-chip molecule this takes ~30 seconds.+- **GLB is bundled into the binary** (the `adom-3d-viewer.min.js`+ library is 13 MB, `environmentSpecular.env` is ~270 KB), pushing+ the release binary to ~18 MB. Trade-off: self-contained, zero+ runtime dependencies, offline-friendly.++## Design document++The original design plan lives at+[`docs/design/valiant-herding-dove.md`](docs/design/valiant-herding-dove.md),+preserved from the v0.1 planning session. Every non-obvious choice+(Rust over Bun, three.js in the generic pane, port 8850, the+"don't touch Ray's guide" boundary, why the slingshot uses a regex+rewrite) is documented there. v0.2 adds the first-class tabs,+rerun button, upgrade check, and the renamed binary on top of that+foundation — see the commit history for the v0.1 → v0.2 evolution.++## Acknowledgements++- **Ray** — for the tscircuit workflow guide this app exists to+ support.+- **tscircuit** — for the DSL and the RunFrame bundle.+- **The Adom platform team** — for the webview panel API,+ Basic3dView, `serve_glb`, the shotlog service, the brand guide+ (icons must be monochrome `#e6edf3` — this app's favicon+ follows that rule), and the `hydrogen-tab-icons` skill that+ documents how tab icons actually work. -## See also+## License -- `adom-tsci/guides/adom-usbc.md` — USB-C placement / rotation / lint-- `adom-tsci/guides/demo-recording.md` — making a 30-second narrated- demo video of a board-- `adom-tsci/guides/export-wiki.md` — the molecule publish flow- (hero image, plan.md, body completeness rules)+MIT. See [LICENSE](LICENSE).
SKILL.md+893−41@@ -1,30 +1,29 @@ --- name: adom-tsci description: >- Interactive tscircuit preview inside a Hydrogen webview panel- ("Tscircuit Board Viewer"). First-class 3D / PCB / Schematic tabs- rendered from the tscircuit build output, plus: an **Inspect tool**- (hover any chip / pad / hole / via / silkscreen / board and get a- labelled info card with refdes, footprint, net routing, stackup —- all pulled from circuit.json), a **Measure tool** (Fusion 360-style- smart picking + precision + mm/inches/mils), a **Walkthrough Demo**- (guided 10-step tour with 3D flyover animations across the contact- ring + testpoints), a one-click **Re-run autorouter**, a background- **stale-deps check**, and a **--texture-resolution N** flag that- re-bakes the board-surface texture up to 8192x8192 so silkscreen- stays readable at zoom. The 3D viewer is fully AI-drivable: every- toolbar action has a CLI subcommand + HTTP endpoint + JS eval- channel + console forwarder. Use when the user asks to preview a- tscircuit project, inspect board features, measure pin-to-pin- distances, start a walkthrough demo, re-run the autorouter, bump- the silkscreen resolution, or upgrade tscircuit packages. Trigger- words: tscircuit preview, adom-tsci, tsci dev webview, preview- tscircuit, interactive tscircuit viewer, inspect board, hover chip- info, what is this chip, pcb inspect tool, measure pcb distance,- measure between pins, tscircuit hot reload, rerun autorouter, re-run- autorouter, walkthrough demo, board walkthrough, sharper silkscreen,- bump texture resolution, tscircuit 8192, upgrade tscircuit, stale- tscircuit deps, hide chip see traces, AI-driven 3D pcb viewer.+ Trigger words: tscircuit preview, adom-tsci, tsci dev webview, preview+ tscircuit, interactive tscircuit viewer, inspect board, hover chip info, what+ is this chip, pcb inspect tool, measure pcb distance, measure between pins,+ tscircuit hot reload, rerun autorouter, re-run autorouter, walkthrough demo,+ board walkthrough, sharper silkscreen, bump texture resolution, tscircuit+ 8192, upgrade tscircuit, stale tscircuit deps, hide chip see traces,+ AI-driven 3D pcb viewer, usb-c, type-c, UsbCReceptacle, place usb-c, rotate+ usb-c. Interactive tscircuit preview inside a Hydrogen webview panel+ ("Tscircuit Board Viewer"). First-class 3D / PCB / Schematic tabs rendered+ from the tscircuit build output, plus: an Inspect tool (hover any chip /+ pad / hole / via / silkscreen / board and get a labelled info card with+ refdes, footprint, net routing, stackup — all from circuit.json), a Measure+ tool (Fusion 360-style smart picking, mm/inches/mils), a Walkthrough Demo+ (guided 10-step 3D flyover tour), a one-click Re-run autorouter, a+ background stale-deps check, and a --texture-resolution N flag that re-bakes+ board-surface texture up to 8192x8192 for readable silkscreen at zoom. The+ 3D viewer is fully AI-drivable: every toolbar action has a CLI subcommand,+ HTTP endpoint, JS eval channel, and console forwarder. Use when the user+ asks to preview a tscircuit project, inspect board features, measure+ pin-to-pin distances, start a walkthrough demo, re-run the autorouter, bump+ silkscreen resolution, or upgrade tscircuit packages. Also routes the+ `adom-usbc` child guide (`adom-tsci/guides/adom-usbc.md`) for USB-C+ placement, rotation math, and the canonical `UsbCReceptacle` wrapper. --- # adom-tsci@@ -36,6 +35,52 @@ npm registry, and a secondary **tsci live** tab (the raw tscircuit RunFrame, served through a slingshot proxy) for features the primary UI hasn't wrapped. +## 🛡 INVARIANTS — audit BEFORE every change to nearby code++These are user-confirmed behaviours that have already regressed at least+once. Before editing the listed files (or any function with the listed+keywords in its name) read this list and grep the relevant code path.+If your change would alter any of these, **ask the user first** —+don't silently re-trade them for "improvements".++| # | Invariant | What it protects | Where the rule lives in the code |+|---|---|---|---|+| 1 | **Right-click hide ghosts at 20% alpha — never `setEnabled(false)`** for any mesh kind, including `InstancedMesh`. The picker must still be able to hit the ghost so a second right-click can unhide. **Hide auto-promotes** the component (clone source mesh + clone material) so the alpha write hits ONLY this instance, not its siblings. | The user's only "show me what's under this part" workflow. If we full-disable, they cannot ever bring the part back without a CLI command — UX dead-end. Without promotion, hiding R1 also ghosts R2/R3 because tscircuit shares one material across InstancedMesh siblings. | `setComponentVisibility()` in `src/assets/shell.html` calls `_promoteComponent(name)` BEFORE the alpha mutation. The "other" branch always goes through `material.alpha = 0.20` on the (now-promoted) standalone mesh. Per-instance isolation also belongs in `_makeFloatingMarker` (HIGHLIGHT path), but via overlay-only architecture — not promotion. |+| 2 | **Highlight = real Babylon `HighlightLayer` glow on the actual chip mesh.** No bbox tube outlines, no synthetic cone+tail arrows, no emissive material tints, no fake floating squares. ONE consistent visual language for "this is selected": a teal halo around the chip itself. Auto-promote first so the addMesh hits only the target component (not its InstancedMesh siblings). | Half-assed approaches (rectangles around bounding boxes, floating arrows that point at things, tinting the material) all read as different "selected" states and confused users. The user explicitly retired those: glow is the only highlight now. | `_getHighlightLayer()` returns the singleton (strong-blur outer-glow). `highlightComponents()` in `shell.html` adds the target's meshes via `hl.addMesh(mesh, teal)` after `_promoteComponent(name)`. `drawFeatureOutline()` for `feature.kind === 'chip'` routes to the same glow path; only non-chip features (pads, holes, vias, board-edges) still get the tube outline because they have no mesh to glow. |+| 3 | **Highlight material uses real PBR via stolen-constructor.** `new B.PBRMaterial()` throws because the viewer ships a minified BABYLON. Find the class via `scene.materials.find(m => 'albedoColor' in m && typeof m.metallic === 'number').constructor`. | Phong/StandardMaterial reads as a flat-shaded cartoon eyeball under the scene's 52,845-watt physical spotlight. Real PBR matches the board's shader family and looks like a glossy teal orb. | `_makePbrTealMat()` in `src/assets/shell.html`. Always exclude lights with `intensity > 100` from the material so the physical spotlight doesn't clamp the channel to white. |+| 4 | **Walkthrough JSON must match the live project.** Mismatched `_meta.project_name` triggers a loud banner; `walkthrough-gen` is auto-run on `start` if the file is stale. | Copy-paste-from-another-board class of bug. The user has been burned by this exact thing. | `walkthrough-gen` CLI + `auto_regen_if_stale()` called in `cli/start.rs`; banner check in `src/assets/shell.html`. |+| 5 | **Every toolbar/HUD action has a CLI subcommand.** No "open the preview and click X" instructions. | Ralph-loop testability + AI drivability + reproducibility. CLI-FIRST is the whole point of this app. | See the CLI table below in this skill. New UI affordance? Add a CLI subcommand at the same time. |+| 6 | **Babylon classes added to `Adom3DViewer.BABYLON` belong in our vendored `vendor/3d-viewer/standalone.ts`, not in shell.html workarounds.** Every "is not a constructor" we hit (Quaternion, TransformNode, PBRMaterial, HighlightLayer, GlowLayer, SceneLoader) traced to the upstream `adom-inc/3d-viewer` only exposing 5 Babylon classes. Workaround = silent breakage; right answer = export the class from our vendored bundle and rebuild. | If you see a workaround that says "the bundle doesn't export X, so we manually …", flag it as tech debt and add X to the vendored standalone.ts instead. | `vendor/3d-viewer/standalone.ts` is our fork of Colby's upstream. Add the import + the export under `window.Adom3DViewer.BABYLON.X`, then `bash vendor/build-viewer.sh` to rebuild `src/assets/js/adom-3d-viewer.min.js`, then `cargo build --release`. Upstream stays untouched. |+| 7 | **Two render modes: instanced (default, fast) and per-chip (on first per-component mutation).** Default rendering keeps tscircuit's `InstancedMesh` packing — one source mesh + one material per package family — so the board is fast even with hundreds of passives. Any operation that needs per-component isolation (hide, future colour-override, future per-component wireframe) PROMOTES that single instance to a standalone Mesh + cloned material. Highlight uses overlay-only (no promotion). | Lets the user manipulate one component without paying instancing-cost-restoration on the other 99. Promotion is sticky for the session — once R1 is standalone, it stays standalone. | `_promoteComponent(name)` in `src/assets/shell.html`. Auto-runs from `setComponentVisibility()` on hide. Manual: `adom-tsci toggle-component R1 --promote`. `meta.promoted = true` flag on componentMap entry. |+| 8 | **Trace polygons use earcut + offset polyline + half-disc endcaps + miter joins. EXACT widths from circuit.json — no `Math.max(0.15, …)` floor, no `× 1.05` oversize. Same rule for vias and pcb_plated_holes: `via_diameter`, `outer_diameter`, `hole_diameter` are taken straight from circuit.json.** | EDA traces are flat copper polygons, not 3D pipes — must look like KiCad/Altium/Eagle output. Floors and oversize multipliers visually lie about real PCB widths and break trace-to-pad-edge alignment. The user has explicitly called out "be exact … don't be lazy" multiple times. | `_traceBuildOutline()` + `_traceBuildPolygonMesh()` in `src/assets/shell.html` use `window.Adom3DViewer.earcut`. `_traceRenderNet` consumes `seg.width` and `seg.via_diameter` raw. `_traceRenderPlatedHoles` consumes `outer_diameter` and `hole_diameter` raw. Earcut is bundled in `vendor/3d-viewer/standalone.ts` (rebuild via `bash vendor/build-viewer.sh`). |+| 9 | **Layer Z is derived from the actual board substrate mesh (`Box0_primitive[012]` bounding box), NOT `window._boardBottomZ`.** Top-layer traces draw at `top + 0.05`, bottom-layer at `bottom - 0.05`, vias and plated-through-holes are full-thickness cylinders spanning `top → bottom` rotated onto the Z axis. EDA convention: top = red, bottom = blue. | `window._boardBottomZ` is wrongly mirrored to `+topZ` elsewhere in the app — trusting it caused EVERY bottom-layer trace to render on top, EVERY via to have zero height (invisible discs). Discovered when the user pointed out "blue traces on top? you're missing the whole point." | `_traceComputeBoardZ()` + `_traceLayerZ()` in `src/assets/shell.html`. Cached on enable, invalidated on disable (`_traceBoardZ = null` in `_traceDisposeAll`). |+| 10 | **Trace mode is heavy-on-demand: meshes only exist while the Nets panel is open.** Closing the panel calls `_traceDisposeAll()` which dispose every wire polygon, via cylinder, plated-hole barrel, AND every shared / per-net material. Board x-ray is also restored on close (`_disableBoardXray` re-applies the captured original alpha + transparencyMode + backFaceCulling per substrate mesh). | Nets HUD on a 200-net board would otherwise leave thousands of mesh+material objects in memory after the user clicks ×. Same architectural rule as instanced-vs-promoted: pay the heavy cost only when the user is actively in that mode. | `enableTraceMode()` / `disableTraceMode()` + `_traceDisposeAll()` + `_disableBoardXray()` in `src/assets/shell.html`. |++If you're about to ship a change that touches highlight, hide,+visibility, walkthrough, or marker material — re-read the row above and+confirm it still holds. If you intentionally trade an invariant, **call+it out in the user-facing message** so the user can veto.++## Guides (child content routed by this skill)++`adom-tsci` is a hub for its own workflow. Child guides live as+`adom-tsci/guides/<name>.md` in the repo and deploy to+`~/.claude/skills/adom-tsci/guides/<name>.md` via `adom-tsci install`+— mirroring the `adom/guides/<name>.md` pattern Ray uses for his+cross-tool skills. This skill's description carries the triggers;+when one matches, load the referenced guide.++| Guide | Load when the user says | Path |+|---|---|---|+| **adom-usbc** — placement, rotation, lint for USB-C receptacles on an Adom tscircuit molecule; covers `TYPE-C-31-M-12`, per-edge rotation math, the `UsbCReceptacle` wrapper, and the "no SMT pad past the board edge" rule | "usb-c", "type-c", "smd usb c", "UsbCReceptacle", "place usb-c", "rotate usb-c", "usb-c east edge", "usb-c west edge" | `~/.claude/skills/adom-tsci/guides/adom-usbc.md` |+| **demo-recording** — making a 30-second demo video of an adom-tsci board: 30/70 workspace split, `-s small` captions with pre-flight wrap test, mandatory x-ray shot (hide a subset of LEDs/chips), Andrew Neural TTS narration, ffmpeg audio-mix recipe | "demo video", "record demo", "30-second demo", "make a demo of this board", "x-ray shot", "show off the 3D viewer", "demo for the wiki" | `~/.claude/skills/adom-tsci/guides/demo-recording.md` |+| **export-wiki** — publishing a tscircuit project to the Adom Wiki as a molecule with the full interactive 3D viewer (auto-play walkthrough on load, Components / Nets HUDs, Inspect, Measure), the source bundle (lib/, package.json, walkthrough.json, plan.md), and a comprehensive AI-handoff body. Required ingredients: hero image, plan.md, body markdown that captures every design decision so a future agent can pick up | "publish to wiki", "export to wiki", "wiki page for this board", "share the molecule", "make this a molecule on the wiki", "publish molecule", "ship it to the wiki" | `~/.claude/skills/adom-tsci/guides/export-wiki.md` |++No standalone `adom-usbc` skill exists — USB-C placement is meaningless+outside adom-tsci's workflow, so it lives under this parent, not as a+sibling. If older installs left `~/.claude/skills/adom-usbc/` behind,+`adom-tsci install` 1.3.4+ removes it automatically.+ ## ⚡ CLI-FIRST — non-negotiable If an `adom-tsci` preview is running (or you're about to start one),@@ -63,7 +108,7 @@ Quick-reference table — the subcommand you want is probably here: | Stop the preview (clean: kills tsci dev group + releases PID lock) | `adom-tsci stop [--port <N>]` | | Re-run the autorouter on the running project | `adom-tsci rerun [--clean] [--fast]` | | Kick the 3D / PCB / Schematic tabs to re-poll the build | `adom-tsci reload [--port <N>]` |-| Move the 3D camera | `adom-tsci view <front\|back\|left\|right\|top\|bottom\|iso>` or `adom-tsci camera --alpha <A> --beta <B> --radius <R>` |+| Move the 3D camera | `adom-tsci view <home\|front\|back\|left\|right\|top\|bottom\|iso>` (home = ViewCube Home) or `adom-tsci camera --alpha <A> --beta <B> --radius <R>` | | Cinematic orbit | `adom-tsci tour start \| stop` | | Run the guided walkthrough | `adom-tsci walkthrough <start\|next\|prev\|pause\|resume\|close\|status>` | | Flip a toolbar flag | `adom-tsci toggle <ground\|wireframe\|axes>` |@@ -77,6 +122,404 @@ Quick-reference table — the subcommand you want is probably here: Every write-mutation targeting the preview has `--port <N>` if you need to target a specific instance; defaults to 8850. +## 🚨 Iframe is opaque-origin — CORS is mandatory on every route++The Hydrogen webview mounts shell.html inside an iframe whose origin is+**opaque / null** even when the URL is on your own `*.adom.cloud`+subdomain. That means every `fetch()` from shell.html back to the+slingshot is **cross-origin** from the browser's perspective. Without+`Access-Control-Allow-Origin: *` on the slingshot response, the fetch+promise rejects with a CORS error — which shell.html's old `catch {}`+handlers silently ate. Symptom: 3D tab stuck on "Loading Adom 3D+viewer…" forever, `/eval/pending` never consumed, `/console?since=0`+empty. Cost an hour to diagnose once; no more.++**Enforcement already shipped:**++- `cors!()` and `no_cache!()` macros in `src/server/mod.rs` both emit+ `Access-Control-Allow-Origin: *` plus Methods + Headers.+- Top-of-`route()` OPTIONS preflight handler returns 204 with CORS.+- Every existing `request.respond(...)` call is wrapped in one of+ those two macros.+- `tests/cors_enforcement.rs` greps the source and **fails the build**+ if you add a bare `request.respond(Response::...)` without the+ wrapper. Run with `cargo test --test cors_enforcement`.+- `shell.html` wraps fetch calls in `bridgeFetch()`; any failure paints+ a red sticky banner at the top naming the URL + error + detected+ origin. If CORS regresses, you see it in 1 second.++**Rules for future edits:**++1. New HTTP route on the slingshot: use `cors!(resp)` or+ `no_cache!(resp)`. Never call `request.respond` on a bare Response.+2. New fetch in shell.html: use `bridgeFetch(url, opts)`, not plain+ `fetch`. Failures will surface in the banner automatically.+3. If the banner appears in the tab, **read it first**. It names the+ offending URL. Don't guess or reset — look at the URL, check its+ route for `cors!`/`no_cache!` wrapping, add whichever fits, rebuild.++## 🌩 Cloudflare caching — read `cloudflare-networking.md` BEFORE touching the server++All Adom container traffic routes through Cloudflare, which caches+assets at the edge for up to an hour regardless of what the browser+thinks it's doing. When you edit `shell.html`, the embedded viewer+bundle, or any static asset, **the webview may still serve the+previous version** unless the response carries the Cloudflare-specific+anti-cache triple:++```+Cache-Control: no-store, no-cache, must-revalidate, proxy-revalidate+Pragma: no-cache+Surrogate-Control: no-store ← the header Cloudflare actually obeys+```++These three are set by the `no_cache!` macro in `src/server/mod.rs`.+Every static-asset response goes through it. When you add a new+asset route, use the macro — do **not** hand-roll a `Cache-Control`+header.++Defense-in-depth: `adom-tsci start` / `adom-tsci open` both append a+`?_cb={ms-since-epoch}` timestamp to the webview URL on every tab+creation. A URL Cloudflare has never seen before cannot hit a cached+entry — this is the belt to the headers' suspenders.++**Full rule:** `~/.claude/skills/adom/guides/cloudflare-networking.md`.+Read it before writing any HTTP response on this binary. Skipping it+previously cost ~2 hours of "why is the bundle not updating" thrash.++## 👁 Always show your latest work — non-negotiable++If you're working on anything `adom-tsci` is designed to visualize — a+tscircuit molecule, a PCB change, a walkthrough edit, a feature of the+viewer itself — **the user must be looking at it live in the webview+before you claim progress.** The user is human; they verify with their+eyes, not by reading a diff.++**The reflex** after any of these changes:++1. Ensure an `adom-tsci` preview is running on the project you just+ touched (`adom-tsci status --json` → check `project_dir` matches,+ otherwise `adom-tsci stop && adom-tsci start <dir>`).+2. Ensure the `adom-tsci preview` tab (or the named instance) is+ activated in a non-VS-Code pane: `adom-cli hydrogen workspace+ active-tab --panel-id <P> --tab-id <T>`.+3. Ensure the webview is pointed at the slingshot URL for *this*+ instance's port — don't assume it didn't drift; `adom-cli hydrogen+ webview navigate --tab-id <T> "$HOST/proxy/<port>/"`.+4. Confirm the 3D scene is actually rendered (not stuck on "Loading+ Adom 3D viewer…"): `adom-tsci list-chips` must succeed, and+ `adom-cli hydrogen screenshot panel --name "adom-tsci preview"`+ should show the board — then **Read the screenshot** to verify with+ your own eyes before telling the user it's working.+5. Only then report the change is done.++**Applies to skill-structure changes too.** When you change how+`adom-tsci install` deploys skills, when you bump the binary, when you+edit SKILL.md — don't stop at "the file is on disk." Run the thing+against a real project and show the user the webview. Filesystem checks+(`ls ~/.claude/skills/...`) are not proof the feature works; the+webview rendering the board is.++**Pitfalls that mean you're not actually showing it:**++- Webview drifted to `https://adom.inc` or a cached page — always+ screenshot-verify the URL bar shows `…/proxy/<port>/`.+- Tab still says "Loading Adom 3D viewer…" — the GLB hasn't been+ produced yet. Run `bunx tsci build lib/index.tsx --glbs --svgs+ --3d-png --pcb-png` in the project dir and `adom-tsci reload`.+- The tab is inactive / buried behind another tab in the same pane —+ `active-tab` brings it forward.+- A second `adom-tsci` is running on another port and the webview is+ pointed at the wrong one — `adom-tsci status --port <N>` each+ instance you started.++Don't ask the user "want me to show it?" — just show it. The user+told you once; don't make them ask again.++## 🎯 Walkthrough highlight rules — DETERMINISTIC, don't "improve" casually++The `highlightComponents(names)` function in `src/assets/shell.html` paints+the "this is what the walkthrough step is pointing at" affordance. It has+**four design rules** baked in as comments. If you change any of them,+update THIS section in the same PR so the next Claude doesn't re-break+the same things.++### The four rules++1. **Colour is Adom TEAL — `rgb(0.35, 0.75, 0.70)` — NEVER amber / yellow.**+ Amber is reserved for the Inspect tool ("what you're probing"). Teal is+ the Walkthrough tool ("what we're pointing at"). Mixing the two destroys+ the colour language of the app. There is no "more visible" colour —+ keep it teal, keep it consistent with the teal app accent.++2. **Always clear everything before applying a new highlight.** Prior+ material clones, HighlightLayer meshes, AND synthesised testpoint discs+ all get disposed at the top of every `highlightComponents` call.+ Without this, walkthrough steps visually bleed into each other — the+ "capacitors" step ends up glowing the resistors that a previous step+ highlighted. If you add a new highlight-drawing mechanism (anything+ that creates geometry or mutates materials), also add its cleanup path+ to the dispose loop at the top of the function.++3. **Testpoints have no 3D mesh of their own.** Tscircuit renders every+ testpoint as a flat copper pad *baked into the board surface texture*;+ the "ghost cuboid" mesh that `enumerateComponents` creates above each+ testpoint is `setEnabled(false)` on load (the cuboid is visually+ noisy and hides the pad). So:+ - `HighlightLayer.addMesh(ghost)` paints nothing — the mesh is hidden.+ - `material.emissiveColor = ...` paints nothing — same reason.+ - The ONLY way to make a testpoint highlight visible is to **synthesise+ a small cyan disc** (1.6 mm × 0.05 mm cylinder, `TESTPOINT_CYAN`) at+ the testpoint's XY position on the board's top surface+ (`window._boardTopZ`) and track it in `_highlightMarkers` so the next+ call can dispose it.++ If a user says "the testpoints aren't highlighting any more", it's+ almost always because someone removed the disc-synthesis path or+ switched it to a material/mesh approach without realising testpoints+ don't have one. Restore it.++4. **Empty / missing `names` clears everything.** Nothing pins between+ steps. If you need a "pin this forever" primitive, build it as a+ separate call (e.g. the Inspect tool's pinned card) — don't leak it+ through `highlightComponents`.++5. **Classification is `source_component.ftype` — NOT regex-on-refdes.**+ `componentMap.kind` for every component is derived from tscircuit's+ emitted `source_component.ftype` (which reflects the JSX tag the+ board author wrote: `<capacitor/>` → `simple_capacitor` → kind+ `capacitor`). The old regex-based `classify(name)` (which guessed+ `capacitor` from anything starting with `C`) is kept as a FALLBACK+ for refdes prefixes tscircuit doesn't yet distinguish (testpoint /+ contact / mounting), and only those. Any new kind must be added to+ `_FTYPE_TO_KIND` in shell.html — NOT expressed as a regex.++ **Why it matters.** A board author who writes `<capacitor name="CB_FILTER">`+ used to get misclassified as a resistor because of `/^R[_\d]/`.+ Now the `ftype` says `simple_capacitor` and the kind is `capacitor`+ regardless of what the refdes looks like. Classification agrees+ with the JSX source by construction.++ **If you see a misclassification:** the fix lives in the board's+ lib/index.tsx (wrong JSX tag), not in shell.html. Only touch+ `_FTYPE_TO_KIND` if tscircuit ships a genuinely new ftype.++### Ralph loop = visual proof in shotlog (pup-driven). State JSON is NOT a ralph loop.++**The user has been burned by this twice.** A "JSON-level state test"+(eg querying mesh.material.alpha for every mesh after a hide) tells YOU+that the code works but it doesn't show the USER anything. The user+wants to see the actual rendered result, per-component, side-by-side+in shotlog so they can scroll the channel and confirm with their eyes.++**Every ralph loop in this skill MUST end with shotlog-injected+side-by-side BEFORE/AFTER pairs, one per component, zoomed in tight on+that component.** No exceptions, no "I'll check the JSON instead." If+you find yourself reaching for `setComponentVisibility(name, false, true)`+in eval and parsing the result as JSON, stop — that's a sanity check,+not a ralph loop.++#### Drive the page through pup, NOT the Hydrogen webview++Hydrogen workspace is shared with whatever else the user is doing+(aci, code editor, other panels). Touching it for a 16-component test+loop is intrusive. Run the test in a **pup** browser window instead:++- One pup session per app: `sessionId=adom-tsci`, `profile=adom-tsci`,+ `url=https://<slug>.adom.cloud/proxy/<port>/`. Survives sleep/wake.+- Shotlog goes in the SAME pup window. **Do NOT call `shotlog open+ --channel X`** — that adds a Hydrogen tab the user has to manually+ close every time. The shotlog channel page is hosted at+ `https://<slug>.adom.cloud/proxy/8820/log/<channel>/` and is+ reachable directly. Open it as a pup tab via:+ `adom-desktop browser_eval '{"sessionId":"adom-tsci","expr":"window.open(\"<url>\",\"shotlog-<channel>\")"}'`+- Drive `setComponentVisibility(name, visible, true)` directly via+ `browser_eval` — DO NOT go through `/api/toggle-component` because+ that's a 500ms-poll path and the screenshot races it. Direct eval+ is synchronous in page context.++#### Mandatory recipe (pup + shotlog tab in pup)++```bash+SESSION=adom-tsci+PORT=8889+CHAN=my-test+DIR=/tmp/tsci-ralph; mkdir -p $DIR++# Open shotlog channel as a TAB in the pup window. NEVER `shotlog+# open` (that adds a Hydrogen tab; user has corrected this 10+ times).+SHOTLOG_URL="https://<slug>.adom.cloud/proxy/8820/log/$CHAN/"+adom-desktop browser_eval "{\"sessionId\":\"$SESSION\",\"expr\":\"window.open('$SHOTLOG_URL','shotlog-$CHAN')\"}"++# Lock the camera + suppress UI overlays for clean diffs+adom-desktop browser_eval "{\"sessionId\":\"$SESSION\",\"expr\":\"(()=>{ document.getElementById('comp-overlay').style.display='none'; const tb=document.getElementById('toolbar'); if(tb)tb.style.display='none'; const t=document.getElementById('toast'); if(t)t.style.display='none'; const c=window.viewer.getCamera(); c.alpha=0; c.beta=0.001; c.radius=35; window.viewer.getScene().render(); return 'ok' })()\"}"++cap_shot() {+ local out=$1+ local resp=$(adom-desktop browser_screenshot "{\"sessionId\":\"$SESSION\",\"maxWidth\":1600}")+ local src=$(echo "$resp" | python3 -c "import sys,json; print(json.load(sys.stdin).get('savedTo',''))")+ cp "$src" "$out"+}++mutate() { # name, visible (true/false)+ adom-desktop browser_eval "{\"sessionId\":\"$SESSION\",\"expr\":\"(()=>{ setComponentVisibility('$1', $2, true); window.viewer.getScene().render(); return 'ok' })()\"}"+}++i=0+for COMP in $(adom-tsci list-chips --port $PORT | tail -n +2 | awk -F': ' '{print $2}' | tr ',' '\n' | tr -d ' '); do+ i=$((i+1)); num=$(printf '%02d' $i)+ cap_shot "$DIR/${num}-${COMP}-A.png"+ mutate "$COMP" "false"+ cap_shot "$DIR/${num}-${COMP}-B.png"+ convert "$DIR/${num}-${COMP}-A.png" "$DIR/${num}-${COMP}-B.png" \+ -compose difference -composite -colorspace gray -threshold 5% \+ "$DIR/${num}-${COMP}-mask.png"+ PIXELS=$(convert "$DIR/${num}-${COMP}-mask.png" -format "%[fx:w*h*mean]" info: | awk '{printf "%d", $1}')+ convert "$DIR/${num}-${COMP}-A.png" -modulate 70 \+ \( "$DIR/${num}-${COMP}-mask.png" -fill '#ff3030' -opaque white -transparent black \) \+ -composite "$DIR/${num}-${COMP}-overlay.png"+ montage -label "BEFORE" "$DIR/${num}-${COMP}-A.png" \+ -label "HIDE $COMP ($PIXELS px)" "$DIR/${num}-${COMP}-B.png" \+ -label "DIFF" "$DIR/${num}-${COMP}-overlay.png" \+ -tile 3x1 -geometry '+4+4' -background '#111' -bordercolor '#111' \+ -fill white -font DejaVu-Sans -pointsize 18 \+ "$DIR/${num}-${COMP}-trio.png"+ shotlog inject -c $CHAN -d "${num} ${COMP} ${PIXELS}px" -s pup "$DIR/${num}-${COMP}-trio.png"+ mutate "$COMP" "true"+done+```++Then **link the shotlog channel URL in your message back to the user**.+The user expects to scroll the channel in the pup tab they already have+open. Don't paste a single composite when the loop covered 17+components.++`fit-to-component` exists as a CLI for per-component zoom; if a+sibling-bleed test needs zoomed shots instead of top-down full-board+diffs, use it before each `cap_shot` call.++### Ralph-loop-test before claiming the highlight works++Eyeball-verification is a wasted turn. Use the `adom-tsci highlight` CLI+to deterministically test highlighting for each kind — flip on, screenshot,+flip off, screenshot, compare.++```bash+# 1. List the kinds the current board actually has (reads componentMap,+# reflects real ftype classification, not a guess).+adom-tsci highlight --list-kinds --port 8889++# 2. Baseline OFF screenshot — camera wherever it is now.+adom-tsci highlight --off --port 8889+adom-cli hydrogen screenshot panel --name '<board tab>' --reason 'ralph: off'++# 3. Per-kind ON screenshot — camera auto-fits to the kind.+for kind in capacitor resistor inductor chip testpoint contact mounting; do+ adom-tsci highlight --kind $kind --port 8889 || continue+ adom-cli hydrogen screenshot panel --name '<board tab>' --reason "ralph: $kind"+done++# 4. Read each pair back and compare. If the ON shot doesn't have teal+# strokes (or cyan discs for testpoints) on every named component,+# the highlight path is broken — fix it, rebuild, re-run.+```++`--no-fit` skips the camera zoom when you want to verify ON/OFF from the+same viewpoint. `--list-kinds` prints counts so you know what to iterate.++Fix workflow if the diff shows the highlight is wrong:+1. Read the SKILL.md four rules (above) first — you probably broke one.+2. Fix the code in `highlightComponents` / `_makeTestpointDisc` / the+ classifier.+3. `cargo build --release && cp target/release/adom-tsci ~/.local/bin/`.+4. `adom-tsci stop && adom-tsci start …` so the new shell.html is served.+5. Re-run the ralph loop. If still wrong, goto 1.++### Shape of the function today++```js+const HIGHLIGHT_TEAL = { r: 0.35, g: 0.75, b: 0.70 }; // stroke + emissive+const TESTPOINT_CYAN = { r: 0.40, g: 0.90, b: 0.95 }; // disc colour++// highlightComponents(names):+// 1. Dispose _compHighlightBackup (material clones), clear+// _compHighlightLayer (Babylon HighlightLayer), dispose+// _highlightMarkers (synthesised cyan markers).+// 2. For each refdes:+// - kind === 'testpoint' → _makeTestpointDisc() at the ghost cuboid's+// centroid XY, clamped to window._boardTopZ. Push to _highlightMarkers.+// - otherwise → addMesh() to the HighlightLayer (teal stroke) + clone+// the material with an emissive teal tint (60% luminance of the+// stroke) + push to _compHighlightBackup.+```++## 🎯 Rotation-center sphere is a USER affordance — suppress it on programmatic camera moves++The little sphere that pops up at the camera target during rotation is+the **adom-3d-viewer's center-of-rotation indicator**. It exists to tell+a human user dragging with the mouse "here's what you're orbiting+around." It is *not* a general "camera is moving" badge.++**Rule:** the sphere must only appear when the user is directly driving+the camera with pointer input (drag to orbit, middle-click to pan, wheel+to zoom). CLI-driven moves — `adom-tsci view <preset>`, `adom-tsci+camera --alpha/--beta/--radius`, `adom-tsci tour`, `/api/camera-command`+polling, walkthrough auto-fly, `viewer.frameModel()`, anything that runs+through the HTTP API — must **not** pop the sphere. It's distracting in+recorded demos and misleads the user into thinking they did something.++**Implementation:** shell.html wraps programmatic entry points in a+`_programmaticRotation` flag while the move is in flight and patches+the viewer's `detectAndShowRotationSphere` to early-return whenever the+flag is set. User pointer-down on the canvas clears the flag. Any new+programmatic driver added to shell.html must set this flag around its+camera mutation — otherwise the sphere will leak into demo recordings.++**Upstream intent:** the correct long-term fix lives in+[`adom-3d-viewer`](https://github.com/adom-inc/adom-3d-viewer): only+call `detectAndShowRotationSphere` from the pointer handlers, not from+`onBeforeRenderObservable`. The shell.html patch is the workaround+until that ships.++## 💸 Do NOT rebuild example projects unless you actually need to++Every `examples/<name>/` directory in this repo ships with a **pre-built+`dist/lib/index/` on disk** — `3d.glb`, `3d.png`, `pcb.svg`, `pcb.png`,+`schematic.svg`, `circuit.json`. That pre-build is **the whole point of+example projects.** A new user (or a new Claude reading the wiki) should+be able to look at the examples, screenshot them, stitch them into a+demo, paste them into docs — **without ever running `bunx tsci build`.**++A rebuild is:++- **60–180 seconds of wall time** per example. Eight examples = 15++ minutes of a user's session you just ate.+- **~$0.10–$0.50 of Anthropic token cost** — log tailing, polling,+ error re-reads, retries. Invisible to you, very visible to the user.+- **Non-deterministic** — tscircuit router output can drift between+ versions; a "fresh" rebuild may produce *different* traces than the+ checked-in `dist/`, so the example's wiki screenshots stop matching.++**The rule:** when the user asks you to show, screenshot, stitch, or+demo an example, **use the pre-built `dist/` on disk.** `ls+examples/<name>/dist/lib/index/` first. Only rebuild when:++- `dist/` is genuinely missing AND you need something it would produce.+- The user just edited `lib/index.tsx` and wants to see the change.+- You're investigating a build-time bug and the failure *is* the goal.++**Never** rebuild "to make sure it's fresh" or "to match current CLI+version." The checked-in `dist/` is authoritative unless proven stale+by a source-tree edit. If a specific artifact is missing (e.g.+`3d.png` but `pcb.svg` exists), **fall back to the sibling artifact**+for that scene — a rasterized `pcb.svg` makes a perfectly fine demo+still image.++This rule applies to **any other Claude helping a new Adom user** too:+the first contact with `adom-tsci` is "point me at an example" and the+experience must be instant, not a 2-minute pause while eight builds+run. Update the wiki example page if you change this.+ ## No Adom Viewer dependency (v0.3.0+) As of v0.3.0, the **3D tab is a self-contained three.js canvas** — no@@ -87,7 +530,73 @@ a subcommand, plus there's a JS eval channel for hot-patching and a console forwarder so Claude can read UI-side errors. See the "Remote-control from the CLI" section below. -## What's new in v0.4.x+## 📐 Schematic layout — place parts, never let them pile (v1.4.x)++tscircuit AUTO-lays-out the schematic when components have no `schX`/`schY`.+For a board with many repeated parts (LED grids, resistor arrays) that means+**one giant unreadable vertical column**. Fix: give parts explicit schematic+coords.++- **Place repeated parts on a grid.** Mirror the physical/pixel grid into+ schematic space via `schX` / `schY` (+ `schRotation` to orient a cell). On+ the OLIVER nameplate this makes the schematic literally spell the name in+ LED+resistor cells instead of a 90-tall column.+- **Wrapper components MUST forward `schX`/`schY`/`schRotation`** to their inner+ `<chip>`. A wrapper that doesn't (the old `UsbCReceptacle`) silently defaults+ to the schematic ORIGIN (0,0) and piles on top of other parts. Always pass+ them through. (Canonical USB-C wrapper is in the `adom-usbc` guide.)+- **`<Molecule>` machine pins pile at the origin** and can't be placed from the+ board source (they're emitted internally). Until the molecule lib places them+ at the schematic corners, LIFT the rest of your layout (`schY` offset) so the+ name/parts sit clear of the origin pile.++## ✅ `adom-tsci lint` — run BEFORE shipping a board++`adom-tsci lint [--project DIR]` reads `dist/lib/index/circuit.json` and catches+mistakes the tscircuit build does NOT fail on. Run it after every `start` /+`rerun`. Checks:+1. tscircuit build errors hidden in circuit.json+2. unrouted traces (autorouter silently produced 0)+3. ghost chips (a `simple_chip` source with no `cad_component`)+4. **USB-C / directional connector geometry** — body forward of pads, mouth+ actually exits the board edge within a connector depth (place the receptacle+ at `pcbX ≈ W/2 − 4` for an east edge, per the `adom-usbc` guide)+5. **schematic overlaps** — any components whose symbols stack on each other+ (the classic "forgot schX/schY → piled at origin" bug). Reports the parts ++ worst pairs with fix hints.++A non-zero exit = board not ready to ship. The schematic-overlap check is the+linter half of "smart layout that never overlaps."++## 🎬 Walkthrough is GENERATED, never hand-written++The Walkthrough is built deterministically by **`adom-tsci walkthrough-gen`**+from `circuit.json` (intro → connectors → ICs → grouped passives → one nets+x-ray step → outro). Rules:++- **Never hand-edit `walkthrough.json`** — it carries a `_meta.do_not_edit`+ stamp and is OVERWRITTEN on regen.+- It **auto-regenerates** on `adom-tsci start` whenever circuit.json drifts+ (SHA changes) — so editing the board + restarting the preview is enough; you+ don't author it.+- Per-board facts are **computed** (connector edge from PCB position, counts,+ nets) — not guessed; long refdes lists are capped (`…+N more`).+- It **finishes on the ViewCube Home view with the Nets / x-ray panel OFF** — a+ tour must never leave the board mid-orbit or the x-ray meshes loaded.+- Want a custom intro story? Put it in the board's `package.json` `description`+ (it becomes the intro step and survives regen). Don't hand-edit the JSON.++## 🪟 Ground plane is 65% transparent (HARD invariant)++The 3D ground MUST stay 65% transparent (board bottom shows through). It's the+viewer's own `shadowGround` mesh (`groundMat`, PBR) — `applyGroundTransparency`+sets `alpha 0.35` and an every-frame guard re-asserts it (goHome/frameModel+reset it otherwise). The footer shows the live `ground: 65% transparent`+readout. Verify after any viewer change with `adom-tsci view bottom` + a+screenshot — never claim it's fine without seeing the board's underside through+the ground.++## What's new in v0.4.x (history) Everything below is in the **v0.4.0 / v0.4.1** binary that's on the wiki right now:@@ -164,21 +673,93 @@ what I'm building" step trivial. ## Quick start +### Zero-to-preview — cold container, nothing installed++Run these top-to-bottom. The whole thing takes ~2 min on a warm cache.+ ```bash-# One-time install-curl -fsSL https://wiki-ufypy5dpx93o.adom.cloud/static/apps/adom-tsci/adom-tsci \- -o /usr/local/bin/adom-tsci && \- chmod +x /usr/local/bin/adom-tsci && \+# 1. bun (the tscircuit runtime — may not be pre-installed)+command -v bun >/dev/null || curl -fsSL https://bun.sh/install | bash+export PATH="$HOME/.bun/bin:$PATH"++# 2. adom-tsci binary + skill+sudo curl -fsSL https://wiki-ufypy5dpx93o.adom.cloud/static/apps/adom-tsci/adom-tsci \+ -o /usr/local/bin/adom-tsci && sudo chmod +x /usr/local/bin/adom-tsci && \ adom-tsci install -# From any tscircuit project directory:+# 3. Get a project. If you don't have one yet, clone the example repo:+# (skip if you already have a tscircuit project)+git clone https://github.com/adom-inc/adom-tsci.git+cd adom-tsci/examples/SN65HVD230-Molecule && bun install++# 4. CRITICAL: adom-tsci start refuses to create its webview tab unless an+# existing Web View pane is already present ("no existing Web View pane+# found" error). If the layout is a single VS Code pane, split one:+VSCODE_PANEL_ID=$(adom-cli hydrogen workspace tabs \+ | python3 -c 'import json,sys; [print(t["panelId"]) for t in json.load(sys.stdin)["tabs"] if t["name"]=="Visual Studio Code"]' \+ | head -1)+adom-cli hydrogen workspace split \+ --panel-id "$VSCODE_PANEL_ID" \+ --direction horizontal \+ --panel-type adom/a1b2c3d4-0031-4000-a000-000000000031 \+ --display-name "Web View" --display-icon "mdi:web" --ratio 0.5++# 5. Start the preview — auto-opens a tab in the Web View pane above.+adom-tsci start .++# 6. If the pane ever gets closed (e.g. after an approval-dialog cancel+# resets the workspace), re-split step 4 and run:+adom-tsci open+```++### Day-to-day++```bash+# From any tscircuit project directory (webview pane already present): adom-tsci start .-# …open the adom-tsci tab in your editor, edit lib/index.tsx,-# click Re-run autorouter when you want fresh outputs,-# stop with:+# …edit lib/index.tsx, click Re-run autorouter, stop with: adom-tsci stop ``` +**Web View panel UUID (for reference):** `adom/a1b2c3d4-0031-4000-a000-000000000031`.+Every split that lands the adom-tsci tab needs this `--panel-type`.++### Moving or renaming a project directory — don't rebuild++When you relocate an existing tscircuit project (e.g. promoting a+scratch project in `tscircuit-projects/` to `adom-tsci/examples/`),+**just `mv` it.** Don't delete `node_modules` / `bun.lock` / `dist`+and re-run `bun install` — that's minutes of wasted time and bandwidth+on every move.++- `node_modules` has **no path-dependent state**. Bun / npm symlink+ binaries *inside* the folder; nothing points out to the old location.+ Moving the whole tree keeps every link valid.+- Changing the `name` field in `package.json` does **not** invalidate+ installs — deps are keyed by the dep graph, not the root package+ name. You can rename `@tsci/scratch.Foo` → `@tsci/john.Foo` without+ re-installing.+- `bun.lock` is relative and portable. Keep it.+- `dist/` is stale after an edit anyway; you'll rebuild the next time+ you change `lib/index.tsx`. No reason to pre-emptively wipe it at+ move time.++Safe sequence:++```bash+# stop any live preview watching the old path+adom-tsci stop --port <port>++mv old/path/MyChip-Molecule new/path/MyChip-Molecule++# edit package.json if renaming scope — that's it, no reinstall+adom-tsci start new/path/MyChip-Molecule --port <port> \+ --tab-name "MyChip (new location)" --display-icon "mdi:chip"+```++Rule of thumb: `bun install` is for *dependency* changes+(bumping versions, adding/removing packages), not *location* changes.+ ## The UI ```@@ -319,6 +900,124 @@ RunFrame hangs at `loading...` forever. Tsci dev uses HTTP polling (`/api/events/list?since=<id>`) for hot reload — **no WebSockets** — so the slingshot is a plain HTTP forwarder. +## 3D scene conventions (Z-up, XY = board)++The Babylon scene in `shell.html` is **Z-up**: the board's copper /+silkscreen surface lies in the **XY plane**, and **+Z is the board+normal** (above-board direction). Component positions from+`circuit.json` (`pcb_component.center.x/y`) map directly to world X/Y,+and `window._boardTopZ` holds the Z of the top copper layer.++This matters whenever you add overlay geometry to the scene. Babylon's+`MeshBuilder.CreateDisc` and `MeshBuilder.CreatePlane` create meshes+**in the XY plane with normal +Z by default**, which — in this scene+— *already lies flat on the board*. **Don't rotate them.** A+`rotation.x = π/2` stands the overlay vertical, edge-on to the board,+which is wrong for anything that's meant to sit on the copper.++### Highlight pattern: pad-surface overlay for baked-texture features++Components whose "body" is **baked into the board-surface texture**+(test points, silkscreen markers, contact pads) have no separate mesh+to tint. Their 3D representation is just texels on the PCB surface.+Hover-highlight via material clone fails — there's no mesh.++The pattern used for testpoints is:++1. On enumeration, store each component's `(x, y)` world position+ **and** its `pcb_smtpad` geometry (`shape`, `radius` / `width` /+ `height`) on `componentMap.get(name).pad`.+2. In `highlightComponents`, count how many of a component's meshes+ are actually `isEnabled()`. If zero, take the **overlay path**.+3. Overlay path: create a `CreateDisc` (circle pads) or+ `CreatePlane` (rect pads) at `(x, y, _boardTopZ + 0.3)`, sized+ to the **exact** pad geometry. **No rotation** — it lies flat.+ **Z offset ≥ 0.3 mm is load-bearing:** at 0.1 mm the overlay+ z-fights the baked pad texture and disappears from top-down.+4. **`sideOrientation: 2` (DOUBLESIDE)** on `CreateDisc` /+ `CreatePlane` options. Default single-side culls the face whose+ normal points away from the camera — a top-down view of an+ XY-plane disc can show the back face, rendering nothing.+5. Material: `disableLighting = true`, `emissiveColor` **vivid ++ opaque** (e.g. `Color3(0.45, 1.0, 0.95)`, no alpha). An+ alpha-blended dim cyan over the green board is invisible — it+ reads as a shadow, not a highlight. Keep it opaque, let the pad+ geometry do the visual work. `renderingGroupId = 2` draws over+ silkscreen + pads.+6. Track overlays in `_compHighlightOverlays` and dispose every+ mesh + material on each `highlightComponents()` call so they+ clear when the hover leaves.++Effect: the gold pad that's baked into the board-surface texture+appears to light up cyan when you hover its row in the Components+HUD, giving the user **something to visually toggle** for a feature+that otherwise has no 3D body to tint.++### Highlight glow — manual halo (no GlowLayer in bundled Babylon)++The bundled `adom-3d-viewer.min.js` **does not ship** `BABYLON.GlowLayer`,+`HighlightLayer`, or any post-processing pipeline. Cross-wiring+CDN-Babylon's `GlowLayer` onto the bundled scene fails because newer+Babylon's GlowLayer calls `scene.addObjectRenderer`, which the older+bundled runtime doesn't expose.++Fake it with a manual halo:++- **Chips / resistors / capacitors (meshes with body):** clone the+ mesh via `baseMesh.clone(name, null, true)`, parent it to the base,+ scale `1.06x`, apply an additive-blended emissive material+ (`alphaMode = 1` = ADD, `disableLighting = true`, `alpha = 0.5`).+ The additive blend makes overlapping pixels brighter than either+ source — reads as bloom.+- **Testpoints (overlay discs):** stack 2 concentric discs underneath+ the solid cyan core at `1.8x` + `2.6x` radius, with+ `alpha = 0.45` / `0.22` and additive blend. Place at progressively+ lower Z (0.005 mm decrement) so the solid core still wins depth+ but the halo bleeds outward.++Both get disposed by `_disposeHalos()` on every `highlightComponents()`+call so the scene doesn't accumulate stale halos as the walkthrough or+hover state changes.++### Walkthrough steps auto-highlight their focus++`walkthroughRenderStep` derives the highlight set from `step.focus`+whenever `step.highlight` isn't explicitly provided:++| `focus.kind` | auto-highlight |+|---|---|+| `component` + `name` | `[name]` |+| `components` + `names` | `names` |+| `kindAll` + `componentKind` | every `componentMap` entry whose `kind` matches (e.g. all testpoints) |+| anything else (`view`, `all`, `silkscreen`) | `[]` |++This is why a step whose focus is `{ kind: 'kindAll', componentKind:+'testpoint', flyover: true }` lights up every testpoint pad with a+cyan overlay disc for the duration of that step — even without an+author-provided `highlight` list. The step is **about** those+components by definition, so they should be tinted.++Explicit `step.highlight` still wins when set — useful for+cross-cutting steps that want to highlight components outside the+focus target (e.g. "this chip talks to THAT chip").++### Don't confuse "toggle visibility" with highlight++Toggle (`setComponentVisibility`) and highlight (`highlightComponents`)+touch the same meshes but differently:++- **Toggle** uses `mesh.visibility = 0.05 | 1.0` (per-mesh, works with+ shared materials). Always `highlightComponents([])` **first** to+ restore any clone materials — otherwise the visibility change lands+ on a short-lived clone that gets disposed when the row re-renders.+- **Highlight** clones the mesh's material and tints the clone's+ `emissiveColor`. For baked-texture components (no enabled mesh),+ falls back to the overlay-disc path above.++The two mechanisms share state through `componentMap.get(name).meshes`+but one-way: toggle never reads highlight state, highlight restores+any clones it created before doing its work.+ ## CLI commands Every command follows the Adom CLI conventions (`OK:` / `ERROR:` /@@ -342,7 +1041,7 @@ Every command follows the Adom CLI conventions (`OK:` / `ERROR:` / | Command | Purpose | |---|---|-| `adom-tsci view <preset>` | Camera preset: front / back / left / right / top / bottom / iso / isometric |+| `adom-tsci view <preset>` | Camera preset: **home** (ViewCube Home — default angle + fit) / front / back / left / right / top / bottom / iso / isometric | | `adom-tsci camera [--alpha A] [--beta B] [--radius R]` | Raw alpha/beta (radians) + radius multiplier | | `adom-tsci tour start \| stop` | Cinematic slow-orbit camera | | `adom-tsci toggle <flag>` | Flip a toolbar flag: `ground`, `wireframe`, `axes` |@@ -439,7 +1138,7 @@ curl -sS -X POST http://127.0.0.1:8850/shutdown > **User:** preview my SN65HVD230 molecule >-> **Claude:** *(runs `adom-tsci start ~/project/tscircuit-projects/SN65HVD230-Molecule`)*+> **Claude:** *(runs `adom-tsci start ~/project/adom-tsci-projects/SN65HVD230-Molecule`)* > > Output: > ```@@ -474,6 +1173,20 @@ curl -sS -X POST http://127.0.0.1:8850/shutdown ## Troubleshooting +**`Error: no existing Web View pane found`**+`adom-tsci start` will refuse to create its tab over a non-webview+pane. See step 4 of the Zero-to-preview section above for the exact+`workspace split` command — it needs `--panel-type+adom/a1b2c3d4-0031-4000-a000-000000000031`. Once a Web View pane+exists, re-run `adom-tsci start`, or `adom-tsci open` if the server+is already running.++**Webview tab vanished mid-session (workspace reset)**+Dismissing certain approval dialogs (screen-share, AI events access)+can reset the Hydrogen layout back to a single VS Code pane and drop+the adom-tsci tab. The server keeps running; just re-split the pane+(step 4 above) and run `adom-tsci open` to re-attach.+ **`HINT: @tscircuit/cli is out of date`** Not an error. tscircuit does daily releases; `adom-tsci upgrade` in your project directory pulls the latest. Or ignore it — the hint is@@ -537,13 +1250,149 @@ group (`setsid`) and the whole group is killed on shutdown. If you see an orphan `bun ... tsci dev` after `adom-tsci stop`, file an issue with `ps auxf | grep tsci` output. +## Exporting fab files (gerbers / BOM / CPL)++`adom-tsci` is a preview tool, but the underlying `tsci` CLI can emit+manufacture-ready files. **Four gotchas** to know before the obvious+command actually ships a manufacturable package.++**Gotcha 0 — `tsci export` does NOT write `dist/`.** Only `tsci build`+does. If you're inspecting `dist/lib/index/circuit.json` to check route+counts, autorouter errors, or component sizes, make sure the last thing+you ran was `tsci build` (not `tsci export`). If you only ran `export`+and then grep `dist/`, you're reading a **stale circuit.json from the+previous build** — which can make it look like routing failed when the+gerber zip it just wrote actually has full routing. Always:++```bash+bunx tsci build lib/index.tsx --glbs --svgs # writes dist/, regenerates circuit.json+bunx tsci export lib/index.tsx --format gerbers --output ../gerbers.zip # reads current dist/, writes zip+```++If you need the latest numbers after an export, run `build` again or+extract + parse a `.gbr` file directly (e.g. `grep -c '^X.*D0[12]'` on+`F_Cu.gbr` counts draw operations).++**Gotcha 0.5 — BOM + CPL inside `gerbers.zip` are EMPTY.**+`tsci export --format gerbers` writes `bom.csv` and `pick_and_place.csv`+*into the zip*, next to the gerber files. JLCPCB reads those (not any+sibling file on disk) when you upload the zip. But tsci's versions are+**stub CSVs with only designators + values — no LCSC, no MFR part+number, no package** — so the JLCPCB assembly tool will show "no parts+matched" on every row regardless of what you put in `fab/bom.csv`+alongside.++Fix: after `tsci export`, splice your populated BOM/CPL into the zip+over tsci's empty ones. Quick Python:++```python+import zipfile, shutil+src = 'fab/gerbers.zip'; tmp = 'fab/_tmp.zip'+with zipfile.ZipFile(src,'r') as zin, zipfile.ZipFile(tmp,'w',zipfile.ZIP_DEFLATED) as zout:+ for n in zin.namelist():+ if n in ('bom.csv','pick_and_place.csv'): continue+ zout.writestr(n, zin.read(n))+ zout.writestr('bom.csv', open('fab/bom.csv').read())+ zout.writestr('pick_and_place.csv',open('fab/cpl.csv').read())+shutil.move(tmp, src)+```++Verify with `unzip -p fab/gerbers.zip bom.csv | head -3` — first row+after the header should show your real part number, not a blank.++**Gotcha 1 — `--output` path resolution.**+`tsci export <file> --output <path>` treats the output path as *relative+to the entrypoint's directory* (`lib/`), including absolute paths, which+fails with++```+Error writing file: ENOENT ... lib/<your path>/gerbers.zip+```++Workaround: pass a `../`-prefixed relative path and move the file after.++```bash+cd ~/project/your-molecule+bunx tsci export lib/index.tsx --format gerbers --output ../gerbers.zip+mv gerbers.zip fab/+```++**Gotcha 2 — inner-layer support.**+Out of the box, `@tscircuit/cli` only emits **F_Cu** and **B_Cu** gerbers,+regardless of whether the board declares `layers={4}`. Inner-layer traces+are routed on `inner1` / `inner2` in `circuit.json` but never written out,+so a 4-layer board ships as a 2-layer gerber package (silent+data loss — fab will build wrong).++**Fix** (patch `node_modules/@tscircuit/cli/dist/cli/main.js` until it's+upstreamed):++1. Extend the layer-prefix map to include inner layers:+ ```js+ var layerRefToGerberPrefix = {+ top: "F_", bottom: "B_",+ inner1: "In1_", inner2: "In2_" // ← add+ };+ ```+2. In `convertSoupToGerberCommands`, add the two new copper layer+ headers to the `glayers` object between `F_Paste` and `B_Cu`:+ ```js+ In1_Cu: getCommandHeaders({ layer: "inner1", layer_type: "copper" }),+ In2_Cu: getCommandHeaders({ layer: "inner2", layer_type: "copper" }),+ ```+ and include `"In1_Cu", "In2_Cu"` in the aperture-loop array.+3. Change the two hardcoded trace/via loops from `["top", "bottom"]`+ / `["top", "bottom", "edgecut"]` to include inner layers:+ ```js+ for (const layer of ["top", "inner1", "inner2", "bottom", "edgecut"]) {+ ```+4. Update `getAllTraceWidths` to return `inner1` + `inner2` width lists.+5. **File extension fix** — most fabs (JLCPCB, PCBWay, Eurocircuits)+ reject `.gbr` on inner layers and expect **`.g1` / `.g2`** (Protel+ convention). Change the zip-write loop:+ ```js+ for (const [fileName, fileContents] of Object.entries(gerberFileContents)) {+ const ext = fileName === "In1_Cu" ? "g1"+ : fileName === "In2_Cu" ? "g2"+ : "gbr";+ zip.file(`${fileName}.${ext}`, fileContents);+ }+ ```++With all five patches applied, the resulting zip contains++```+F_Cu.gbr In1_Cu.g1 In2_Cu.g2 B_Cu.gbr+F_Mask.gbr B_Mask.gbr F_Paste.gbr B_Paste.gbr+F_SilkScreen.gbr B_SilkScreen.gbr Edge_Cuts.gbr+drill.drl drill_npth.drl bom.csv pick_and_place.csv+```++and JLCPCB's upload will accept it as a proper 4-layer board.++> **Upstream this:** the patches above are stopgaps in a local+> `node_modules`. When the project is rebuilt from a fresh install they+> get wiped — the real fix is a PR to `tscircuit/tscircuit-cli`+> extending `convertSoupToGerberCommands` with configurable layer lists+> + the `.g1/.g2` extension map.++**Other useful formats** (all honour the same `--output ../` trick):++| Flag | Output |+|---|---|+| `--format gerbers` | Zip of gerbers + drill + BOM + CPL |+| `--format kicad_zip` | KiCad 7/8 project zip (importable in pcbnew) |+| `--format kicad_pcb` | Single `.kicad_pcb` file |+| `--format step` | 3D STEP model of the assembled board |+| `--format readable-netlist` | Human-readable net list |+ ## Publishing your molecule `adom-tsci` is a preview tool — it doesn't publish anything. Use tscircuit's own flow: ```bash-cd ~/project/tscircuit-projects/MyChip-Molecule+cd ~/project/adom-tsci-projects/MyChip-Molecule bunx tsci push ``` @@ -558,10 +1407,13 @@ guide at `~/.claude/skills/adom/guides/adom-tscircuit-skill.md`. ## See also - **Ray's guide:** `~/.claude/skills/adom/guides/adom-tscircuit-skill.md`- — the authoritative tscircuit workflow. Go there for production- molecule families.-- **adom-tscircuit skill:** `~/.claude/skills/adom-tscircuit/SKILL.md`- — onboarding layer that points new users to this app for display.+ — the authoritative tscircuit-on-Adom workflow (deployed by+ `gallia/install.mjs`). **Single source of truth** for writing+ tscircuit molecules: connector families, molecule generators,+ sizing grid, `<MachineContactMedium>` placement rules, DRC, review+ servers, snapshot approval, all of it. If the user asks how to+ build a molecule, route them here — do not summarise Ray's+ content in this skill. - **hydrogen-tab-icons skill:** documents how the `#e6edf3` monochrome chip favicon in `src/assets/icon.svg` ends up on the tab bar (spoiler: the page's `<link rel="icon">` overrides any `--display-icon` placeholder).
VERSIONadded+1@@ -0,0 +1 @@+1.4.13
adom-tsci-billboard.pngadded⋯ 1 unchanged line ⋯
guides/adom-usbc.mdadded+382@@ -0,0 +1,382 @@+# adom-usbc — making USB-C receptacles actually work++**Read this before touching a USB-C receptacle on an Adom tscircuit+board.** Written because Claude got USB-C wrong ~20 times before we+wrote the rules down. If you skip this skill and try to wing it, you+will repeat every one of those failures.++## THE CORE INVARIANT — DO NOT VIOLATE++**Footprint (pads + holes) and cadModel (3D body) are tied together.+Whatever rotation you apply to one MUST be applied identically to the+other. Same rotation AND same translation.** They are one physical+object. Treating them as independently rotatable is the root cause+of every USB-C alignment bug we've hit.++Ways to satisfy this invariant:++1. **Bundle them in one component** with a single `pcbRotation` prop+ that applies to BOTH the `<chip>`'s footprint children AND the+ cadModel's `positionOffset`. The wrapper in §"The workaround"+ below does this — it computes `positionOffset` as `rotate(native_offset,+ pcbRotation)` in userland so the body rotates with the footprint.+2. **Compute body placement in the SAME local frame as the footprint.**+ Never express body offset in world coords while the footprint is+ in local coords. If you set `positionOffset` directly without+ rotating it by `pcbRotation`, the body will drift off its pads on+ every non-zero rotation.++The common violation: "I'll rotate the chip 270° for east edge, and+I'll add 2.5 mm positionOffset to shift the body forward." The first+rotation applies to pads/holes (tscircuit auto-rotates them in the+footprint local frame). The second shift applies to the body (in+world frame, no rotation). They decouple. The body ends up on the+wrong side of the pads, and the anchor holes end up OUTSIDE the body+footprint. I rebuilt this ~15 times before realising this was the+bug, not a tuning issue.++**Test for the invariant**: the distance between the body centroid+and each anchor hole must be the same at `pcbRotation=0` as it is at+`pcbRotation=90/180/270`. If that distance changes with rotation, the+invariant is broken, the build is wrong, and you must NOT ship the+board.++## The fundamental problem++`@tsci/seveibar.smd-usb-c` and `@tsci/ArnavK-09.smd-usb-c` both+hardcode a `cadModel` with:++```js+cadModel: {+ objUrl: "…easyeda_models/download?…pn=C165948",+ rotationOffset: { x: 0, y: 0, z: 180 },+ positionOffset: { x: 0, y: -2.5, z: 0 },+}+```++This works at the default `pcbRotation=0` (mouth facing south) because+`(0, -2.5)` puts the body 2.5 mm south of the pad centroid, which is+where the receptacle's body sits relative to its back-row of SMT pads.++**It breaks at every other rotation.** Two reasons:++1. **`positionOffset` is applied in world coords, not rotated with+ `pcbRotation`.** So at `pcbRotation=270` (pads rotated to the east),+ the body stays 2.5 mm south instead of moving 2.5 mm *east* with+ the pads. The body drifts off the pads and the mouth points into+ the board.+2. **The wrapper's `{...props, cadModel: HARDCODED}` spread order+ silently discards any `cadModel` override passed by the caller.**+ So you can't fix bug #1 by passing `cadModel` on `<SmdUsbC>`.++Filed upstream as `TSCIRCUIT_FEATURE_REQUESTS.md §6`.++## The workaround: a local `UsbCReceptacle.tsx`++Ship a local wrapper in your project's `lib/` that builds a raw+`<chip>` with an explicit footprint (copied from ArnavK-09's JSX) and+your own `cadModel` that rotates `positionOffset` with `pcbRotation`.+This survives `bun install`, gives you full control, and — critically+— you can unit-test it with the lint below without waiting on+upstream fixes.++### The template (copy this verbatim)++```tsx+// lib/UsbCReceptacle.tsx+const USB_C_PN = "C165948"+const USB_C_UUID = "2a4bc2358b36497d9ab2a66ab6419ba3"++// Body-to-pad offset in the mouth direction for TYPE-C-31-M-12.+// Measured: the SMT pad row sits at the BACK of the receptacle, and+// the body centroid is 2.5 mm forward of that. Centering body on pads+// (positionOffset 0) makes the back half of the body extend BEHIND+// the pads into the board — wrong geometry, even if numerics look+// "clean".+const BODY_FORWARD_MAG = 2.5++const pinLabels = {+ pin1: ["GND1", "A1"], pin2: ["GND2", "B12"],+ pin3: ["VBUS1", "A4"], pin4: ["VBUS2", "B9"],+ pin5: ["SBU2", "B8"], pin6: ["CC1", "A5"],+ pin7: ["DM2", "B7"], pin8: ["DP1", "A6"],+ pin9: ["DM1", "A7"], pin10: ["DP2", "B6"],+ pin11: ["SBU1", "A8"], pin12: ["CC2", "B5"],+ pin13: ["VBUS1", "A9"], pin14: ["VBUS2", "B4"],+ pin15: ["GND1", "A12"], pin16: ["GND2", "B1"],+} as const++// 16 SMT pads, pitch 0.5 mm, centered at y=0 so bounds.center = pad+// centroid (no holes included — see §Why we drop the holes).+const PADS: Array<{ port: string; x: number }> = [+ { port: "A1", x: -3.35 }, { port: "B12", x: -3.05 },+ { port: "A4", x: -2.55 }, { port: "B9", x: -2.25 },+ { port: "B8", x: -1.75 }, { port: "A5", x: -1.25 },+ { port: "B7", x: -0.75 }, { port: "A6", x: -0.25 },+ { port: "A7", x: 0.25 }, { port: "B6", x: 0.75 },+ { port: "A8", x: 1.25 }, { port: "B5", x: 1.75 },+ { port: "B4", x: 2.25 }, { port: "A9", x: 2.55 },+ { port: "B1", x: 3.05 }, { port: "A12", x: 3.35 },+]++export const UsbCReceptacle = ({+ name, pcbX, pcbY, pcbRotation = 0, schX, schY, schRotation,+}: {+ name: string; pcbX: number; pcbY: number;+ pcbRotation?: 0 | 90 | 180 | 270+ // SCHEMATIC placement — ALWAYS forward to the inner <chip>. Without these+ // the receptacle silently defaults to the schematic origin (0,0) and piles+ // on top of other parts (caught by `adom-tsci lint` Check 5). A wrapper+ // component MUST pass schX/schY/schRotation through, or it can't be placed+ // on the schematic.+ schX?: number; schY?: number; schRotation?: number+}) => {+ // Native CAD has mouth at local +Y. cad.rotation.z = pcbRotation+ // + rotationOffset.z (tscircuit adds them, CCW). We set+ // rotationOffset.z=0 so cad_z = pcbRotation. Then the mouth direction+ // in world frame is rotate((0,+1), pcbRotation CCW) =+ // (-sin pcbRotation, cos pcbRotation).+ const rad = (pcbRotation * Math.PI) / 180+ const mouthX = -Math.sin(rad)+ const mouthY = Math.cos(rad)+ const posX = BODY_FORWARD_MAG * mouthX+ const posY = BODY_FORWARD_MAG * mouthY++ return (+ <chip+ name={name}+ pcbX={pcbX} pcbY={pcbY} pcbRotation={pcbRotation}+ schX={schX} schY={schY} schRotation={schRotation}+ supplierPartNumbers={{ jlcpcb: [USB_C_PN] }}+ manufacturerPartNumber="TYPE-C-31-M-12"+ pinLabels={pinLabels as any}+ cadModel={{+ objUrl: `https://modelcdn.tscircuit.com/easyeda_models/download?uuid=${USB_C_UUID}&pn=${USB_C_PN}`,+ rotationOffset: { x: 0, y: 0, z: 0 },+ positionOffset: { x: posX, y: posY, z: 0 },+ }}+ footprint={+ <footprint>+ {PADS.map(p => (+ <smtpad key={p.port} portHints={[p.port]}+ shape="rect" width="0.3mm" height="1.3mm"+ pcbX={`${p.x}mm`} pcbY="0mm" />+ ))}+ </footprint>+ }+ />+ )+}+```++## Per-edge placement recipe++For a board of width `W` centered at x=0 (so edges at ±W/2):++| Edge you want mouth to face | `pcbRotation` | `pcbX` | `pcbY` |+|---|---|---|---|+| South (bottom) | 0 | 0 or offset along x | −(H/2) + 4 |+| East (right) | 270 | +(W/2) − 4 | 0 or offset along y |+| North (top) | 180 | 0 or offset along x | +(H/2) − 4 |+| West (left) | 90 | −(W/2) + 4 | 0 or offset along y |++`pcbX / pcbY` is the **chip origin** = **pad row centroid** position.+For a 64×32 board with USB-C on east edge: `pcbX=28, pcbY=0,+pcbRotation=270`. Pads at x=28 (inside the board by 4 mm), body at+x=28+2.5=30.5, mouth cantilevers to about x=34.5 (2.5 mm past edge).++### **Corollary: no SMT pad may extend past the board edge.**++The **body / mouth / mechanical shell** can and should cantilever+past the edge. SMT **pads** must not. If any copper pad (VBUS1/2,+GND1/2, DP1/2, DM1/2, CC1, CC2, SBU1/2) ends up at `|pcbX| > W/2`,+tscircuit grows the GLB board mesh asymmetrically to include the+overhang — and because the texture art is drawn with `pcbX=0` at+the canvas centre, the whole silkscreen / copper / annular-ring+layer renders shifted by `asymmetry/2` relative to the 3D mesh.+Every via drill appears off-centre in its annular ring, every+passive body appears shifted from its pad. The PCB itself is fine,+but the 3D view looks broken.++Real failure: a 72×32 board with the USB-C receptacle at `pcbX=35`.+That put the east-most SMT pads at `pcbX≈37.2`, growing the mesh+to `[-38.174, +37.2]` (centre `-0.487`) and shifting every copper+feature ~0.5 mm in the render. Fix: moved the receptacle to+`pcbX=33` (= `W/2 − 4 + overhang_0`). Mesh became `[-37.2, +37.2]`,+shift gone. **Use `pcbX = W/2 − 4` for the east edge; do not improvise.**++## Mandatory verification lint++**Run this after every build with a USB-C receptacle.** Not+optional. The only way we stopped getting USB-C wrong was by+gating on these two numeric checks:++```js+// scripts/check-usbc.mjs+import j from "./dist/lib/index/circuit.json" with { type: "json" }++for (const src of j.filter(x => x.type === "source_component")) {+ // Heuristic: USB-C receptacle = any chip with jlcpcb matching a+ // known USB-C family. Extend as new families are supported.+ const pn = src.supplier_part_numbers?.jlcpcb?.[0]+ if (!pn || !/^C(165948|2760486|283540|840342)/.test(pn)) continue++ const pc = j.find(x => x.type === "pcb_component"+ && x.source_component_id === src.source_component_id)+ const pads = j.filter(x => x.type === "pcb_smtpad"+ && x.pcb_component_id === pc.pcb_component_id)+ const cad = j.find(x => x.type === "cad_component"+ && x.pcb_component_id === pc.pcb_component_id)+ const board = j.find(x => x.type === "pcb_board")++ const padCx = pads.reduce((s, p) => s + p.x, 0) / pads.length+ const padCy = pads.reduce((s, p) => s + p.y, 0) / pads.length+ const rot = cad.rotation.z * Math.PI / 180+ const mX = -Math.sin(rot), mY = Math.cos(rot)++ // Check 1: body forward of pads by ~BODY_FORWARD_MAG in mouth dir+ const fwd = (cad.position.x - padCx) * mX + (cad.position.y - padCy) * mY+ if (fwd < 1.5 || fwd > 3.5) {+ throw new Error(`${src.name}: body_forward=${fwd.toFixed(2)} mm; expected ~2.5.+ Body is ${fwd < 0 ? "BEHIND" : "too close to"} pads — geometry wrong.`)+ }++ // Check 2: mouth ray exits the board outline+ const ray = { x: cad.position.x + 12 * mX, y: cad.position.y + 12 * mY }+ const bx = board.center?.x ?? 0, by = board.center?.y ?? 0+ const inside = ray.x >= bx - board.width/2 && ray.x <= bx + board.width/2+ && ray.y >= by - board.height/2 && ray.y <= by + board.height/2+ if (inside) {+ throw new Error(`${src.name}: mouth points INTO the board at ${ray.x.toFixed(1)},${ray.y.toFixed(1)}.+ Cable would collide with FR4. Rotate receptacle 180° (toggle rotationOffset.z 0↔180).`)+ }++ console.log(`✓ ${src.name}: body_fwd=${fwd.toFixed(2)}mm, mouth dir (${mX.toFixed(2)},${mY.toFixed(2)}) exits board at (${ray.x.toFixed(1)},${ray.y.toFixed(1)})`)+}+```++**Wire it into your build loop:**++```bash+bunx tsci build lib/index.tsx --glbs --svgs && adom-tsci lint+```++`adom-tsci lint` does exactly the checks above (for every JLC USB-C+part in its known list) plus catches ghost chips and autorouter+silent-failures. Same exit-code gating. Prefer it over the inline+`node -e` above — it's shipped with the tool and maintained+alongside the lint rules.++Or if you only have `node` and not `adom-tsci lint`:++## Visual proof-of-placement (the toggle-and-shotlog ritual)++**Numeric lint can be gamed.** A body can satisfy `forward_dot >= 1.5`+yet still be visually misaligned with the pad geometry if a footprint+dimension is wrong. After every USB-C placement change, do this+three-step visual proof:++```bash+# 1. Top view with receptacle visible+adom-tsci view top --port 8853+adom-desktop browser_screenshot '{"sessionId":"<project>","maxWidth":1800}'+shotlog inject -c usbc-validate -d "USB-C placed on east edge, body visible cantilevered off the board" -s pup_screenshot <path>++# 2. Hide the receptacle to see the pads underneath+adom-tsci toggle-component J1 --port 8853 --hide+adom-desktop browser_screenshot '{"sessionId":"<project>","maxWidth":1800}'+shotlog inject -c usbc-validate -d "USB-C hidden; pads underneath should be flush with board east edge, all 16 pads on copper" -s pup_screenshot <path>++# 3. Un-hide+adom-tsci toggle-component J1 --port 8853 --show+```++The user reads the two screenshots side-by-side and confirms:+- Body cantilevers off the correct edge (mouth away from board)+- The pads sit inside the board (none hanging off)+- Pad row aligns with where the body's back edge is++**If you don't post both screenshots to shotlog, the user can't+verify, and you can't claim the placement works.** "The lint passes"+is necessary but not sufficient.++## Include ALL mounting holes — do NOT drop them to simplify the math++**The rule:** the TYPE-C-31-M-12 footprint must include all 4 plated+corner anchor holes + 2 drill-only support holes. Without the anchors+the connector is held by 16 tiny SMT joints only, which tear off the+board under cable insert/extract stress in well under 100 cycles. A+USB-C port that rips loose after a week is a bug, not a trade-off.++**The compensation:** tscircuit computes `pcb_component.center` as+the bbox of pads + holes. For the TYPE-C-31-M-12 layout (pads at+local y=0, holes at y ∈ {-1.04, -1.27, -5.22}), the bbox center sits+2.61 mm south of the pad centroid. To keep the cad body 2.5 mm+forward of the pad row, bump `BODY_FORWARD_MAG` from 2.5 to 2.5 ++2.61 = **5.11** in the wrapper template.++Formula for any directional connector with asymmetric holes:++```text+BODY_FORWARD_MAG = body_over_pads + |bbox_center_y − pad_row_y|+```++Measure the bbox shift by building once with holes included, reading+`pcb_component.center` from `circuit.json`, and subtracting your+known pad row y.++Filed upstream as TSCIRCUIT_FEATURE_REQUESTS §6.1: `cadModel` should+accept `boundsSource: "pads" | "all"` so we don't have to compensate+in userland. Until then, the compensation above is load-bearing.++## Failure modes → diagnosis table++| Symptom | Cause | Fix |+|---|---|---|+| Body visible but pads hanging off the board edge | `pcbX/Y` too close to the edge; pads should be INSIDE copper | Move `pcbX` inward by ~4 mm |+| Mouth points into the board (butt cantilevers off edge) | `rotationOffset.z` is wrong by 180° | Flip `rotationOffset.z` (0 ↔ 180) |+| Body drifts diagonally off the pads at non-zero rotation | You used upstream `SmdUsbC` without the rotation-compensation | Switch to the local `UsbCReceptacle.tsx` wrapper above |+| Body centered on pads, but half the body extends into the board | `positionOffset` is (0,0); forgot `BODY_FORWARD_MAG * mouth_dir` | Add the forward-offset math |+| USB-C missing from Components HUD in adom-tsci | refdes doesn't match `/^J[\d_]/i` OR no cad_component emitted | Rename to `J1`/`J_1`; check `circuit.json` for `cad_component` row |+| `pcb_autorouting_error: Failed to solve 1 nodes` after adding USB-C | USB-C pads near high-density chip pin row | Move USB-C further from adjacent chips, or grow board |++## USB-C on a MOLECULE edge (not a bare `<board>`)++When the board is an Adom `<Molecule>` (machine-pin corners for the probing+workcell), edge placement gets one extra wrinkle: **the molecule's wings+grow the real board outline beyond the nominal `size`.** A `size="128x48"`+molecule renders as a ~130.4×50.4 mm board (≈1.2 mm wing per side); large+pins grow it more. If you place the USB-C with `pcbX = nominal/2 − 4`, it+ends up several mm *inside* the real edge and the plug can't reach.++Rules that make it work:++1. **Use absolute sizing** — `size="138x58 absolute"` — so the real outer+ edge is exactly `W/2`. The size increments are coarse (…96, 128, 160…)+ and can't hit the width you need anyway; absolute gives you the precise+ edge to cantilever the mouth past.+2. **Place `pcbX ≈ W/2 − 3` and iterate ONCE** against `circuit.json`:+ - `pcb_component_outside_board_error: J1 extends outside by N mm` → pull+ J1 in by N (reduce `pcbX`).+ - confirm the rightmost USB-C pad's `gap-to-edge > 0` (no pad past edge).+ - target: body/mouth ~2–3 mm past the edge, pads ~5 mm inside.+3. **Large corner pins + `roundEdges={true}` conflict.** The rounded-corner+ arc carves out the corner where the machine pins sit, so large pins throw+ a *constant* `MP1..MP4 extends outside by ~0.48mm` (size-independent — the+ tell that it's the corners). Use **`roundEdges={false}`** with large pins.+4. Put the connector mid-edge (between the two corner pins) — it won't+ collide with the corner machine pins there.++Worked example: the OLIVER LED nameplate molecule — `size="178x58 absolute"`,+`type="4pin"`, `pinType="MachinePinLargeStandard"`, `roundEdges={false}`,+`<UsbCReceptacle mount="east" pcbX={W/2 − 4} pcbY={0}>` → SMT pads ~6.5 mm+inside, plated anchor holes ~0.8 mm clear of the edge, body/mouth cantilevers+~2.5 mm proud so a cable seats. (W/2 − 4 is the audited, canonical value;+earlier W/2 − 3.2 shoved the anchor holes ~0.03 mm from the edge — too close.)++## Upstream feature requests++See `adom-tsci/TSCIRCUIT_FEATURE_REQUESTS.md §6` for:+- cadModel.positionOffset should co-rotate with pcbRotation+- @tsci wrappers should use `{ cadModel: props.cadModel ?? DEFAULT, ...props }` so caller overrides work+- bounds.center should be opt-in pads-only for cadModel placement
guides/demo-recording.mdadded+286@@ -0,0 +1,286 @@+# adom-tsci/demo-recording — making 30-second adom-tsci demos that don't suck++**Read this before recording a demo of any adom-tsci board.** Captures+the rules, layout choices, and beat structure that produce a watchable+30-second showcase video. Written after several false starts on the+Brianna LED Nameplate demo where captions covered the 3D viewer,+captions wrapped to four lines, and the x-ray shot was missing.++## The non-negotiables++These are the rules. Violating any of them fails the demo, so check+them up-front rather than after the recording.++### 1. Workspace layout: 30% AI chat (VS Code) / 70% content (3D viewer)++A demo where the 3D viewer is anything less than 70% of the frame+looks cramped and amateur. The chat panel exists so the viewer reads+"this is being driven by an AI", but it should not compete for+attention.++**Set this before you start the recording.** Find the horizontal+split that holds VS Code on the left and the adom-tsci tab on the+right, then resize:++```bash+# 1. Find the split+adom-cli hydrogen workspace get | python3 -c '+import json, sys+d = json.load(sys.stdin)+def walk(n, parent=None):+ if n.get("type") == "leaf": return+ yield n+ yield from walk(n["first"], n)+ yield from walk(n["second"], n)+print([(s["id"], s.get("ratio")) for s in walk(d["root"])])+'++# 2. Resize the relevant split to 30/70+adom-cli hydrogen workspace resize --split-id <ID> --ratio 0.30+```++The ratio is the proportion of width given to the **first** child of+the split (VS Code is typically the first child).++### 2. Captions: ALWAYS use `-s small` for adom-tsci demos++`adom-cli hydrogen caption show "<text>" -d <sec> -p bottom -s small`++`-s large` and `-s medium` produce captions that wrap to 3-4 lines+even at moderate text length, dominate the 3D viewer, and look like a+slideshow not a demo. `-s small` produces a single-line caption at+the bottom of the workspace that doesn't compete with the content.++**Pre-flight test before recording — required.** Show one sample+caption at the size you plan to use, screenshot the panel, and+verify visually:++```bash+adom-cli hydrogen caption show "Sample caption text — does this wrap?" -d 4 -p bottom -s small+adom-cli hydrogen screenshot panel --tab-id <BRIANNA_TAB_ID> --reason "test caption size for demo"+# Read the screenshot and confirm: single line, no wrap, no scrollbar+```++If the test caption wraps OR shows a scrollbar (overflow:auto on the+caption container kicking in), shorten the text *or* drop to a+smaller size before recording. Once you're committed to the recording,+you can't fix it without re-recording.++**Caption text length budget:** about 35 characters at `-s small` to+guarantee a single line on a typical workspace width. If the demo+demands a longer line, split the thought into two beats with two+captions instead of one wrapped caption.++### 3. The x-ray shot is mandatory — and it is the Nets HUD's net-isolate-with-GlowLayer view, not "hide some components"++EE viewers literally clap when they see this. It is the differentiator+of adom-tsci's 3D viewer.++**The actual x-ray** is what `enableTraceMode()` + `selectNet(<id>)`+produce together: the FR4 substrate goes 30% alpha (you see THROUGH+the board), every routed `pcb_trace` lifts off the baked surface as a+live 3D copper polygon, every plated through-hole renders as a real+via, the SELECTED net's traces and vias get HighlightLayer + GlowLayer+bloom on top, and the GlowLayer intensity throbs ~1.5 Hz. You can see+the conductive path light up *through* the board. That's the shot.++**It is NOT "hide some chips and look at the resistor mesh underneath."**+That just exposes the baked surface — no transparency, no glow, no+vias-through-board. Don't confuse the two. Hiding components is a+useful *secondary* shot for some demos, but it's not the x-ray.++**How to drive it:**++```bash+# Open the Nets HUD — auto-glow picks the biggest power/ground net+# (or the biggest signal net if no power/ground), so the GlowLayer+# x-ray is on the moment the panel opens. v1.3.91+.+adom-tsci net --on --port $PORT+sleep 4 # let the substrate fade and the auto-pick fire++# Or: manually select a specific net to focus the glow on it.+adom-tsci net VBUS --port $PORT # by net name+adom-tsci net "J1.VBUS1 ↔ R0.pin1" --port $PORT # by id++# Close to dispose the heavy trace meshes and return to baked surface.+adom-tsci net --off --port $PORT+```++**For boards where every net is a tiny 2-port pair** (e.g. a 70-LED+nameplate where each LED-to-resistor is its own net), auto-glow still+works but the highlighted segment is only ~1 mm long and barely reads+on camera. In those cases, declare an explicit power rail+(`<net name="VBUS"/>` plus `<trace from="..." to="net.VBUS"/>` for+every endpoint) so the autorouter merges them into one mega-net the+HUD can light up gloriously. If you can't change the board, fall back+to a `tour` orbit while panning slowly — the substrate transparency+alone is striking even without a single net selected.++The non-mandatory secondary shot of "hide letter B's LEDs to expose+the baked resistor row" is fine to include, but it goes in the same+beat as the x-ray, not as a replacement for it.++### 4. Captions AND TTS — both, not either++Captions handle sound-off viewers. TTS handles eyes-elsewhere+viewers. A demo with only one is half-finished. The two carry the+same script — the caption is a tighter rendering of the same thought+spoken in TTS.++**Voice:** `en-US-AndrewNeural` (Adom house voice — see+`adom-tts` skill). Generate one MP3 per beat in parallel:++```bash+mkdir -p /tmp/<project>-tts+adom-tts say --out /tmp/<project>-tts/01.mp3 "BRIANNA. A USB-C powered desk nameplate." &+adom-tts say --out /tmp/<project>-tts/02.mp3 "Each letter is a 3 by 5 LED bitmap." &+# ... one per beat+wait+```++Mix the TTS clips into the recorded webm with ffmpeg, transcoding to+mp4 for portability:++```bash+ffmpeg -y -i recording.webm \+ -i 01.mp3 -i 02.mp3 -i 03.mp3 -i 04.mp3 \+ -i 05.mp3 -i 06.mp3 -i 07.mp3 -i 08.mp3 \+ -filter_complex "+ [1:a]adelay=0|0[a1];+ [2:a]adelay=3000|3000[a2];+ [3:a]adelay=6000|6000[a3];+ [4:a]adelay=10000|10000[a4];+ [5:a]adelay=15500|15500[a5];+ [6:a]adelay=19000|19000[a6];+ [7:a]adelay=23000|23000[a7];+ [8:a]adelay=28500|28500[a8];+ [a1][a2][a3][a4][a5][a6][a7][a8]amix=inputs=8:dropout_transition=0:normalize=0[a]" \+ -map 0:v -map "[a]" \+ -t 32 \+ -c:v libx264 -preset veryfast -crf 22 -pix_fmt yuv420p \+ -c:a aac -b:a 160k \+ demo.mp4+```++The `adelay` offsets are milliseconds and should match the beat+timing in the recording script. **Set them with the same script+that drove the recording** — copy the timestamps directly so audio+and captions stay in sync.++### 5. ~30 seconds total, 8 beats of 3-5 s each++A 30-second demo is the sweet spot — long enough to show the x-ray,+short enough that someone scrolling Slack will actually watch it.+Going over 30 s for an adom-tsci demo means you're showing too many+components. Going under 20 s means you skipped the x-ray.++The beat structure that's worked:++| Beat | Time | Caption | Camera | Purpose |+|---|---|---|---|---|+| 1 | 0-3 | "<board name> — <one-line pitch>" | iso | hero shot |+| 2 | 3-6 | "<key concept>" | top | establish board geometry |+| 3 | 6-10 | "Real CAD bodies, autorouted" | tour start | cinematic flyover |+| 4 | 10-15 | "X-ray. See through the FR4." | `adom-tsci net --on` → substrate goes transparent, traces lift off, GlowLayer pulse on the auto-selected net | **the money shot** |+| 5 | 15-18 | "<per-pixel or per-net detail>" | optionally `adom-tsci net <NAME>` for a specific rail; or close panel | secondary detail |+| 6 | 18-22 | "<USB-C / power detail>" | iso | power input |+| 7 | 22-27 | "Bottom silkscreen documents how it all works" | bottom | flip to docs |+| 8 | 27-30 | "Plug it in. <thing> happens." | iso | closing |++Beat 4's text says "X-ray" explicitly. EEs scan for that word.++## Pre-flight checklist (run before `recording start`)++```bash+# 1. Layout: 30/70+adom-cli hydrogen workspace resize --split-id <ID> --ratio 0.30++# 2. Tab is active and on the correct slingshot URL+adom-cli hydrogen workspace active-tab --tab-id <ID> --panel-id <ID>+# screenshot to confirm 3D scene is rendered, not stuck on "Loading…"+adom-cli hydrogen screenshot panel --tab-id <ID> --reason "verify 3D rendered before recording"+# Read the screenshot. Does the board fill the right pane?++# 3. Caption size test+adom-cli hydrogen caption show "Sample caption text" -d 4 -p bottom -s small+sleep 1+adom-cli hydrogen screenshot panel --tab-id <ID> --reason "verify caption single line"+# Read the screenshot. Single line? No scrollbar? If yes, proceed.++# 4. Hide the components HUD so it doesn't dominate the recording+adom-tsci eval "document.querySelector('#toggle-components-btn')?.click()" --port $PORT++# 5. Start at iso view+adom-tsci view iso --port $PORT+```++## Post-flight checklist++```bash+# 1. Frame-extract at the x-ray timestamp and confirm subset is hidden+ffmpeg -hide_banner -loglevel error -i recording.webm -ss 12 -vframes 1 /tmp/xray-check.png+# Read it. Are the right LEDs missing?++# 2. Frame-extract at every caption beat — confirm single-line, readable+for t in 1 5 12 20 25 28; do+ ffmpeg -hide_banner -loglevel error -i recording.webm -ss $t -vframes 1 /tmp/caption-$t.png+done+# Read them. Any wrapping? Any overflow? Any scrollbar pixels?++# 3. Mix TTS, transcode to mp4, verify duration is 30-32 s+ffprobe -v error -show_entries format=duration -of default=nw=1:nk=1 demo.mp4++# 4. RE-ENCODE to add keyframes every 1 s — wiki video element can't+# scrub a webm with only one keyframe at frame 0. Mandatory for any+# single-shot demo bound for `adom-wiki asset upload --asset-type video`.+ffmpeg -y -i recording.webm -c:v libvpx-vp9 -b:v 2M -deadline good \+ -g 30 -keyint_min 30 -c:a copy demo.webm+# (mp4 path: -c:v libx264 -g 30 -keyint_min 30 \+# -force_key_frames "expr:gte(t,n_forced*1)" -c:a aac demo.mp4)++# Verify keyframes landed at ~1 s intervals (should print multiple K_ lines):+ffprobe -v error -select_streams v:0 -show_entries packet=pts_time,flags \+ -of csv=p=0 demo.webm | awk -F, '$2 ~ /K/' | head++# 5. Upload to wiki page as asset-type=video+adom-wiki asset upload <type>/<slug> --asset-type video --file demo.mp4 \+ --caption "30-second demo: …"+```++## Failure modes to recognise instantly++| Symptom in the recording | Likely cause | Fix |+|---|---|---|+| Caption wraps to 2-4 lines | `-s large` or `-s medium`; long caption text | Re-record with `-s small` and ≤35-char captions |+| Caption shows a scrollbar | Caption container CSS `overflow:auto`; text too long | Shorten text, split into two beats |+| 3D viewer feels cramped | Workspace split is 50/50 or worse | Resize to 30/70 before recording |+| Skipped the x-ray shot (only hid components) | Confused "x-ray" with "hide subset". Real x-ray = substrate-transparent + GlowLayer-on-selected-net via the Nets HUD | Re-record using `adom-tsci net --on` (auto-glow picks the biggest power/ground net) |+| Substrate doesn't go transparent | Trace mode never enabled | `adom-tsci net --on` (or click the ⌇ toolbar button) |+| Substrate transparent but no glow on any net | No net selected; auto-glow disabled or failed to pick | Click the ✦ button in the Nets HUD header to re-arm; or `adom-tsci net <NAME>` to pick manually |+| Selected net glows but the highlight is barely visible | Every net on this board is tiny (2-port LED-R pair). Single-segment glow doesn't read on camera | Add an explicit `<net name="VBUS"/>` + connect every endpoint to it so the autorouter merges into one mega-rail |+| Audio out of sync | `adelay` offsets don't match beat timing | Re-derive offsets from the recording script's `sleep` accumulation |+| Camera doesn't actually move | `adom-tsci view` called before tab was active | Active-tab the recording tab BEFORE the demo script |++## Tooling stack++- **adom-cli hydrogen recording start/stop** — produces a webm+- **adom-cli hydrogen caption show** — bottom overlay, recordable+- **adom-cli hydrogen workspace resize** — set the 30/70 split+- **adom-tsci view / tour / toggle-component** — drive the 3D viewer+- **adom-tts say** — Andrew Neural TTS clip per beat+- **ffmpeg** — mix TTS audio onto the webm, transcode to mp4++The demo workflow lives entirely on adom CLIs + ffmpeg. No GUI+post-production needed for a 30-second showcase.++## See also++- **Brianna LED Nameplate** — first board this guide was written+ against. Wiki page:+ `https://wiki-ufypy5dpx93o.adom.cloud/molecules/brianna-led-nameplate`.+ Demo video at `/static/apps/brianna-led-nameplate/brianna-demo.mp4`+ is the canonical example.+- **adom-tts** — TTS skill, voice selection, pronunciation overrides.+- **video-post** — for longer-form post-production (speedup, voice+ re-recording, multi-clip stitching). Not needed for the 30-second+ case described here.
guides/export-wiki.mdadded+241@@ -0,0 +1,241 @@+# adom-tsci/export-wiki — publishing a tscircuit molecule to the Adom Wiki++**Read this before running `adom-tsci export-wiki` on any project.**+The CLI is deliberately opinionated: it ships the interactive 3D+viewer, the source bundle, the plan.md, AND a comprehensive body+markdown to the wiki page in one shot. Skipping any of those pieces+makes the page useless to a future viewer (human or AI) — the goal+is "another agent can pick up exactly where this one left off" with+nothing missing.++## What the CLI does, in order++```+adom-tsci export-wiki [<project>] --slug <wiki-slug> \+ --page-type molecules \+ --label "Live 3D viewer" \+ [--rebuild] [--texture-resolution N] [--no-upload]+```++1. **Confirms `dist/lib/index/circuit.json` exists.** Skip-rebuild is+ the default — `bunx tsci build` can take 10 minutes on dense+ boards. Pass `--rebuild` to force.++ **Fallback-dist search** (1.3.91+): if the in-place dist is missing,+ the CLI looks for an existing dist at known parallel locations+ before erroring out. Order: `~/project/adom-tsci-projects/<basename>/dist`,+ `~/project/adom-tsci/examples/<basename>/dist`,+ `~/project/tscircuit-projects/<basename>/dist`. Picks the freshest+ by `circuit.json` mtime. Many canonical molecules live in two+ places (the example-library copy and the local scratch-build+ copy); only one has a fresh dist at any given moment, and there's+ no reason to spend 10 minutes rebuilding when the other copy is+ already there.++ **The reflex when a build seems missing:** `ls+ ~/project/adom-tsci-projects/<name>/dist/lib/index/circuit.json`+ FIRST. Only rebuild if neither copy has a fresh dist. The CLI does+ this automatically now, but the same rule applies to any AI helper+ debugging "why doesn't this project have a dist?" — search before+ rebuilding.+2. **Resolves the GLB.** Default = `dist/lib/index/3d.glb` (1024-px+ texture). Pass `--texture-resolution N` to rebake at higher res+ (4096 typical for 100–200 mm boards; first bake ~75 s, cache hit+ ~190 ms).+3. **Stages the viewer bundle** at `/tmp/adom-tsci-export-<slug>/`:+ - `index.html` — `shell.html` rewritten for static hosting (fetch+ monkeypatch, walkthrough autoplay, all `glb`-style URLs+ remapped to relative paths).+ - `js/adom-3d-viewer.min.js`, `js/tscircuit-alphabet.js`,+ `js/environmentSpecular.env`, `adom.css`, `favicon.svg` —+ embedded into the binary, copied out at staging time.+ - `3d.glb`, `circuit.json`, `walkthrough.json`, `pcb.svg`,+ `schematic.svg` — the project's build artifacts.+4. **Tarballs the viewer bundle** as+ `<slug>-tsci-viewer.tar.gz`. Uploads via `adom-wiki asset upload+ --asset-type tsci-viewer`. The wiki extracts to+ `/static/<page-type-plural>/<slug>/tsci-viewer/<label-slug>/` and+ auto-renders an iframe section on the page.+5. **Builds a separate source bundle** at+ `/tmp/<slug>-source.tar.gz` containing every file an agent would+ need to recreate the molecule:+ - `lib/` (recursive)+ - `package.json`+ - `tscircuit.config.json`+ - `tsconfig.json`+ - `walkthrough.json`+ - `.npmrc`+ - **`plan.md`** (or `PLAN.md`) — the design brief. Required by+ convention; see "plan.md" below.+ - `README.md` (if present)+ Excluded: `dist/` (build output) and `node_modules/` (deps).+ Uploaded as a `file` asset.+6. **Uploads `plan.md` standalone** as a separate `file` asset so the+ wiki can render a one-click link to it without untar.++Failures on steps 5 / 6 are non-fatal — the viewer publish in step 4+is the load-bearing one and lands first. If a transient wiki failure+blocks the source-bundle upload, the CLI emits a hint with the retry+command.++## Walkthrough autoplay++The static export of `shell.html` registers a small bootstrapper that+**fires `walkthroughStart()` ~2 s after the page loads**. This is on+by default for the wiki context — the walkthrough is the canonical+"tour the board" experience for an external viewer who isn't going+to know to click anything. It can be opted out with+`?autoplay=off` on the iframe URL if a future wiki template wants to+defer firing it.++The walkthrough is loaded from `walkthrough.json` in the bundle. If+the project has no `walkthrough.json`, the autoplay no-ops gracefully+(no walkthroughStart call until the array is populated). Generate+one with:++```bash+adom-tsci walkthrough-gen # auto-generate from circuit.json+```++…or hand-edit `walkthrough.json` directly. The `_meta` block has a+`do_not_edit_by_hand` field — set it to a plausible-looking string+(e.g. "Hand-edited intentionally") to keep `walkthrough-gen` from+overwriting your edits.++## plan.md — the design brief++**Every molecule publish should include `plan.md` at the project+root.** plan.md is the original prompt the project was authored from,+plus the refinements that came out during build. It carries forward+to the wiki so a future agent skimming the page can answer:++- What was the user actually asking for?+- What design decisions were made and why?+- What's intentionally out of scope?++If the project doesn't have a plan.md when you run `export-wiki`,+**create one before publishing**. Reconstruct it from the original+prompt + the design conversation; check it into the project root.+Skeleton:++```markdown+# Plan — <Project Name>++## Original prompt++> <copy the user's first ask verbatim>++## Refinements during the build++- <each meaningful refinement, one bullet per>++## Hardware decisions++- <each component choice with rationale>++## Out of scope (deliberate)++- <what was explicitly NOT built and why>++## Build → publish workflow++```++(See `adom-tsci-projects/Brianna-LED-Nameplate/plan.md` for a full+example.)++The CLI uploads `plan.md` standalone (one-click link on the page)+AND inside the source bundle (preserved alongside the rest of the+project source).++## The body markdown — comprehensive, AI-readable++The wiki page's `content` field — the markdown body — must give a+downstream agent everything they'd need to continue work without+reading external docs. **The CLI does not write the body for you;+you produce it via `adom-wiki page edit --field content` (or as part+of `page publish`).** Required sections:++| Section | What it captures |+|---|---|+| **Brief** (above the fold) | One-line elevator pitch. Same as the page brief. |+| **Live 3D viewer** | Pointer to the iframe, mention `?autoplay=off` opt-out. |+| **How it works** | The circuit story: what each block does, key constants, font/layout source. |+| **Hardware summary** table | Block / Part / JLCPCB / Notes for every refdes family. |+| **Mechanical** | Board dims, USB-C edge, workcell-pin info, silkscreen choices. |+| **Source layout** | The project's directory tree with one-line per file. |+| **Build commands** | The exact bash to clone → install → build → preview → publish. |+| **Color options / variants** (if applicable) | What's parameterised and how to retune. |+| **Known issues / lessons learned** | Things that bit during the build, in case they come up again. |+| **Build provenance** | Author, tooling versions, walkthrough authorship. |+| **Files attached** | Two sub-lists: **Source — recreate the molecule** (plan.md, source bundle) and **Build outputs** (GLB, SVGs, demo video). |++**Rule:** if a future agent has to ask the human user any factual+question about the project that they could have answered by reading+this page, the body is incomplete. Treat it as a writeup, not a+README stub.++## Hero image++`adom-wiki page publish` requires `--hero-image <path>` (the wiki+enforces it for non-app pages). The hero image is **the thumbnail**+that shows up on the molecule index page — heavy interactive viewers+can't be embedded in index lists, so a static iso render is the+right artifact for that role.++For an adom-tsci-published molecule:++- The **index/listing page** uses the `--hero-image` PNG as the+ thumbnail.+- The **full molecule page** has the interactive `tsci-viewer`+ iframe as its hero (rendered automatically by the wiki when a+ `tsci_viewer` asset is present).++Take the hero image with `adom-cli hydrogen screenshot panel+--tab-id <brianna-tab>` after setting the camera to `iso` view and+hiding the components HUD. ~1500 px wide is plenty.++## Verifying the publish++After `export-wiki` returns `OK: published …`, open the molecule URL+in a browser:++```+https://wiki-ufypy5dpx93o.adom.cloud/molecules/<slug>+```++Confirm visually:++1. **Walkthrough auto-plays** within ~2 s of page load. Step text+ appears at the bottom of the iframe with Back/Next controls.+2. **3D scene is rendered** — the board (not the loading screen) is+ visible.+3. **Source bundle download works** — click the `*-source.tar.gz`+ link in the body, untar, confirm `lib/index.tsx` and `plan.md`+ are present.+4. **plan.md link works** — opens directly without untar.++If the walkthrough doesn't fire, check the iframe URL has no+`?autoplay=off`, and that `walkthrough.json` was non-empty in the+bundle (`tar -tzf <bundle>.tar.gz | grep walkthrough`).++## Troubleshooting++| Symptom | Cause | Fix |+|---|---|---|+| `dist/lib/index/circuit.json not found` | No build artifacts | Pass `--rebuild` or run `bunx tsci build` first |+| `--texture-resolution N` warns "no rebake-glb.mjs in cache" | First-time use without prior `adom-tsci start` | Run `adom-tsci start .` once to populate the cache, then re-export |+| `bundle is over 95 MB` warning | Big GLB (very high texture res) or many extra files | Drop `--texture-resolution` to 4096 or 2048 |+| `adom-wiki upload failed: page-type mismatch` | Slug exists at a different page-type | Either pass the matching `--page-type`, OR delete the existing page first (`adom-wiki page delete <type>/<slug>`) |+| Source-bundle upload fails but viewer publish succeeds | Transient wiki error | Re-run `adom-wiki asset upload <page>/<slug> --asset-type file --file /tmp/<slug>-source.tar.gz` manually — the CLI prints this hint |+| Walkthrough doesn't auto-play on the wiki page | walkthrough.json missing or empty | Run `adom-tsci walkthrough-gen` in the project, hand-edit if needed, re-export |++## See also++- `adom-tsci/guides/demo-recording.md` — making a 30-second narrated+ demo video to attach to the page (separate workflow; the live+ viewer is now the primary artifact, demo video is supplementary).+- `adom-tsci/guides/adom-usbc.md` — USB-C placement rules for the+ molecule before you publish.+- `adom-wiki` skill — the underlying CLI for asset uploads and page+ edits.
install.sh+23−15@@ -1,15 +1,23 @@-#!/bin/bash-set -e-PKG_DIR="$(cd "$(dirname "$0")" && pwd)"-SKILLS_DIR="$HOME/.claude/skills"-mkdir -p "$SKILLS_DIR/adom-tsci"-if [ -f "$PKG_DIR/SKILL.md" ]; then- cp "$PKG_DIR/SKILL.md" "$SKILLS_DIR/adom-tsci/SKILL.md"-fi-for skill_dir in "$PKG_DIR/skills"/*/; do- [ -d "$skill_dir" ] || continue- skill_name=$(basename "$skill_dir")- mkdir -p "$SKILLS_DIR/$skill_name"- cp -r "$skill_dir"* "$SKILLS_DIR/$skill_name/"-done-echo "Installed adom-tsci"+#!/usr/bin/env bash+# adom-tsci installer (adompkg / wiki.adom.inc). Pulls the prebuilt Linux+# x86_64 binary from the canonical Adom Wiki release asset, installs it to+# ~/.local/bin, then deploys the bundled SKILL.md + guides via `adom-tsci install`.+set -euo pipefail++BIN_URL="https://wiki.adom.inc/api/pages/adom/adom-tsci/releases/latest/download/adom-tsci?platform=linux"+DEST="$HOME/.local/bin/adom-tsci"++mkdir -p "$HOME/.local/bin"+echo "→ downloading adom-tsci binary…"+curl -fsSL "$BIN_URL" -o "$DEST"+chmod +x "$DEST"+echo "→ installed binary to $DEST ($("$DEST" --version 2>/dev/null || echo '?'))"++# Deploy SKILL.md + guides (adom-tsci ships its own installer for these).+"$DEST" install >/dev/null 2>&1 || echo " (skill deploy skipped — run 'adom-tsci install' manually if needed)"++case ":$PATH:" in+ *":$HOME/.local/bin:"*) ;;+ *) echo " NOTE: add ~/.local/bin to PATH: export PATH=\"\$HOME/.local/bin:\$PATH\"" ;;+esac+echo "✓ adom-tsci installed. Try: adom-tsci start <project-dir>"
package.json+31−4@@ -1,11 +1,38 @@ { "slug": "adom-tsci",- "version": "1.0.0",+ "version": "1.4.13", "type": "app",- "description": "Interactive tscircuit preview in a Hydrogen webview — first-class 3D / PCB / Schematic tabs, Components & Nets HUDs, an auto-glow x-ray view, Inspect, Measure, a Walkthrough Demo, and a one-click `e",+ "description": "Interactive tscircuit board viewer in a Hydrogen webview \u2014 first-class 3D / PCB / Schematic tabs, an Inspect tool, a Fusion-style Measure tool, net x-ray, guided Walkthrough demos, one-click autorouter re-run, and auto high-res board-surface texture. Fully AI-drivable (CLI + HTTP + JS eval). Also publishes molecules/boards to the Adom Wiki.", "dependencies": {},+ "hero": {+ "type": "image",+ "path": "adom-tsci-billboard.png"+ },+ "tags": [+ "tscircuit",+ "pcb",+ "3d-viewer",+ "schematic",+ "eda",+ "inspect"+ ], "scripts": { "install": "./install.sh", "uninstall": "./uninstall.sh"- }-}+ },+ "install": {+ "binary_name": "adom-tsci"+ },+ "files": [+ "SKILL.md",+ "install.sh",+ "uninstall.sh",+ "README.md",+ "package.json",+ "VERSION",+ "adom-tsci-billboard.png",+ "guides/adom-usbc.md",+ "guides/demo-recording.md",+ "guides/export-wiki.md"+ ]+}
page.json+34−8@@ -4,8 +4,15 @@ "slug": "adom-tsci", "title": "adom-tsci — Tscircuit Board Viewer", "brief": "Interactive tscircuit preview in a Hydrogen webview — first-class 3D / PCB / Schematic tabs, Components & Nets HUDs, an auto-glow x-ray view, Inspect, Measure, a Walkthrough Demo, and a one-click `exp",- "version": "1.3.94",- "tags": [],+ "version": "1.4.13",+ "tags": [+ "tscircuit",+ "pcb",+ "3d-viewer",+ "schematic",+ "eda",+ "inspect"+ ], "license": "MIT", "discovery_triggers": [ "tscircuit preview",@@ -33,10 +40,7 @@ ], "discovery_pitch": "Author tscircuit boards in TSX, preview the result with a full 3D viewer (Components/Nets HUDs, Walkthrough, Inspect, Measure, x-ray view), and one-shot publish the molecule to the Adom Wiki with the live interactive viewer + source bundle + plan.md attached.", "install": {- "binary_name": "adom-tsci",- "install_dir": "",- "install_hint": "",- "version_cmd": ""+ "binary_name": "adom-tsci" }, "readme": "# adom-tsci — Tscircuit Board Viewer\n\nAn Adom-Docker CLI that gives you **first-class 3D / PCB / Schematic\ntabs** for a tscircuit project inside a Hydrogen webview, plus the\ncanonical \"ship this molecule to the wiki\" flow.\n\n## What it does\n\n- **3D tab** — full Babylon-based viewer (orbit, zoom, ortho/perspective)\n with the tscircuit GLB loaded, ground shadow, IBL environment.\n- **Components HUD** — every chip / passive / testpoint / machine pin\n the GLB ships, grouped by kind. Click a row to hide that component\n and see the traces underneath.\n- **Nets HUD** — every routed pcb_trace lifts off the baked surface\n as a live polygon mesh on top of an x-ray-transparent substrate. The\n `✦` button auto-selects the biggest power/ground net the moment you\n open the panel — that's the gorgeous GlowLayer x-ray shot the\n walkthrough demos.\n- **Walkthrough Demo** — narrated tour of the board with cinematic\n camera flyovers. Drives by `walkthrough.json` you can hand-edit per\n project. Auto-plays in static-export mode (wiki embed).\n- **Inspect / Measure** — Fusion 360-style smart picking. Hover any\n pad/hole/chip/silkscreen/board for an info card; pin two clicks for\n a measurement.\n- **Re-run autorouter** — one-click hot reload, regenerates traces\n + GLB without restarting tsci dev.\n- **export-wiki** — bundle the running viewer + source + plan.md and\n publish to the Adom Wiki as a molecule page.\n\n## Install\n\nAuto-discover-friendly, single command:\n\n```bash\ncurl -fsSL https://wiki-ufypy5dpx93o.adom.cloud/static/apps/adom-tsci/adom-tsci -o /tmp/adom-tsci \\\n && chmod +x /tmp/adom-tsci \\\n && sudo install -m 0755 /tmp/adom-tsci /usr/local/bin/adom-tsci \\\n && adom-tsci install\n```\n\n`adom-tsci install` deploys SKILL.md + the `adom-usbc`,\n`demo-recording`, and `export-wiki` guides to `~/.claude/skills/`.\n\n## Quick start\n\n```bash\n# Boot the preview against any tscircuit project (lib/index.tsx convention):\ncd ~/project/adom-tsci-projects/My-Board\nadom-tsci start . --port 8861 --tsci-port 3061 --texture-resolution 4096\n\n# Drive the running viewer programmatically:\nadom-tsci view top\nadom-tsci net --on # x-ray view, auto-glow on biggest net\nadom-tsci walkthrough start # narrated tour\nadom-tsci toggle-component U1 --hide # see traces under the chip\n\n# Publish to the wiki as a molecule:\nadom-tsci export-wiki . \\\n --slug my-board \\\n --page-type molecules \\\n --label \"Live 3D viewer\"\n```\n\n`export-wiki` bundles the static viewer + a tarball of the project\nsource (lib/, package.json, walkthrough.json, **plan.md**) + uploads\nthe plan.md standalone so a future agent picking up your molecule\nsees both the brief and the source in one place.\n\n## See also\n\n- `adom-tsci/guides/adom-usbc.md` — USB-C placement / rotation / lint\n- `adom-tsci/guides/demo-recording.md` — making a 30-second narrated\n demo video of a board\n- `adom-tsci/guides/export-wiki.md` — the molecule publish flow\n (hero image, plan.md, body completeness rules)\n", "author": {@@ -47,10 +51,32 @@ "visibility": { "public": true },- "hero": null,+ "hero": {+ "type": "image",+ "path": "adom-tsci-billboard.png"+ }, "sample_prompts": [], "metadata": {}, "created_at": "2026-05-28T05:29:11.948Z", "updated_at": "2026-05-28T05:29:11.948Z",- "skills": []+ "skills": [],+ "description": "Interactive tscircuit board viewer in a Hydrogen webview — first-class 3D / PCB / Schematic tabs, an Inspect tool, a Fusion-style Measure tool, net x-ray, guided Walkthrough demos, one-click autorouter re-run, and auto high-res board-surface texture. Fully AI-drivable (CLI + HTTP + JS eval). Also publishes molecules/boards to the Adom Wiki.",+ "dependencies": {},+ "scripts": {+ "install": "./install.sh",+ "uninstall": "./uninstall.sh"+ },+ "files": [+ "SKILL.md",+ "install.sh",+ "uninstall.sh",+ "README.md",+ "package.json",+ "VERSION",+ "adom-tsci-billboard.png",+ "guides/adom-usbc.md",+ "guides/demo-recording.md",+ "guides/export-wiki.md"+ ],+ "org": "adom" }
uninstall.sh+7−4@@ -1,4 +1,7 @@-#!/bin/bash-set -e-rm -rf "$HOME/.claude/skills/adom-tsci"-echo "Uninstalled adom-tsci"+#!/usr/bin/env bash+# adom-tsci uninstaller (wiki-v2 / adompkg).+set -euo pipefail++rm -f "$HOME/.local/bin/adom-tsci"+rm -rf "$HOME/.claude/skills/adom-tsci" "$HOME/.claude/skills/adom-tsci-build"+echo "✓ adom-tsci removed (binary + skills)."