Adom 3D Viewer
Public Made by Adomby adom
The Babylon 9.5 engine behind every component page's 3D tab on wiki.adom.inc. Versioned ESM bundle with GLB loading, view cube, layers toolbar, ground shadows, and Z-up CAD framing.
Install?
The Babylon 9.5 engine behind every component page's 3D tab on wiki.adom.inc. Versioned ESM bundle with GLB loading, view cube, layers toolbar, ground shadows, and Z-up CAD framing.
adom-wiki pkg install adom/adom-3d-viewer
Latest: v0.9.1, published
Contents
README
markdownAdom 3D Viewer
The Babylon 9.5 viewer engine behind every component page's 3D tab on
wiki.adom.inc. A fork of adom-inc/3d-viewer (Babylon 7.54) upgraded to
Babylon 9.5 with the redesigned Inspector v9, built as a versioned ESM bundle
that downstream tools vendor (the wiki's native component viewer, chiplinter,
and the next wave of Adom 3D apps).
This page is the canonical home: the source lives on the Files tab
(adom-wiki repo clone adom/adom-3d-viewer), and each version's
built bundle is attached as a release asset you can pin.
What is in 0.4.x
- Layers toolbar: slide-out per-layer visibility panel driven by the node naming convention (fr4_board, pad_top, silk, barrel, pin1_marker, ...).
- Z-fighting fixes for PCB models: 10x depth precision, polygon-offset tie-breaking for nested shells, near plane at radius/50, XY-area container ordering.
- Skybox mesh replaced by a screen-space background layer (tight far plane, faded ground rim): frameModel no longer fits a 5000-unit skybox.
- Sandbox storage guard: localStorage/sessionStorage access is wrapped, so the viewer initializes fully (lights, IBL, shadows) inside sandboxed iframes.
- Transparent radial-fade ground, hover tooltips, orbit-center UX, world-origin and mesh-local axis helpers, laser-etch text overlays.
Get the bundle (pin a version)
Every release attaches the built bundle as a tarball asset:
adom-wiki release download adom/adom-3d-viewer <version> adom-3d-viewer-babylon9-<version>.tar.gz
tar -xzf adom-3d-viewer-babylon9-<version>.tar.gz -C ./vendor/3d-viewer/
The extracted VERSION file records exactly which build you have. The bundle
code-splits into ~60 chunks; serve them all from the same directory as the
entry (adom-3d-viewer-babylon9.esm.js) and browsers fetch chunks on demand.
Embed it
<!-- Browser shim for Node-style globals that @babylonjs/inspector's React
(Fluent UI) deps assume. Add BEFORE loading the bundle. -->
<script>
window.process = window.process || { env: { NODE_ENV: "production" } };
window.global = window.global || window;
</script>
<script type="module">
await import("./vendor/3d-viewer/adom-3d-viewer-babylon9.esm.js");
const v = window.Adom3DViewerBabylon9;
const viewer = v.init(host, { zUp: true, showViewCube: true });
await viewer.loadModel("chip.glb");
viewer.frameModel();
</script>
Why ESM (not IIFE)
Babylon 9's @babylonjs/inspector has circular imports that resolve to TDZ
when collapsed into a single IIFE scope, regardless of minifier. Vite's ESM
output preserves module boundaries so the inspector initializes cleanly. The
global is window.Adom3DViewerBabylon9 (distinct from the legacy
window.Adom3DViewer), so both bundles can coexist during a migration.
API
window.Adom3DViewerBabylon9 = {
init(host, options), // mounts the viewer, returns ViewerInstance
rotateYUpToZUp(scene), // kicad-cli GLBs (Y-up) -> adom viewer (Z-up)
addLaserEtch(scene, opts), // bake a laser-etched chip-marking text overlay
version: "0.5.0", // this build, injected from package.json
BABYLON: <full @babylonjs/core namespace>, // re-exported for consumers
};
ViewerInstance exposes:
loadModel(url)/loadModelBytes(bytes, { extension? })/clearScene()/frameModel(fillFraction?)whenReady()/getReadyReport(): the ready report (see "Sandboxed frames" below): resolves once the first scene is up and reflects the current state on every later call; never rejectsgetScene()/getEngine()/getCamera()/getShadowGenerator()addContentRoot(node, opts?)/removeContentRoot(node)toggleDebugLayer()/showDebugLayer()/hideDebugLayer()setGroundVisible(visible)/goHome()setBottomLight(on)/getBottomLightState(): underside fill light, default offsetProjectionMode(ortho)/getProjectionMode()toggleAxes("world" | "mesh-local", force?)/getAxesState()getLayers()/setLayerVisible(name, visible): layers, either declared by the content viaextras.adomLayeror matched against the built-in board vocabulary (docs/CONTENT-CONVENTIONS.md). Each entry carriesgroupfor sectioning anddeclaredto say which source it came from. Empty for GLBs with fewer than two layerscaptureScreenshot({ width?, height? }): resolves to a PNG data URL. Renders through an offscreen target, so the result does not depend on the on-screen canvas sizesetShadowRefreshRate(rate | null): pin the shadow map refresh rate;nullrestores automatic behavior. Needed for content animated from outside the viewer, whichscene.animationGroupscannot seedestroy()
init() options: zUp, modelUrl, showViewCube, showGround,
environmentUrl, environmentBytes, environment ("studio" | "none"),
initialViewMode, onReady(report), onMeshHover(mesh, tip), and
onGroundCreated(ground). The hover
hook is called on the pick the viewer already performs: return a string to
override the tooltip, null to suppress it, or nothing to leave the viewer's
own text alone. It is called with (null, null) when the pointer leaves
content, so a consumer-driven highlight can clear itself.
onGroundCreated is called with the ground mesh after every rebuild. The
ground's geometry belongs to the viewer, because it is sized from content bounds
and rebuilt whenever the model is reframed; its appearance does not, so this is
where a consumer applies its own ground material. Setting the material once from
outside will not survive the next reframe.
Sandboxed frames (wiki widgets and readmes)
The wiki's widget and readme frames are opaque origins under a strict CSP
(connect-src 'self' data:, img-src data: https:, no blob:), and every
localStorage access there throws. Since 0.7.0 the viewer is built for that
(adom/adom-3d-viewer#5):
Orbit
The camera orbits freely through both poles (0.7.1). Dragging through a flat
top or bottom view keeps rotating in the same direction; there is no stop
near the underside and no beta limit. Presets (front, back, the view cube)
still land upright. npm run test:orbit drives real pointer drags across both
poles in the Y-up and Z-up frames.
The ? button: controls card and the Shift+Alt tip
Every viewer draws a small ? in its bottom-right corner (0.7.3). The card it
opens leads with the move that makes large boards and scaffolds navigable:
Shift+Alt+click a point on the model to set the center of rotation (a glowing
dot marks it; orbit and zoom then work around that spot). The rest of the
controls follow: drag to orbit, right-drag or Ctrl+drag to pan, scroll to zoom
toward the cursor, the cube faces with Home and Fit, hold Space to spin. The
? key and Escape toggle the card. The first time a model is ready in a
browser, a tip bubble says the Shift+Alt line once and is then remembered
(ephemeral storage in sandboxed frames). init(host, { showHelp: false })
hides all of it for hosts that draw their own help (0.7.4: a host container that is already positioned, such as position: absolute; inset: 0, keeps its positioning; 0.7.3 forced it to relative, and the canvas collapsed to 150 px after the host hid and re-showed the viewer); viewer.showHelp() and
viewer.hideHelp() drive the card. npm run test:help covers it.
View cube: click on release, drag to orbit
A view-cube face navigates on mouse RELEASE (0.7.2, adom/adom-3d-viewer#4),
not on press: pressing on the cube and dragging orbits the model exactly like
dragging on the model, the way Fusion's cube behaves. A release within 4 px
of the press, still over the same face, is the click that animates the camera
to that face. npm run test:viewcube checks all three cases (drag on the cube
matches a drag on the model, a clean click snaps on release, a wander does
not snap).
- The studio environment map loads from memory (Babylon's buffer option), and
images (environment mip levels, GLB-embedded textures) decode as
ImageBitmaps, so no
blob:URL is ever fetched.environmentBytesinjects your own.envbytes the same way;environment: "none"skips the map. - Preferences fall back to document-local memory when storage is denied; the
report says
storage: "ephemeral". loadModelBytes(bytes)takes a model the host already fetched or verified, so the frame's CSP never sees a model URL.- The loading overlay always comes down, model failure included, and
whenReady()resolves with a report:
const viewer = Adom3DViewerBabylon9.init(host, { modelUrl: "/adom/part/render/part.glb" });
const report = await viewer.whenReady();
// { ok, engine: { webglVersion, contextLost }, environment: { source, ready, error },
// camera: { ready, mode }, storage: "persistent" | "ephemeral",
// loadingOverlay: "hidden" | "visible", model: { requested, loaded, meshes, vertices, error },
// cspViolations: [{ directive, blocked }], hints: [{ code, level, message, action }], version }
ok is true only when the scene rendered, the overlay is gone, the
environment is ready (or deliberately absent), no CSP violation was recorded
and any requested model loaded with geometry. Otherwise the hints name the fix:
CSP_BLOCKED, MODEL_LOAD_FAILED, MODEL_EMPTY, ENVIRONMENT_FAILED,
ENVIRONMENT_PENDING, STORAGE_EPHEMERAL (info), LOADING_OVERLAY_STUCK,
WEBGL_CONTEXT_LOST.
One thing the frame's host must do: an opaque origin fetches module scripts in
CORS mode, so the bundle must be served with Access-Control-Allow-Origin
(the wiki's /static/vendor/ route does).
npm run test:sandbox proves all of it in headless Chrome: an https server
carrying the wiki's exact widget CSP, an sandbox="allow-scripts" iframe, a
ready report that says ok, painted chip pixels, then three destroy/recreate
rounds alternating URL and bytes transport. Reads puppeteer-core and a
Chrome from ~/.cache/puppeteer (or PUPPETEER_EXECUTABLE_PATH).
Scale
Millimeter-scale models are supported. The near plane is radius/50
recomputed per frame, and light height, intensity, panning speed and radius
limits are all derived from model size, so a 5 mm part and a 500 mm assembly
both frame and light correctly without a unit setting.
The full @babylonjs/core namespace is exposed at
Adom3DViewerBabylon9.BABYLON so consumers do not need their own import side.
chiplinter uses V.BABYLON.MeshBuilder.CreateDisc, V.BABYLON.PBRMaterial,
V.BABYLON.Tools.CreateScreenshotUsingRenderTarget, and friends directly.
Build from source
adom-wiki repo clone adom/adom-3d-viewer
cd adom-3d-viewer
npm install
npx vite build --config vite.standalone.config.ts # -> dist-standalone/
npm run build (plain vite build) produces the npm-consumable dist/
library instead; dist-standalone/ is the self-contained browser bundle the
wiki and release assets ship.
Consumers
- wiki.adom.inc component pages: git-wiki vendors
dist-standalone/intostatic/vendor/adom-3d-viewer-babylon9and pins it viaVIEWER_BUNDLE_VER. adom-chiplinter: vendors the release tarball intovendor/3d-viewer/.- Downstream viewer work is tracked on adom/wiki issues (#48, #53, #60).
Development
Source of record is this page's repo. GitHub adom-inc/adom-3d-viewer-babylon9
is the working mirror where CI and day-to-day commits happen; each released
version is ported here (source push + package publish + bundle release asset).