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
Add adompkg installer files
4 files changed
+68−824
SKILL.md+38−824@@ -1,29 +1,30 @@ --- 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.+ 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. --- # adom-tsci@@ -35,52 +36,6 @@ 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),@@ -122,404 +77,6 @@ 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@@ -607,93 +164,21 @@ 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 && \+# 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 && \ 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):+# From any tscircuit project directory: adom-tsci start .-# …edit lib/index.tsx, click Re-run autorouter, stop with:+# …open the adom-tsci tab in your editor, edit lib/index.tsx,+# click Re-run autorouter when you want fresh outputs,+# 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 ```@@ -834,124 +319,6 @@ 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:` /@@ -1072,7 +439,7 @@ curl -sS -X POST http://127.0.0.1:8850/shutdown > **User:** preview my SN65HVD230 molecule >-> **Claude:** *(runs `adom-tsci start ~/project/adom-tsci-projects/SN65HVD230-Molecule`)*+> **Claude:** *(runs `adom-tsci start ~/project/tscircuit-projects/SN65HVD230-Molecule`)* > > Output: > ```@@ -1107,20 +474,6 @@ 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@@ -1184,149 +537,13 @@ 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+cd ~/project/tscircuit-projects/MyChip-Molecule bunx tsci push ``` @@ -1341,13 +558,10 @@ 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.+ — 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. - **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).
install.shadded+15@@ -0,0 +1,15 @@+#!/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"
package.jsonadded+11@@ -0,0 +1,11 @@+{+ "slug": "adom-tsci",+ "version": "1.0.0",+ "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",+ "dependencies": {},+ "scripts": {+ "install": "./install.sh",+ "uninstall": "./uninstall.sh"+ }+}
uninstall.shadded+4@@ -0,0 +1,4 @@+#!/bin/bash+set -e+rm -rf "$HOME/.claude/skills/adom-tsci"+echo "Uninstalled adom-tsci"