Writer.setSceneModel ​

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.

Method of Writer · inherited by Tx, Session, testing.FakeSession · stable · since 2.0.0 · called as session.setSceneModel

ts
setSceneModel(model: DeepReadonly<SceneModel>, options?: RecordedOptions<C> & SceneModelOptions): Promise<Report>;

Replaces the scene model (recorded; replacing; in the write queue). Keeps every nodeId; a new glbUrl under the same nodeId replaces the content and keeps the node. Loads run hidden; the swap is one frame. Two pending calls coalesce only when adjacent in the queue: the older resolves 'superseded' and only the newer one's options apply. An older call that carries undo: 'rebase', a checkpoint or actor 'shopper' is superseded only by one that carries the same, and a call is superseded only by one made under the same history group, or under none when it was (O14); otherwise both commit (spec section 6.1, O2c), so a load keeps its floor and a gesture keeps its last write. An equal model commits nothing (changed: false). It is compared as assetBaseUrl resolves it, since the state keeps the resolved URLs (getSceneModel() reads them): the same model under a base that resolves every URL the same ('…/164' and '…/164/') is equal, and under a base that resolves a URL elsewhere it is a change, so that node loads from there (spec section 6.1, O15). Equal or not, problems lists one asset-failed, with node, for each node the model places (spec section 6.9, ID10) that this write leaves without its asset: a fresh failure, a node an earlier write failed whose refetch after the failure window fails again, or one still inside it (no request; spec sections 6.1, O15, and 6.4). To make only what a write adds all or nothing, act on the problems whose node the model before it (getSceneModel()) didn't hold. Connections (spec section 6.12, A8): model.connections given are restored without a re-snap ('connection-stale' drops one whose planes no longer meet; a connectionsBasis mismatch, 'connections-outdated', restores none); omitted keeps the current ones; [] clears them. What to pass: SceneModel.connections and connectionsBasis. A model you got from getSceneModel() still decrypts: the runtime keeps key material in a private store filled from your inputs. It never moves the camera, the first call included: model.defaultView is stored data (degrees, in other names): convert it with cameraViewFromStored, then camera.set, or call camera.fit. Outcome: 'committed' at its commit point, after its loads; through a tx, 'staged' once loaded; 'rolled-back' with reason 'internal-error' when applying it failed inside the runtime (through a tx, it then rejects AbortError and the transaction resolves 'rolled-back'; spec section 6.4). Errors: rejects HoloInputError 'missing-node-id' or 'duplicate-node-id'. Asset failures: problems.

Options ​

Through Session:

Option Type Default What it does
actor Actor the expectGroup's actor when one is given, else 'app' Who made the write.
assetBaseUrl string the runtime's asset root Absolute HTTP(S) directory for the model's GLB, try-on rig (tryOnUrl, tryOnCandidateUrl), texture and HDR URLs; any other value rejects…
checkpoint JsonValue unchanged Page state stored atomically with this step; returned in state().checkpoint and the feed.
dryRun boolean false Runs and reports, then rolls back by design ('dry-run').
expectGroup HistoryGroup undefined A guard, checked at the call: this write belongs to the undo group you are running.
expectRevision number undefined (no check) If-match on session.revision.
origin string null Your tag, echoed unchanged in Report.origin and cause.origin.
signal AbortSignal undefined Cancels this call; the member's JSDoc says what an abort does.
supersede boolean false (the write is queued behind the transaction and applies after it closes) While a transaction is open: roll it back (it resolves 'superseded'); the writes queued behind it apply in call order, then this one, which supersedes an…
undo 'rebase' | (string & {}) a recorded-state change gets one step labelled 'Change'; a look-only write gets none; the session's first model (cause 'load') gets none, whatever the label: it is the floor of history An undo label makes this write one undo step.

Through Tx:

Option Type Default What it does
assetBaseUrl string the runtime's asset root Absolute HTTP(S) directory for the model's GLB, try-on rig (tryOnUrl, tryOnCandidateUrl), texture and HDR URLs; any other value rejects…
signal AbortSignal undefined Cancels this call; the member's JSDoc says what an abort does.

Refused here (typed never, so passing one fails to compile): actor, checkpoint, dryRun, expectGroup, expectRevision, origin, supersede, undo.

Abort (signal) ​

This call is a write (SignalOption.signal, the kinds of call).

A write: before the commit point it rolls back and rejects AbortError; after it, no effect, except that a running camera move stops where the last frame drew it (CameraMoveOptions).

Through a tx the commit point is the transaction's: aborting a write still loading rejects AbortError and stages nothing (catch it: unobserved, it rolls the transaction back, spec section 6.1, O6); aborting one already staged unstages it, its paths keeping the latest other write to them as after a failed savepoint (O7), and its Promise stays resolved 'staged'.

Examples ​

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/05-Writer.setSceneModel-1.ts):

ts
import type { IConfigurator, JsonPatchOp, JsonValue, Session } from '@holostep-configurators/runtime-client/contract';
declare const session: Session;
declare const signal: AbortSignal;
// A store sync, both ways. The store's document stays the source of truth.
declare const store: { replace(doc: IConfigurator): void };
declare function rootFor(doc: IConfigurator): string;
// applies a commit's scene-model ops to `doc` by nodeId: each node's position, rotation,
// scale and enabled and each group's defaultNodeId (a field op, or a node an undo adds back)
declare function mergeChanges(doc: IConfigurator, patch: readonly JsonPatchOp[]): IConfigurator;
// what the store owns beyond placement (an instance list, selections); at most 256 KB
declare function structureOf(doc: IConfigurator): JsonValue;
declare function withStructure(doc: IConfigurator, structure: JsonValue): IConfigurator;
declare function reportSyncError(error: unknown): void;
const SYNC = 'store-sync';
let current: IConfigurator | null = null;        // the store's latest document
let echoed: IConfigurator | null = null;         // the document this code pushed into the store
let edits = 0;                                   // store edits so far, echoes excluded
let editsAtUndo = 0;                             // `edits` when the shopper last pressed undo

