name: adom-hardware-component-publish description: > The one-stop-shop for publishing a HARDWARE COMPONENT (or molecule/board) to the Adom Wiki — everything you need in one place. Covers the whole lifecycle: what a component page is made of, the clone→edit→preview→push loop and its hard rules, how to make a great hero image (Hero Component Studio: a lit part on a procedural PCB or a molecule on a shadow ground, 16:10, overlay-safe, datasheet-backed), the animated 3D composite model, the interactive 3D viewer, ct thumbnails (symbol + footprint + 3D icons), the datasheet-accuracy audit, and every wiki push/pull gotcha we have hit. Use whenever someone wants to publish, update, or improve a component/molecule/board page on wiki.adom.inc. Trigger words: publish component, publish hardware, publish to wiki, component page, molecule page, board page, wiki page, hero image, component thumbnail, card art, 3d composite, insertion.glb, interactive viewer, ct thumbnails, datasheet audit, wiki push, wiki clone, page hero, set hero, land pattern, footprint viewer. PRIVATE — Adom only. metadata: type: reference

Publishing a Hardware Component to the Adom Wiki

The single place to look when you want to put a hardware component on the wiki and have it look right. "Component" here means any physical thing that gets a wiki page: a discrete part (resistor, MLCC, LED, inductor, connector, IC), a molecule (an assembled sub-board), or a full board. The recipe is the same shape for all of them; the differences are called out inline.

Living document. When you hit a new wiki issue or find a better recipe, add it to the right section, bump the version, and open a PR against adom/adom-hardware-component-publish. Never re-solve a problem that's already written here.

Private / Adom-only. Published under the adom org, visibility private.

First decide which kind of page you're making — a single COMPONENT (Path A) or a full BOARD / MOLECULE (Path B). The required contents differ (see "Two kinds of page" below). On a component page, the 3D viewer + cited datasheet content stand in for the datasheet: every fact is datasheet-accurate and traceable to a specific figure/table/page (cite your sources). A board page is the author's presentation of how their board works, not a cited datasheet.


The lifecycle at a glance

1. Gather inputs      datasheet (electrical + packing/tape&reel), CAD (STEP/GLB), KiCad sym/fp
2. Build the assets   3D composite (datasheet-accurate) · ct thumbnails · timeline viewer
3. Make the hero      Hero Component Studio → 16:10, overlay-safe, datasheet-backed  (§3)
4. Assemble ONE page  README.html = datasheet content + full-page timeline viewer, self-sizing (§5.0)
5. Preview            RENDERED preview via the readme-frame harness through the container proxy
6. Snapshot + approve save the pre-change files; get an explicit human go-ahead
7. Push               clone fresh → README.html + viewer-config.json + hero → rm retired files
8. Verify live        commit at HEAD · blobs 200 · /readme 200 · hero set · hard-refresh (cache)

Everything a page needs is below, section by section. If you only touch one thing (e.g. "make the hero better"), jump to that section — but the hard rules (§0) and push gotchas (§8) apply to every push.


Two kinds of page — pick your path FIRST

A single component (a part: an LED, resistor, MLCC, IC, connector) and a full board or molecule (an assembled sub-board) are different deliverables with different required contents. Decide which you're publishing, build the required contents for that path, then use the shared mechanics (hero §3, composite §4, viewer §5, wiki push §2/§8) to assemble and ship it. Both paths end as the one seamless full-page README (§5.0).

