# 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.
