# 3D Viewer

A standalone Svelte component that wraps Babylon.js with shared lighting, gray skybox, and arc-rotate camera controls (orbit, pan, zoom, WASD). No floor—just the canvas and skybox. Supports Z-up or Y-up and loading GLB/GLTF models by URL or via API.

## How `dist` works

When you run `npm run build`, Vite compiles the library from `src/` into the **`dist/`** folder (bundled JavaScript and type definitions). The package is set up so that when another app installs this repo and does:

```js
import { ThreeDViewer } from '3d-viewer';
```

Node resolves that to **`dist/index.js`** (see `main`, `module`, and `exports` in `package.json`). So **the built `dist/` is what other projects actually use**—not the raw `src/` files.

You must run `npm run build` in this repo before:

- Using `npm link` or `npm install /path/to/3d-viewer` in another app, or  
- Publishing to a registry (otherwise consumers get no or stale build).

If `dist/` is missing or outdated, the other app’s import will fail or use old code.

This package includes a **`prepare`** script that runs **`npm run build`** after `npm install`. So when someone installs from GitHub (e.g. `npm install github:adom-inc/3d-viewer`), npm will build the library automatically and `dist/` will be present—no need to commit `dist/` or use a Vite alias in the consuming app.

## Installation

**From GitHub** (recommended; `prepare` builds automatically):

```bash
npm install github:adom-inc/3d-viewer
```

**From a local path** (e.g. while developing the viewer):

```bash
cd /path/to/3d-viewer
npm install
npm run build
# then in your app:
npm install /path/to/3d-viewer
```

Or, after publishing, use the package name from your registry.

**Peer dependencies** (your app must have these):

- `svelte` ^4.0.0 or ^5.0.0
- `@babylonjs/core` ^7.0.0
- `@babylonjs/loaders` ^7.0.0

If your app doesn’t already depend on Babylon, install them:

```bash
npm install @babylonjs/core @babylonjs/loaders
```

## Basic usage

Give the viewer a fixed height (it fills 100% width and height of its parent):

```svelte
<script>
  import { ThreeDViewer } from '3d-viewer';
</script>

<div class="viewer-wrapper">
  <ThreeDViewer />
</div>

<style>
  .viewer-wrapper {
    width: 100%;
    height: 400px; /* or 100vh, etc. */
  }
</style>
```

## Props

| Prop             | Type              | Default | Description |
|------------------|-------------------|---------|-------------|
| `zUp`            | `boolean`         | `true`  | Use Z-axis as up (true) or Y-axis (false, Babylon default). |
| `modelUrl`       | `string \| undefined` | `undefined` | Optional URL of a GLB/GLTF to load when the viewer is ready. |
| `showViewCube`   | `boolean`         | `true`  | Show or hide the view cube in the corner. |
| `showGround`     | `boolean`         | `false` | Show a ground plane beneath the model for receiving shadows. Use `setGroundVisible(true/false)` at runtime to toggle. |
| `environmentUrl` | `string \| undefined` | `undefined` | URL of a custom `.env` file for PBR reflections. A neutral studio environment is bundled by default — no setup needed. |
| `initialViewMode` | `'perspective' \| 'orthographic'` | `'perspective'` | Initial camera projection. Use `setProjectionMode()` or `getProjectionMode()` at runtime to change or read. |

Example with props:

```svelte
<ThreeDViewer
  zUp={true}
  modelUrl="/models/part.glb"
  showViewCube={true}
  showGround={true}
/>
```

## Loading and clearing models

Use `bind:this` to get the component instance and call `loadModel(url)` or `clearScene()`:

```svelte
<script>
  import { ThreeDViewer } from '3d-viewer';

  let viewer;
  let modelUrl = '';

  async function loadFromUrl() {
    if (viewer && modelUrl) {
      await viewer.loadModel(modelUrl);
    }
  }

  function clear() {
    if (viewer) {
      viewer.clearScene();
    }
  }
</script>

<div class="viewer-wrapper">
  <ThreeDViewer bind:this={viewer} />
</div>

<div class="controls">
  <input type="text" bind:value={modelUrl} placeholder="GLB/GLTF URL" />
  <button on:click={loadFromUrl}>Load model</button>
  <button on:click={clear}>Clear scene</button>
</div>

<style>
  .viewer-wrapper {
    width: 100%;
    height: 400px;
  }
  .controls {
    display: flex;
    gap: 0.5rem;
    padding: 0.5rem;
  }
</style>
```

