Contract 2.0.0 · package sha256
a857eca95b35921109faf997094bb4192f12527d9d0fcd4be7e97f06425bad6e· generated frommigration.jsonandconformance/port-usage.jsonbyservices/onsite-scripter/scripts/build-contract-docs.mjs; edit the source, not this file.
Every 1.x member (contract 1.10), every internal pattern the apps reach and every version-1 viewer-document key, each with one disposition (migration.json, 258 rows: 55 kept, 32 removed, 138 replaced, 33 runtime-owned). Each replaced row carries a recipe that compiles against the 2.0 declaration under the apps' strict settings (139 recipes); a recipe takes the 1.x values as their 1.x types (V1, the v1 tenant declaration) and calls the 2.0 contract.
as and recipe).replacement) and a compiled recipe (code).recipe).Port each use through its row, then delete the hatch file, every engine import in src/ and the dead code. A gap goes back into the rc, never around it: no casts, no element or shadow-root reads, no engine import, no hatch "for now".
"Used by" names the apps whose 1.x source or paper port uses the row (conformance/port-usage.json, 28 apps, configurators be828d5d).
/contract; replace @holo/interfaces imports)TenantEventMap → Events)runtime-escape-hatches.ts)What 2.0.0 doesn't do yet. Every form the declaration names is built: the four stage forms it first shipped without (a panorama background, environment: 'room', ground.shadow.on and a stage.set fadeMs above 0) draw on the runtime and the fake since F1a (DL64), and their supports() ids answer true.
2.0.0 doesn't have these rules yet, on the runtime and the fake alike: both reach the same outcome, so a port's unit test agrees with the browser. Write the app for the outcome below until a 2.x release builds the rule.
onFrame hook writes shows one frame later, not in the frame that wrote it: hide a cut wall with Frame.visible in the same hook, which draws in that frame, and expect its ghost rules a frame aftertransaction after a staged scene write that adds its part reports matched: 0 and pending: true, and applies when the transaction commitsCollection.load item while a queued scene write still prepares counts that item at the call, so aborting the load afterwards records no target-unmatched; behind an open transaction it doesMaterialNode that a scene write shows (select, setSceneModel, applyPatch) is ready at its colour maps, its detail maps (normal, roughness, metallic, occlusion, ORM) landing after the commit; a rule's material maps are waited for whole, so a rule with a slow normal map commits once that map has decodedMaterial waits for none of its maps, and a map of it that fails is in no write's Reportcompleted and settled with the neutral stand-in in that slot, and lists no asset-failed in its problemsambientOcclusion or bloom object a layer writes over a merged true or false is drawn with the document's fields it omits, but stage.get() reads the layer's object alone: read the document's own values for those fieldsboot(options) ​kept · written for 1.x 1.5
add contract: '2.0'; a renderer that can't start rejects HoloBootError (1.4 rethrew the raw error, 1.5 a BootError)
Used by: app-04c2774145, app-0c773a4ee3, app-37356bf09f, app-378d190f1b, app-4762caacdc, app-4f16d2f6da, app-4fd23bc869, app-510d750508, app-59c5b5751e, app-822bb9d6aa, app-8675b3225c, app-b362a81c85, app-b4ae2fce25, app-bcb7ef21da, app-c1384707fc, app-ceb4eccc72, app-ddfb017681, app-e3b47169bc, app-e61a0b42ad, app-e9a33606d2, app-f07e9033d9, app-f3598fad77, app-f9a32142f1, app-fb16dd1985, app-fc07850558, app-fc306ec3dc, app-fd09090420, configurator-template.
BootOptions.el, .accountId, .configuratorId, .document, .signal ​kept · written for 1.x 1.5
.document is the page layer: it merges over the platform defaults and under the account's defaults and the configurator's stored document, which win, as on 1.x since production 8211ec9a30 (before it, the page was the top layer)
Used by: app-04c2774145, app-0c773a4ee3, app-37356bf09f, app-378d190f1b, app-4762caacdc, app-4f16d2f6da, app-4fd23bc869, app-510d750508, app-59c5b5751e, app-822bb9d6aa, app-8675b3225c, app-b362a81c85, app-b4ae2fce25, app-bcb7ef21da, app-c1384707fc, app-ceb4eccc72, app-ddfb017681, app-e3b47169bc, app-e61a0b42ad, app-e9a33606d2, app-f07e9033d9, app-f3598fad77, app-f9a32142f1, app-fb16dd1985, app-fc07850558, app-fc306ec3dc, app-fd09090420, configurator-template.
BootError (1.5) ​replaced (replaced by HoloBootError) · 2.0: HoloBootError · written for 1.x 1.5
e.name === 'BootError' → e instanceof HoloBootError; reason and cause keep their meaning, and message is now a plain sentence for reason (the underlying text stays in cause); 'error' → 'other'
Used by: no app in the cross-check.
/** 1.x `BootError`: inspect the typed boot rejection. */
export function bootErrorDetails(
error: unknown,
): { reason: HoloBootError['reason']; message: string; cause: unknown } | null {
if (!(error instanceof HoloBootError)) return null;
return { reason: error.reason, message: error.message, cause: error.cause };
}
BootFailureReason (1.5) ​replaced · 2.0: HoloBootError · written for 1.x 1.5
the closed output union HoloBootError['reason']: 'no-webgl2', 'webgl-context' and 'disconnected' kept; 'error' and the open (string & {}) tail → 'other' (3.6). runtime-client's bootFailureReason drops its message-text fallback (packages/runtime-client/src/boot-failure.ts:43-46 at 342b26ce) and keeps its !env.hasWebGl2 check (:42): a boot that ran rejects HoloBootError, but an entry that failed to import does not (5.1), so that check still sends a browser that can't parse the entry to 'no-webgl2''s advice
Used by: no app in the cross-check.
/** 1.x `BootFailureReason`: keep the browser check when the entry cannot import. */
export function bootFailureReason(
error: unknown,
env: { hasWebGl2: boolean },
): HoloBootError['reason'] {
if (!env.hasWebGl2) return 'no-webgl2';
return error instanceof HoloBootError ? error.reason : 'other';
}
BootOptions.viewerLine ​removed · written for 1.x 1.5
contract declares compatibility; the viewer line is diagnostic only
Used by: app-04c2774145, app-0c773a4ee3, app-37356bf09f, app-378d190f1b, app-4762caacdc, app-4f16d2f6da, app-4fd23bc869, app-510d750508, app-59c5b5751e, app-822bb9d6aa, app-8675b3225c, app-b362a81c85, app-b4ae2fce25, app-bcb7ef21da, app-c1384707fc, app-ceb4eccc72, app-ddfb017681, app-e3b47169bc, app-e61a0b42ad, app-e9a33606d2, app-f07e9033d9, app-f3598fad77, app-f9a32142f1, app-fb16dd1985, app-fc07850558, app-fc306ec3dc, app-fd09090420, configurator-template.
RUNTIME_CONTRACT, RUNTIME_VERSION, RuntimeContract ​kept · written for 1.x 1.5
apiVersion is '2.0.x'
Used by: no app in the cross-check.
Point3, ReadonlyPoint3, ScreenPoint, ProjectedPoint, JsonValue ​kept · written for 1.x 1.5
—
Used by: app-37356bf09f, app-4f16d2f6da, app-4fd23bc869, app-822bb9d6aa, app-8675b3225c, app-b362a81c85, app-b4ae2fce25, app-bcb7ef21da, app-c1384707fc, app-ddfb017681, app-e3b47169bc, app-e61a0b42ad, app-e9a33606d2, app-f07e9033d9, app-f3598fad77, app-fb16dd1985, app-fc07850558, app-fc306ec3dc, app-fd09090420, configurator-template.
Bounds ​kept · written for 1.x 1.5
—
Used by: no app in the cross-check.
BoundsOptions ​replaced · 2.0: Session.find, Parts.bounds · written for 1.x 1.5
camera.getBounds({ nodeIds }) → session.find(nodeIds.map((node) => ({ node }))).bounds()
Used by: no app in the cross-check.
/**
* `camera.getBounds({ nodeIds })`: the bounds of those models' parts; without `nodeIds`, of every
* scene-model part (role 'product'). Null when nothing matched.
*/
export function boundsOf(session: Session, options: V1.BoundsOptions = {}): Bounds | null {
const query = options.nodeIds
? options.nodeIds.map((node) => ({ node }))
: { role: 'product' as const };
return session.find(query).bounds();
}
Capability ​kept · written for 1.x 1.5
—
Used by: app-bcb7ef21da, app-e3b47169bc, app-e61a0b42ad, app-f07e9033d9, app-fb16dd1985, app-fd09090420, configurator-template.
KnownCapability ​replaced · 2.0: KnownCapability, Session.supports · written for 1.x 1.5
the 2.0 union, id by id: 'history.transaction' → 'history.group'; 'interaction.pick' → 'input.pick'; 'interaction.setDraggable' → 'input.drag'; 'interaction.snapTo' → 'assembly.attach'; 'interaction.pickGround' → none (camera.ray plus /math, always present); 'overlays.anchors', 'overlays.hotspots' kept; 'overlays.project' → none (camera.project is always present); 'idle', 'camera.getPose', 'camera.fitTo', 'camera.getBounds' → none (idle(), camera.state, camera.fit, find().bounds() are always present: delete the guard); 'events.*' → none (events are typed). A stale id still compiles, answers false and records unknown-capability (3.7)
Used by: no app in the cross-check.
/**
* 1.x `KnownCapability`: rename guarded capabilities; ask `supports` for the ids 2.0 keeps under
* their own name (a runtime may not build each, as 1.x's answered `tryOn.mode.handoff` false);
* always-present members need no guard.
*/
export function capabilityAvailable(session: Session, id: V1.KnownCapability): boolean {
switch (id) {
case 'history.transaction':
return session.supports('history.group');
case 'interaction.pick':
return session.supports('input.pick');
case 'interaction.setDraggable':
return session.supports('input.drag');
case 'interaction.snapTo':
return session.supports('assembly.attach');
case 'overlays.anchors':
case 'overlays.hotspots':
case 'activateAR.overlayRoot':
case 'activateAR.ui.headless':
case 'canTryOn':
case 'endAR':
case 'tryOn':
case 'tryOn.mode.handoff':
return session.supports(id);
default:
return true;
}
}
CommandReport ​replaced (replaced by Report) · 2.0: Report · written for 1.x 1.5
ok → r.outcome === 'committed' && r.problems.length === 0; failed → r.problems; superseded → r.outcome === 'superseded'
Used by: no app in the cross-check.
/** 1.x `CommandReport.ok`, `.failed`, `.superseded`: read the unified report. */
export function commandResult(r: Report): {
ok: boolean;
failed: readonly Problem[];
superseded: boolean;
} {
return {
ok: r.outcome === 'committed' && r.problems.length === 0,
failed: r.problems,
superseded: r.outcome === 'superseded',
};
}
AssetFailure ​replaced · 2.0: Problem, ReportFields.problems · written for 1.x 1.5
a Problem with code 'asset-failed', url, node and asset, in Report.problems: kind → asset ('material' → 'texture'); nodeId → node; materialNodeId → node (a MaterialNode is a scene-model node)
Used by: no app in the cross-check.
/** 1.x `AssetFailure`: asset failures now live in `Report.problems`. */
export function assetFailures(report: Report): readonly Problem[] {
return report.problems.filter((problem) => problem.code === 'asset-failed');
}
Problem ​kept (kept (changed)) · written for 1.x 1.5
nodeId → node; pluginId removed (no plugins); docs, op, didYouMean added
Used by: no app in the cross-check.
ProblemCode ​replaced · 2.0: ProblemCode, Problem · written for 1.x 1.5
'three-duplicate' → 'engine-conflict'; 'viewer-line-mismatch' removed with BootOptions.viewerLine (a bootstrap that refuses an entry of another major rejects HoloBootError 'other', 5.1); 'viewer-disconnected' → 'host-disconnected'; 'model-failed', 'material-failed' → 'asset-failed'; `apply:${string}` (1.x's prefix on each problem of the viewer's apply report, which carries the key's path: rt/session.ts:216-222 at eb5ce2afe) → 'document-error' with its JSON path and layer, while 'apply-failed' (kept) is a failure inside the runtime, with layer and no path; 'plugin-missing', 'plugin-disabled', 'setter-failed' removed (no plugins or setters); 'checkpoint-rejected' removed (history.checkpoint is gone: an oversized checkpoint option rejects HoloInputError('checkpoint-rejected') and records 'invalid-input' at /checkpoint); 'transaction-timeout' now means a transaction rollback; the rest kept
1.x codes: apply-failed → apply-failed; apply:${string} → document-error; checkpoint-rejected → none; defaults-unavailable → defaults-unavailable; deprecated → deprecated; document-error → document-error; handler-async → handler-async; handler-rejected → handler-rejected; handler-threw → handler-threw; hotspot-id-taken → hotspot-id-taken; invalid-input → invalid-input; material-failed → asset-failed; model-failed → asset-failed; plugin-disabled → none; plugin-missing → none; setter-failed → none; sync-failed → sync-failed; tenant-document-absent → tenant-document-absent; tenant-document-unavailable → tenant-document-unavailable; three-duplicate → engine-conflict; transaction-timeout → transaction-timeout; unknown-boot-option → unknown-boot-option; unknown-event → unknown-event; viewer-disconnected → host-disconnected; viewer-line-mismatch → none; xr-${string} → none.
Used by: no app in the cross-check.
/** 1.x `ProblemCode`: translate codes and retain document paths and layers. */
export function migrateProblem(
problem: V1.Problem,
layer: string,
): Omit<Problem, 'at' | 'docs'> | null {
const { code, message } = problem;
if (code.startsWith('apply:'))
return { code: 'document-error', message, path: problem.path, layer };
switch (code) {
case 'three-duplicate':
return { code: 'engine-conflict', message };
case 'viewer-disconnected':
return { code: 'host-disconnected', message };
case 'model-failed':
return { code: 'asset-failed', asset: 'model', node: problem.nodeId, message };
case 'material-failed':
return { code: 'asset-failed', asset: 'texture', node: problem.nodeId, message };
case 'viewer-line-mismatch':
case 'plugin-missing':
case 'plugin-disabled':
case 'setter-failed':
case 'checkpoint-rejected':
return null;
case 'document-error':
return { code, message, path: problem.path, layer };
case 'apply-failed':
return { code, message, layer };
default:
return { code: code as ProblemCode, message };
}
}
InfoReport ​kept (kept (changed)) · written for 1.x 1.5
three removed → renderer.engineConflict; viewer removed (diagnostic of an internal); renderer, device and ar (the AR modes this device can open, before the tap) added
Used by: no app in the cross-check.
NodeTarget ​replaced · 2.0: Query · written for 1.x 1.5
{ nodeId } → the query { node: nodeId }
Used by: app-04c2774145, app-37356bf09f, app-4762caacdc, app-ceb4eccc72, app-f9a32142f1.
/** 1.x `NodeTarget { nodeId }`: use a node query. */
export function nodeTarget(target: V1.NodeTarget): Query {
return { node: target.nodeId };
}
NodeReference ​replaced · 2.0: Hit.part, Part.node, Part.name · written for 1.x 1.5
{ nodeId, name } → hit.part.node, hit.part.name
Used by: no app in the cross-check.
/** 1.x `NodeReference { nodeId, name }`: read the hit part. */
export function nodeReference(hit: Hit): V1.NodeReference {
return { nodeId: hit.part.node, name: hit.part.name };
}
NodePose ​replaced (replaced by Pose) · 2.0: Pose, Part.worldPose, vec3, angle · written for 1.x 1.5
position → session.node(nodeId).worldPose.position; the radians field rotationY → the yaw about +Y of session.node(nodeId).worldPose.quaternion: angle.radToDeg(Math.atan2(f.x, f.z)) with f = vec3.applyQuat({ x: 0, y: 0, z: 1 }, quaternion), which keeps a 180° yaw at 180°; rotationDeg.y and quat.toEulerDeg decompose X then Y then Z, so a 180° yaw reads (180, 0, 180) and its y reads 0 (app-37356bf09f's 1.4 test src/__tests__/runtime/drag-commit.test.ts:72 at be828d5d)
Used by: no app in the cross-check.
/** 1.x `NodePose.position` and `rotationY`: read world position and the forward-vector yaw. */
export function nodeWorldPose(
session: Session,
nodeId: string,
): { position: ReadonlyPoint3; yawDeg: number } {
const { position, quaternion } = session.node(nodeId).worldPose;
const f = vec3.applyQuat({ x: 0, y: 0, z: 1 }, quaternion);
return { position, yawDeg: angle.radToDeg(Math.atan2(f.x, f.z)) };
}
/contract; replace @holo/interfaces imports) ​configurators' tenant-api keeps its own types-only copy of these stored-format types, taken from runtime-client's vendored contract so that it needs no workspace dependency (apps/tenant-api/src/core/catalog/configurator.ts:1 at c4c6ffd2). It reads stored documents into catalogs and spreads them through, so the 2.0 fields lose no data there, but the copy is re-copied from the 2.0 /contract in the commit of the first port that stores connections and connectionsBasis, or its types drift. Its date pass maps node.children unguarded (configurator-dates.ts:26-32), so the re-copy also guards a node without children, which 2.0 makes optional.
IConfigurator ​kept · written for 1.x 1.5
gains top-level connections and connectionsBasis. hdrUrl, hdrName and toneMapping stay storage only: the runtime never reads them, as 1.4's scene model left hdrUrl out (configurator-runtime/tenant-types.ts:454-464 at eb5ce2afe) and its viewer took the curve from the viewer document alone; an app that wants that HDR or curve writes stage.set or its page document. So app-e3b47169bc's /test-viewer/minimal page, whose stored studio HDR and 'AGX' 1.4 never applied either (apps/app-e3b47169bc/src/app/test-viewer/minimal/page.tsx:26-27 at be828d5d), renders as before. backgroundColor is read: a non-null value is the scene model's background layer, as 1.x's setSceneModel set it (BackgroundValue, 5.5)
Used by: no app in the cross-check.
IConfiguratorNode ​kept (kept (changed)) · written for 1.x 1.5
nodeId becomes required (normalizeNodeIds repairs old saves, and takes a loose input type, so no cast). Also: _id, label, order, expanded, createdAt, updatedAt, children and attributes become optional, and createdAt/updatedAt become Date | string. Code that read them as always present gets a compile error to fix
Used by: no app in the cross-check.
IModelNode ​kept · written for 1.x 1.5
gains space (labs, runtime-derived models only); dek, encryption, keyRef leave the type and every output; plainGlbUrl, plainOptGlbUrl input-only
Note: An encrypted tenant's stored keyRef (a .holo node's { glbUrl, plainGlbUrl, encrypted: true, keyRef }, as platform-assets.json manifests carry it) needs no 2.0 field: the node goes to the runtime as stored, and the .holo names its own key ref, which the runtime resolves as 1.x did (spec 8.5). Declare keyRef?: number in the app's own type, never Pick<IModelNode, 'keyRef'>, which fails TS2344; it passes through undeclared, as every undeclared node field does, and no output carries it, so keep the app's own copy for its saves (the backend also restores it by glbUrl). A dek is the only key input, passed the same way. No gap to file and no cast.
Used by: no app in the cross-check.
/**
* 1.x `IModelNode` key material, as an encrypted tenant's manifest typed it (`Pick<IModelNode,
* 'glbUrl' | 'plainGlbUrl' | 'encrypted' | 'keyRef'>`): 2.0 declares no `keyRef`, so the app's
* own type carries it, never `Pick<IModelNode, 'keyRef'>` (TS2344). The node goes to the runtime
* as stored: the `.holo` names its own key ref, which the runtime resolves as 1.x did, and no
* output carries it.
*/
export type StoredAsset = Pick<IModelNode, 'glbUrl' | 'plainGlbUrl' | 'encrypted'> & {
readonly keyRef?: number;
};
export function encryptedModelNode(
node: IModelNode,
asset: Pick<V1.IModelNode, 'glbUrl' | 'plainGlbUrl' | 'encrypted' | 'keyRef'>,
): IModelNode {
const stored: StoredAsset = asset;
return { ...node, ...stored };
}
IMaterialNode ​kept · written for 1.x 1.5
format unchanged; convert with materialSpecFromNode / materialNodeFromSpec
Used by: no app in the cross-check.
IGroupNode, IChildDefaults, IGlobalMaterialListEntry, IMagneticPlaneConfig, IMagneticPlaneRule, IPreset, IPresetNode, IPrice, IGradientBackground, IGradientColorStop, GradientDirection, BackgroundValue, ConfiguratorNode ​kept · written for 1.x 1.5
IMagneticPlaneConfig maps to Connector as 6.12 says (connector is the side, read through meta.connector); a GroupNode's childDefaults resolve under each direct child at every scene-model write, as in 1.4, and the model reads back as passed (6.12)
Used by: no app in the cross-check.
IDefaultView ​kept · written for 1.x 1.5
the runtime never reads it, as in 1.4, where only the debug tools copy one: a scene write never moves the camera, and the app applies its stored view with camera.set. The template's cameraViewFor (8 apps) lets a stored position win: port it as camera.set with the CameraView that helper computes (azimuth and polar by atan2, distance by hypot, focalOffset kept), or the app's first camera.set changes; a LookAtView zeroes focalOffset, so { position, target } only for a stored view without one (apps/configurator-template/src/components/viewer/use-viewer.ts:36-47 at c4c6ffd2)
Used by: no app in the cross-check.
ViewerDocumentJson ​replaced (replaced by version 2) · 2.0: ViewerDocumentJson · written for 1.x 1.5
no plugins, no engine keys; each version-1 key per C.7; the page document must be version 2, while published layers may stay version 1 (8.3)
Used by: app-04c2774145, app-37356bf09f, app-4762caacdc, app-4f16d2f6da, app-4fd23bc869, app-822bb9d6aa, app-8675b3225c, app-b362a81c85, app-bcb7ef21da, app-c1384707fc, app-e3b47169bc, app-e61a0b42ad, app-e9a33606d2, app-f07e9033d9, app-f3598fad77, app-f9a32142f1, app-fb16dd1985, app-fc306ec3dc, app-fd09090420, configurator-template.
/** 1.x `ViewerDocumentJson`: assemble the page from the C.7 fragments, preserving notes. */
export function pageDocument(
document: V1.ViewerDocumentJson,
migrated: Pick<ViewerDocumentJson, 'stage' | 'camera' | 'materials'>,
): ViewerDocumentJson {
return { schema: 'holo.viewer-document', version: 2, meta: document.meta, ...migrated };
}
SceneModel ​kept (kept (redeclared)) · written for 1.x 1.5
gains connections; IConfigurator stays assignable
Used by: app-04c2774145, app-0c773a4ee3, app-37356bf09f, app-4762caacdc, app-4fd23bc869, app-822bb9d6aa, app-8675b3225c, app-b362a81c85, app-bcb7ef21da, app-c1384707fc, app-e3b47169bc, app-e61a0b42ad, app-e9a33606d2, app-f07e9033d9, app-f3598fad77, app-f9a32142f1, app-fb16dd1985, app-fc306ec3dc, app-fd09090420, configurator-template.
SceneModelNode, SceneMaterialNode, SceneGroupNode, SceneNode ​replaced · 2.0: ConfiguratorNode, IModelNode, IMaterialNode, IGroupNode, normalizeNodeIds · written for 1.x 1.5
the stored node types (IModelNode, IMaterialNode, IGroupNode, ConfiguratorNode), with nodeId required
Used by: no app in the cross-check.
/** 1.x `SceneModelNode`, `SceneMaterialNode`, `SceneGroupNode`, `SceneNode`: repair required node ids. */
export function storedNodes(root: V1.SceneNode[]): ConfiguratorNode[] {
return normalizeNodeIds({ root }).doc.root;
}
SceneModelOptions ​kept · written for 1.x 1.5
member options only (assetBaseUrl, signal); the commit options come from WriteOptions on the session overload and are rejected through a tx (B1)
Used by: app-04c2774145, app-0c773a4ee3, app-37356bf09f, app-4762caacdc, app-4fd23bc869, app-822bb9d6aa, app-8675b3225c, app-b362a81c85, app-bcb7ef21da, app-c1384707fc, app-e3b47169bc, app-e61a0b42ad, app-e9a33606d2, app-f07e9033d9, app-f3598fad77, app-f9a32142f1, app-fb16dd1985, app-fc306ec3dc, app-fd09090420, configurator-template.
TextureChannel ​kept · written for 1.x 1.5
—
Used by: no app in the cross-check.
TextureFields, SurfaceFields, SceneFields, SceneNodeOptional, SceneNodeRequired ​removed · written for 1.x 1.5
internal helper types, folded into the declarations
Used by: no app in the cross-check.
stripNodeDeks (a value, not a type: app-f9a32142f1's cart postMessage, apps/app-f9a32142f1/src/lib/post-message.ts:1, 52-57 at c4c6ffd2) ​removed · written for 1.x 1.5
no 2.0 export: the app's own walk over root and every children, deleting dek on a copy, as 1.x's stripNodeDeks does (libraries/interfaces/src/node-dek.ts:9-17 at eb5ce2afe; 8.5); no backend sends a dek now, so the walk only guards older inputs. A helper joins the contract package when a second app forwards documents (3.1 rule 3)
Used by: app-f9a32142f1.
TenantSession (exported as Session) ​kept · written for 1.x 1.5
the 1.x entry exported TenantSession as Session, the name 2.0 keeps; each member has its own row below
Used by: no app in the cross-check.
applyDocument(document, options), ApplyDocumentOptions, ApplyReport ​kept (kept (changed)) · written for 1.x 1.5
ApplyDocumentOptions → SignalOption, which keeps its signal; ApplyReport.problems holds Problems; an apply aborted in time rejects AbortError (1.x resolved superseded: true); a failure inside the runtime while applying resolves ok: false with one 'apply-failed' problem (1.x rethrew it, rt/session.ts:186-189 at eb5ce2afe)
Used by: app-04c2774145, app-37356bf09f, app-4762caacdc, app-4f16d2f6da, app-4fd23bc869, app-822bb9d6aa, app-8675b3225c, app-b362a81c85, app-bcb7ef21da, app-c1384707fc, app-e3b47169bc, app-e61a0b42ad, app-e9a33606d2, app-f07e9033d9, app-f3598fad77, app-f9a32142f1, app-fb16dd1985, app-fc306ec3dc, app-fd09090420, configurator-template.
DocumentProblem ​replaced · 2.0: Problem, ApplyReport.problems · written for 1.x 1.5
Problem with code 'document-error', its path (always set), layer and, for a version-1 key, didYouMean; pluginId removed
Used by: no app in the cross-check.
/** 1.x `DocumentProblem`: read document errors, including their path, layer and suggestions. */
export function documentProblems(report: Report): readonly Problem[] {
return report.problems.filter((problem) => problem.code === 'document-error');
}
document ​kept · written for 1.x 1.5
—
Used by: app-4f16d2f6da, app-b362a81c85, app-fc07850558, app-fc306ec3dc.
setSceneModel(model, options) ​kept (kept (changed)) · written for 1.x 1.5
resolves a Report at the commit point; nodeId required; connections restored (omitted: kept where their nodes remain); in the write queue; checkpoint may ride along; app-fc306ec3dc passes undo: 'rebase' (revision 3 called that value undo: false)
Used by: app-04c2774145, app-0c773a4ee3, app-37356bf09f, app-4762caacdc, app-4fd23bc869, app-822bb9d6aa, app-8675b3225c, app-b362a81c85, app-bcb7ef21da, app-c1384707fc, app-e3b47169bc, app-e61a0b42ad, app-e9a33606d2, app-f07e9033d9, app-f3598fad77, app-f9a32142f1, app-fb16dd1985, app-fc306ec3dc, app-fd09090420, configurator-template.
select(globalMaterialNodeId, materialNodeId) ​kept · written for 1.x 1.5
resolves a Report
Used by: app-04c2774145, app-0c773a4ee3, app-37356bf09f, app-378d190f1b, app-4762caacdc, app-4f16d2f6da, app-4fd23bc869, app-510d750508, app-822bb9d6aa, app-8675b3225c, app-b362a81c85, app-b4ae2fce25, app-bcb7ef21da, app-c1384707fc, app-ddfb017681, app-e3b47169bc, app-e61a0b42ad, app-e9a33606d2, app-f07e9033d9, app-f3598fad77, app-f9a32142f1, app-fb16dd1985, app-fc07850558, app-fc306ec3dc, app-fd09090420, configurator-template.
SelectReport ​replaced · 2.0: Session.select, Session.getSceneModel · written for 1.x 1.5
r.model → session.getSceneModel() after the await
Used by: no app in the cross-check.
/** 1.x `SelectReport.model`: read the scene model after awaiting selection. */
export async function selectModel(
session: Session,
group: string,
material: string,
): Promise<DeepReadonly<SceneModel> | null> {
await session.select(group, material);
return session.getSceneModel();
}
setCameraView(view, { transition }) ​replaced · 2.0: Camera.set, CameraView, CameraMoveOptions.durationMs, CameraMoveOptions.easing · written for 1.x 1.5
session.camera.set({ target, azimuthDeg: azimuth, polarDeg: polar, distance: radius, focalOffset }, { durationMs: transition ? GLIDE_MS : 0, easing: 'damped' }). 1.4's transition: true was a damped glide with no duration; it moved only the target: azimuth, polar and distance jumped at the call (viewer/controllers/camera.controller.ts:362-378 at eb5ce2afe), so a preset button whose views share a target (app-04c2774145's and app-4762caacdc's src/lib/camera/camera-manager.ts:102-113 at c4c6ffd2) cut in 1.4 and orbits in 2.0, with the same end frame, a visual change on the port's sign-off (D44); a port that keeps the cut passes durationMs: 0 when the target is unchanged. GLIDE_MS is round(1000π × smoothTime) from the version-1 controls.smoothTime of the page document the move runs under, set in migration.json per page document (1,571 ms at the default 0.5; section 12, Q20), and 'damped' moves the target as 1.4 did, within 1.38 % of the travel (spike G1). A glide retargeted mid-way stops at its new target, where 1.4's target passed a target just ahead by 8–10 cm and came back, and one called while the camera drifts after a shopper's gesture starts from rest: D44 items for a port that does either (Q20)
Visual changes to sign off (D44): A preset button whose views share a target cut in 1.4 (only the target moved) and orbits in 2.0, with the same end frame; a port that keeps the cut passes durationMs: 0 when the target is unchanged.; A glide retargeted mid-way stops at its new target, where 1.4 passed a target just ahead by 8–10 cm and came back.; A glide called while the camera drifts after a shopper's gesture starts from rest..
Used by: app-04c2774145, app-0c773a4ee3, app-37356bf09f, app-378d190f1b, app-4762caacdc, app-4fd23bc869, app-59c5b5751e, app-822bb9d6aa, app-8675b3225c, app-b362a81c85, app-bcb7ef21da, app-c1384707fc, app-e3b47169bc, app-e61a0b42ad, app-e9a33606d2, app-f07e9033d9, app-f3598fad77, app-f9a32142f1, app-fb16dd1985, app-fc07850558, app-fc306ec3dc, app-fd09090420, configurator-template.
/** `session.setCameraView(view, { transition })`; `glideMs` is the app's `GLIDE_MS`. */
export function setCameraView(
session: Session,
view: V1.CameraView,
transition: boolean,
glideMs: number,
): Promise<Report> {
return session.camera.set(
{
target: view.target,
azimuthDeg: view.azimuth,
polarDeg: view.polar,
distance: view.radius,
focalOffset: view.focalOffset,
},
{ durationMs: transition ? glideMs : 0, easing: 'damped' },
);
}
fitToScene(options) ​replaced · 2.0: Camera.fit, CameraMoveOptions.durationMs, CameraMoveOptions.easing · written for 1.x 1.5
session.camera.fit(undefined, { durationMs: GLIDE_MS, easing: 'damped' }) (Q20); without durationMs the fit becomes a jump. 1.4 also turned the camera to the nearest axis view: camera-controls' fitToBox rounds azimuth and polar to multiples of 90° (camera-controls.module.js:1627-1629, 3.1.2, reached through viewer/controllers/camera.controller.ts:328-331 at eb5ce2afe), while 2.0's fit keeps the angles (Q33). So every caller's framed view changes (the template's fit button and its first view without a defaultView, apps/configurator-template/src/components/viewer/use-viewer-actions.ts:87 and use-viewer.ts:149, and the apps copied from it; app-04c2774145's and app-4762caacdc's use-viewer.ts:714, at 5cdd2780): a visible change for the port's D44 diff. A port that keeps 1.4's framing passes the current azimuth and polar rounded to the nearest 90° as fit's view, one move as in 1.4
Visual changes to sign off (D44): 1.4's fit turned the camera to the nearest axis view; 2.0's fit keeps the angles, so every caller's framed view changes unless the port passes the angles rounded to 90° as fit's view..
Used by: app-04c2774145, app-4762caacdc, app-4fd23bc869, app-510d750508, app-822bb9d6aa, app-8675b3225c, app-b362a81c85, app-bcb7ef21da, app-c1384707fc, app-e3b47169bc, app-e61a0b42ad, app-e9a33606d2, app-f07e9033d9, app-f3598fad77, app-f9a32142f1, app-fb16dd1985, app-fc306ec3dc, app-fd09090420, configurator-template.
/**
* `session.fitToScene()`: one damped fit over the scene. 1.x also turned to the nearest axis
* view; `keepAxisView` keeps that framing by fitting from the angles rounded to 90°.
*/
export function fitToScene(
session: Session,
glideMs: number,
keepAxisView = false,
): Promise<Report> {
const { azimuthDeg, polarDeg } = session.camera.state;
const round90 = (deg: number) => Math.round(deg / 90) * 90;
return session.camera.fit(undefined, {
durationMs: glideMs,
easing: 'damped',
...(keepAxisView
? { view: { azimuthDeg: round90(azimuthDeg), polarDeg: round90(polarDeg) } }
: {}),
});
}
CameraView ​replaced (replaced by 2.0 CameraView) · 2.0: CameraView · written for 1.x 1.5
rename azimuth → azimuthDeg, polar → polarDeg, radius → distance (degrees both sides); a stored IDefaultView maps the same way
Used by: app-04c2774145, app-0c773a4ee3, app-37356bf09f, app-378d190f1b, app-4762caacdc, app-4fd23bc869, app-59c5b5751e, app-822bb9d6aa, app-8675b3225c, app-b362a81c85, app-bcb7ef21da, app-c1384707fc, app-e3b47169bc, app-e61a0b42ad, app-e9a33606d2, app-f07e9033d9, app-f3598fad77, app-f9a32142f1, app-fb16dd1985, app-fc07850558, app-fc306ec3dc, app-fd09090420, configurator-template.
/** 1.x `CameraView` or stored `IDefaultView`: keep angles in degrees and rename fields. */
export function cameraView(view: V1.CameraView | V1.IDefaultView): CameraView {
return {
target: view.target,
azimuthDeg: view.azimuth,
polarDeg: view.polar,
distance: view.radius,
focalOffset: view.focalOffset,
};
}
CameraCommandOptions ​replaced (replaced by CameraMoveOptions) · 2.0: CameraMoveOptions · written for 1.x 1.5
transition: true → { durationMs: GLIDE_MS, easing: 'damped' }, false → { durationMs: 0 }: 1.4's transition was its damped glide (Q20)
Used by: app-04c2774145, app-0c773a4ee3, app-37356bf09f, app-378d190f1b, app-4762caacdc, app-4fd23bc869, app-510d750508, app-59c5b5751e, app-822bb9d6aa, app-8675b3225c, app-b362a81c85, app-bcb7ef21da, app-c1384707fc, app-e3b47169bc, app-e61a0b42ad, app-e9a33606d2, app-f07e9033d9, app-f3598fad77, app-f9a32142f1, app-fb16dd1985, app-fc07850558, app-fc306ec3dc, app-fd09090420, configurator-template.
/** 1.x `CameraCommandOptions.transition`: give the damped move an explicit duration. */
export function cameraCommandOptions(
options: V1.CameraCommandOptions,
glideMs: number,
): CameraMoveOptions {
return { durationMs: options.transition ? glideMs : 0, easing: 'damped' };
}
animations ​kept (kept (namespace)) · written for 1.x 1.5
members below
Used by: app-04c2774145, app-0c773a4ee3, app-37356bf09f, app-4762caacdc, app-4fd23bc869, app-822bb9d6aa, app-8675b3225c, app-b362a81c85, app-bcb7ef21da, app-c1384707fc, app-ceb4eccc72, app-e3b47169bc, app-e61a0b42ad, app-e9a33606d2, app-f07e9033d9, app-f3598fad77, app-f9a32142f1, app-fb16dd1985, app-fc306ec3dc, app-fd09090420, configurator-template.
SessionAnimations.list(), AnimationInfo ​replaced · 2.0: Animations.list, ClipInfo · written for 1.x 1.5
animations.list() → ClipInfo { node, item, clip, durationMs, playing } (seconds become ms; node is null only for a collection item's clip)
Used by: app-04c2774145, app-0c773a4ee3, app-37356bf09f, app-4762caacdc, app-4fd23bc869, app-822bb9d6aa, app-8675b3225c, app-b362a81c85, app-bcb7ef21da, app-c1384707fc, app-e3b47169bc, app-e61a0b42ad, app-e9a33606d2, app-f07e9033d9, app-f3598fad77, app-f9a32142f1, app-fb16dd1985, app-fc07850558, app-fc306ec3dc, app-fd09090420, configurator-template.
/** 1.x `SessionAnimations.list()` and `AnimationInfo`: clips carry milliseconds and their address. */
export function animationClips(session: Session): ClipInfo[] {
return session.animations.list();
}
SessionAnimations.playAll() ​replaced · 2.0: Animations.list, Animations.play · written for 1.x 1.5
each model's first clip, as 1.x's playAll() kept one clip of clips sharing a node (viewer/controllers/animation.controller.ts:323-339 at eb5ce2afe; 5.10): const seen = new Set<string>(); for (const c of session.animations.list()) if (c.node && !seen.has(c.node)) { seen.add(c.node); void session.animations.play(c.node, c.clip, { toggle: true }); }
Used by: app-37356bf09f, app-4fd23bc869, app-822bb9d6aa, app-8675b3225c, app-b362a81c85, app-c1384707fc, app-e3b47169bc, app-e61a0b42ad, app-e9a33606d2, app-f07e9033d9, app-f3598fad77, app-fb16dd1985, app-fc306ec3dc, app-fd09090420, configurator-template.
/** 1.x `SessionAnimations.playAll()`: toggle each model's first clip. */
export function playAll(session: Session): void {
const seen = new Set<string>();
for (const c of session.animations.list()) {
if (c.node && !seen.has(c.node)) {
seen.add(c.node);
void session.animations.play(c.node, c.clip, { toggle: true });
}
}
}
camera ​kept (kept (namespace)) · written for 1.x 1.5
members below
Used by: app-04c2774145, app-0c773a4ee3, app-37356bf09f, app-378d190f1b, app-4762caacdc, app-4f16d2f6da, app-510d750508, app-59c5b5751e, app-b362a81c85, app-b4ae2fce25, app-bcb7ef21da, app-ceb4eccc72, app-ddfb017681, app-e3b47169bc, app-e61a0b42ad, app-f3598fad77, app-f9a32142f1, app-fb16dd1985, app-fc07850558, app-fc306ec3dc, app-fd09090420.
SessionCamera.getPose(), CameraPose ​replaced · 2.0: Camera.state, CameraState · written for 1.x 1.5
session.camera.state (azimuthDeg, polarDeg, distance, target, focalOffset)
Used by: app-04c2774145, app-37356bf09f, app-378d190f1b, app-4762caacdc, app-b362a81c85, app-bcb7ef21da, app-e3b47169bc, app-e61a0b42ad, app-f3598fad77, app-f9a32142f1, app-fb16dd1985, app-fc07850558, app-fd09090420.
/** 1.x `SessionCamera.getPose()`: read camera state. */
export function cameraPose(session: Session): CameraState {
return session.camera.state;
}
SessionCamera.getBounds(options) ​replaced · 2.0: Session.find, Parts.bounds · written for 1.x 1.5
session.find(query).bounds()
Used by: no app in the cross-check.
/** 1.x `SessionCamera.getBounds(options)`: measure matching product parts. */
export function cameraBounds(session: Session, options: V1.BoundsOptions = {}): Bounds | null {
const query: Query = options.nodeIds
? options.nodeIds.map((node) => ({ node }))
: { role: 'product' };
return session.find(query).bounds();
}
SessionCamera.fitTo(options), FitOptions ​replaced · 2.0: Camera.fit, CameraMoveOptions.durationMs, CameraMoveOptions.easing · written for 1.x 1.5
session.camera.fit(nodeIds.map((node) => ({ node })), { paddingM, durationMs: transition ? GLIDE_MS : 0, easing: 'damped' }) (Q20). 1.4 turned to the nearest axis view here too (viewer/configurator-viewer.ts:1600 at eb5ce2afe); see the fitToScene row, and its margin note: an omitted paddingM keeps a 4% margin (5.6)
Used by: no app in the cross-check.
/** 1.x `SessionCamera.fitTo(options)`: fit the models, optionally retaining the nearest axis view. */
export function fitModels(
session: Session,
options: V1.FitOptions,
glideMs: number,
keepAxisView = false,
): Promise<Report> {
const { azimuthDeg, polarDeg } = session.camera.state;
const round90 = (deg: number): number => Math.round(deg / 90) * 90;
return session.camera.fit(
options.nodeIds?.map((node) => ({ node })),
{
paddingM: options.padding,
durationMs: options.transition ? glideMs : 0,
easing: 'damped',
...(keepAxisView
? { view: { azimuthDeg: round90(azimuthDeg), polarDeg: round90(polarDeg) } }
: {}),
},
);
}
history ​kept (kept (namespace)) · written for 1.x 1.5
members below
Used by: app-04c2774145, app-37356bf09f, app-4762caacdc, app-4f16d2f6da, app-510d750508, app-59c5b5751e, app-b4ae2fce25, app-bcb7ef21da, app-ceb4eccc72, app-ddfb017681, app-e3b47169bc, app-e61a0b42ad, app-f07e9033d9, app-f3598fad77, app-f9a32142f1, app-fc07850558, app-fc306ec3dc, app-fd09090420, configurator-template.
SessionHistory.transaction(options), HistoryTransaction, TransactionOptions ​replaced · 2.0: History.group, HistoryGroup, Session.transaction · written for 1.x 1.5
used to group undo steps (a drag, a turn; app-37356bf09f's drag gesture, src/runtime/drag-commit.ts:73 at c4c6ffd2): history.transaction({ label }) → session.history.group({ label }); commit() and open unchanged; no 10 s auto-commit: the group default is 60 s. Used to keep work out of history, as app-04c2774145 does with its assembly snaps: do that work inside the sync's session.transaction(work, { undo }), so the snaps are part of that one step, never a step of their own
Used by: app-04c2774145, app-37356bf09f, app-4762caacdc, app-510d750508, app-bcb7ef21da, app-f3598fad77.
/** 1.x `SessionHistory.transaction({ label })`: use an explicit undo group and commit its handle. */
export function historyGroup(session: Session, options: V1.TransactionOptions = {}): HistoryGroup {
return session.history.group({ label: options.label });
}
/** Keep a sync and its assembly snaps in one step instead of suppressing snap history. */
export function syncWithSnaps(
session: Session,
model: SceneModel,
nodeIds: readonly string[],
undo: string,
): Promise<TransactionReport<void>> {
return session.transaction(
async (tx) => {
await tx.setSceneModel(model);
for (const node of nodeIds) await tx.assembly.attach(node);
},
{ undo },
);
}
screenshot(options), CaptureOptions ​replaced · 2.0: Session.capture, CaptureResult, Session.idle · written for 1.x 1.5
(await session.capture({ size: { width, height }, format, quality })).image. 1.x waited, at most 3 s, for the material applies in flight, a fresh pick included; for the configuration just made, first await session.idle({ signal: AbortSignal.timeout(5000) }), its AbortError caught (5.11, 6.8 rule 2)
Used by: app-04c2774145, app-4762caacdc, app-4f16d2f6da, app-4fd23bc869, app-510d750508, app-822bb9d6aa, app-8675b3225c, app-b362a81c85, app-b4ae2fce25, app-bcb7ef21da, app-c1384707fc, app-ceb4eccc72, app-ddfb017681, app-e3b47169bc, app-e61a0b42ad, app-e9a33606d2, app-f07e9033d9, app-f3598fad77, app-f9a32142f1, app-fb16dd1985, app-fc07850558, app-fc306ec3dc, app-fd09090420, configurator-template.
/** 1.x `screenshot(options)`: wait for the current configuration, then return the capture image. */
export async function screenshot(session: Session, options: V1.CaptureOptions = {}): Promise<Blob> {
try {
await session.idle({ signal: AbortSignal.timeout(5000) });
} catch (error) {
if (!(error instanceof DOMException && error.name === 'AbortError')) throw error;
}
const { width, height, format, quality } = options;
return (
await session.capture({
...(width !== undefined && height !== undefined ? { size: { width, height } } : {}),
format,
quality,
})
).image;
}
activateAR(options), ActivateAROptions, ActivateARReport ​kept (kept (changed)) · written for 1.x 1.5
unions gain 'other'; usdzUrl (the file Quick Look opens; a glbUrl never reaches it) and title added; scale and occlusion added as experimental; a WebXR session draws its own product-only scene instead of the live one (5.11); reason gains 'cancelled', and the call routes by @holo/xr's route order (5.11, D64); plainGlbUrl removed: glbUrl is never a .holo, so the plain twin goes in glbUrl, and no app passes plainGlbUrl at c4c6ffd2; on Android an account that opts in (ar.place.android: 'holostep') opens Holostep's in-page WebXR presenter over a copy of the committed product, and 'webxr' joins info().ar.modes once the route can open it (5.11, D64)
Used by: app-04c2774145, app-37356bf09f, app-4762caacdc, app-4fd23bc869, app-822bb9d6aa, app-8675b3225c, app-b362a81c85, app-bcb7ef21da, app-c1384707fc, app-e3b47169bc, app-e61a0b42ad, app-e9a33606d2, app-f07e9033d9, app-f3598fad77, app-f9a32142f1, app-fb16dd1985, app-fc306ec3dc, app-fd09090420, configurator-template.
undo(), UndoReport ​kept (kept (changed)) · written for 1.x 1.5
resolves a Report; restored removed: restored: false → outcome: 'refused', reason: 'empty'
Used by: app-04c2774145, app-37356bf09f, app-4762caacdc, app-4fd23bc869, app-822bb9d6aa, app-8675b3225c, app-b362a81c85, app-bcb7ef21da, app-c1384707fc, app-e3b47169bc, app-e61a0b42ad, app-e9a33606d2, app-f07e9033d9, app-f3598fad77, app-f9a32142f1, app-fb16dd1985, app-fc07850558, app-fc306ec3dc, app-fd09090420, configurator-template.
checkpoint(state) ​replaced · 2.0: CheckpointOption.checkpoint, SessionState.checkpoint · written for 1.x 1.5
pass the page state with the change it belongs to: session.setSceneModel(model, { checkpoint: state }) (any recorded write takes it), or session.transaction(work, { checkpoint: state }); read it back from session.state().checkpoint or the 'committed' feed
Used by: app-37356bf09f, app-4fd23bc869, app-822bb9d6aa, app-8675b3225c, app-bcb7ef21da, app-c1384707fc, app-e3b47169bc, app-e61a0b42ad, app-e9a33606d2, app-f07e9033d9, app-f3598fad77, app-fb16dd1985, app-fd09090420, configurator-template.
/** 1.x `checkpoint(state)`: record page state alongside the scene change and read it back. */
export async function checkpoint(
session: Session,
model: SceneModel,
state: V1.JsonValue,
): Promise<JsonValue | null> {
await session.setSceneModel(model, { checkpoint: state });
return session.state().checkpoint;
}
onEvent(event, handler) ​replaced · 2.0: Session.on · written for 1.x 1.5
session.on(event, handler, { signal })
Used by: app-04c2774145, app-0c773a4ee3, app-37356bf09f, app-378d190f1b, app-4762caacdc, app-4fd23bc869, app-822bb9d6aa, app-8675b3225c, app-b362a81c85, app-bcb7ef21da, app-c1384707fc, app-e3b47169bc, app-e61a0b42ad, app-e9a33606d2, app-f07e9033d9, app-f3598fad77, app-f9a32142f1, app-fb16dd1985, app-fc306ec3dc, app-fd09090420, configurator-template.
/** 1.x `onEvent(event, handler)`: subscribe to the typed event map with an abort signal. */
export function onEvent<K extends keyof Events>(
session: Session,
event: K,
handler: (payload: Events[K]) => void,
signal: AbortSignal,
): () => void {
return session.on(event, handler, { signal });
}
info() ​kept (kept (changed)) · written for 1.x 1.5
see InfoReport
Used by: app-b362a81c85, app-f9a32142f1, app-fc07850558, app-fd09090420.
supports(capability) ​kept · written for 1.x 1.5
—
Used by: app-bcb7ef21da, app-e3b47169bc, app-e61a0b42ad, app-f07e9033d9, app-fb16dd1985, app-fd09090420, configurator-template.
interaction ​replaced · 2.0: Session.input, Input · written for 1.x 1.5
session.input
Used by: app-04c2774145, app-37356bf09f, app-4762caacdc, app-e61a0b42ad, app-fc306ec3dc.
/** 1.x `interaction`: use the input member. */
export function interaction(session: Session): Input {
return session.input;
}
SessionInteraction.pick(at, options), PickOptions ​replaced · 2.0: Input.pick, Hit · written for 1.x 1.5
session.input.pick(at, { query: nodeIds.map((node) => ({ node })) }) returns a Hit
Used by: app-b4ae2fce25, app-ddfb017681, app-e61a0b42ad, app-fc07850558, app-fc306ec3dc.
/** 1.x `SessionInteraction.pick(at, options)`: pick a hit within the queried models. */
export function pick(
session: Session,
at: V1.ScreenPoint,
nodeIds: readonly string[],
): Promise<Hit | null> {
return session.input.pick(at, { query: nodeIds.map((node) => ({ node })) });
}
SessionInteraction.pickGround(at, { planeY }), PickGroundOptions ​replaced · 2.0: Camera.ray, ray · written for 1.x 1.5
ray.intersectPlane(session.camera.ray(at), { normal: { x: 0, y: 1, z: 0 }, constant: -planeY }) from /math (synchronous); app-37356bf09f's inventory drop (src/runtime/panel-drop.ts:11 at c4c6ffd2)
Used by: app-37356bf09f.
/** 1.x `SessionInteraction.pickGround(at, { planeY })`: intersect the camera ray synchronously. */
export function pickGround(
session: Session,
at: V1.ScreenPoint,
options: V1.PickGroundOptions = {},
): Point3 | null {
const planeY = options.planeY ?? 0;
return ray.intersectPlane(session.camera.ray(at), {
normal: { x: 0, y: 1, z: 0 },
constant: -planeY,
});
}
SessionInteraction.snapTo(target, { near }), SnapOptions, SnapReport ​replaced · 2.0: Assembly.attach, AttachReport · written for 1.x 1.5
session.assembly.attach(target.nodeId, { near }) (6.12); snapped → attached; pose → session.node(target.nodeId).worldPose; 'no-planes' → 'no-candidate'; 'rejected' → 'rejected'; 'plugin-missing' → none (always present). An app that only reads the snapped pose for its own placement math (app-37356bf09f) takes it from attach(…, { dryRun: true })'s patch, so no connection is recorded (K63)
Used by: app-04c2774145, app-37356bf09f, app-4762caacdc, app-ceb4eccc72, app-f9a32142f1.
/** 1.x `SessionInteraction.snapTo(target, { near })`: read attachment status and the world pose. */
export async function snapTo(
session: Session,
target: V1.NodeTarget,
options: V1.SnapOptions = {},
): Promise<{ report: AttachReport; pose: Pose }> {
const report = await session.assembly.attach(target.nodeId, { near: options.near });
return { report, pose: session.node(target.nodeId).worldPose };
}
/** Placement-only math reads a dry run's patch without recording a connection. */
export async function snapPatch(
session: Session,
target: V1.NodeTarget,
options: V1.SnapOptions = {},
): Promise<readonly JsonPatchOp[]> {
const report = await session.assembly.attach(target.nodeId, { near: options.near, dryRun: true });
return report.outcome === 'dry-run' ? report.patch : [];
}
SessionInteraction.setDraggable(nodeIds, options), DraggableOptions ​replaced · 2.0: Input.drag · written for 1.x 1.5
const stop = session.input.drag(nodeIds.map((node) => ({ node })), { on: 'floor' }); setDraggable([]) → stop()
Used by: app-04c2774145, app-37356bf09f, app-4762caacdc, app-fc306ec3dc.
/** 1.x `SessionInteraction.setDraggable(nodeIds)` and `setDraggable([])`: retain and call the disposer. */
export function setDraggable(session: Session, nodeIds: readonly string[]): () => void {
const stop = session.input.drag(
nodeIds.map((node) => ({ node })),
{ on: 'floor' },
);
return () => stop();
}
overlays ​kept (kept (namespace)) · written for 1.x 1.5
members below
Used by: app-37356bf09f, app-fd09090420.
SessionOverlays.anchors.set/clear, AnchorOptions, AnchorPosition ​kept (kept (changed)) · written for 1.x 1.5
AnchorOptions.margin → marginPx, CSS px as before (the unit in the name, 3.3)
Used by: app-04c2774145, app-37356bf09f, app-378d190f1b, app-4762caacdc, app-f9a32142f1.
AnchorSpec ​replaced · 2.0: AnchorSpec · written for 1.x 1.5
{ id, position, nodeId } → { id, at: position, on: { node: nodeId } }
Used by: app-04c2774145, app-37356bf09f, app-378d190f1b, app-4762caacdc, app-f9a32142f1.
/** 1.x `AnchorSpec { id, position, nodeId }`: rename the point and optional node query. */
export function anchorSpec(anchor: V1.AnchorSpec): AnchorSpec {
return {
id: anchor.id,
at: anchor.position,
...(anchor.nodeId !== undefined ? { on: { node: anchor.nodeId } } : {}),
};
}
SessionOverlays.project(points) ​replaced · 2.0: Camera.project · written for 1.x 1.5
session.camera.project(points) (synchronous)
Note: For a capture with its own view or size (a quote still taken off-display, app-fd09090420's corner check), camera.project can't check the framing before the shot: it projects only through camera.state. /math projection.project({ view, size }, points) projects through that view at that size with the lens the capture draws with (the default lens when the page set none, else pass its fovDeg or horizontalFovDeg), so a loop that backs off until every corner is inside the frame runs before capture({ view, size }) and never moves the live camera.
Used by: app-37356bf09f, app-4f16d2f6da, app-b362a81c85, app-b4ae2fce25, app-bcb7ef21da, app-ddfb017681, app-fc07850558, app-fc306ec3dc, app-fd09090420.
/** 1.x `SessionOverlays.project(points)`: project synchronously through the camera. */
export function project(
session: Session,
points: readonly V1.Point3[],
): Array<ProjectedPoint | null> {
return session.camera.project(points);
}
SessionOverlays.hotspots.set(badges, options), HotspotOptions ​kept (kept (changed)) · written for 1.x 1.5
set(badges): HotspotOptions.alwaysVisible is removed; no app passed it, and an always-shown marker is the app's own DOM on overlays.anchors, as app-4762caacdc and app-04c2774145 draw theirs
Used by: app-37356bf09f, app-f3598fad77, app-fc07850558, app-fd09090420.
HotspotBadge ​replaced · 2.0: HotspotBadge · written for 1.x 1.5
{ id, position, nodeId, markerClass, group } → { id, at: position, label, stylePart: markerClass }; nodeId is dropped, since no app passes it (app-fd09090420 and app-f3598fad77 pass { id, position, markerClass }); group is dropped, since badges never cluster in 2.0 and no app passes it at c4c6ffd2; label is required; markerClass was a CSS class, and stylePart is a part name styled with ::part(<stylePart>), so the rename makes a kept class name fail to compile
Used by: app-37356bf09f, app-f3598fad77, app-fc07850558, app-fd09090420.
/** 1.x `HotspotBadge`: provide an accessible label and style its part with `::part(...)`. */
export function hotspotBadge(badge: V1.HotspotBadge, label: string): HotspotBadge {
return { id: badge.id, at: badge.position, label, stylePart: badge.markerClass };
}
HotspotReference ​replaced · 2.0: HotspotReference · written for 1.x 1.5
nodeId is dropped: a badge sits at a world point
Used by: no app in the cross-check.
/** 1.x `HotspotReference`: identify the world-space badge by id alone. */
export function hotspotReference(hotspot: V1.HotspotReference): HotspotReference {
return { id: hotspot.id };
}
idle() ​kept · written for 1.x 1.5
throws HoloScopeError inside work
Used by: app-bcb7ef21da, app-e61a0b42ad, app-f3598fad77, app-fb16dd1985, app-fd09090420, configurator-template.
destroy() ​kept · written for 1.x 1.5
rolls back an open transaction in place
Used by: app-04c2774145, app-0c773a4ee3, app-37356bf09f, app-378d190f1b, app-4762caacdc, app-4f16d2f6da, app-4fd23bc869, app-510d750508, app-59c5b5751e, app-822bb9d6aa, app-8675b3225c, app-b362a81c85, app-b4ae2fce25, app-bcb7ef21da, app-c1384707fc, app-ceb4eccc72, app-ddfb017681, app-e3b47169bc, app-e61a0b42ad, app-e9a33606d2, app-f07e9033d9, app-f3598fad77, app-f9a32142f1, app-fb16dd1985, app-fc07850558, app-fc306ec3dc, app-fd09090420, configurator-template.
canTryOn(), TryOnAvailability, TryOnKind, TryOnUnavailable (1.6) ​kept (kept (experimental)) · written for 1.x 1.6
a 2.0 experimental member (@stability labs @experimental), reached through has(session, 'canTryOn'), which answers true on a 2.0 runtime; available, kind and reason keep their 1.7 meaning (kind includes 'finger', which answers reason: 'unsupported' until its tracker ships, and 'not-enabled' also covers an account that has not set its consent mode, which every kind needs; WD35, ae5c5d0ede), modes adds 'handoff' where the hand-off can open (F1b: ['here', 'handoff'] when available, ['handoff'] with reason: 'unsupported' where only this device can't; 1.7 lists ['here'] or nothing), and reason: 'not-enabled' is the enablement (no permits('tryOn')); re-read it on 'committed', where 1.6 re-reads it on 'scene:settled' (delivery-run 2026-10-20; DECIDED: XR alignment reply §3, Erik)
Used by: no app in the cross-check.
tryOn(options), TryOnOptions, TryOnReport, TryOnMode (1.6) ​kept (kept (experimental)) · written for 1.x 1.6
a 2.0 experimental member, reached through has(session, 'tryOn'), true on a 2.0 runtime; it resolves { launched, mode, reason?, durationMs, handoffUrl?, problems } (launched, not opened), an aborted signal rejects AbortError, and mode: 'handoff' is used only after supports('tryOn.mode.handoff') (true on a 2.0 runtime since F1b: Holostep's QR hand-off screen, launched: true once it showed; 1.x answers false and resolves 'unsupported'); its option root is overlayRoot, as activateAR's (delivery-run 2026-10-20; DECIDED: XR alignment reply §3, Erik; R3-API-9)
Used by: no app in the cross-check.
endAR() (1.6) ​kept (kept (experimental)) · written for 1.x 1.6
a 2.0 experimental member, reached through has(session, 'endAR'), true on a 2.0 runtime; it closes Holostep's in-page AR or try-on from page code, as in 1.6 (delivery-run 2026-10-20; DECIDED: XR alignment reply §3, Erik)
Used by: no app in the cross-check.
ActivateAROptions.overlayRoot, .ui (1.6) ​kept (kept (experimental)) · written for 1.x 1.6
activateAR's experimental options: overlayRoot used only after supports('activateAR.overlayRoot'), ui: 'headless' only after supports('activateAR.ui.headless'). overlayRoot answers true since 2.0 takes production's in-page AR route (DL28, cycle 15): the tenant's UI over the camera on both in-page routes, with the inset properties --holo-ar-inset-top and --holo-ar-inset-bottom as production's AR v3 sets them; ui.headless answers true since production's AR v3 (!1099, DL51): the in-page camera route runs without Holostep's screens and reports through 'ar:status''s place, the other routes open the native viewer (delivery-run 2026-10-20; DECIDED: XR alignment reply §3, Erik; cycle 19, DL51)
Used by: no app in the cross-check.
TenantEventMap → Events) ​TenantEventMap ​replaced (replaced by Events) · 2.0: Events, Session.on · written for 1.x 1.5
TenantEventMap → Events; each event has its own row below
Used by: app-04c2774145, app-0c773a4ee3, app-37356bf09f, app-378d190f1b, app-4762caacdc, app-4fd23bc869, app-822bb9d6aa, app-8675b3225c, app-b362a81c85, app-bcb7ef21da, app-c1384707fc, app-e3b47169bc, app-e61a0b42ad, app-e9a33606d2, app-f07e9033d9, app-f3598fad77, app-f9a32142f1, app-fb16dd1985, app-fc306ec3dc, app-fd09090420, configurator-template.
/** 1.x `TenantEventMap` → 2.0 `Events`: the same subscription, typed by the 2.0 map. */
export function subscribe<K extends keyof Events>(
session: Session,
event: K,
handler: (payload: Events[K]) => void,
signal: AbortSignal,
): () => void {
return session.on(event, handler, { signal });
}
'model:load-start', 'model:loaded', 'model:updated', 'model:removed' ​replaced · 2.0: ReportFields.problems, Problem, Session.getSceneModel, Session.node, Part.exists · written for 1.x 1.5
await the write's Report; per-load passes become rules or beforeCommit; a 'models missing' notice reads the asset-failed problems of the latest setSceneModel and of every select, undo and redo after it (a sync names every node, and an equal sync still lists the missing ones, O15) and keeps the nodes the scene model still holds that are not loaded (session.node(id).exists false), since an undo or redo that drops a failed node reports nothing about it and a later write may load one, or checks session.node(id).exists for the scene model's ModelNodes after each 'committed', instead of pairing 'model:failed' with a late 'model:loaded' (app-f9a32142f1 use-viewer.ts:271-296 at 342b26ce)
Used by: app-fc306ec3dc.
/**
* `'model:load-start'`, `'model:loaded'`, `'model:updated'`, `'model:removed'`: await the write's
* Report. A "models missing" notice reads the 'asset-failed' problems of the latest
* `setSceneModel` and of every `select`, `undo` and `redo` after it, and keeps the ModelNodes the
* scene model still holds that are not loaded: an undo that drops a failed node reports nothing
* about it, and a later write may load one.
*/
export function missingModels(
session: Session,
latestSync: Report,
since: readonly Report[],
): string[] {
const failed = new Set<string>();
for (const report of [latestSync, ...since]) {
for (const problem of report.problems) {
if (problem.code === 'asset-failed' && problem.asset === 'model' && problem.node) {
failed.add(problem.node);
}
}
}
const held = new Set<string>();
const walk = (nodes: readonly DeepReadonly<ConfiguratorNode>[] | undefined): void => {
for (const node of nodes ?? []) {
if (node.type === 'ModelNode') held.add(node.nodeId);
walk(node.children);
}
};
walk(session.getSceneModel()?.root);
return [...failed].filter((node) => held.has(node) && !session.node(node).exists);
}
'model:failed', 'material:failed' ​replaced · 2.0: ReportFields.problems, Events.problem, Problem · written for 1.x 1.5
Report.problems and 'problem' with 'asset-failed'
Used by: app-f9a32142f1, app-fc306ec3dc.
/** `'model:failed'`, `'material:failed'`: `Report.problems`, and `'problem'` with 'asset-failed'. */
export function onAssetFailed(
session: Session,
handler: (problem: Problem) => void,
signal: AbortSignal,
): () => void {
return session.on(
'problem',
({ problem }) => {
if (problem.code === 'asset-failed') handler(problem);
},
{ signal },
);
}
'selection:changed' ​removed · written for 1.x 1.5
the viewer's object selection is not part of 2.0; the app owns it, with 1.x's rules (viewer/controllers/interaction.controller.ts at eb5ce2afe): on a 'click' whose hit is non-null, select hit.part.node; a click on empty space keeps the selection (:528), so a port that clears it there records that as its own UX change; Ctrl toggles membership (:514-522), read from the app's own pointer listener on el, since 'click' carries no modifier keys. No 'click' follows a drag that moved (5.7), yet 1.x selected the dragged model at a mouse or pen release (:491-501; the tenant 'click' is travel-filtered at rt/session.ts:446): select the dragged unit's part.node on 'drag:start', as app-37356bf09f's paper port does, or on a 'drag:end' with moved and reason 'released'. On 'scene-model:changed', drop a selected node the model no longer holds, as forgetSubtree did (:262-293). app-37356bf09f (apps/app-37356bf09f/src/hooks/use-app-37356bf09f-assembly.ts:23-29) and app-f9a32142f1 (apps/app-f9a32142f1/src/components/viewer/use-viewer.ts:787-789) port this way
Used by: app-37356bf09f, app-f9a32142f1.
'hover:changed' ​replaced · 2.0: Events.hover, Hit · written for 1.x 1.5
'hover' { at, hit }; mouse and pen only, and the runtime picks for it only while a listener exists
Used by: app-fd09090420.
/** `'hover:changed'` `{ object }` → `'hover'` `{ at, hit }` (mouse and pen only). */
export function onHover(
session: Session,
handler: (object: V1.NodeReference | null) => void,
signal: AbortSignal,
): () => void {
return session.on(
'hover',
({ hit }) =>
handler(hit && hit.part.node ? { nodeId: hit.part.node, name: hit.part.name } : null),
{ signal },
);
}
'drag:start' ​kept (kept (payload changed)) · written for 1.x 1.5
{ node, position } → { part, pose, pointerId }
Used by: app-37356bf09f, app-fc306ec3dc.
'drag:end' ​kept (kept (payload changed)) · written for 1.x 1.5
{ node, position, rotationY } → { part, pose, moved, reason, cause, op }; snapping moves into constrain or an attach after the event; a refused drop (app-04c2774145's pouf) is commit: 'app' plus input.revert, as the Input.drag example shows
Used by: app-04c2774145, app-37356bf09f, app-4762caacdc, app-f9a32142f1, app-fc306ec3dc.
'anchors:moved' ​kept · written for 1.x 1.5
—
Used by: app-04c2774145, app-37356bf09f, app-378d190f1b, app-4762caacdc.
'hotspot:click', 'hotspot:hover' ​kept · written for 1.x 1.5
hotspot.nodeId is dropped; 'hotspot:hover' also reports the badge with keyboard focus when the pointer is on none, where 1.4 fired only on mouseenter and mouseleave (viewer/plugins/hotspot/hotspot-plugin.ts:621-627 at eb5ce2afe; WCAG 1.4.13)
Used by: app-04c2774145, app-0c773a4ee3, app-4762caacdc, app-4f16d2f6da, app-4fd23bc869, app-510d750508, app-822bb9d6aa, app-8675b3225c, app-b362a81c85, app-b4ae2fce25, app-bcb7ef21da, app-c1384707fc, app-ceb4eccc72, app-ddfb017681, app-e3b47169bc, app-e61a0b42ad, app-e9a33606d2, app-f07e9033d9, app-f3598fad77, app-f9a32142f1, app-fb16dd1985, app-fc07850558, app-fc306ec3dc, app-fd09090420, configurator-template.
'scene:settled' ​replaced · 2.0: Session.idle, Events.committed · written for 1.x 1.5
await session.idle(), or 'committed'
Used by: app-e61a0b42ad, app-f3598fad77, app-fb16dd1985.
/** `'scene:settled'` → `await session.idle()` (or the `'committed'` feed). */
export async function settled(session: Session, signal: AbortSignal): Promise<void> {
await session.idle({ signal });
}
'click' ​kept (kept (payload changed)) · written for 1.x 1.5
{ object } → { at, hit }; object.nodeId → hit?.part.node; one per touch tap
Used by: app-04c2774145, app-0c773a4ee3, app-4762caacdc, app-4f16d2f6da, app-4fd23bc869, app-510d750508, app-822bb9d6aa, app-8675b3225c, app-b362a81c85, app-b4ae2fce25, app-bcb7ef21da, app-c1384707fc, app-ceb4eccc72, app-ddfb017681, app-e3b47169bc, app-e61a0b42ad, app-e9a33606d2, app-f07e9033d9, app-f3598fad77, app-f9a32142f1, app-fb16dd1985, app-fc07850558, app-fc306ec3dc, app-fd09090420, configurator-template.
'camera:control-start', 'camera:control-end' ​kept · written for 1.x 1.5
pointer and touch gestures only, as 1.x: a wheel or touchpad zoom fires neither
Used by: no app in the cross-check.
'camera:rest' ​kept (kept (payload added)) · written for 1.x 1.5
{ state }
Used by: app-b362a81c85, app-fb16dd1985, app-fc306ec3dc.
'camera:changed' ​kept (kept (payload changed)) · written for 1.x 1.5
{ pose } → { state }
Used by: app-e61a0b42ad.
'resize' ​kept · written for 1.x 1.5
—
Used by: no app in the cross-check.
'animation:started', 'animation:stopped', 'animation:finished' ​kept (kept (payload changed)) · written for 1.x 1.5
{ name, nodeId } → { clip, node, item } (item for a collection item's clip, experimental)
Used by: app-04c2774145, app-37356bf09f, app-4762caacdc, app-4fd23bc869, app-822bb9d6aa, app-8675b3225c, app-b362a81c85, app-bcb7ef21da, app-c1384707fc, app-e3b47169bc, app-e61a0b42ad, app-e9a33606d2, app-f07e9033d9, app-f3598fad77, app-f9a32142f1, app-fb16dd1985, app-fc306ec3dc, app-fd09090420, configurator-template.
'ar:status' ​kept · written for 1.x 1.5
status gains a closed union plus 'other'
Used by: app-fc07850558.
'material:changed' ​replaced · 2.0: Events.material:selected, Cause, Session.select, CommitOptions.origin · written for 1.x 1.5
'material:selected' { cause, group, material }; unlike 1.4's event, which fired for select only, it also fires when undo, redo or a sync changes the selection. cause.kind tells undo and redo; a sync and an autocommit select are both 'write', so an app that needs 1.4's event passes its own origin on select and reads cause.origin. cause.actor says who, not which call: an app's select on a shopper's click is 'app' unless the app passes actor
Used by: app-b362a81c85, app-f3598fad77, app-fb16dd1985.
/**
* `'material:changed'` → `'material:selected'` `{ cause, group, material }`. 1.4 fired it for a
* `select` only; 2.0 also fires it when undo, redo or a sync changes the selection. The handler
* gets the `cause`: `cause.kind` tells undo and redo, and `cause.origin` the app's own selects,
* which `selectMaterial` tags. A sync is a `'write'` too, and `cause.actor` says who, not which
* call.
*/
export const SELECT_ORIGIN = 'material-select';
export function onMaterialSelected(
session: Session,
handler: (group: string, material: string, cause: Cause) => void,
signal: AbortSignal,
): () => void {
return session.on(
'material:selected',
({ cause, group, material }) => handler(group, material, cause),
{ signal },
);
}
/** 1.4's `select(group, material)`, tagged so the handler tells it by `cause.origin`. */
export function selectMaterial(session: Session, group: string, material: string): Promise<Report> {
return session.select(group, material, { origin: SELECT_ORIGIN });
}
'history:changed' ​kept (kept (payload added)) · written for 1.x 1.5
adds cause, undoLabel, redoLabel
Used by: app-04c2774145, app-37356bf09f, app-4762caacdc, app-4fd23bc869, app-822bb9d6aa, app-8675b3225c, app-b362a81c85, app-bcb7ef21da, app-c1384707fc, app-e3b47169bc, app-e61a0b42ad, app-e9a33606d2, app-f07e9033d9, app-f3598fad77, app-f9a32142f1, app-fb16dd1985, app-fc306ec3dc, app-fd09090420, configurator-template.
'history:restored' ​replaced · 2.0: Events.committed · written for 1.x 1.5
'committed' where cause.kind is 'undo' or 'redo'; its checkpoint is the restored page state
Used by: app-04c2774145, app-37356bf09f, app-4762caacdc, app-4fd23bc869, app-822bb9d6aa, app-8675b3225c, app-b362a81c85, app-bcb7ef21da, app-c1384707fc, app-e3b47169bc, app-e61a0b42ad, app-e9a33606d2, app-f07e9033d9, app-f3598fad77, app-f9a32142f1, app-fb16dd1985, app-fc306ec3dc, app-fd09090420, configurator-template.
/** `'history:restored'` → `'committed'` where `cause.kind` is 'undo' or 'redo'. */
export function onRestored(
session: Session,
restore: (checkpoint: JsonValue | null) => void,
signal: AbortSignal,
): () => void {
return session.on(
'committed',
({ cause, checkpoint }) => {
if (cause.kind === 'undo' || cause.kind === 'redo') restore(checkpoint);
},
{ signal },
);
}
'scene-model:changed' ​kept (kept (payload added)) · written for 1.x 1.5
adds cause; model is never null
Used by: app-4fd23bc869, app-822bb9d6aa, app-8675b3225c, app-b362a81c85, app-bcb7ef21da, app-c1384707fc, app-e3b47169bc, app-e61a0b42ad, app-e9a33606d2, app-f07e9033d9, app-f3598fad77, app-fb16dd1985, app-fd09090420, configurator-template.
'scene:synced' ​replaced · 2.0: Events.scene-model:changed · written for 1.x 1.5
'scene-model:changed', which fires for each commit that changed the scene model, with cause and the model. 1.4 fired 'scene:synced' after every scene sync (viewer/configurator-viewer.ts:2174 at eb5ce2afe); an equal sync now fires nothing (O15). 'committed' filtered on recorded is the wider choice, since it also counts commits that change only the checkpoint or the connections; unfiltered, it fires for look-only commits too, a rule or a camera move
Used by: app-04c2774145, app-0c773a4ee3, app-37356bf09f, app-4762caacdc, app-4fd23bc869, app-822bb9d6aa, app-8675b3225c, app-b362a81c85, app-bcb7ef21da, app-c1384707fc, app-e3b47169bc, app-e61a0b42ad, app-e9a33606d2, app-f07e9033d9, app-f3598fad77, app-f9a32142f1, app-fb16dd1985, app-fc306ec3dc, app-fd09090420, configurator-template.
/**
* `'scene:synced'` → `'scene-model:changed'`, once per commit that changed the scene model; an
* equal sync fires nothing. `'committed'` filtered on `recorded` is the wider choice.
*/
export function onSceneModelChanged(
session: Session,
handler: (model: DeepReadonly<SceneModel>) => void,
signal: AbortSignal,
): () => void {
return session.on('scene-model:changed', ({ model }) => handler(model), { signal });
}
'document:applied', 'problem' ​kept · written for 1.x 1.5
—
Used by: app-e61a0b42ad, app-fc07850558.
'xr:start', 'xr:end', 'xr:cta' (1.6) ​kept (kept (experimental)) · written for 1.x 1.6
2.0's experimental XR events, typed in Events: 'xr:start' { presenter, experience }, 'xr:end' { reason, durationMs } and 'xr:cta' { action, configuration } ('xr:state' and 'xr:variant' are not in 2.0.0: they wait for XR's P3); v1's unversioned 'xr:session-start' and 'xr:session-end' (@holo/xr's) map to 'xr:start' and 'xr:end', so one handler fits both lines, except that 2.0 names presenter as ActivateARReport.mode does: 1.x's 'holostep-webxr', and 'other' with experience: 'place' (the iOS camera route), are 'webxr', and 'appclip' and 'none' are 'other' (delivery-run 2026-10-20; DECIDED: XR alignment reply §3, Erik; R3-API-9)
Used by: no app in the cross-check.
on(event, handler) (the unversioned stream with engine objects) ​replaced · 2.0: Session.on · written for 1.x 1.5
the typed on
Used by: no app in the cross-check.
/** The unversioned native `on(event, handler)` → the typed `session.on`. */
export function onClick(
session: Session,
handler: (nodeId: string | null) => void,
signal: AbortSignal,
): () => void {
return session.on('click', ({ hit }) => handler(hit?.part.node ?? null), { signal });
}
requestFrame() ​removed · written for 1.x 1.5
writes autocommit and draw; onFrame(cb, { continuous: true }) keeps drawing
Used by: app-04c2774145, app-0c773a4ee3, app-4762caacdc, app-b362a81c85, app-bcb7ef21da, app-c1384707fc, app-e61a0b42ad, app-f07e9033d9, app-f3598fad77, app-fb16dd1985, app-fd09090420.
scene ​removed · written for 1.x 1.5
find(), Part, rules and collections
Used by: app-04c2774145, app-0c773a4ee3, app-37356bf09f, app-378d190f1b, app-4762caacdc, app-4f16d2f6da, app-4fd23bc869, app-510d750508, app-59c5b5751e, app-822bb9d6aa, app-8675b3225c, app-b362a81c85, app-b4ae2fce25, app-bcb7ef21da, app-c1384707fc, app-ceb4eccc72, app-ddfb017681, app-e3b47169bc, app-e61a0b42ad, app-e9a33606d2, app-f07e9033d9, app-f3598fad77, app-f9a32142f1, app-fb16dd1985, app-fc07850558, app-fc306ec3dc, app-fd09090420, configurator-template.
sceneModel ​replaced · 2.0: Session.getSceneModel · written for 1.x 1.5
session.getSceneModel()
Used by: app-04c2774145, app-4762caacdc, app-f9a32142f1.
/** `session.sceneModel` → `session.getSceneModel()`. */
export function sceneModel(session: Session): DeepReadonly<SceneModel> | null {
return session.getSceneModel();
}
nodes.find(nodeId) ​replaced · 2.0: Session.getSceneModel · written for 1.x 1.5
walk session.getSceneModel()
Used by: no app in the cross-check.
/** `nodes.find(nodeId)` → a walk over `session.getSceneModel()`. */
export function findNode(session: Session, nodeId: string): DeepReadonly<ConfiguratorNode> | null {
const walk = (
nodes: readonly DeepReadonly<ConfiguratorNode>[] | undefined,
): DeepReadonly<ConfiguratorNode> | null => {
for (const node of nodes ?? []) {
if (node.nodeId === nodeId) return node;
const found = walk(node.children);
if (found) return found;
}
return null;
};
return walk(session.getSceneModel()?.root);
}
nodes.object(nodeId) ​replaced · 2.0: Session.node · written for 1.x 1.5
session.node(nodeId), the model's root part (find({ node }) is every part of it)
Used by: app-4f16d2f6da, app-b362a81c85, app-b4ae2fce25, app-bcb7ef21da, app-ddfb017681, app-fc07850558, app-fc306ec3dc.
/** `nodes.object(nodeId)` → `session.node(nodeId)`, the model's root part. */
export function modelRoot(session: Session, nodeId: string): Part<CommitOptions> {
return session.node(nodeId);
}
nodes.ownerOf(object) ​replaced · 2.0: Hit.part, Part.node · written for 1.x 1.5
hit.part.node
Used by: app-0c773a4ee3.
/** `nodes.ownerOf(object)` → `hit.part.node`. */
export const ownerOf = (hit: Hit): string | null => hit.part.node;
nodes.setVisible(nodeId, visible) ​replaced · 2.0: Part.set, PartState.visible · written for 1.x 1.5
a look-only hide: session.node(nodeId).set({ visible }), reset with { visible: null }. To take the node out of the configuration (priced, saved), set enabled: false in the scene model instead
Used by: app-4f16d2f6da, app-b362a81c85.
/**
* `nodes.setVisible(nodeId, visible)` → a look-only hide, reset with `{ visible: null }`. To take
* the node out of the configuration (priced, saved), set `enabled: false` in the scene model.
*/
export function setVisible(session: Session, nodeId: string, visible: boolean): Promise<Report> {
return session.node(nodeId).set({ visible });
}
/** Drops the hide: the part shows what is underneath again. */
export function resetVisible(session: Session, nodeId: string): Promise<Report> {
return session.node(nodeId).set({ visible: null });
}
plugin(id) ​replaced · 2.0: Input.drag, Session.assembly, Overlays.dimensions, Session.select, Session.capture, Session.history, Session.camera, Session.stage · written for 1.x 1.5
the member that replaces that plugin: drag → input.drag; snap connectors → assembly; dimensions → overlays.dimensions; material swap and global materials → rules and select; screenshot → capture; scene history → history; camera controls → camera; post-process → stage.ambientOcclusion and stage.bloom through stage.set (C.6's setAO row)
Used by: app-04c2774145, app-4762caacdc, app-510d750508, app-b362a81c85, app-bcb7ef21da, app-c1384707fc, app-ceb4eccc72, app-e61a0b42ad, app-f07e9033d9, app-f9a32142f1, app-fb16dd1985, app-fc07850558, app-fc306ec3dc.
/** `plugin(id)` → the member that replaces that plugin. */
export function pluginMembers(session: Session) {
return {
drag: session.input.drag,
snapConnectors: session.assembly,
dimensions: session.overlays.dimensions,
materialSwap: session.select,
screenshot: session.capture,
sceneHistory: session.history,
cameraControls: session.camera,
postProcess: session.stage,
};
}
register(plugin), unregister(name) ​replaced · 2.0: Session.onFrame, Session.collection, Session.beforeCommit · written for 1.x 1.5
collections, onFrame, rules and beforeCommit
Used by: app-0c773a4ee3, app-378d190f1b, app-4f16d2f6da, app-510d750508, app-59c5b5751e, app-b362a81c85, app-b4ae2fce25, app-ceb4eccc72, app-ddfb017681, app-f9a32142f1, app-fc07850558, app-fc306ec3dc.
/** `register(plugin)`, `unregister(name)` → `onFrame` (and collections, rules, `beforeCommit`). */
export function registerFrameLogic(
session: Session,
everyFrame: (frame: Frame) => void,
signal: AbortSignal,
): () => void {
return session.onFrame(everyFrame, { signal });
}
viewer ​removed · written for 1.x 1.5
the host element is the app's own el; style through ::part(); size from 'resize'
Used by: app-04c2774145, app-0c773a4ee3, app-37356bf09f, app-4762caacdc, app-4f16d2f6da, app-4fd23bc869, app-510d750508, app-59c5b5751e, app-822bb9d6aa, app-8675b3225c, app-b362a81c85, app-bcb7ef21da, app-c1384707fc, app-ceb4eccc72, app-e3b47169bc, app-e61a0b42ad, app-e9a33606d2, app-f07e9033d9, app-f3598fad77, app-f9a32142f1, app-fb16dd1985, app-fc07850558, app-fc306ec3dc, app-fd09090420, configurator-template.
removed · written for 1.x 1.5
the engine-free /contract profile is the only one
Used by: no app in the cross-check.
window.__holoSession, window.__snapPlugin ​replaced · 2.0: Assembly.connections · written for 1.x 1.5
dev and E2E builds only; assembly.connections() for connector reads
Used by: app-04c2774145, app-4762caacdc.
/** `window.__holoSession`, `window.__snapPlugin` (dev and E2E builds only) → `assembly.connections()`. */
export function connectionsForE2E(session: Session): readonly Connection[] {
return session.assembly.connections();
}
runtime-escape-hatches.ts) ​Revision 3 mapped every public 1.4 member but only the coarse internals. Round 6 found about 33 patterns that ports meet inside the hatch files and plugin calls (fleet report, F7; pilot report, F18). Each row becomes a migration.json entry with a compiled recipe.
playAnimation(name) (app-04c2774145, app-fd09090420, app-fb16dd1985; app-b362a81c85's and app-0c773a4ee3's doors, K64) ​replaced · 2.0: Session.animations, Animations.play, PlayOptions.toggle · written for 1.x 1.5
session.animations.play(node, clip, { toggle: true })
Used by: app-04c2774145, app-0c773a4ee3, app-4762caacdc, app-b362a81c85, app-bcb7ef21da, app-f9a32142f1, app-fb16dd1985, app-fd09090420.
/** Viewer element `playAnimation(name)`. */
export function playAnimation(session: Session, node: string, clip: string): Promise<Report> {
return session.animations.play(node, clip, { toggle: true });
}
resetAnimations() (app-04c2774145, app-f9a32142f1, app-b362a81c85, app-0c773a4ee3) ​replaced · 2.0: Session.animations, Animations.reset · written for 1.x 1.5
session.animations.reset(node?); inside a transaction or beforeCommit it is staged, so reads see frame 0 at once. It also clears each clip's last direction, so the next toggle plays forward, as 1.x set every clip forward (viewer/controllers/animation.controller.ts:366-379 at eb5ce2afe). Unlike 1.x's silent reset (:362-366), it fires 'animation:stopped' for each playing clip, so delete the hand counter reset (app-04c2774145 use-viewer.ts:876-883 at c4c6ffd2)
Used by: app-04c2774145, app-0c773a4ee3, app-4762caacdc, app-b362a81c85, app-bcb7ef21da, app-f9a32142f1, app-fd09090420.
/** Viewer element `resetAnimations()`, without a hand-maintained animation counter reset. */
export function resetAnimations(session: Session, node?: string): Promise<Report> {
return session.animations.reset(node);
}
stopAllAnimations() ​replaced · 2.0: Session.animations, Animations.list, Animations.pause, ClipRef.node, ClipRef.clip · written for 1.x 1.5
for (const c of session.animations.list()) if (c.node) void session.animations.pause(c.node, c.clip)
Used by: app-0c773a4ee3, app-b362a81c85, app-bcb7ef21da, app-fd09090420.
/** Viewer element `stopAllAnimations()`. */
export function stopAllAnimations(session: Session): void {
for (const c of session.animations.list()) {
if (c.node) void session.animations.pause(c.node, c.clip);
}
}
setModelPRS(nodeId, prs, { animate: false }) to place a node (app-04c2774145 position-sync.ts) ​replaced · 2.0: Session.applyPatch, JsonPatchOp · written for 1.x 1.5
a recorded move: session.applyPatch([{ op: 'replace', path: '/model/node/' + id + '/position', value }]) (and /rotation). Not a pose rule: that is look-only and never saved
Used by: app-04c2774145, app-4762caacdc, app-c1384707fc, app-f3598fad77, app-f9a32142f1.
/** Viewer element `setModelPRS(nodeId, prs, { animate: false })`, recorded in the model. */
export function setModelPrs(
session: Session,
id: string,
position: V1.IModelNode['position'],
rotation: V1.IModelNode['rotation'],
): Promise<Report> {
const path = '/model/node/' + id.replace(/~/g, '~0').replace(/\//g, '~1');
return session.applyPatch([
{ op: 'replace', path: path + '/position', value: { ...position } },
{ op: 'replace', path: path + '/rotation', value: { ...rotation } },
]);
}
setModelPRS(…, { animate: true }), a tween (app-f3598fad77) ​replaced · 2.0: Session.node, Part.pose, Part.set, PartState.pose, PoseInput, PartWriteOptions.durationMs, Report · written for 1.x 1.5
a slide tied to a scene write (app-f3598fad77's module swap, apps/app-f3598fad77/src/components/viewer/animate-scene-swap.ts:293-320 at c4c6ffd2) takes pose rules on the model roots: before the sync, a 0 ms node(id).set({ pose }) holds each moving survivor where it is drawn; right after the sync is called, another starts each newcomer at 1 mm, which waits for the sync's node (Report.pending) instead of recording 'target-unmatched' (Writer.node, Q4); when the sync's Report resolves 'committed', set({ pose }, { durationMs }) tweens them to their places, from the pose drawn last, a newcomer to its destination's scale (1 when left out: a pose write merges field by field, so the 1 mm would stay, R2-DOC-3); release the rules with null when the slide is off, since a pose rule beats the scene model's pose (R7; the app-f3598fad77 paper port). A tween with no scene write is an onFrame hook writing frame.pose(session.node(id), …), then the recorded patch above
Used by: app-04c2774145, app-4762caacdc, app-c1384707fc, app-f3598fad77, app-f9a32142f1.
/**
* Viewer element `setModelPRS(..., { animate: true })` around a module-swap sync. `destination`
* is the node's pose in the synced model; a newcomer grows from 1 mm to its `scale` (1 when
* left out), because a later pose write merges into the starting one field by field.
*/
export async function slideSceneSwap(
session: Session,
sync: () => Promise<Report>,
moving: readonly { id: string; newcomer: boolean; destination: PoseInput }[],
durationMs: number,
slide: boolean,
): Promise<Report> {
if (!slide) {
for (const move of moving) await session.node(move.id).set({ pose: null });
return sync();
}
// Placed modules stay where they are drawn until the sync has moved them.
for (const move of moving.filter((m) => !m.newcomer)) {
const { position, quaternion, scale } = session.node(move.id).pose;
await session.node(move.id).set({ pose: { position, quaternion, scale } }, { durationMs: 0 });
}
// A newcomer's node exists once the sync commits: its starting rule, written after the sync
// was called, waits for that node instead of recording 'target-unmatched'.
const syncing = sync();
const starts = moving
.filter((m) => m.newcomer)
.map((m) =>
session.node(m.id).set({ pose: { ...m.destination, scale: 0.001 } }, { durationMs: 0 }),
);
const report = await syncing;
await Promise.all(starts);
if (report.outcome === 'committed') {
await Promise.all(
moving.map(({ id, newcomer, destination }) => {
// a placed module keeps its held scale (a mirror's −1 among them) unless one is given
const pose = newcomer ? { ...destination, scale: destination.scale ?? 1 } : destination;
return session.node(id).set({ pose }, { durationMs });
}),
);
}
return report;
}
updateControls(options) (app-04c2774145; app-4f16d2f6da also passes minPolarAngle and truckAxes, pergola-viewer.tsx:124-135 at 342b26ce) ​replaced · 2.0: Camera.limits, CameraLimits.panAxes, CameraLimits.minPolarDeg, Camera.mode · written for 1.x 1.5
session.camera.limits({ … }) (a merge patch, so distance and collision can be set by separate calls) and session.camera.mode(…); truckAxes: 'none' is not needed once the target is bounded by limits({ target }) (app-4f16d2f6da set it only when it could not bound the target); truckAxes: 'horizontal' is limits({ panAxes: 'horizontal' })
Used by: app-04c2774145, app-4762caacdc, app-4f16d2f6da, app-ceb4eccc72, app-f9a32142f1, app-fc306ec3dc.
/**
* Viewer element `updateControls(options)`: `camera.limits` (a merge patch; radians become
* degrees) and `camera.mode`. `truckAxes: 'horizontal'` is `panAxes: 'horizontal'`; `'none'` is
* not needed once the target is bounded by `limits({ target })`.
*/
export async function updateControls(
session: Session,
controls: {
minDistance?: number;
maxDistance?: number;
minPolarAngle?: number;
maxPolarAngle?: number;
truckAxes?: 'horizontal' | 'none';
},
mode?: 'orbit' | 'pan' | 'fixed',
): Promise<void> {
const limits: CameraLimits = {};
if (controls.minDistance !== undefined) limits.minDistance = controls.minDistance;
if (controls.maxDistance !== undefined) limits.maxDistance = controls.maxDistance;
if (controls.minPolarAngle !== undefined) {
limits.minPolarDeg = angle.radToDeg(controls.minPolarAngle);
}
if (controls.maxPolarAngle !== undefined) {
limits.maxPolarDeg = angle.radToDeg(controls.maxPolarAngle);
}
if (controls.truckAxes) limits.panAxes = controls.truckAxes;
await session.camera.limits(limits);
if (mode) await session.camera.mode(mode);
}
PluginContext.getCameraControls(): minDistance, maxDistance, minPolarAngle, maxPolarAngle fields, setBoundary(box), getFocalOffset, setFocalOffset(x, y, z, true) (app-fc07850558 runtime-escape-hatches.ts:119-172 at 5cdd2780, app-4f16d2f6da :105-119, 353-384 at 342b26ce; app-fc306ec3dc) ​replaced · 2.0: Session.camera, Camera.limits, CameraLimits.minDistance, CameraLimits.maxDistance, CameraLimits.minPolarDeg, CameraLimits.maxPolarDeg, CameraLimits.target, Camera.state, CameraState.focalOffset, Camera.set, CameraView.focalOffset, CameraMoveOptions.durationMs, angle · written for 1.x 1.5
session.camera.limits({ minDistance, maxDistance, minPolarDeg, maxPolarDeg, target: { min, max } }) (radians become degrees); a focal-offset read is camera.state.focalOffset; an eased offset is camera.set({...view, focalOffset }, { durationMs }) from camera.state, or camera.lens({ insets }) when it makes room for a sheet (5.6)
Used by: app-4f16d2f6da, app-510d750508, app-b362a81c85, app-b4ae2fce25, app-ceb4eccc72, app-ddfb017681, app-fc07850558, app-fc306ec3dc.
/** `PluginContext.getCameraControls()` limits, boundary and focal-offset access. */
export async function cameraControls(
session: Session,
minDistance: number,
maxDistance: number,
minPolarAngle: number,
maxPolarAngle: number,
min: V1.Point3,
max: V1.Point3,
focalOffset: V1.Point3,
durationMs: number,
): Promise<V1.Point3> {
await session.camera.limits({
minDistance,
maxDistance,
minPolarDeg: angle.radToDeg(minPolarAngle),
maxPolarDeg: angle.radToDeg(maxPolarAngle),
target: { min, max },
});
const { target, azimuthDeg, polarDeg, distance, focalOffset: previous } = session.camera.state;
const view = { target, azimuthDeg, polarDeg, distance };
// Use an orbit view: a LookAtView would clear the focal offset.
await session.camera.set({ ...view, focalOffset }, { durationMs });
return previous;
}
CameraControls.enabled = false while the app sets the view every frame, with minDistance and maxDistance pinned to the eye and all three put back on dispose (app-ddfb017681's panorama and collider-overlay plugins, apps/app-ddfb017681/src/viewer-plugins/panorama-plugin.ts:498-510, 970-978, collider-overlay-plugin.ts:526-529; estate's copies, apps/app-b4ae2fce25/src/viewer-plugins/panorama-plugin.ts:507, collider-overlay-plugin.ts:479, at be828d5d) ​replaced · 2.0: Session.camera, Camera.mode, Session.onFrame, Frame, Frame.camera, Frame.timeMs, FrameOptions.continuous, FrameOptions.signal, CameraView · written for 1.x 1.5
session.camera.mode('fixed'): the shopper's orbit, pan and dolly are off and the wheel is left to the page; the view of each frame is frame.camera(view) in an onFrame(…, { continuous: true, signal }) hook (Frame.camera, 5.12, K41), which commits nothing. Leaving is camera.mode('orbit') and aborting the hook, after which the committed view draws again. The distance pins go: Frame.camera holds the view
Used by: app-b4ae2fce25, app-ddfb017681.
/**
* app-ddfb017681's and estate's `controls.enabled = false` with the view set every frame:
* `camera.mode('fixed')` and `Frame.camera` in a continuous hook. The returned call leaves: the
* hook ends, so the committed view draws again, and the shopper orbits again.
*/
export async function driveCamera(
session: Session,
viewAt: (timeMs: number) => CameraView,
): Promise<() => Promise<Report>> {
await session.camera.mode('fixed');
const hook = new AbortController();
session.onFrame((frame) => frame.camera(viewAt(frame.timeMs)), {
continuous: true,
signal: hook.signal,
});
return () => {
hook.abort();
return session.camera.mode('orbit');
};
}
'controlend' and before a frame, and the orbit pivot lowered to the floor after each fit, vertical pan left free (apps/app-4f16d2f6da/src/lib/pergola-plugins/pergola-camera-guard-plugin.ts:10-55, 264-325 at c4c6ffd2) ​replaced · 2.0: Session.camera, Camera.limits, CameraLimits.maxPolarDeg, CameraLimits.minAzimuthDeg, CameraLimits.maxAzimuthDeg, Session.find, Parts.bounds, Bounds, Camera.state, CameraState.fovDeg, CameraState.aspect, CameraState.position, CameraState.target, Camera.set, LookAtView, CameraMoveOptions.durationMs, angle, vec3 · written for 1.x 1.5
the clamps become persistent camera.limits({ maxPolarDeg: 90, minAzimuthDeg: 9.8, maxAzimuthDeg: 170.2 }) (E0; a baked scene takes its manifest's angle), and app limits are sticky, so the 'controlend' re-clamp and the before-frame restore go; the ground-pinned framing is app math on /math, as app-fc306ec3dc's (G43): bounds() plus the padding, camera.state.fovDeg and aspect give the eye point, written as one camera.set({ position, target: { x, y: 0, z } }, { durationMs }) (a baked scene: the box's lowest y). camera.fit keeps the box-centre pivot, and a camera.set after it would play a second move; no target box, whose faces would stop the vertical pan app-4f16d2f6da keeps
Used by: app-4f16d2f6da, app-ceb4eccc72.
/** app-4f16d2f6da's `controlend` re-clamps and ground-pinned framing. */
export async function frameFromGround(
session: Session,
query: Query,
paddingM: number,
durationMs: number,
): Promise<Report | null> {
await session.camera.limits({ maxPolarDeg: 90, minAzimuthDeg: 9.8, maxAzimuthDeg: 170.2 });
const bounds = session.find(query).bounds();
if (!bounds) return null;
const { x, z } = bounds.center;
const target = { x, y: 0, z };
const state = session.camera.state;
const direction = vec3.normalize(vec3.sub(state.position, state.target));
const right = vec3.normalize(vec3.cross({ x: 0, y: 1, z: 0 }, direction));
const up = vec3.cross(direction, right);
const tanY = Math.tan(angle.degToRad(state.fovDeg) / 2);
const tanX = tanY * state.aspect;
let distance = paddingM;
for (const bx of [bounds.min.x - paddingM, bounds.max.x + paddingM]) {
for (const by of [bounds.min.y - paddingM, bounds.max.y + paddingM]) {
for (const bz of [bounds.min.z - paddingM, bounds.max.z + paddingM]) {
const corner = vec3.sub({ x: bx, y: by, z: bz }, target);
distance = Math.max(
distance,
vec3.dot(corner, direction) + Math.abs(vec3.dot(corner, right)) / tanX,
vec3.dot(corner, direction) + Math.abs(vec3.dot(corner, up)) / tanY,
);
}
}
}
const position = vec3.add(target, vec3.scale(direction, distance));
return session.camera.set({ position, target: { x, y: 0, z } }, { durationMs });
}
camera.fov, setViewOffset, setHorizontalFov (app-04c2774145, app-fd09090420, app-fc306ec3dc; app-bcb7ef21da's phone stage inset, framed-view.ts:50-53 at 342b26ce; chair and app-378d190f1b, which hold a vertical field of view every frame, and app-e3b47169bc's preview) ​replaced · 2.0: Session.camera, Camera.lens, CameraLens.fovDeg, CameraLens.horizontalFovDeg, CameraLens.insets, Insets, CameraMoveOptions.durationMs, Camera.fit · written for 1.x 1.5
session.camera.lens({ fovDeg | horizontalFovDeg, insets }, { durationMs }); chair, app-378d190f1b and app-e3b47169bc write camera.lens({ fovDeg: 40 }), 42 and 45 once (C.7's viewer.camera.fov row); app-bcb7ef21da's inset frustum math (stage-inset.ts) is then camera.lens({ insets: { top: 10, right: 0, bottom: toolbarBandPx, left: 0 } }) plus camera.fit() (Insets has four required sides)
Note: A transient screen-space shift is not an inset-plus-fit: app-fd09090420's mobile sheet (startViewOffsetGlide, panel-glide.ts) holds the picture where it was when the stage's height changes by delta, then eases the offset to zero over 260 ms. In 2.0 that is holdThroughResize above: in the same task as the resize (the layout effect), camera.lens({ insets: held }, { durationMs: 0 }), then camera.lens({ insets: zero }, { durationMs: 260, easing: 'easeOut' }), and no camera.fit(). Insets are 0 or more, so the held inset is bottom: delta for a growing stage and top: -delta for a shrinking one. CameraMoveOptions.durationMs says the eased write starts from the 0 ms one (it jumps, then glides), and Camera.lens says the lens is drawn in the frame that shows the new aspect, so the picture never jumps; reduced motion lands at zero at once, as the 1.x hatch did.
Used by: app-bcb7ef21da, app-fd09090420.
/** Camera `fov`, `setHorizontalFov` and the phone stage's `setViewOffset`. */
export async function setCameraLens(
session: Session,
lens: { fovDeg: number } | { horizontalFovDeg: number },
toolbarBandPx: number,
durationMs: number,
): Promise<Report> {
await session.camera.lens(
{ ...lens, insets: { top: 10, right: 0, bottom: toolbarBandPx, left: 0 } },
{ durationMs },
);
return session.camera.fit();
}
/**
* A transient `setViewOffset` glide (app-fd09090420's mobile sheet): when the stage's height changes
* by `deltaPx` (new minus old, CSS px), hold the picture where it was in the same task as the
* resize (a layout effect), then ease the offset back to zero. No fit, no view write. `Insets`
* are 0 or more, so a growing stage holds `bottom: delta` and a shrinking one `top: -delta`.
*/
export function holdThroughResize(
session: Session,
deltaPx: number,
durationMs = 260,
): Promise<Report> {
const held: Insets =
deltaPx >= 0
? { top: 0, right: 0, bottom: deltaPx, left: 0 }
: { top: -deltaPx, right: 0, bottom: 0, left: 0 };
// drawn in the frame that shows the new aspect; the eased write starts from it
void session.camera.lens({ insets: held }, { durationMs: 0 });
return session.camera.lens(
{ insets: { top: 0, right: 0, bottom: 0, left: 0 } },
{ durationMs, easing: 'easeOut' },
);
}
getCameraView() read through the DOM (app-822bb9d6aa's showroom camera: a querySelector('holo-configurator-viewer') read of the fitted view, then the showroom view written twice, apps/app-822bb9d6aa/src/components/viewer/initial-camera.ts:44-71 at be828d5d) ​replaced · 2.0: Session.camera, Camera.fit, CameraMoveOptions.durationMs, Camera.state, CameraState, Camera.set, CameraView · written for 1.x 1.5
session.camera.state after an awaited session.camera.fit(undefined, { durationMs: 0 }): a durationMs: 0 write reads back from when it starts, so read after the await (5.6); then one session.camera.set(view, { durationMs: 0 }), which lands on the view itself, so the second write goes
Used by: app-822bb9d6aa, app-e61a0b42ad, app-f07e9033d9, app-fc306ec3dc.
/** DOM `getCameraView()` after fitting, followed by one showroom-view write. */
export async function fittedCameraView(
session: Session,
view: V1.CameraView,
): Promise<CameraState> {
await session.camera.fit(undefined, { durationMs: 0 });
const fitted = session.camera.state;
await session.camera.set(
{
target: view.target,
azimuthDeg: view.azimuth,
polarDeg: view.polar,
distance: view.radius,
focalOffset: view.focalOffset,
},
{ durationMs: 0 },
);
return fitted;
}
renderer.setSize, resizeViewerCanvas (app-fd09090420) ​runtime-owned · written for 1.x 1.5
the aspect follows a resize at once (K37)
Used by: app-4f16d2f6da, app-510d750508, app-ceb4eccc72, app-e3b47169bc, app-fc07850558, app-fd09090420.
shadowMap.needsUpdate, shadowMap settings (app-04c2774145, pergola; app-fc07850558 and app-fc306ec3dc reach it through session.viewer.renderer, app-fc07850558 runtime-escape-hatches.ts:209-224, app-fc306ec3dc shadow-refresh.ts:10-21, at 5cdd2780) ​runtime-owned · written for 1.x 1.5
shadows refresh as B.4 X11 says; the map per tier is the runtime's (ShadowSpec)
Used by: app-fc306ec3dc.
renderer.transmissionResolutionScale = 0.5 (pergola src/lib/pergola-plugins/pergola-lighting-plugin.ts:98, app-4f16d2f6da :109 at be828d5d) ​runtime-owned · written for 1.x 1.5
the transmission pass's resolution is the runtime's (B.4 X17); the port drops the setting and its unit test's assertion, which 10.3's glass run replaces
Used by: app-4f16d2f6da, app-ceb4eccc72.
shadow-light.ts (aiming the key light at the product) ​runtime-owned · written for 1.x 1.5
ShadowSpec.covers (default: role 'product')
Used by: app-04c2774145, app-4762caacdc, app-59c5b5751e, app-e61a0b42ad.
shadow.radius, .bias, .normalBias set by the app (app-fc306ec3dc room rig, use-planner-viewer.ts:1780-1789 at 342b26ce: radius 14 → 3, normalBias 0.035) ​replaced · 2.0: Session.find, Query, Parts.set, PartState.shadows, Session.collection, CollectionOptions.role, Collection.set, CollectionSpec.stage, StageSettings.lights, ItemSpec, LightSpec, ShadowSpec · written for 1.x 1.5
a collection with stage: { lights: 'none' } and light items ({ key, kind: 'light', light: { kind: 'sun', direction, intensity, shadow: { softness } } }); the filter follows the receivers and bias is runtime-owned (K27). A 1.x radius r carries over as softness 0.3 × r / base, base 3 while a product part receives the shadow and 14 while only the ground and scenery do (ShadowSpec): app-fc306ec3dc's and chair's 3 with their product receiving (chairPlugin.ts:41, settings.ts:367 at be828d5d; app-fc306ec3dc's scene-model cabinets through a receive: true rule, PartState.shadows) is 0.3, and app-510d750508's 1.5 (crib-lighting-plugin.ts:77 at be828d5d) is 0.15 while its crib receives, the default, or about 0.032 with receive: false on the crib, as 1.x; the rig itself is a signed-off visual change (D44)
Visual changes to sign off (D44): The room rig (collection with light items) is a signed-off visual change..
Used by: no app in the cross-check.
/** Key-light `shadow.radius`, `bias` and `normalBias` writes. */
export async function setShadowSoftness(
session: Session,
radius: number,
productReceives: boolean,
direction: V1.Point3,
intensity: number,
): Promise<CollectionReport> {
await session.find({ role: 'product' }).set({ shadows: { receive: productReceives } });
return session.collection('shadow-rig', { role: 'scenery' }).set({
stage: { lights: 'none' },
items: [
{
key: 'key',
kind: 'light',
light: {
kind: 'sun',
direction,
intensity,
shadow: { softness: (0.3 * radius) / (productReceives ? 3 : 14) },
},
},
],
});
}
ShadowGround hidden, the rig re-applied after a re-seed (app-fc07850558 shadow-rig.ts:63-102 at 5cdd2780, since d1debb8a9) ​replaced · 2.0: Session.collection, CollectionOptions.role, Collection.set, CollectionSpec.stage, StageSettings.lights, StageSettings.ground, ItemSpec, LightSpec, ShadowSpec, HexColor · written for 1.x 1.5
one 'scenery' collection with stage: { lights: 'none', ground: 'none' } and light items: a sun with shadow: { covers: [{ collection: 'room' }, { collection: 'kitchen' }] } for the key, two sun fills without a shadow, a sky with equal colours for the ambient (role 'scenery', not 'helper', so captures keep the rig). Bias and the shadow box are runtime-owned (K27). The items are desired state, so no re-apply check is needed (6.5). The rig is a signed-off visual change (D44)
Visual changes to sign off (D44): app-fc07850558's 'scenery' rig is a signed-off visual change..
Used by: app-fc07850558.
/** Re-seeded key/fill/ambient rig mutations and hiding `ShadowGround`. */
export function setRoomRig(
session: Session,
direction: V1.Point3,
keyIntensity: number,
fillIntensity: number,
ambientColor: HexColor,
): Promise<CollectionReport> {
return session.collection('room-rig', { role: 'scenery' }).set({
stage: { lights: 'none', ground: 'none' },
items: [
{
key: 'key',
kind: 'light',
light: {
kind: 'sun',
direction,
intensity: keyIntensity,
shadow: { covers: [{ collection: 'room' }, { collection: 'kitchen' }] },
},
},
{
key: 'fill-left',
kind: 'light',
light: { kind: 'sun', direction: { x: 1, y: -1, z: 0 }, intensity: fillIntensity },
},
{
key: 'fill-right',
kind: 'light',
light: { kind: 'sun', direction: { x: -1, y: -1, z: 0 }, intensity: fillIntensity },
},
{
key: 'ambient',
kind: 'light',
light: { kind: 'sky', sky: ambientColor, ground: ambientColor },
},
],
});
}
snapTo(source, target) ​replaced · 2.0: Session.assembly, Assembly.attach, AttachOptions.from, AttachOptions.to, ConnectorRef.node, ConnectorRef.connector · written for 1.x 1.5
session.assembly.attach(source.nodeId, { from: <source mesh name>, to: { node: target.nodeId, connector: <target mesh name> } }) (6.12)
Used by: app-04c2774145, app-37356bf09f, app-4762caacdc, app-ceb4eccc72, app-f9a32142f1.
/** Snap plugin `snapTo(source, target)`. */
export function snapTo(
session: Session,
source: V1.NodeTarget,
target: V1.NodeTarget,
sourceMeshName: string,
targetMeshName: string,
): Promise<AttachReport> {
return session.assembly.attach(source.nodeId, {
from: sourceMeshName,
to: { node: target.nodeId, connector: targetMeshName },
});
}
getPlanesForNode(nodeId), getPlane(id), getConnections() ​replaced · 2.0: Session.assembly, Assembly.connectors, Assembly.connections, Connector, Connector.ref, ConnectorRef.connector, Connection · written for 1.x 1.5
session.assembly.connectors(nodeId); session.assembly.connections()
Used by: app-04c2774145, app-4762caacdc, app-c1384707fc, app-f9a32142f1.
/** Snap plugin `getPlanesForNode(nodeId)`, `getPlane(id)` and `getConnections()`. */
export function getPlanesForNode(
session: Session,
nodeId: string,
meshName: string,
): { connectors: Connector[]; connector: Connector | null; connections: Connection[] } {
const connectors = session.assembly.connectors(nodeId);
return {
connectors,
connector: connectors.find((c) => c.ref.connector === meshName) ?? null,
connections: session.assembly.connections(),
};
}
disconnect(planeId) ​replaced · 2.0: Session.assembly, Assembly.detach, ConnectorRef.node, ConnectorRef.connector · written for 1.x 1.5
session.assembly.detach({ node, connector })
Used by: no app in the cross-check.
/** Snap plugin `disconnect(planeId)`. */
export function disconnect(session: Session, node: string, connector: string): Promise<Report> {
return session.assembly.detach({ node, connector });
}
canSnap(a, b) ​replaced · 2.0: Session.assembly, Assembly.candidates, ConnectorRef.node · written for 1.x 1.5
session.assembly.candidates(node, { from }), or attach(…, { dryRun: true })
Used by: no app in the cross-check.
/** Snap plugin `canSnap(a, b)`. */
export function canSnap(
session: Session,
source: V1.NodeTarget,
target: V1.NodeTarget,
from: string,
): boolean {
return (
session.assembly.candidates(source.nodeId, { from, to: { node: target.nodeId } }).length > 0
);
}
SnapPlane fields: id, nodeId, type, enabled, priority, connectedTo, userData, world normal, center, polygon ​replaced · 2.0: Session.assembly, Assembly.connectors, Connector, Connector.ref, Connector.type, Connector.enabled, Connector.priority, Connector.peer, Connector.free, Connector.meta, Connector.accepts, Connector.normal, Connector.position, Connector.polygon, ConnectorRef.connector, JsonValue · written for 1.x 1.5
Connector: id (nodeId:meshName) → ref ({ node, connector: meshName }); type, enabled, priority kept; connectedTo → peer and free; userData.connector (the side) → meta.connector; userData.allowedTypes → accepts; other userData keys → meta; normal, center → normal, position; polygon kept
Used by: no app in the cross-check.
/** `SnapPlane` identity, connection, metadata and world-geometry fields. */
export function readSnapPlane(
session: Session,
nodeId: string,
meshName: string,
): {
ref: Connector['ref'];
type: string;
enabled: boolean;
priority: number;
peer: Connector['peer'];
free: boolean;
side: JsonValue | undefined;
accepts: readonly string[];
meta: Connector['meta'];
normal: Connector['normal'];
position: Connector['position'];
polygon: Connector['polygon'];
} | null {
const c = session.assembly.connectors(nodeId).find((c) => c.ref.connector === meshName);
if (!c) return null;
return {
ref: c.ref,
type: c.type,
enabled: c.enabled,
priority: c.priority,
peer: c.peer,
free: c.free,
side: c.meta.connector,
accepts: c.accepts,
meta: c.meta,
normal: c.normal,
position: c.position,
polygon: c.polygon,
};
}
history.transaction around assembly snaps, to keep them out of history (app-04c2774145) ​replaced · 2.0: Session.transaction, Tx, Tx.assembly, Assembly.attach, AttachOptions.from, AttachOptions.to, ConnectorRef, TransactionOptions.undo, TransactionReport, AttachReport · written for 1.x 1.5
do the snaps inside the sync's session.transaction(work, { undo }): they are part of that one step
Used by: app-04c2774145, app-37356bf09f, app-4762caacdc, app-510d750508, app-bcb7ef21da, app-f3598fad77.
/** `history.transaction` around the sync's assembly snaps. */
export function syncAndSnap(
session: Session,
sync: (tx: Tx) => Promise<Report>,
source: V1.NodeTarget,
target: V1.NodeTarget,
from: string,
connector: string,
undo: string,
): Promise<TransactionReport<AttachReport>> {
return session.transaction(
async (tx) => {
await sync(tx);
return tx.assembly.attach(source.nodeId, { from, to: { node: target.nodeId, connector } });
},
{ undo },
);
}
findModelRoot(scene, nodeId), root traversal by userData.nodeId (app-04c2774145, app-fc306ec3dc, app-c1384707fc) ​replaced · 2.0: Session.node, Part, CommitOptions · written for 1.x 1.5
session.node(nodeId)
Used by: app-04c2774145, app-4762caacdc.
/** `findModelRoot(scene, nodeId)` and traversal by `userData.nodeId`. */
export function findModelRoot(session: Session, nodeId: string): Part<CommitOptions> {
return session.node(nodeId);
}
Box3.setFromObject(root), expandByObject metrics (app-04c2774145, app-f9a32142f1) ​replaced · 2.0: Session.node, Part.visible, Part.bounds, Bounds, box3 · written for 1.x 1.5
session.node(nodeId).bounds({ includeHidden: true }) (hidden descendants count, as they did); a scene metric is the union over the model nodes whose root visible is true, because includeHidden: true also counts a hidden root and app-04c2774145's metric skips one (compute-scene-center.ts:36-43 at c4c6ffd2)
Used by: app-04c2774145, app-0c773a4ee3, app-4762caacdc, app-4f16d2f6da, app-510d750508, app-b362a81c85, app-bcb7ef21da, app-c1384707fc, app-ceb4eccc72, app-e3b47169bc, app-f9a32142f1, app-fc07850558, app-fc306ec3dc.
/** `Box3.setFromObject(root)` and `expandByObject` metrics, including hidden descendants. */
export function sceneBounds(session: Session, nodeIds: readonly string[]): Bounds | null {
let result: Bounds | null = null;
for (const nodeId of nodeIds) {
const root = session.node(nodeId);
if (!root.visible) continue;
const bounds = root.bounds({ includeHidden: true });
if (bounds) result = result ? box3.union(result, bounds) : bounds;
}
return result;
}
material-swap.applyPBR(pattern, props), applyPBRTo(root, pattern, props) (app-f07e9033d9, app-fc306ec3dc; app-0c773a4ee3 calls neither: its finishes go through select, 8.2, apps/app-0c773a4ee3/src/lib/configurator-3d/material-sync.ts:51-57 at c4c6ffd2) ​replaced · 2.0: Session.find, Query, Parts.set, HexColor, PartState.material, MaterialSpec.baseColor, MaterialSpec.metallic, MaterialSpec.roughness, MaterialSpec.side, MaterialSpec.maps, TextureSpec.url, TextureSpec.scale · written for 1.x 1.5
one persistent rule per region: session.find({ name: <glob>, kind: 'mesh' }).set({ material: { … } }); kind: 'mesh' because 1.4 matched meshes only, and a glob that also names a group would cascade to every mesh below (R8, C.8). Props: color → baseColor (a HexColor), metalness → metallic, roughness kept, map, normalMap, roughnessMap → maps.baseColor, maps.normal, maps.roughness, repeat → scale, side kept. Delete the "re-apply after every sync" effects. With material-swap.resetUnspecifiedProps: true (app-fc306ec3dc, apps/app-fc306ec3dc/src/viewer/page-document.json:34 at 5cdd2780), 1.4 also cleared the map, normalMap and aoMap a call didn't name (viewer/plugins/material-swap-plugin.ts:695-698 at eb5ce2afe), so Tölgy → Fehér left no grain. A rule merges (R10), so a literal port keeps the old maps: name those slots in every finish of such a family, 'none' for an absent map (maps: { baseColor, normal, occlusion }), or switch named materials with { ref }, which replaces the rule's material (R10) and so also drops authored maps 1.4 kept (a visual change, D44). applyPBR also installed one clone of the first matched mesh's material on every match (viewer/plugins/material-swap-plugin.ts:664-686 at eb5ce2afe), while a rule merges over each part's own material (R10): a pattern over meshes with different authored materials, such as app-f07e9033d9's engine-casing|engine-plastic (apps/app-f07e9033d9/src/lib/scene/boat-materials.ts:188, whose pillow_logo gets its own call for this reason, :276-279 at c4c6ffd2), shows each mesh's own maps in 2.0, a D44 item. Its normalMap went through the flipping loader (5.4), so app-f07e9033d9's Linetex and Silvertex weave and deck-grain normal maps (src/lib/scene/fabric-ranges.ts:31-44, boat-materials.ts:150) render unflipped in 2.0, a second D44 item
Note: maps.* URLs (1.x map, normalMap, roughnessMap) resolve against the page's base URI, as fetch() resolves them, never against the scene model's assetBaseUrl: make a stored relative path absolute with new URL(path, assetBaseUrl).href first. On an app-proxy page (app-fc306ec3dc, app-fc07850558) the page's origin is the shop's, so build texture URLs on the app's deployed origin (NEXT_PUBLIC_APP_ORIGIN), never window.location.origin (MIGRATION-PROMPT.md, "URLs on an app-proxy page").
Visual changes to sign off (D44): { ref } replaces the rule's material and so drops authored maps 1.4 kept.; A pattern over meshes with different authored materials shows each mesh's own maps in 2.0 (1.4 installed one clone of the first match's material on every match).; Normal maps 1.4 loaded through the flipping loader render unflipped in 2.0 (app-f07e9033d9's weave and deck-grain maps)..
Used by: app-0c773a4ee3, app-37356bf09f, app-822bb9d6aa, app-8675b3225c, app-bcb7ef21da, app-c1384707fc, app-e3b47169bc, app-e61a0b42ad, app-e9a33606d2, app-f07e9033d9, app-fc306ec3dc, configurator-template.
/** `material-swap.applyPBR(pattern, props)` and `applyPBRTo(root, pattern, props)`. */
export function applyPbr(
session: Session,
glob: string,
props: {
color: HexColor;
metalness: number;
roughness: number;
map?: string;
normalMap?: string;
roughnessMap?: string;
aoMap?: string;
repeat: number;
side: 'front' | 'back' | 'double';
},
): Promise<Report> {
const texture = (url: string | undefined): { url: string; scale: number } | 'none' =>
url ? { url, scale: props.repeat } : 'none';
// Explicit absent slots preserve resetUnspecifiedProps; maps remain per-part authored merges.
return session.find({ name: glob, kind: 'mesh' }).set({
material: {
baseColor: props.color,
metallic: props.metalness,
roughness: props.roughness,
side: props.side,
maps: {
baseColor: texture(props.map),
normal: texture(props.normalMap),
roughness: texture(props.roughnessMap),
occlusion: texture(props.aoMap),
},
},
});
}
camera-presets.applyPreset(pose), .apply(name), .save(name) (app-fb16dd1985 runtime-escape-hatches.ts:154-196, app-510d750508 :131-139, pergola :161-169 at c4c6ffd2; app-378d190f1b turns the plugin off, page-document.json:23) ​replaced · 2.0: Session.camera, Camera.state, CameraState, CameraView, Camera.set, Camera.views, CameraMoveOptions.durationMs, Camera.limits, CameraLimits.maxPolarDeg, Session.on, Events.camera:control-end, CameraMoveOptions.easing · written for 1.x 1.5
session.camera.views.go(name, { durationMs: GLIDE_MS, easing: 'damped' }) (views live in the version-2 document's camera.views); session.camera.set(view, { durationMs: GLIDE_MS, easing: 'damped' }); save = read session.camera.state and store the view. 1.4's plugin moved with camera-controls' transition while smoothTransitions is on, its default (setLookAt(…, smooth) and fitToBox(box, smooth), viewer/plugins/camera-presets-plugin.ts:29, 76-80, 104 at eb5ce2afe), so a preset glides as C.3's view row does (Q20), and with it off the move is durationMs: 0. pergola's toolbar (src/components/viewer/pergola-viewer.tsx:214, 450-494): apply('top') is camera.views.go('top', { durationMs: GLIDE_MS, easing: 'damped' }) and apply('default') is camera.set(saved, { durationMs: GLIDE_MS, easing: 'damped' }); its underside view looks up past the garden's 90° polar clamp, so it is camera.limits({ maxPolarDeg: 180 }) then camera.set(undersideCameraPose(config), { durationMs: 0 }), and the clamp returns with camera.limits({ maxPolarDeg: 90 }) when the view is left and on 'camera:control-end', as src/lib/pergola-plugins/pergola-camera-guard-plugin.ts:68-78, 122-127 does; limits are sticky, so nothing re-clamps per frame
Used by: app-378d190f1b, app-4f16d2f6da, app-510d750508, app-ceb4eccc72, app-fb16dd1985.
/**
* `camera-presets.applyPreset(pose)`, `apply(name)` and `save(name)`. 1.4's plugin moved with
* camera-controls' transition while `smoothTransitions` is on: the damped `GLIDE_MS` (`glideMs`).
*/
export function cameraPresets(
session: Session,
saved: Map<string, CameraView>,
glideMs: number,
smoothTransitions = true,
): {
save: (name: string) => void;
apply: (name: string) => Promise<Report>;
applyPreset: (view: V1.CameraView) => Promise<Report>;
underside: (position: V1.Point3, target: V1.Point3) => Promise<Report>;
leaveUnderside: () => Promise<Report>;
dispose: () => void;
} {
const move: CameraMoveOptions = {
durationMs: smoothTransitions ? glideMs : 0,
easing: 'damped',
};
const leaveUnderside = (): Promise<Report> => session.camera.limits({ maxPolarDeg: 90 });
const dispose = session.on('camera:control-end', () => {
void leaveUnderside();
});
return {
save: (name) => {
const { target, azimuthDeg, polarDeg, distance, focalOffset } = session.camera.state;
saved.set(name, { target, azimuthDeg, polarDeg, distance, focalOffset });
},
apply: async (name) => {
await leaveUnderside();
const view = saved.get(name);
return view ? session.camera.set(view, move) : session.camera.views.go(name, move);
},
applyPreset: (view) =>
session.camera.set(
{
target: view.target,
azimuthDeg: view.azimuth,
polarDeg: view.polar,
distance: view.radius,
focalOffset: view.focalOffset,
},
move,
),
underside: async (position, target) => {
await session.camera.limits({ maxPolarDeg: 180 });
return session.camera.set({ position, target }, { durationMs: 0 });
},
leaveUnderside,
dispose,
};
}
dimension.formatValue, patchDimensionLabels, the dimension plugin's options (app-fb16dd1985, app-fc306ec3dc, app-fd09090420) ​replaced · 2.0: Session.overlays, Overlays.dimensions, Query, DimensionOptions.per, DimensionOptions.axes, DimensionOptions.format · written for 1.x 1.5
session.overlays.dimensions(query, { per, axes, format }); a datasheet snap is format: (metres, axis) => …. app-fd09090420's ruler always prints the client's size table for width and depth and measures only height (its 'always override' tolerance, apps/app-fd09090420/src/viewer/page-document.ts:38, 51-76, src/lib/lms/spec-dimensions.ts:1-22 at c4c6ffd2), so it is format: (m, axis) => axis === 'height' ? fmt(m) : fmt(table[axis]), called again on each layout change, where 1.4 re-applied its withRuler document (src/components/viewer/use-viewer-actions.ts:148-158, 179-184)
Used by: app-37356bf09f, app-4fd23bc869, app-822bb9d6aa, app-8675b3225c, app-bcb7ef21da, app-c1384707fc, app-ceb4eccc72, app-e3b47169bc, app-e61a0b42ad, app-e9a33606d2, app-f07e9033d9, app-f3598fad77, app-fb16dd1985, app-fd09090420, configurator-template.
/** `dimension.formatValue`, `patchDimensionLabels` and the ruler options. */
export function dimensionLabels(
session: Session,
query: Query,
table: { width: number; depth: number },
fmt: (metres: number) => string,
): void {
// Call again when the layout or the client's size table changes.
session.overlays.dimensions(query, {
per: 'all',
axes: ['width', 'height', 'depth'],
format: (m, axis) => (axis === 'height' ? fmt(m) : fmt(table[axis])),
});
}
adaptive-zoom.fitToScene(), fitToBox(box) ​replaced · 2.0: Session.camera, Camera.lens, CameraLens.insets, Insets, Camera.fit, CameraMoveOptions.durationMs, Query, CameraMoveOptions.easing · written for 1.x 1.5
session.camera.fit(query, { paddingM, durationMs: fitTransition ? GLIDE_MS : 0, easing: 'damped' }): 1.4's fitToBox(box, fitTransition) moved with camera-controls' transition, fitTransition on by default (viewer/plugins/adaptive-zoom-plugin.ts:187 at eb5ce2afe), so a fit glides as C.3's fit row does (Q20). Per-side fitToBox paddings have no 2.0 form: app-510d750508 frames its crib with 0.3 × its largest size above, left and right and 0.15 × below on every size change (apps/app-510d750508/src/lib/crib-plugins/crib-geometry-plugin.ts:113-124 at c4c6ffd2). The closest is session.camera.lens({ insets }) (CSS px, kept across resizes), which fit frames into, plus fit's one symmetric padding: a framing change for D44. A fit without paddingM keeps a 4% margin each side (5.6, DL36); a port matching 1.4's framing by eye passes its paddingM, 0 for the box exactly
Visual changes to sign off (D44): Per-side fitToBox paddings (app-510d750508's crib) become camera.lens({ insets }) plus one symmetric padding: a framing change..
Used by: app-04c2774145, app-378d190f1b, app-4762caacdc, app-4fd23bc869, app-510d750508, app-822bb9d6aa, app-8675b3225c, app-b362a81c85, app-bcb7ef21da, app-c1384707fc, app-ceb4eccc72, app-e3b47169bc, app-e61a0b42ad, app-e9a33606d2, app-f07e9033d9, app-f3598fad77, app-f9a32142f1, app-fb16dd1985, app-fc306ec3dc, app-fd09090420, configurator-template.
/**
* `adaptive-zoom.fitToScene()` and `fitToBox(box)` with asymmetric padding. 1.4 fitted with
* camera-controls' transition while `fitTransition` is on: the damped `GLIDE_MS` (`glideMs`).
*/
export async function adaptiveZoom(
session: Session,
query: Query,
paddingM: number,
insets: Insets,
glideMs: number,
fitTransition = true,
): Promise<Report> {
// Insets are CSS pixels; this is a framing change from per-side metre paddings.
await session.camera.lens({ insets });
return session.camera.fit(query, {
paddingM,
durationMs: fitTransition ? glideMs : 0,
easing: 'damped',
});
}
PostProcessPlugin.setAO({ aoRadius }) through session.plugin('post-process') (app-b362a81c85 measures the radius once after load and pins it, so later swaps don't measure it again: apps/app-b362a81c85/src/components/viewer/runtime-escape-hatches.ts:130-150, use-viewer-plugins.ts:113-122 at be828d5d) ​replaced · 2.0: Session.collection, CollectionOptions.role, Session.stage, Stage.set, StageSettings, StageWriteOptions.owner · written for 1.x 1.5
session.stage.set({ ambientOcclusion: { radius } }, { owner }) with the measured metres, in a stage layer the app owns (K32); or no measurement, keeping the document's ambientOcclusion: { radius: 'auto' } (role 'product' bounds, StageSettings.ambientOcclusion), as app-b362a81c85's paper port does
Used by: app-b362a81c85.
/**
* app-b362a81c85's `PostProcessPlugin.setAO({ aoRadius })`: the radius it measured, pinned in a stage
* layer the app owns. Without the measurement, the document's `radius: 'auto'` stays.
*/
export function pinAoRadius(session: Session, aoRadius: number): Promise<Report> {
const owner = session.collection('ao-radius', { role: 'scenery' });
return session.stage.set({ ambientOcclusion: { radius: aoRadius } }, { owner });
}
PluginContext render:before ​replaced · 2.0: Session.onFrame, Frame, FrameOptions.phase · written for 1.x 1.5
session.onFrame(cb) (phase: 'late' when it reads animated poses)
Used by: app-4f16d2f6da, app-510d750508, app-59c5b5751e, app-b362a81c85, app-ceb4eccc72, app-f9a32142f1, app-fc306ec3dc.
/** `PluginContext`'s `render:before` reading animated poses. */
export function beforeRender(session: Session, cb: (frame: Frame) => void): () => void {
return session.onFrame(
(frame) => {
cb(frame);
},
{ phase: 'late' },
);
}
PluginContext render:after ​replaced · 2.0: Session.on, Events.committed, Events.camera:changed · written for 1.x 1.5
onFrame in the next frame, or the 'committed' and 'camera:changed' events
Used by: app-4f16d2f6da, app-510d750508, app-59c5b5751e, app-b362a81c85, app-ceb4eccc72.
/** `PluginContext`'s `render:after` updates after commits or camera changes. */
export function afterRender(session: Session, cb: () => void): () => void {
const offCommit = session.on('committed', () => {
cb();
});
const offCamera = session.on('camera:changed', () => {
cb();
});
return () => {
offCommit();
offCamera();
};
}
queueRender(), requestFrame() ​removed · written for 1.x 1.5
writes draw by themselves; onFrame(cb, { continuous: true }) keeps drawing
Used by: app-04c2774145, app-0c773a4ee3, app-378d190f1b, app-4762caacdc, app-4f16d2f6da, app-510d750508, app-59c5b5751e, app-b362a81c85, app-b4ae2fce25, app-bcb7ef21da, app-c1384707fc, app-ceb4eccc72, app-ddfb017681, app-f9a32142f1, app-fc07850558, app-fc306ec3dc.
register(plugin) host plugins (pergola, app-0c773a4ee3, chair, app-510d750508, app-378d190f1b, app-b362a81c85) ​replaced · 2.0: Session.collection, CollectionOptions.role, Collection.set, CollectionSpec, Session.find, Parts.set, PartState.material, MaterialSpec, Session.stage, Stage.set, StageSettings, StageWriteOptions.owner, Session.onFrame, Frame, FrameOptions.signal, Collection.signal, Collection.dispose · written for 1.x 1.5
collections for content, rules for looks, onFrame for per-frame logic, stage.set for lights and environment
Used by: app-0c773a4ee3, app-378d190f1b, app-4f16d2f6da, app-510d750508, app-59c5b5751e, app-b362a81c85, app-b4ae2fce25, app-ceb4eccc72, app-ddfb017681, app-f9a32142f1, app-fc07850558, app-fc306ec3dc.
/** Host `register(plugin)` for content, looks, per-frame logic and stage settings. */
export async function registerContent(
session: Session,
content: CollectionSpec,
query: Query,
material: MaterialSpec,
stage: StageSettings,
cb: (frame: Frame) => void,
): Promise<() => void> {
const owner = session.collection('app-content', { role: 'scenery' });
await owner.set(content);
await session.find(query).set({ material });
await session.stage.set(stage, { owner });
const off = session.onFrame(
(frame) => {
cb(frame);
},
{ signal: owner.signal },
);
return () => {
off();
owner.dispose();
};
}
default-visibility.ts, app-fb16dd1985 tint-body-colour.ts) ​replaced · 2.0: Query · written for 1.x 1.5
Query.name globs over the whole name (C.8)
Used by: app-59c5b5751e, app-e61a0b42ad, app-f07e9033d9, app-fb16dd1985.
/** RegExp visibility/material patterns such as `/body/` and `/^mp-[lr]/`. */
export const defaultVisibilityQuery: Query = [
{ name: '*body*', kind: 'mesh' },
{ name: 'mp-l*', kind: 'mesh' },
{ name: 'mp-r*', kind: 'mesh' },
];
replaced · 2.0: Session.onFrame, Frame.timeMs, Session.find, Query, Parts.list, Part.material, Frame.material, FrameOptions.continuous · written for 1.x 1.5
a continuous hook that writes the colour and the strength each frame: const off = session.onFrame((f) => { t0 ??= f.timeMs; for (const m of new Set(session.find(partMeshes(part).map((name) => ({ name, kind: 'mesh' as const }))).list().map((p) => p.material))) if (m) f.material(m, { emissive: '#8a6a3a', emissiveStrength: pulseIntensity(spec, f.timeMs - t0) }); }, { continuous: true }), then off() at durationMs, which releases both values (5.12). The GLB's authored emissive is black, so a strength alone draws nothing; app-e61a0b42ad writes both and restores both (apps/app-e61a0b42ad/src/components/viewer/runtime-escape-hatches.ts:425-459, src/lib/shoe-scene/highlight.ts:17 at c4c6ffd2). The Materials are read every frame, since a commit may give a part a different one (Part.material), and the reduced-motion flash (300 ms, peak 0.3) stays the app's (Q6). The material is shared by every part that wears it, so those parts pulse with it, as 1.4's writes to the shared instance did
Used by: no app in the cross-check.
/** An emissive pulse every frame, restoring both authored colour and strength on removal. */
export function emissivePulse(
session: Session,
meshNames: readonly string[],
durationMs: number,
pulseIntensity: (elapsedMs: number) => number,
): () => void {
let t0: number | undefined;
const off = session.onFrame(
(f) => {
t0 ??= f.timeMs;
for (const m of new Set(
session
.find(meshNames.map((name) => ({ name, kind: 'mesh' as const })))
.list()
.map((p) => p.material),
)) {
if (m)
f.material(m, {
emissive: '#8a6a3a',
emissiveStrength: pulseIntensity(f.timeMs - t0),
});
}
if (f.timeMs - t0 >= durationMs) off();
},
{ continuous: true },
);
return off;
}
replaced · 2.0: Session.node, Part.follow, Query, FollowOptions.offset · written for 1.x 1.5
follow(target, { offset: 'rest' }), or an onFrame(cb, { phase: 'late' }) hook reading worldPose
Used by: no app in the cross-check.
/** Skeleton or bone polling every frame to keep an attachment at its rest offset. */
export function followSkeleton(
session: Session,
attachmentNodeId: string,
target: Query,
): () => void {
return session.node(attachmentNodeId).follow(target, { offset: 'rest' });
}
GLTFExporter on the scene (lib/ar/export.ts, app-f07e9033d9 export-scene.ts, app-510d750508 crib export (dormant: no mounted control calls it at c4c6ffd2, so its port deletes it, or ports it to export if the control returns), app-4f16d2f6da pergola-ar/export.ts, app-fc07850558 lib/ar/export.ts:34-78, re-read at 5cdd2780: live since 096bdd556, the kitchen units and generated parts on a skeleton-safe clone) ​replaced · 2.0: Session.permits, Session.export, ExportOptions.format, ExportOptions.query, ExportOptions.prepare, ExportOptions.arQuickLook, ExportResult.blob, Session.activateAR, ActivateAROptions.glbUrl, ActivateAROptions.usdzUrl, ActivateAROptions.title, ActivateAROptions.scale, ActivateAROptions.occlusion, Session.supports, ActivateARReport · written for 1.x 1.5
session.export({ format, query, prepare, arQuickLook }) behind permits('export'); indexing every primitive (app-4f16d2f6da's indexGeometries) and un-mirroring mirrored parts are now the runtime's job; an uploaded GLB (glbUrl) and, in Safari, the in-page USDZ export's blob: URL (usdzUrl, made with the app's prepare) launch through activateAR({ glbUrl, usdzUrl, title }) instead of an app-built intent and Quick Look link; app-4f16d2f6da adds the experimental scale: 'fixed' and occlusion: false (9.5)
Used by: app-4f16d2f6da, app-510d750508, app-ceb4eccc72, app-f07e9033d9, app-fb16dd1985, app-fc07850558.
/** Live-scene `GLTFExporter`, app preparation and uploaded GLB plus Safari USDZ AR launch. */
export async function exportForAr(
session: Session,
query: Query,
prepare: NonNullable<ExportOptions['prepare']>,
upload: (blob: Blob) => Promise<string>,
title: string,
): Promise<{ launch: ActivateARReport; release: () => void } | null> {
if (!session.permits('export')) return null;
const glb = await session.export({ format: 'glb', query, prepare, arQuickLook: false });
const glbUrl = await upload(glb.blob);
const usdz = await session.export({ format: 'usdz', query, prepare, arQuickLook: true });
const usdzUrl = URL.createObjectURL(usdz.blob);
try {
const launch = await session.activateAR({
glbUrl,
usdzUrl,
title,
...(session.supports('activateAR.scale.fixed') ? { scale: 'fixed' as const } : {}),
...(session.supports('activateAR.occlusion') ? { occlusion: false } : {}),
});
return { launch, release: () => URL.revokeObjectURL(usdzUrl) };
} catch (error) {
URL.revokeObjectURL(usdzUrl);
throw error;
}
}
usdz-from-glb.ts:23-27) ​replaced · 2.0: Session.permits, Session.export, ExportOptions.format, ExportResult.blob, Session.activateAR, ActivateAROptions.usdzUrl, ActivateARReport · written for 1.x 1.5
session.export({ format: 'usdz' }), whose blob: URL activateAR({ usdzUrl }) takes in Safari, or a prebuilt USDZ asset passed as activateAR({ usdzUrl }); with neither, activateAR() exports the committed product for Quick Look itself
Used by: app-59c5b5751e, app-e61a0b42ad, app-fb16dd1985.
/** app-fb16dd1985's client GLB-to-USDZ conversion for Safari Quick Look. */
export async function usdzFromProduct(
session: Session,
): Promise<{ launch: ActivateARReport; release: () => void } | null> {
if (!session.permits('export')) return null;
const { blob } = await session.export({ format: 'usdz' });
const usdzUrl = URL.createObjectURL(blob);
try {
const launch = await session.activateAR({ usdzUrl });
return { launch, release: () => URL.revokeObjectURL(usdzUrl) };
} catch (error) {
URL.revokeObjectURL(usdzUrl);
throw error;
}
}
lib/ar/quick-look.ts and lib/ar/platform.ts, triggerIOSQuickLook and the Scene Viewer intent builders (app-510d750508 crib-ar/export.ts, dormant, pergola and app-4f16d2f6da pergola-ar/export.ts, app-fc07850558 lib/ar/export.ts), app-fd09090420 lib/lms/ar.ts, the device checks in the template's viewer-toolbar.tsx copies (at c4c6ffd2) ​replaced · 2.0: Session.info, InfoReport.ar, Session.activateAR, ActivateAROptions.glbUrl, ActivateAROptions.usdzUrl, ActivateAROptions.title, ActivateARReport · written for 1.x 1.5
activateAR({ glbUrl, usdzUrl, title }), which picks Quick Look, Scene Viewer or WebXR per device; info().ar.modes decides whether the AR control shows; the template's components/ar/ar-button.tsx copies are unimported dead code (11.1)
Used by: app-04c2774145, app-37356bf09f, app-4762caacdc, app-4f16d2f6da, app-4fd23bc869, app-510d750508, app-822bb9d6aa, app-8675b3225c, app-b362a81c85, app-bcb7ef21da, app-c1384707fc, app-ceb4eccc72, app-e3b47169bc, app-e9a33606d2, app-f07e9033d9, app-f3598fad77, app-f9a32142f1, app-fb16dd1985, app-fc07850558, app-fc306ec3dc, app-fd09090420, configurator-template.
/** `triggerIOSQuickLook`, Scene Viewer intents and device checks for the AR control. */
export function triggerQuickLook(
session: Session,
glbUrl: string,
usdzUrl: string,
title: string,
): Promise<ActivateARReport> | null {
if (session.info().ar.modes.length === 0) return null;
return session.activateAR({ glbUrl, usdzUrl, title });
}
lib/watermark/screenshot.ts canvas reads through shadowRoot (9 apps) ​removed · written for 1.x 1.5
dead code; session.capture() where a screenshot is needed
Used by: no app in the cross-check.
captureBlob({ pathTracing }) and preparePathTracing() (no app) ​removed · written for 1.x 1.5
no user: no app passes pathTracing or calls preparePathTracing at c4c6ffd2 (3.1 rule 3), so the hero-shot chunk stays out of the 2.0 entry; session.capture() is the raster path (viewer/plugins/screenshot-plugin.ts:46-53, 238 at eb5ce2afe)
Used by: no app in the cross-check.
saturate() on the canvas plus a capture re-filter (capture-saturation.ts, 19 copies) ​replaced · 2.0: ViewerDocumentJson, ViewerDocumentJson.schema, ViewerDocumentJson.version, ViewerDocumentJson.stage, StageSettings.grade · written for 1.x 1.5
the page document's stage.grade.saturation (BootOptions.document), as the ports set it (app-fc306ec3dc src/viewer/page-document.ts:30-34); drop the CSS filter from captured images too, or they saturate twice. The grade is the same saturate() matrix on the encoded output, so the value moves unchanged (StageSettings.grade)
Used by: app-04c2774145, app-0c773a4ee3, app-4762caacdc, app-4f16d2f6da, app-4fd23bc869, app-510d750508, app-59c5b5751e, app-822bb9d6aa, app-8675b3225c, app-b362a81c85, app-bcb7ef21da, app-c1384707fc, app-ceb4eccc72, app-e3b47169bc, app-e61a0b42ad, app-e9a33606d2, app-f07e9033d9, app-f9a32142f1, app-fb16dd1985, app-fc306ec3dc, app-fd09090420, configurator-template.
/** CSS `saturate()` and the duplicate capture filter move into the boot page document. */
export function saturatedPageDocument(saturation: number): ViewerDocumentJson {
return {
schema: 'holo.viewer-document',
version: 2,
stage: { grade: { saturation } },
};
}
isInteracting() (new at aa80b3e3) ​replaced · 2.0: Session.on, Events.camera:control-start, Events.camera:control-end · written for 1.x 1.5
a flag the app keeps from 'camera:control-start' and 'camera:control-end'
Used by: app-fc306ec3dc.
/** app-fc306ec3dc `isInteracting()` from the camera's control-start/control-end events. */
export function interactionFlag(session: Session): {
isInteracting: () => boolean;
dispose: () => void;
} {
let interacting = false;
const start = session.on('camera:control-start', () => {
interacting = true;
});
const end = session.on('camera:control-end', () => {
interacting = false;
});
return {
isInteracting: () => interacting,
dispose: () => {
start();
end();
},
};
}
lookAt(position, target, transition) (new at aa80b3e3) ​replaced · 2.0: Session.camera, Camera.set, LookAtView.position, LookAtView.target, CameraMoveOptions.durationMs, CameraMoveOptions.easing · written for 1.x 1.5
session.camera.set({ position, target }, { durationMs: transition ? GLIDE_MS : 0, easing: 'damped' }) (Q20); a LookAtView clears the focal offset in the same write
Used by: app-04c2774145, app-4762caacdc, app-4f16d2f6da, app-510d750508, app-b4ae2fce25, app-bcb7ef21da, app-ceb4eccc72, app-ddfb017681, app-fc07850558, app-fc306ec3dc.
/** app-fc306ec3dc `lookAt(position, target, transition)` with the app's `GLIDE_MS`. */
export function lookAt(
session: Session,
position: V1.Point3,
target: V1.Point3,
transition: boolean,
glideMs: number,
): Promise<Report> {
return session.camera.set(
{ position, target },
{ durationMs: transition ? glideMs : 0, easing: 'damped' },
);
}
apply-fabric-uv-repeat.ts, app-0c773a4ee3 anisotropy.ts, app-b362a81c85 anisotropy.ts (C.7's textureOptions row), app-e61a0b42ad repairTextureSampling) ​replaced · 2.0: Session.find, Query, Parts.set, PartState.material, MaterialSpec.maps, TextureSpec.scale, TextureSpec.url · written for 1.x 1.5
a URL-less TextureSpec: find(q).set({ material: { maps: { baseColor: { scale } } } }) (R9), over the GLB's or a select()'s maps (app-f3598fad77, app-0c773a4ee3, app-b362a81c85). app-e61a0b42ad's maps are its leather rule's own urls (apps/app-e61a0b42ad/src/lib/shoe-scene/materials.ts:49-62 at c4c6ffd2), which R9 places by their own fields, so its repair ports to scale: 1.13 on each TextureSpec that rule brings, never a URL-less rule: its repeat wrap, sRGB albedo and flipY: false are the 2.0 defaults (wrap 'repeat', colorSpace 'srgb' for colour slots, the glTF orientation of 5.4)
Used by: app-59c5b5751e, app-e61a0b42ad, app-f3598fad77.
/** `apply-fabric-uv-repeat.ts` and sampling repairs on authored or rule-owned maps. */
export function applyFabricUvRepeat(
session: Session,
query: Query,
scale: number,
leatherUrls?: { baseColor: string; normal: string; roughness: string },
): Promise<Report> {
if (leatherUrls) {
return session.find(query).set({
material: {
maps: {
baseColor: { url: leatherUrls.baseColor, scale: 1.13 },
normal: { url: leatherUrls.normal, scale: 1.13 },
roughness: { url: leatherUrls.roughness, scale: 1.13 },
},
},
});
}
return session.find(query).set({ material: { maps: { baseColor: { scale } } } });
}
aoMap = null on baked AO (app-e61a0b42ad dropBakedAO) ​replaced · 2.0: Session.find, Query, Parts.set, PartState.material, MaterialSpec.maps, TextureSlot · written for 1.x 1.5
maps: { occlusion: 'none' }
Used by: app-e61a0b42ad.
/** `aoMap = null` to drop baked ambient occlusion. */
export function dropBakedAo(session: Session, query: Query): Promise<Report> {
return session.find(query).set({ material: { maps: { occlusion: 'none' } } });
}
depthWrite: false with transparent: true and a renderOrder (app-fc07850558's selection brackets and cut-away ghost colour pass, app-fc306ec3dc's selection brackets, app-4f16d2f6da's opacity glass) ​replaced · 2.0: Session.find, Query, Parts.set, PartState.material, MaterialSpec.alphaMode, MaterialSpec.alpha, MaterialSpec.labs · written for 1.x 1.5
alphaMode: 'blend' (also at alpha 1), which writes no depth, with labs.drawOrder for the order (5.4, K13)
Used by: no app in the cross-check.
/** `depthWrite: false`, `transparent: true` and `renderOrder`. */
export function transparentPass(
session: Session,
query: Query,
drawOrder: number,
): Promise<Report> {
return session.find(query).set({
material: {
alphaMode: 'blend',
alpha: 1,
labs: { drawOrder },
},
});
}
polygonOffset on a fixture coplanar with the wall's faces (app-fc07850558's door and window casings, room-fixtures.ts:66-87 at c4c6ffd2) ​replaced · 2.0: Session.collection, CollectionOptions.role, Collection.set, CollectionSpec.items, ItemSpec, ItemBase.at, PoseInput, Shape, vec3 · written for 1.x 1.5
the app builds the casing a millimetre or two off the wall's jamb and header planes; MaterialSpec has no polygon offset (one app, 3.1 rule 3; K13)
Used by: app-fc07850558.
/** Casing `polygonOffset` becomes geometry placed two millimetres off each wall plane. */
export function offsetCasing(
session: Session,
faces: readonly { key: string; position: V1.Point3; normal: V1.Point3; size: V1.Point3 }[],
): Promise<CollectionReport> {
return session.collection('casings', { role: 'scenery' }).set({
items: faces.map((face) => ({
key: face.key,
kind: 'mesh',
shape: { kind: 'box', size: face.size },
at: { position: vec3.add(face.position, vec3.scale(vec3.normalize(face.normal), 0.002)) },
})),
});
}
scene.scale.x = -1 (app-b362a81c85, app-0c773a4ee3) ​replaced · 2.0: Session.labs, Labs.mirror, Collection · written for 1.x 1.5
session.labs.mirror('x', { owner }). The material.side flip that went with it (DoubleSide mirrored, FrontSide otherwise, on every material: apps/app-b362a81c85/src/lib/hooks/use-orientation-mirror.ts:14-15, apps/app-0c773a4ee3/src/lib/configurator-3d/orientation-mirror.ts:21 at be828d5d) was a winding workaround and goes, so authored sides stay; both orientations' backface views are one item of the port's G5 diff (D44)
Visual changes to sign off (D44): The material.side flip goes, so authored sides stay; both orientations' backface views are one item of the port's G5 diff..
Used by: no app in the cross-check.
/** `scene.scale.x = -1`, keeping the authored material sides. */
export function mirrorScene(session: Session, owner: Collection): Promise<Report> {
return session.labs.mirror('x', { owner });
}
model:loaded and scene:synced (app-b362a81c85 use-fireplace-trim.ts, new at 342b26ce) ​removed · written for 1.x 1.5
the fixed chimney GLBs (G54); the event re-run and the userData.nodeId walk go with it
Used by: app-04c2774145, app-0c773a4ee3, app-37356bf09f, app-4762caacdc, app-4fd23bc869, app-822bb9d6aa, app-8675b3225c, app-b362a81c85, app-bcb7ef21da, app-c1384707fc, app-e3b47169bc, app-e61a0b42ad, app-e9a33606d2, app-f07e9033d9, app-f3598fad77, app-f9a32142f1, app-fb16dd1985, app-fc306ec3dc, app-fd09090420, configurator-template.
session.viewer.renderer.localClippingEnabled and material clippingPlanes (app-fc07850558 runtime-escape-hatches.ts:230-240, scene-content.ts:1071-1105 at 5cdd2780) ​removed · written for 1.x 1.5
1.x already turns local clipping on (viewer/controllers/renderer.controller.ts:670 at eb5ce2afe); in 2.0 the split is app geometry (B.5 G57)
Used by: app-fc07850558.
bind-pose-raycast.ts) ​removed · written for 1.x 1.5
a runtime fix (K45); delete the patch and its three-mesh-bvh dependency
Used by: app-59c5b5751e, app-bcb7ef21da, app-e61a0b42ad.
hideMountHelpers(root) and hideSceneMountHelpers(session) on every 'scene:synced' (src/lib/lms/mount-helpers.ts:25-44, src/components/viewer/runtime-escape-hatches.ts:129-137, 152 at c4c6ffd2) ​runtime-owned · written for 1.x 1.5
the runtime hides every connector plane itself (6.12); delete the traversal, mount-helpers.ts and their hatch calls (K58)
Used by: app-fd09090420.
pick-proxy.ts) ​removed · written for 1.x 1.5
the hover cost they worked around (a 151,712-triangle skinned upholstery mesh, apps/app-fd09090420/src/lib/lms/pick-proxy.ts:1-41 at c4c6ffd2) is the runtime fix of K45: every stock skinned mesh takes the baked-pose pick (viewer/core/raycast-utils.ts:92-105 at eb5ce2afe). Delete pick-proxy.ts and refreshScenePickProxies, pick the real meshes and map hit.part.node to the module's slot (a collection item's node is null, so a transparent item can't). Only if app-fd09090420's hover bar (G25) fails: a transparent pickable item carrying the slot id in meta, read from hit.part.meta
Used by: app-59c5b5751e, app-e61a0b42ad, app-fd09090420.
accessory-grab.ts, new at 342b26ce) ​removed · written for 1.x 1.5
build the accessories as space: 'parent' children of their vanity in the app's scene model and register input.drag([{ node: vanityId }], …): the drag unit (5.7) is the vanity; map hit.part.node to its owner for tap and hover
Used by: app-59c5b5751e, app-e61a0b42ad, app-fc306ec3dc.
getAllAnimations(), getNodeAnimations(nodeId) and playAllAnimations() (app-4762caacdc, app-04c2774145, app-b362a81c85 and app-0c773a4ee3 list clips; app-b362a81c85 plays them all) ​replaced · 2.0: Session.animations, Animations.list, ClipInfo.durationMs, ClipInfo.playing, ClipRef.node, ClipRef.clip, Animations.play, PlayOptions.toggle · written for 1.x 1.5
session.animations.list(target?): a ClipInfo per clip, 1.x's nodeId, name, duration (seconds) and isPlaying read as node, clip, durationMs and playing; play all is session.animations.play(c.node, c.clip, { toggle: true }) for each listed clip with a node, as 1.x's playAll({ toggle: true }) (viewer/configurator-viewer.ts:1658-1688 at eb5ce2afe)
Used by: app-04c2774145, app-0c773a4ee3, app-4762caacdc, app-b362a81c85.
/**
* Viewer element `getAllAnimations()` and `getNodeAnimations(nodeId)` in 1.x's shape (`duration`
* in seconds), and `playAllAnimations()`: each clip toggles, as 1.x's `playAll({ toggle: true })`.
*/
export function listAnimations(
session: Session,
nodeId?: string,
): Array<{ nodeId: string | null; name: string; duration: number; isPlaying: boolean }> {
return session.animations.list(nodeId).map((c) => ({
nodeId: c.node,
name: c.clip,
duration: c.durationMs / 1000,
isPlaying: c.playing,
}));
}
export function playAllAnimations(session: Session): Promise<Report[]> {
const plays: Promise<Report>[] = [];
for (const c of session.animations.list()) {
if (c.node) plays.push(session.animations.play(c.node, c.clip, { toggle: true }));
}
return Promise.all(plays);
}
getModelPRS(nodeId) (app-c1384707fc, app-f3598fad77, app-f9a32142f1) ​replaced · 2.0: Session.node, Part.exists, Part.pose, Pose.rotationDeg, angle · written for 1.x 1.5
session.node(nodeId).pose: position and scale as they were, rotationDeg in degrees where 1.x gave Euler radians in the same intrinsic X, Y, Z order (angle.degToRad); 1.x threw for a node not loaded, where node(id).exists is false
Used by: app-c1384707fc, app-f3598fad77, app-f9a32142f1.
/** Viewer element `getModelPRS(nodeId)`: 1.x's Euler radians (XYZ) from the pose's degrees. */
export function getModelPrs(
session: Session,
nodeId: string,
): { position: V1.Point3; rotation: V1.Point3; scale: V1.Point3 } | null {
const root = session.node(nodeId);
if (!root.exists) return null; // 1.x threw "Model not found"
const { position, rotationDeg, scale } = root.pose;
return {
position: { x: position.x, y: position.y, z: position.z },
rotation: {
x: angle.degToRad(rotationDeg.x),
y: angle.degToRad(rotationDeg.y),
z: angle.degToRad(rotationDeg.z),
},
scale: { x: scale.x, y: scale.y, z: scale.z },
};
}
setOrbitTarget(target, enableTransition) (use-planner-viewer.ts:1295 at be828d5d) ​replaced · 2.0: Session.camera, Camera.state, CameraState.position, Camera.set, LookAtView.position, LookAtView.target, CameraMoveOptions.durationMs, CameraMoveOptions.easing · written for 1.x 1.5
session.camera.set({ position, target }, { durationMs: enableTransition ? GLIDE_MS : 0, easing: 'damped' }) with position from camera.state: 1.x moved only the pivot and kept the camera where it was (viewer/controllers/camera.controller.ts:218-225 at eb5ce2afe)
Used by: app-fc306ec3dc.
/** app-fc306ec3dc `setOrbitTarget(target, enableTransition)`: the camera stays where it is, the pivot moves. */
export function setOrbitTarget(
session: Session,
target: V1.Point3,
enableTransition: boolean,
glideMs: number,
): Promise<Report> {
const { x, y, z } = session.camera.state.position;
return session.camera.set(
{ position: { x, y, z }, target },
{ durationMs: enableTransition ? glideMs : 0, easing: 'damped' },
);
}
adaptive-zoom.recalculateZoomLimits() (app-fc306ec3dc runtime-escape-hatches.ts:363 at be828d5d) ​runtime-owned · written for 1.x 1.5
while no document layer and no limits call sets minDistance or maxDistance, the runtime refits both to the role 'product' bounds at every commit that changes them, 1.x's formula (5.6), and a distance set by a layer or a limits call stops that refit (limits are sticky). A call that recomputes them after a layout change: delete it. app-fc306ec3dc never recomputes: it replaces it with a no-op (pinAdaptiveZoomLimits) so the plugin never rewrites its room camera's distances. In 2.0 the room camera's own camera.limits({ minDistance, maxDistance }) already stops the refit, so delete the pin and add no product-based limits call: one would override the room camera's distances. The formula is for only a page that wants 1.x's adaptive limits and also sets a distance itself, after each layout change: session.camera.limits({ minDistance: Math.max(0.1, 0.7 * d), maxDistance: 10 * d }), d the largest side of session.find({ role: 'product' }).bounds()
Used by: app-378d190f1b, app-510d750508, app-822bb9d6aa, app-c1384707fc, app-ceb4eccc72, app-e3b47169bc, app-fc306ec3dc.
loadModel(url, nodeId) and removeModel(nodeId) for a catalogue preview (app-fc306ec3dc components/planner/catalogue-preview.ts:157-185 at be828d5d) ​replaced · 2.0: Session.collection, CollectionOptions.role, CollectionOptions.signal, Collection.load, Collection.dispose, Session.find, Parts.set, PartState.material · written for 1.x 1.5
a 'helper' collection (spec 8.5): collection('catalogue-preview', { role: 'helper', signal }).load(url, { key, signal }), dressed by a rule on find({ collection, kind: 'mesh' }), removed with the collection's dispose(); no owned copy and no scene graph walk
Used by: app-fc306ec3dc.
/**
* Viewer element `loadModel(url, nodeId)` then `removeModel(nodeId)` for a catalogue preview
* (app-fc306ec3dc): a helper collection's item, dressed by a rule, removed with the collection.
*/
export async function catalogPreview(
session: Session,
url: string,
finish: MaterialSpec,
signal: AbortSignal,
): Promise<{ part: Part; dispose: () => void }> {
// the preview's lifecycle: an abort disposes the collection, as `dispose()` does
const preview = session.collection('catalogue-preview', { role: 'helper', signal });
const part = await preview.load(url, { key: 'preview', signal });
// the room's finish on the preview: a rule on the collection's meshes
await session.find({ collection: preview.name, kind: 'mesh' }).set({ material: finish });
return { part, dispose: () => preview.dispose() };
}
drag:move, setMode('ground') and setMode('plane') with setPlaneFromMesh(mesh), isDragging (use-planner-drag.ts:629-630, 1348, runtime-escape-hatches.ts:255-277 at be828d5d; pergola's and app-bcb7ef21da's own setMode are their own objects) ​replaced · 2.0: Session.input, Input.drag, DragOptions.on, DragOptions.constrain, DragMove, Session.on, Events.drag:start, Events.drag:end · written for 1.x 1.5
session.input.drag(units, { on: 'floor' }) for 'ground' and { on: { name: wallMesh, kind: 'mesh' } } for a plane from a mesh, registered again when the mode changes (the newest registration that names a unit applies); each move reaches constrain as a DragMove; isDragging is a flag the app keeps from 'drag:start' and 'drag:end'
Used by: app-fc306ec3dc.
/**
* app-fc306ec3dc's drag plugin: `setMode('ground')` drags on the floor, `setMode('plane')` with
* `setPlaneFromMesh(wall)` on a wall mesh, `'drag:move'` per move, `isDragging` from the drag
* events. Register again with the other `on` when the mode changes (the newest registration
* that names a unit applies).
*/
export function plannerDrag(
session: Session,
units: Query,
wallMesh: string | null,
onMove: (move: DragMove) => void,
signal: AbortSignal,
): { readonly isDragging: boolean } {
let dragging = false;
session.input.drag(units, {
on: wallMesh ? { name: wallMesh, kind: 'mesh' } : 'floor',
constrain: (move) => {
onMove(move);
return null; // the move as the runtime computed it
},
signal,
});
session.on('drag:start', () => (dragging = true), { signal });
session.on('drag:end', () => (dragging = false), { signal });
return {
get isDragging() {
return dragging;
},
};
}
material-swap.restoreOriginals() (app-bcb7ef21da runtime-escape-hatches.ts:37, app-fc306ec3dc) ​replaced · 2.0: Session.find, Parts.set, PartState.material · written for 1.x 1.5
session.find(q).set({ material: null }) on each region's rule: the rule's material goes and the part wears its authored one again (R10; C.7's plugins.global-material row)
Used by: app-37356bf09f, app-822bb9d6aa, app-8675b3225c, app-bcb7ef21da, app-c1384707fc, app-e3b47169bc, app-e61a0b42ad, app-e9a33606d2, app-f07e9033d9, app-fc306ec3dc, configurator-template.
/** Material-swap `restoreOriginals()`: each region's rule drops its material (R10). */
export function restoreOriginals(session: Session, regions: readonly Query[]): Promise<Report[]> {
return Promise.all(regions.map((q) => session.find(q).set({ material: null })));
}
material-swap.getGroups(), registerGroup(group) and selectMaterial(groupId, materialId) (app-c1384707fc runtime-escape-hatches.ts:184-188 at be828d5d; in the other apps selectMaterial is their own sync hook's field) ​replaced · 2.0: Session.find, Query, Parts.set, PartState.material, MaterialSpec · written for 1.x 1.5
the groups are the app's own list; a pick is one persistent rule on the group's meshes, session.find({ name: <glob>, kind: 'mesh' }).set({ material }) (R10), painted on every match now and after reloads, undo and redo, as app-c1384707fc's port does (src/lib/door/door-materials.ts); a stored MaterialNode pick is session.select(group, material)
Used by: app-37356bf09f, app-822bb9d6aa, app-8675b3225c, app-bcb7ef21da, app-c1384707fc, app-e3b47169bc, app-e61a0b42ad, app-e9a33606d2, app-f07e9033d9, configurator-template.
/**
* app-c1384707fc's material-swap groups (`getGroups`, `registerGroup`, `selectMaterial`): the groups are
* the app's own list, and a pick is one persistent rule on the group's meshes (R10), painted
* on every match now and after reloads, undo and redo.
*/
export function materialGroups(session: Session): {
registerGroup: (group: { id: string; meshGlob: string }) => void;
getGroups: () => ReadonlyArray<{ id: string; meshGlob: string }>;
selectMaterial: (groupId: string, material: MaterialSpec) => Promise<Report> | null;
} {
const groups = new Map<string, { id: string; meshGlob: string }>();
return {
registerGroup: (group) => void groups.set(group.id, group),
getGroups: () => [...groups.values()],
selectMaterial: (groupId, material) => {
const group = groups.get(groupId);
return group ? session.find({ name: group.meshGlob, kind: 'mesh' }).set({ material }) : null;
},
};
}
global-material.applyAll() after each select (app-f9a32142f1 use-viewer.ts:1109-1115 at be828d5d) ​runtime-owned · written for 1.x 1.5
a select applies the group's material and its visibility zone in its own commit (8.2 rule 7, C.7's plugins.global-material row); delete the re-apply and the history snapshot after it
Used by: app-04c2774145, app-37356bf09f, app-4762caacdc, app-4fd23bc869, app-822bb9d6aa, app-8675b3225c, app-b362a81c85, app-bcb7ef21da, app-c1384707fc, app-e3b47169bc, app-e61a0b42ad, app-e9a33606d2, app-f07e9033d9, app-f3598fad77, app-f9a32142f1, app-fb16dd1985, app-fc306ec3dc, app-fd09090420, configurator-template.
scene-history.saveSnapshot() and clear() after a first load that leaked undo steps (app-4762caacdc, app-04c2774145 runtime-escape-hatches.ts:277, app-f9a32142f1) ​removed · written for 1.x 1.5
the session's first model is the floor of history (11.2): nothing to clear; a later load that must start clean passes undo: 'rebase' (C.3's history rows)
Used by: app-04c2774145, app-4762caacdc, app-f9a32142f1.
PluginContext.sceneIndex.getModelObjects(nodeId) (app-f9a32142f1 runtime-escape-hatches.ts:176-177 at be828d5d) ​replaced · 2.0: Session.find, Parts.list, Session.node · written for 1.x 1.5
session.find({ node: nodeId }).list() for the parts, session.node(nodeId) for the model root
Used by: app-f9a32142f1.
/** `PluginContext.sceneIndex.getModelObjects(nodeId)`: the model root and its parts. */
export function modelObjects(session: Session, nodeId: string): Part[] {
return session.find({ node: nodeId }).list();
}
WebGLRenderer.capabilities.maxTextureSize to size frame-sequence decodes (app-b4ae2fce25, app-ddfb017681 runtime-escape-hatches.ts:168-169 at be828d5d) ​replaced · 2.0: Session.info, InfoReport.device · written for 1.x 1.5
session.info().device.maxTextureSize
Used by: no app in the cross-check.
/** `renderer.capabilities.maxTextureSize`, to size frame-sequence decodes (estate, app-ddfb017681). */
export function maxTextureSize(session: Session): number {
return session.info().device.maxTextureSize;
}
quality:tier-changed and viewer element getPerfStats().qualityTier to retune shadows per tier (app-ceb4eccc72, app-4f16d2f6da runtime-escape-hatches.ts:300-310 at be828d5d) ​runtime-owned · written for 1.x 1.5
the runtime sets the shadow map and filter per tier (ShadowSpec, B.4 X11); info().renderer.tier reads the tier where a page shows it; delete the retune
Used by: app-4f16d2f6da, app-ceb4eccc72.
PluginContext.events.emit('model:loaded') so the ambient occlusion and adaptive zoom re-measure a procedural model (app-ceb4eccc72, app-4f16d2f6da emitModelLoaded, runtime-escape-hatches.ts:303-313 at be828d5d) ​runtime-owned · written for 1.x 1.5
a collection write is measured like a model: the 'auto' occlusion radius and camera.fit read the committed scene; delete the emit (C.6's model:loaded row covers listening)
Used by: app-4f16d2f6da, app-510d750508, app-59c5b5751e, app-ceb4eccc72.
Every key the 17 apps' page documents use at aa80b3e3, re-checked at 342b26ce (with app-fd09090420's hero document), plus dimension (set at run time through applyDocument by the template's use-viewer-actions.ts) and camera-collision (app-04c2774145 and app-4762caacdc). It also maps the version-1 keys the viewer added after aa80b3e3 (at 26d38f8b6), and every other key the viewer's schema and plugin manifests declare (viewer/document/viewer-schema.ts, viewer/plugins/manifests/*.ts), because D50 maps every version-1 key and published layers may carry them; the migration test reads the schema from the viewer's source and fails on a key with no row. It has a row for each of the 21 plugin ids in the viewer's manifest list (viewer/plugins/manifests/index.ts:31-53 at eb5ce2afe), because the admin's viewer-defaults form is built from those manifests (services/admin/app/components/viewer-defaults/document-form-model.ts:1-20), and for each option key the page documents set at c4c6ffd2. The host-only recovery settings are not document keys and need no row, nor are viewer.loader.dracoDecoderPath and .ktx2TranscoderPath, which the schema marks 'x-holo-host-only' (viewer/document/viewer-schema.ts:374-383 at eb5ce2afe); 7.5's accepted glTF says where 2.0's decoders come from. "Runtime-owned" means the key may still appear in the layers Holostep publishes, where the runtime reads it; a 2.0 page document that sets it records document-error. The runtime reads version-1 published layers through this same table (8.3).
meta (top level; app-ddfb017681's and estate's documents keep a note in it, apps/app-ddfb017681/src/viewer/page-document.json:4-6 at c4c6ffd2) ​kept · written for 1.x 1.5
meta in the version-2 document, one to one; the runtime ignores it
| Version-1 key | Disposition | 2.0 home | Via |
|---|---|---|---|
meta |
kept | meta |
— |
Used by: no app in the cross-check.
plugins.adaptive-zoom (fitOnModelLoad, fitPadding, adaptOnResize, fitOnSelection, fitOnSelectionClear, fitTransition, selectionPadding, persistState, persistKey) ​removed · written for 1.x 1.5
removed: call camera.fit(query, { paddingM }) after the first scene write; the view adapting to a resize is runtime-owned and refits nothing (5.6), so adaptOnResize: false (app-f07e9033d9) maps to nothing: 1.x's key only re-derived size-based limits a resize leaves alone. A fit on a pick, or back to the product on a miss, is camera.fit in the app's 'click' handler; app-e3b47169bc sets both fitOnSelection keys false, 1.x's default (apps/app-e3b47169bc/src/viewer/page-document.json:30-31 at c4c6ffd2), so its port writes nothing. fitTransition is the durationMs of those calls: on, 1.x's default, a fit moved with camera-controls' transition, so { durationMs: GLIDE_MS, easing: 'damped' } as C.6's adaptive-zoom row, and off durationMs: 0; selectionPadding (how many times the distance that just fits the selection's bounding sphere, 2.5 by default, viewer/plugins/manifests/adaptive-zoom.ts:24-28 at eb5ce2afe) is that fit's paddingM, matched by eye; persistState and persistKey (the camera saved to localStorage at rest and restored at boot, viewer/plugins/adaptive-zoom-plugin.ts:107, 304, 361) are app code: keep the state of 'camera:rest' and write it back with camera.set(view, { durationMs: 0 }) after the first scene write. No page document sets those four at be828d5d
| Version-1 key | Disposition | 2.0 home | Via |
|---|---|---|---|
plugins.adaptive-zoom.options.fitOnModelLoad |
removed | — | camera.fit(query, { paddingM }) after the first scene write |
plugins.adaptive-zoom.options.fitPadding |
removed | — | camera.fit paddingM |
plugins.adaptive-zoom.options.adaptOnResize |
removed | — | nothing: the resize view is runtime-owned |
plugins.adaptive-zoom.options.fitOnSelection |
removed | — | camera.fit in the app's 'click' handler |
plugins.adaptive-zoom.options.fitOnSelectionClear |
removed | — | camera.fit in the app's 'click' handler |
plugins.adaptive-zoom.options.fitTransition |
removed | — | the durationMs of the app's camera.fit: GLIDE_MS with easing 'damped' when on, 0 when off |
plugins.adaptive-zoom.options.selectionPadding |
removed | — | camera.fit paddingM in the app's 'click' handler |
plugins.adaptive-zoom.options.persistState |
removed | — | app code: store the state of 'camera:rest', restore with camera.set |
plugins.adaptive-zoom.options.persistKey |
removed | — | app code: the app's own storage key |
Used by: app-378d190f1b, app-4f16d2f6da, app-4fd23bc869, app-510d750508, app-822bb9d6aa, app-8675b3225c, app-b362a81c85, app-bcb7ef21da, app-c1384707fc, app-ceb4eccc72, app-e3b47169bc, app-e61a0b42ad, app-e9a33606d2, app-f07e9033d9, app-fb16dd1985, app-fc306ec3dc, app-fd09090420.
plugins.adaptive-zoom mobileZoomScale, mobileTruckScale and enabled ​replaced · written for 1.x 1.5
camera.limits.coarsePointerDollyScale and .coarsePointerPanScale, default 1.5 and 0.8 as 1.x, which multiplies the dolly and pan speed by them on a coarse pointer wherever the plugin is on (viewer/plugins/adaptive-zoom-plugin.ts:22-25, 72-75, 112, 319-328 at eb5ce2afe). The fleet layer turns the plugin on for every configurator (viewer/document/default-document.ts:19-28) and no page document sets either key, so with no value phones keep 1.x's feel (app-fb16dd1985 pinches at 1.35 and pans at 1.6). A layer that turns it off (enabled: false: app-4762caacdc, app-378d190f1b, estate, app-04c2774145 and app-ddfb017681, page-document.json at c4c6ffd2) maps to 1 and 1, so app-04c2774145's and app-4762caacdc's own 1.6 touch pan (G62) is not scaled again
| Version-1 key | Disposition | 2.0 home | Via |
|---|---|---|---|
plugins.adaptive-zoom.options.mobileZoomScale |
replaced | camera.limits.coarsePointerDollyScale |
— |
plugins.adaptive-zoom.options.mobileTruckScale |
replaced | camera.limits.coarsePointerPanScale |
— |
plugins.adaptive-zoom.enabled |
replaced | camera.limits.coarsePointerDollyScale |
— |
Used by: app-04c2774145, app-378d190f1b, app-4762caacdc, app-4fd23bc869, app-510d750508, app-59c5b5751e, app-822bb9d6aa, app-8675b3225c, app-b362a81c85, app-b4ae2fce25, app-bcb7ef21da, app-c1384707fc, app-ceb4eccc72, app-ddfb017681, app-e3b47169bc, app-e9a33606d2, app-f07e9033d9, app-fb16dd1985, app-fc07850558, app-fd09090420.
/**
* `plugins.adaptive-zoom` `mobileZoomScale`, `mobileTruckScale`, `enabled` → the coarse-pointer
* scales, 1.x's 1.5 and 0.8 by default, and 1 and 1 where the plugin is off.
*/
export function adaptiveZoomScales(
enabled: boolean,
mobileZoomScale = 1.5,
mobileTruckScale = 0.8,
): ViewerDocumentJson {
return {
schema: 'holo.viewer-document',
version: 2,
camera: {
limits: {
coarsePointerDollyScale: enabled ? mobileZoomScale : 1,
coarsePointerPanScale: enabled ? mobileTruckScale : 1,
},
},
};
}
plugins.camera-collision.enabled and the plugin's own option enabled (app-04c2774145 and app-4762caacdc set the option false, apps/app-04c2774145/src/viewer/page-document.json:36, the same line in app-4762caacdc, at be828d5d) ​replaced · written for 1.x 1.5
camera.limits.collision (CameraLimits): true only where the plugin is on and its option is not false, as 1.x ran it, so app-04c2774145 and app-4762caacdc keep 2.0's default false; the plugin's other keys (bufferDistance, rayCount, coneAngle, slideEnabled, useBVH, viewer/plugins/manifests/camera-collision.ts at eb5ce2afe) are runtime-owned: the clearance stays 1.x's default 0.08 m with sliding on, and no page document sets them at be828d5d
| Version-1 key | Disposition | 2.0 home | Via |
|---|---|---|---|
plugins.camera-collision.enabled |
replaced | camera.limits.collision |
— |
plugins.camera-collision.options.enabled |
replaced | camera.limits.collision |
— |
plugins.camera-collision.options.bufferDistance |
runtime-owned | — | — |
plugins.camera-collision.options.rayCount |
runtime-owned | — | — |
plugins.camera-collision.options.coneAngle |
runtime-owned | — | — |
plugins.camera-collision.options.slideEnabled |
runtime-owned | — | — |
plugins.camera-collision.options.useBVH |
runtime-owned | — | — |
Used by: app-04c2774145, app-4762caacdc, app-f9a32142f1.
/**
* `plugins.camera-collision.enabled` (the plugin's switch) and its option `enabled` →
* `camera.limits.collision`, on only where both are, as 1.x ran it; its other keys are
* runtime-owned.
*/
export function cameraCollision(pluginOn: boolean, optionOn = true): ViewerDocumentJson {
return {
schema: 'holo.viewer-document',
version: 2,
camera: { limits: { collision: pluginOn && optionOn } },
};
}
plugins.camera-presets.presets ​replaced · written for 1.x 1.5
camera.views in the version-2 document, each preset's position and target as a LookAtView; smoothTransitions → { durationMs: GLIDE_MS, easing: 'damped' } on views.go, durationMs: 0 when it is off, as C.6's camera-presets row
| Version-1 key | Disposition | 2.0 home | Via |
|---|---|---|---|
plugins.camera-presets.options.presets |
replaced | camera.views |
— |
plugins.camera-presets.options.smoothTransitions |
replaced | camera.views.go durationMs |
— |
Used by: app-04c2774145, app-378d190f1b, app-4762caacdc, app-510d750508, app-bcb7ef21da, app-ceb4eccc72, app-e3b47169bc, app-fb16dd1985.
/**
* `plugins.camera-presets.options.presets`, 1.x's array of `{ name, position, target }` →
* `camera.views`, each preset under its name as a `LookAtView`.
*/
export function cameraViews(
presets: readonly { name: string; position: Point3; target: Point3 }[],
): ViewerDocumentJson {
const views: Record<string, LookAtView> = {};
for (const { name, position, target } of presets) views[name] = { position, target };
return { schema: 'holo.viewer-document', version: 2, camera: { views } };
}
/** `smoothTransitions` → the damped `GLIDE_MS` (`glideMs`) on `views.go`, or no move time. */
export function goToView(
session: Session,
name: string,
smoothTransitions: boolean,
glideMs: number,
): Promise<Report> {
return session.camera.views.go(name, {
durationMs: smoothTransitions ? glideMs : 0,
easing: 'damped',
});
}
plugins.controls-legend (labels, position, mode, autoHide, hideAfterMs) ​removed · written for 1.x 1.5
removed: no page document sets it at c4c6ffd2; a page that shows gesture hints draws them in its own DOM
| Version-1 key | Disposition | 2.0 home | Via |
|---|---|---|---|
plugins.controls-legend |
removed | — | the page's own DOM |
plugins.controls-legend.options.labels |
removed | — | — |
plugins.controls-legend.options.position |
removed | — | — |
plugins.controls-legend.options.mode |
removed | — | — |
plugins.controls-legend.options.autoHide |
removed | — | — |
plugins.controls-legend.options.hideAfterMs |
removed | — | — |
Used by: no app in the cross-check.
plugins.decal ​removed · written for 1.x 1.5
removed: no page document sets it at c4c6ffd2 (app-bcb7ef21da projects its logos itself, use-logo-layer.ts); a decal is labs.decal (5.15)
| Version-1 key | Disposition | 2.0 home | Via |
|---|---|---|---|
plugins.decal |
removed | — | labs.decal |
Used by: no app in the cross-check.
plugins.dimension (unit, mode, axes, selectedOnly, lineColor, lineWidth, labelBackground, labelColor, overrides: every key of its manifest, viewer/plugins/manifests/dimension.ts:39-78 at eb5ce2afe) ​removed · written for 1.x 1.5
removed: overlays.dimensions(query, { per, axes, format }); unit → format.unit; the label ends in the unit, as 1.x (viewer/plugins/dimension-plugin.ts:697-701 at eb5ce2afe), but 1.x printed centimetres to one decimal, a whole value without '.0' ('212.5 cm'), and format's default decimals: 0 prints '213 cm', a D44 item; overrides (a datasheet snap: a measured length within tolerance of target prints the target, viewer/plugins/dimension-plugin.ts:686-693 at eb5ce2afe; app-fb16dd1985 apps/app-fb16dd1985/src/data/app-fb16dd1985-dimensions.ts:89-105, app-fd09090420 apps/app-fd09090420/src/viewer/page-document.ts:38, 51-76 at c4c6ffd2) → the format function, as C.6's patchDimensionLabels row shows; selectedOnly → the query the app passes; mode → per: 'combined', one box over every tracked model (viewer/plugins/dimension-plugin.ts:28-33 at eb5ce2afe), is the default per: 'all', which the template's measure toggle and its copies send through applyDocument (apps/configurator-template/src/components/viewer/use-viewer-actions.ts:102 at c4c6ffd2), and 'individual', one box per object passed to showFor, is per: 'node' ('part' where supports says no), as app-fc306ec3dc's plan rulers (apps/app-fc306ec3dc/src/viewer/page-document.json:44 at c4c6ffd2, 5.9); the line and label styling (pergola, apps/app-ceb4eccc72/src/viewer/page-document.json:50-61 at c4c6ffd2) → CSS on ::part(ruler) and ::part(ruler-label)
| Version-1 key | Disposition | 2.0 home | Via |
|---|---|---|---|
plugins.dimension |
removed | — | overlays.dimensions(query, { per, axes, format }) |
plugins.dimension.options.unit |
removed | — | DimensionOptions.format unit |
plugins.dimension.options.mode |
removed | — | DimensionOptions.per |
plugins.dimension.options.axes |
removed | — | DimensionOptions.axes |
plugins.dimension.options.selectedOnly |
removed | — | the query the app passes |
plugins.dimension.options.lineColor |
removed | — | CSS on ::part(ruler) |
plugins.dimension.options.lineWidth |
removed | — | CSS on ::part(ruler) |
plugins.dimension.options.labelBackground |
removed | — | CSS on ::part(ruler-label) |
plugins.dimension.options.labelColor |
removed | — | CSS on ::part(ruler-label) |
plugins.dimension.options.overrides |
removed | — | the format function (C.6/dimension.formatValue) |
Visual changes to sign off (D44): 1.x printed centimetres to one decimal ('212.5 cm'); format's default decimals: 0 prints '213 cm'..
Used by: app-37356bf09f, app-4f16d2f6da, app-4fd23bc869, app-822bb9d6aa, app-8675b3225c, app-bcb7ef21da, app-c1384707fc, app-ceb4eccc72, app-e3b47169bc, app-e61a0b42ad, app-e9a33606d2, app-f07e9033d9, app-f3598fad77, app-fb16dd1985, app-fc306ec3dc, app-fd09090420, configurator-template.
plugins.drag (draggable, mode, groundY, collisionCheck) ​removed · written for 1.x 1.5
removed: input.drag(query, { on, constrain }); groundY → on: 'floor'; collision is app code in constrain
| Version-1 key | Disposition | 2.0 home | Via |
|---|---|---|---|
plugins.drag |
removed | — | input.drag(query, { on, constrain }) |
plugins.drag.options.draggable |
removed | — | the query input.drag takes |
plugins.drag.options.mode |
removed | — | input.drag |
plugins.drag.options.groundY |
removed | — | input.drag on: 'floor' |
plugins.drag.options.collisionCheck |
removed | — | app code in constrain |
Used by: app-04c2774145, app-37356bf09f, app-4762caacdc, app-fc306ec3dc.
plugins.environment.intensity ​replaced · written for 1.x 1.5
stage.environment.intensity (StageSettings); without a url it retunes the lower layers' environment, as app-04c2774145's page document sets only the intensity (apps/app-04c2774145/src/viewer/page-document.json:20 at 5cdd2780)
| Version-1 key | Disposition | 2.0 home | Via |
|---|---|---|---|
plugins.environment.options.intensity |
replaced | stage.environment.intensity |
— |
Used by: app-04c2774145, app-0c773a4ee3, app-37356bf09f, app-4762caacdc, app-4fd23bc869, app-822bb9d6aa, app-8675b3225c, app-b362a81c85, app-bcb7ef21da, app-c1384707fc, app-e3b47169bc, app-e61a0b42ad, app-e9a33606d2, app-f07e9033d9, app-f3598fad77, app-f9a32142f1, app-fb16dd1985, app-fc306ec3dc, app-fd09090420, configurator-template.
/** `plugins.environment.intensity` → `stage.environment.intensity`, without a `url`. */
export function environmentIntensity(intensity: number): ViewerDocumentJson {
return { schema: 'holo.viewer-document', version: 2, stage: { environment: { intensity } } };
}
plugins.environment.rotation ​replaced · written for 1.x 1.5
stage.environment.rotationDeg (StageSettings), one to one: 1.x turned the environment by scene.environmentRotation.y in degrees (viewer/plugins/environment-plugin.ts:254, 296 at eb5ce2afe), whose lookup three 0.186.1 turns by the inverse rotation (renderers/webgl/WebGLMaterials.js:245), so it samples the panorama at u = atan2(d.z, d.x) / 2π + 0.5 + yaw / 2π, the 'standard' convention's yaw (BackgroundSpec); without a url it turns the lower layers' environment, as intensity retunes it. No page document sets it at be828d5d
| Version-1 key | Disposition | 2.0 home | Via |
|---|---|---|---|
plugins.environment.options.rotation |
replaced | stage.environment.rotationDeg |
— |
Used by: app-0c773a4ee3, app-4fd23bc869.
/** `plugins.environment.rotation` (degrees) → `stage.environment.rotationDeg`, without a `url`. */
export function environmentRotation(rotation: number): ViewerDocumentJson {
return {
schema: 'holo.viewer-document',
version: 2,
stage: { environment: { rotationDeg: rotation } },
};
}
plugins.environment.backgroundIntensity ​removed · written for 1.x 1.5
removed, never stage.background: { environment }: 1.4's environment plugin wrote only scene.backgroundIntensity and never drew the environment behind the product (viewer/plugins/environment-plugin.ts:256, 298, 370 at eb5ce2afe). The backdrop was the stored backgroundColor, a gradient, a colour or none over the page (viewer/controllers/scene.controller.ts:174-186), whose typed form is BackgroundSpec, so a page without one keeps 'transparent': app-f3598fad77's sofa sits on the page's white (apps/app-f3598fad77/src/viewer/page-document.json:10 at c4c6ffd2). three scales a texture background by that value (WebGLBackground.js, read in three 0.183; unverified at the viewer's 0.186.1), so on a page whose stored background is a gradient 1.4 drew it at this brightness (default 0.5); its port folds the factor into the stops or records the change under D44
| Version-1 key | Disposition | 2.0 home | Via |
|---|---|---|---|
plugins.environment.options.backgroundIntensity |
removed | — | — |
Visual changes to sign off (D44): On a page whose stored background is a gradient, 1.4 drew it at this brightness (default 0.5); the port folds the factor into the stops or records the change..
Used by: app-04c2774145, app-0c773a4ee3, app-37356bf09f, app-4762caacdc, app-4fd23bc869, app-822bb9d6aa, app-8675b3225c, app-b362a81c85, app-bcb7ef21da, app-c1384707fc, app-e3b47169bc, app-e61a0b42ad, app-e9a33606d2, app-f07e9033d9, app-f3598fad77, app-f9a32142f1, app-fb16dd1985, app-fc306ec3dc, app-fd09090420, configurator-template.
plugins.environment.enabled (app-4f16d2f6da, pergola, app-378d190f1b, app-ddfb017681, estate and app-510d750508 set false; app-fc07850558 turned it back on) ​replaced · written for 1.x 1.5
false → stage.environment: 'none'; true → the document's environment, 'studio' unless a layer sets one
| Version-1 key | Disposition | 2.0 home | Via |
|---|---|---|---|
plugins.environment.enabled |
replaced | stage.environment |
— |
Used by: app-0c773a4ee3, app-378d190f1b, app-4f16d2f6da, app-4fd23bc869, app-510d750508, app-59c5b5751e, app-b4ae2fce25, app-ceb4eccc72, app-ddfb017681, app-fc07850558.
/** `plugins.environment.enabled: false` → `stage.environment: 'none'`; `true` writes nothing. */
export function environmentEnabled(enabled: boolean): ViewerDocumentJson {
return enabled
? { schema: 'holo.viewer-document', version: 2 }
: { schema: 'holo.viewer-document', version: 2, stage: { environment: 'none' } };
}
plugins.environment.mode, plugins.environment.hdrUrl ​removed · written for 1.x 1.5
removed: 1.x stored them and never read them (viewer/plugins/manifests/environment.ts:76 at eb5ce2afe), so every tenant got the neutral studio, which is 2.0's 'studio', the runtime's 'default' layer's environment. An HDR an app loaded at run time through the element's loadEnvironment(url) (app-c1384707fc's studio HDR, apps/app-c1384707fc/src/components/viewer/runtime-escape-hatches.ts:198-204 at c4c6ffd2) is stage.environment: { url } in its page document
| Version-1 key | Disposition | 2.0 home | Via |
|---|---|---|---|
plugins.environment.options.mode |
removed | — | — |
plugins.environment.options.hdrUrl |
removed | — | stage.environment: { url } in the page document |
Used by: app-04c2774145, app-0c773a4ee3, app-4762caacdc, app-4f16d2f6da, app-4fd23bc869, app-c1384707fc, app-ceb4eccc72.
plugins.exploded-view ​removed · written for 1.x 1.5
removed: no page document sets it at c4c6ffd2; an explode is the app's pose writes with durationMs (PartWriteOptions)
| Version-1 key | Disposition | 2.0 home | Via |
|---|---|---|---|
plugins.exploded-view |
removed | — | pose writes with durationMs |
Used by: no app in the cross-check.
plugins.global-material, plugins.material-swap, plugins.material.retainOriginalMaterials ​runtime-owned · written for 1.x 1.5
runtime-owned: select and rules always work, a selected MaterialNode's visibility zone included (8.2 rule 7); material-swap.resetUnspecifiedProps has no 2.0 key (C.6's applyPBR row). material.retainOriginalMaterials (1.x kept a mesh's GLB material for global-material's clearMaterialWhenEmpty to put back, viewer/plugins/material-plugin.ts:349 at eb5ce2afe) needs no switch: the 3D file's material is always the bottom layer, so a removed rule or a null shows it again (R1, R5). No page document sets it at be828d5d
| Version-1 key | Disposition | 2.0 home | Via |
|---|---|---|---|
plugins.global-material |
runtime-owned | select |
— |
plugins.material-swap |
runtime-owned | select and rules |
— |
plugins.material-swap.options.resetUnspecifiedProps |
removed | — | C.6/material-swap.applyPBR |
plugins.material.options.retainOriginalMaterials |
runtime-owned | — | — |
Used by: app-04c2774145, app-37356bf09f, app-4762caacdc, app-4fd23bc869, app-59c5b5751e, app-822bb9d6aa, app-8675b3225c, app-bcb7ef21da, app-c1384707fc, app-ceb4eccc72, app-e3b47169bc, app-e61a0b42ad, app-e9a33606d2, app-f07e9033d9, app-f3598fad77, app-f9a32142f1, app-fb16dd1985, app-fc306ec3dc, app-fd09090420, configurator-template.
plugins.material-swap.textureOptions (anisotropy, encoding; app-b362a81c85's Livorno and Loua documents set anisotropy: 8, page-document.livorno.json:42, page-document.loua.json:36 at c4c6ffd2) ​removed · written for 1.x 1.5
removed: 1.4 applied it only to textures the material-swap plugin loads itself, for its groups and applyPBR (viewer/plugins/material-swap-plugin.ts:727-743 at eb5ce2afe), and app-b362a81c85's picks load through the material plugin, so the key changes nothing there. app-b362a81c85's sharpening is its own sweep (components/viewer/anisotropy.ts: level 8 on every map slot, capped by the device), which ports to C.6's sampling row: a URL-less rule (R9) with sampling: { anisotropy: 8 } on each slot it sweeps
| Version-1 key | Disposition | 2.0 home | Via |
|---|---|---|---|
plugins.material-swap.options.textureOptions |
removed | — | — |
plugins.material-swap.options.textureOptions.anisotropy |
removed | — | TextureSpec sampling.anisotropy (C.6/apply-fabric-uv-repeat.ts) |
plugins.material-swap.options.textureOptions.encoding |
removed | — | — |
Used by: app-04c2774145, app-37356bf09f, app-4762caacdc, app-4fd23bc869, app-822bb9d6aa, app-8675b3225c, app-bcb7ef21da, app-c1384707fc, app-e3b47169bc, app-e61a0b42ad, app-e9a33606d2, app-f07e9033d9, app-f3598fad77, app-fb16dd1985, app-fd09090420, configurator-template.
plugins.highlight (color, edgeThickness, edgeOpacity, surfaceOpacity, hoverEnabled, selectionEnabled) ​replaced · written for 1.x 1.5
PartState.outline, written from the app's 'hover' handler on the hovered product's model root (or collection root), as 1.x outlined the model root, and on the selected product (section 12, Q17); color → outline.color; hoverEnabled and selectionEnabled → which of the two the app writes; edgeThickness → outline.widthPx, matched by eye; edgeOpacity and surfaceOpacity have no field: the parts keep their colour, so 1.x's 12% fill in #e8f4fd (the defaults, viewer/plugins/manifests/highlight.ts:10-34 at eb5ce2afe) goes, a visual change each of the four ports signs off in its diff (D44, the 9.5 outline row)
| Version-1 key | Disposition | 2.0 home | Via |
|---|---|---|---|
plugins.highlight |
replaced | PartState.outline |
— |
plugins.highlight.options.color |
replaced | PartState.outline.color |
— |
plugins.highlight.options.edgeThickness |
replaced | PartState.outline.widthPx |
— |
plugins.highlight.options.edgeOpacity |
removed | — | — |
plugins.highlight.options.surfaceOpacity |
removed | — | — |
plugins.highlight.options.hoverEnabled |
removed | — | the app writes the outline from its 'hover' handler |
plugins.highlight.options.selectionEnabled |
removed | — | the app writes the outline on the selected product |
Visual changes to sign off (D44): 1.x's 12% fill in #e8f4fd goes (no edgeOpacity or surfaceOpacity field); each of the four ports signs it off in its diff..
Used by: app-4f16d2f6da, app-510d750508, app-822bb9d6aa, app-ceb4eccc72.
/**
* `plugins.highlight` → `PartState.outline` on the hovered product's model root, written from
* the app's `'hover'` handler: `color` → `outline.color`, `edgeThickness` → `outline.widthPx`
* (by eye). `edgeOpacity` and `surfaceOpacity` have no field.
*/
export function outlineOnHover(
session: Session,
color: ColorValue,
edgeThickness: number,
signal: AbortSignal,
): () => void {
let outlined: string | null = null;
return session.on(
'hover',
({ hit }) => {
const node = hit?.part.node ?? null;
if (node === outlined) return;
if (outlined) void session.node(outlined).set({ outline: null });
if (node) void session.node(node).set({ outline: { color, widthPx: edgeThickness } });
outlined = node;
},
{ signal },
);
}
plugins.hotspot ​runtime-owned · written for 1.x 1.5
runtime-owned: overlays.hotspots; clustering has no 2.0 home (of the 27 page documents at c4c6ffd2, 15 set "hotspot": {} and 12 have none)
| Version-1 key | Disposition | 2.0 home | Via |
|---|---|---|---|
plugins.hotspot |
runtime-owned | overlays.hotspots |
— |
plugins.hotspot.options.clustering |
removed | — | — |
Used by: app-37356bf09f, app-4fd23bc869, app-822bb9d6aa, app-8675b3225c, app-bcb7ef21da, app-c1384707fc, app-e3b47169bc, app-e61a0b42ad, app-e9a33606d2, app-f07e9033d9, app-f3598fad77, app-f9a32142f1, app-fb16dd1985, app-fd09090420, configurator-template.
plugins.material.quality, plugins.material.maxVramBytes ​runtime-owned · written for 1.x 1.5
runtime-owned: the quality tier and its texture budget (B.4 X10). The runtime prefers the optimized twin on every tier, so the originals-first 'high' has no 2.0 home (app-f9a32142f1 moved to 'auto' and a 64 MiB budget at 342b26ce, page-document.ts:20-32). The pilot pins 'high' (apps/app-04c2774145/src/viewer/page-document.json:39, the same line in app-4762caacdc, at c4c6ffd2), so its fabrics show their optimized twins in 2.0: a look change its G5 states include as a fabric close-up at both sizes. The same close-up shows stored metalness, roughness and ORM maps magnified smooth, where 1.x drew them nearest-neighbour (8.2 rule 1)
| Version-1 key | Disposition | 2.0 home | Via |
|---|---|---|---|
plugins.material.options.quality |
runtime-owned | the quality tier |
— |
plugins.material.options.maxVramBytes |
runtime-owned | the quality tier's texture budget |
— |
Visual changes to sign off (D44): The pilot's pinned 'high' shows the optimized twins: a fabric close-up at both sizes in its G5 states, and stored metalness, roughness and ORM maps magnified smooth..
Used by: app-04c2774145, app-4762caacdc, app-59c5b5751e, app-ceb4eccc72, app-f9a32142f1.
plugins.material.progressive, plugins.material.adaptiveTextureBudget (no app sets them; 1.4's defaults are true, viewer/plugins/material-plugin.ts:343-344 at eb5ce2afe) ​runtime-owned · written for 1.x 1.5
runtime-owned: colour maps first, detail maps as late swaps (6.6 rule 7, section 12, Q36); the texture budget follows the quality tier (B.4 X10)
| Version-1 key | Disposition | 2.0 home | Via |
|---|---|---|---|
plugins.material.options.progressive |
runtime-owned | — | — |
plugins.material.options.adaptiveTextureBudget |
runtime-owned | — | — |
Used by: app-04c2774145, app-4762caacdc, app-59c5b5751e, app-ceb4eccc72.
plugins.post-process.ao, .bloom ​replaced · written for 1.x 1.5
stage.ambientOcclusion, stage.bloom. ao.aoRadius → radius (metres or 'auto'); ao.intensity → strength = min(1, intensity / 3) (21 of 27 page documents set 3, so they map to 1; the 1.x default 1.6 maps to 0.53; section 12, Q35); ao.distanceFalloff, .denoiseSamples, .denoiseRadius, .denoiseIterations, .transparencyAware → runtime-owned per tier (B.4 X14)
| Version-1 key | Disposition | 2.0 home | Via |
|---|---|---|---|
plugins.post-process.options.ao |
replaced | stage.ambientOcclusion |
— |
plugins.post-process.options.ao.aoRadius |
replaced | stage.ambientOcclusion.radius |
— |
plugins.post-process.options.ao.intensity |
replaced | stage.ambientOcclusion.strength |
— |
plugins.post-process.options.ao.distanceFalloff |
runtime-owned | — | — |
plugins.post-process.options.ao.denoiseSamples |
runtime-owned | — | — |
plugins.post-process.options.ao.denoiseRadius |
runtime-owned | — | — |
plugins.post-process.options.ao.denoiseIterations |
runtime-owned | — | — |
plugins.post-process.options.ao.transparencyAware |
runtime-owned | — | — |
plugins.post-process.options.bloom |
replaced | stage.bloom |
— |
Used by: app-04c2774145, app-37356bf09f, app-4762caacdc, app-4f16d2f6da, app-4fd23bc869, app-510d750508, app-59c5b5751e, app-822bb9d6aa, app-8675b3225c, app-b362a81c85, app-bcb7ef21da, app-c1384707fc, app-ceb4eccc72, app-e3b47169bc, app-e61a0b42ad, app-e9a33606d2, app-f07e9033d9, app-f9a32142f1, app-fb16dd1985, app-fc07850558, app-fc306ec3dc, app-fd09090420, configurator-template.
/**
* `plugins.post-process.ao`, `.bloom` → `stage.ambientOcclusion`, `stage.bloom`: `aoRadius` →
* `radius`; `intensity` → `strength = min(1, intensity / 3)` (1.x's default 1.6 → 0.53); the
* denoise keys are runtime-owned.
*/
export function ambientOcclusion(
ao: false | { aoRadius?: number; intensity?: number },
bloom: boolean,
): ViewerDocumentJson {
return {
schema: 'holo.viewer-document',
version: 2,
stage: {
ambientOcclusion:
ao === false
? false
: { radius: ao.aoRadius ?? 'auto', strength: Math.min(1, (ao.intensity ?? 1.6) / 3) },
bloom,
},
};
}
plugins.post-process.enabled and the plugin's own option enabled (app-fc07850558 sets the switch, "post-process": { "enabled": true, … }, apps/app-fc07850558/src/components/viewer/page-document.json:12-13 at be828d5d) ​replaced · written for 1.x 1.5
false (either one) → stage.ambientOcclusion: false and stage.bloom: false: 1.x then drew each frame directly, past its effect chain (viewer/plugins/post-process-plugin.ts:766-769 at eb5ce2afe), so no AO and no bloom, while anti-aliasing stays the tier's (B.4 X9); true writes nothing, and the ao and bloom row above maps the options
| Version-1 key | Disposition | 2.0 home | Via |
|---|---|---|---|
plugins.post-process.enabled |
replaced | stage.ambientOcclusion |
— |
plugins.post-process.options.enabled |
replaced | stage.ambientOcclusion |
— |
Used by: app-59c5b5751e, app-b362a81c85, app-ceb4eccc72, app-fc07850558.
/**
* `plugins.post-process.enabled`, or its option `enabled`, `false` → no AO and no bloom: 1.x drew
* past its effect chain. `true` writes nothing; the `ao` and `bloom` row maps the options.
*/
export function postProcessEnabled(enabled: boolean): ViewerDocumentJson {
return enabled
? { schema: 'holo.viewer-document', version: 2 }
: {
schema: 'holo.viewer-document',
version: 2,
stage: { ambientOcclusion: false, bloom: false },
};
}
plugins.post-process.antialias, .multisampling, .lowTierBypass, .beautyFrameEffects, .maxAoPixels ​runtime-owned · written for 1.x 1.5
runtime-owned: the quality tier; the medium and high tiers anti-alias, and the low tier draws one sample with no anti-aliasing for an antialias: 'none' chain, as production since 090cd0ee3 (B.4 X9, DL49). app-b362a81c85 (Livorno, Loua) and app-f9a32142f1 set lowTierBypass: false so the low tier keeps AO (a D44 item, DL22) and, before 090cd0ee3, antialiasing; 2.0's low tier keeps a half-float frame (Q3). Whether the effect chain runs on the idle still (beautyFrameEffects) and the drawing-buffer size above which the AO pass drops to half resolution (maxAoPixels, viewer/plugins/post-process-plugin.ts:279-280, 1102 at eb5ce2afe) follow the tier too (B.4 X14). No page document sets those two at be828d5d
| Version-1 key | Disposition | 2.0 home | Via |
|---|---|---|---|
plugins.post-process.options.antialias |
runtime-owned | — | — |
plugins.post-process.options.multisampling |
runtime-owned | — | — |
plugins.post-process.options.lowTierBypass |
runtime-owned | — | — |
plugins.post-process.options.beautyFrameEffects |
runtime-owned | — | — |
plugins.post-process.options.maxAoPixels |
runtime-owned | — | — |
Used by: app-04c2774145, app-37356bf09f, app-4762caacdc, app-4f16d2f6da, app-4fd23bc869, app-510d750508, app-59c5b5751e, app-822bb9d6aa, app-8675b3225c, app-b362a81c85, app-bcb7ef21da, app-c1384707fc, app-ceb4eccc72, app-e3b47169bc, app-e61a0b42ad, app-e9a33606d2, app-f07e9033d9, app-f9a32142f1, app-fb16dd1985, app-fc07850558, app-fc306ec3dc, app-fd09090420, configurator-template.
plugins.post-process.ownToneMapping ​runtime-owned · written for 1.x 1.5
runtime-owned: the colour pipeline (one tone curve, in every path)
| Version-1 key | Disposition | 2.0 home | Via |
|---|---|---|---|
plugins.post-process.options.ownToneMapping |
runtime-owned | — | — |
Used by: app-04c2774145, app-37356bf09f, app-4762caacdc, app-4f16d2f6da, app-4fd23bc869, app-510d750508, app-59c5b5751e, app-822bb9d6aa, app-8675b3225c, app-b362a81c85, app-bcb7ef21da, app-c1384707fc, app-ceb4eccc72, app-e3b47169bc, app-e61a0b42ad, app-e9a33606d2, app-f07e9033d9, app-f9a32142f1, app-fb16dd1985, app-fc07850558, app-fc306ec3dc, app-fd09090420, configurator-template.
plugins.preload (prefetchTextures, prefetchVariants, preloadActiveGlbs, concurrency, constrainedMaxAssets, quality) ​runtime-owned · written for 1.x 1.5
runtime-owned: readiness. The runtime keeps the 1.4 viewer's prefetch rule (Erik, 2026-10-14: required models first): it starts after the first complete frame, with at most 2 requests in flight, and none while a model the shopper asked for or a picked material's maps load. Phones, tablets and slow links get only the enabled siblings of an active choice, at most 8; Save-Data gets nothing; WebKit uses low-priority fetch() (viewer/plugins/preload-plugin.ts:13-43 at 26d38f8b6). Past those first 8 siblings, the rest of the list stops at 64 MiB per visit on every device (DECIDED, R1-GAP-1: the 1.4 rule took app-04c2774145's and app-4762caacdc's whole 494 MB catalogue on a desktop, which their page documents switch off), so app-f9a32142f1's switch-off (prefetchVariants: false, set against the earlier blanket prefetch) and app-4762caacdc's and app-04c2774145's need no 2.0 key. To fetch one option ahead of the pick, labs.prefetch (G56) replaces its hover and focus preload (configurators 971f858a5)
| Version-1 key | Disposition | 2.0 home | Via |
|---|---|---|---|
plugins.preload |
runtime-owned | readiness |
— |
plugins.preload.options.prefetchTextures |
runtime-owned | — | — |
plugins.preload.options.prefetchVariants |
runtime-owned | — | — |
plugins.preload.options.preloadActiveGlbs |
runtime-owned | — | — |
plugins.preload.options.concurrency |
runtime-owned | — | — |
plugins.preload.options.constrainedMaxAssets |
runtime-owned | — | — |
plugins.preload.options.quality |
runtime-owned | — | — |
Used by: app-04c2774145, app-37356bf09f, app-378d190f1b, app-4762caacdc, app-4f16d2f6da, app-510d750508, app-59c5b5751e, app-b4ae2fce25, app-ceb4eccc72, app-ddfb017681, app-f9a32142f1, app-fc07850558.
plugins.material.enabled, plugins.preload.enabled, plugins.camera-presets.enabled (estate and app-ddfb017681 set all three false, apps/app-b4ae2fce25/src/viewer/page-document.json:10-13 at c4c6ffd2; seven more apps set one or two, app-4762caacdc and app-04c2774145 preload, page-document.json:47) ​removed · written for 1.x 1.5
removed, with nothing to turn off: the scene model's materials always apply (R1) and the prefetch rule is runtime-owned (the row above), so an app with no scene model loads and prefetches nothing and one with a model gets the prefetch rule; camera.views holds only the views the document gives
| Version-1 key | Disposition | 2.0 home | Via |
|---|---|---|---|
plugins.material.enabled |
removed | — | — |
plugins.preload.enabled |
removed | — | — |
plugins.camera-presets.enabled |
removed | — | — |
Used by: app-04c2774145, app-378d190f1b, app-4762caacdc, app-4f16d2f6da, app-510d750508, app-59c5b5751e, app-b4ae2fce25, app-bcb7ef21da, app-ceb4eccc72, app-ddfb017681, app-fb16dd1985, app-fc07850558.
plugins.review-annotation ​removed · written for 1.x 1.5
removed: a review tool of the configurator service's playground (services/configurator/app/routes/playground.tsx); no page document sets it at c4c6ffd2
| Version-1 key | Disposition | 2.0 home | Via |
|---|---|---|---|
plugins.review-annotation |
removed | — | — |
Used by: no app in the cross-check.
plugins.runtime-lod ​runtime-owned · written for 1.x 1.5
runtime-owned: geometry detail follows the quality tier, as the texture budget does (B.4 X10), and LOD swaps stop during a hold (6.6 rule 3); no page document enables it at c4c6ffd2. It changes only the drawn detail: reads, exports and captures use each part's full geometry (B.4 X18)
| Version-1 key | Disposition | 2.0 home | Via |
|---|---|---|---|
plugins.runtime-lod |
runtime-owned | the quality tier |
— |
Used by: no app in the cross-check.
plugins.scene-history.maxHistory, .autoCaptureEvents and .enabled (app-04c2774145 and app-4762caacdc name the plugin with "scene-history": {}, which turns it on, apps/app-04c2774145/src/viewer/page-document.json:40, the same line in app-4762caacdc, at be828d5d) ​runtime-owned · written for 1.x 1.5
runtime-owned: 50 steps on every tier, the value 15 apps set (section 12, Q19; B.4 X15); history is always on, as the 1.x runtime kept the plugin on whatever a document said (rt/floor.ts:5, 17-26 at eb5ce2afe), so .enabled has nothing to write. autoCaptureEvents (viewer events that also took a debounced undo snapshot, none by default, viewer/plugins/scene-history-plugin.ts:102 at eb5ce2afe) is removed: a 2.0 step is a recorded write with its undo label, never a viewer event. No page document sets it at be828d5d
| Version-1 key | Disposition | 2.0 home | Via |
|---|---|---|---|
plugins.scene-history.options.maxHistory |
runtime-owned | — | — |
plugins.scene-history.enabled |
runtime-owned | — | — |
plugins.scene-history.options.autoCaptureEvents |
removed | — | nothing: a step is a recorded write and its undo label |
Used by: app-04c2774145, app-37356bf09f, app-4762caacdc, app-4fd23bc869, app-822bb9d6aa, app-8675b3225c, app-bcb7ef21da, app-c1384707fc, app-e3b47169bc, app-e61a0b42ad, app-e9a33606d2, app-f07e9033d9, app-f3598fad77, app-f9a32142f1, app-fb16dd1985, app-fc306ec3dc, app-fd09090420, configurator-template.
plugins.scene-history.restoreScene (app-fc306ec3dc: false, with maxHistory: 50) ​removed · written for 1.x 1.5
removed: an app that keeps its own undo passes undo: 'rebase' on its scene writes (D49, 11.2)
| Version-1 key | Disposition | 2.0 home | Via |
|---|---|---|---|
plugins.scene-history.options.restoreScene |
removed | — | undo: 'rebase' on the app's scene writes |
Used by: app-04c2774145, app-4762caacdc, app-fc306ec3dc.
plugins.screenshot ​runtime-owned · written for 1.x 1.5
runtime-owned: capture
| Version-1 key | Disposition | 2.0 home | Via |
|---|---|---|---|
plugins.screenshot |
runtime-owned | capture |
— |
Used by: app-04c2774145, app-4762caacdc, app-4f16d2f6da, app-4fd23bc869, app-510d750508, app-822bb9d6aa, app-8675b3225c, app-b362a81c85, app-b4ae2fce25, app-bcb7ef21da, app-c1384707fc, app-ceb4eccc72, app-ddfb017681, app-e3b47169bc, app-e61a0b42ad, app-e9a33606d2, app-f07e9033d9, app-f3598fad77, app-f9a32142f1, app-fb16dd1985, app-fc07850558, app-fc306ec3dc, app-fd09090420, configurator-template.
plugins.snap-connector (debugVisualize) ​runtime-owned · written for 1.x 1.5
runtime-owned: assembly; debugVisualize is a HoloDebug concern
| Version-1 key | Disposition | 2.0 home | Via |
|---|---|---|---|
plugins.snap-connector |
runtime-owned | assembly |
— |
plugins.snap-connector.options.debugVisualize |
runtime-owned | — | — |
Used by: app-04c2774145, app-37356bf09f, app-4762caacdc, app-c1384707fc, app-f9a32142f1.
viewer.ar.enabled, .preferredModes, .allowUnfetchableSceneViewerUrl ​runtime-owned · written for 1.x 1.5
runtime-owned: the account's published layers, read back as info().ar (enabled, and modes in that order, narrowed to this device); activateAR() reports 'disabled'. A page document that sets .preferredModes moves it into its account's published layer at the port (app-fd09090420: webxr first, page-document.json:12 at c4c6ffd2). A version-1 order with 'webxr' first (app-fd09090420) becomes ar.place.android: 'holostep' in that layer, because 2.0 has no second WebXR path (5.11, D64). allowUnfetchableSceneViewerUrl (Scene Viewer launched on a page-scoped blob: URL it cannot fetch, viewer/document/viewer-schema.ts:428 at eb5ce2afe) is removed: activateAR never picks Scene Viewer without an http(s) glbUrl (5.11). No page document sets it at be828d5d
| Version-1 key | Disposition | 2.0 home | Via |
|---|---|---|---|
viewer.ar.enabled |
runtime-owned | info().ar.enabled |
— |
viewer.ar.preferredModes |
runtime-owned | info().ar.modes |
— |
viewer.ar.allowUnfetchableSceneViewerUrl |
removed | — | nothing: activateAR picks Scene Viewer only with an http(s) glbUrl (5.11) |
Used by: app-fd09090420.
ar.place.* (android, ios, autoPlace, androidShare) and ar.native.* (banner, title, cta, cartUrlTemplate), next to viewer.ar.enabled ​runtime-owned · written for 1.x 1.5
runtime-owned: activateAR's route reads them from the account's published layer (5.11); a page document that sets one records document-error (D64, XR §6.3)
| Version-1 key | Disposition | 2.0 home | Via |
|---|---|---|---|
ar.place.android |
runtime-owned | — | — |
ar.place.ios |
runtime-owned | — | — |
ar.place.autoPlace |
runtime-owned | — | — |
ar.place.androidShare |
runtime-owned | — | — |
ar.native.banner |
runtime-owned | — | — |
ar.native.title |
runtime-owned | — | — |
ar.native.cta |
runtime-owned | — | — |
ar.native.cartUrlTemplate |
runtime-owned | — | — |
Used by: app-fc07850558.
xr.tryOn.* (enabled, consent) and xr.ui.* (theme, copy, controls) ​runtime-owned · written for 1.x 1.5
runtime-owned, read by @holo/xr; xr.tryOn.consent is read from the account layer only (D64, XR §6.3)
| Version-1 key | Disposition | 2.0 home | Via |
|---|---|---|---|
xr.tryOn.enabled |
runtime-owned | — | — |
xr.tryOn.consent |
runtime-owned | — | — |
xr.ui.theme |
runtime-owned | — | — |
xr.ui.copy |
runtime-owned | — | — |
xr.ui.controls |
runtime-owned | — | — |
Used by: no app in the cross-check.
viewer.camera.fov ​removed · written for 1.x 1.5
removed: 1.x used it only until the viewer first sized, then derived the vertical field of view from horizontalFov on every resize (viewer/types.ts:139-151, viewer/controllers/camera.controller.ts:511-545 at eb5ce2afe), so the document key maps to nothing. An app that holds a vertical field of view through a hatch on every frame writes camera.lens({ fovDeg }) once, which 2.0 holds across resizes: chair 40 (holdVerticalFov, apps/app-59c5b5751e/src/scene/chairPlugin.ts:148, 158), app-378d190f1b 42 (setHorizontalFov(horizontalFovFor(aspect)), apps/app-378d190f1b/src/three/pergola-scene-plugin.ts:47-48, 76-83, src/three/camera.ts:32) and app-e3b47169bc 45 once its own-renderer preview moves to a session (apps/app-e3b47169bc/src/components/viewer/glb-preview.tsx:236, all at c4c6ffd2)
| Version-1 key | Disposition | 2.0 home | Via |
|---|---|---|---|
viewer.camera.fov |
removed | — | camera.lens({ fovDeg }) once, for an app that held a vertical field of view |
Used by: app-59c5b5751e.
viewer.camera.horizontalFov ​replaced · written for 1.x 1.5
camera.lens.horizontalFovDeg in the version-2 document; no page document sets it at c4c6ffd2, and the default lens holds 1.x's 60° horizontal, except that a fit on a portrait element frames at 45° vertical at most (CameraLens; 5.6, DL36, DL61's 2026-10-26 amendment); a port whose phone framing must stay 1.x's may write horizontalFovDeg: 60, which is never capped
| Version-1 key | Disposition | 2.0 home | Via |
|---|---|---|---|
viewer.camera.horizontalFov |
replaced | camera.lens.horizontalFovDeg |
— |
Used by: no app in the cross-check.
/** `viewer.camera.horizontalFov` → `camera.lens.horizontalFovDeg` (the default lens holds 60°). */
export function horizontalFov(degrees: number): ViewerDocumentJson {
return {
schema: 'holo.viewer-document',
version: 2,
camera: { lens: { horizontalFovDeg: degrees } },
};
}
viewer.camera.near, .far ​runtime-owned · written for 1.x 1.5
runtime-owned (K40, B.4 X12)
| Version-1 key | Disposition | 2.0 home | Via |
|---|---|---|---|
viewer.camera.near |
runtime-owned | — | — |
viewer.camera.far |
runtime-owned | — | — |
Used by: app-378d190f1b, app-59c5b5751e.
viewer.camera.adaptiveZoomLimits ​replaced · written for 1.x 1.5
true (1.x's default; 18 of 25 page documents at c4c6ffd2): no distance in the version-2 document, so the runtime fits them (CameraLimits.minDistance, 5.6); false (app-4762caacdc, chair, app-37356bf09f, app-378d190f1b, app-04c2774145, app-f9a32142f1, app-fc306ec3dc): the layer's controls.minDistance and .maxDistance, else 1.x's 0.5 and 50 m, as camera.limits.minDistance and .maxDistance (viewer/controllers/camera.controller.ts:285-324 at eb5ce2afe)
| Version-1 key | Disposition | 2.0 home | Via |
|---|---|---|---|
viewer.camera.adaptiveZoomLimits |
replaced | camera.limits.minDistance |
— |
Used by: app-04c2774145, app-37356bf09f, app-378d190f1b, app-4762caacdc, app-59c5b5751e, app-f9a32142f1, app-fc306ec3dc.
/**
* `viewer.camera.adaptiveZoomLimits`: `true` writes no distance (the runtime fits them); `false`
* takes the layer's `controls.minDistance` and `.maxDistance`, else 1.x's 0.5 and 50 m.
*/
export function adaptiveZoomLimits(
adaptive: boolean,
minDistance = 0.5,
maxDistance = 50,
): ViewerDocumentJson {
return adaptive
? { schema: 'holo.viewer-document', version: 2 }
: {
schema: 'holo.viewer-document',
version: 2,
camera: { limits: { minDistance, maxDistance } },
};
}
viewer.controls.minDistance, .maxDistance ​replaced · written for 1.x 1.5
camera.limits.minDistance, .maxDistance (CameraLimits) when the layer sets adaptiveZoomLimits: false or maxDistanceFitFactor; otherwise removed, since 1.x's adaptive limits ignored them
| Version-1 key | Disposition | 2.0 home | Via |
|---|---|---|---|
viewer.controls.minDistance |
replaced | camera.limits.minDistance |
— |
viewer.controls.maxDistance |
replaced | camera.limits.maxDistance |
— |
Used by: app-04c2774145, app-37356bf09f, app-378d190f1b, app-4762caacdc, app-59c5b5751e, app-b4ae2fce25, app-ddfb017681, app-e61a0b42ad, app-f07e9033d9, app-f9a32142f1, configurator-template.
/**
* `viewer.controls.minDistance`, `.maxDistance` → `camera.limits` when the layer sets
* `adaptiveZoomLimits: false` or `maxDistanceFitFactor`; otherwise 1.x ignored them.
*/
export function distances(minDistance: number, maxDistance: number): ViewerDocumentJson {
return {
schema: 'holo.viewer-document',
version: 2,
camera: { limits: { minDistance, maxDistance } },
};
}
viewer.controls.maxDistanceFitFactor ​replaced · written for 1.x 1.5
camera.limits.maxDistanceFit (CameraLimits), with the layer's distances (else 1.x's 0.5 and 50 m), at least the factor that keeps the app's start view unclamped: 1.x clamped only a dolly gesture to the cap, 2.0 clamps every view write (5.6), so take the larger of the 1.x factor and the stored start distance (with any pull-back the app adds) ÷ the sphere-fit distance r / sin(h) at the narrowest aspect the page shows, and pin 1.x's lens (horizontalFovDeg) if the app frames from that distance. 1.x values at c4c6ffd2: the template (2, 0.3–14 m; its port needs 6 with horizontalFovDeg: 60), app-b362a81c85's Livorno and Loua documents (2: check their start views), app-f07e9033d9 (100, 3.57–25 m), app-e61a0b42ad (100, 0.2–1.5 m). A raised factor also lets the shopper dolly further out than 1.x (the template: 2.24 m → 5.9–6.7 m): a visual change on the port's own D44 sign-off
| Version-1 key | Disposition | 2.0 home | Via |
|---|---|---|---|
viewer.controls.maxDistanceFitFactor |
replaced | camera.limits.maxDistanceFit |
— |
Used by: app-e61a0b42ad, app-f07e9033d9, configurator-template.
/**
* `viewer.controls.maxDistanceFitFactor` → `camera.limits.maxDistanceFit`, with the distances.
* 2.0 clamps every view write to the cap where 1.x clamped only a dolly gesture, so the factor is
* at least `startFactor`: the app's start distance (with its pull-back) ÷ the sphere-fit distance
* at the narrowest aspect the page shows, rounded up (the template: 2 → 6).
*/
export function maxDistanceFit(
factor: number,
minDistance = 0.5,
maxDistance = 50,
startFactor = 0,
): ViewerDocumentJson {
return {
schema: 'holo.viewer-document',
version: 2,
camera: {
limits: {
maxDistanceFit: Math.max(factor, Math.ceil(startFactor)),
minDistance,
maxDistance,
},
},
};
}
viewer.controls.minPolarAngle, .maxPolarAngle (radians) ​replaced · written for 1.x 1.5
camera.limits.minPolarDeg, .maxPolarDeg (degrees)
| Version-1 key | Disposition | 2.0 home | Via |
|---|---|---|---|
viewer.controls.minPolarAngle |
replaced | camera.limits.minPolarDeg |
— |
viewer.controls.maxPolarAngle |
replaced | camera.limits.maxPolarDeg |
— |
Used by: app-04c2774145, app-37356bf09f, app-378d190f1b, app-4762caacdc, app-59c5b5751e, app-f3598fad77, app-f9a32142f1, app-fd09090420, configurator-template.
/** `viewer.controls.minPolarAngle`, `.maxPolarAngle` (radians) → `minPolarDeg`, `maxPolarDeg`. */
export function polarLimits(minPolarAngle: number, maxPolarAngle: number): ViewerDocumentJson {
const degrees = (radians: number) => (radians * 180) / Math.PI;
return {
schema: 'holo.viewer-document',
version: 2,
camera: {
limits: { minPolarDeg: degrees(minPolarAngle), maxPolarDeg: degrees(maxPolarAngle) },
},
};
}
viewer.controls.touchpadDollyScale ​replaced · written for 1.x 1.5
camera.limits.touchpadDollyScale, default 2.5 as 1.x (viewer/types.ts:780 at eb5ce2afe): app-4762caacdc and app-04c2774145 set 5, app-37356bf09f 10, app-f9a32142f1 12 (its document says 10 under the 1.x schema cap, and apps/app-f9a32142f1/src/components/viewer/use-viewer.ts:63, 193 raises it to 12 after boot), chair 1, and the other 20 page documents rely on 2.5 (page-document.json at c4c6ffd2)
| Version-1 key | Disposition | 2.0 home | Via |
|---|---|---|---|
viewer.controls.touchpadDollyScale |
replaced | camera.limits.touchpadDollyScale |
— |
Used by: app-04c2774145, app-37356bf09f, app-4762caacdc, app-59c5b5751e, app-f9a32142f1.
/** `viewer.controls.touchpadDollyScale` → `camera.limits.touchpadDollyScale` (default 2.5). */
export function touchpadDollyScale(scale = 2.5): ViewerDocumentJson {
return {
schema: 'holo.viewer-document',
version: 2,
camera: { limits: { touchpadDollyScale: scale } },
};
}
viewer.controls.truckAxes, .enableZoom ​replaced · written for 1.x 1.5
truckAxes is camera.limits.panAxes (CameraLimits): 'horizontal' (app-37356bf09f's and app-f9a32142f1's page documents) and 'none' map one to one, and 'none' can also be a target box around the target; pan only is camera.mode('pan'). enableZoom is removed: no zoom (enableZoom: false: the template's src/viewer/hero-document.json and six copies, app-4fd23bc869, app-f07e9033d9, app-fd09090420, app-e9a33606d2, app-8675b3225c and app-f3598fad77, at c4c6ffd2) is dollySpeed: 0 in the version-2 document's camera.limits (or camera.limits({ dollySpeed: 0 })), so a hero framed by camera.fit keeps its fitted distance, the wheel then scrolls the page and a two-finger touch orbits, as in 1.x (5.6); camera.limits({ minDistance: d, maxDistance: d }) only for a known fixed distance
| Version-1 key | Disposition | 2.0 home | Via |
|---|---|---|---|
viewer.controls.truckAxes |
replaced | camera.limits.panAxes |
— |
viewer.controls.enableZoom |
removed | — | camera.limits.dollySpeed: 0 |
Used by: app-04c2774145, app-37356bf09f, app-4762caacdc, app-4fd23bc869, app-8675b3225c, app-e9a33606d2, app-f07e9033d9, app-f3598fad77, app-f9a32142f1, app-fd09090420, configurator-template.
/**
* `viewer.controls.truckAxes` → `camera.limits.panAxes`, one to one; `enableZoom: false` is
* `dollySpeed: 0`, which leaves the wheel to the page and lets a two-finger touch orbit.
*/
export function panAndZoom(
truckAxes: 'horizontal' | 'none',
enableZoom: boolean,
): ViewerDocumentJson {
const limits: CameraLimits = { panAxes: truckAxes, ...(enableZoom ? {} : { dollySpeed: 0 }) };
return { schema: 'holo.viewer-document', version: 2, camera: { limits } };
}
viewer.controls.rotateSpeed, .dollySpeed, .truckSpeed, .dollyToCursor ​replaced · written for 1.x 1.5
camera.limits.rotateSpeed, .dollySpeed, .panSpeed, .dollyToCursor (CameraLimits), one to one, with 1.x's defaults (0.8, 0.9, 2, false; viewer/types.ts:762-774 at eb5ce2afe). Pages tune them: dollySpeed 0.6 (app-04c2774145, app-4762caacdc) or 1.2 (app-37356bf09f, app-f9a32142f1), dollyToCursor true (the template, app-b362a81c85), rotateSpeed 1 (chair, app-378d190f1b) (page-document.json at c4c6ffd2). app-04c2774145 and app-4762caacdc slow the two-finger pan to 1.6 on a coarse pointer (components/viewer/page-device.ts:14-25, viewer-types.ts:30-36); the port sets that with camera.limits({ panSpeed: 1.6 }) (G62)
| Version-1 key | Disposition | 2.0 home | Via |
|---|---|---|---|
viewer.controls.rotateSpeed |
replaced | camera.limits.rotateSpeed |
— |
viewer.controls.dollySpeed |
replaced | camera.limits.dollySpeed |
— |
viewer.controls.truckSpeed |
replaced | camera.limits.panSpeed |
— |
viewer.controls.dollyToCursor |
replaced | camera.limits.dollyToCursor |
— |
Used by: app-04c2774145, app-37356bf09f, app-378d190f1b, app-4762caacdc, app-59c5b5751e, app-f9a32142f1, configurator-template.
/**
* `viewer.controls.rotateSpeed`, `.dollySpeed`, `.truckSpeed`, `.dollyToCursor` → `camera.limits`
* one to one (`truckSpeed` → `panSpeed`), with 1.x's defaults 0.8, 0.9, 2 and false.
*/
export function controlSpeeds(controls: {
rotateSpeed?: number;
dollySpeed?: number;
truckSpeed?: number;
dollyToCursor?: boolean;
}): ViewerDocumentJson {
return {
schema: 'holo.viewer-document',
version: 2,
camera: {
limits: {
rotateSpeed: controls.rotateSpeed ?? 0.8,
dollySpeed: controls.dollySpeed ?? 0.9,
panSpeed: controls.truckSpeed ?? 2,
dollyToCursor: controls.dollyToCursor ?? false,
},
},
};
}
viewer.controls feel: smoothTime, draggingSmoothTime, infinityDolly, boundaryFriction, restThreshold, dollyDragInverted ​runtime-owned · written for 1.x 1.5
runtime-owned: restThreshold (the movement below which 1.x's controls fired 'rest', viewer/document/viewer-schema.ts:252 at eb5ce2afe) is when the runtime fires 'camera:rest', and the drag dolly keeps 1.x's default direction (dollyDragInverted: false, :313). No page document sets those two at be828d5d
| Version-1 key | Disposition | 2.0 home | Via |
|---|---|---|---|
viewer.controls.smoothTime |
runtime-owned | — | — |
viewer.controls.draggingSmoothTime |
runtime-owned | — | — |
viewer.controls.infinityDolly |
runtime-owned | — | — |
viewer.controls.boundaryFriction |
runtime-owned | — | — |
viewer.controls.restThreshold |
runtime-owned | — | — |
viewer.controls.dollyDragInverted |
runtime-owned | — | — |
Used by: app-378d190f1b, app-4fd23bc869, app-59c5b5751e, app-822bb9d6aa, app-8675b3225c, app-e9a33606d2, app-fd09090420.
viewer.controls.panBoundary ('none' / 'model') ​runtime-owned · written for 1.x 1.5
runtime-owned in published layers: 'model' keeps the orbit target inside the role 'product' bounds and refreshes the box when they change, as 1.4 does (viewer/controllers/camera.controller.ts:244-262 at 26d38f8b6). In a page, camera.limits({ target }) with the box the app wants. No app sets it at 342b26ce
| Version-1 key | Disposition | 2.0 home | Via |
|---|---|---|---|
viewer.controls.panBoundary |
runtime-owned | — | — |
Used by: no app in the cross-check.
viewer.loader.cache, .modelQuality, .emitRemovedOnReplace, .nativeBvh, .skinnedPickAcceleration, .dracoWorkerLimit, .retainGltfParser ​runtime-owned · written for 1.x 1.5
runtime-owned; a replaced model shows in the commit feed (5.14). cacheEncrypted: true (app-fc306ec3dc) is the only behaviour: the byte cache keeps .holo ciphertext, never decrypted bytes (6.5). How a model decodes (the Draco workers), builds its pick and collision BVHs and keeps its parser's buffers (viewer/document/viewer-schema.ts:394-410 at eb5ce2afe) is the runtime's too: no host code reaches the parser (C.5's scene row). No page document sets those four at be828d5d
| Version-1 key | Disposition | 2.0 home | Via |
|---|---|---|---|
viewer.loader.cache |
runtime-owned | — | — |
viewer.loader.modelQuality |
runtime-owned | — | — |
viewer.loader.emitRemovedOnReplace |
runtime-owned | — | — |
viewer.loader.nativeBvh |
runtime-owned | — | — |
viewer.loader.skinnedPickAcceleration |
runtime-owned | — | — |
viewer.loader.dracoWorkerLimit |
runtime-owned | — | — |
viewer.loader.retainGltfParser |
runtime-owned | — | — |
Used by: app-04c2774145, app-37356bf09f, app-4762caacdc, app-4fd23bc869, app-822bb9d6aa, app-8675b3225c, app-bcb7ef21da, app-c1384707fc, app-e3b47169bc, app-e61a0b42ad, app-e9a33606d2, app-f07e9033d9, app-f3598fad77, app-fb16dd1985, app-fc306ec3dc, configurator-template.
viewer.renderer.toneMapping ​replaced · written for 1.x 1.5
stage.toneMapping: today's ACES → 'aces'; 'neutral', 1.x's default (viewer/document/viewer-schema.ts:79-80 at eb5ce2afe), → 'product'; 'agx' → 'mood'; 'none' → 'none'. 'reinhard' is removed: no page document or preset sets it at be828d5d, so it records document-error with didYouMean 'product' and the layer below decides
| Version-1 key | Disposition | 2.0 home | Via |
|---|---|---|---|
viewer.renderer.toneMapping |
replaced | stage.toneMapping |
— |
Used by: app-04c2774145, app-0c773a4ee3, app-37356bf09f, app-378d190f1b, app-4762caacdc, app-4f16d2f6da, app-4fd23bc869, app-510d750508, app-59c5b5751e, app-822bb9d6aa, app-8675b3225c, app-bcb7ef21da, app-c1384707fc, app-ceb4eccc72, app-e3b47169bc, app-e61a0b42ad, app-e9a33606d2, app-f07e9033d9, app-f9a32142f1, app-fb16dd1985, app-fc07850558, app-fc306ec3dc, app-fd09090420, configurator-template.
/**
* `viewer.renderer.toneMapping` → `stage.toneMapping`: ACES → 'aces', 'neutral' → 'product',
* 'agx' → 'mood', 'none' → 'none'; 'reinhard' is removed (the layer below decides).
*/
export function toneMapping(
curve: 'aces' | 'neutral' | 'agx' | 'none' | 'reinhard',
): ViewerDocumentJson {
const map = { aces: 'aces', neutral: 'product', agx: 'mood', none: 'none' } as const;
return curve === 'reinhard'
? { schema: 'holo.viewer-document', version: 2 }
: { schema: 'holo.viewer-document', version: 2, stage: { toneMapping: map[curve] } };
}
viewer.renderer.toneMappingExposure ​replaced · written for 1.x 1.5
stage.exposureEv, from the curve the version-1 layers merge to: with 'aces', log2(value / 0.83) (the preset already includes the fleet's 0.83, so 21 apps' 0.83 becomes 0); with 'agx', log2(value) + 0.6, since 'mood' already includes −0.6 EV (1.x's mood preset is AgX at 0.65, viewer/document/tone-mapping-presets.ts:41-45, so it becomes −0.02); with another curve, log2(value)
| Version-1 key | Disposition | 2.0 home | Via |
|---|---|---|---|
viewer.renderer.toneMappingExposure |
replaced | stage.exposureEv |
— |
Used by: app-04c2774145, app-0c773a4ee3, app-37356bf09f, app-378d190f1b, app-4762caacdc, app-4f16d2f6da, app-4fd23bc869, app-510d750508, app-59c5b5751e, app-822bb9d6aa, app-8675b3225c, app-bcb7ef21da, app-c1384707fc, app-ceb4eccc72, app-e3b47169bc, app-e61a0b42ad, app-e9a33606d2, app-f07e9033d9, app-f9a32142f1, app-fb16dd1985, app-fc306ec3dc, app-fd09090420, configurator-template.
/**
* `viewer.renderer.toneMappingExposure` → `stage.exposureEv`, from the curve the version-1 layers
* merge to: ACES `log2(value / 0.83)`, AgX `log2(value) + 0.6`, another `log2(value)`.
*/
export function exposure(value: number, curve: string): ViewerDocumentJson {
const exposureEv =
curve === 'aces'
? Math.log2(value / 0.83)
: curve === 'agx'
? Math.log2(value) + 0.6
: Math.log2(value);
return { schema: 'holo.viewer-document', version: 2, stage: { exposureEv } };
}
viewer.renderer.antialias, .beautyFrame, .beautyFrameMaxPixelRatio, .pixelRatioLimit, .adaptiveQuality, .qualityTier, .temporalAccumulation, .beautyFrameMultiplier, .beautyFrameDelayMs, .temporalAccumulationFrames, .fpsTarget ​runtime-owned · written for 1.x 1.5
runtime-owned: the quality tier; an app no longer pins a tier (app-b362a81c85 pins 'high' today, so its fire and glass checks in 9.5 run on the adaptive tier; app-fc07850558 pins 'high' with adaptiveQuality: false since configurators d1debb8a9 because the ladder switched tier mid-orbit, page-document.json:5 at 5cdd2780, which B.4 X13 makes runtime-owned); forcing a tier for a measurement is a HoloDebug concern. The idle still's size, delay and frame count and the frame rate the ladder holds (fpsTarget, 55 in 1.x; B.4 X13) are the tier's too (viewer/document/viewer-schema.ts:118, 148-165 at eb5ce2afe). No page document sets those four at be828d5d
| Version-1 key | Disposition | 2.0 home | Via |
|---|---|---|---|
viewer.renderer.antialias |
runtime-owned | — | — |
viewer.renderer.beautyFrame |
runtime-owned | — | — |
viewer.renderer.beautyFrameMaxPixelRatio |
runtime-owned | — | — |
viewer.renderer.pixelRatioLimit |
runtime-owned | — | — |
viewer.renderer.adaptiveQuality |
runtime-owned | — | — |
viewer.renderer.qualityTier |
runtime-owned | — | — |
viewer.renderer.temporalAccumulation |
runtime-owned | — | — |
viewer.renderer.beautyFrameMultiplier |
runtime-owned | — | — |
viewer.renderer.beautyFrameDelayMs |
runtime-owned | — | — |
viewer.renderer.temporalAccumulationFrames |
runtime-owned | — | — |
viewer.renderer.fpsTarget |
runtime-owned | — | — |
Used by: app-37356bf09f, app-378d190f1b, app-59c5b5751e, app-f9a32142f1, app-fc07850558, app-fc306ec3dc.
viewer.renderer.powerPreference, .precompile, .shaderWarmup (viewer/document/viewer-schema.ts:124-137 at eb5ce2afe) ​runtime-owned · written for 1.x 1.5
runtime-owned: the GPU the context asks for and when shaders compile; a new look shows once its shaders are ready, so no frame waits for a shader link (10.3's frame-time gate). No page document sets them at be828d5d
| Version-1 key | Disposition | 2.0 home | Via |
|---|---|---|---|
viewer.renderer.powerPreference |
runtime-owned | — | — |
viewer.renderer.precompile |
runtime-owned | — | — |
viewer.renderer.shaderWarmup |
runtime-owned | — | — |
Used by: no app in the cross-check.
viewer.renderer.brightness, .contrast (display-space, after the tone curve and the output encoding, viewer/document/viewer-schema.ts:91-104 at eb5ce2afe) ​removed · written for 1.x 1.5
removed: stage.grade takes only saturation, and exposureEv acts before the tone curve, so neither matches; a port that needs one asks for a grade field under 3.1's generic-API test. No page document sets them at be828d5d
| Version-1 key | Disposition | 2.0 home | Via |
|---|---|---|---|
viewer.renderer.brightness |
removed | — | — |
viewer.renderer.contrast |
removed | — | — |
Used by: no app in the cross-check.
viewer.perf (monitor, sampleIntervalMs, dropFpsThreshold, dropSustainMs, spikeMs, slowSpanMs: the viewer's own perf:* telemetry, viewer/document/viewer-schema.ts:452-480 at eb5ce2afe) ​runtime-owned · written for 1.x 1.5
runtime-owned: performance telemetry is the runtime's beacons and a HoloDebug concern, as forcing a tier is; the v1 tenant declaration carries no perf:* event, so no app listens to one. No page document sets them at be828d5d
| Version-1 key | Disposition | 2.0 home | Via |
|---|---|---|---|
viewer.perf.monitor |
runtime-owned | — | — |
viewer.perf.sampleIntervalMs |
runtime-owned | — | — |
viewer.perf.dropFpsThreshold |
runtime-owned | — | — |
viewer.perf.dropSustainMs |
runtime-owned | — | — |
viewer.perf.spikeMs |
runtime-owned | — | — |
viewer.perf.slowSpanMs |
runtime-owned | — | — |
Used by: no app in the cross-check.
viewer.renderer.preserveDrawingBuffer (app-fc306ec3dc, pergola, app-4f16d2f6da) ​removed · written for 1.x 1.5
removed: capture() reads the frame (source: 'displayed' for the displayed one, G19; K38); app-fc306ec3dc's thumbnails (capture-thumbnail.ts) port to capture({ size }), which keeps the lens's 60° horizontal field of view where 1.x's captureBlob kept the live vertical one, so they frame as 1.x's only at a 4:3 stage (9.8)
| Version-1 key | Disposition | 2.0 home | Via |
|---|---|---|---|
viewer.renderer.preserveDrawingBuffer |
removed | — | capture() |
Used by: app-4f16d2f6da, app-ceb4eccc72, app-fc306ec3dc.
viewer.shadows.enabled: false ​replaced · written for 1.x 1.5
stage.ground: 'none' and lights without shadow (no app sets it at c4c6ffd2)
| Version-1 key | Disposition | 2.0 home | Via |
|---|---|---|---|
viewer.shadows.enabled |
replaced | stage.ground |
— |
Used by: app-59c5b5751e.
/** `viewer.shadows.enabled: false` → `stage.ground: 'none'`, and lights without `shadow`. */
export const shadowsOff: ViewerDocumentJson = {
schema: 'holo.viewer-document',
version: 2,
stage: { ground: 'none' },
};
viewer.shadows.intensity ​replaced · written for 1.x 1.5
stage.ground: { shadow: { opacity } }. chair sets 0 and removes the runtime's ground itself, with shadows on because its key spot shadows the chair (apps/app-59c5b5751e/src/runtime/page-document.json:12, page-document.ts:13-15, src/scene/chairPlugin.ts:94-101 at c4c6ffd2): ground: 'none', and its key spot keeps shadow: { strength: 0.3 }, 1.x's shadow intensity, with softness tuned for its radius 3 against the 9.6 chair golden (src/scene/settings.ts:340-369)
| Version-1 key | Disposition | 2.0 home | Via |
|---|---|---|---|
viewer.shadows.intensity |
replaced | stage.ground.shadow.opacity |
— |
Used by: app-59c5b5751e.
/** `viewer.shadows.intensity` → `stage.ground: { shadow: { opacity } }`. */
export function shadowOpacity(intensity: number): ViewerDocumentJson {
return {
schema: 'holo.viewer-document',
version: 2,
stage: { ground: { shadow: { opacity: intensity } } },
};
}
viewer/controllers/scene.controller.ts:122-127, 310, viewer/types.ts:724-731 at eb5ce2afe) ​kept · written for 1.x 1.5
nothing to write: stage.ground.shadow y, sizeM and opacity default to the same (StageSettings.ground)
Used by: no app in the cross-check.
viewer.shadows.radius, .mapSize, .soft, .softness ​runtime-owned · written for 1.x 1.5
runtime-owned: the shadow filter per tier and per receiver (ShadowSpec); an app tunes it with ShadowSpec.softness, carrying a 1.x radius r over as 0.3 × r / base (base 3 while a product part receives, else 14; C.6's key-light row). The map size follows the tier as well (1.x's tier preset replaced mapSize before the first frame, viewer/document/viewer-schema.ts:36-40 at eb5ce2afe), and the idle soft ground shadow (soft, softness) is the runtime's idle refinement. No page document sets those three at be828d5d
| Version-1 key | Disposition | 2.0 home | Via |
|---|---|---|---|
viewer.shadows.radius |
runtime-owned | — | — |
viewer.shadows.mapSize |
runtime-owned | — | — |
viewer.shadows.soft |
runtime-owned | — | — |
viewer.shadows.softness |
runtime-owned | — | — |
Used by: no app in the cross-check.
viewer.transition.enabled (only app-fc07850558, app-fc306ec3dc and app-f3598fad77 set it, to false: apps/app-fc07850558/src/components/viewer/page-document.json:6, apps/app-fc306ec3dc/src/viewer/page-document.json:12, apps/app-f3598fad77/src/viewer/page-document.json:7 at c4c6ffd2) ​replaced · written for 1.x 1.5
false → materials.fadeMs: 0 in the version-2 document; true → the default selection crossfade, 420 ms. The key never touched the camera: 1.x crossfades every appearance-only material change, a select included (viewer/types.ts:394-420, 830-833, viewer/plugins/material-plugin.ts:1415-1441 at eb5ce2afe)
| Version-1 key | Disposition | 2.0 home | Via |
|---|---|---|---|
viewer.transition.enabled |
replaced | materials.fadeMs |
— |
Used by: app-f3598fad77, app-fc07850558, app-fc306ec3dc.
/** `viewer.transition.enabled: false` → `materials.fadeMs: 0`; `true` keeps the 420 ms fade. */
export function transitionEnabled(enabled: boolean): ViewerDocumentJson {
return enabled
? { schema: 'holo.viewer-document', version: 2 }
: { schema: 'holo.viewer-document', version: 2, materials: { fadeMs: 0 } };
}
viewer.transition.durationMs ​replaced · written for 1.x 1.5
materials.fadeMs in the version-2 document
| Version-1 key | Disposition | 2.0 home | Via |
|---|---|---|---|
viewer.transition.durationMs |
replaced | materials.fadeMs |
— |
Used by: no app in the cross-check.
/** `viewer.transition.durationMs` → `materials.fadeMs`. */
export function transitionDuration(durationMs: number): ViewerDocumentJson {
return { schema: 'holo.viewer-document', version: 2, materials: { fadeMs: durationMs } };
}
viewer.ar.place.* (android, ios, autoPlace, androidShare, inpage, inpageSheet, shadow, occlusion, calibrateLength, ui.*), viewer.ar.placement, viewer.ar.scale and viewer.ar.native.* (banner, title, cta, cartUrlTemplate) (1.6: the viewer schema's paths of the ar.place.* and ar.native.* row; inpage, inpageSheet and shadow are production's in-page AR keys, viewer/document/viewer-schema.ts since feat/xr-inpage-ar and, for shadow, feat/xr-inpage-quicklook; occlusion, calibrateLength, ui.*, placement and scale are AR v3's, !1099, contract 1.8) ​runtime-owned · written for 1.x 1.6
runtime-owned, as that row: activateAR's route reads them from the account's published layer (5.11), and the platform file's top-level xr key (place, deny) switches in-page AR off for v1 and v2 alike (5.1, D64); a page document that sets one records document-error (delivery-run 2026-10-20; DECIDED: XR alignment reply §3, Erik; the in-page keys as their siblings, delivery-run cycle 15)
| Version-1 key | Disposition | 2.0 home | Via |
|---|---|---|---|
viewer.ar.place.android |
runtime-owned | — | — |
viewer.ar.place.ios |
runtime-owned | — | — |
viewer.ar.place.autoPlace |
runtime-owned | — | — |
viewer.ar.place.androidShare |
runtime-owned | — | — |
viewer.ar.place.inpage |
runtime-owned | — | — |
viewer.ar.place.inpageSheet |
runtime-owned | — | — |
viewer.ar.place.shadow |
runtime-owned | — | — |
viewer.ar.place.occlusion |
runtime-owned | — | — |
viewer.ar.place.calibrateLength |
runtime-owned | — | — |
viewer.ar.place.ui |
runtime-owned | — | — |
viewer.ar.placement |
runtime-owned | — | — |
viewer.ar.scale |
runtime-owned | — | — |
viewer.ar.native.banner |
runtime-owned | — | — |
viewer.ar.native.title |
runtime-owned | — | — |
viewer.ar.native.cta |
runtime-owned | — | — |
viewer.ar.native.cartUrlTemplate |
runtime-owned | — | — |
Used by: app-fc07850558.
viewer.xr.tryOn.* (enabled, consent) and viewer.xr.ui.* (theme, copy, controls) (1.6: the viewer schema's paths of the xr.tryOn.* and xr.ui.* row) ​runtime-owned · written for 1.x 1.6
runtime-owned, as that row: read by @holo/xr, consent from the account layer only (D64, XR §6.3), and the platform xr key's tryOn switches try-on off for v1 and v2 alike (5.1) (delivery-run 2026-10-20; DECIDED: XR alignment reply §3, Erik)
| Version-1 key | Disposition | 2.0 home | Via |
|---|---|---|---|
viewer.xr.tryOn.enabled |
runtime-owned | — | — |
viewer.xr.tryOn.consent |
runtime-owned | — | — |
viewer.xr.ui.theme |
runtime-owned | — | — |
viewer.xr.ui.copy |
runtime-owned | — | — |
viewer.xr.ui.controls |
runtime-owned | — | — |
Used by: no app in the cross-check.
Query.name is a glob over the whole name (6.10, R6). The regular expressions apps use today become globs like this:
/hull.*outside/ (unanchored) ​replaced · 2.0: Query · written for 1.x 1.5
{ name: '*hull*outside*' }
Note: an unanchored RegExp matches anywhere, so wrap it in *
Used by: no app in the cross-check.
/** `/hull.*outside/` matches anywhere, so the glob is wrapped in `*`. */
export const hullOutside: Query = { name: '*hull*outside*' };
/^bimini-mechanism$/ (anchored) ​replaced · 2.0: Query · written for 1.x 1.5
{ name: 'bimini-mechanism' }
Note: a glob is anchored already; it no longer matches bimini-mechanism-inox
Used by: no app in the cross-check.
/** `/^bimini-mechanism$/`: a glob is anchored already (no longer matches `bimini-mechanism-inox`). */
export const biminiMechanism: Query = { name: 'bimini-mechanism' };
/^mp[-_]/ ​replaced · 2.0: Query · written for 1.x 1.5
[{ name: 'mp-*' }, { name: 'mp_*' }]
Note: a character class becomes an array (any-of)
Used by: no app in the cross-check.
/** `/^mp[-_]/`: a character class becomes an array (any of). */
export const connectorPlanes: Query = [{ name: 'mp-*' }, { name: 'mp_*' }];
/a|b/ ​replaced · 2.0: Query · written for 1.x 1.5
[{ name: '*a*' }, { name: '*b*' }]
Note: an alternation becomes an array
Used by: no app in the cross-check.
/** `/a|b/`: an alternation becomes an array. */
export const eitherName: Query = [{ name: '*a*' }, { name: '*b*' }];
/text-boat-side/i (case-insensitive) ​replaced · 2.0: Query · written for 1.x 1.5
the spellings the GLB has, as an array
Note: Query.name is case-sensitive; didYouMean on target-unmatched names the authored spelling
Used by: no app in the cross-check.
/** `/text-boat-side/i`: the spellings the GLB has, as an array (`Query.name` is case-sensitive). */
export const boatSideText = (spellings: readonly string[]): Query =>
spellings.map((spelling) => ({ name: `*${spelling}*` }));
/^Name_\d+$/ (loader-numbered primitives) ​replaced · 2.0: Query · written for 1.x 1.5
{ name: 'Name' } plus slot
Note: a multi-primitive mesh is one part per primitive; match its parts by slot
Used by: no app in the cross-check.
/** `/^Name_\d+$/` (loader-numbered primitives): the mesh's name, its primitives by `slot`. */
export const primitiveOf = (slot: string): Query => ({ name: 'Name', slot });
replaced · 2.0: Query · written for 1.x 1.5
{ slot: '<glob>' }, its spellings as an array
Note: slot is the same glob as name, over the authored material's slot (R6)
Used by: no app in the cross-check.
/** A RegExp over material names → `{ slot: <glob> }`, its spellings as an array. */
export const bySlots = (slots: readonly string[]): Query => slots.map((slot) => ({ slot }));
transparent with a map) ​replaced · 2.0: Query · written for 1.x 1.5
the slots that hold that state in the shipped GLB, listed by name after reading its materials
Note: no Query field reads a material's state, and a word glob reaches slots the test skips: in pergola's garden.glb only grass1 and grass2 are BLEND with a map, while *tree* also matches Christmas_tree_Billboard_Mat, a MASK at cutoff 0.218 (apps/app-ceb4eccc72/src/lib/pergola-plugins/pergola-environment-plugin.ts:444-447, public/env/garden.glb at c4c6ffd2)
Used by: no app in the cross-check.
/**
* A test of the loaded material's state (pergola's foliage: `transparent` with a `map`) → the
* slots that hold that state in the shipped GLB, by name: in `garden.glb`, `grass1` and `grass2`.
*/
export const blendedFoliage: Query = [{ slot: 'grass1' }, { slot: 'grass2' }];
/\./ in a sanitized name ​replaced · 2.0: Query · written for 1.x 1.5
{ name: 'main.l' } or { name: 'mainl' }
Note: both the authored and the sanitized form match
Used by: no app in the cross-check.
/** `/\./` in a sanitized name: the authored ('main.l') and the sanitized ('mainl') form match. */
export const mainLeft: Query = { name: 'main.l' };
applyPBR, getAllMeshes) ​replaced · 2.0: Query · written for 1.x 1.5
add kind: 'mesh': { name: 'Corpus*', kind: 'mesh' }; a primitive the RegExp named by its mesh name becomes an id: app-fc306ec3dc's finish is [{ name: 'Corpus*', kind: 'mesh' }, { id: '400x400x450-2F-DO033' }, { id: '600x400x450-2F_-DO001' }]
Note: a glob also matches a group named like its meshes, and a rule on the group cascades to every mesh below (R8); app-fc306ec3dc's ^Corpus would reach the Corpus012 group of its two 2F bodies and paint their matte black accent (apps/app-fc306ec3dc/src/components/planner/finish-materials.ts:100-150 at c4c6ffd2). Those bodies are primitive parts named after the glTF mesh (Query's slot), so Corpus* alone misses them, as 1.4's ^Corpus did; app-fc306ec3dc's RegExp lists them $-anchored, and their matte black siblings are …_1
Used by: app-e61a0b42ad, app-f07e9033d9, app-fc306ec3dc.
/**
* A RegExp 1.4 tested over meshes only (`applyPBR`, `getAllMeshes`): add `kind: 'mesh'`, and a
* primitive the RegExp named by its mesh name becomes an `id` (app-fc306ec3dc's finish).
*/
export const app_fc306ec3dcCorpusFinish: Query = [
{ name: 'Corpus*', kind: 'mesh' },
{ id: '400x400x450-2F-DO033' },
{ id: '600x400x450-2F_-DO001' },
];
The runtime does these; a port deletes the code that did them.
| Id | Need | Today's route | 2.0 | Apps |
|---|---|---|---|---|
| B.4/X1 | Serialised scene sync, newest wins | scene-sync-scheduler.ts (15 copies: the template's, 12 identical to it, and app-b362a81c85's and app-fc306ec3dc's own, at c4c6ffd2) |
the write queue (O2): call order, adjacent scene writes coalesce, a select between them is kept; supersede: true only for a sync that re-derives everything |
14 apps plus the template |
| B.4/X2 | Echo suppression | refs in use-viewer-sync.ts; skipNextSyncRef, 16 ms debounce |
origin, cause.origin for runtime → store; the equal-write rule (O15) for store → runtime |
the template's copies; app-f9a32142f1 |
| B.4/X3 | Node id repair and duplication | model-node-id-normalizer.ts (app-37356bf09f at projection time: apps/app-37356bf09f/src/lib/app-37356bf09f/project.ts:226-243, src/lib/configurator/model-node-id-normalizer.ts:158-182 at be828d5d) |
normalizeNodeIds, duplicateNodes; app-37356bf09f's projection-time use stays app code: it re-ids each placed module's cloned template children on every store change with deterministic ids (the base id plus an occurrence suffix), so an equal re-projection commits nothing (O15), where duplicateNodes mints new ids per call |
app-04c2774145, app-4762caacdc, app-f9a32142f1, app-37356bf09f |
| B.4/X4 | Freezing the frame during a sync | scene-freeze.ts (8 s cap) |
transaction with the capped hold |
app-04c2774145, app-4762caacdc |
| B.4/X5 | Re-snapping on load and undo | sofa-assembly.ts |
stored connections | app-04c2774145, app-4762caacdc, app-f9a32142f1; app-37356bf09f stores no connection (K63) |
| B.4/X6 | Page state paired with undo | the 1.4 page-state call | setSceneModel(model, { checkpoint }) or any recorded write's checkpoint; transaction({ checkpoint }); state().checkpoint |
14 apps (grep) |
| B.4/X7 | Undo grouping over a gesture | the 1.4 history grouping member | history.group |
app-4762caacdc, app-37356bf09f, app-04c2774145, app-f3598fad77, app-bcb7ef21da (grep) |
| B.4/X8 | Stored ModelNodes without a transform load | withIdentityTransforms before every setSceneModel (scene-model.ts:48-98 at 342b26ce) |
runtime-owned: missing or partial transforms read as identity and outputs carry them, as 1.x since holostep 160b346ba (viewer/utils/model-utils.ts:150-178) |
app-f9a32142f1 (Elisa, Henri Fotel and Henri Kanapé were seeded without one) |
| B.4/X9 | Anti-aliasing on every quality tier, done once | renderer.antialias: false with the composer's 4× MSAA and lowTierBypass: false (app-f9a32142f1 page-document.json:9, 39 at 342b26ce) |
runtime-owned: the medium and high tiers anti-alias with the chain's MSAA, and the canvas is never multisampled under a multisampled composer. The low tier draws the composer's scene buffer with one sample, as production's viewer does since 090cd0ee3 (0.93 ms a frame against 1.03 ms with 4× MSAA at the low tier on an iPhone 13 Pro), so a chain with antialias: 'none', 2.0's own and every composing production app's, has no anti-aliasing there, and 'auto' keeps FXAA; the switch changes only the targets' sample count and links no program (DL49) |
app-f9a32142f1, app-b362a81c85 |
| B.4/X10 | A texture budget that holds on desktop and on phones | material.maxVramBytes: 67108864 on every tier (page-document.json:42; 381 → 224 MB steady-state WebGL texture memory on RIO, page-document.ts:26-32); app-04c2774145 and app-4762caacdc raise it to 128 MB on narrow screens and 256 MB elsewhere, because one Wind fabric's resident set is about 82 MB and the 64 MB mobile default evicted on every texture load (apps/app-04c2774145/src/viewer/page-document.ts:55-57, apps/app-4762caacdc/src/viewer/page-document.ts:55-57 at 5cdd2780) |
runtime-owned: the tier's texture budget; the port measures against the 64 MiB pin; on a phone, the pilot's parity run swaps fabrics with the GOLF Wind set resident and sees no re-upload of a texture the current commit draws; eviction takes only textures it doesn't draw, and on the 'high' and 'med' tiers keeps the look the last commit replaced until the next pick that loads (Erik's decision 1); the 'low' tier keeps none, and a tier change down to 'low' or a lost context drops a kept look (the council's 33725fae8) |
app-f9a32142f1; app-04c2774145 and app-4762caacdc |
| B.4/X11 | Shadows follow what moves, and an orbit costs no shadow pass | renderer.shadowMap.needsUpdate plus each casting light's shadow.needsUpdate through session.viewer.renderer when a casting signature changes (app-fc07850558 runtime-backend.ts:126-156, shadow-rig.ts:207-217; app-fc306ec3dc shadow-refresh.ts:10-21, at 5cdd2780); 1.x renders shadows on demand (viewer/controllers/renderer.controller.ts:674-677 at eb5ce2afe) |
runtime-owned: the shadow map re-renders at most once a frame when what the shadow pass draws changes: a casting part's drawn pose, visibility, geometry, cast flag (shadows) or cut-out (alphaMode, alphaCutoff, an alpha map; through a rule or Frame.material), or a casting light's position, direction, target, on/off or ShadowSpec, or the shadow camera's fit (covers); by a commit, a drag move, Frame.pose, Frame.visible, a clip step or the release of a frame write; never for a camera move alone. pergola's sun slider (pergola-lighting-plugin.ts:520-531), app-fc07850558's rig refit and cast signature (runtime-backend.ts:131-158) and 1.x's refresh on 'material:changed' (viewer/configurator-viewer.ts:693-709 at eb5ce2afe) are covered |
app-fc07850558, app-fc306ec3dc, app-04c2774145, pergola |
| B.4/X12 | Depth precision where parts meet (no joint flicker) | near 0.1 m and far 100 m written to the engine camera in every render:before (app-fc07850558 runtime-backend.ts:108-114, room-camera.ts:13-21 at 5cdd2780) against the 1.x default 0.01–1000 m (viewer/types.ts:755-756, re-applied by viewer/controllers/camera.controller.ts:529-533 at eb5ce2afe); app-4f16d2f6da pergola-camera-guard-plugin.ts:210-228 |
runtime-owned: near and far are fitted to camera.limits and the bounds of every drawn part, whatever its role (helper included), seen from the drawn camera, a Frame.camera view included (app-ddfb017681's helper colliders span about 6,000 units, and 1.4 sets 0.3–6000 by hand, apps/app-ddfb017681/src/viewer-plugins/collider-overlay-plugin.ts:47-48, 588-596 at c4c6ffd2; estate the same, apps/app-b4ae2fce25/src/viewer-plugins/collider-overlay-plugin.ts:47-48, 541-545 at c4c6ffd2), never the fixed 0.01–1000 m, and no resize or document layer resets them; two faces 2.5 mm apart (a door on its carcass) never swap at the orbit's farthest distance, nor two faces 1 mm apart (app-fc07850558's casing lip on its reveal, room-fixtures.ts:220-227 at be828d5d) within the room's limits.maxDistance |
app-fc07850558, app-4f16d2f6da (environment lab) |
| B.4/X13 | One look during a gesture: no tier switch mid-orbit, mid-transition or mid-drag | adaptiveQuality: false, qualityTier: 'high' in the page document (app-fc07850558 page-document.json:5, reason in page-document.test.ts:11-14 at 5cdd2780; the 1.x ladder: 55 fps target and 1 s step-down, viewer/core/quality-manager.ts:47-48, 163 at eb5ce2afe) |
runtime-owned: the ladder keeps measuring during a camera gesture, a view move or a drag, and applies a tier change measured during one when it ends ('camera:control-end', the move landing, 'drag:end'), whatever else then moves the camera (autoRotate, an onFrame hook's Frame.camera); a change measured outside them applies at once, as 1.x's ladder samples and steps on every rendered frame (viewer/controllers/renderer.controller.ts:1027-1034 at eb5ce2afe); no app tier key (C.7) |
app-fc07850558, app-b362a81c85 |
| B.4/X14 | AO that holds still on thin, deep strips while the camera moves (sills, radiator fins, skirting) | denoise 16 samples / 12 px / 4 iterations in the page document (page-document.json:12-29; measured in page-document.test.ts:1-17 at 5cdd2780) |
runtime-owned: the AO denoise per tier. The default is the fleet's: 8 samples / 12 px / 2 iterations, transparency not detected per frame, as 21 of the 22 production apps that compose ship it (the ladder caps the samples at 2 below the high tier; DL43, from the delivery run's composed-parity measurement: 16 / 12 px / 4 as a default would move those apps' frames by p99 12 code values on high). app-fc07850558 keeps its measured 16 / 12 px / 4 through its own stored version-1 layer, which the runtime reads (C.7), so its sills keep their steadiness and the contact shade at the foot of a unit | app-fc07850558 |
| B.4/X15 | Undo depth parity: 50 steps, phones included | plugins.scene-history.maxHistory (e.g. apps/configurator-template/src/viewer/page-document.json:32 at c4c6ffd2) |
runtime-owned: 50 undo steps on every tier (section 12, Q19) | the 15 apps whose page documents set maxHistory: 50 (app-f3598fad77's undo store mirrors it); app-37356bf09f, which sets 20 (apps/app-37356bf09f/src/runtime/page-document.json:35 at c4c6ffd2); app-04c2774145, app-4762caacdc and app-f9a32142f1, which set 20, or 10 on phones |
| B.4/X16 | A sharp view after a devicePixelRatio change: a window moved to another screen, a browser zoom |
none: its e2e check is a test.fixme, since 1.x recomputes the interactive ratio only on a resize or a tier change, so a view dragged from a 1x to a 2x screen orbits at 1x (apps/app-378d190f1b/e2e/render-sharpness.spec.ts:168-181 at be828d5d), and the canvas behind el is runtime-owned in 2.0 |
runtime-owned: within one frame of a devicePixelRatio change, at rest and during a gesture, the drawing buffer is the element's CSS size × the live view's pixel ratio (the device's, capped by the quality tier, C.7) |
app-378d190f1b |
| B.4/X17 | Glass costs no full-resolution transmission pass | renderer.transmissionResolutionScale = 0.5 through the hatch, each asserted by its unit test (pergola src/lib/pergola-plugins/pergola-lighting-plugin.ts:93-98, app-4f16d2f6da :104-109 at be828d5d): three otherwise redraws the opaque scene into a full-resolution multisampled target with mipmaps every frame a transmissive part shows (pergola's sliding glass closure; app-4f16d2f6da's glass and all five roof coverings, src/lib/pergola-engine/materials/roof.ts:22-57). 1.x never sets it (viewer/core/transmission-targets.ts only tracks the targets) |
runtime-owned: what a transmissive part shows through it is drawn at half the drawing buffer's resolution or less on every tier, the engine's choice and no public setting. Every tenant whose GLBs carry KHR_materials_transmission gets it too, so its G5 diff lists it as a D44 item. app-4f16d2f6da's clear roofs keep shadows: { cast: false } (roof.ts:71-75), since a transmissive part casts a solid shadow |
pergola, app-4f16d2f6da |
| B.4/X18 | Detail levels change only the live view | none: 1.x's runtime-lod swaps mesh.geometry in place and invalidates the node's bounds (viewer/plugins/runtime-lod-plugin.ts:723, 731-734 at eb5ce2afe), and its in-page AR export serialises the live scene (viewer/controllers/ar.controller.ts:338-350, 360-370), so with it on, bounds, picks and the AR copy see the simplified level |
runtime-owned: a level of detail changes only what the live view draws. Reads and outputs use each part's full geometry, the asset the runtime selected (its optimized twin included, X10) with its committed labs.geometry.write, labs.skin and labs.mirror writes, never an original fetched instead: worldPose, bounds(), connectors(), dimension rulers, pick and raycast Hits, camera collision, export, activateAR's own GLB and USDZ copy and a source: 'render' capture. A 'displayed' capture copies what is shown |
every app on a tier that draws a simplified level; none today, since no page document or app enables runtime-lod at be828d5d |
| B.4/X19 | Repeated assets load once (council I7) | none: each 2.0 model node and collection model item fetched and parsed its own copy (the council's app-fc07850558-like scene downloaded 180.5 MB; a app-fc306ec3dc-like row of 12 cabinets made 12 requests, be45f3218) |
runtime-owned: Repeated assets: within a session, every model node and collection model item whose resolved URL, keyRef and loader options match shares one fetch and one parse. Each item draws its own clone, with its own nodes, skeleton, materials and clips, so a look, pose, transform or clip on one item never reaches another. The parse is freed with the last item that shows it. A KHR_materials_variants model is parsed per item. (Measured at be45f3218: 180.5 → 99.7 MB downloaded, first presented frame 3,927 → 2,922 ms, GPU memory 1,744 → 1,148 MB on the app-fc07850558-like scene; 12 → 1 request on the app-fc306ec3dc-like row; 0 eager viewer bytes) |
app-fc07850558 (41 items over 31 URLs), app-fc306ec3dc (a row of identical cabinets); any page that shows one GLB more than once |
Each app's 1.x patterns, read from its paper port's notes and its 1.x source at configurators be828d5d (conformance/port-usage.mjs), matched to the rows above. A pattern is a 1.x name the 1.x viewer, the v1 surface or Appendix C.5 declares, a plugin id, an event or a version-1 document key; a row covers it when its 1.x cell, members, keys or recipe name it. Engine classes are listed apart: a port replaces them by need, through Appendix B.
| App | Rows used | Patterns with no row | Engine classes it imports |
|---|---|---|---|
| app-04c2774145 | 94 | none | 7 |
| app-0c773a4ee3 | 41 | none | 16 |
| app-37356bf09f | 79 | none | 0 |
| app-378d190f1b | 37 | none | 20 |
| app-4762caacdc | 94 | none | 7 |
| app-4f16d2f6da | 51 | none | 95 |
| app-4fd23bc869 | 60 | none | 0 |
| app-510d750508 | 46 | none | 35 |
| app-59c5b5751e | 49 | none | 27 |
| app-822bb9d6aa | 62 | none | 0 |
| app-8675b3225c | 60 | none | 0 |
| app-b362a81c85 | 69 | none | 28 |
| app-b4ae2fce25 | 27 | none | 25 |
| app-bcb7ef21da | 79 | none | 29 |
| app-c1384707fc | 69 | none | 8 |
| app-ceb4eccc72 | 57 | none | 46 |
| app-ddfb017681 | 27 | none | 25 |
| app-e3b47169bc | 67 | none | 19 |
| app-e61a0b42ad | 81 | none | 1 |
| app-e9a33606d2 | 60 | none | 0 |
| app-f07e9033d9 | 70 | none | 1 |
| app-f3598fad77 | 65 | none | 3 |
| app-f9a32142f1 | 85 | none | 20 |
| app-fb16dd1985 | 72 | none | 1 |
| app-fc07850558 | 55 | none | 33 |
| app-fc306ec3dc | 88 | none | 43 |
| app-fd09090420 | 78 | none | 6 |
| configurator-template | 65 | none | 0 |
Each needs a row of its own (spec 9.1 item 4: every internal pattern the apps use). The 2.0 home each note names is a reading of the declaration, to be confirmed in that row.
None.
CameraControls.getPosition (camera controls; app-ceb4eccc72): C.6/PluginContext.getCameraControls. pergola reads and writes the orbit through the camera controls (setLookAt, setTarget, getTarget, getPosition); the row covers that route, its 2.0 home is camera.set and camera.stateCameraControls.getTarget (camera controls; app-ceb4eccc72): C.6/PluginContext.getCameraControls. as CameraControls.getPositionCameraControls.setTarget (camera controls; app-ceb4eccc72): C.6/PluginContext.getCameraControls. as CameraControls.getPositioncontrols:changed (event; app-b362a81c85): C.6/register. app-b362a81c85's camera-stage host plugin re-applies its limits on each controls change; the plugin is C.6/register's, its clamp is camera.limits (target)dimension.markDirty (plugin; app-ceb4eccc72): C.6/dimension.formatValue. the dimension plugin's members (markDirty, setOverrides, showFor, getUnit, setUnit), which the row covers as the plugin's options; 2.0's rulers update themselves and format takes the configured sizedimension.setOverrides (plugin; app-ceb4eccc72): C.6/dimension.formatValue. as dimension.markDirtyfindSnapCandidateNear (plugin; app-f9a32142f1): C.6/snapTo. app-f9a32142f1's snap-connector lookup of the pair nearest a position (runtime-escape-hatches.ts:231-242); the snap rows cover the plugin, and 2.0's interaction.snapTo(target, { near }) picks that pairgetUnit (plugin; app-fb16dd1985): C.6/dimension.formatValue. app-fb16dd1985's dimension plugin read (its hatch); as dimension.markDirtymarkDirty (plugin; app-fb16dd1985, app-ceb4eccc72, app-4f16d2f6da, app-f9a32142f1): C.6/dimension.formatValue. app-fb16dd1985's and pergola's dimension plugin call (app-4f16d2f6da's own occluder object has a method of that name too); as dimension.markDirtymoveTo (plugin; app-510d750508, app-b362a81c85, app-fc07850558, app-ceb4eccc72, app-4f16d2f6da): C.6/register. app-b362a81c85's camera-stage host plugin moves the orbit target (CameraControls.moveTo); in app-510d750508, pergola and app-4f16d2f6da it is a shape path's moveTo (engine geometry, ported to shapes) and app-fc07850558's ownonModelLoaded (plugin; app-0c773a4ee3, app-b362a81c85, app-f9a32142f1): C.6/register. model-load hooks inside host plugins (app-0c773a4ee3 and app-b362a81c85 anisotropy; app-f9a32142f1's mattress-texture, agyracs-mesh-suffix, mattress-lid-follower and pocket-bone-follower); the row's app list does not name app-f9a32142f1, estate, app-ddfb017681, app-fc07850558, app-4f16d2f6da or app-fc306ec3dc, which register host plugins tooonModelRemoved (plugin; app-f9a32142f1): C.6/register. as onModelLoaded (app-f9a32142f1's followers)setEnabled (plugin; app-fc306ec3dc): C.6/CameraControls.enabled-false. app-fc306ec3dc's camera-lock host plugin turns the controls off; the row covers that pattern in app-ddfb017681setOverrides (plugin; app-ceb4eccc72): C.6/dimension.formatValue. app-ceb4eccc72's callPluginMethod(session, 'dimension', 'setOverrides', …); as dimension.setOverridessetUnit (plugin; app-fc306ec3dc): C.6/dimension.formatValue. app-fc306ec3dc's dimension plugin option; as dimension.markDirtyPluginContext.canvas (plugin context; app-b4ae2fce25, app-ddfb017681): C.6/register. host plugins draw DOM over the canvas (estate's and app-ddfb017681's collider overlay, app-b362a81c85's dimension drawing, app-fc306ec3dc); 2.0: anchors and overlaysPluginContext.getRenderer (plugin context; app-510d750508, app-59c5b5751e, app-0c773a4ee3, app-b4ae2fce25, app-b362a81c85, app-ceb4eccc72, app-ddfb017681): C.6/shadowMap.needsUpdate. the renderer reached through the plugin context; each use is a renderer row (shadow map refresh, transmission scale, clipping)canvas (plugin context; app-510d750508, app-f07e9033d9, app-b4ae2fce25, app-b362a81c85, app-fc07850558, app-ceb4eccc72, app-4f16d2f6da, app-ddfb017681, app-fc306ec3dc, app-e3b47169bc, app-bcb7ef21da): C.6/register. as PluginContext.canvasgetRenderer (plugin context; app-510d750508, app-59c5b5751e, app-0c773a4ee3, app-378d190f1b, app-b4ae2fce25, app-b362a81c85, app-ceb4eccc72, app-4f16d2f6da, app-ddfb017681): C.6/shadowMap.needsUpdate. as PluginContext.getRendererrenderer.Exposure (renderer; app-378d190f1b): C.7/viewer.renderer.toneMappingExposure. app-378d190f1b's tone mapping and exposure (renderer.toneMapping/Exposure in its notes): C.7's viewer.renderer.toneMapping and viewer.renderer.toneMappingExposure rowsPluginContext.events.emit('model:loaded') (event; app-ceb4eccc72, app-4f16d2f6da): C.6/PluginContext.events.emit. pergola's and app-4f16d2f6da's plugins emit 'model:loaded' into the viewer's bus so its post-process AO and adaptive zoom re-measure the procedural model (emitModelLoaded, runtime-escape-hatches.ts:303-313); C.6's PluginContext.events.emit row: runtime-owned, a collection write is measured like a model17 more candidates were ruled out on review (the app's own names; reviewed in conformance/port-usage.json says why). Rows no app in the cross-check uses: 74 of 258.