# `SceneModel`

> Contract 2.0.0 · package sha256 `a857eca95b35921109faf997094bb4192f12527d9d0fcd4be7e97f06425bad6e` · generated from `configurator-runtime.d.ts` by `services/onsite-scripter/scripts/build-contract-docs.mjs`; edit the source, not this file.

Interface · `import type { SceneModel } from '@holostep-configurators/runtime-client/contract';` · stable · since 2.0.0 · section 21. Stored data (vendored; replaces @holo/interfaces)

What the runtime renders: the IConfigurator fields it reads. IConfigurator stays assignable to
it. The runtime's copy ([`getSceneModel()`](Writer.getSceneModel.md)) carries no key material: never save it as is.
The package has no tree helpers: the runtime never prices, and in an app with its own store the
store owns the model (one scene-model writer). The example's helpers are the app-side ones a
page writes once; run `withNode` on the store's document, then sync.

## Members

| Member | Kind | Summary |
| --- | --- | --- |
| <a id="root"></a>[`root`](SceneModel.root.md) | property | The node tree. |
| <a id="defaultnodeid"></a>[`defaultNodeId?`](SceneModel.defaultNodeId.md) | property | As [`IConfigurator.defaultNodeId`](IConfigurator.defaultNodeId.md). |
| <a id="connections"></a>[`connections?`](SceneModel.connections.md) | property | Input only: [`getSceneModel()`](Writer.getSceneModel.md), [`state().model`](SessionState.model.md) and the events never carry it (read [`assembly.connections()`](Assembly.connections.md)). |
| <a id="connectionsbasis"></a>[`connectionsBasis?`](SceneModel.connectionsBasis.md) | property | Input only, as `connections`. |
| <a id="defaultview"></a>[`defaultView?`](SceneModel.defaultView.md) | property | Stored data the app converts with [`cameraViewFromStored`](cameraViewFromStored.md), then passes to [`camera.set`](Camera.set.md); a scene write never moves the camera ([`IDefaultView`](IDefaultView.md)). |
| <a id="presets"></a>[`presets?`](SceneModel.presets.md) | property |  |
| <a id="attributes"></a>[`attributes?`](SceneModel.attributes.md) | property |  |
| <a id="parameters"></a>[`parameters?`](SceneModel.parameters.md) | property |  |
| <a id="tonemapping"></a>[`toneMapping?`](SceneModel.toneMapping.md) | property | Carried for a spread [`IConfigurator`](IConfigurator.md); never read ([`IConfigurator.toneMapping`](IConfigurator.toneMapping.md)). |
| <a id="backgroundcolor"></a>[`backgroundColor?`](SceneModel.backgroundColor.md) | property | As [`IConfigurator.backgroundColor`](IConfigurator.backgroundColor.md): a non-null value is the scene model's background layer ([`BackgroundValue`](BackgroundValue.md)). |
| <a id="hdrurl"></a>[`hdrUrl?`](SceneModel.hdrUrl.md) | property | Carried for a spread [`IConfigurator`](IConfigurator.md); never read ([`IConfigurator.hdrUrl`](IConfigurator.hdrUrl.md)). |
| <a id="name"></a>[`name?`](SceneModel.name.md) | property |  |
| <a id="_id"></a>[`_id?`](SceneModel._id.md) | property |  |

## Declaration

```ts
export interface SceneModel {
  root: ConfiguratorNode[];
  defaultNodeId?: string;
  connections?: Connection[];
  connectionsBasis?: string;
  defaultView?: IDefaultView;
  presets?: IPreset[];
  attributes?: Record<string, unknown>;
  parameters?: Record<string, unknown>;
  toneMapping?: string;
  backgroundColor?: BackgroundValue;
  hdrUrl?: string;
  name?: string;
  _id?: string;
}
```

## Example

Imports: values (`has`, `validate`, the `Holo*Error` classes, the pure helpers) from `@holostep-configurators/runtime-client`, types from `@holostep-configurators/runtime-client/contract`, `/math` and `/testing` names from those entries; `boot` from the runtime module the root's `loadRuntimeAtFloor` resolves to. A type your page defines is your app's: declare it there, never import it from the package.

Compiled under the contract's strict settings (`examples/declaration/21-SceneModel.ts`):

```ts
import type { ConfiguratorNode, DeepReadonly, SceneModel, Session } from '@holostep-configurators/runtime-client/contract';
declare const session: Session;
declare const signal: AbortSignal;
// A node anywhere in the tree, by nodeId.
export function findNode(
  nodes: readonly DeepReadonly<ConfiguratorNode>[],
  nodeId: string,
): DeepReadonly<ConfiguratorNode> | null {
  for (const node of nodes) {
    if (node.nodeId === nodeId) return node;
    const found = findNode(node.children ?? [], nodeId);
    if (found) return found;
  }
  return null;
}
// A copy with `node` added under `parentId` (at the root without one), the rest untouched.
export function withNode(
  model: DeepReadonly<SceneModel>,
  node: ConfiguratorNode,
  parentId?: string,
): SceneModel {
  const copy = structuredClone(model) as SceneModel;
  const addUnder = (nodes: ConfiguratorNode[]): boolean =>
    nodes.some((n) => {
      if (n.nodeId !== parentId) return addUnder(n.children ?? []);
      n.children = [...(n.children ?? []), node];
      return true;
    });
  if (parentId === undefined) copy.root.push(node);
  else if (!addUnder(copy.root)) throw new Error(`no node ${parentId}`);
  return copy;
}
// A global-material group's selected material: `select(group, material)` records it as the
// group's `defaultNodeId`. Prices are your app's: read them from your catalogue by this id.
export function selectedMaterial(model: DeepReadonly<SceneModel>, group: string): string | null {
  const node = findNode(model.root, group);
  return node?.type === 'GroupNode' ? (node.defaultNodeId ?? null) : null;
}
```

## Generic-API test (spec 3.1)

| Rule | Result |
| --- | --- |
| 1. Neutral name | pass: `SceneModel` names no product |
| 2. A precedent | not recorded: no ledger row (coverage.json) names this member or its owner |
| 3. Two apps, or labs | pass: 20 apps (fleet cross-check: app-04c2774145, app-0c773a4ee3, app-37356bf09f, app-4762caacdc, app-4fd23bc869, app-822bb9d6aa, app-8675b3225c, app-b362a81c85 and 12 more) |
| 4. The cheaper fix first | not recorded per member (Appendix B decides it per need) |
