Open bug report

Per-slot custom viewer override for component pages (swap native 3D/symbol/footprint viewers in place) + cross-viewer highlight bus

Ray · 4d ago

Per-slot custom viewer override for component pages (+ a cross-viewer highlight bus)

The goal (John, via Ray): the three native viewer slots on a component page — the "3D Model" panel and the "Schematic Symbol" / "PCB Footprint" panels — are the only viewers we want, and they have to do everything (cross-highlight, hover tooltips, layer toggles, 3D lifecycle animation). We do NOT want a second viewer widget in the readme, and we do NOT want adom-lbr's default 3-column app. We want to replace each native viewer, in its existing slot/position, with a custom embeddable viewer (from adom-lbr, chipsmith, or an AI-authored export). Today that's impossible — the viewer routes are hardwired and nothing in page.json or the repo overrides them.

Ask 1 — per-slot override (the unblock)

Let page.json point each slot at a self-contained embeddable HTML in the repo, e.g.:

"component": { "viewers": {
  "model_3d":  "viewers/3d.html",
  "symbol":    "viewers/symbol.html",
  "footprint": "viewers/footprint.html"
}}

If a slot has an override, the platform embeds THAT file in the existing slot iframe (same position, same panel chrome, same "Open full view" affordance) instead of its default renderer. Absent → current default. Same sandbox as the readme (it already got connect-src 'self', frame-src https:, readme-height auto-grow, readme-lightbox), so these exports can lazy-load their own repo CAD (.glb/.kicad_sym/.kicad_mod), toggle layers, animate, etc. This is the readme-sandbox contract you already shipped, just addressable to the viewer slots.

Ask 2 — cross-viewer highlight bus

Cross-highlight across slots (hover a symbol pin -> light the footprint pad AND the 3D lead) needs the sibling iframes to talk. Have the parent relay a tiny message between a page's viewer slots: a slot posts { type: "lbr-hover", pin: "1" } (or null to clear) and the parent rebroadcasts to the other slots on that page. Data-only (a pin id), so no security surface. That single primitive makes the whole symbol<->footprint<->3D cross-highlight work with each viewer authored independently.

Ready content

The viewers are the easy part — I've already built self-contained symbol + footprint viewers (inline SVG from the real .kicad_sym/.kicad_mod: pin names, pin-1, cross-highlight, hover tooltips with datasheet function, Copper/Silk/Fab/Courtyard layer toggles). The moment Ask 1 lands I can drop them into viewers/{symbol,footprint}.html and they light up in the native slots. 3D (with the lifecycle animation + lasered MPN John wants) is the bigger build and wants the same override.

Relates to #48 (cross-highlight) — that becomes trivial once these two primitives exist.

4 Replies

Ray · 4d ago

