# LuxarLayer Embed Contract

## Scope

`LuxarLayer` is the headless embedding API for adding a Luxar scene to a host application's own
Three.js renderer, camera, controls, scene graph, and render loop. It contributes one
`THREE.Group` plus Luxar's loading, nD slicing, LOD selection, depth sorting, and teardown
bookkeeping. It does not create or own host rendering infrastructure.

The supported public entry point is:

```ts
import { LuxarLayer } from '@luxar/viewer';
```

The runnable reference host is `packages/luxar-viewer/examples/layer/`.

## Ownership and lifecycle

The host owns the renderer, scene, camera, controls, canvas, render loop, post-processing, and UI.
It passes live camera and viewport getters because either can change after construction.

```ts
const layer = new LuxarLayer({
  renderer,
  scene,
  getCamera: () => camera,
  getViewportSize: () => ({ width, height }),
  requestRender: () => {
    frameRequested = true;
  },
});

await layer.load(src);
```

`load()` attaches the returned root to the host scene. A later successful load replaces and
disposes the previous subtree. A failed replacement leaves the layer empty rather than displaying
geometry backed by a disposed loader. Concurrent `load()` calls are rejected.

The host must await `dispose()` before constructing another `LuxarLayer`. Disposal waits for an
active load and dimension update, detaches layer geometry, and releases the process-wide loader,
material, LOD, sorting, and worker resources.

## Public surface

The constructor also accepts loader and asset-path overrides plus `gpuPoolMaxBytes`, `lodFade`,
`lodEnergyComp`, `lodFinest`, `lodBias`, and `depthSort` feature switches. `renderOrder` defaults to 10.
`requestRender` is optional only for hosts that render continuously; an on-demand host must use it
to schedule a future frame when progressive geometry, a lazy LOD level, or a retry commits
asynchronously.

| Member | Contract |
| --- | --- |
| `root` | Current layer root, or `null` before load and after disposal. |
| `load(src)` | Load and attach a scene, replacing the previous successful load. |
| `update()` | Run depth sorting and LOD selection before the host renders. |
| `resize()` | Publish the current camera projection and drawing-buffer size. |
| `getDimensions()` | Return cloned dimension state, or `null` when unavailable. |
| `findDimension(name)` | Find a dimension case-insensitively, or return `null`. |
| `getDimensionNames()` | Return names in center-column order. |
| `setDimensionValue(index, value)` | Coalesce and commit a foreground slice update. |
| `prefetchDimensionValue(index, value, budgetMs?)` | Warm a slice without committing it. |
| `awaitDimensionUpdate()` | Resolve when no dimension update remains in flight. |
| `setVisible(visible)` / `isVisible()` | Hide or show without discarding caches. |
| `setExposure(multiplier)` / `getExposure()` | Scale opacity relative to authored values. |
| `getBounds()` | Return world-space bounds for the current root, or `null`. |
| `alignTo(matrix)` | Place the root in host world space before or after `load()`. |
| `onDatasetFault(listener)` | Subscribe to `DatasetFaultPayload` archive faults, replaying the current fault immediately; returns an unsubscribe function and throws after `dispose()`. |
| `getDatasetFault()` | Return the current `DatasetFaultPayload`, or `null` before a fault occurs, once a new `load()` begins, and after disposal. No callback fires when a fault clears, so a host that renders recoverable state must poll this method rather than latch the callback result. |
| `handleContextLost()` / `handleContextRestored()` | Re-arm Luxar-owned GPU resources around host context recovery. |
| `dispose()` | Asynchronously release the layer and process-wide Luxar resources. |

## Per-frame ordering

Every host frame must execute:

```ts
layer.update();
renderer.render(scene, camera);
```

`update()` deliberately runs the depth-sort scheduler before the LOD selector. Sorting establishes
cross-node render ranks; LOD evaluation can then change the visible level without leaving those
ranks stale for the frame. Calling only `renderer.render()` may leave lazy geometry, LOD, and
depth-order state stale.

`requestRender` does not replace `update()`. It wakes an on-demand host after asynchronous work;
the requested frame must still call `update()` before `renderer.render()`.

Call `resize()` after changing the viewport or camera projection. The layer reads the host camera
and viewport but cannot observe those changes itself.

## Dimensions

`getDimensionNames()` returns center-column order; embedders must not assume `x,y,z,time` ordering.
`setDimensionValue()` coalesces rapid requests and resolves after the pass containing that value
commits. Playback should issue foreground updates without awaiting each frame and use
`prefetchDimensionValue()` for the next value.

## Draw order and visibility

The layer stamps its configured `renderOrder` (10 by default) onto every owned `THREE.Group`,
including groups attached lazily. The host remains responsible for assigning compatible orders to
its own transparent groups.

`setVisible(false)` keeps caches and in-flight requests alive. Lazy LOD loads pause while hidden;
resident hidden levels are preferred eviction candidates under GPU pressure.

## Context loss and restore

The host owns the WebGL context event handlers. On loss it must prevent the browser default and
notify the layer; after rebuilding its own renderer and post-processing resources it must notify
the layer again:

```ts
canvas.addEventListener('webglcontextlost', (event) => {
  event.preventDefault();
  layer.handleContextLost();
});

canvas.addEventListener('webglcontextrestored', () => {
  layer.handleContextRestored();
});
```

The loss hook clears shader warm-up state and reduces the GPU budget. The restore hook marks only
the layer subtree dirty and re-arms Luxar-owned resources; it does not restore host resources.

## Single-instance rule

Only one `LuxarLayer` may exist per page, and it must not coexist with `LuxarApp`. The scene-loader,
dimension, material, LOD, and worker managers are process singletons. Multiple owners would share
and tear down each other's state.

## Known limitations

- Scene `viewer_config` values implemented by Luxar UI, camera, or post-processing code are inert:
  tone mapping, global exposure/gamma/offset, bloom, background, camera settings, and related UI.
- The host must provide its own tone mapping. Without a rolloff, overlapping emissive additive
  geometry can clip flat in the framebuffer.
- Near-plane culling is not derived in layer mode; the host should keep its near plane clear of the
  data.
- `setDimensionValue()` before `load()` is a no-op because dimension metadata comes from the scene.
- Default opaque mesh materials interpret opacity as alpha-cutout coverage rather than a smooth
  dimmer; use normal blending when smooth transparency is required.
- KTX2 mesh textures decode through a renderer-owned decoder installed by the layer. Other host
  texture and renderer resources remain the host's responsibility.
- The UI-layer `notifier` is not registered, so Luxar toasts and error overlays are silently
  dropped. Hosts must provide their own user-facing status and error surface. Archive failures
  remain observable through `onDatasetFault()` and `getDatasetFault()`.

## Coverage note

The runnable example's LOD E2E fixture exercises the legacy `coverage` selector path. The current
`screen-area` selector used by newly authored ladders requires separate fixture coverage.
