Publish a Hardware Component to the Adom Wiki
Public Made by Adomby adom
The one-stop-shop for publishing a hardware component (or molecule/board) to the Adom Wiki. The whole lifecycle: page anatomy, the clone→edit→preview→push loop and its hard rules, making a great hero image (Hero Component Studio — 16:10, overlay-safe, datasheet-backed), the animated 3D composite, the interactive 3D viewer, ct thumbnails, the datasheet-accuracy audit, and every wiki push/pull gotcha we've hit. Now includes the worked Path B (board) pipeline: STEP-to-GLB, refdes tagging, layer de-
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
adomorg, 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
- 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.)
- 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.
- Solder joints — either the default fillets, or (preferably) the placement/shape the user specifies (where and how the solder sits on the pads).
- 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.
- 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
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:
- 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). - 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.
- Static behaviours — should anything light up or move? Which LEDs, what states, what the toggle is called. Explicitly ask; "none" is a fine answer.
- 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
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-groupdoverrides the default dwell. The author's whole input is one small JSON file; the geometry does the rest. Worked example: the feeder-reader mock'sviewer-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_mmis a distance, leave it). Use these as the starting defaults, not the numbers you slow down from:Key Value Why fall_s0.47 the drop reads as a snap, not a float stagger_s0.027 members of one grouping land as a beat dwell_s6.3 enough to read eyebrow + head + rows, not to re-read rest_s4.0 the closing look at the finished board per-group d4 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×.)
Provenance per §7B — every table generated from the design files, DNP honoured, and every claim labelled generated / author-asserted / illustrative.
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 plainrender/sheet-*.svgimages. 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-schematicadom-schematic embed --file <sheet>.kicad_sch --out sch-<n>.htmlPCB layout ( .kicad_pcb)adom/adom-2dboardadom-2dboard embed --file <board>.kicad_pcb --out layout.htmlembedemits 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-diror you silently publish a one-sheet schematic.embed --file root.kicad_schalone reports0 child sheets bundledeven 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.html→3 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. (Onadom/feeder-readerthe 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 needKICAD_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).
page.jsonmapping 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.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>.pdfsilently 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.)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.htmlIS 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>.htmlchar blob is merged into the README and deleted — one canonical page, not two. (On in-s42atr the merge pulled the char content in and removedIN-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)
- 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. - 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 cloneis NOT a git repo — see 8j; copy files by hand.) - 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. - Only push the files you changed (
--files a b c), and only the page you're editing. Never touch another page. - 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).
- Real changelog required (
WIKI_STRICT_AUTHis 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>.htmlcharacterization blob and the<MPN>-symview/-fpview.htmliframe viewers are retired in the gold-standard build — the char content merges into the README and the symbol/footprint become the staticct/SVGs (interactivesrc=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:coverscales 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 clearsoverlayCutoff(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.chipScalesizes 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).keepoutis 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) writespanX/panY, zoom (wheel) writesmargin— 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. (
EffectComposersilently discards the renderer'santialias: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 PARTfor parts,ADOM MOLECULEfor molecules),family,owner,slug. All copy comes from the wikipage.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 throughdisplayEntry(). Symptom: your change to the data does nothing. - The compositor caches
components.jsonat 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.png → repo push --files screenshots/hero.png → page 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.minfrom 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 — noextrasescape hatch (onlyextras.tooltipis 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
BLENDmaterial, 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.jsoncaptions/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 bundle — adom-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-upgrade → assets/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:
- Start from the characterization content (the spec cards / tables / charts / calculator). If a
standalone
<MPN>.htmlexists, its body is the base; then delete<MPN>.html(one canonical page). If not, author the content directly. - Embed the timeline viewer as a full-bleed
srcdocat the top (§5a below). Full-width (break out of the text column), viewport-tall. - Convert every nested
src=iframe — they are sandbox-blocked (§8k): the 3D viewer → thesrcdocviewer; symbol/footprint viewers → inlinect/SVG<img>s; any molecule/board viewer → a link. - Absolute-URL everything the srcdoc fetches — the bundle, the GLB,
viewer-config.json, and any<img>— tohttps://wiki.adom.inc/blob/component/<slug>/…(relative 404s in the readme sandbox). - Fix stale URLs — old pages often reference a dead host (
wiki-ufypy5dpx93o.adom.cloud/static/ libraries/…); rewrite to the current blob host. - Add the self-sizing script (§8k-0b): size the viewer to
screen.availHeight − ~200, thenparent.postMessage({type:"readme-height", height: document.documentElement.scrollHeight}, "*")on load/resize so the readme-frame grows to fit — no box-in-box scrollbar. - Push together:
README.html+viewer-config.json+screenshots/hero.png. Thenrepo rmthe 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.jsonis the editable source of truth for the Timeline captions (the data-point cards +t0/t1timing = 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-cliauthors 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 listtags 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/sessionStoragethrow on access → Colby'sSceneBuilderaborts (no lights/IBL/ shadows/skybox). Shim both with no-ops first. - Fetch-inject the bundle (don't
<script src>it): blob.jsisapplication/octet-stream + nosniff→ refused as a script src.fetch().text()→ inline<script>. - Null the model's
rotationQuaternionafter load: glTF sets a quaternion that overrides the Eulerrotation.zwe animate. - Harvest classes not on the bundle's
B:V.BABYLONexposes onlyMeshBuilder, StandardMaterial, Vector3, Color3, DynamicTexture; get others from live instances'.constructor. NotablyMatrixis absent andVector3.Projectneeds one (any DOM label pinned over the 3D scene) — harvest it asconst 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.readPixelsacross a row after an explicitscene.render()— pup screenshots read blank (nopreserveDrawingBuffer). Drive viaadom-desktoppup against the container proxy URL ($VSCODE_PROXY_URI;127.0.0.1is 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 ≈ 0and every animation looks broken when it isn't — an invitation to "fix" working code. Give the viewer a debug handle (window.__dbgwith atsetter that also resets caption state,setMode,pause, and asnap()that jumps the eased camera straight to its goal), then drive it: set mode → pause → seekt→ 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,environmentIntensity1.0, disable/^Background/meshes (no skybox), ground = agroundFadeTexradial-fade circle at alpha 0.2. Re-apply after every re-frame AND in the Ground toggle handler (setGroundVisiblerebuilds 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):
- 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 componentdependencies. - 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 theservice-kicad pcb export glb-zupshortcut: that is fine for a quick local preview, never for the published model. - STEP is exported on the design machine so custom 3D models bake in.
- Every stage verifies before the next starts — a failed gate stops the run; never "probably fine".
- Import reuses, never regenerates (
--no-optimize). - Every emitted GLB declares its up-axis (
asset.extras.adomMolecule.upAxis). An unstamped Y-uprender/board.glbrenders 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 editrender/manifest.json.--render-boardfully owns the archive and content-dedups; double-writing fights it. (Learned the hard way onadom/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, writesrender/<version>/…and prepends aversions[]entry tomanifest.json(v2:{v:2, current, versions[]}, newest-first). A deps/metadata-only republish adds no entry. - Verify:
manifest.jsonparses,current== the version you just published, a content change added aversions[]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 exitsnothing rendered (not a KiCad board?)— not an error you can otherwise read. Move<name>.kicad_pcb/.kicad_sch/.kicad_proup 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 setsheets: []+pcb: falsein the current manifest only —versions[]stays untouched, so the version archive is preserved and only the display changes. - ⚠️ A missing sub-sheet renders as a BLANK sheet, silently.
--render-boardwill happily emitsheet-00N.svgfor every sheet the root references; if a referenced.kicad_schis 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 enablingsheets[]— element count is not enough (an empty A4 frame is still ~900 elements). Verified onadom/feeder-reader. - The registry tarball is the source of record per version —
pkg 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:
- Parse the BOM (CSV beside the sources, else the
.kicad_sch); electrical rows only — skip logos, silkscreen art, fiducials, mounting holes, mechanical symbols. - Resolve the real MPN per row. Key components by MPN, never by value.
R_4.3K_0402is a property, not an identity; a row with no MPN and no unambiguous standard part is FLAGGED for a human, never silently dropped. - Create/update
<board-slug>-library(type: component,tags:["library"], alibrary.stepso the 3D lint passes, hero + description) whosedependenciesare the owner-qualified component refs. The board'sdependenciesis then exactly{ "<owner>/<board-slug>-library": "*" }.Why a separate library page at all? It is required by
molecule-publishStage 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 aboard → library → componentsgraph 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. - 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 +componentblock. Assets come from the KiCad stdlib viaservice-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. - 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_pathmust survive). - Inherit the board's visibility. A private board's library and component pages are published
--privateto 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_pathset; 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.jsonpresent withcurrent== 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). Runstep2glb healthFIRST — 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, inextensionsRequired) — 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 dequantizefirst. 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 OPAQUEat 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 theadom-schematic+adom-2dboardembeds (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):
- 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).
WebSearchif you don't have a URL. Store it in the repo (§7 mandate) and note its revision + date (footer). pdftotext -layoutfirst — 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.- 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. - 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.
- Figures are where the machine-unreadable data hides. The packing / tape & reel table and the
dimension drawings are usually images (
pdftotextreturns 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"). - 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.
- 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 normalizeresolves the package string against the KiCad standard library. Theds-extractservice (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.
- Every extracted number keeps its source all the way to the page's
srccitation (§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):
- 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. - 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. - 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).
- 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
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
.jswon't run via<script src>(octet-stream + nosniff). Load it viafetch().text()→ inline<script>(s.textContent = code; document.head.appendChild(s)). - 8k-0.
README.mdSTRIPS raw<iframe>; push aREADME.htmlto embed on the page. The wiki rendersREADME.mdas sanitized markdown (keeps its native/viewer/3d|symbol|footprintembeds, removes user<iframe>s). A pushedREADME.htmlis auto-detected (page.readme_variants_json.bareHtml), wins overREADME.md, and serves at/readme/<owner>/component/<slug>?variant=publicinside 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:600pxwith 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 owndocument.documentElement.scrollHeightfits 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 (noallow-same-origin) can't read the parent's viewport, so proxy it fromscreen.availHeightminus 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 againstaravk/in-s42atr. Tradeoff still stands:README.mdtext is native/full-width but can't hold the viewer;README.htmlputs 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/sessionStoragethrow on access → the shared viewer'sSceneBuilderaborts (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 withgl.readPixelsafter ascene.render()with the render loop stopped (nopreserveDrawingBuffer). - 8k-iv. Embedding the viewer IN the README (
README.html+srcdoc). The readme-frame is sandboxed with CSPsandbox 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 (noallow-same-origin) — embed via<iframe srcdoc="…">with the viewer HTML inlined; relative asset paths 404 (they resolve against/readme/…) — use absolutehttps://wiki.adom.inc/blob/component/<slug>/<file>('self'=wiki.adom.inc, blobs serve CORS); srcdoc-escape the HTML.replace('&','&').replace('"','"')(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 withpostMessage(separate opaque origin). Verified live onaravk/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 requiresAccess-Control-Allow-Originon the response. The real blob host sends it; a plain local preview server (pythonhttp.server,npx serve) does NOT — the bundle fetch dies withnet::ERR_FAILEDand the viewer never boots, which looks exactly like a broken viewer. Any local preview server must sendAccess-Control-Allow-Origin: *(the shippedscripts/preview_readme/serve.pydoes).
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
herofield 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; neverPOST /generateyourself. 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
srcdociframe is a SEPARATE document — it gets its own unpatchedfetchand nowindow.__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 inertdata-*attribute, substitute the cap into it, then assignsrcdoconce. - The bootstrap is appended at the END of the served document, so any inline script that reads
window.__wikiCapearlier seesundefined(silently → no cap → 404 → "bundle failed to load"). Defer toDOMContentLoadedand 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 likeadom-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_GTransformwithgp_GTrsf().SetVectorialPart(gp_Mat(1,0,0, 0,1,0, 0,0,s)). - Do NOT
step2glba cadquery-cut STEP — malformed GLB (accessors with nobufferView). 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. |
Related skills
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, withassets/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-manager— ownsrender/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 viaadom-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.pyare SUPERSEDED — board maps were this skill's own invention; a board page now embedsadom-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-boardversioning, and the library/component dependency graph frommolecule-publish; exercising all of it end-to-end onadom/feeder-readeris 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 FORMATDECIDED (§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-readmetool. - 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 +RESOLVED for the viewer side: the board viewer template + config shape exist (§5c, §Path B); what remains is theviewer-cliconfig are component-specificboard-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.
---
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`](https://wiki.adom.inc/adom/adom-schematic) | `adom-schematic embed --file <sheet>.kicad_sch --out sch-<n>.html` |
| PCB layout (`.kicad_pcb`) | [`adom/adom-2dboard`](https://wiki.adom.inc/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.html`
→ `3 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.
```bash
# 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):
```bash
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.
```bash
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):
```bash
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.png` → `repo push --files screenshots/hero.png` → `page 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 bundle** — `adom-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-upgrade` → `assets/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`](https://wiki.adom.inc/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
```bash
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 only** — `versions[]`
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 version** — `pkg 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).
```bash
# 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:
```bash
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.
```bash
# 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 <port> --directory
<dir> --bind 0.0.0.0 >log 2>&1 </dev/null &` and kill by real PID (`lsof -ti:<port>` / `fuser
<port>/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('&','&').replace('"','"')` (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):
```bash
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. |
---
## Related skills
- **`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-manager`** — **owns `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**.