// Runtime → store. Skip our own sync by `origin`, and merge only what each commit changed
// (its `patch`), never its whole model: a commit's payload can predate the store's latest
// document (a write called before that sync can commit after the sync's call, `Cause.seq`),
// and merging a whole snapshot would put that document's newer placement back (spec section
// 6.11, rule 6). The delta keeps both: a shopper's pick made while a sync was queued returns
// to the store, and the store's own newer edits stay (the runtime's model has no key
// material: never save it). An undo or redo can add or remove nodes, which placement can't
// carry: rebuild the store's structure from the checkpoint it restored first. A store edit
// made after undo was pressed owns the future: its queued sync commits after the undo and
// re-projects the store, so that undo is not adopted.
session.on('committed', ({ cause, patch, checkpoint }) => {
  if (cause.origin === SYNC || !current) return;
  if (!patch.some((op) => op.path.startsWith('/model/'))) return;   // a look-only commit
  const undone = cause.kind === 'undo' || cause.kind === 'redo';
  if (undone && edits !== editsAtUndo) return;
  const base = undone && checkpoint !== null ? withStructure(current, checkpoint) : current;
  store.replace((echoed = mergeChanges(base, patch)));
}, { signal });

// The toolbar's undo (a redo button does the same).
export function onUndoClick(): void {
  editsAtUndo = edits;
  session.undo({ actor: 'shopper', origin: 'toolbar-undo' }).catch(reportSyncError);
}

// Store → runtime. Newest wins; a labelled undo step. Never `undo: 'rebase'` here: after a
// shopper edit it would erase the shopper's undo steps.
export function onStoreChange(doc: IConfigurator): void {
  current = doc;
  // The reverse echo (the store repeating our own merge) goes back without `supersede`: equal,
  // it commits nothing and rolls back no open transaction; if a sync queued before the shopper's
  // change replaced it meanwhile, it puts that change back. Never skip it.
  const echo = doc === echoed;
  if (!echo) edits += 1;
  const options = { assetBaseUrl: rootFor(doc), origin: SYNC, supersede: !echo, undo: 'Configure' };
  // Rejects only on invalid input or abort. The checkpoint lets an undo restore the structure.
  const sync = () => session.setSceneModel(doc, { ...options, checkpoint: structureOf(doc) });
  // An internal failure resolves 'rolled-back' with the scene restored (spec section 6.4):
  // report it, and send this document once more unless a newer one has come.
  sync().then(async (r) => {
    if (r.outcome !== 'rolled-back') return;
    reportSyncError(r);
    const again = doc === current ? await sync() : null;
    if (again?.outcome === 'rolled-back') reportSyncError(again);
  }).catch(reportSyncError);
}

Compiled under the contract's strict settings (examples/declaration/05-Writer.setSceneModel-2.ts):

ts
import type { DeepReadonly, IModelNode, SceneModel, Session } from '@holostep-configurators/runtime-client/contract';
declare const session: Session;
declare const signal: AbortSignal;
// Read, modify, write: add a shelf and turn a node off, in one recorded step.
// getSceneModel() is DeepReadonly<SceneModel>, which setSceneModel takes as it is: build the
// next model with spreads into a DeepReadonly<SceneModel>. Never annotate it `SceneModel`
// (the read is not assignable to it, TS2322) and never mutate the read.
declare const shelf: IModelNode;                 // a new node with its own nodeId
declare const lampId: string;                    // a root node's nodeId
const model = session.getSceneModel();
if (model) {
  const next: DeepReadonly<SceneModel> = {
    ...model,
    root: [...model.root.map((n) => (n.nodeId === lampId ? { ...n, enabled: false } : n)), shelf],
  };
  await session.setSceneModel(next, { undo: 'Add shelf' });
}

Compiled under the contract's strict settings (examples/declaration/05-Writer.setSceneModel-3.ts):

ts
import type { SceneModel, Session } from '@holostep-configurators/runtime-client/contract';
declare const session: Session;
declare const signal: AbortSignal;
// Show one GLB you have, with no saved design (a modeller's check, a quick demo): the smallest
// scene model is one ModelNode with its required fields. Without `enabled: true` it validates
// and is never drawn.
declare const glbUrl: string;                     // absolute, or relative to assetBaseUrl
const model: SceneModel = {
  root: [{
    type: 'ModelNode', nodeId: 'preview', glbUrl, enabled: true,
    position: { x: 0, y: 0, z: 0 }, rotation: { x: 0, y: 0, z: 0 }, scale: { x: 1, y: 1, z: 1 },
  }],
};
const r = await session.setSceneModel(model);
for (const p of r.problems) console.warn(p.code, p.url ?? '', p.message); // 'asset-failed'
await session.camera.fit();                       // frame it
console.info(session.describe('preview')?.manifest ?? 'no stamp: manifestFromGlb reads the file');

Generic-API test (spec 3.1) ​

Rule Result
1. Neutral name pass: setSceneModel names no product
2. A precedent pass: Unity Undo.RecordObject (row X6); a correctness rule (rows G33)
3. Two apps, or labs pass: 22 apps (ledger: app-37356bf09f, app-4f16d2f6da, app-4fd23bc869, app-822bb9d6aa, app-8675b3225c, app-bcb7ef21da, app-c1384707fc, app-ddfb017681 and 10 more; 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 decided per need in Appendix B: rows G33, X6 at tier stable (a member only where no rule, data, asset or app code serves the need)