← Commit history

Migrate asset from Helium wiki

Kyle Bergstedt ·0d6e3bbc18 ·4mo ago ·parent 3d64cc4
1 file changed +1361
SKILL.mdadded+1361
@@ -0,0 +1,1361 @@+---+name: adom-tsci+description: >+  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++An Adom app that gives you **first-class 3D / PCB / Schematic tabs** for a+tscircuit project inside a Hydrogen webview panel, plus a one-click+**Re-run autorouter** button, a background **stale-deps check** against the+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),+**every interaction goes through the `adom-tsci` CLI.** Do not reach+for `adom-cli hydrogen workspace add-tab`, raw `curl` to the slingshot,+`fetch('/api/...')` inside a shell, or any other workaround. The CLI+has a subcommand for every runtime concern — including multi-instance+tab management, JS eval inside the webview, console tailing, camera+control, per-component visibility, and autorouter rerun.++**The reflex:** when you think "I need to touch the running preview",+run `adom-tsci --help` first and find the subcommand. If the CLI+doesn't expose it, that's a **bug in the tool, not a reason to+hand-roll bash** — stop and extend the CLI instead. (If you need to+ship a temporary workaround, leave a `TODO: CLI gap — add <flag>`+breadcrumb in the skill so the gap gets closed.)++Quick-reference table — the subcommand you want is probably here:++| I want to… | Command |+|---|---|+| Spin up a preview (1st instance) | `adom-tsci start <dir>` |+| Spin up a **2nd / Nth** preview (no tab collision) | `adom-tsci start <dir> --port 8851 --tsci-port 3041 --tab-name "adom-tsci <ProjectName>"` |+| Open / re-attach the webview tab on an existing slingshot | `adom-tsci open --port <N> [--tab-name "..."] [--display-icon mdi:led-on]` |+| 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>` |+| 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>` |+| Hide a chip to see traces under it | `adom-tsci toggle-component U1 --hide` |+| List components in the loaded GLB | `adom-tsci list-chips` |+| Run JS inside the webview | `adom-tsci eval "<js>"` |+| Read the browser console | `adom-tsci console [--follow] [--tail N] [--level log,warn,error]` |+| Health / status | `adom-tsci health [--port <N>]`, `adom-tsci status [--port <N>] [--json]` |+| Upgrade tscircuit packages in the target project | `adom-tsci upgrade [<dir>]` |++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+external service, no port 8770 probe, no "AV not reachable" states.+The old Basic3dView iframe and the `av_bridge` module were removed. The+3D view is fully **AI-drivable from the CLI**: every toolbar button has+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++Everything below is in the **v0.4.0 / v0.4.1** binary that's on the+wiki right now:++- **Inspect tool** (🔍 / keybind `I`). Hover any feature on the board+  for a labelled info card: chip refdes + footprint + pin count + JLCPCB+  part #; pad → pin name + net + "Connects to MC1.pin1, TP1.pin1" via+  N direct traces + solder-mask state; plated hole → drill/outer+  diameters + role (mechanical-only vs wired); via → drill/pad/bridged+  layers/net; silkscreen → text + layer + parent; board → dims ++  thickness + layer count + baked texture resolution. Click to pin,+  Esc unpins. Fields tscircuit doesn't emit today (MPN, datasheet,+  surface finish) render as `—` with a tooltip explaining and linking+  to upstream feature requests filed in `TSCIRCUIT_FEATURE_REQUESTS.md`.+- **Measure tool** (📏 / keybind `M`). Fusion 360-style smart picking+  (pad / hole / chip / pin / edge), precision + secondary units+  (mm / inches / mils), vertex snap. Hover highlights in cyan; Inspect+  uses amber so you can tell them apart.+- **Walkthrough Demo** (🎬). A narrated 10-step tour. Auto-advances+  based on narration text length; orbit auto-pauses; **3D flyover+  animations** on the contact-ring + testpoint steps (6 waypoints per+  step, ease-in-out, so the camera visibly scans instead of staring).+  CLI drivable: `adom-tsci walkthrough start | next | prev | pause |+  resume | close | status`.+- **Board-surface texture resolution** — tscircuit bakes silkscreen ++  copper pads + annular rings + solder mask into a single PNG per+  board face, hardcoded at 1024. On big boards that's unreadable at+  zoom. `adom-tsci start --texture-resolution 8192` re-bakes the GLB+  via `circuit-json-to-gltf` and caches by circuit.json content hash+  (first bake ~75s; cache hit ~190ms). Current resolution is shown on+  the "Board Surface → Baked" row of the Components HUD.+- **Components HUD group toggles.** Master eye per group (●/○/◐).+  **Test points are hidden by default** — tscircuit's default cuboid+  placeholders add noise without detail; toggle them on when needed.+- **Custom monochrome SVG toolbar icons** per the Adom brand rule — no+  emoji anywhere. Ruler with ticks for Measure, magnifier with a+  data-point for Inspect, clapperboard with play arrow for+  Walkthrough.+- **Robust shutdown + PID lock** (v0.4.1). `adom-tsci start` scans+  `/proc` for stale `bun tsci dev` processes from prior runs and+  SIGTERM+SIGKILL them before spawning. Atomic PID lock at+  `/tmp/adom-tsci-<port>.pid` refuses to double-start on the same+  port. Fixes the "15 orphan tsci devs from 15 crashed restarts"+  situation that used to compound.++## Keyboard shortcuts++| Key | Action |+|---|---|+| `M` | Toggle Measure tool |+| `I` | Toggle Inspect tool |+| `G` | Toggle ground plane |+| `W` | Toggle wireframe |+| `C` | Toggle Components panel |+| `Space` | Pause / resume walkthrough |+| `Esc` | Unpin Inspect card / close tool |++Shortcuts only fire when the 3D tab is active and no input field is+focused.++## 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` (author: `ray`,+22 kB). Everything this tool 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, the SN65HVD230 reference — traces back to Ray's guide. If+you're building more than a single-chip molecule (connector families,+molecule generators, review servers, DRC tooling), **go read Ray's+guide**. This app doesn't replace any of it; it just makes the "show me+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+# 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++# 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 .+# …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++```+┌──────────────────────────────────────────────────────────────────────────┐+│ [3D*]  [PCB]  [Schematic]   GLB: 8:56:56 AM   ⟳ Re-run autorouter  │+│                                                ─────   tsci live    │+├────┬─────────────────────────────────────────────────┬───────────────────┤+│ ⌂  │                                                 │  COMPONENTS       │+│ T  │                                                 │   U1    CHIP      │+│ F  │               (three.js canvas —                │   U2    CHIP      │+│ R  │            self-contained, no AV)               │   C1    CAPACITOR │+│ I  │                                                 │   Y1    CRYSTAL   │+│ ⎚  │                                                 │   TP1   TESTPOINT │+│ ▭  │                                                 │   ...             │+└────┴─────────────────────────────────────────────────┴───────────────────┘+```++**Primary tabs** (first-class, big, prominent):++- **3D (default).** A full-bleed canvas rendered by the real+  **Adom 3D Viewer** (`adom-3d-viewer.min.js`, the same Babylon-based+  engine the rest of Adom uses for 3D), bundled into the adom-tsci+  binary so there is **no AV service dependency** — no port 8770+  probe, no "AV not reachable" states. Shadows, IBL environment,+  transparent ground plane all work out of the box.++  Everything else floats over the canvas so it doesn't steal real+  estate:+  - **Floating toolbar** (top-centre, draggable via the `⋮⋮` grip):+    ⌂ home (iso), ◱ frame, T top, F front, R right, I iso,+    ▭ ground, ⎚ wireframe, ⊡ ortho/perspective, 📏 measure.+  - **Measure tool**: click ``📏`` then click two points on the+    model to get the distance between them in millimetres, with an+    on-screen label at the midpoint. Click a third time to clear.+  - **Floating component panel** (top-right, draggable via header,+    collapsible with `—`, dismissible with `×`, restorable via the+    `☰` button): every chip / passive / testpoint / machine contact+    from `circuit.json`, listed with kind. **Click a row to hide+    that component** — the "hide U1 to see the traces routed under+    the FPGA" use case. Click "all on" to restore.++  Mesh → component-name mapping: tscircuit's GLB writer names meshes+  `Box0_primitive0 / OBJBox12_primitive1` (no component names in the+  scene graph). The UI fetches `/circuit.json`, auto-detects the GLB+  unit scale (meters vs mm), probes orientation via mounting-post+  corner positions, and groups meshes by nearest-neighbour assignment+  within a per-kind tolerance (chip=10mm, testpoint=1.2mm,+  contact=2mm, …). Meshes outside tolerance land in a catch-all+  `Board` group so the board outline and any unmatched geometry is+  still toggleable.+- **PCB.** Inline `dist/lib/index/pcb.svg` in a panzoom container. Scroll+  wheel zooms toward cursor, drag to pan. Lazy-loaded on first click.+- **Schematic.** Inline `dist/lib/index/schematic.svg`, same panzoom.+  Lazy-loaded on first click.++**Secondary tab:**++- **tsci live.** The raw tscircuit RunFrame served through the slingshot+  proxy. Escape hatch for features tscircuit ships in the future that+  we haven't wrapped yet. Dimmed via CSS `filter: brightness(0.78)`+  because RunFrame's light theme is hardcoded and would be blinding+  inside a dark editor.++## The Re-run autorouter button++Top-right of the header. Click it to:++1. **Touch `lib/index.tsx` via the slingshot → `tsci dev` file server.**+   A no-op `POST /api/files/upsert` fires a `FILE_UPDATED` event,+   `tsci dev` re-evaluates the circuit (which re-runs the autorouter),+   and RunFrame (the `tsci live` tab) refreshes its in-memory view.+2. **Spawn `bunx tsci build lib/index.tsx --glbs --svgs` in the+   background.** This regenerates `dist/lib/index/{3d.glb,pcb.svg,+   schematic.svg}` on disk, which the first-class 3D / PCB / Schematic+   tabs poll for (`/glb/meta` every 3 s). Within ~30 s the tabs+   re-render with the new output.+3. **Shift-click for a clean re-run.** Holding Shift while clicking+   deletes `manual-edits.json` first, so any interactive trace edits+   made in RunFrame's PCB editor are discarded and the autorouter runs+   from scratch.++**Same feature from the CLI:**++```bash+adom-tsci rerun                # re-run autorouter, preserve manual edits+adom-tsci rerun --clean        # also delete manual-edits.json first+```++## Stale-deps check++On `adom-tsci start`, a background thread queries+`https://registry.npmjs.org/@tscircuit/cli/latest` and compares against+the `version` field in+`<project>/node_modules/@tscircuit/cli/package.json`. If the installed+version is behind the latest, you see a `HINT:` line:++```+HINT: @tscircuit/cli is out of date (installed 0.1.1226, latest 0.1.1234).+Run `adom-tsci upgrade` to refresh.+```++The check is **non-fatal** (3-second timeout, no network → no warning)+and **never blocks startup**. tscircuit does daily releases — this just+nudges you when you're behind.++`adom-tsci upgrade [project-dir]` shells out to+`bun update --latest` in the target directory, upgrading every dep to+its latest version (not just tscircuit). Restart `adom-tsci start` for+changes to take effect.++## Credit to the slingshot: the 127.0.0.1 problem++`tsci dev` emits a hardcoded absolute URL in its HTML shell:++```html+<script>+  window.TSCIRCUIT_FILESERVER_API_BASE_URL = "http://127.0.0.1:3040/api";+</script>+<script type="module" src="/standalone.min.js"></script>+```++Inside a Hydrogen webview iframe, `127.0.0.1` resolves to the+**user's machine**, not the container, so every fetch fails and+RunFrame hangs at `loading...` forever.++`adom-tsci`'s slingshot:++1. Fetches the shell HTML from `127.0.0.1:3040/`.+2. Rewrites the two hardcoded references to **relative paths**:+   `TSCIRCUIT_FILESERVER_API_BASE_URL = "api"` and+   `<script src="standalone.min.js">`.+3. Proxies `/runframe/api/*` and `/runframe/standalone.min.js` to+   `127.0.0.1:3040`, **preserving HTTP method, body, and query+   string**. A v0.1 bug dropped query strings on forward, which broke+   `files/get?file_path=...`; fixed in v0.2 with a full method-aware+   proxy.++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:` /+`Hint:` prefixes, both human and `--json` output).++| Command | Purpose |+|---|---|+| `adom-tsci start <dir> [--port 8850] [--tsci-port 3040] [--no-open] [--texture-resolution N]` | Spawn `tsci dev`, start the slingshot + shell server, open the webview tab. `--texture-resolution N` re-bakes the board-surface texture at N×N (default 1024; recommend 2048 for 60-100 mm boards, 4096 for 100-200 mm, 8192 for >200 mm) |+| `adom-tsci stop` | Clean shutdown of the preview server + the `tsci dev` process group (and the descendant sweep + PID lock release — no more orphan bun processes) |+| `adom-tsci walkthrough <start\|next\|prev\|pause\|resume\|close\|status>` | Drive the Walkthrough Demo from the CLI / AI |+| `adom-tsci status [--json]` | GET `/state` from the running instance |+| `adom-tsci open` | Re-add or navigate to the webview tab |+| `adom-tsci reload` | Force the 3D / PCB / Schematic tabs to re-poll the build outputs |+| `adom-tsci rerun [--clean]` | Re-run the tscircuit autorouter (optionally deleting `manual-edits.json`) |+| `adom-tsci upgrade [dir]` | `bun update --latest` in the target tscircuit project |+| `adom-tsci health` | Probe `tsci_reachable` |+| `adom-tsci install` | Drop `SKILL.md` into `~/.claude/skills/adom-tsci/` |+| `adom-tsci --version` | Print version |++**Remote-control the 3D viewer from the CLI** (no clicking):++| Command | Purpose |+|---|---|+| `adom-tsci view <preset>` | Camera preset: 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` |+| `adom-tsci toggle-component <NAME> [--hide \| --show]` | Show/hide a single component — e.g. `adom-tsci toggle-component U1 --hide` to see traces under the FPGA |+| `adom-tsci list-chips` | List the components the 3D viewer discovered in the current GLB |+| `adom-tsci eval "<js>"` | Run a JS snippet inside the running webview, print the result |+| `adom-tsci console [--follow] [--tail N] [--level log,warn,error]` | Print the JS console log forwarded from the webview (all console.\* + uncaught errors) |++## HTTP API++Every CLI verb is a thin wrapper over an HTTP endpoint. Curl them+directly if you want to script without the CLI. All on+`127.0.0.1:8850` by default.++| Route | Method | Purpose |+|---|---|---|+| `/` | GET | The tabbed shell HTML |+| `/runframe/` | GET | Slingshot root: rewrites tsci dev's HTML shell to relative paths |+| `/runframe/api/*` | any | Full HTTP proxy to `127.0.0.1:<tsci-port>/api/*` (method, body, query string preserved) |+| `/runframe/standalone.min.js` | GET | Streamed proxy of the 8.6 MB RunFrame bundle |+| `/adom.css`, `/favicon.svg` | GET | Static brand assets |+| `/glb` | GET | Streams `<project>/dist/lib/index/3d.glb` |+| `/glb/meta` | GET | `{mtime, size}` for polling |+| `/pcb.svg` | GET | Streams `<project>/dist/lib/index/pcb.svg` |+| `/schematic.svg` | GET | Streams `<project>/dist/lib/index/schematic.svg` |+| `/circuit.json` | GET | Streams `<project>/dist/lib/index/circuit.json` (UI uses it to map GLB meshes to component names) |+| `/rerun[?clean=1]` | POST | Touch `lib/index.tsx` via upsert + spawn `bunx tsci build --glbs --svgs` |+| `/reload` | POST | Bump GLB mtime to force 3D tab reload |+| `/health` | GET | `{ok, tsci_reachable, project_dir}` |+| `/state` | GET | Server state + toolbar flags + component visibility + component list |+| `/shutdown` | POST | Kill tsci dev process group, return 204, exit |+| `/api/set-view` | POST | Body `{"view":"top"}` — camera preset |+| `/api/set-camera` | POST | Body `{"alpha":…,"beta":…,"radius":…}` — raw camera |+| `/api/tour` | POST | Body `{"action":"start"\|"stop"}` — slow orbit |+| `/api/toggle` | POST | Body `{"name":"wireframe"}` — toggle a toolbar flag |+| `/api/toggle-component` | POST | Body `{"name":"U1"[,"visible":false]}` — show/hide a component |+| `/api/components` | GET/POST | GET returns the UI-discovered component list; POST is how the UI publishes it after GLB load |+| `/api/camera-command` | GET | UI polls this every 500 ms; returns the latest queued command and atomically clears it |+| `/console` | GET/POST | POST from UI appends a console message; GET returns `{messages, next_since}` for the CLI `adom-tsci console` |+| `/eval` | POST | Body `{"code":"…"}` — queue a JS snippet; returns `{"id":…}` |+| `/eval/pending` | GET | UI polls this; returns the next pending snippet or 204 |+| `/eval/:id/result` | POST | UI posts the snippet's result |+| `/eval/:id` | GET | AI polls for the result |++**curl examples:**++```bash+# Health check+curl -sS http://127.0.0.1:8850/health++# Re-run autorouter, clean mode+curl -sS -X POST "http://127.0.0.1:8850/rerun?clean=1"++# Fetch the PCB SVG+curl -sS http://127.0.0.1:8850/pcb.svg > pcb.svg++# Stop the server+curl -sS -X POST http://127.0.0.1:8850/shutdown+```++## Architecture++```++---------------------------+        +---------------------------++|  Hydrogen webview tab     |        |  adom-tsci (Rust)         |+|  /proxy/8850/             |<------>|  tiny_http on 127.0.0.1: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|<------------+------->|  /compare       compare.html+|  +----------+             |        |  /av/basic3d    AV bridge+|  | 3d cmp   |             |        |  /av/upload-glb AV bridge+|  +----------+             |        |                           |+|                           |        |  /rerun         [-> upsert + spawn build]+|  ⟳ Re-run autorouter -----+------->|  /reload  /shutdown  /health++---------------------------+        +---------------------------++                                              |           |+                                              v           v+                          +--------------------+   +----------------++                          | bun tsci dev       |   | AV (8770/8771) |+                          |   -p 3040          |   | Basic3dView +  |+                          | (process group)    |   | serve_glb      |+                          +--------------------+   +----------------++```++## Example session++> **User:** preview my SN65HVD230 molecule+>+> **Claude:** *(runs `adom-tsci start ~/project/adom-tsci-projects/SN65HVD230-Molecule`)*+>+> Output:+> ```+> OK: spawning tsci dev -p 3040+> HINT: @tscircuit/cli is out of date (installed 0.1.1226, latest 0.1.1234).+>       Run `adom-tsci upgrade` to refresh.+> OK: tsci dev is ready+> OK: webview tab created+> OK: adom-tsci running — project=... port=8850 tsci_port=3040+> ```+>+> The `adom-tsci` webview tab opens in the editor (with a brand-compliant+> `#e6edf3` monochrome chip icon in the tab bar). The 3D tab is active+> by default, showing the molecule in a clean dark-themed three.js+> viewer. User rotates with the mouse, clicks through to PCB, then+> Schematic, sees the tscircuit SVGs in panzoom containers.+>+> **User:** move R_TERM to pcbX=8+>+> **Claude:** *(edits `lib/index.tsx`)* Done. Click **⟳ Re-run+> autorouter** in the header to regenerate the build outputs, then the+> 3D / PCB / Schematic tabs will auto-refresh.+>+> **User:** *(clicks the button)*+>+> Shell flashes `⟳ Running…` → `⟳ Re-ran`. ~30 s later the 3D tab shows+> R_TERM at its new position; PCB and Schematic re-fetch on next click.+>+> **User:** ship it+>+> **Claude:** *(runs `bunx tsci push`)*++## 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+fire-and-forget.++**Re-run autorouter button does nothing visible**+The rebuild is async and takes ~30 s. Watch the `GLB: <time>` pill in+the header; when it updates, the 3D tab has re-rendered. PCB and+Schematic lazy-reload on next click.++**`tsci dev` crashed with `MethodNotAllowedError: only POST accepted`**+That's tsci dev's own internal fileserver spitting non-fatal noise+during its node_modules upload on startup. Doesn't break anything —+the server recovers. Check `/health` to confirm `tsci_reachable: true`.++**Webview stuck at `Loading files...`**+Known tscircuit v0.1.1226+ issue. RunFrame's `isLoadingFiles` flag+only clears when `files/list` + `files/get?file_path=...` both succeed.+In v0.1 of this app, the slingshot dropped query strings on forward,+which broke `files/get`; **v0.2 forwards the query string**. If you're+hitting this on v0.2, `curl http://127.0.0.1:8850/runframe/api/files/get?file_path=lib/index.tsx`+and see if the response has the real file content.++**3D tab — flat-lit, no shadows, no transparent ground**+The same-origin bridge didn't run. Symptoms:+- ground plane is opaque (not 50% transparent)+- no board shadow on the ground+- `iframe.contentWindow.viewer` is `undefined`++Causes to check:+1. **Different origin.** If the shell is on `/proxy/8850/` but+   Basic3dView is on a different host (not `/proxy/8770/` on the+   same subdomain), the contentWindow.viewer access will throw+   `SecurityError: Blocked a frame with origin`. Check that+   `VSCODE_PROXY_URI` is set to the same subdomain as AV's 8770.+2. **Viewer not yet constructed.** The bridge polls for up to+   10 s waiting for `scene.meshes` to have at least one+   non-ground mesh. On a cold cache where Basic3dView has to+   download its Babylon bundle + the GLB, this may not be enough.+3. **basic-3d.html changed module structure.** The bridge relies+   on `var viewer` being top-level scope in a non-module `<script>`.+   If upstream Adom wraps basic-3d.html in `type="module"`, the bridge+   breaks. Confirm with+   `curl .../proxy/8770/basic-3d.html | grep 'var viewer'`.++**3D compare pane is still flat-lit**+Known v0.2 limitation. The `3d compare` secondary tab lives in a+separate `compare.html` where both Basic3dView and three.js use+default lighting. The compare view is for material-fixup regression,+not for aesthetic rendering. Tracked for v0.3.++**"Some sites block embedding" banner at the top of the webview**+That banner is a persistent affordance shown on every webview tab+regardless of whether the page loaded. It is **not an error signal**.+If the panel below it is actually blank, check `/health` and the+`tsci-preview.log` file for subprocess errors.++**tsci dev subprocess doesn't die after stop**+Shouldn't happen in v0.1.0+. tsci dev is spawned in a new process+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/adom-tsci-projects/MyChip-Molecule+bunx tsci push+```++This publishes to `@tsci/<your-user>.MyChip-Molecule` on the+tscircuit registry. Requires `bunx tsci login` once (browser OAuth,+must be an `adom-inc` org member).++For the full production workflow (connector families, molecule+generators, DRC, review servers, snapshot approval), go to Ray's+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-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).+- **Basic3dView:** `~/.claude/skills/adom/guides/basic-3d-viewer.md`+  — the Adom Babylon viewer used in the 3D compare tab's left pane.+- **Web View panel API:** `~/.claude/skills/adom/webview/SKILL.md`.+- **Adom brand guide:** `~/.claude/skills/adom/guides/brand/SKILL.md`+  — all icons must be monochrome `#e6edf3`, the chip favicon follows+  this rule.+- [tscircuit docs](https://docs.tscircuit.com) · [iconify MDI+  catalog](https://icon-sets.iconify.design/mdi/)