Path A — a COMPONENT page (a single part) · required contents

  1. Datasheet information — the whole point, and it must be COMPLETELY datasheet-accurate. This is the former "characterization page", merged into the wiki page. The datasheet PDF itself MUST be stored in the page repo (Adom standard — every component page has a datasheet); it's the source of truth and every citation links to it. Every single data point must be traceable to a specific place in that datasheet — cite the revision + page/table/figure, linked into the stored PDF (e.g. "VF 2.1 V typ · 2.6 V max — [IN-S42AT V1.1 p.3]", "2θ½ 130° — [p.3 fig.4]"). See §7/§7a for the citation format, §7b for the "datasheet changes ⇒ citations change" rule AND the mandatory datasheet revision+date line near the top of the page, and §7c for how to read a datasheet. Charts are redrawn to the datasheet curve, not invented. If a value isn't in the datasheet, it does not go on the page — flag the gap, never estimate. (See feedback_never-fabricate-data, feedback_characterization-pages-are-datasheets.)
  2. Interactive viewer (§5), with the modes the part warrants:
    • Static — the seated part with the standard Layers. If the component does something — a visual behaviour like emitting light or actuating — show it cleanly and datasheet-backed (e.g. an LED's glow colour from its λD, its FOV cone from 2θ½).
    • Timeline — a visual replacement for the visually-explainable parts of the datasheet (power-on, optical cone, reflow, tape & reel). Everything shown is datasheet-backed; the captions cite the datasheet (§5a datasheet-vs-measured rule).
    • Pick & Place — a datasheet-accurate carrier tape & reel, with either the standard drop-in ghost nozzle or a specific nozzle the user chooses.
  3. Solder joints — either the default fillets, or (preferably) the placement/shape the user specifies (where and how the solder sits on the pads).
  4. The geometry carries the facts — the viewer shows the real pad size, the chip model, and the correct orientation (cathode/pin-1), all datasheet-accurate.
  5. Symbol + footprint — the schematic symbol and PCB footprint (inline ct/ SVGs, since interactive symview/fpview iframes are sandbox-blocked, §8k).

Path B — a BOARD / MOLECULE page · required contents

  1. Interactive viewer (§5) — this is the centrepiece; a board/molecule is not a datasheet part, so it has no characterization / tape&reel / pick-and-place requirement. A board viewer has exactly TWO modes (an earlier draft split assembly/walkthrough/operation into three tabs — that was redundant; drop-in and explanation are ONE story, and an "operation" reel took the camera away from the user):

    • Assembly — the groupings drop in together, and the caption explains each as it lands. The board starts bare with an overview caption; then each functional grouping's parts fall in as one beat (all members of the power grouping together, then all the connectors, …) in the author's order, while a data-card caption (eyebrow / head / rows / note) explains what that grouping is and does. Camera frames each grouping — but any drag/wheel yields it to the user for 5 s, then it eases back (the exact convention of the single-component Timeline viewer; the viewer must never fight for control). If the board uses molecules, each drops in already populated and seats like a single component. Scrubber ticks mark group starts.

    • Static — the seated board, and the camera is entirely the user's. No timeline, no scripted moves, no caption sequence — hover tooltips, the section legend (click to isolate), section labels, and any author-specified visual functions as toggles (e.g. a "Power" button lighting the rail LEDs). Behaviours here are the author's call and must be labelled author-specified (§7B); nothing is invented. For rich animated behaviour (blink patterns, a scanning row, a display) use the LED-lighting / animated-emission method (adom-basic-part-page-upgrade, Caleb's playbook) — still as a user-triggered toggle, never a camera-stealing reel.

    • ASK THE AUTHOR — this is their presentation, so collect (don't invent) exactly:

      1. The groupings — which parts belong together. Default straight from the schematic sheets (§5c.4 derives sheet membership automatically — e.g. the MCU sub-sheet is a grouping, Power is a grouping); the author refines via blocks.json (split a sheet, merge two, name them).
      2. How each grouping should be explained — the caption content per group (what it is, the key parts/values, why it's there), plus the drop order.
      3. Static behaviours — should anything light up or move? Which LEDs, what states, what the toggle is called. Explicitly ask; "none" is a fine answer.
    • Choreography convention (the config, adom-board-viewer-config/0.2): the grouping is the unit of authorship. blocks[] (id, label, colour, one-liner) + modes.assembly{ order[], timing{fall_s, stagger_s, dwell_s, rest_s, drop_height_mm}, intro{caption}, captions{<group-id>: {eyebrow, head, rows, note, d?}} } + modes.static{ blurb, functions{...author-specified toggles} }. Per-group d overrides the default dwell. The author's whole input is one small JSON file; the geometry does the rest. Worked example: the feeder-reader mock's viewer-config.json; runnable viewer: scripts/board-viewer-template.html.

    • PACE — the first cut is always too slow. A caption you have already read is dead air, and a viewer that feels slow gets abandoned before the last grouping lands. Author the timeline 1.5× faster than feels right on the first pass — i.e. divide every duration by 1.5 (drop_height_mm is a distance, leave it). Use these as the starting defaults, not the numbers you slow down from:

      Key Value Why
      fall_s 0.47 the drop reads as a snap, not a float
      stagger_s 0.027 members of one grouping land as a beat
      dwell_s 6.3 enough to read eyebrow + head + rows, not to re-read
      rest_s 4.0 the closing look at the finished board
      per-group d 4 for short captions (intro, mounting) 2–3 rows do not need a full dwell

      Tune a group UP only when its caption genuinely has more rows to read — never tune the whole reel down. (Verified on adom/feeder-reader: the original 0.7 / 0.04 / 9.5 / 6.0 pass read as sluggish; these values are that reel at 1.5×.)

  2. Provenance per §7B — every table generated from the design files, DNP honoured, and every claim labelled generated / author-asserted / illustrative.

  3. Schematic + layout come from the two Adom viewer apps — do NOT hand-roll them. Earlier drafts of this skill invented their own board maps (scripts/kicad_board_map.py) and leaned on the wiki's plain render/sheet-*.svg images. Both are superseded: there are real, maintained apps that render the EDA's own output and add live interactivity, so a board page embeds those instead of static pictures.

    Need App Command
    Schematic (.kicad_sch, every sheet) adom/adom-schematic adom-schematic embed --file <sheet>.kicad_sch --out sch-<n>.html
    PCB layout (.kicad_pcb) adom/adom-2dboard adom-2dboard embed --file <board>.kicad_pcb --out layout.html
    • embed emits one self-contained interactive HTML file (SVG + metadata + viewer JS inlined, no external assets) — which is exactly what the wiki's sandboxed readme frame can host, and it keeps working on a private page with no cap plumbing at all (see §8k-vii).
    • What you get over a flat image: schematic — hover a symbol for KiCad-style highlight plus live MPN / stock / price / LCSC / wiki link, click to pin the card, and a component list linked to the drawing. Layout — click any pad/via/trace/zone to highlight the whole net while the rest dims, hover for pad number/size/shape/side/net + live part data, independent layer toggles and Top/Bottom mirroring.
    • Pass --sheet-dir or you silently publish a one-sheet schematic. embed --file root.kicad_sch alone reports 0 child sheets bundled even when the sub-sheets sit right beside the root — hierarchical children are only pre-rendered when you point at their directory: adom-schematic embed --file <root>.kicad_sch --sheet-dir <dir> --out sch.html3 child sheets bundled, and the embed's SHEET selector switches between them instantly with no reload. One embed covers the whole hierarchy, so you do NOT need one iframe per sheet. (On adom/feeder-reader the root holds 25 of 138 parts — without the flag the MCU, Power and CS sheets were simply absent and the page looked complete.)
    • Get the sources from the design machine, not from whatever copy is lying around the container: pull with adom-desktop pull_file '{"filePaths":[...],"saveTo":"<dir>"}' and sha256-compare the root sheet + board against what the page already publishes before using them. Stale local checkouts of the same project are common and differ silently (a Jul-10 copy had 18 root symbols against the published 45).
    • The layout embed needs only the .kicad_pcb; side panels auto-hide below 620 px, so give each frame real height.
    • Install: adom-wiki pkg install adom/adom-schematic adom/adom-2dboard. Both need KICAD_SERVICE_API (service-kicad) for the authentic render; the layout viewer self-renders if it can't reach it (and looks less like KiCad, so prefer having it up).
  4. page.json mapping for a board (the schema is component-shaped; use this convention): component.mpn = the KiCad project name, component.manufacturer = the org (e.g. Adom), component.category = board, component.model_3d_path = the baked board GLB. Mandatory files: the board GLB, the KiCad source (or a pointer to where it lives), and the two viewer embeds from item 3.

  5. NO DATASHEET on a board or molecule page. A board is not a catalogue part — it has no manufacturer datasheet, and a page that shows one is asserting a document that does not exist. Concretely: ship no PDF in the repo. The wiki auto-scans any PDF into a "Datasheet" section and there is no manifest opt-out (molecule-publish, page-polish rules), so a stray <project>.pdf silently grows a datasheet section nobody authored. The schematic and layout viewers of item 3 ARE the board's documentation — they replace both the datasheet and the "Symbol & Footprint" slot (a board has no schematic symbol either). If a board genuinely needs long-form docs, put them on a dedicated docs page or attach them as release assets, never as a PDF in the page repo. (Path A component pages are the opposite — there the datasheet is mandatory and every number cites it, §7.)

  6. Any additional information the user wants to add — a board page is theirs to compose.

Everything else — publishing mechanics, the hero image and grid thumbnail (§3), the wiki clone→push loop (§2), and every gotcha (§8) — is shared by both paths and applies to each.


The gold-standard page — what "done" looks like

The reference build is aravk/in-s42atr (Inolux red 0402 LED) — the COMPONENT (Path A) gold standard. A board/molecule (Path B) shares the same shape (one seamless full-page README, the viewer as the hero, the ADOM hero image + grid thumbnail) but its content is the viewer-as-presentation plus whatever the author adds — not a cited datasheet. Aim a component page at this shape:

  • ONE seamless full-page. The README.html IS the page. It is a single sandboxed iframe (#readme-frame) that grows to fit its content (§8k-0b) — no boxed 600px widget, no "click through to the datasheet". A visitor lands on the full-page 3D viewer and scrolls down through the entire datasheet in one continuous document.
  • The interactive 3D viewer is the hero of the page, full-bleed and viewport-tall at the top (§5). Below it flows everything a datasheet has: spec cards, electrical table, operating conditions, package & land-pattern dims, the emission/IV/derating/brightness charts (redrawn to the datasheet), a live resistor calculator, environmental/handling, the symbol + footprint, pricing, and document links. The 3D viewer + these facts replace the PDF for everyday reference.
  • There is no separate characterization page. The old <MPN>.html char blob is merged into the README and deleted — one canonical page, not two. (On in-s42atr the merge pulled the char content in and removed IN-S42ATR.html.)
  • Everything datasheet-backed, tooltips split datasheet-vs-measured (§5), hero rendered in Hero Component Studio at 16:10 (§3).

The rest of this skill is how to build each piece. If you're making a new page, this is the target; §5.0 is the assembly recipe for the single-page README.


0. Hard rules (never skip — this is why pages don't get clobbered)

  1. Always pull fresh before editing. Someone else may have pushed since you last looked. adom-wiki repo clone <owner>/<slug> --dir <fresh> immediately before you edit.
  2. Snapshot before push. Copy the pre-change files to a timestamped dir and print the path, so revert is a one-liner. (adom-wiki repo clone is NOT a git repo — see 8j; copy files by hand.)
  3. Preview before push, then get explicit human approval. A rendered preview (pup/Hydrogen screenshot, or the served HTML), NOT curl 200. No preview → no push. Ever.
  4. Only push the files you changed (--files a b c), and only the page you're editing. Never touch another page.
  5. Everything must trace to the datasheet. Every electrical number, every hero glow colour, every tape/reel dimension, every chart curve. Cite the datasheet page. If it's a guess, remove it or flag it explicitly — never invent. Characterization pages ARE datasheets (§7).
  6. Real changelog required (WIKI_STRICT_AUTH is on): -m "<≥2 words, ≥10 chars, specific>". Placeholders (update, release v1) are 400-rejected.

1. Anatomy of a component page

A component page is a small file repo at <owner>/<slug>. Typical contents:

File What it is Required?
page.json metadata: slug, type, description, component{mpn,manufacturer,category,model_3d_path}, hero{type,path}, dependencies yes
README.html the whole page — the merged single-page document (viewer + full datasheet content), served in the self-sizing readme-frame (§5.0, 8k-0b). .html wins over .md; .md strips iframes. Keep a short README.md in the repo as source-of-record yes
screenshots/hero.png the hero image — one 16:10 image used for BOTH the page header and the grid card (§3) yes
<MPN>-datasheet.pdf the datasheet — MANDATORY on every component page (§7); source of truth + citation target yes (component)
viewer-config.json the timeline viewer's captions / tooltips / chapters config (§5) — push it alongside the README so the embedded viewer's text loads interactive pages
<MPN>.glb the canonical 3D model the viewer loads by name (see 8b) for anything 3D
<MPN>.insertion.glb the animated composite (part drops onto its footprint) — what the timeline viewer loads composite pages
<MPN>-viewer.html the interactive 3D viewer source (the timeline viewer, §5). Its content is inlined as srcdoc into the README; keep the file as the editable source interactive pages
adom-3d-viewer.min.js Colby's shipped viewer bundle (the viewer fetch-injects it by absolute URL) interactive pages
<MPN>.kicad_sym / .kicad_mod KiCad symbol / footprint source if it has EDA
ct/ thumbnail bundle: 3D icons + symbol SVG + footprint SVG (used inline in the page, since interactive symview/fpview iframes are sandbox-blocked) + manifest (§6) standard

Note: the standalone <MPN>.html characterization blob and the <MPN>-symview/-fpview.html iframe viewers are retired in the gold-standard build — the char content merges into the README and the symbol/footprint become the static ct/ SVGs (interactive src= iframes are blocked in the readme sandbox, §8k). Old pages may still have them; new pages should not.

type in page.json is component for discrete parts and molecules alike (the BME690 and USB3-Ethernet molecules both use type: component, category Sensor / board). There isn't a separate "molecule" page type — a molecule is a component whose model happens to be a whole board.

Served asset URL (memorise): https://wiki.adom.inc/blob/component/<slug>/<FILE>. This is what the page HTML and the 3D viewer fetch. It's also the URL your browser loads the hero from — not the /api/pages/... path (see 8m).


2. The wiki CLI + publish lifecycle

One CLI over the whole git-wiki API: adom-wiki. Every call takes --json.

Prefer the runnable module scripts/publish_wiki_page.py — stdlib-only wrappers around every step (ensure_author, find_page_owner, clone_page, snapshot, push_files, set_hero, verify_asset, publish_new_package) with the gotchas baked in. Read it top-to-bottom to learn the flow, or import it to drive a publish. The raw commands below are the ground truth.

# identity (once per container) — WIKI_STRICT_AUTH needs a real author + changelog
adom-wiki whoami
adom-wiki set-author                       # SSO your Adom identity into the wiki

# find the page and its OWNER (owner is not always `adom` — see 8a)
adom-wiki discover search -q "<mpn>" --limit 5
adom-wiki page stats <owner>/<slug>        # 404 => not under that owner

# --- EXISTING page: clone → edit → preview → snapshot → approve → push ---
adom-wiki repo clone <owner>/<slug> --dir <fresh>
#   ...edit locally, RENDERED preview, snapshot, get approval...
adom-wiki repo push <owner>/<slug> --files <f1> <f2> -m "<real changelog>"
adom-wiki repo log <owner>/<slug> --limit 5    # confirm ONLY your commit landed

# --- HERO (page card image) — takes a REPO-RELATIVE path, not a local file (see §3 / 8n) ---
adom-wiki repo push <owner>/<slug> --files screenshots/hero.png -m "hero: <what changed>"
adom-wiki page hero <owner>/<slug> --image screenshots/hero.png    # NOTE: no --changelog on this verb

# --- NEW page: scaffold + publish ---
adom-wiki pkg init <slug> --type component
adom-wiki pkg publish --org adom --private -m "<changelog>"        # from the pkg dir

Verify a pushed asset authenticated (private pages 403 unauthenticated — see 8h):

curl -s -H "Authorization: Bearer $(cat ~/.config/adom-wiki/token)" \
  "https://wiki.adom.inc/blob/component/<slug>/<FILE>" -o /tmp/x -w "%{http_code} %{size_download}\n"

The authoritative confirmation a push landed is adom-wiki repo log (your hash at HEAD); then cross-check the served byte count equals what you pushed (a stable URL means HTTP 200 alone doesn't prove the new bytes are live).


3. THE HERO IMAGE — do this well, it's the first thing anyone sees

Every browsable object gets exactly one hero: a single picture that lets a human identify the thing in a fraction of a second, without reading the title. If you can't tell ten of these apart at a glance in the grid, the design is broken. The hero is used in two places from one file — the page header AND the landing-grid card. There is no separate thumbnail asset (page.hero_thumbnail is always null; the grid renders the hero file directly).

3.1 Use Hero Component Studio

~/project/hero-component-studio (the hero-component-studio skill). A small web app + CLI that frames a component in a studio scene, lets you tune every variable live, and renders headlessly at full quality. Every function has a CLI verb — you never have to click.

hero-component-studio serve --detach          # starts on 8872
hero-component-studio show                     # opens the Hydrogen webview tab
hero-component-studio mode component|molecule  # pick the scene
hero-component-studio set part <MPN>           # or: set molecule <name>
hero-component-studio list                     # every setting: value, range, what it does
hero-component-studio save <name>              # save a preset;  load <name> to reuse
hero-component-studio render --width 1344 --height 1200   # writes out/<subject>-hero.png

Two scene modes:

  • component — one part standing on a generated circuit board (routed traces, flat under-mask vias, scattered scenery chips, a keep-out around the hero). This is the LED/passive path.
  • molecule — a finished assembly on its own, no invented circuitry (a molecule IS a real board), sitting on an invisible shadow-catching ground.

Getting your own model in (validated + de-Draco'd on ingest):

hero-component-studio add-part     ./<MPN>.glb
hero-component-studio add-molecule ./<name>.glb        # wiki molecule: adom-wiki repo clone, point at its render/board.glb

3.2 The rules that make a hero good (each cost real iteration)

  • 16:10 aspect, rendered natively — never scaled to fit. The wiki grid slot is 16:10; a 1200×630 image letterboxes with bars top and bottom on the card. Render at 1920×1200 (canonical) — or the image panel at 1344×1200 if you composite a text block over the left 30% (see 3.3). If you must reshape an existing render, crop, don't stretch: background-size:cover scales uniformly and crops the overflow; scaling one axis distorts the part. The canonical hero-image spec (adom/adom-wiki-hero-image) actually wants 1600×1000 @ deviceScaleFactor 2 = 3200×2000 for retina crispness — render there if you want it razor sharp.
  • Shoot from the ISO view. The hero camera is the classic isometric three-quarter: camAzimuth ≈ ±45–55°, camElevation ≈ 30° — the angle that shows the subject's top AND two sides at once, the way every CAD tool's "ISO" button frames a part. Straight-down reads like a board map, edge-on hides the body; the iso view is the standard for both component and board heroes (the feeder-reader hero is azimuth −52° / elevation 29°).
  • Overlay-safe framing. The finished hero draws a title block over the lower band. Framing on the part's centre puts half of it under that band, clipping the pads. autoClearOverlay (on by default) pans the camera (never rotates — your angle survives) until the lowest projected corner of body + pads clears overlayCutoff (default 0.345 = bottom 34.5%). Hit the Band button to draw the reserved zone in the viewport.
  • Centre on the VISUAL mass, not the bounding box. A board with pins hanging below reads "high" even when its bbox is perfectly centred. Measure the luminance centroid and correct with panX / panY (in units of the subject radius; right-click-drag in the viewport writes them). On the BME690 the bbox was centred to 0.6% but the visual centroid was off by ~3.7% vertically.
  • The hero renders at TRUE scale; the scenery is what you size. In component mode the hero part is always 1:1 — the camera fits to it, so it dominates through framing (fov / margin), not by being scaled up. chipScale sizes the scenery chips as real packages (by body length: 0.6 = 0201, 1.0 = 0402, 1.6 = 0603, 2.0 = 0805) — so pick scenery that reads right next to your hero's package (a 0402 hero looks natural amid 0201–0603 scenery). keepout is the clear radius around the hero.
  • Every scenery component has a trace in AND out (enforced in the generator). A chip with no routing reads as a part that does nothing; the router guarantees both terminals of every scenery chip connect outward to two different nodes. Vias and the hero's own pads join nearby nets too.
  • Everything datasheet-backed (§0.5). An emitter's glow colour comes from its dominant wavelength λD (RGB → white; every other LED → its own λD colour), the FOV cone from 2θ½. Never reuse another part's numbers.
  • Mouse ⇄ sliders. Orbit (left-drag) writes camAzimuth/camElevation, pan (right-drag) writes panX/panY, zoom (wheel) writes margin — so a view you find by dragging survives Save and renders identically. Save a look as a preset (save <name> / load <name>); the browser clipboard is blocked in the webview, so there's no "copy", only presets.
  • The render is NOT a screenshot. It renders at 2× with SSAA + 4× MSAA and downsamples — that's what removes jagged edges. (EffectComposer silently discards the renderer's antialias:true, so the composer needs its own multisampled target — already handled in the app.)
  • Preview aids never render. The light guide (showLightHelper) and the overlay band are tuning overlays; the renderer forces them off, so they never bake into the image.

3.3 The compositor (title block + branding)

The bare render is a part on a dark background. To get the ADOM-branded card (wordmark, title, MPN + manufacturer, badge on a dark gradient), composite it through the Hero Studio compositor (hero-studio-local, the local Node compositor in the led-hero scratchpad):

  • Add a record to hero-studio-local/data/components.json: name (big title), spec (subtitle = MPN), mfr (trails the MPN), badge (ADOM BASIC PART for parts, ADOM MOLECULE for molecules), family, owner, slug. All copy comes from the wiki page.json + README — never invent a title.
  • The compositor places the render panel in the right 70%, fades its left edge under the gradient, and draws the text block over the left 30%. Type is all in cqw, so it holds at 2000×1250.
  • Gotcha — displayEntry() drops unknown fields. The compositor rebuilds a fresh object from the record; a new field (e.g. badge) won't reach the overlay until you thread it through displayEntry(). Symptom: your change to the data does nothing.
  • The compositor caches components.json at startup — restart its Node server after editing data.
  • Output at 1920×1200; it verifies the bottom-third text column stays text-free.

3.4 Setting the hero on the page

page hero --image takes a repo-relative path, not a local file. So: clone fresh → copy the PNG to screenshots/hero.pngrepo push --files screenshots/hero.pngpage hero --image screenshots/hero.png. The wiki warns if the aspect isn't 16:10 ("crops left/right on the grid").

Caching: the served hero carries cache-control: max-age=86400. After a re-push the origin is correct immediately (confirm with a cache-busted curl ...?cb=$(date +%s)), but a browser holds the old image for 24 h — a hard refresh (Ctrl/Cmd+Shift+R) is the fix; closing/reopening a tab is not. The page loads the hero from /blob/component/<slug>/screenshots/hero.png, so verify that URL, not the /api/pages/... one.

3.5 Sharing a hero for review

To send a render to a teammate for feedback, adom-google DMs it as you (better than the adom-gchat webhook, which only posts to shared spaces). Caveats learned the hard way: a human account cannot send a Chat card (bot-only) and driveDataRef attachments silently drop — so upload the PNG to Drive (api --upload-file, --content-type image/png), share it anyone-with-link reader, and DM the Drive link as text (durable, unlike the container proxy URL which dies when the container sleeps).


4. The 3D model / animated composite (<MPN>.glb / <MPN>.insertion.glb)

The centrepiece of a characterization page: the component drops onto its own footprint with a smooth insert animation and a Layers toggle. See scripts/build_composite_led.py (LED, 3 materials + light) and scripts/build_composite_cap.py (cap, 2 materials) for full worked builders. Key facts:

  • All metres, Z-up, 1:1 scale (matches the wiki viewer + machinecontactmedium).
  • Node names the viewer recognises as Layers: fr4_board, pad_top, silk, solder_top, plus <MPN> (the part). Board/pads/solder/silk are built from the part's .kicad_mod.
  • Insert animation = translation channel on the <MPN> node, LINEAR, keyframes [0, 1.2, 3.0, 4.2, 6.0] s, Z [0, 0, LIFT, LIFT, 0]. LIFT = 3 × the component's measured height (z.max − z.min from geometry) — auto-scales per part.
  • Layers menu = FIXED vocabulary. The shared viewer only turns a node into a toggle if its name is in a hardcoded map (fr4_board, pad_top, pad_bottom, solder_top, solder_bottom, paste_top, paste_bottom, silk, silk_top, silk_bottom, barrel, pin1_marker). Custom nodes (LED_lit, a FOV cone) and the part body do not appear — no extras escape hatch (only extras.tooltip is read). Custom toggles need the viewer owner (Colby) to add a key. Design GLBs to degrade gracefully when the toggle doesn't exist (e.g. the LED renders lit).
  • Real lights: embed via KHR_lights_punctual. At mm scale, point-light intensity must be tiny (~4e-5) — inverse-square blows the board white at 0.3. Parent the light to <MPN> so it rides the animation. Red light on a green FR4 board reads warm/orange (green absorbs red — physics).
  • Emissive brightness: boost with KHR_materials_emissive_strength.
  • Side embossing (part number/value): cadquery boolean-cut the text into the STEP; auto-fit the font to the measured width×height taper so the whole string fits off the terminations.
  • Colouring a re-tessellated (embossed) mesh: transfer material per-triangle by densified nearest-neighbour from the original colour GLB (a plane split only works for a clean 2-material boundary; 3-material parts need densified NN or you get zigzag artifacts).
  • Translucent overlays (FOV cone, keep-out): build in world coords as their own top-level node, double-sided BLEND material, added to the insert animation's translation channel. Drive dimensions from a datasheet spec. Not in the fixed Layers vocabulary → renders always-on until Colby adds a toggle.
  • Height correction: apply a node scale=[1,1,h_target/h_model] to <MPN> (and any glow node) to match a datasheet height — avoids re-plumbing the emboss/NN pipeline.

5. The interactive 3D viewer — the model somewhat replaces the datasheet

The single most valuable thing on a single-component page. The idea: a person should be able to learn the part from the 3D viewer without opening the PDF. That only works if every datasheet-accurate fact is baked into the model and its tooltips — the body dims, the pad/land pattern, the optical cone, the tape & reel, the electricals — each either measured live off the geometry or shown as a datasheet-cited row. If a number isn't in the model or a tooltip, it isn't in the viewer, so put it there (§4 builds the geometry; the tooltips below carry the rest).

Both paths use Timeline, but they mean different things (this supersedes an earlier note that said Timeline was component-only):

  • Component (Path A) Timeline = the part's visual datasheet story — power on → optical cone → lift → solder reflow → tape & reel. Every beat is datasheet-backed. This is what the viewer-config.json captions/tooltips below describe.
  • Board/molecule (Path B) Timeline = the author's presentation of the board — a walkthrough of its functional sections the user defines (power: ESD → fuse → LDO; user input: buttons; …). It's a narrative, not a fixed lifecycle. It also has no tape&reel / pick-and-place (a board isn't reeled). Its Static mode is components (and pre-populated molecules) dropping into place, choreographed by the author.

🛑 START HERE — never write a viewer from scratch

The viewer is not re-invented per page. Every page's viewer is the SAME base plus a config file. Rebuilding it freehand is how pages drift, and it is the single easiest way to waste a day.

The base is Colby's shipped bundleadom-3d-viewer.min.js, exposing window.Adom3DViewer (the same engine as the wiki's own native viewer). It is used UNMODIFIED: all our behaviour is host-page code layered on top, so it stays purely additive and can be offered back upstream. Do not fork it, patch it, or vendor a different three/Babylon build.

Copy the template that matches your path, then edit only its config:

Path Template to copy Live page to compare against
A — component adom-basic-part-page-upgradeassets/viewer-template.html aravk/in-s42atr
B — board/molecule this skill → scripts/board-viewer-template.html adom/feeder-reader

The layer cake — know which layer you are allowed to touch:

Layer What it is Do you edit it?
adom-3d-viewer.min.js Colby's engine — scene, camera, picking, GLB load NEVER
*-viewer.html template our host page: modes, captions, legend, tooltips, timeline rarely — fix bugs upstream in the template, not per page
viewer-config.json the per-page authoring surface — blocks, order, timings, captions, static toggles YES — this is the file you write
<slug>.glb + board-index.json the geometry and its refdes→section index (§5c) generated, never hand-edited

So the actual per-page job is: bake the GLB (§5c) → write viewer-config.json (Path B item 1) → drop in the template unchanged. If you find yourself writing scene/camera/animation code, stop — that belongs in the template, and if the template can't express it, extend the template once so every page gets it.

Why this matters beyond tidiness: the template already encodes the load-sequence fixes (§5b), the sandbox/storage shims, the cap plumbing for private pages (§8k-vii), the camera-yield convention, and Colby's exact ground/sky. A from-scratch viewer silently loses all of it and the failures look like "the viewer is broken on the wiki" rather than "we re-derived it wrong".

The full worked reference is the adom-basic-part-page-upgrade skill (assets/viewer-template.html).

5.0 Assembling the single-page README (the gold-standard build)

The whole page is one README.html = the datasheet content with the viewer as a full-page srcdoc at the top. To build (or rebuild) it:

  1. Start from the characterization content (the spec cards / tables / charts / calculator). If a standalone <MPN>.html exists, its body is the base; then delete <MPN>.html (one canonical page). If not, author the content directly.
  2. Embed the timeline viewer as a full-bleed srcdoc at the top (§5a below). Full-width (break out of the text column), viewport-tall.
  3. Convert every nested src= iframe — they are sandbox-blocked (§8k): the 3D viewer → the srcdoc viewer; symbol/footprint viewers → inline ct/ SVG <img>s; any molecule/board viewer → a link.
  4. Absolute-URL everything the srcdoc fetches — the bundle, the GLB, viewer-config.json, and any <img> — to https://wiki.adom.inc/blob/component/<slug>/… (relative 404s in the readme sandbox).
  5. Fix stale URLs — old pages often reference a dead host (wiki-ufypy5dpx93o.adom.cloud/static/ libraries/…); rewrite to the current blob host.
  6. Add the self-sizing script (§8k-0b): size the viewer to screen.availHeight − ~200, then parent.postMessage({type:"readme-height", height: document.documentElement.scrollHeight}, "*") on load/resize so the readme-frame grows to fit — no box-in-box scrollbar.
  7. Push together: README.html + viewer-config.json + screenshots/hero.png. Then repo rm the retired <MPN>.html / -symview.html / -fpview.html.

Preview by mirroring the wiki's readme-frame + resize handler locally and loading through the container proxy (headless puppeteer in the container reaches 127.0.0.1; pup can't). Verify the frame grows and the viewer renders before pushing.

5a. Timeline mode + the config-driven text (viewer-cli)

The Timeline viewer is the PRIMARY, canonical viewer — and it is under active development. Not the old Static/PnP-only one. It lives in a work dir (/home/adom/project/in-s42atr-work/… for in-s42atr). Pull it FRESH before every embed/push — the copy on the wiki goes stale the moment it's edited. Never re-embed a cached copy. Regeneration is mechanical (steps 4→7 above); the viewer already ships the sandbox storage shim + bundle fetch-inject, so no other adaptation is needed.

The viewer has three modes via a segmented control: Timeline (a ~56 s narrated explainer reel — chapters: the part → powered on → viewing angle → lift off → solder → tape & reel → rest), Static (inspect the seated part), and Pick & Place (a nozzle lifts the part from a datasheet-exact carrier tape and seats it). Every scripted camera move, caption, and tooltip is data, not code:

  • viewer-config.json is the editable source of truth for the Timeline captions (the data-point cards + t0/t1 timing = when each appears), the hover tooltips, the chapter marks, and the swappable tape/nozzle GLBs. The viewer merges it over built-in defaults, so it works standalone but every bit of text is authorable.
  • ./viewer-cli authors it (adom-cli conventions — OK:/ERROR:/Hint: + --json): caption list|set|add|rm, tooltip list|set|add|rm|enable|disable, chapter list|set, assets set-tape|set-nozzle, verify (headless load + config⇄viewer check), doctor. It validates timing (t0<t1) and validates a swapped GLB against the loader (rejects Draco / external buffers, warns on node transforms, reports the bbox).
  • The datasheet-vs-measured rule (non-negotiable). A tooltip's rows are either datasheet rows — editable via viewer-cli tooltip set --row — or measured rows computed live from the model geometry, which are never edited. Every stated spec stays datasheet-cited; every measurement stays honest. tooltip list tags which tooltips also append measured rows. Do NOT add a way to hand-type a measured value.

5b. Load-sequence gotchas (building the host page on Colby's bundle)

  • Storage shim BEFORE the bundle loads. Wiki blobs render sandboxed (no allow-same-origin) → localStorage/sessionStorage throw on access → Colby's SceneBuilder aborts (no lights/IBL/ shadows/skybox). Shim both with no-ops first.
  • Fetch-inject the bundle (don't <script src> it): blob .js is application/octet-stream + nosniff → refused as a script src. fetch().text() → inline <script>.
  • Null the model's rotationQuaternion after load: glTF sets a quaternion that overrides the Euler rotation.z we animate.
  • Harvest classes not on the bundle's B: V.BABYLON exposes only MeshBuilder, StandardMaterial, Vector3, Color3, DynamicTexture; get others from live instances' .constructor. Notably Matrix is absent and Vector3.Project needs one (any DOM label pinned over the 3D scene) — harvest it as const Matrix = scene.getTransformMatrix().constructor, and wrap the projection in try/catch so a bad frame can't kill the render loop.
  • Camera: frameModel() frames the 5000-unit skybox (mm part → sub-pixel). Run it for its light/ ground setup, then set an explicit mm-scale camera (cam.target.copyFromFloats(...), cam.radius ~0.006 static / ~0.026 P&P).
  • Verify render with gl.readPixels across a row after an explicit scene.render() — pup screenshots read blank (no preserveDrawingBuffer). Drive via adom-desktop pup against the container proxy URL ($VSCODE_PROXY_URI; 127.0.0.1 is refused — pup runs on the user's machine).
  • Verify a TIME-BASED viewer by SEEKING, never by waiting. Headless swiftshader renders a heavy scene (a board is ~400k faces) at well under 1 fps, so "wait 10 s then screenshot" samples t ≈ 0 and every animation looks broken when it isn't — an invitation to "fix" working code. Give the viewer a debug handle (window.__dbg with a t setter that also resets caption state, setMode, pause, and a snap() that jumps the eased camera straight to its goal), then drive it: set mode → pause → seek t → wait 2 frames → snap → screenshot. Sample the moments that matter (mid-drop, each chapter, end state) in ONE browser session. On a real GPU (the user's webview) the same page runs at full speed — headless slowness is a harness artifact, not a bug.
  • Match Colby's ground/sky by inspecting his LIVE native scene (the vendored bundle is older): clearColor #2a3340, environmentIntensity 1.0, disable /^Background/ meshes (no skybox), ground = a groundFadeTex radial-fade circle at alpha 0.2. Re-apply after every re-frame AND in the Ground toggle handler (setGroundVisible rebuilds the ground opaque).

Embedding the viewer on the page (§8k) is the other half — README.html + srcdoc for the landing page, an <iframe> hero section for the characterization page.

Bake the datasheet into the 3D model (§4 + §5 checklist): body dims → the composite geometry (height-corrected to the datasheet); land pattern → pads built from the .kicad_mod; optical cone → the FOV overlay at the datasheet 2θ½; glow colour → the dominant wavelength λD; tape & reel → the datasheet packing table; electricals (VF, IV, λD/λP) → tooltip datasheet rows. When all of that is in the model + tooltips, the viewer stands in for the datasheet for everyday reference.

5b-2. Path B sits INSIDE the molecule-pipeline — use it, don't reinvent it

A board/molecule page is not a bespoke artifact this skill invents from scratch: it is the output of adom/molecule-pipeline, the org's router for moving a design from an EDA → the wiki → Hydrogen. This skill composes that pipeline and adds the page craft (viewer choreography, hero, prose, provenance). Anything below that the pipeline already owns, let it own — hand-rolling it is how pages drift out of spec.

KiCad ──kicad-export-molecule──┐
Fusion ─fusion-export-molecule─┼→ molecule-publish → WIKI → adom-molecule-import → HYDROGEN
Altium ─altium-export-molecule─┘   (convert+package+publish)      (fetch + register)

Install: adom-wiki pkg install adom/molecule-pipeline (plus your EDA's export skill).

Which shape am I? Adom machine pins + MP1–MP4 markers → molecule (anchoring + footprint / symbol JSON + all stats gates). Otherwise → board (plain convert, tag board, no anchor gates). A plain PCB that should become a molecule goes through kicad-to-molecule first.

Pipeline invariants that bind this skill (violate one and the page is broken, not just untidy):

  1. The full published set reaches the wiki. Molecule = OCCT GLB + STEP + schematic + board + footprint/symbol JSON. Plain board = GLB + STEP + every schematic sheet + board + project file. Both also carry the render/ assets and component dependencies.
  2. The only GLB is OCCT's (step2glb) — never an EDA-native export (KiCad / Fusion / service-kicad), which bypasses anchoring, gold pins, meter normalisation and footprint emit. ⚠️ This supersedes the service-kicad pcb export glb-zup shortcut: that is fine for a quick local preview, never for the published model.
  3. STEP is exported on the design machine so custom 3D models bake in.
  4. Every stage verifies before the next starts — a failed gate stops the run; never "probably fine".
  5. Import reuses, never regenerates (--no-optimize).
  6. Every emitted GLB declares its up-axis (asset.extras.adomMolecule.upAxis). An unstamped Y-up render/board.glb renders sideways on the wiki and wrong in APM — no single guess fixes both.

Name the GLB exactly <slug>.glb. At publish the wiki auto-converts the STEP into a plain <slug>.glb and points model_3d_path at it — if your anchored GLB has any other name, the plain one shadows it and every import silently gets an unanchored model. (This is what broke the COSMIIC boards.) Verify by sha256: the GLB served at model_3d_path must be byte-identical to your gated conversion output.

Versioning + the render/ archive — adom-project-manager OWNS this

adom-wiki pkg install adom/adom-project-manager   # once per container
adom-project-manager --render-board <slug>        # AFTER pkg publish + repo push — ordering is load-bearing
  • Never hand-write render/<version>/ or edit render/manifest.json. --render-board fully owns the archive and content-dedups; double-writing fights it. (Learned the hard way on adom/feeder-reader, where a hand-built manifest had to be handed back to the tool.)
  • Run it after publish: it stamps the archive with the version it reads from the published package.json. Run it early and it stamps the previous version.
  • It renders + pushes top-level render/* = current version, then, only if content changed, writes render/<version>/… and prepends a versions[] entry to manifest.json (v2: {v:2, current, versions[]}, newest-first). A deps/metadata-only republish adds no entry.
  • Verify: manifest.json parses, current == the version you just published, a content change added a versions[] entry, a metadata-only republish did not.
  • It needs the KiCad sources at the repo TOP LEVEL (the Stage-2 packaging layout). With them under src/ it exits nothing rendered (not a KiCad board?) — not an error you can otherwise read. Move <name>.kicad_pcb / .kicad_sch / .kicad_pro up and re-run.
  • It re-enables the native display sections every run (sheets[], pcb: true), because it renders whatever it finds. On a page that documents itself with the interactive viewers (Path B item 3) that produces a duplicate static PCB image and a redundant Schematics section, so after each run set sheets: [] + pcb: false in the current manifest onlyversions[] stays untouched, so the version archive is preserved and only the display changes.
  • ⚠️ A missing sub-sheet renders as a BLANK sheet, silently. --render-board will happily emit sheet-00N.svg for every sheet the root references; if a referenced .kicad_sch is not in the source set, that SVG is just a frame + title block. It reports success and the manifest lists the sheet by name, so the page ends up publishing an empty page labelled "MCU". Rasterize and LOOK at each rendered sheet before enabling sheets[] — element count is not enough (an empty A4 frame is still ~900 elements). Verified on adom/feeder-reader.
  • The registry tarball is the source of record per versionpkg install <owner>/<slug>@<version> delivers any past version's real sources. render/ is previews only, never source.
  • Re-run on every content update so it never goes stale.

Component dependencies + auto-created part pages (NOT optional)

A board whose dependencies is {} while it has electrical parts is a failed publish — it looks fine but shows up in the Project Manager with no BOM panel. The graph is board → <board-slug>-library → components:

  1. Parse the BOM (CSV beside the sources, else the .kicad_sch); electrical rows only — skip logos, silkscreen art, fiducials, mounting holes, mechanical symbols.
  2. Resolve the real MPN per row. Key components by MPN, never by value. R_4.3K_0402 is a property, not an identity; a row with no MPN and no unambiguous standard part is FLAGGED for a human, never silently dropped.
  3. Create/update <board-slug>-library (type: component, tags:["library"], a library.step so the 3D lint passes, hero + description) whose dependencies are the owner-qualified component refs. The board's dependencies is then exactly { "<owner>/<board-slug>-library": "*" }.

    Why a separate library page at all? It is required by molecule-publish Stage 2.5 (wiki issue #15), not a convention we invented: the board deliberately does not carry the raw parts list, because adom-project-manager (APM) is built around a board → library → components graph and one-hop dependency expansion. Putting 100+ part refs directly on the board flattens that graph and the BOM panel stops working. Reference shapes: barrett-land/usb3-to-ethernet-dataonly-molecule-library, adom/uv-current-monitor-board-library. It also gives the BOM one place to record pending/unresolved rows without polluting the board.

  4. Auto-create any missing component page (type: component, slug/title = normalized MPN) with <MPN>.kicad_sym + <MPN>.kicad_mod + STEP + step2glb GLB + <MPN>.adom-lbr.json + hero + component block. Assets come from the KiCad stdlib via service-kicad (fp fetch, sym fetch, model fetch), else from the board's own embedded footprint/symbol blocks. 3D is hard-required by publish lint — if a part's model exists only in the designer's local library, flag the page as pending with the exact missing file; never fabricate geometry and never drop the part.
  5. Order, one version per cycle: component pages → library → board. A dep must exist when the depender publishes. Merge everything before the board's publish so the cycle produces exactly one registry version. MERGE deps, never overwrite (component, hero, tags, model_3d_path must survive).
  6. Inherit the board's visibility. A private board's library and component pages are published --private to the same org — a public part page hanging off a private board leaks the design.

Completion checklist (a Path B publish is NOT done until all pass):

  • metadata.model_3d_path set; page GLB sha256 == your gated conversion output
  • board deps = its library page; library deps = every electrical BOM part
  • every referenced component page exists, or is explicitly flagged pending with what it needs
  • render/manifest.json present with current == the version just published
  • no PDF anywhere in the repo (Path B item 5)
  • molecule shape only: provenance stamp + footprint/symbol artifacts

5c. Path B build pipeline — board STEP → tagged, viewer-ready GLB

Worked end-to-end on the Feeder Reader (130×50 mm, 4-layer, 143 footprints, 303 solids). Every number below was measured on that build. The runnable step is scripts/bake_board_glb.py (its docstring repeats this section); the viewer that consumes the output is scripts/board-viewer-template.html.

1. STEP → GLB. Two blessed converters, chosen by consumer:

  • step2glb (the Adom skill/CLI; OCCT XCAF on a shared service container). Run step2glb health FIRST — the CLI does nothing locally, and fail fast if the service is down. A 32 MB board is a gzip upload + async job + polling: ~2.5 min wall, so give Bash ≥5 min. Output is 44% smaller (KHR_mesh_quantization, in extensionsRequired) — fine for three.js/Babylon/Colby's viewer, but trimesh/pygltflib misread quantized positions by 32767×. If it must feed a trimesh pipeline, gltf-transform dequantize first. Also ~19k primitives (one per B-rep face region) vs cascadio's ~300 — merge if draw calls matter.
  • cascadio (pip-installable OCCT wheel, runs locally in a venv, no service): plain float32, ~300 primitives — use this for the bake step below (it's trimesh-based). Both produce the SAME scene graph: root = board name, per-component nodes named by refdes, meshes named by footprint/3D-model name, board layers as <Board>_PCB/_copper/_soldermask/ _silkscreen. Units are metres, and Y is negated vs the .kicad_pcb (glb_y = −pcb_y/1000) — get this wrong and the board mirrors silently.

2. Tag solids by REFDES from the node hierarchy — not by position. KiCad's STEP export names each component's assembly node with its refdes; geometry-bearing leaf nodes are OCCT tags (=>[0:1:1:41]), so walk UP the parents to the first name matching a refdes that exists in the .kicad_pcb (297/297 solids resolved on the reference board). Do NOT use nearest-centroid as the primary method — on the same board it provably mistagged 4/297: a big IC's pin-1 dimple solid sits closer to a neighbouring 0402 than to its own footprint origin, and stale geometry (see 3) lands wherever the part was when the STEP was exported. Name the baked meshes section|REFDES|i — that one convention is what makes isolate/dim, per-section cameras, drop-in choreography, and hover tooltips possible downstream.

3. Cross-check positions → STALE-GEOMETRY report. For each refdes, compare its solids' centroid against the footprint position in the .kicad_pcb. A distance beyond ~2 mm means the board was edited AFTER the STEP export (on the reference board D15 had moved 7.3 mm). Keep the refdes tag (it's correct), but flag it in the coverage report and SAY IT ON THE PAGE — the model shows that part where it used to be. The fix is re-exporting the STEP, not the pipeline.

4. Sections come from the SCHEMATIC by default. The .kicad_pcb carries (path "/<sheet-uuid>/<symbol-uuid>") per footprint and the ROOT .kicad_sch declares each sub-sheet's uuid + Sheetname — so sheet membership is derivable from files you already have (the sub-sheet .kicad_sch files are not needed). Use sheet-derived sections as the automatic default; an author-supplied blocks.json (finer groupings, e.g. splitting "MCU sheet" into mcu/usb/comms) overrides it. This is the answer to "how does the author communicate sections": they usually don't have to — the schematic already says.

5. Bake node transforms, recenter, verify. KiCad places every solid purely by node transform, and the Adom viewer bundle does NOT apply node transforms — unbaked, the entire BOM renders collapsed at the origin. Bake world matrices into vertex data, recentre X/Y on the board-outline centre (keep Z), and assert every exported node matrix is identity.

6. De-fight the layers (the "layers clip each other" shimmer). Measured root causes on the reference board, all fixed at bake + one viewer-side line:

  • The exporter emits mask/silk as zero-thickness sheets 10–15 µm off the copper, and components seat 10 µm above the top silk. The bundle's camera sets minZ = radius/1000, making one 24-bit depth step ≈ 10 µm at board-viewing distance — so those gaps are ~1 depth step: guaranteed z-fighting.
  • Bake offsets (defaults in bake_board_glb.py, preserving silk>mask>copper order): top MASK +15 µm, top SILK +15 µm, components +30 µm (skip parts that wrap the board edge, e.g. a USB shell — detected by z-extent below board mid); mirrored negative on the bottom sheets.
  • PCB material must be OPAQUE. Exporters emit the FR-4 solid at alpha 0.98 BLEND; it re-blends over the exactly coplanar buried copper faces every frame — the worst clip of all, and unfixable by offsets (one copper mesh carries both slabs). Force baseColor alpha 1 + alphaMode OPAQUE at bake.
  • Viewer-side: cam.minZ = max(cam.radius/100, 2e-4) per frame (≈1 µm depth steps; near plane still 0.3 mm out at closest zoom). In the template already.

7. Coverage report → the page. Real parts with NO solid (they are simply invisible in the viewer — on the reference board 11 of them, incl. the DIP switch and the crystal) vs legitimately-bodiless footprints (solder pads, mounting holes, jumpers), plus DNP refs and the stale-geometry list. bake_board_glb.py --report emits it; the README build should render this section FROM the report (provenance rule §7B — never hand-type it; a hand-typed copy of this list was off by one within a day).

8. Verify by seeking (§5b) — the board runs at <1 fps in headless swiftshader; screenshot by driving the debug handle, not by waiting.


6. ct/ thumbnail bundle (standard on every COMPONENT page)

Path A only. A board/molecule has no schematic symbol and no footprint, so it gets no ct/ symbol+footprint pair and no "Symbol & Footprint" section — its equivalents are the adom-schematic + adom-2dboard embeds (Path B item 3).

# stage the part's STEP into the chip-fetcher library, then:
adom-chip-thumbnailer once <MPN>                              # 3D icons + outlines + manifest
adom-symbol   render --file <MPN>.kicad_sym --out ct/<MPN>-symbol.svg
adom-footprint render --file <MPN>.kicad_mod --out ct/<MPN>-footprint.svg

Put them in ct/, add a README "Thumbnails" section with a provenance blockquote (who/what produced each artifact), and — for a basic part — the page hero can be ct/<MPN>-3d-iso-icon.png. But a proper Hero Component Studio hero (§3) beats the plain 3D icon for the card.

Boards (Path B) have no schematic symbol — the ct/ equivalent is a pair of board maps: top + bottom SVG renders straight from the .kicad_pcb (outline, copper, silkscreen, vias, pads coloured by functional section). Two shipped scripts, stdlib-only, no KiCad needed:

python3 scripts/kicad_board_geom.py <board.kicad_pcb> geom.json      # parse (cached)
python3 scripts/kicad_board_map.py geom.json F ct/<slug>-top.svg    blocks.json
python3 scripts/kicad_board_map.py geom.json B ct/<slug>-bottom.svg blocks.json

7. Datasheet-accuracy audit — for COMPONENT pages, this is non-negotiable

MANDATE: every component page MUST include its datasheet, stored in the page repo. Push the PDF alongside the page (<MPN>-datasheet.pdf, or the manufacturer's filename) — it is the source of truth the page is verified against and the target every citation links to. No datasheet in the repo → the page is not publishable. (This is an Adom standard: every component page has a datasheet.)

Every number and every curve must be traceable to a specific place in that datasheet, and cited so a reader can verify it. Not "roughly right" — exactly right, with the page / table / figure named next to it. The page stands in for the datasheet, so it must be at least as trustworthy: if someone can't check a value against the datasheet in seconds, the page has failed. This applies to the merged datasheet content, the viewer tooltips (§5a), the Timeline captions, and every rendered chart. If a value isn't in the datasheet, it does not appear — flag the gap, never estimate. (Boards/molecules, Path B, carry no cited datasheet — this section is Path-A only.)

7a. Citation format — cite to a page in the stored datasheet

Every citation names the datasheet revision + the specific page (and table/figure where useful), and links into the stored PDF at that page: <a href="<MPN>-datasheet.pdf#page=3">p.3</a>. The revision matters — page numbers move between datasheet revisions, so the citation is IN-S42AT V1.1 · p.3, never a bare "p.3". This is the same src vocabulary the viewer tooltips already use (Inolux IN-S42AT V1.1 · p.3 electro-optical table), so the whole page cites one way.

Granularity — PER-ROW src (the chosen convention). Every data row carries its own citation: a small muted, clickable src naming the datasheet revision + page (and table/figure), deep-linked into the stored PDF — exactly like a viewer tooltip row's src. This is the most informative and the most verifiable: a reader can check any single value against its exact datasheet location without guessing which section-level citation covers it. Yes it repeats when a table is all one page — that's the point; keep it muted (small, dim text in a trailing src cell/line) so it's present but quiet. Charts cite the figure they were redrawn from (p.5 fig.6) in the chart footer. Render the src as IN-S42AT V1.1 · p.3 linking to <MPN>-datasheet.pdf#page=3. (Section-only / footnote styles were considered and rejected: coarser or a scroll-hunt to verify.)

7b. RULE — datasheet changes ⇒ citations change; show the datasheet's date+revision on the page

The datasheet is versioned. Whenever the datasheet is updated (new revision) OR you change a value, re-run the audit and update the citations — page numbers, table names, and even values shift between revisions, so a stale p.3 silently points at the wrong place. On any page update: confirm the stored PDF is the revision the citations name; if the revision changed, re-verify every cited value and fix the page references. Bump the datasheet revision in every src string when you swap the PDF.

The page MUST show, near the top, which datasheet it's built from — revision AND date. A short line under the title, e.g. "Datasheet: IN-S42AT V1.1 · 25 Apr 2017" (link it to the stored PDF). This lets an engineer see at a glance whether the datasheet is current and go pull a newer one if it's stale. The date + revision usually live in the datasheet's page footer (Inolux: "April 25, 2017" on every page). Both is ideal; date is the minimum.

7c. How to READ a datasheet (methodology — from ds2sf / the ds-extract parser)

Getting the numbers right is the hard part. The distilled method (see ds2sf, the datasheet-parser skill, and process-datasheets):

  1. Find the official PDF. Prefer the manufacturer's own site over aggregators (ti.com, bosch-sensortec.com, st.com, nxp.com, microchip.com, analog.com, inolux-corp.com). WebSearch if you don't have a URL. Store it in the repo (§7 mandate) and note its revision + date (footer).
  2. pdftotext -layout first — as a MAP, not the truth. It gives you a rough index of what's on each page (which page has the electrical table, the curves, the packing figure). Its reading order, tables, and figures are mangled — expected. When text and the rendered image disagree, trust the image.
  3. Render pages and READ THE IMAGE. pdftoppm -png -r 300 <ds.pdf> <out> → Claude vision reads each page accurately. Downscale before Reading — a 300-dpi page is ~2480×3508 px and the Read tool caps ~2000 px/side in multi-image turns; make a ≤1500-px-long-edge mirror to read from, keep the full-res original for figure crops.
  4. Tables: reconstruct with correct alignment; watch min / typ / max columns, units, and footnote markers (a value is often "@ 20 mA" or "note 3"). Cite the row's page.
  5. Figures are where the machine-unreadable data hides. The packing / tape & reel table and the dimension drawings are usually images (pdftotext returns nothing) — rasterize the page and read the figure directly (in-s42at: p.9). Same for any curve — crop the figure and redraw the chart to the actual curve, never to invented points (§7 "charts redrawn").
  6. Cross-check against standards, not just the sheet: EIA-481 / IEC 60286-3 for carrier tape (§7 tape validation), IPC-7351 for land patterns, package-family norms for body dims. A datasheet figure transcribed by eye + a standard agreeing is far stronger than either alone.
  7. Let tools do the deterministic part. ds2sf extract <MPN> reads the datasheet → symbol + footprint + provenance JSON (each pad/pin cites where it came from); ds2sf normalize resolves the package string against the KiCad standard library. The ds-extract service (docling + pdfplumber
    • PyMuPDF, confidence-routed) does bulk table/figure extraction and only escalates ambiguous crops to vision — reuse it rather than reading 14 pages by hand when you can.
  8. Every extracted number keeps its source all the way to the page's src citation (§7a) — the provenance from ds2sf and the page/table you read it from are the citation.
# 1. render the page's claims to plain text
python3 -c "import re,html;t=open('<MPN>.html').read();t=re.sub(r'<(script|style).*?</\\1>','',t,flags=re.S);print(html.unescape(re.sub(r'<[^>]+>',' ',t)))"
# 2. datasheet text + FIGURES (charts/dims are images — render them)
pdftotext -layout DS.pdf -                    # tables (Vf, wavelengths, ratings)
pdftoppm -png -r 300 -f <pg> -l <pg> -x <X> -y <Y> -W <W> -H <H> DS.pdf /tmp/crop   # zoom a figure
#    (ImageMagick is NOT installed — crop with pdftoppm's -x -y -W -H at high DPI, then Read the PNG)
# 3. compare EACH value + EACH chart curve; cite the datasheet page for every finding

Charts must be redrawn to the datasheet, not just re-captioned — an SVG curve path can plot the wrong numbers under a corrected caption. Common LED-page discrepancies found in the wild: land-pattern "center-to-center" that's actually the edge-to-edge gap (c-c = pad_width + gap); brightness-vs-current/temperature curves drawn to invented numbers; FWHM stated when the datasheet's Δλ is blank; footprint pad W/H swapped; 3D model height = generic KiCad body (0.5 mm) vs datasheet (0.45 mm).

Carrier-tape / reel dims (for the interactive viewer's tape): on many datasheets the packing table is an image, so pdftotext can't pull it and the numbers get transcribed from the figure — treat those as not yet validated and say so. Cross-check against EIA-481 / IEC 60286-3 (8 mm tape is standardised: W 8.0, Po 4.0, Do 1.5, E 1.75, F 3.5, T 0.2–0.4; only Ao/Bo/Ko pocket + P1 pitch are part-specific), distributor packaging data (adom-mouser/adom-digikey/ adom-parts-search), OCR (tesseract), or the manufacturer's packing spec.


7B. Provenance for BOARD pages (Path B) — the analogue of §7

A board carries no cited datasheet — but that does NOT mean a board page needs no provenance. It means the provenance comes from somewhere else: the design files. The rule (proven on the feeder-reader build):

  1. Generate every table from the design files — never hand-type them. The BOM (grouped by value+footprint, designators listed), the board outline/stack-up/layer count, the footprint→section membership, the board maps, and the 3D model all derive mechanically from .kicad_pcb / .kicad_sch / the STEP export. A generated page cannot drift from the design; a hand-typed one already has.
  2. Honour DNP. KiCad marks do-not-populate as (attr … dnp) on the footprint (NOT a (dnp yes) node — grep for the attr). DNP parts change the effective BOM and usually encode intent (bus termination fitted only at the ends, etc.); tag them visibly in the BOM and say what they're for. A generated BOM that ignores DNP is wrong.
  3. Label every claim's provenance class, on the page. Three classes:
    • Generated — mechanically derived from the design files (the tables above).
    • Author-asserted — design intent from the board's author: poll rates, baud rates, system topology, scope notes. True because the author says so, not because a file proves it.
    • Illustrative — anything shown to demonstrate behaviour that isn't captured data (example IDs in a scan animation, sample bus traffic). Must be labelled as such wherever it appears. The page gets a dedicated "how this page was built, and what is not verified" section making the split explicit — including that part-level values are as-drawn from the schematic, not verified against each part's datasheet (that's what the parts' own Path A pages are for).
  4. Print the 3D coverage report (see the Path B pipeline, §5c): which real parts have no solid in the STEP export, vs footprints that are legitimately bodiless.

8. Wiki push/pull gotchas — ADD TO THIS when you hit new ones

8a. Owner is not always adom. Basic-parts pages are under aravk/ (e.g. aravk/in-s42atr). A adom/<slug> stats check 404s even though the page exists. Confirm via discover search first.

8b. The 3D viewer loads <slug>.glb BY NAME. Hardcoded as /blob/component/<slug>/<MPN>.glb inside the /viewer/3d/component/<slug> iframe; it ignores page.json component.model_3d_path. To change the 3D model, make the canonical <MPN>.glb itself the new content (keep an identical <MPN>.insertion.glb sibling).

8c. Assets are CDN/browser-cached by URL. After overwriting <MPN>.glb, bust the page HTML with ?v=$(date +%s) in a fresh pup session; confirm the new bytes with a direct curl of the blob URL.

8d. Same-origin iframe control. Page and its /viewer/3d/... iframe are both on wiki.adom.inc, so a parent browser_eval can reach in: document.querySelector("iframe").contentWindow.BABYLON... — use it to pause the animation at a frame for a clean verification screenshot.

8e. page hero takes no --changelog (unlike repo push). Passing it errors.

8f. Derived viewers embed geometry — regenerate them. <MPN>-fpview.html / -symview.html bake in pad/pin geometry; changing the .kicad_mod leaves them stale until regenerated (§9). Same for the ct/ footprint SVG.

8g. http.server cleanup footgun. A pkill -f "http.server <port>" compound line can SIGKILL the shell (exit 144). Start preview servers with `setsid python3 -m http.server --directory

--bind 0.0.0.0 >log 2>&1 ` / `fuser /tcp`), never a broad `pkill -f` (it also matches VS Code Server → kills the container).

8h. Verifying a served asset needs auth — private pages 403 unauthenticated. Send the bearer (~/.config/adom-wiki/token): -H "Authorization: Bearer $(cat ~/.config/adom-wiki/token)". The authoritative "did it land" is adom-wiki repo log (your hash at HEAD) + matching served byte count.

8i. Reaching a container preview from desktop Chrome (pup). pup runs on the user's desktop, so it can't reach the container's 127.0.0.1. Bind the preview server to 0.0.0.0 and open the proxied URL from $VSCODE_PROXY_URI (https://<host>/proxy/<port>/…), not localhost.

8j. adom-wiki repo clone is a plain file checkout, NOT a git repo. No .git; git show HEAD:<file> returns nothing (a snapshot built that way is 0 bytes). Snapshot by copying the file from the fresh clone BEFORE you edit it.

8k. Embedding a live interactive viewer in a page. On-page <canvas>/iframe viewers load assets blob-relative (/blob/component/<slug>/…). To embed a self-contained viewer, push the host HTML + any bundle it needs into the page repo and reference them blob-relative. Sub-points:

  • 8k-i. Blob .js won't run via <script src> (octet-stream + nosniff). Load it via fetch().text() → inline <script> (s.textContent = code; document.head.appendChild(s)).
  • 8k-0. README.md STRIPS raw <iframe>; push a README.html to embed on the page. The wiki renders README.md as sanitized markdown (keeps its native /viewer/3d|symbol|footprint embeds, removes user <iframe>s). A pushed README.html is auto-detected (page.readme_variants_json.bareHtml), wins over README.md, and serves at /readme/<owner>/component/<slug>?variant=public inside a sandboxed iframe (#readme-frame, sandbox="allow-scripts allow-downloads").
  • 8k-0b. The readme-frame RESIZES — you can make it fill the page. It ships min-height:600px with no height set (so it defaults to 600px), but the parent (app.js) listens for a message from the README and grows it: postMessage({ type:"readme-height", height:<px> }, "*")frame.style.minHeight="0"; frame.style.height = clamp(height, 120, 16000). So a README that posts its own document.documentElement.scrollHeight fits exactly (no box-in-box scrollbar), and a viewer sized near the viewport reads as a seamless full-page part of the wiki. Height caveat: the sandbox (no allow-same-origin) can't read the parent's viewport, so proxy it from screen.availHeight minus chrome (~200px). Full-bleed width: break the viewer out of your centered text column. This corrects an earlier note that called the frame "fixed 600px, no auto-resize" — it is not. Verified live-shape against aravk/in-s42atr. Tradeoff still stands: README.md text is native/full-width but can't hold the viewer; README.html puts ALL its content (text + viewer) inside the readme-frame — no mixing native markdown with it.
  • 8k-ii. Wiki blobs render SANDBOXED (no allow-same-origin)localStorage/sessionStorage throw on access → the shared viewer's SceneBuilder aborts (dark, unlit). Shim both storages with no-ops before loading the bundle.
  • 8k-iii. frameModel() mis-frames the embed → "blank." It frames the 5000-unit skybox (camera ~2500 m from a mm part). Set the camera explicitly to mm scale after load; verify with gl.readPixels after a scene.render() with the render loop stopped (no preserveDrawingBuffer).
  • 8k-iv. Embedding the viewer IN the README (README.html + srcdoc). The readme-frame is sandboxed with CSP sandbox allow-scripts allow-downloads; script-src 'unsafe-inline' https:; connect-src 'self' data:; frame-src https:; base-uri 'none'. So: a nested <iframe src="/blob/…"> is blocked (no allow-same-origin) — embed via <iframe srcdoc="…"> with the viewer HTML inlined; relative asset paths 404 (they resolve against /readme/…) — use absolute https://wiki.adom.inc/blob/component/<slug>/<file> ('self' = wiki.adom.inc, blobs serve CORS); srcdoc-escape the HTML .replace('&','&amp;').replace('"','&quot;') (that order). The storage shim still runs inside the srcdoc child. Preview by mirroring the byte-exact CSP locally, loaded via the container proxy; bridge the child's render status out with postMessage (separate opaque origin). Verified live on aravk/in-s42atr.
  • 8k-v. CORS: the srcdoc viewer is an OPAQUE ORIGIN — every fetch it makes is cross-origin. Because the readme-frame sandbox has no allow-same-origin, the srcdoc document has no origin at all, so even a fetch to the very host serving the page is cross-origin and requires Access-Control-Allow-Origin on the response. The real blob host sends it; a plain local preview server (python http.server, npx serve) does NOT — the bundle fetch dies with net::ERR_FAILED and the viewer never boots, which looks exactly like a broken viewer. Any local preview server must send Access-Control-Allow-Origin: * (the shipped scripts/preview_readme/serve.py does).

8k-vi. Publishing an APP/SKILL package: the hero gate (learned publishing this very skill). Component pages take any hero image, but new app/skill pages hard-reject at publish (HERO_STUDIO_REQUIRED) unless BOTH of these hold:

  • The manifest hero field is the image form {"type":"image","path":"docs/hero.png"}. The old billboard descriptor {headline, subhead, screenshot} is what actually trips "the auto-composed billboard hero is retired" — the server reads the SHAPE of the manifest field, so no PNG will ever pass while the manifest still asks for a billboard. (Three publish attempts went down this hole while the error message pointed at the image.)
  • The PNG itself is generated through Hero Studio's pose gate by a human: hero-studio pose <owner>/<slug> --title … --brief … blocks until the human ratifies the checklist and clicks Generate; never POST /generate yourself. The studio stamps provenance into the PNG. Posing aids (state POSTs, injecting a vitrine screenshot as a data URL) are fine; the CLICK must be human. If the studio webview looks frozen, check the tab's proxy mode (adom-cli hydrogen webview set-proxy false --name …) — proxy mode silently blocks page JS.

8k-vii. Interactive embeds on a PRIVATE page — signed caps, and the nested-srcdoc trap. Publisher HTML (readme frame, blob previews, component.viewers slots) is served sandboxed WITHOUT allow-same-origin, so it runs as an opaque origin and its fetches are anonymous — on a private page every asset 404s and the viewer comes up blank. As of adom/wiki#73 the wiki fixes this with short-lived, page-scoped signed caps: while serving a frame to an authenticated reader it mints a token, injects a bootstrap that wraps fetch and rewrites img/source/video/audio srcs to append ?cap=<token> on same-host URLs, and the file routes (/api/pages/.../files/*, /blob/*) accept it as the read grant. Verified on adom/feeder-reader: cap-only fetch = 200, no cap = 404. Two things that will still bite you:

  • A nested sandboxed srcdoc iframe is a SEPARATE document — it gets its own unpatched fetch and no window.__wikiCap, so the bootstrap does not reach a viewer embedded that way. Fix from the parent (which does have the cap): keep the frame's HTML in an inert data-* attribute, substitute the cap into it, then assign srcdoc once.
  • The bootstrap is appended at the END of the served document, so any inline script that reads window.__wikiCap earlier sees undefined (silently → no cap → 404 → "bundle failed to load"). Defer to DOMContentLoaded and poll briefly for the token before substituting. Two ways to sidestep the whole class: the native viewers (trusted, unsandboxed, already authenticated), and self-contained embeds like adom-schematic/adom-2dboard embed, which fetch nothing. Note the readme itself is capped at 4 MB rendered (README_TOO_LARGE), so inlining a multi-MB 3D bundle+model into README.html is not a viable workaround — that is what the cap mechanism is for.

8l. WIKI_STRICT_AUTH rejects a push without a real author (set-author) + a changelog (≥2 words / ≥10 chars, specific).

8m. The hero's cache + which URL the page uses. Served with cache-control: max-age=86400; the page loads /blob/component/<slug>/screenshots/hero.png (not /api/pages/...). After a re-push, verify the blob URL cache-busted; the viewer's browser needs a hard refresh. page.hero_thumbnail is always null — one 16:10 file serves both header and card, so there's nothing else to regenerate.

8n. page hero --image wants a REPO-RELATIVE path, not a local file (a local path 404s "No such file in the repo"). Push the file into the repo first, then set the hero to its repo path. The verb also pins the hero to screenshots/hero.png regardless of the filename you pass, so put the content there.


9. Regenerating derived viewers

When the .kicad_mod changes, regenerate the footprint viewer (Node ESM, gallia):

node -e 'import("/home/adom/gallia/viewer/kicad-footprint-viewer.js").then(async m=>{
  const html=await m.generateFootprintViewer("<dir>/<MPN>.kicad_mod","<MPN>",
    {manufacturer:"<mfr>",partName:"<MPN>",datasheetUrl:"<url>"});
  await (await import("fs/promises")).writeFile("<dir>/<MPN>-fpview.html",html);
})'

Renders via the KiCad service, or a local fallback SVG from the parsed footprint — either way it reflects the corrected pads.


10. Environment / tools

  • cadquery in a venv (system Python is externally-managed): python3 -m venv venv && venv/bin/pip install cadquery pygltflib trimesh numpy.
  • Non-uniform STEP scaling (height fix) via OCP: BRepBuilderAPI_GTransform with gp_GTrsf().SetVectorialPart(gp_Mat(1,0,0, 0,1,0, 0,0,s)).
  • Do NOT step2glb a cadquery-cut STEP — malformed GLB (accessors with no bufferView). Tessellate in cadquery (shape.tessellate(tol, ang)) instead.
  • Do NOT tessellate a raw imported STEP compound before a boolean — segfaults. Measure the taper from the original colour GLB's vertices instead.
  • CLIs used: adom-wiki, hero-component-studio, adom-chip-thumbnailer, adom-symbol, adom-footprint, step2glb, adom-desktop (pup preview), adom-google (share a render).

11. Reference scripts (in scripts/)

file what
publish_wiki_page.py The programmatic publish reference. Stdlib-only wrapper around adom-wiki: auth → locate owner → clone → snapshot → push → verify, plus new-page publish. Start here.
build_composite_led.py Full LED composite builder: emboss (auto-fit taper), densified-NN 3-material colour, red lens + LED_lit glow + KHR_lights_punctual light, insert animation, node Z-scale height correction.
build_composite_cap.py Cap composite builder: plane-split 2-material colour, top+side emboss. Simpler start.
audit_checklist.md The datasheet-accuracy audit checklist + common-discrepancy list.

  • hero-component-studio — the hero-image app (§3). The tool you actually drive to make the card.
  • adom-basic-part-page-upgrade — the full interactive-viewer + tape/reel + hero treatment for basic parts (the deep §5 reference, with assets/viewer-template.html).
  • adom-wiki-cli — the wiki CLI itself (every pillar/verb).
  • adom-wiki-hero-image (adom/adom-wiki-hero-image) — the canonical hero spec (16:10, 1600×1000@2×, midnight theme, one-app-one-screenshot).
  • adom-basic-parts — the basic-parts inventory + per-MPN slugs.
  • molecule-pipeline (adom/molecule-pipeline) — the router this skill's Path B sits inside (§5b-2): EDA export → molecule-publish → wiki → adom-molecule-import. Owns the shape rules, the conversion gates, the dependency/library graph and the pipeline invariants.
  • molecule-publish (adom/molecule-publish) — the convert/package/publish middle: OCCT conversion + stats gates, component dependencies + auto-created part pages, and the publish completion checklist.
  • adom-project-managerowns render/ and the per-version archive via --render-board <slug>; also the consumer of the dependency graph (BOM panel).
  • adom-schematic / adom-2dboard — the interactive schematic and PCB embeds a board page uses instead of hand-rolled board maps or static sheet images (Path B item 3).
  • adom-step2glb — the OCCT conversion CLI/service that produces the one blessed GLB.

Status — what's PROVEN vs still to build (read before trusting a section)

Be honest about maturity so nobody assumes a section is turnkey when it isn't.

Proven end-to-end (built and shipped live on aravk/in-s42atr):

  • The full COMPONENT (Path A) page: merged datasheet content + cited tooltips, the single seamless full-page README, the full-page Timeline viewer embed, static symbol/footprint SVGs, the hero from Hero Component Studio, the readme-frame self-sizing, the whole wiki push/rm flow.

Proven LIVE on a private board page (adom/feeder-reader, adom-org private):

  • The whole Path B page published and verified as an org member: the two-mode interactive viewer (Assembly timeline + Static) rendering inside the private readme frame via the wiki's signed caps (§8k-vii, adom/wiki#73), alongside the native 3D viewer.
  • The cap plumbing for a nested sandboxed srcdoc — the parent-substitution + deferred-token pattern in §8k-vii, which is what makes a custom viewer work on a private page at all.
  • What does NOT work on a private page: inlining a self-contained multi-MB bundle+model into README.html — the readme render cap is 4 MB (README_TOO_LARGE). Measured at 26.8 MB.

Proven as a LOCAL mock (built end-to-end on the Feeder Reader board — 130×50 mm, 4-layer, 143 footprints — viewed in the wiki-faithful preview harness):

  • The Path B pipeline (§5c): STEP→GLB (step2glb service verified; cascadio for trimesh-bound bakes), refdes tagging from the node hierarchy (297/297), stale-geometry detection (caught a real 7.3 mm post-export move), sheet-derived sections, transform baking, layer de-fighting, coverage report. Runnable: scripts/bake_board_glb.py.
  • A board viewer (scripts/board-viewer-template.html): the two-mode spec — Assembly (groupings drop in together while their captions explain them; scripted camera yields to the user for 5 s on input) and Static (free camera, hover tooltips, legend isolate, author-specified toggles like Power) — config-driven via adom-board-viewer-config/0.2 (blocks[] + modes.assembly + modes.static, see §Path B).
  • The preview harness (scripts/preview_readme/ — real sandbox flags + the readme-height handler + CORS).
  • ⚠️ scripts/kicad_board_map.py / kicad_board_geom.py are SUPERSEDED — board maps were this skill's own invention; a board page now embeds adom-schematic + adom-2dboard (Path B item 3). The scripts remain only for the section-colouring data the viewer config uses.

Still to build for Path B:

  • A board-viewer-cli — the config shape exists and is authorable as JSON, but the "never hand-edit, go through a CLI" rule has no CLI yet for board viewer configs.
  • Full molecule-pipeline conformance on a real page — §5b-2 documents the invariants, adom-project-manager --render-board versioning, and the library/component dependency graph from molecule-publish; exercising all of it end-to-end on adom/feeder-reader is in progress.
  • Board visual functions (LEDs blinking, indicators). The LED-lighting method is referenced (Caleb's playbook) but not built into our tooling; the data-HUD path (§Path B) is the proven alternative for non-visual boards.

Underspecified even for Path A (decide + document a convention):

  • Citation FORMAT DECIDED (§7a): cite to the datasheet revision + page, deep-linked into the stored PDF (<MPN>-datasheet.pdf#page=N); per-table by default, per-row where a row's source differs; charts cite their figure. The datasheet is now MANDATORY in the repo (§7), and datasheet changes must update citations (§7b). Still to do: apply this format to the in-s42atr merged content (it currently carries the values but not yet the per-row deep-links).
  • Non-emitter components (resistors, MLCCs, inductors) — the emit/actuate visual path is LED-proven; passives get the viewer + tape/reel + symbol/footprint but no glow/cone. The passive path is less worked.
  • The preview harness (readme-frame + resize handler mirror) is ad-hoc per session, not a shipped script. Worth turning into a reusable preview-readme tool.
  • Tape/reel dimensions: VALIDATED for IN-S42ATR (read from datasheet p.9 figure at 220 dpi + cross-checked against EIA-481 — every value matches). The other 10 LED tapes are still transcribed-not-verified; validate each by rasterizing its datasheet p.9 the same way (pdftoppm -png -r 220 -f 9 -l 9 <ds.pdf> → read). See project_tape-dims-need-validation.
  • The Timeline viewer + viewer-cli config are component-specific RESOLVED for the viewer side: the board viewer template + config shape exist (§5c, §Path B); what remains is the board-viewer-cli (see "Still to build"). The original note for context: a board Timeline (functional-section narrative) needs a different config shape / authoring flow.

Contributing

Hit a new wiki issue, a viewer quirk, or a better hero recipe? Add it to the right section (usually §8 gotchas, §3 hero, or §4 composite), bump the version, and open a PR against adom/adom-hardware-component-publish. Keep findings specific and cited.