← Commit history
README.md+23
@@ -4,6 +4,14 @@ Interactive tscircuit preview inside a Hydrogen webview panel. First-class **3D / PCB / Schematic** tabs rendered from the tscircuit build output, plus: +- **`start --view top`** — start with a top-down camera preset and reapply it+  after page refreshes and model rebuilds. Other camera controls remain available.+- **Live PCB/schematic refresh** — SVG changes refresh independently of the GLB;+  `reload` also refreshes unchanged files. Existing 2D pan and zoom are preserved.+- **Direct artifact review** — `status --json` reports generated SVG, PNG, and+  GLB paths, availability, and file versions in `artifacts`. Read the generated+  PCB image directly when reviewing layout or silkscreen.+ - **🔍 Inspect tool** (key `I`) — hover any feature for a labelled info   card: chip refdes + footprint + pin count, pad → net routing   ("Connects to MC1.pin1, TP1.pin1 via 2 direct traces"), machine-@@ -26,6 +34,21 @@ build output, plus: - **Stale-deps check** against `@tscircuit/cli` on startup. - **Secondary tabs:** `tsci live` (raw RunFrame) and `3d compare`. +For a top-down work session:++```bash+adom-tsci start . --view top+adom-tsci status --json+```++The `artifacts.pcb_svg.path` and `artifacts.schematic_svg.path` fields locate+the generated 2D images. Check `available` before opening an artifact; PNG+exports may not exist. If your image reader needs a raster image, rasterize+the SVG locally. Use viewer screenshots for testing the viewer or inspecting+3D behavior. Keep any disabled walkthrough preference disabled.++Developer checks: `cargo test` and `node --test tests/build_review.test.cjs`.+ **Recent (1.4.x):** - **Canonical Adom app-header** — one-row `brand · subject · actions` with a   hover app-info popover (version / engine / wiki / GitHub), brand fonts
SKILL.md+55−48
@@ -8,8 +8,7 @@ description: >   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, usb-c looks wrong, usb-c doesn't look correct, connector is a white-  box, cuboid placeholder. Interactive tscircuit preview inside a Hydrogen webview panel+  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@@ -24,9 +23,7 @@ description: >   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, the canonical `UsbCReceptacle` wrapper, the mandatory-  lint + numeric + visual verification, and the "USB-C looks wrong" triage-  (render placeholder vs real placement bug).+  placement, rotation math, and the canonical `UsbCReceptacle` wrapper. ---  # adom-tsci@@ -49,15 +46,16 @@ 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. |+| 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. Instanced parts glow through a temporary overlay (`_componentGlowMeshes`: a geometry-sharing clone of the source at the instance, named `hl-ovl-*`, disposed when the highlight clears), so only the target glows and nothing is promoted (Ray, 2026-10-01). | 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()` and `drawFeatureOutline()` in `shell.html` add `_componentGlowMeshes(name).meshes` via `hl.addMesh(mesh, teal)`; never `_promoteComponent` on a highlight or hover path. `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. |+| 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 and hover use overlay-only (no promotion): hover used to promote every part it touched, so one Inspect sweep de-instanced the F411 (81 -> 0 instanced meshes, 21 -> 177 materials). | 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`. |+| 11 | **The camera moves only when the user moves it or explicitly asks.** Opening a board shows it still (no walkthrough autoplay, no tour). Hover, Components-panel hover, click-select and right-click glow the part but never retarget or zoom. Orbit and pan keep a fixed target; a press becomes a click (select / context menu) only if the pointer comes back up within 4 px. Click-to-orbit-center is OFF by default and, when on, needs a STILL press-and-hold: any drag cancels it. A rebuild (save, `rerun`, `reload`) keeps the user's pose; only the page's first load frames the board. Opening Nets or clicking a net glows it without moving the camera. Framing is opt-in: walkthrough steps, `adom-tsci net`, `adom-tsci highlight` (without `--no-fit`), `fit-to-component`, `view`, `camera`. Click-vs-drag uses pointer events: Babylon preventDefaults pointerdown, so `mousedown` never fires on the canvas. | Ray, 2026-10-01: "I just want to view the board without it moving. panning and orbiting is horrible cuz it keeps thinking I want to zoom in on this part I'm randomly hovering over." Every orbit press selected the part under it and flew the camera there; panel hover did the same; the 500 ms click-to-set timer fired mid-drag. | `highlightComponents()` has no camera code. Click-select is on `pointerup` gated by `_wasClick()` in `_setupCompContextMenu`. `_initClickNav()` defaults off; the pref key is `tsciClickNavPref`, written only by a toolbar click. Drag-cancel lives in `vendor/3d-viewer/.../AdomCamera.ts` (`clickHoldSlopPx`). Probe: camera target and radius must not change across hover / orbit / pan / panel hover. |  If you're about to ship a change that touches highlight, hide, visibility, walkthrough, or marker material — re-read the row above and@@ -75,7 +73,7 @@ when one matches, load the referenced guide.  | Guide | Load when the user says | Path | |---|---|---|-| **adom-usbc** — USB-C receptacles on an Adom tscircuit molecule: the copy-verbatim `UsbCReceptacle` wrapper (`TYPE-C-31-M-12`, merged same-net pads, all 6 holes), placement by `mount` + `pcbX = W/2 − 4`, the lint → numeric edge check → two-screenshot visual proof gate, and the **"it looks wrong" triage tree** (white-box placeholder = poisoned rebake cache, NOT a footprint bug). **Load it at three moments: before placing, after every build, and the instant anyone says the USB-C looks wrong** | "usb-c", "type-c", "smd usb c", "UsbCReceptacle", "place usb-c", "rotate usb-c", "usb-c east edge", "usb-c west edge", "usb-c looks wrong", "connector doesn't look right", "iphone charger", "cuboid placeholder" | `~/.claude/skills/adom-tsci/guides/adom-usbc.md` |+| **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` | @@ -100,28 +98,14 @@ LED nameplates are a signature Adom build and the catalog's LEDs are excellent — if the project smells like a nameplate (a name in an LED grid), start from Adom Basic Parts, not from random JLCPCB searches. -**USB-C gets its own guide, and it is not optional.** Placement is subtle-(mouth must slightly overhang the board edge or the cable can't seat) and-popular enough that it's a bundled sub-skill of this app:+**USB-C gets its own guide.** Placement is subtle (mouth must slightly+overhang the board edge or the cable can't seat) and popular enough+that it's a bundled sub-skill of this app: `~/.claude/skills/adom-tsci/guides/adom-usbc.md` — the CLI also nudges you (`Hint:` lines on `start` and `lint`) whenever it detects a USB-C part in the project. Trust those hints; they exist because USB-C was mis-placed ~20 times before the rules were written down. -The three hard rules, so you don't skip the guide and wing it:--1. **Copy `lib/UsbCReceptacle.tsx` verbatim** from the guide (or `diff`-   against a shipped nameplate). Never author, simplify, or re-derive a-   wrapper; never touch its offsets to "fix" a direction — pass `mount`.-2. **Every build with a USB-C ends with the gate**: `adom-tsci lint` →-   the numeric edge check → body-ON and body-OFF screenshots that you-   READ. Lint alone is not proof (it can't see a poisoned render).-3. **"The USB-C doesn't look right" means run the guide's triage tree-   first.** A flat white/black box where the receptacle should be is the-   viewer's cuboid placeholder — a re-bake cache problem, not a footprint-   problem. Check `adom-tsci console` for `cuboid placeholder` before-   editing a single line of the board.- ## 🧹 Process hygiene — don't be the thread that leaks tsci devs (v1.4.23)  Every preview holds a `bun tsci dev` that idles at ~200 MB and grows.@@ -258,26 +242,58 @@ entry — this is the belt to the headers' suspenders. 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+## Review generated images directly++For PCB layout, silkscreen, contact labels, and schematic review, read the+generated 2D artifact directly. Do not take a screenshot of the PCB picture+inside the viewer as the default review loop.++1. Build using the project's existing build/review command.+2. Use `adom-tsci status --port <port> --json` to confirm `project_dir` and+   inspect `artifacts`. Each entry has `path`, `available`, `size`, `mtime_ms`,+   and `version`. The version is a file-change token, not a DRC result.+3. Read `artifacts.pcb_svg.path` or `artifacts.schematic_svg.path` directly.+   If a generated PNG is available, it can be read directly too. Otherwise,+   rasterize the SVG locally when the image reader requires PNG; use the+   project's renderer if it has one. Never substitute a stale PNG for a newer+   SVG: verify build timestamps or regenerate it.+4. Keep the live preview current for the user. SVG changes are polled every+   three seconds independently of the GLB. `adom-tsci reload --port <port>`+   forces artifact refresh even without a file timestamp change. Hidden SVG+   panels refresh when selected; the active panel preserves pan and zoom.++For a new top-down 3D work session, use `adom-tsci start <project> --view top`.+The selected preset applies on initial load, page refresh, and each model+rebuild. Supported values: `top`, `bottom`, `front`, `back`, `left`, `right`,+`isometric` (default), `iso`, and `home`. Existing camera controls still work+between rebuilds. Do not restart an existing preview just to set its camera;+use `adom-tsci view top --port <port>`. Preserve disabled walkthrough/demo+preferences. Do not start a walkthrough as a side effect of reviewing a board.++Screenshots remain appropriate for actual 3D appearance and viewer UI+verification. Generated-image inspection does not prove the viewer refreshed;+viewer screenshots do not replace electrical checks.++## 👁 Keep the live preview current  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.+viewer itself — keep the relevant preview available to the user. Review PCB+layout through the generated artifact as described above. Verify actual viewer+changes in the viewer. Preserve the user's foreground and panel preferences.  **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>`.+2. When the user asks to see the preview, activate the named instance in+   the requested pane with `adom-cli hydrogen workspace active-tab+   --panel-id <P> --tab-id <T>`. Otherwise preserve the foreground. 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+4. When testing the 3D viewer, confirm the scene is 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@@ -350,6 +366,12 @@ the same things.    switched it to a material/mesh approach without realising testpoints    don't have one. Restore it. +   **Same for machine contacts / pins with no 3D model** (molecules): tscircuit+   emits a translucent 24-vertex placeholder box over each pad. They load with+   `visibility = 0` (still pickable) and are shown only while their part is+   highlighted, so a contact step still glows. Real contact models (alpha 1)+   are never touched. `_isPlaceholderBox()` / `_hidePlaceholderBoxes()`.+ 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@@ -1250,21 +1272,6 @@ curl -sS -X POST http://127.0.0.1:8850/shutdown  ## Troubleshooting -**A part renders as a flat white or black box (USB-C, any chip with a cadModel)**-`adom-tsci console --tail 50 | grep -i placeholder` → `cuboid placeholder-for "J1"` means the GLB the server is SERVING has no body mesh for that-part. `start` re-bakes the GLB for texture resolution (`circuit-json-to-gltf`)-and caches the result by circuit.json hash in-`~/.cache/adom-tsci/rebakes/<hash>-<res>.glb`; if a modelcdn OBJ fetch-failed during that bake, the cached mesh has 0 vertices and stays poisoned-until circuit.json changes. Confirm with `curl -s :8850/glb -o /tmp/served.glb`-and compare its size to `dist/lib/index/3d.glb` (the tsci-built one is-fine). Fix: delete that cache entry, `adom-tsci stop && adom-tsci start …`,-or re-bake by hand and `adom-tsci open` (a bare `reload` does not re-read-the GLB). The footprint is not the problem — do not edit the board.-(TODO: CLI gap — `start` should validate a rebake against the tsci-built-GLB's per-mesh vertex counts before caching it.)- **`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
VERSION+1−1
@@ -1 +1 @@-1.4.24+1.5.0
guides/adom-usbc.md+276−422
@@ -1,391 +1,27 @@ # adom-usbc — making USB-C receptacles actually work -**Read this at THREE moments, not one:**--1. **Before** you write a board that has a USB-C receptacle.-2. **After every build** that contains one (`adom-tsci lint` + the numeric-   check + the visual proof below — all three, every time).-3. **The moment anyone says "the USB-C doesn't look right."** Go straight to-   §5 (triage). Do NOT start by editing the footprint or the rotation — the-   last three "USB-C looks wrong" reports were a *render* problem, not a-   placement problem, and rewriting the wrapper is how it gets broken.--Written because Claude got USB-C wrong ~20 times before we wrote the rules-down, and then got it wrong again by re-deriving instead of copying. If you-skip a step you will repeat one of those failures.--## 0. The rules in one screen--| Rule | Value |-|---|---|-| Part | TYPE-C-31-M-12 · JLCPCB **C165948** (16-pin, power + CC + D±) |-| Wrapper | **Copy `UsbCReceptacle.tsx` from §1 verbatim.** Never write, "simplify", or re-derive one. |-| Placement API | `<UsbCReceptacle name="J1" mount="east" pcbX={W/2 - 4} pcbY={0} />` — pass `mount`, never a rotation number |-| Mouth | Overhangs the board edge by ~2–3 mm. Flush-or-proud, **never recessed** |-| SMT pads | **Inside** the copper, ~5–7 mm from the edge. No pad past the edge, ever |-| CC pulldowns | Rd = 5.1 k ±10 % on CC1 **and** CC2 (Adom stocks 4.7 k 0402 — in window). Without them a charger never enables VBUS |-| Gate | `adom-tsci lint` passes **and** §3 numeric check passes **and** §4 screenshots read back |--`W` is the board's nominal width (the `size="80x24"` number, not the wing-grown-82.4). For a Molecule the wings add ~1.2 mm per side; `W/2 − 4` already-accounts for that on medium pins (see §"USB-C on a MOLECULE edge" for large-pins / absolute sizes).--## 1. The wrapper — copy this file, do not author one--This is the wrapper that ships on every LED nameplate (`bill-nameplate`,-`vijay-nameplate`, …). It differs from the old guide template in two ways-that matter, and both were learned the hard way:--- **All 6 mounting holes are included** (4 plated pill anchors + 2 drill-  supports). Without the anchors the port is held by 16 tiny SMT joints and-  tears off in < 100 cable cycles.-- **The four same-net pad pairs are merged** into single 0.6 mm tabs.-  USB-C doubles GND (A1/B12, B1/A12) and VBUS (A4/B9, B4/A9) for flip-  symmetry; as separate 0.3 mm pads on 0.3 mm pitch they have **0 mm-  clearance** (fails `pcb_pad_pad_clearance_error`) and make `J1.GND1` an-  ambiguous multi-pad reference. Merging keeps the pad centroid at x=0 so-  the body/rotation invariant is untouched. Pin aliases: `GND1`/`GND2`,-  `VBUS1`/`VBUS2` = **left/right tab**, `CC1`, `CC2`, `DP1`/`DP2`, `DM1`/`DM2`,-  `SBU1`/`SBU2`.--```tsx-// lib/UsbCReceptacle.tsx-/**- * USB-C receptacle (TYPE-C-31-M-12 / JLCPCB C165948).- *- * ─────────────── THE CORE INVARIANT ───────────────- * Footprint (pads + holes) and cadModel (3D body) are TIED TOGETHER.- * Whatever rotation is applied to one MUST be applied identically to- * the other. Earlier iterations violated this by (a) shifting pad- * positions in the footprint (Y_SHIFT) while leaving the cadModel- * offset in a different frame, and (b) letting tscircuit apply- * positionOffset in WORLD coords while pads rotated in LOCAL. Result:- * at pcbRotation≠0 the 3D body drifted away from its own holes.- *- * Fix: keep footprint at native ArnavK-09 positions AND compute- * positionOffset by rotating the native (0, -2.5) vector by pcbRotation- * in userland. Both footprint and body then follow the SAME rotation.- *- * ─────────────── NATIVE LAYOUT ───────────────- *   pads at local (x, +2.449) for 16 pads — this is the BACK edge of- *                                the receptacle (solder-tails come out- *                                the back of the plastic shell)- *   drill holes at (±2.9, +1.18)- *   plated anchor holes at (±4.325, +1.41) and (±4.325, -2.774)- *                                — 4 corners of the receptacle body- *   body centroid at local (0, -2.5) in native (from SmdUsbC hardcode)- *   mouth at local -Y (south) in native- *- * This layout has body_centroid south of pads by 4.95 mm — that's the- * distance from the pad row (back of receptacle) through the body to- * the body's midpoint. Rotated together, this 4.95 mm distance is- * preserved at every pcbRotation so holes ALWAYS stay inside the body.- */--const USB_C_PN = "C165948"-const USB_C_UUID = "2a4bc2358b36497d9ab2a66ab6419ba3"--// Native positionOffset from ArnavK-09 / seveibar.smd-usb-c (pcbRotation=0):-// body centroid is 2.5 mm south of chip origin. Rotated by pcbRotation-// in userland below so the body rotates with the footprint.-const NATIVE_POS_OFFSET = { x: 0, y: -2.5 }--// 12 physical pads (see PADS below): the four same-net pairs that USB-C-// doubles for flip symmetry are single wider tabs, exactly as they are on-// the real TYPE-C-31-M-12 land pattern. Each merged tab is ONE pin carrying-// both of its native contact designators.-//-// The GND1/GND2/VBUS1/VBUS2 aliases are kept, but now mean LEFT/RIGHT tab-// rather than the A-row/B-row contact. That is what makes them unambiguous:-// one alias -> exactly one pad. Naming them per-row instead (the native-// scheme) would put VBUS1 on both tabs and VBUS2 on both tabs, and the-// router would emit a degenerate pad-to-itself trace.-const pinLabels = {-  pin1:  ["GND1", "A1", "B12"],   // left GND tab   (merged)-  pin2:  ["VBUS1", "A4", "B9"],   // left VBUS tab  (merged)-  pin3:  ["SBU2", "B8"],-  pin4:  ["CC1", "A5"],-  pin5:  ["DM2", "B7"],-  pin6:  ["DP1", "A6"],-  pin7:  ["DM1", "A7"],-  pin8:  ["DP2", "B6"],-  pin9:  ["SBU1", "A8"],-  pin10: ["CC2", "B5"],-  pin11: ["VBUS2", "B4", "A9"],   // right VBUS tab (merged)-  pin12: ["GND2", "B1", "A12"],   // right GND tab  (merged)-} as const--// ArnavK-09 native positions — DO NOT SHIFT in the mouth (Y) direction.-// Shifting Y decouples the footprint from the cadModel, which was the bug.-//-// X-direction merge (safe, and required to pass DRC): the four native-// pad PAIRS that sit on 0.3 mm pitch — (A1,B12) and (B1,A12) are both-// GND, (A4,B9) and (B4,A9) are both VBUS — are 0.3 mm wide at 0.3 mm-// spacing, i.e. exactly 0 mm clearance. `adom-tsci lint` fails that as-// pcb_pad_pad_clearance_error (min 0.1 mm), and the doubled labels also-// make `J1.GND1` an ambiguous multi-pad reference. Because each pair is-// the SAME NET (USB-C doubles those pins for flip symmetry), the correct-// fix is to merge each pair into one 0.6 mm pad carrying both port hints.-// This keeps the pad centroid at x=0 (so the body/rotation invariant is-// untouched) and restores 0.2 mm clearance everywhere.-const PAD_Y_LOCAL = 2.449  // all pads at this y-const PADS: Array<{ ports: string[]; x: number; w: number }> = [-  { ports: ["A1", "B12"], x: -3.2,  w: 0.6 },   // GND  (merged pair)-  { ports: ["A4", "B9"],  x: -2.4,  w: 0.6 },   // VBUS (merged pair)-  { ports: ["B8"],        x: -1.75, w: 0.3 },-  { ports: ["A5"],        x: -1.25, w: 0.3 },-  { ports: ["B7"],        x: -0.75, w: 0.3 },-  { ports: ["A6"],        x: -0.25, w: 0.3 },-  { ports: ["A7"],        x:  0.25, w: 0.3 },-  { ports: ["B6"],        x:  0.75, w: 0.3 },-  { ports: ["A8"],        x:  1.25, w: 0.3 },-  { ports: ["B5"],        x:  1.75, w: 0.3 },-  { ports: ["B4", "A9"],  x:  2.4,  w: 0.6 },   // VBUS (merged pair)-  { ports: ["B1", "A12"], x:  3.2,  w: 0.6 },   // GND  (merged pair)-]--/** Rotate a 2D vector by θ radians CCW (tscircuit's pcbRotation convention). */-const rotCCW = (x: number, y: number, θ: number) => ({-  x: x * Math.cos(θ) - y * Math.sin(θ),-  y: x * Math.sin(θ) + y * Math.cos(θ),-})--/**- * Mount convention — pass `mount` instead of raw `pcbRotation` so the- * edge-facing semantics are explicit. Native mouth = -Y (south) after- * the built-in rotationOffset.z = 180 flip, so:- *   south → pcbRotation = 0- *   east  → pcbRotation = 90   (native -Y rotates CCW 90 to +X east... wait,- *                                CCW 90 of (0,-1) = (1, 0) = east? Let me re-derive:- *                                CCW θ: (x,y) → (x cos θ - y sin θ, x sin θ + y cos θ).- *                                For θ=90: (0,-1) → (0*0 - (-1)*1, 0*1 + (-1)*0) = (1, 0) = east ✓)- *   north → pcbRotation = 180- *   west  → pcbRotation = 270- */-type Mount = "south" | "east" | "north" | "west"-const MOUNT_TO_PCBROT: Record<Mount, 0 | 90 | 180 | 270> = {-  south: 0, east: 90, north: 180, west: 270,-}--type Props = {-  name: string-  pcbX: number-  pcbY: number-  /** Where the mouth points. Equivalent: pass pcbRotation directly via `pcbRotationDeg`. */-  mount?: Mount-  pcbRotationDeg?: 0 | 90 | 180 | 270-}--export const UsbCReceptacle = ({-  name, pcbX, pcbY, mount, pcbRotationDeg,-}: Props) => {-  const pcbRot = pcbRotationDeg ?? (mount ? MOUNT_TO_PCBROT[mount] : 0)-  const θ = (pcbRot * Math.PI) / 180--  // Rotate the native positionOffset by pcbRotation so the body-  // rotates with the footprint. This preserves the body-to-pad and-  // body-to-hole distances at every rotation — the invariant the-  // user (correctly) demanded.-  const rotated = rotCCW(NATIVE_POS_OFFSET.x, NATIVE_POS_OFFSET.y, θ)--  return (-    <chip-      name={name}-      pcbX={pcbX}-      pcbY={pcbY}-      pcbRotation={pcbRot}-      supplierPartNumbers={{ jlcpcb: [USB_C_PN] }}-      manufacturerPartNumber="TYPE-C-31-M-12"-      pinLabels={pinLabels as any}-      cadModel={{-        objUrl: `https://modelcdn.tscircuit.com/easyeda_models/download?uuid=${USB_C_UUID}&pn=${USB_C_PN}`,-        // Native SmdUsbC rotationOffset — CAD body's native +Y mouth-        // gets flipped to -Y so it matches the native footprint's-        // mouth-south orientation.-        rotationOffset: { x: 0, y: 0, z: 180 },-        // Rotated version of the native (0, -2.5). This is what ties-        // the body to the footprint.-        positionOffset: { x: rotated.x, y: rotated.y, z: 0 },-      }}-      footprint={-        <footprint>-          {PADS.map(p => (-            <smtpad key={p.ports.join("_")} portHints={p.ports}-              shape="rect" width={`${p.w}mm`} height="1.3mm"-              pcbX={`${p.x}mm`} pcbY={`${PAD_Y_LOCAL}mm`} />-          ))}-          {/* 2 drill-only mechanical support holes, 1.18 mm south of chip origin */}-          <hole pcbX="-2.9mm" pcbY="1.18mm" diameter="0.75mm" />-          <hole pcbX="2.9mm"  pcbY="1.18mm" diameter="0.75mm" />-          {/* 4 plated anchor holes at the 4 body corners — 2 near pad row-              (at y=+1.41) + 2 toward mouth (at y=-2.774). Rotate together-              with the body, so they stay inside the body footprint. */}-          <platedhole portHints={["alt_0"]} pcbX="-4.325mm" pcbY="1.41mm"-            shape="pill" outerWidth="1.2mm" outerHeight="2.0mm"-            innerWidth="0.8mm" innerHeight="1.6mm" height="1.6mm" />-          <platedhole portHints={["alt_1"]} pcbX="4.325mm"  pcbY="1.41mm"-            shape="pill" outerWidth="1.2mm" outerHeight="2.0mm"-            innerWidth="0.8mm" innerHeight="1.6mm" height="1.6mm" />-          <platedhole portHints={["alt_2"]} pcbX="4.325mm"  pcbY="-2.774mm"-            shape="pill" outerWidth="1.2mm" outerHeight="1.8mm"-            innerWidth="0.8mm" innerHeight="1.4mm" height="1.4mm" />-          <platedhole portHints={["alt_3"]} pcbX="-4.325mm" pcbY="-2.774mm"-            shape="pill" outerWidth="1.2mm" outerHeight="1.8mm"-            innerWidth="0.8mm" innerHeight="1.4mm" height="1.4mm" />-        </footprint>-      }-    />-  )-}-```--Wire it like this (power-only board; add D± traces if you use data):--```tsx-<UsbCReceptacle name="J1" pcbX={36} pcbY={0} mount="east" />   // 80-wide board-<resistor name="R_CC1" resistance="4.7k" footprint="0402" pcbX={29} pcbY={2} />-<resistor name="R_CC2" resistance="4.7k" footprint="0402" pcbX={29} pcbY={-2} />-<capacitor name="C_BYP" capacitance="100nF" footprint="0402" pcbX={32} pcbY={5} />--<trace from="J1.VBUS1" to="net.VBUS" />  <trace from="J1.VBUS2" to="net.VBUS" />-<trace from="J1.GND1"  to="net.GND" />   <trace from="J1.GND2"  to="net.GND" />-<trace from="R_CC1.pin1" to="J1.CC1" />  <trace from="R_CC1.pin2" to="net.GND" />-<trace from="R_CC2.pin1" to="J1.CC2" />  <trace from="R_CC2.pin2" to="net.GND" />-<trace from="C_BYP.pin1" to="net.VBUS" /><trace from="C_BYP.pin2" to="net.GND" />-```--**Schematic:** if you add `schX`/`schY` to other parts, forward them through-the wrapper too (add the props to `Props` and pass them to `<chip>`), or the-receptacle piles at the schematic origin — `adom-tsci lint` check 5 catches it.--## 2. Placement — by `mount`, never by rotation number--`pcbX`/`pcbY` is the chip origin = the SMT pad-row centroid. The body-extends ~2.5 mm *forward* of the pads and the mouth ~4.7 mm.--| Mouth faces | `mount` | `pcbX` | `pcbY` |-|---|---|---|---|-| East (right) — the iPhone-charger default | `"east"`  | `+W/2 − 4` | 0 (or along the edge) |-| West (left)  | `"west"`  | `−W/2 + 4` | 0 |-| South (bottom) | `"south"` | 0 | `−H/2 + 4` |-| North (top)  | `"north"` | 0 | `+H/2 − 4` |--Keep the receptacle **mid-edge, between the corner machine pins**. Leave a-~13 mm zone inboard of the connector for the CC resistors and bypass cap —-on an 80×24 nameplate that means the LED text must end at `x ≤ +20`.--Do not touch `rotationOffset`, `positionOffset`, `NATIVE_POS_OFFSET` or-`PAD_Y_LOCAL` in the wrapper to "fix" a direction. If the mouth points the-wrong way you passed the wrong `mount`; if the body is off its pads you-edited the wrapper — restore it from §1.--## 3. Build → lint → numeric edge check (every build)--```bash-bunx tsci build lib/index.tsx --glbs --svgs   # writes dist/ (tsci export does NOT)-adom-tsci lint                                 # body_fwd + mouth-exits-edge + clearance + ghost chips-```--`lint` reports `J1: body_fwd=5.26mm, mouth … exits board edge at (41.2, 0)`-for a correct east-edge placement (5.26 = 2.5 mm body-over-pads + the 2.6 mm-bbox shift the anchor holes add; see §"Include ALL mounting holes").--Then the numbers lint does not print — pad and hole gaps to the actual edge:--```bash-python3 - <<'EOF'-import json-c=json.load(open('dist/lib/index/circuit.json'))-sc=[s for s in c if s['type']=='source_component' and s['name']=='J1'][0]-pc=[p for p in c if p['type']=='pcb_component' and p['source_component_id']==sc['source_component_id']][0]-pads=[p for p in c if p['type']=='pcb_smtpad' and p['pcb_component_id']==pc['pcb_component_id']]-holes=[p for p in c if p['type'] in('pcb_plated_hole','pcb_hole') and p['pcb_component_id']==pc['pcb_component_id']]-cad=[p for p in c if p['type']=='cad_component' and p['pcb_component_id']==pc['pcb_component_id']][0]-b=[e for e in c if e['type']=='pcb_board'][0]; edge=b['center']['x']+b['width']/2   # east edge; adapt for other edges-print('edge x', edge)-print('pad gap to edge  %.2f mm  (want 5..8)' % (edge-max(p['x']+max(p['width'],p['height'])/2 for p in pads)))-print('hole gap to edge %.2f mm  (want > 0.8)' % (edge-max(h['x']+h.get('outer_width',h.get('hole_diameter',0))/2 for h in holes)))-print('mouth overhang   %.2f mm  (want 2..3)' % (cad['position']['x']+4.7-edge))-print('pads', len(pads), '(want 12)', 'holes', len(holes), '(want 6)')-EOF-```--Reference values from `vijay-nameplate` (80×24 molecule, `pcbX=36`): pad gap-7.35, hole gap 1.83, overhang 2.31, 12 pads, 6 holes. If yours differ by more-than ~1 mm, fix placement before going further.--## 4. Visual proof — two screenshots, both READ back--Numbers can be gamed; the user verifies with their eyes. Take these in the-live `adom-tsci` tab (Hydrogen) — pup + shotlog is the alternative when the-workspace must not be touched.--```bash-adom-tsci fit-to-component J1-adom-tsci camera --alpha 0.4 --beta 1.0 --radius 1.6-adom-cli hydrogen screenshot panel --name "<tab name>" --reason "USB-C body"        # 1. body ON-adom-tsci toggle-component J1 --hide-adom-cli hydrogen screenshot panel --name "<tab name>" --reason "USB-C pads"        # 2. body OFF → pads/holes-adom-tsci toggle-component J1 --show-adom-tsci highlight --off && adom-tsci view home-```--Read shot 1: a **grey/silver receptacle shell with a visible mouth opening**-hanging ~2 mm off the edge, mouth outward. Read shot 2: 12 gold pads in a-row + 4 pill anchors + 2 drill holes, all on green copper inside the edge.--**If shot 1 shows a flat white or black box instead of a receptacle, stop —-that is not a placement bug. Go to §5.**--## 5. "It doesn't look right" — triage IN THIS ORDER--```-Is the 3D body a featureless box (white/black slab)?-│-├─ yes → RENDER problem, footprint is probably fine.-│        adom-tsci console --tail 50 | grep -i placeholder-│        └─ "cuboid placeholder for J1" → the served GLB has no J1 body.-│           adom-tsci serves a RE-BAKED GLB (texture upscale) cached by-│           circuit.json hash in ~/.cache/adom-tsci/rebakes/<hash>-<res>.glb.-│           If the modelcdn OBJ fetch for C165948 failed during that bake,-│           the cache holds a 0-vertex J1 mesh — and it STAYS poisoned-│           until circuit.json changes.-│           Confirm:  curl -s :8850/glb -o /tmp/served.glb-│                     ls -l /tmp/served.glb dist/lib/index/3d.glb   # served much smaller?-│           Fix:      rm ~/.cache/adom-tsci/rebakes/<newest>.glb-│                     adom-tsci stop && adom-tsci start . --tab-name "<same>"-│                     (or re-bake by hand with circuit-json-to-gltf and-│                      overwrite the cache file, then `adom-tsci open`;-│                      a bare `adom-tsci reload` is NOT enough)-│           Then redo §4.-│-└─ no, it's a real receptacle but wrong →-   ├─ mouth points INTO the board / butt hangs off → wrong `mount`. Fix the prop.-   ├─ body sits beside / diagonal to its pads → wrapper was edited. Restore §1.-   ├─ body is inboard, pads near the edge → `pcbX` sign or W confusion. Recompute W/2 − 4.-   ├─ pads hang past the edge → pcbX too big; the GLB mesh grows asymmetrically-   │  and EVERY feature renders shifted. Pull in to W/2 − 4.-   └─ lint fails `pcb_pad_pad_clearance_error` on J1 → you have the old-      16-pad wrapper. Replace with §1 (merged pairs).-```--Never "fix" a render-side symptom by editing `UsbCReceptacle.tsx`. If lint-and §3 pass, the copper is right regardless of what the 3D tab shows.--## 6. Definition of done--- [ ] wrapper is byte-identical to §1 (`diff` against a nameplate that shipped)-- [ ] placed by `mount`, `pcbX = ±(W/2 − 4)`, mid-edge-- [ ] CC1 **and** CC2 pulled down; VBUS bypassed-- [ ] `adom-tsci lint` passes (0 build errors, body_fwd ≈ 5.3, mouth exits edge)-- [ ] §3 numbers within ~1 mm of reference; 12 pads, 6 holes-- [ ] §4 body shot shows a real receptacle mouth-out over the edge; pads shot shows 12 + 4 + 2 inside-- [ ] `plan.md` records the placement numbers-------# Background — why the rules are what they are+**Read this before touching a USB-C receptacle on an Adom tscircuit+board.** Written because Claude got USB-C wrong ~20 times before we+wrote the rules down. If you skip this skill and try to wing it, you+will repeat every one of those failures.++## THE OVERHANG RULE — why the mouth must clear the board edge++A USB-C receptacle is useless if the plug can't seat. The plug's+collar is wider than the receptacle mouth, so the mouth must **slightly+overhang the board edge** (for TYPE-C-31-M-12 the mouth cantilevers+~2.5 mm past the edge at the recommended placement) — flush-or-proud+of the edge, NEVER recessed inboard. Recessed = the cable bottoms out+on the board edge and never makes contact. This is also the general+wiki rule: see the **pcb-design** skill on the wiki+([wiki.adom.inc/adom/pcb-design](https://wiki.adom.inc/adom/pcb-design)),+rule "USB connector placement": edge-mounted, flush or slightly+overhanging. `adom-tsci lint` enforces the geometry (body forward of+pads, mouth exits the board edge within a connector depth).++**Sourcing:** use the Adom Basic Parts catalog for the receptacle and+its passives — verified symbols, footprints, and 3D bodies:+[wiki.adom.inc/adom/adom-basic-parts](https://wiki.adom.inc/adom/adom-basic-parts).  ## THE CORE INVARIANT — DO NOT VIOLATE @@ -399,9 +35,9 @@ Ways to satisfy this invariant:  1. **Bundle them in one component** with a single `pcbRotation` prop    that applies to BOTH the `<chip>`'s footprint children AND the-   cadModel's `positionOffset`. The §1 wrapper does this — it computes-   `positionOffset` as `rotCCW(NATIVE_POS_OFFSET, pcbRotation)` in userland-   so the body rotates with the footprint.+   cadModel's `positionOffset`. The wrapper in §"The workaround"+   below does this — it computes `positionOffset` as `rotate(native_offset,+   pcbRotation)` in userland so the body rotates with the footprint. 2. **Compute body placement in the SAME local frame as the footprint.**    Never express body offset in world coords while the footprint is    in local coords. If you set `positionOffset` directly without@@ -453,6 +89,235 @@ where the receptacle's body sits relative to its back-row of SMT pads.  Filed upstream as `TSCIRCUIT_FEATURE_REQUESTS.md §6`. +## The workaround: a local `UsbCReceptacle.tsx`++Ship a local wrapper in your project's `lib/` that builds a raw+`<chip>` with an explicit footprint (copied from ArnavK-09's JSX) and+your own `cadModel` that rotates `positionOffset` with `pcbRotation`.+This survives `bun install`, gives you full control, and — critically+— you can unit-test it with the lint below without waiting on+upstream fixes.++### The template (copy this verbatim)++```tsx+// lib/UsbCReceptacle.tsx+const USB_C_PN = "C165948"+const USB_C_UUID = "2a4bc2358b36497d9ab2a66ab6419ba3"++// Body-to-pad offset in the mouth direction for TYPE-C-31-M-12.+// Measured: the SMT pad row sits at the BACK of the receptacle, and+// the body centroid is 2.5 mm forward of that. Centering body on pads+// (positionOffset 0) makes the back half of the body extend BEHIND+// the pads into the board — wrong geometry, even if numerics look+// "clean".+const BODY_FORWARD_MAG = 2.5++const pinLabels = {+  pin1:  ["GND1", "A1"],  pin2:  ["GND2", "B12"],+  pin3:  ["VBUS1", "A4"], pin4:  ["VBUS2", "B9"],+  pin5:  ["SBU2", "B8"],  pin6:  ["CC1", "A5"],+  pin7:  ["DM2", "B7"],   pin8:  ["DP1", "A6"],+  pin9:  ["DM1", "A7"],   pin10: ["DP2", "B6"],+  pin11: ["SBU1", "A8"],  pin12: ["CC2", "B5"],+  pin13: ["VBUS1", "A9"], pin14: ["VBUS2", "B4"],+  pin15: ["GND1", "A12"], pin16: ["GND2", "B1"],+} as const++// 16 SMT pads, pitch 0.5 mm, centered at y=0 so bounds.center = pad+// centroid (no holes included — see §Why we drop the holes).+const PADS: Array<{ port: string; x: number }> = [+  { port: "A1",  x: -3.35 }, { port: "B12", x: -3.05 },+  { port: "A4",  x: -2.55 }, { port: "B9",  x: -2.25 },+  { port: "B8",  x: -1.75 }, { port: "A5",  x: -1.25 },+  { port: "B7",  x: -0.75 }, { port: "A6",  x: -0.25 },+  { port: "A7",  x:  0.25 }, { port: "B6",  x:  0.75 },+  { port: "A8",  x:  1.25 }, { port: "B5",  x:  1.75 },+  { port: "B4",  x:  2.25 }, { port: "A9",  x:  2.55 },+  { port: "B1",  x:  3.05 }, { port: "A12", x:  3.35 },+]++export const UsbCReceptacle = ({+  name, pcbX, pcbY, pcbRotation = 0, schX, schY, schRotation,+}: {+  name: string; pcbX: number; pcbY: number;+  pcbRotation?: 0 | 90 | 180 | 270+  // SCHEMATIC placement — ALWAYS forward to the inner <chip>. Without these+  // the receptacle silently defaults to the schematic origin (0,0) and piles+  // on top of other parts (caught by `adom-tsci lint` Check 5). A wrapper+  // component MUST pass schX/schY/schRotation through, or it can't be placed+  // on the schematic.+  schX?: number; schY?: number; schRotation?: number+}) => {+  // Native CAD has mouth at local +Y. cad.rotation.z = pcbRotation+  // + rotationOffset.z (tscircuit adds them, CCW). We set+  // rotationOffset.z=0 so cad_z = pcbRotation. Then the mouth direction+  // in world frame is rotate((0,+1), pcbRotation CCW) =+  // (-sin pcbRotation, cos pcbRotation).+  const rad = (pcbRotation * Math.PI) / 180+  const mouthX = -Math.sin(rad)+  const mouthY =  Math.cos(rad)+  const posX = BODY_FORWARD_MAG * mouthX+  const posY = BODY_FORWARD_MAG * mouthY++  return (+    <chip+      name={name}+      pcbX={pcbX} pcbY={pcbY} pcbRotation={pcbRotation}+      schX={schX} schY={schY} schRotation={schRotation}+      supplierPartNumbers={{ jlcpcb: [USB_C_PN] }}+      manufacturerPartNumber="TYPE-C-31-M-12"+      pinLabels={pinLabels as any}+      cadModel={{+        objUrl: `https://modelcdn.tscircuit.com/easyeda_models/download?uuid=${USB_C_UUID}&pn=${USB_C_PN}`,+        rotationOffset: { x: 0, y: 0, z: 0 },+        positionOffset: { x: posX, y: posY, z: 0 },+      }}+      footprint={+        <footprint>+          {PADS.map(p => (+            <smtpad key={p.port} portHints={[p.port]}+              shape="rect" width="0.3mm" height="1.3mm"+              pcbX={`${p.x}mm`} pcbY="0mm" />+          ))}+        </footprint>+      }+    />+  )+}+```++## Per-edge placement recipe++For a board of width `W` centered at x=0 (so edges at ±W/2):++| Edge you want mouth to face | `pcbRotation` | `pcbX` | `pcbY` |+|---|---|---|---|+| South (bottom) | 0   | 0 or offset along x  | −(H/2) + 4 |+| East (right)   | 270 | +(W/2) − 4           | 0 or offset along y |+| North (top)    | 180 | 0 or offset along x  | +(H/2) − 4 |+| West (left)    | 90  | −(W/2) + 4           | 0 or offset along y |++`pcbX / pcbY` is the **chip origin** = **pad row centroid** position.+For a 64×32 board with USB-C on east edge: `pcbX=28, pcbY=0,+pcbRotation=270`. Pads at x=28 (inside the board by 4 mm), body at+x=28+2.5=30.5, mouth cantilevers to about x=34.5 (2.5 mm past edge).++### **Corollary: no SMT pad may extend past the board edge.**++The **body / mouth / mechanical shell** can and should cantilever+past the edge. SMT **pads** must not. If any copper pad (VBUS1/2,+GND1/2, DP1/2, DM1/2, CC1, CC2, SBU1/2) ends up at `|pcbX| > W/2`,+tscircuit grows the GLB board mesh asymmetrically to include the+overhang — and because the texture art is drawn with `pcbX=0` at+the canvas centre, the whole silkscreen / copper / annular-ring+layer renders shifted by `asymmetry/2` relative to the 3D mesh.+Every via drill appears off-centre in its annular ring, every+passive body appears shifted from its pad. The PCB itself is fine,+but the 3D view looks broken.++Real failure: a 72×32 board with the USB-C receptacle at `pcbX=35`.+That put the east-most SMT pads at `pcbX≈37.2`, growing the mesh+to `[-38.174, +37.2]` (centre `-0.487`) and shifting every copper+feature ~0.5 mm in the render. Fix: moved the receptacle to+`pcbX=33` (= `W/2 − 4 + overhang_0`). Mesh became `[-37.2, +37.2]`,+shift gone. **Use `pcbX = W/2 − 4` for the east edge; do not improvise.**++## Mandatory verification lint++**Run this after every build with a USB-C receptacle.** Not+optional. The only way we stopped getting USB-C wrong was by+gating on these two numeric checks:++```js+// scripts/check-usbc.mjs+import j from "./dist/lib/index/circuit.json" with { type: "json" }++for (const src of j.filter(x => x.type === "source_component")) {+  // Heuristic: USB-C receptacle = any chip with jlcpcb matching a+  // known USB-C family. Extend as new families are supported.+  const pn = src.supplier_part_numbers?.jlcpcb?.[0]+  if (!pn || !/^C(165948|2760486|283540|840342)/.test(pn)) continue++  const pc = j.find(x => x.type === "pcb_component"+                      && x.source_component_id === src.source_component_id)+  const pads = j.filter(x => x.type === "pcb_smtpad"+                          && x.pcb_component_id === pc.pcb_component_id)+  const cad = j.find(x => x.type === "cad_component"+                        && x.pcb_component_id === pc.pcb_component_id)+  const board = j.find(x => x.type === "pcb_board")++  const padCx = pads.reduce((s, p) => s + p.x, 0) / pads.length+  const padCy = pads.reduce((s, p) => s + p.y, 0) / pads.length+  const rot   = cad.rotation.z * Math.PI / 180+  const mX    = -Math.sin(rot), mY = Math.cos(rot)++  // Check 1: body forward of pads by ~BODY_FORWARD_MAG in mouth dir+  const fwd = (cad.position.x - padCx) * mX + (cad.position.y - padCy) * mY+  if (fwd < 1.5 || fwd > 3.5) {+    throw new Error(`${src.name}: body_forward=${fwd.toFixed(2)} mm; expected ~2.5.+      Body is ${fwd < 0 ? "BEHIND" : "too close to"} pads — geometry wrong.`)+  }++  // Check 2: mouth ray exits the board outline+  const ray = { x: cad.position.x + 12 * mX, y: cad.position.y + 12 * mY }+  const bx = board.center?.x ?? 0, by = board.center?.y ?? 0+  const inside = ray.x >= bx - board.width/2 && ray.x <= bx + board.width/2+              && ray.y >= by - board.height/2 && ray.y <= by + board.height/2+  if (inside) {+    throw new Error(`${src.name}: mouth points INTO the board at ${ray.x.toFixed(1)},${ray.y.toFixed(1)}.+      Cable would collide with FR4. Rotate receptacle 180° (toggle rotationOffset.z 0↔180).`)+  }++  console.log(`✓ ${src.name}: body_fwd=${fwd.toFixed(2)}mm, mouth dir (${mX.toFixed(2)},${mY.toFixed(2)}) exits board at (${ray.x.toFixed(1)},${ray.y.toFixed(1)})`)+}+```++**Wire it into your build loop:**++```bash+bunx tsci build lib/index.tsx --glbs --svgs && adom-tsci lint+```++`adom-tsci lint` does exactly the checks above (for every JLC USB-C+part in its known list) plus catches ghost chips and autorouter+silent-failures. Same exit-code gating. Prefer it over the inline+`node -e` above — it's shipped with the tool and maintained+alongside the lint rules.++Or if you only have `node` and not `adom-tsci lint`:++## Visual proof-of-placement (the toggle-and-shotlog ritual)++**Numeric lint can be gamed.** A body can satisfy `forward_dot >= 1.5`+yet still be visually misaligned with the pad geometry if a footprint+dimension is wrong. After every USB-C placement change, do this+three-step visual proof:++```bash+# 1. Top view with receptacle visible+adom-tsci view top --port 8853+adom-desktop browser_screenshot '{"sessionId":"<project>","maxWidth":1800}'+shotlog inject -c usbc-validate -d "USB-C placed on east edge, body visible cantilevered off the board" -s pup_screenshot <path>++# 2. Hide the receptacle to see the pads underneath+adom-tsci toggle-component J1 --port 8853 --hide+adom-desktop browser_screenshot '{"sessionId":"<project>","maxWidth":1800}'+shotlog inject -c usbc-validate -d "USB-C hidden; pads underneath should be flush with board east edge, all 16 pads on copper" -s pup_screenshot <path>++# 3. Un-hide+adom-tsci toggle-component J1 --port 8853 --show+```++The user reads the two screenshots side-by-side and confirms:+- Body cantilevers off the correct edge (mouth away from board)+- The pads sit inside the board (none hanging off)+- Pad row aligns with where the body's back edge is++**If you don't post both screenshots to shotlog, the user can't+verify, and you can't claim the placement works.** "The lint passes"+is necessary but not sufficient.+ ## Include ALL mounting holes — do NOT drop them to simplify the math  **The rule:** the TYPE-C-31-M-12 footprint must include all 4 plated@@ -461,18 +326,14 @@ the connector is held by 16 tiny SMT joints only, which tear off the board under cable insert/extract stress in well under 100 cycles. A USB-C port that rips loose after a week is a bug, not a trade-off. -**The §1 wrapper already includes all six holes at the native ArnavK-09-positions** (pads at local y=+2.449, drill supports at +1.18, anchors at-+1.41 and −2.774) with the native body offset `(0, −2.5)`. That is why-`adom-tsci lint` reports `body_fwd ≈ 5.3 mm` rather than 2.5: lint measures-from the pad-row centroid, and the pad row sits ~2.8 mm behind the chip-origin that the body offset is relative to. **5.3 is the correct number for-this wrapper — do not "fix" it toward 2.5.** (`adom-tsci lint` only requires-`body_fwd > 0.5` plus a mouth that exits the edge within 12 mm; the 2.5-figure in older text was for a pads-only footprint.)+**The compensation:** tscircuit computes `pcb_component.center` as+the bbox of pads + holes. For the TYPE-C-31-M-12 layout (pads at+local y=0, holes at y ∈ {-1.04, -1.27, -5.22}), the bbox center sits+2.61 mm south of the pad centroid. To keep the cad body 2.5 mm+forward of the pad row, bump `BODY_FORWARD_MAG` from 2.5 to 2.5 ++2.61 = **5.11** in the wrapper template. -If you ever build a different directional connector with asymmetric holes,-the compensation you need is:+Formula for any directional connector with asymmetric holes:  ```text BODY_FORWARD_MAG = body_over_pads + |bbox_center_y − pad_row_y|@@ -480,25 +341,33 @@ BODY_FORWARD_MAG = body_over_pads + |bbox_center_y − pad_row_y|  Measure the bbox shift by building once with holes included, reading `pcb_component.center` from `circuit.json`, and subtracting your-known pad row y. For TYPE-C-31-M-12 this is already baked into §1.+known pad row y.  Filed upstream as TSCIRCUIT_FEATURE_REQUESTS §6.1: `cadModel` should accept `boundsSource: "pads" | "all"` so we don't have to compensate in userland. Until then, the compensation above is load-bearing. +## Failure modes → diagnosis table++| Symptom | Cause | Fix |+|---|---|---|+| Body visible but pads hanging off the board edge | `pcbX/Y` too close to the edge; pads should be INSIDE copper | Move `pcbX` inward by ~4 mm |+| Mouth points into the board (butt cantilevers off edge) | `rotationOffset.z` is wrong by 180° | Flip `rotationOffset.z` (0 ↔ 180) |+| Body drifts diagonally off the pads at non-zero rotation | You used upstream `SmdUsbC` without the rotation-compensation | Switch to the local `UsbCReceptacle.tsx` wrapper above |+| Body centered on pads, but half the body extends into the board | `positionOffset` is (0,0); forgot `BODY_FORWARD_MAG * mouth_dir` | Add the forward-offset math |+| USB-C missing from Components HUD in adom-tsci | refdes doesn't match `/^J[\d_]/i` OR no cad_component emitted | Rename to `J1`/`J_1`; check `circuit.json` for `cad_component` row |+| `pcb_autorouting_error: Failed to solve 1 nodes` after adding USB-C | USB-C pads near high-density chip pin row | Move USB-C further from adjacent chips, or grow board |+ ## USB-C on a MOLECULE edge (not a bare `<board>`)  When the board is an Adom `<Molecule>` (machine-pin corners for the probing workcell), edge placement gets one extra wrinkle: **the molecule's wings grow the real board outline beyond the nominal `size`.** A `size="128x48"` molecule renders as a ~130.4×50.4 mm board (≈1.2 mm wing per side); large-pins grow it more.+pins grow it more. If you place the USB-C with `pcbX = nominal/2 − 4`, it+ends up several mm *inside* the real edge and the plug can't reach. -For the common case — **medium pins, nominal `size`, `wing="nominal"`** (every-LED nameplate: 64x24, 80x24) — `pcbX = nominal/2 − 4` is verified to give a-2.3 mm mouth overhang and 1.8 mm hole clearance (§3 reference numbers). The-rules below are for **large pins or absolute sizes**, where the wing growth-is bigger and the nominal formula lands the mouth inside the edge:+Rules that make it work:  1. **Use absolute sizing** — `size="138x58 absolute"` — so the real outer    edge is exactly `W/2`. The size increments are coarse (…96, 128, 160…)@@ -523,21 +392,6 @@ inside, plated anchor holes ~0.8 mm clear of the edge, body/mouth cantilevers ~2.5 mm proud so a cable seats. (W/2 − 4 is the audited, canonical value; earlier W/2 − 3.2 shoved the anchor holes ~0.03 mm from the edge — too close.) -## Failure modes → diagnosis table--| Symptom | Cause | Fix |-|---|---|---|-| **Body renders as a flat white/black box** | Viewer cuboid placeholder: served (re-baked) GLB has a 0-vertex J1 mesh because the modelcdn OBJ fetch failed during the bake; cache is keyed by circuit.json hash so it sticks | §5: check `adom-tsci console` for `cuboid placeholder`, delete the rebake cache entry, restart, `adom-tsci open`. Do NOT touch the footprint |-| Body visible but pads hanging off the board edge | `pcbX/Y` too close to the edge; pads should be INSIDE copper | Move `pcbX` inward to `W/2 − 4` |-| Mouth points into the board (butt cantilevers off edge) | wrong `mount` | Pass the edge the mouth should face |-| Body drifts diagonally off the pads at non-zero rotation | Upstream `SmdUsbC`, or an edited wrapper | Restore the §1 wrapper verbatim |-| Body centered on pads, half of it inside the board | `positionOffset` zeroed; forgot `NATIVE_POS_OFFSET` rotation | Restore the §1 wrapper verbatim |-| `pcb_pad_pad_clearance_error` on J1 | Old 16-pad wrapper (0 mm between same-net pairs) | Restore the §1 wrapper (merged pairs) |-| `trace[.J1 > port.A4, .J1 > port.A4]` self-trace / "disconnected endpoint" | Pin labels duplicated across pads (`VBUS1` on two pins) | §1 wrapper: one alias → one pad |-| USB-C missing from Components HUD in adom-tsci | refdes doesn't match `/^J[\d_]/i` OR no cad_component emitted | Rename to `J1`; check `circuit.json` for the `cad_component` row |-| `pcb_autorouting_error` after adding USB-C | USB-C pads near a dense chip pin row | Move USB-C further from adjacent chips, or grow the board |-| Charger plugged in, nothing lights | No CC pulldowns (or only one) | 4.7 k–5.1 k from CC1 and CC2 to GND |- ## Upstream feature requests  See `adom-tsci/TSCIRCUIT_FEATURE_REQUESTS.md §6` for:
guides/export-wiki.md+202−211
@@ -1,250 +1,241 @@-# adom-tsci/export-wiki — publishing a tscircuit board to the Adom Wiki+# adom-tsci/export-wiki — publishing a tscircuit molecule to the Adom Wiki  **Read this before running `adom-tsci export-wiki` on any project.**+The CLI is deliberately opinionated: it ships the interactive 3D+viewer, the source bundle, the plan.md, AND a comprehensive body+markdown to the wiki page in one shot. Skipping any of those pieces+makes the page useless to a future viewer (human or AI) — the goal+is "another agent can pick up exactly where this one left off" with+nothing missing. -> **Read `wiki-readme-html` from the [Adom Wiki Skill Pack](https://wiki.adom.inc/adom/adom-wiki-skillpack)-> first.** It owns the generic readme.html contract: the sandbox shims, the-> size budget, the `readme-height` protocol, the shadowing rule, theming. This-> guide covers only what is specific to shipping a **tscircuit board** through-> that contract, and defers to it everywhere else.--## Current state of the tooling (2026-08-20)--**`adom-tsci export-wiki` cannot upload.** It shells out to-`adom-wiki asset upload --asset-type tsci-viewer`, and `adom-wiki` has no-`asset` pillar any more (file-bearing verbs are now `repo push`,-`release upload`, `page attach`). Use `--no-upload` for the staging half —-which is still correct — and push the files yourself.--The older text of this guide described the wiki auto-rendering an iframe-section from a `tsci-viewer` asset. That is not what happens. Current pages-push `viewer/` as ordinary repo files and embed it from a hand-written-`README.html`, which the wiki renders in preference to `README.md`.--## What a board page ships+## What the CLI does, in order  ```-README.html                  the page body; embeds the viewer (see wiki-readme-html)-README.md                    plain-text source of record-board.glb                    Z-UP, for the wiki's own native 3D viewer-pcb.svg  schematic.svg       picked up by the native Symbol & Footprint section-viewer/                      the self-contained adom-tsci viewer bundle-plan.md                      the brief and every decision made against it-<slug>-source.tar.gz         full project source-docs/hero.png                16:10, 1920x1200+adom-tsci export-wiki [<project>] --slug <wiki-slug> \+    --page-type molecules \+    --label "Live 3D viewer" \+    [--rebuild] [--texture-resolution N] [--no-upload] ``` -## The three tscircuit-specific traps--Everything else in the sandbox is `wiki-readme-html`'s territory.--### 1. `board.glb` must be Z-up; `viewer/3d.glb` must not--glTF is Y-up by spec and that is what tscircuit emits — `Box0` spans about-±0.7 in **Y**, which is the board thickness. Two consumers, two conventions:--| file | consumer | needs |-|---|---|---|-| `viewer/3d.glb` | the adom-tsci viewer | **Y-up** — it rotates at runtime (`zUp: true` + `applyGlbZUpTransform`) |-| `board.glb` | the wiki's native 3D Model viewer | **Z-up** — it consumes the raw file |--Publish an unrotated `board.glb` and the wiki stands your board on edge like a-wall, which reads as a modelling error rather than a convention mismatch.-Rotate both and the tsci viewer double-rotates. The wiki viewer also looks-from −Y, so a board whose connector edge is at +X faces away; composing 180°-about Z is usually wanted.+1. **Confirms `dist/lib/index/circuit.json` exists.** Skip-rebuild is+   the default — `bunx tsci build` can take 10 minutes on dense+   boards. Pass `--rebuild` to force.++   **Fallback-dist search** (1.3.91+): if the in-place dist is missing,+   the CLI looks for an existing dist at known parallel locations+   before erroring out. Order: `~/project/adom-tsci-projects/<basename>/dist`,+   `~/project/adom-tsci/examples/<basename>/dist`,+   `~/project/tscircuit-projects/<basename>/dist`. Picks the freshest+   by `circuit.json` mtime. Many canonical molecules live in two+   places (the example-library copy and the local scratch-build+   copy); only one has a fresh dist at any given moment, and there's+   no reason to spend 10 minutes rebuilding when the other copy is+   already there.++   **The reflex when a build seems missing:** `ls+   ~/project/adom-tsci-projects/<name>/dist/lib/index/circuit.json`+   FIRST. Only rebuild if neither copy has a fresh dist. The CLI does+   this automatically now, but the same rule applies to any AI helper+   debugging "why doesn't this project have a dist?" — search before+   rebuilding.+2. **Resolves the GLB.** Default = `dist/lib/index/3d.glb` (1024-px+   texture). Pass `--texture-resolution N` to rebake at higher res+   (4096 typical for 100–200 mm boards; first bake ~75 s, cache hit+   ~190 ms).+3. **Stages the viewer bundle** at `/tmp/adom-tsci-export-<slug>/`:+   - `index.html` — `shell.html` rewritten for static hosting (fetch+     monkeypatch, walkthrough autoplay, all `glb`-style URLs+     remapped to relative paths).+   - `js/adom-3d-viewer.min.js`, `js/tscircuit-alphabet.js`,+     `js/environmentSpecular.env`, `adom.css`, `favicon.svg` —+     embedded into the binary, copied out at staging time.+   - `3d.glb`, `circuit.json`, `walkthrough.json`, `pcb.svg`,+     `schematic.svg` — the project's build artifacts.+4. **Tarballs the viewer bundle** as+   `<slug>-tsci-viewer.tar.gz`. Uploads via `adom-wiki asset upload+   --asset-type tsci-viewer`. The wiki extracts to+   `/static/<page-type-plural>/<slug>/tsci-viewer/<label-slug>/` and+   auto-renders an iframe section on the page.+5. **Builds a separate source bundle** at+   `/tmp/<slug>-source.tar.gz` containing every file an agent would+   need to recreate the molecule:+   - `lib/` (recursive)+   - `package.json`+   - `tscircuit.config.json`+   - `tsconfig.json`+   - `walkthrough.json`+   - `.npmrc`+   - **`plan.md`** (or `PLAN.md`) — the design brief. Required by+     convention; see "plan.md" below.+   - `README.md` (if present)+   Excluded: `dist/` (build output) and `node_modules/` (deps).+   Uploaded as a `file` asset.+6. **Uploads `plan.md` standalone** as a separate `file` asset so the+   wiki can render a one-click link to it without untar.++Failures on steps 5 / 6 are non-fatal — the viewer publish in step 4+is the load-bearing one and lands first. If a transient wiki failure+blocks the source-bundle upload, the CLI emits a hint with the retry+command.++## Walkthrough autoplay++The static export of `shell.html` registers a small bootstrapper that+**fires `walkthroughStart()` ~2 s after the page loads**. This is on+by default for the wiki context — the walkthrough is the canonical+"tour the board" experience for an external viewer who isn't going+to know to click anything. It can be opted out with+`?autoplay=off` on the iframe URL if a future wiki template wants to+defer firing it.++The walkthrough is loaded from `walkthrough.json` in the bundle. If+the project has no `walkthrough.json`, the autoplay no-ops gracefully+(no walkthroughStart call until the array is populated). Generate+one with:  ```bash-node scripts/glb-make-zup.mjs <pristine.glb> board.glb 180+adom-tsci walkthrough-gen        # auto-generate from circuit.json ``` -Verify by rotating probe vectors, not by squinting: `+Y → +Z`, and the-bounding box should come out with its thin axis on Z.+…or hand-edit `walkthrough.json` directly. The `_meta` block has a+`do_not_edit_by_hand` field — set it to a plausible-looking string+(e.g. "Hand-edited intentionally") to keep `walkthrough-gen` from+overwriting your edits. -### 2. The viewer bundle is 13 MB, and `render/` 413s over ~4 MB+## plan.md — the design brief -`wiki-readme-html` gives you the document-level size budget. This is the-per-file one: any single file served through-`https://wiki.adom.inc/<owner>/<slug>/render/<path>` over roughly 4 MB returns-`{"error":"File too large to render"}`. A `<script src>` cannot surface that;-it just fails.+**Every molecule publish should include `plan.md` at the project+root.** plan.md is the original prompt the project was authored from,+plus the refinements that came out during build. It carries forward+to the wiki so a future agent skimming the page can answer: -Ship `adom-3d-viewer.min.js` as 3.5 MB parts and reassemble client-side:+- What was the user actually asking for?+- What design decisions were made and why?+- What's intentionally out of scope? -```bash-split -b 3500000 -d js/adom-3d-viewer.min.js js/adom-3d-viewer.min.js.part-cat js/adom-3d-viewer.min.js.part* | cmp - js/adom-3d-viewer.min.js   # always verify-```+If the project doesn't have a plan.md when you run `export-wiki`,+**create one before publishing**. Reconstruct it from the original+prompt + the design conversation; check it into the project root.+Skeleton: -Two rules in the loader:+```markdown+# Plan — <Project Name> -- **Reassemble as BYTES, never text.** A multi-byte character can straddle a-  split boundary; decoding per part corrupts it. Concatenate `Uint8Array`s,-  then `new TextDecoder('utf-8').decode(all)` once.-- **Inject with `script.textContent`, not a blob URL.** `script-src` allows-  `'unsafe-inline'` but not `blob:`.+## Original prompt -### 3. GLB textures must be `data:` URIs, and externalising them does not work+> <copy the user's first ask verbatim> -`wiki-readme-html` already says `blob:` images are blocked. The tscircuit--specific sting: Babylon's glTF loader blobs every bufferView-backed image, and-this failure **throws** rather than degrading, killing the entire model load:+## Refinements during the build -```-Unable to load from ./3d.glb: /textures/0: … blob:null/… - Fallback texture was used-```+- <each meaningful refinement, one bullet per> -The obvious fix — write the images out as sibling `.png` files and point the-GLB at them — **does not work either**, because Babylon still blobs a relative-URI in a null-origin document. `data:` URIs are the only accepted form.+## Hardware decisions -```bash-node scripts/glb-datauri-textures.mjs <in.glb> viewer/3d.glb-```--Costs about 8% file size, and the script prunes the vacated bufferViews while-remapping every accessor.--## One more, if your page has both viewers--A hardware page renders the wiki's own native 3D viewer from `board.glb` above-your README. If your README's viewer iframe starts pulling 15 MB at the same-time, the native viewer's ~2.8 MB fetch is starved and it gives up with-*"Failed to load model — taking too long"*. The file is fine. Wait for `load`-plus ~2.5 s before arming the `IntersectionObserver` that loads your iframe.+- <each component choice with rationale> -Prefer auto-load-on-scroll over a click-to-load poster: a poster cannot be-driven by a synthetic click from outside the sandboxed frame, so it is-untestable in a ralph loop, and a still with a button on it reads as broken to-anyone who does not realise it is a button.+## Out of scope (deliberate) -## Diagnosing a blank 3D tab+- <what was explicitly NOT built and why> -If the viewer is blank, the storage shim from `wiki-readme-html` is missing or-ran late. Confirm that before chasing anything else:+## Build → publish workflow -```js-const s = viewer.getScene(), e = viewer.getEngine()-;({ lights: s.lights.map(l => l.name), env: !!s.environmentTexture,-    loops: e.activeRenderLoops.length, cam: String(viewer.getCamera()) })-// healthy: ["light","spotLight"], true, 1, "Name: adomCamera, type: ArcRotateCamera"-// broken:  [], false, 0, "null" ``` -Two things about the broken state that waste hours if you do not know them:--- **`viewer.getCamera()` returning `null` silently kills the walkthrough.**-  `walkthroughFlyTo` opens with `if (!scene || !cam) return;`, so every camera-  fly bails on line 4 while the step counter and narration keep advancing. It-  presents as "the walkthrough animates nothing", which is a different bug-  than the one you actually have.-- **A `typeof window !== "undefined"` guard is not enough.** `window` exists in-  a sandboxed iframe; the *origin* is null, so the property read itself throws.-  The shipped bundle still has one unguarded read-  (`localStorage.getItem("adom_camera_mode")`) behind exactly that wrong guard.-  Tracked in [`adom/wiki#60`](https://wiki.adom.inc/adom/wiki/issues/60).--## Measurement traps--Both of these produce confident wrong conclusions about a viewer.--- **`engine.getFps()` lies.** It reports 60 while `scene.render()` has never-  been called once. Count real frames instead:-  ```js-  let n = 0; scene.onAfterRenderObservable.add(() => n++)-  await new Promise(r => setTimeout(r, 1200))   // n === 0 -> nothing is drawing-  ```-- **An occluded window has rAF at zero.** `setTimeout` keeps running, so a-  walkthrough advances its steps and narration while the camera sits perfectly-  still — indistinguishable from a broken tween. Check `document.hidden`-  before believing any animation measurement, and either foreground the window-  (`pup_raise_os_window`) or force frames with-  `for (let i = 0; i < 90; i++) scene.render()`.--## Publish sequence+(See `adom-tsci-projects/Brianna-LED-Nameplate/plan.md` for a full+example.) -```bash-bunx tsci build lib/index.tsx --glbs --svgs --3d-png --pcb-png-adom-tsci lint--adom-tsci export-wiki . --slug <slug> --page-type molecules --no-upload-mkdir -p wiki-page/viewer && cp -r /tmp/adom-tsci-export-<slug>/* wiki-page/viewer/--node scripts/glb-datauri-textures.mjs <rebake.glb> wiki-page/viewer/3d.glb-node scripts/glb-make-zup.mjs        <rebake.glb> wiki-page/board.glb 180-split -b 3500000 -d wiki-page/viewer/js/adom-3d-viewer.min.js \-                    wiki-page/viewer/js/adom-3d-viewer.min.js.part-#  + the storage shim and the part-loader patch in viewer/index.html-#  + README.html per wiki-readme-html--adom-wiki page create <slug> --type hardware --kind molecule --org adom \-  --title "..." --brief "..." --because "..." \-  --headline "..." --subhead "..." --screenshot docs/hero.png --no-chrome \-  --source tscircuit --version 1.0.0--cd wiki-page-adom-wiki repo push <owner>/<slug> --message "..." \-  $(find . -type f | sed 's|^\./||' | sed 's/^/--files /' | tr '\n' ' ')-```+The CLI uploads `plan.md` standalone (one-click link on the page)+AND inside the source bundle (preserved alongside the rest of the+project source). -`repo push` requires explicit, repeatable `--files`; there is no-"push everything changed" mode.+## The body markdown — comprehensive, AI-readable -## Verify on a cold profile, from the wiki+The wiki page's `content` field — the markdown body — must give a+downstream agent everything they'd need to continue work without+reading external docs. **The CLI does not write the body for you;+you produce it via `adom-wiki page edit --field content` (or as part+of `page publish`).** Required sections: -Not your dev server, and not a browser that may have cached a working bundle.+| Section | What it captures |+|---|---|+| **Brief** (above the fold) | One-line elevator pitch. Same as the page brief. |+| **Live 3D viewer** | Pointer to the iframe, mention `?autoplay=off` opt-out. |+| **How it works** | The circuit story: what each block does, key constants, font/layout source. |+| **Hardware summary** table | Block / Part / JLCPCB / Notes for every refdes family. |+| **Mechanical** | Board dims, USB-C edge, workcell-pin info, silkscreen choices. |+| **Source layout** | The project's directory tree with one-line per file. |+| **Build commands** | The exact bash to clone → install → build → preview → publish. |+| **Color options / variants** (if applicable) | What's parameterised and how to retune. |+| **Known issues / lessons learned** | Things that bit during the build, in case they come up again. |+| **Build provenance** | Author, tooling versions, walkthrough authorship. |+| **Files attached** | Two sub-lists: **Source — recreate the molecule** (plan.md, source bundle) and **Build outputs** (GLB, SVGs, demo video). |++**Rule:** if a future agent has to ask the human user any factual+question about the project that they could have answered by reading+this page, the body is incomplete. Treat it as a writeup, not a+README stub.++## Hero image++`adom-wiki page publish` requires `--hero-image <path>` (the wiki+enforces it for non-app pages). The hero image is **the thumbnail**+that shows up on the molecule index page — heavy interactive viewers+can't be embedded in index lists, so a static iso render is the+right artifact for that role.++For an adom-tsci-published molecule:++- The **index/listing page** uses the `--hero-image` PNG as the+  thumbnail.+- The **full molecule page** has the interactive `tsci-viewer`+  iframe as its hero (rendered automatically by the wiki when a+  `tsci_viewer` asset is present).++Take the hero image with `adom-cli hydrogen screenshot panel+--tab-id <brianna-tab>` after setting the camera to `iso` view and+hiding the components HUD. ~1500 px wide is plenty.++## Verifying the publish++After `export-wiki` returns `OK: published …`, open the molecule URL+in a browser: -```bash-adom-bridge --ai-thread "<thread>" pup_open_window \-  '{"sessionId":"verify","profile":"verify","freshProfile":true,-    "url":"https://wiki.adom.inc/<owner>/<slug>/render/viewer/index.html"}' ```--- [ ] `lights` is `["light","spotLight"]`, not `[]`, and not a fallback you added-- [ ] `env: true` — the IBL loaded, so it looks like the local webview-- [ ] `activeRenderLoops.length === 1` (2 means something raced the bundle)-- [ ] walkthrough camera radius CHANGES between steps-- [ ] PCB / Schematic / Parts tabs render-- [ ] the wiki's native 3D viewer shows the board flat and readable-- [ ] every `render/` asset returns 200 to an unauthenticated `curl`-- [ ] no `localhost` / `/proxy/` / `.adom.cloud` in README.html — a published-      page must outlive the container that built it--## Known limitations, do not chase these--- **`MP1`–`MP4` stack at the schematic origin and `lint` fails on it.**-  `<Molecule>` renders `jsx(MachinePin, {type, name, pcbX, pcbY})`, and-  `MachinePinOrContact` in `@tsci/adom-inc.library` destructures only-  `{portHints, pcbX, pcbY, name}`, dropping schematic coordinates. No caller-  can fix it, including a hand-rolled `<board>`.-- **`schX`/`schY` is all-or-nothing.** Placing *some* parts disables-  auto-layout for the rest and stacks them at the origin.-- **`GET viewer/runframe/` 404s** on every load — the "tsci live" tab only-  exists against a dev server.-- **`--texture-resolution N` silently falls back** to the 1024 GLB with a hint-  about `rebake-glb.mjs`. The 4096 rebakes are already in-  `~/.cache/adom-tsci/rebakes/`; the keys are content hashes, so match one to-  your board with `scripts/glb-inspect.mjs`.--## Helper scripts--These live in the adom-tsci page repo under `scripts/`. `adom-tsci install`-deploys `SKILL.md` and the guides but **not** these yet, so fetch them:--```bash-for f in glb-datauri-textures glb-make-zup glb-inspect; do-  curl -fsSL "https://wiki.adom.inc/adom/adom-tsci/render/scripts/$f.mjs" -o "scripts/$f.mjs"-done+https://wiki-ufypy5dpx93o.adom.cloud/molecules/<slug> ``` +Confirm visually: -| script | what it does |-|---|---|-| `glb-datauri-textures.mjs` | embedded images → `data:` URIs, prunes vacated bufferViews, remaps accessors |-| `glb-make-zup.mjs` | wraps scene roots in a `ZUpRoot` (+90° about X), optionally composed with a Z spin |-| `glb-inspect.mjs` | node/LED counts, board extents, J1 placement — for matching a cache rebake to your board |+1. **Walkthrough auto-plays** within ~2 s of page load. Step text+   appears at the bottom of the iframe with Back/Next controls.+2. **3D scene is rendered** — the board (not the loading screen) is+   visible.+3. **Source bundle download works** — click the `*-source.tar.gz`+   link in the body, untar, confirm `lib/index.tsx` and `plan.md`+   are present.+4. **plan.md link works** — opens directly without untar.++If the walkthrough doesn't fire, check the iframe URL has no+`?autoplay=off`, and that `walkthrough.json` was non-empty in the+bundle (`tar -tzf <bundle>.tar.gz | grep walkthrough`). -## Worked example+## Troubleshooting -[`adom/evan-led-nameplate`](https://wiki.adom.inc/adom/evan-led-nameplate)-exercises every item on this page.+| Symptom | Cause | Fix |+|---|---|---|+| `dist/lib/index/circuit.json not found` | No build artifacts | Pass `--rebuild` or run `bunx tsci build` first |+| `--texture-resolution N` warns "no rebake-glb.mjs in cache" | First-time use without prior `adom-tsci start` | Run `adom-tsci start .` once to populate the cache, then re-export |+| `bundle is over 95 MB` warning | Big GLB (very high texture res) or many extra files | Drop `--texture-resolution` to 4096 or 2048 |+| `adom-wiki upload failed: page-type mismatch` | Slug exists at a different page-type | Either pass the matching `--page-type`, OR delete the existing page first (`adom-wiki page delete <type>/<slug>`) |+| Source-bundle upload fails but viewer publish succeeds | Transient wiki error | Re-run `adom-wiki asset upload <page>/<slug> --asset-type file --file /tmp/<slug>-source.tar.gz` manually — the CLI prints this hint |+| Walkthrough doesn't auto-play on the wiki page | walkthrough.json missing or empty | Run `adom-tsci walkthrough-gen` in the project, hand-edit if needed, re-export |++## See also++- `adom-tsci/guides/demo-recording.md` — making a 30-second narrated+  demo video to attach to the page (separate workflow; the live+  viewer is now the primary artifact, demo video is supplementary).+- `adom-tsci/guides/adom-usbc.md` — USB-C placement rules for the+  molecule before you publish.+- `adom-wiki` skill — the underlying CLI for asset uploads and page+  edits.
package.json+2−5
@@ -1,6 +1,6 @@ {   "slug": "adom-tsci",-  "version": "1.4.25",+  "version": "1.5.0",   "type": "app",   "description": "Interactive tscircuit board viewer in a Hydrogen webview \u2014 first-class 3D / PCB / Schematic tabs, an Inspect tool, a Fusion-style Measure tool, net x-ray, guided Walkthrough demos, one-click autorouter re-run, and auto high-res board-surface texture. Fully AI-drivable (CLI + HTTP + JS eval). Also publishes molecules/boards to the Adom Wiki.",   "dependencies": {},@@ -28,9 +28,6 @@     "VERSION",     "guides/adom-usbc.md",     "guides/demo-recording.md",-    "guides/export-wiki.md",-    "scripts/glb-datauri-textures.mjs",-    "scripts/glb-make-zup.mjs",-    "scripts/glb-inspect.mjs"+    "guides/export-wiki.md"   ] }
page.json+1−1
@@ -4,7 +4,7 @@   "slug": "adom-tsci",   "title": "Adom TSCI - Tscircuit Board Viewer",   "brief": "Interactive tscircuit preview in a Hydrogen webview — first-class 3D / PCB / Schematic tabs, Components & Nets HUDs, an auto-glow x-ray view, Inspect, Measure, a Walkthrough Demo, and a one-click `exp",-  "version": "1.4.24",+  "version": "1.5.0",   "tags": [     "tscircuit",     "pcb",