Important CSP finding while building the custom viewers (relevant to Ask 1's sandbox choice):

2D viewers work under the strict sandbox; 3D does not. I built self-contained symbol + footprint viewers (inline SVG) and they run fine embedded from /blob/.../viewers/*.html — that blob CSP is sandbox allow-scripts; default-src 'none'; script-src 'unsafe-inline' https:; connect-src 'self'. Good enough for SVG + postMessage.

But a real 3D viewer can't run there. I built one (the vendored self-contained Babylon Adom3DViewer, inlined to dodge the .jsapplication/octet-stream+nosniff serving that blocks <script src>). The bundle loads and inits, but our Draco-compressed board GLB (KHR_draco_mesh_compression) never decodes: the blob CSP has no wasm-unsafe-eval and no worker-src, so Babylon can't compile the Draco WASM or spawn its decode worker. Meanwhile the native /viewer/3d/component/<slug> route renders the same GLB because its CSP is script-src 'self' 'unsafe-inline' 'wasm-unsafe-eval' + worker-src 'self' blob:.

Implication for Ask 1: a custom viewer wired into a native slot must be served under viewer-grade CSP (wasm-unsafe-eval + worker-src 'self' blob:), not the strict readme/blob sandbox — otherwise 3D (and any WASM viewer) is dead on arrival. Two concrete requests:

  1. Per-slot custom viewers render under the same CSP the native viewers already use (wasm + workers allowed).
  2. Serve repo .js as application/javascript (right now it's application/octet-stream+nosniff, so a shared viewer bundle can't be a <script src> — I had to inline 13MB).

Net: our symbol/footprint custom viewers are live in the readme interim now; the custom 3D viewer is built and correct but can only run once it's in a viewer-CSP slot (i.e., Ask 1). Happy to hand over the 3D export as the test payload.

Ray · 4d ago

@colby — one focused ask to unblock the custom viewers: of the two requirements above, the viewer-grade CSP for per-slot viewers is the critical one. Our symbol + footprint custom viewers already run in the readme interim, but the 3D viewer is fully built and just needs wasm-unsafe-eval + worker-src 'self' blob: (the same CSP /viewer/3d/component/<slug> already uses) to decode the Draco GLB. If the per-slot override serves custom viewer HTML under that existing viewer CSP, our 3D (and the lifecycle animation + lasered MPN on top of it) lights up immediately. The .js → application/javascript serving is a nice-to-have; the CSP is the blocker. Can this piece be prioritized? Happy to hand you our viewers/3d.html as the test payload.

Colby Knox · 3d ago

Both asks are live on prod, plus the CSP unblock from your last reply.

Ask 1, per-slot override: declare it exactly as you sketched:

"component": { "viewers": {
  "model_3d":  "viewers/3d.html",
  "symbol":    "viewers/symbol.html",
  "footprint": "viewers/footprint.html"
} }

The override is resolved INSIDE the existing /viewer/{3d,symbol,footprint} routes, so it lands in the same slot, same panel chrome, same Open-full-view link, with zero markup changes anywhere -- and an explicit declaration beats the shipped SymView/FpView filename convention. Broken declarations (missing file, traversal, non-html, over 4 MiB) silently fall back to the native renderer: a bad path can never blank a page. Read-gating unchanged.

Your CSP finding, resolved: overrides serve under a VIEWER-GRADE sandbox -- the full readme contract plus 'wasm-unsafe-eval' and worker-src blob: data:. (Slight change from your literal ask of worker-src 'self' blob:: no URL is ever same-origin to an opaque-origin document, so 'self' workers can never load there; blob:/data: is the whole set that CAN work, and it is what Babylon's Draco path actually uses. Your built 3D viewer should run as-is.) connect-src stays 'self': the same anonymity invariant as the readme, your fetches see what an anonymous visitor sees.

Ask 2, the highlight bus: post from any slot:

parent.postMessage({ type: "lbr-hover", pin: "1" }, "*");   // pin: null clears

Every sibling /viewer/ iframe on the page receives a re-serialized { type: "lbr-hover", pin } (pin bounded to 32 chars; the parent never forwards raw payloads; native viewers ignore it). That is the whole primitive: each viewer authored independently, cross-highlight for free.

Verified E2E on staging with a probe component: declared override served in-slot with the wasm/worker grants, then removed cleanly. Full contract documented in docs/readme-sandbox.md (Per-slot custom viewers section), 19-assert suite pinning precedence, CSP, traversal rejection, fallback, and gating.

Wire your three built viewers into stm32f103c8t6 whenever ready -- once the cross-highlight is live on that page, I think this and the cross-highlight half of #48 both close.

Ray · 1h ago

Yes — saw it, thank you (and for the CSP unblock + override + fallback semantics). This is exactly what we need to make adom-lbr the single Library view and kill the duplicated readme widget.

I'm about to wire our export-viewers into the component-page slots (component.viewers → viewers/{3d,symbol,footprint}.html) and validate end-to-end on a live page — 3D under the new slot CSP especially. Give me that pass and I'll close it the moment it's confirmed working on prod. Really appreciate the fast turnaround.

Log in to reply.