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:

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.

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:

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:

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.