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.
3D Viewer: Context for AIs and maintainers
This document explains how the viewer works, especially its Z-up vs Y-up behavior and how it differs from a typical Babylon.js app. Use it when modifying the viewer or when another AI needs to reason about the codebase.
Coordinate system: Z-up vs Y-up (and why it matters)
The viewer can run in two orientations: Z-up (default) or Y-up. That choice affects the camera, lights, ground plane, and how imported models appear. It exists because different tools and formats use different conventions.
How Babylon.js normally works
- Babylon.js is Y-up by default. The default camera
upVectoris(0, 1, 0), the default ground plane lies in the XZ plane (normal = Y), and many examples and docs assume Y is "up." - Many other ecosystems (CAD, robotics, some GLTF exporters) use Z-up: Z is "up," and the "floor" is in the XY plane.
If you load a Z-up GLB into a default Babylon scene without adjusting anything, the model will look like it's lying on its side: what the model considers "up" (Z) is aligned with Babylon's Y axis instead of Z.
What this viewer does when zUp={true} (default)
- Scene:
scene.useRightHandedSystem = trueis set inAdomStatics.init. In this codebase, handedness is tied to thezUpflag. - Camera: In
AdomCamera's constructor,camera.upVector = (0, 0, 1)whenzUpis true, so the orbit camera treats Z as up. Orbit, pan, zoom, and Q/E "up/down" all respect Z as vertical. - Lights: In
SceneBuilder.init, the hemispheric light direction and the SpotLight's initial position are chosen based onzUp. InThreeDViewer.svelte'sframeModel(), the SpotLight is repositioned: whenzUpis true, height is applied along Z and lateral offset in X/Y; when false, height is along Y and offset in X/Z. So "above the model" and "to the side" stay correct for both conventions. - Ground plane (optional): Babylon's
MeshBuilder.CreateGroundcreates a quad in the XZ plane (normal Y). WhenzUpis true, the viewer rotates that ground byMath.PI/2around X so the plane lies in the XY plane (normal Z) and positions it atworldMin.z. WhenzUpis false, the ground stays in the XZ plane and is positioned atworldMin.y. So in both modes the ground sits "under" the content along the chosen up axis. - View cube: The view cube is rendered in a separate mini-scene in
ViewCubeBuilder.ts, and that scene is always Z-up:camera2.upVector = new Vector3(0, 0, 1). The cube's labels (TOP, FRONT, etc.) and the "Home" (top-front-right) view therefore assume Z is up. If the main scene is run withzUp={false}(Y-up), the view cube still shows a Z-up orientation, which can be visually inconsistent with the main scene.
So with zUp={true} (default), the main scene (camera, lights, ground) is consistent with Z as the vertical axis. With zUp={false}, the main scene matches traditional Babylon Y-up; only the view cube remains Z-up.
Source-specific conventions: Fusion (Z-up) vs KiCad (Y-up)
- Autodesk Fusion 360 typically exports GLB/GLTF with Z-up: the design "up" is the Z axis. Using this viewer with
zUp={true}(default) shows the model upright without extra transforms. - KiCad (and many PCB / electronics tools) often use Y-up in their 3D export or internal coordinates. For such models,
zUp={false}may match the intended orientation so the board doesn't appear rotated or "lying down."
If a model appears wrong side up, try flipping the zUp prop and reloading. The viewer does not auto-detect up axis from the file.
Summary table
zUp |
Camera up | Ground plane | Typical use case |
|---|---|---|---|
true |
Z | XY plane, at min Z | Fusion-style, CAD, Z-up GLBs |
false |
Y | XZ plane, at min Y | Babylon default, KiCad, Y-up |
Quirks and differences from a typical Babylon viewer
These are the main ways this component diverges from a vanilla Babylon.js setup.
Single SpotLight for shadows (no DirectionalLight)
The main scene uses a SpotLight as the only shadow-casting light (and for the lit "pool" on the ground). There is no DirectionalLight. The shadow generator is attached to the SpotLight. Light position and direction are updated inframeModel()proportionally to the model size. This matches a reference "Hydrogen" app and keeps the ground from being uniformly lit (the cone naturally darkens the edges).SpotLight position and intensity scale with the model
InThreeDViewer.svelte'sframeModel(), the SpotLight is moved to a height and lateral offset derived from the model's bounding size (e.g.spotHeight = modelSize * 3). Intensity is scaled by the square of that height (inverse-square falloff) so illumination stays consistent across very different model scales. So lighting is model-relative, not fixed in world space.Ground plane is optional and model-relative
WhenshowGroundis true, the ground is created insideframeModel(): its size and position are based on the current content bounds. There is no permanent "floor" at world zero; it's recreated when you frame and is tied to the content's extent and tozUp.View cube is always Z-up
The view cube lives in its own scene (ViewCubeBuilder) with a fixed Z-up camera. The "Home" button and cube corners (e.g. top-front-right) assume Z is up. If the main scene is Y-up, the cube's orientation does not match the main scene's "up."Camera bounds and limits are set in
frameModel()
Radius limits, target bounds, panning sensibility, and near/far clip planes are recomputed when you callframeModel(). They are driven by the current set of loaded roots andaddContentRootroots, so the camera behavior is content-aware rather than fixed.High-precision depth
The engine is created withuseHighPrecisionFloats: trueto reduce z-fighting when zoomed out or with large scenes.No automatic up-axis detection
The viewer does not read the GLB/GLTF or detect whether the asset is Z-up or Y-up. The app (or user) must set thezUpprop to match the source (e.g. Fusion vs KiCad).Bundled environment and loading assets
A default studio.envand loading logo are embedded as base64 so the package works without serving extra static files. TheenvironmentUrlprop can override the environment for PBR reflections.
Where zUp is used in the codebase
AdomStatics.init(canvas, zUp)— Setsscene.useRightHandedSystem = zUpand passeszUpintoAdomCamera.AdomCameraconstructor — Setsthis.upVector = zUp ? new Vector3(0, 0, 1) : new Vector3(0, 1, 0).SceneBuilder.init(engine, { zUp, ... })— Hemispheric light direction and SpotLight initial position/direction usezUp.ThreeDViewer.svelte—frameModel()useszUpfor SpotLight position (which axis is "height" vs "lateral") and for ground plane rotation and position (worldMin.zvsworldMin.y).ViewCubeBuilder— Does not takezUp; the view cube scene is always Z-up.
This should give other AIs enough context to understand how Z-up works and why the viewer is structured this way.
# 3D Viewer: Context for AIs and maintainers
This document explains how the viewer works, especially its **Z-up vs Y-up** behavior and how it differs from a typical Babylon.js app. Use it when modifying the viewer or when another AI needs to reason about the codebase.
---
## Coordinate system: Z-up vs Y-up (and why it matters)
The viewer can run in two orientations: **Z-up** (default) or **Y-up**. That choice affects the camera, lights, ground plane, and how imported models appear. It exists because different tools and formats use different conventions.
### How Babylon.js normally works
- **Babylon.js is Y-up by default.** The default camera `upVector` is `(0, 1, 0)`, the default ground plane lies in the XZ plane (normal = Y), and many examples and docs assume Y is "up."
- Many other ecosystems (CAD, robotics, some GLTF exporters) use **Z-up**: Z is "up," and the "floor" is in the XY plane.
If you load a Z-up GLB into a default Babylon scene without adjusting anything, the model will look like it's lying on its side: what the model considers "up" (Z) is aligned with Babylon's Y axis instead of Z.
### What this viewer does when `zUp={true}` (default)
- **Scene:** `scene.useRightHandedSystem = true` is set in `AdomStatics.init`. In this codebase, handedness is tied to the `zUp` flag.
- **Camera:** In `AdomCamera`'s constructor, `camera.upVector = (0, 0, 1)` when `zUp` is true, so the orbit camera treats Z as up. Orbit, pan, zoom, and Q/E "up/down" all respect Z as vertical.
- **Lights:** In `SceneBuilder.init`, the hemispheric light direction and the SpotLight's initial position are chosen based on `zUp`. In `ThreeDViewer.svelte`'s `frameModel()`, the SpotLight is repositioned: when `zUp` is true, height is applied along Z and lateral offset in X/Y; when false, height is along Y and offset in X/Z. So "above the model" and "to the side" stay correct for both conventions.
- **Ground plane (optional):** Babylon's `MeshBuilder.CreateGround` creates a quad in the **XZ** plane (normal Y). When `zUp` is true, the viewer rotates that ground by `Math.PI/2` around X so the plane lies in the **XY** plane (normal Z) and positions it at `worldMin.z`. When `zUp` is false, the ground stays in the XZ plane and is positioned at `worldMin.y`. So in both modes the ground sits "under" the content along the chosen up axis.
- **View cube:** The view cube is rendered in a **separate** mini-scene in `ViewCubeBuilder.ts`, and that scene is **always Z-up**: `camera2.upVector = new Vector3(0, 0, 1)`. The cube's labels (TOP, FRONT, etc.) and the "Home" (top-front-right) view therefore assume Z is up. If the main scene is run with `zUp={false}` (Y-up), the view cube still shows a Z-up orientation, which can be visually inconsistent with the main scene.
So with **`zUp={true}`** (default), the main scene (camera, lights, ground) is consistent with Z as the vertical axis. With **`zUp={false}`**, the main scene matches traditional Babylon Y-up; only the view cube remains Z-up.
### Source-specific conventions: Fusion (Z-up) vs KiCad (Y-up)
- **Autodesk Fusion 360** typically exports GLB/GLTF with **Z-up**: the design "up" is the Z axis. Using this viewer with **`zUp={true}`** (default) shows the model upright without extra transforms.
- **KiCad** (and many PCB / electronics tools) often use **Y-up** in their 3D export or internal coordinates. For such models, **`zUp={false}`** may match the intended orientation so the board doesn't appear rotated or "lying down."
If a model appears wrong side up, try flipping the `zUp` prop and reloading. The viewer does **not** auto-detect up axis from the file.
### Summary table
| `zUp` | Camera up | Ground plane | Typical use case |
|---------|-----------|--------------------|--------------------------------|
| `true` | Z | XY plane, at min Z | Fusion-style, CAD, Z-up GLBs |
| `false` | Y | XZ plane, at min Y | Babylon default, KiCad, Y-up |
---
## Quirks and differences from a typical Babylon viewer
These are the main ways this component diverges from a vanilla Babylon.js setup.
1. **Single SpotLight for shadows (no DirectionalLight)**
The main scene uses a **SpotLight** as the only shadow-casting light (and for the lit "pool" on the ground). There is no DirectionalLight. The shadow generator is attached to the SpotLight. Light position and direction are updated in `frameModel()` proportionally to the model size. This matches a reference "Hydrogen" app and keeps the ground from being uniformly lit (the cone naturally darkens the edges).
2. **SpotLight position and intensity scale with the model**
In `ThreeDViewer.svelte`'s `frameModel()`, the SpotLight is moved to a height and lateral offset derived from the model's bounding size (e.g. `spotHeight = modelSize * 3`). Intensity is scaled by the square of that height (inverse-square falloff) so illumination stays consistent across very different model scales. So lighting is **model-relative**, not fixed in world space.
3. **Ground plane is optional and model-relative**
When `showGround` is true, the ground is created **inside** `frameModel()`: its size and position are based on the current content bounds. There is no permanent "floor" at world zero; it's recreated when you frame and is tied to the content's extent and to `zUp`.
4. **View cube is always Z-up**
The view cube lives in its own scene (`ViewCubeBuilder`) with a fixed Z-up camera. The "Home" button and cube corners (e.g. top-front-right) assume Z is up. If the main scene is Y-up, the cube's orientation does not match the main scene's "up."
5. **Camera bounds and limits are set in `frameModel()`**
Radius limits, target bounds, panning sensibility, and near/far clip planes are recomputed when you call `frameModel()`. They are driven by the current set of loaded roots and `addContentRoot` roots, so the camera behavior is content-aware rather than fixed.
6. **High-precision depth**
The engine is created with `useHighPrecisionFloats: true` to reduce z-fighting when zoomed out or with large scenes.
7. **No automatic up-axis detection**
The viewer does not read the GLB/GLTF or detect whether the asset is Z-up or Y-up. The app (or user) must set the `zUp` prop to match the source (e.g. Fusion vs KiCad).
8. **Bundled environment and loading assets**
A default studio `.env` and loading logo are embedded as base64 so the package works without serving extra static files. The `environmentUrl` prop can override the environment for PBR reflections.
---
## Where `zUp` is used in the codebase
- **`AdomStatics.init(canvas, zUp)`** — Sets `scene.useRightHandedSystem = zUp` and passes `zUp` into `AdomCamera`.
- **`AdomCamera` constructor** — Sets `this.upVector = zUp ? new Vector3(0, 0, 1) : new Vector3(0, 1, 0)`.
- **`SceneBuilder.init(engine, { zUp, ... })`** — Hemispheric light direction and SpotLight initial position/direction use `zUp`.
- **`ThreeDViewer.svelte`** — `frameModel()` uses `zUp` for SpotLight position (which axis is "height" vs "lateral") and for ground plane rotation and position (`worldMin.z` vs `worldMin.y`).
- **`ViewCubeBuilder`** — Does **not** take `zUp`; the view cube scene is always Z-up.
This should give other AIs enough context to understand how Z-up works and why the viewer is structured this way.