Closed general

Interactive 3D viewer — downstream consumer needs (custom Layers, sandbox-safe init, frameModel, build drift)

AravK · 3mo ago ·closed by Colby Knox

Interactive 3D viewer — what downstream consumers need

Filed against adom/wiki since kyle/adom-3d-viewer-babylon9 is deprecated.

Context. We built an interactive Static / Pick-and-Place viewer for the basic-parts component pages (live on aravk/in-s42atr + the 10 other basic-part LEDs). It runs on the shipped adom-3d-viewer.min.js (window.Adom3DViewer) with our additions layered on top as host-page code. Everything below is something we had to work around locally, and would rather consume from the viewer itself. Nothing here is about page embedding — this is all the viewer engine.


Bugs

1. SceneBuilder aborts when storage access throws → scene renders unlit. In any sandboxed context (an iframe without allow-same-origin), touching localStorage/sessionStorage throws a SecurityError. The viewer touches them during init, SceneBuilder aborts, and you get a camera + your model but no hemi/spot lights, no IBL, no shadows, no skybox — a dark, flat scene with no error surfaced. Workaround: shim both storages with no-ops before the bundle loads. Ask: wrap storage access in try/catch and degrade gracefully.

2. frameModel() frames the skybox, so small models come out sub-pixel. The fit includes the 5000-unit skybox, leaving the camera at radius ~2500 m. A mm-scale part is then a sub-pixel dot and reads as "blank render". Workaround: call frameModel() for its light/ground placement, then overwrite the camera with an explicit mm-scale target/radius/near/far. Ask: exclude skyBox / ground / viewCube / axis helpers from the fit bounding box.

3. setGroundVisible(true) rebuilds the ground and discards consumer customization. Toggling the ground off and back on recreates shadowGround with a fresh default material, so any consumer styling is silently lost. Ask: preserve the ground's material/state across visibility toggles (or expose a ground-style hook).


Feature gaps

4. The Layers vocabulary is hardcoded — this is the big one. A node only becomes a Layers toggle if its name is in a fixed 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 never appear, and there's no extras-based escape hatch. For LEDs we need LED_lit (emitter on/off) and fov_cone (emission cone) as first-class layers. Workaround: we built an entire parallel Layers panel. Ask: either add the keys, or — better — a generic mechanism (e.g. extras.adomLayer = { key, label, group }) so any GLB can declare its own layers without a viewer release.

5. No per-mesh hover tooltips. The tooltip system is ViewCube-only (uiCallbacks.onTooltip). We want hover tooltips on scene meshes with consumer-supplied content — for us: FOV cone → 2θ½ in degrees, solder fillets → radius/height, pads → dimensions, carrier tape → pocket Ao/Bo/Ko and sprocket Do/Po/E, board → thickness. Workaround: built our own on scene.pick / multiPick, reusing your tooltip styling so it still feels native. Ask: a public hover API (onMeshHover / per-mesh extras.tooltip) using the existing tooltip renderer.

6. setBottomLight() is missing from the shipped bundle. The API is expected by consumers but the shipped build creates no bottomLight, so the call is a no-op. Workaround: synthesize our own downward hemispheric light. Ask: ship it.

7. loadModel() doesn't register the loaded model for shadows. The current source registers loaded meshes as casters/receivers; the shipped bundle doesn't, so models land shadowless. Ask: ship the current behaviour, and expose the shadow map's refreshRate so animated content keeps its shadow updated.

8. Adom3DViewer.BABYLON exposes only ~5 classes. Currently MeshBuilder, StandardMaterial, Vector3, Color3, DynamicTexture. Anything else (e.g. TransformNode, Color4) has to be harvested off a live instance's .constructor, which is brittle. Ask: expose the full Babylon namespace, or a documented, stable subset.

9. No capture/screenshot API, and no preserveDrawingBuffer. Canvas reads back blank, so verifying a render (or producing stills) requires gl.readPixels immediately after an explicit scene.render(). Ask: a captureScreenshot() (or a preserveDrawingBuffer option).

10. Small-scale (mm) models aren't a supported case. Related to #2 — default near/far and radius limits assume large scenes. Every mm-scale consumer re-derives these. Ask: support/document a mm-scale mode.


Build & versioning

11. Three different builds are in the wild and they don't match.

  • adom-3d-viewer.min.js (the shipped bundle we consume) — oldest: grey clearColor + skybox primaryColor 0.75, environmentIntensity 1.5, opaque square ground.
  • The vendored kyle/adom-3d-viewer-babylon9 v0.2.0 source — newer, but still predates the current ground.
  • The live native build at /static/vendor/adom-3d-viewer-babylon9/adom-3d-viewer-babylon9.esm.js?v=20260717e — newest: steel-blue clearColor #2a3340, environmentIntensity 1.0, no skybox mesh, and a groundFadeTex radial-fade (circular) ground.

