Writer.setSceneModel ​Contract 2.0.0 · package sha256
a857eca95b35921109faf997094bb4192f12527d9d0fcd4be7e97f06425bad6e· generated fromconfigurator-runtime.d.tsbyservices/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
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.
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.
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
txthe 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'.
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):
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):
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):
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');
| 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) |