- **`loadModel(url: string): Promise<void>`** — Loads a GLB/GLTF from `url`. Adds meshes to the scene and shadow casters, then frames the camera. Can be called multiple times; the frame will encompass all loaded models.
- **`clearScene(): void`** — Removes all models loaded via `loadModel` (and any from the initial `modelUrl` prop). Does not remove the skybox, lighting, or content you added with `addContentRoot`.
- **`frameModel(fillFraction?: number): void`** — Re-frames the camera to fit all content: loaded models plus any roots registered with `addContentRoot`. `fillFraction` defaults to `1` (fill viewport); use a larger value to zoom out.
- **`getScene(): Scene | null`** — Returns the Babylon scene. Use it to create or modify native objects (spheres, boxes, materials, etc.).
- **`getEngine(): Engine | null`** — Returns the Babylon engine.
- **`getCamera(): Camera | null`** — Returns the active camera.
- **`getShadowGenerator(): ShadowGenerator | null`** — Returns the shadow generator. Call `addShadowCaster(mesh)` for custom meshes that should cast shadows (or use `addContentRoot` with default options).
- **`addContentRoot(node: TransformNode, options?: { castShadows?: boolean; receiveShadows?: boolean }): void`** — Registers a root (e.g. a parent of spheres) so it is included in `frameModel()`. By default its meshes cast and receive shadows. Does not dispose on `clearScene()`.
- **`removeContentRoot(node: TransformNode): void`** — Unregisters a content root from framing (does not dispose the node).
- **`setGroundVisible(visible: boolean): void`** — Show or hide the ground plane at runtime. Updates `showGround` and re-runs framing if there is content.
- **`goHome(): void`** — Jump the camera to the home view (front–top–right, like the view cube corner) and re-frame the model. No animation.
- **`setProjectionMode(ortho: boolean): void`** — Set camera to orthographic (`true`) or perspective (`false`).
- **`getProjectionMode(): 'perspective' \| 'orthographic'`** — Return the current camera projection.
- **`showDebugLayer(): Promise<void>`** — Opens the Babylon.js debug layer (inspector). Lazy-loads the inspector on first use.
- **`hideDebugLayer(): void`** — Closes the debug layer.
- **`toggleDebugLayer(): Promise<void>`** — Toggles the debug layer open or closed.

Example: add a button to toggle the inspector:

```svelte
<script>
  import { ThreeDViewer } from '3d-viewer';
  let viewer;
</script>

<div class="viewer-wrapper">
  <ThreeDViewer bind:this={viewer} />
</div>
<button on:click={() => viewer?.toggleDebugLayer()}>Toggle debug inspector</button>

<style>
  .viewer-wrapper { width: 100%; height: 400px; }
</style>
```

## Creating native Babylon objects (e.g. planets)

You can use the viewer without loading any GLB: get the scene, create meshes with Babylon’s `MeshBuilder`, then register a root and frame so the camera fits your content.

Your app already has `@babylonjs/core` as a peer dependency, so you can use `MeshBuilder`, `StandardMaterial`, `Vector3`, etc.

```svelte
<script lang="ts">
  import { ThreeDViewer } from '3d-viewer';
  import { MeshBuilder } from '@babylonjs/core/Meshes/meshBuilder';
  import { StandardMaterial } from '@babylonjs/core/Materials/standardMaterial';
  import { TransformNode } from '@babylonjs/core/Meshes/transformNode';
  import { Vector3 } from '@babylonjs/core/Maths/math.vector';
  import { Color3 } from '@babylonjs/core/Maths/math.color';

  let viewer;
  let planetsRoot: TransformNode | null = null;

  function addPlanets() {
    const scene = viewer?.getScene();
    if (!scene) return;

    if (planetsRoot) {
      viewer.removeContentRoot(planetsRoot);
      planetsRoot.dispose();
      planetsRoot = null;
    }

    const root = new TransformNode('PlanetsRoot', scene);
    planetsRoot = root;

    const sun = MeshBuilder.CreateSphere('sun', { diameter: 2 }, scene);
    sun.parent = root;
    const sunMat = new StandardMaterial('sunMat', scene);
    sunMat.emissiveColor = new Color3(1, 0.9, 0.5);
    sun.material = sunMat;

    const earth = MeshBuilder.CreateSphere('earth', { diameter: 0.6 }, scene);
    earth.parent = root;
    earth.position = new Vector3(3, 0, 0);
    const earthMat = new StandardMaterial('earthMat', scene);
    earthMat.diffuseColor = new Color3(0.2, 0.4, 0.8);
    earth.material = earthMat;

    viewer.addContentRoot(root);  // include in framing + shadows
    viewer.frameModel();
  }
</script>

<div class="viewer-wrapper">
  <ThreeDViewer bind:this={viewer} />
</div>
<button on:click={addPlanets}>Show planets</button>

<style>
  .viewer-wrapper { width: 100%; height: 400px; }
</style>
```