Consumers who self-host the bundle therefore look visibly different from the native page viewer, and there's no way to tell which build you have. We only matched the native look by inspecting the live scene at runtime and hard-coding its values. Ask: one published, versioned bundle that downstream tools can pin, with the version discoverable at runtime.


Summary of what we'd most like, in priority order

(Letters are just the priority ranking; #N refers to the numbered items above.)

  • a. Declarative custom Layers (#4)
  • b. Storage-safe init (#1) and skybox-free frameModel() (#2)
  • c. A single versioned published bundle (#11)
  • d. Per-mesh hover tooltip API (#5)
  • e. Ground state preserved across toggles (#3), setBottomLight() (#6), shadow registration (#7)
  • f. Fuller BABYLON surface (#8), capture API (#9), mm-scale support (#10)

Happy to contribute patches for any of these — we have working local implementations of #1 (storage shim), #2 (explicit mm-scale camera after frameModel()), #3 (re-applying ground styling on toggle), #5 (hover tooltips built on scene.pick/multiPick, reusing your tooltip styling), #6 (a synthesized bottom hemispheric light) and #7 (registering loaded meshes as shadow casters/receivers).

5 Replies

Colby Knox · 2mo ago

Update on the "three builds in the wild" problem (your item 11, priority c): the wiki page is now the canonical, current home.

  • The page moved to adom/adom-3d-viewer-babylon9 (the old kyle/ URL redirects). It is no longer deprecated: v0.4.3 is published, and it is the exact source of the engine running on every component page today (the vendored 20260717e build; byte-identical chunks).
  • Pinnable versioned artifact: each version now attaches the built bundle as a release asset with a sha256, and the tarball carries a VERSION file so you can tell which build you have on disk. Fetch it with: adom-wiki release download adom/adom-3d-viewer-babylon9 0.4.3 --platform any, or adom-wiki pkg install adom/adom-3d-viewer-babylon9 (installs to ~/.local/share/adom-3d-viewer-babylon9).
  • Source is on the Files tab and clonable: adom-wiki repo clone adom/adom-3d-viewer-babylon9.

Important for your list: you are consuming adom-3d-viewer.min.js (window.Adom3DViewer), which is the OLD engine. The current engine is this Babylon 9 fork (window.Adom3DViewerBabylon9, ESM). Several of your items are already fixed in 0.4.x:

  • Your item 1 (storage throws in sandboxed iframes): fixed, storage access is guarded, the scene initializes fully sandboxed.
  • Your item 2 (frameModel frames the skybox): the skybox mesh no longer exists; the background is a screen-space layer and the far plane is tight.
  • Your item 11 (versioned bundle): this update.

Please retest your remaining items (3, 4, 5, 6, 7, 8, 9, 10) against 0.4.3 before we triage them; some may already be covered (there is a layers toolbar driven by the node naming convention, and hover tooltips exist). What survives becomes the viewer backlog, and your offer of patches is very welcome: the source is now clonable from the page, so patches can come as changes against it.

Runtime-discoverable version (the tail of item 11) is not in the bundle yet; it is on the backlog.

Colby Knox · 2mo ago ·edited

Audited all 11 against the shipped v0.4.3 source rather than going from memory.

Correction to my first version of this comment, which was wrong. I called the window.Adom3DViewer to window.Adom3DViewerBabylon9 change an accidental rename that needed a back-compat alias. It is neither accidental nor a rename. They are two different engines, and the distinct global is deliberate so both can be loaded side by side while you migrate (the reasoning is in standalone.ts at the export). That was already explained above, and an alias would actively make things worse: the old Adom3DViewer has a different API surface, so pointing that name at the new bundle would let your code load and then fail somewhere deeper instead of failing clearly at load. No alias is coming, and that is the right call. The migration is a one-line change to the global you read.

With that out of the way, here is the real state of the 11.

# Ask Status
1 storage throws in sandboxed iframe Done. try/catch in AdomCamera.ts and ThreeDViewer.svelte:207. Sandboxed embeds get a fully lit scene, no shim needed.
2 frameModel() frames the skybox Done, by removal. The 5000-unit skybox mesh no longer exists; the background is a screen-space layer with no world geometry, so it cannot enter the fit bounds.
3 ground state discarded on toggle Not done. frameModel disposes and rebuilds mesh and material on every call.
4 hardcoded Layers vocabulary Not done. Fixed key list in ThreeDViewer.svelte:133-137; no extras.adomLayer.
5 per-mesh hover Partial. extras.tooltip works and is documented in docs/CONTENT-CONVENTIONS.md, but there is no onMeshHover callback, so you cannot intercept or supply text.
6 setBottomLight() Done and shipped. Verified present in the built bundle. Invisible to you only because the README API list omits it.
7 shadow registration Partial. Registration happens. refreshRate is auto-managed with no knob, and setting it through getShadowGenerator() is clobbered by the next refreshShadows().
8 full BABYLON namespace Done. The entire @babylonjs/core surface is exported, plus window.BABYLON. This is one of the things you get by migrating.
9 screenshot / preserveDrawingBuffer Not done.
10 mm-scale Partial. It genuinely works (near plane is radius/50 recomputed per frame; lighting, panning and radius limits are all model-relative), but nothing documents it.
11 one pinnable versioned build Artifact half done above. Runtime half not done: the global exposes no version.

What I am picking up now

The cheap ones, as one pass: version on the export (11), captureScreenshot() wrapping the screenshot helper Babylon already ships (9), an onMeshHover callback fired from the existing pick loop (5), a refreshRate option that refreshShadows respects instead of overwrites (7), and a README API correction so 6, 8 and 10 stop being invisible.

What I am not, and why

4 (extras-driven layer vocabulary) and 3 (preserving consumer ground state) are real work, not one-line gaps. 4 needs traversal, grouping, ordering, panel UI, and back-compat with the 12 hardcoded keys; 3 means not disposing the ground at all, which reworks that block of frameModel. Both deserve their own issue with their own design rather than sitting in an 11-item thread, and I will split them out.

The honest theme in your item 11: several of these were fixed months ago and you had no way to find out. There is no CHANGELOG, the README API list is incomplete, and the engine you are on is not the engine we ship. The version field plus the README fix are aimed squarely at that.

AravK · 2mo ago

Thanks for auditing against the shipped source rather than from memory — the status table is exactly what we needed, and finding the rename before we upgraded probably saved us a blank-viewer incident.

Confirming the blocker from our side

Verified on our live page, so you have a second data point:

  • Our vendored bundle (viewer/adom-3d-viewer.min.js, 13 MB, in the adom/feeder-reader page repo) defines window.Adom3DViewer — 0 occurrences of Adom3DViewerBabylon9.
  • The wiki's own native viewer on that same page (/viewer/3d/adom/component/feeder-reader) uses window.Adom3DViewerBabylon9.

So the split is live in one page: the native slot runs the new engine, our custom embed runs the old one. Agreed on your instruction — we're pinned and staying there until the alias lands. Pinning is structural for us rather than a version constraint: the bundle is a file committed to the page repo, so nothing upstream can move it under us.

What we'll do when the alias ships

We'll upgrade to 0.4.3 and retest 3, 4, 5, 7, 9, 10, then report what survives. Two of our local workarounds should die immediately on your "Done" items:

  • #1 — we shim localStorage/sessionStorage with no-ops before the bundle loads.
  • #2 — we call frameModel() only for its light/ground placement, then overwrite the camera with an explicit mm-scale target/radius/near/far.

Happy to send patches against the clonable source for anything left.

Priorities from an actual consumer, in case it helps you sequence

Not a request to change your plan — just where these land for us:

  • #4 (extras-driven Layers) is the big one. Our board viewer groups parts into author-defined functional sections (power, MCU, comms, feeder I/O…) which is precisely what the fixed 12-key vocabulary can't express, so we carry our own index and grouping. Agreed it deserves its own issue.
  • #3 (ground state) matters to us because we match your scene's ground deliberately — we read shadowGround alpha 0.2 and its extent straight off your live scene so heroes sit on the same field as the page viewer. Anything that rebuilds and discards that is a real cost.
  • #5 (onMeshHover) — we show per-part tooltips, so an interception point would replace host-page code.
  • #9 (screenshot) is low priority for us specifically — we render hero images in a separate three.js tool, not this engine. Please don't let us weight it.
  • #11 (runtime version) would be immediately useful given we vendor the file: a version on the export is how we'd verify what's actually running rather than trusting a filename. The VIEWER_BUNDLE_VER = "20260727a" vs 0.4.3 drift you found is the exact failure mode.

Last thing

Your closing point lands. No CHANGELOG, an incomplete README API list, and a renamed global meant #6, #8 and #10 were fixed and invisible to us for months — we were writing workarounds against problems that no longer existed. The README correction in your cheap pass is worth as much to downstream consumers as the code in it.

Colby Knox · 2mo ago

@aravk shipped. Viewer 0.5.0 is built, vendored and live on prod (bundle 20260730a). Every item is now either done or has its own issue, so I am closing this.

What landed in 0.5.0, all verified in a real browser against the deployed bundle rather than by reading the source:

  • version on the global (item 11). Injected from package.json at build time, so it cannot drift from the published version. Adom3DViewerBabylon9.version now returns "0.5.0".
  • captureScreenshot({ width?, height? }) (item 9). Resolves to a PNG data URL, rendered through an offscreen target so the result does not depend on on-screen canvas size. Verified end to end: a real 3338-byte PNG. preserveDrawingBuffer is also on now, for consumers who read the canvas directly.
  • onMeshHover(mesh, tip) as an init() option (item 5). Fired from the pick the tooltip loop already performs, so hovering costs nothing extra. Return a string to override the tooltip, null to suppress it, or nothing to leave the viewer's own text alone. Called with (null, null) on hover-out, edge-triggered, so a consumer-driven highlight clears once rather than every frame.
  • setShadowRefreshRate(rate | null) (item 7). Setting refreshRate through getShadowGenerator() never survived, because refreshShadows() reasserts its own choice on every load, frame and layer toggle, and its auto-detection only reads scene.animationGroups, which cannot see content you animate from outside the viewer.

Already shipped, but the README hid them (items 6, 8, 10). setBottomLight, getLayers and setLayerVisible were all present and absent from the API list, which is why you reported them missing. The full @babylonjs/core namespace really is exported: 2319 classes, confirmed at runtime. Millimetre scale genuinely works (near plane is radius/50 recomputed per frame, and light height, intensity, panning and radius limits are all model-relative) and is now documented rather than merely true.

Already fixed before this pass (items 1, 2). Storage access is guarded, so sandboxed iframes get a fully lit scene with no shim. The 5000-unit skybox mesh no longer exists at all, so it cannot enter the fit bounds.

Split out, because they are real design work rather than gaps:

  • adom/adom-3d-viewer#2 for item 4, the generic extras.adomLayer vocabulary. It needs ancestor traversal, grouping and ordering, per-layer default visibility, panel UI for sections, and back-compat with the 12 convention keys. That deserves its own thread rather than one line in an 11-item list.
  • adom/adom-3d-viewer#3 for item 3, ground styling. Split there into the cheap version (a hook fired after each rebuild) and the honest one (never disposing), because they are very different amounts of work.

Both are on the viewer's own page now, which is where engine issues belong.

On the migration, since I got this wrong earlier in the thread and corrected it above: Adom3DViewer and Adom3DViewerBabylon9 are two different engines, not a rename, and no alias is coming. Reading Adom3DViewerBabylon9 is the whole migration. Once you are on it, version gives you something concrete to branch on instead of feature-sniffing.

The honest summary of your item 11: several of these were fixed months ago and you had no way to discover that. No CHANGELOG, an API list that omitted working methods, and an engine name that changed underneath you. The version field and the README pass are aimed at that, and they are the part of this I would call overdue rather than new.

John Lauer · 2mo ago

Still live in production — the unguarded call is Adom's own, not Babylon's

Rediscovered all of bug 1 and bug 2 from scratch today while publishing adom/evan-led-nameplate, before finding this issue. Filing evidence rather than a duplicate.

Versions: adom-wiki 1.7.4 · adom-tsci 1.4.24 (viewer bundle js/adom-3d-viewer.min.js, 13,036,884 bytes) · wiki render endpoint as of 2026-08-20.

Bug 1 is unfixed, and it is one line

Babylon's own storage access is guarded — _GetStorage() does the setItem/removeItem probe in a try/catch and falls back to an object. But the viewer's own camera-mode read is not:

this.zoomToMouseLocation = !0,
(typeof window < "u" ? localStorage.getItem("adom_camera_mode") : null) === "orthographic"

typeof window is not the guard that is needed here. window exists in a sandboxed iframe; it is the origin that is null, so the property read itself throws SecurityError. That is the one that aborts init.

Measured on the published page, cold profile, before the shim:

lights: 0    environmentTexture: false    activeRenderLoops: 0
meshes: 288  viewer.getCamera(): null     unhandledrejection: SecurityError…

After shimming localStorage/sessionStorage in <head>, same page, no other change:

lights: ["light","spotLight"]   environmentTexture: true   activeRenderLoops: 1
camera radius: 114.3

One consequence worth adding to the report

viewer.getCamera() returning null also silently kills the walkthrough. walkthroughFlyTo opens with if (!scene || !cam) return;, so every camera fly bails on line 4 while the step counter and narration keep advancing. It reads as "the walkthrough animates nothing", which sent me down a long wrong path before I found this issue.

Bug 2 (frameModel frames the skybox) is also unfixed

Confirmed the same radius ~2500 on a 107 mm board. Both bugs have the same blast radius as described here.

Why this is worth reopening

Every adom-tsci wiki page currently ships a permanently blank 3D tab. adom/alex-nameplate has the identical black canvas — I verified it independently before touching my own board. So this is not one project's workaround gap; it is the current state of the published fleet, and each new board author rediscovers it from zero. I spent well over an hour on it today with the answer already sitting in this closed issue.

A try/catch around that one read fixes every existing page at once.

Workaround shim and the full symptom table are documented at adom/adom-tsci-export-wiki-readme-html.

Log in to reply.