Simpler variant without a named root (use the scene as parent):

```svelte
// Create spheres directly in the scene
const scene = viewer.getScene();
if (!scene) return;
const sphere = MeshBuilder.CreateSphere('planet', { diameter: 1 }, scene);
sphere.position = new Vector3(0, 0, 0);
viewer.addContentRoot(sphere);  // sphere is the root; it will be included in frameModel()
viewer.frameModel();
```

- Use **`getScene()`** to create or modify any Babylon objects (meshes, lights, materials).
- Use **`addContentRoot(node)`** so that node (and its children) are included when you call **`frameModel()`** and so they participate in shadow casting/receiving by default.
- **`clearScene()`** only clears models loaded via **`loadModel()`**; it does not dispose content you added with **`addContentRoot()`**. Dispose your own nodes when you no longer need them.

## Full page example

```svelte
<script lang="ts">
  import { ThreeDViewer } from '3d-viewer';

  let viewer;
  const defaultModel = 'https://example.com/assets/model.glb';
</script>

<main>
  <div class="viewer-container">
    <ThreeDViewer
      bind:this={viewer}
      zUp={true}
      modelUrl={defaultModel}
      showViewCube={true}
    />
  </div>
</main>

<style>
  main {
    display: flex;
    flex-direction: column;
    height: 100vh;
  }
  .viewer-container {
    flex: 1;
    min-height: 0;
  }
</style>
```

## Camera controls

- **Left drag** — Orbit around target (or click-and-hold to set rotation point).
- **Middle mouse drag** — Orbit.
- **Right drag** — Pan.
- **Scroll** — Zoom (zoom-to-cursor when supported).
- **WASD** — Move target.
- **Q / E** — Move target down / up.
- **Space** — Continuous orbit.
- **Caps Lock** — Precision (slower) mode.

## Exposing the API to your app (ground, home, view mode)

Use `bind:this` to get the viewer instance, then call the methods from your own buttons or logic:

- **Ground:** Set initial state with the `showGround` prop, or toggle at runtime with `viewer.setGroundVisible(true)` / `viewer.setGroundVisible(false)`.
- **Home:** Call `viewer.goHome()` to jump to the front–top–right view and re-frame the model.
- **View mode:** Set initial projection with the `initialViewMode` prop (`'perspective'` or `'orthographic'`). At runtime use `viewer.setProjectionMode(true)` for orthographic, `viewer.setProjectionMode(false)` for perspective, and `viewer.getProjectionMode()` to read the current mode.

Example:

```svelte
<script>
  import { ThreeDViewer } from '3d-viewer';
  let viewer;
</script>

<ThreeDViewer bind:this={viewer} showGround={false} initialViewMode="perspective" />
<button on:click={() => viewer?.setGroundVisible(true)}>Show ground</button>
<button on:click={() => viewer?.setGroundVisible(false)}>Hide ground</button>
<button on:click={() => viewer?.goHome()}>Home</button>
<button on:click={() => viewer?.setProjectionMode(true)}>Orthographic</button>
<button on:click={() => viewer?.setProjectionMode(false)}>Perspective</button>
```

**Types / .d.ts** — You don't add a hand-written `.d.ts` in source. The build (Vite + Svelte) emits `dist/index.d.ts` from your entry and component. With `package.json` `"types": "./dist/index.d.ts"`, installers get types for the component and its instance (including the new methods). Ensure `npm run build` has been run (or they install from GitHub so `prepare` runs it).

## Coordinate system and viewer quirks

For detailed context on **Z-up vs Y-up**, how Fusion exports Z-up and KiCad Y-up by default, and other quirks (SpotLight-only shadows, model-relative lighting, view cube always Z-up, etc.), see **[VIEWER_CONTEXT.md](./src/VIEWER_CONTEXT.md)**. That doc is written for AIs and maintainers.

## Notes

- The viewer uses Babylon’s default CDN for environment/skybox assets; no extra static files are required.
- The component owns a single Babylon engine/scene per instance; on destroy it stops the render loop and disposes the engine and scene.

## Publishing

The package already has **`prepublishOnly`: `"npm run build"`**, so `npm publish` builds the library first and consumers get an up-to-date `dist/`. You do not need to commit `dist/` to git.
