@volter/editor-threejs 0.5.211 → 0.5.213

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (133) hide show
  1. package/NOTICE +7 -2
  2. package/README.md +8 -13
  3. package/contributions/animation-mixers.service.ts +2 -4
  4. package/contributions/animation-timeline.utility.tsx +1 -2
  5. package/contributions/three.page.ts +50 -0
  6. package/dist-node/serving.mjs +84 -3
  7. package/dist-node/shared-three.mjs +145 -0
  8. package/package.json +296 -16
  9. package/serving/animation-stamp.ts +1 -1
  10. package/serving/index.ts +22 -5
  11. package/serving/model-import-conversion.ts +6 -3
  12. package/serving/shared-three.ts +176 -0
  13. package/src/adapter/constraint.ts +1 -18
  14. package/src/adapter/object3d-authoring-subject.ts +1 -12
  15. package/src/adapter/reflection-probe.ts +1 -9
  16. package/src/adapter/three-contract.ts +6 -60
  17. package/src/animation/live-mixers.ts +0 -4
  18. package/src/api.ts +7 -0
  19. package/src/asset-loaders.ts +2 -178
  20. package/src/capture/photograph-sampling.ts +333 -0
  21. package/src/capture/scene.ts +1 -1
  22. package/src/ecs/object-marks.ts +6 -14
  23. package/src/ecs/user-data.ts +3 -138
  24. package/src/kit/animation/three-clips-subject.ts +19 -42
  25. package/src/kit/asset-compare-core.ts +161 -0
  26. package/src/kit/asset-compare.ts +8 -27
  27. package/src/kit/asset-preview-command.ts +14 -19
  28. package/src/kit/asset-preview-framing.ts +8 -67
  29. package/src/kit/asset-preview.ts +109 -342
  30. package/src/kit/asset-workflow/model-inspection.ts +11 -154
  31. package/src/kit/asset-workflow/preview-resource-lifetime.ts +25 -0
  32. package/src/kit/authoring/component-instance-root.ts +3 -39
  33. package/src/kit/authoring/live-object-transform.ts +1 -30
  34. package/src/kit/authoring/object3d-document-session-registry.ts +7 -15
  35. package/src/kit/authoring/object3d-document-session.ts +471 -208
  36. package/src/kit/authoring/shell-viewport-policy.ts +18 -24
  37. package/src/kit/authoring/source-object3d-authoring-adapter.ts +6 -48
  38. package/src/kit/authoring/three-projection-core.ts +11 -40
  39. package/src/kit/authoring/viewport-pick-context.ts +5 -12
  40. package/src/kit/authoring/viewport-raycast.ts +15 -93
  41. package/src/kit/authoring/world-hidden-viewport.ts +8 -61
  42. package/src/kit/capture-camera-pose.ts +68 -0
  43. package/src/kit/collaboration-presence.ts +41 -0
  44. package/src/kit/components/CameraInfo.tsx +4 -6
  45. package/src/kit/components/InspectorObjectPreview.tsx +3 -30
  46. package/src/kit/components/Object3DDocumentToolbar.css +75 -0
  47. package/src/kit/components/Object3DDocumentToolbar.tsx +18 -84
  48. package/src/kit/components/Object3DDocumentViewport.tsx +6 -11
  49. package/src/kit/components/StageHost.css +40 -0
  50. package/src/kit/components/StageHost.tsx +192 -439
  51. package/src/kit/components/StageOverlays.tsx +1 -2
  52. package/src/kit/components/StatsOverlay.tsx +5 -17
  53. package/src/kit/components/ViewportFurniture.tsx +27 -154
  54. package/src/kit/components/ViewportOverlay.tsx +13 -17
  55. package/src/kit/components/ViewportOverlaysMenu.tsx +177 -0
  56. package/src/kit/components/ViewportShadingMenu.tsx +2 -42
  57. package/src/kit/components/ViewportViewMenu.tsx +7 -18
  58. package/src/kit/components/asset-viewers/EntityModelDocument.tsx +6 -17
  59. package/src/kit/components/asset-viewers/EnvironmentAssetDocument.tsx +9 -11
  60. package/src/kit/components/asset-viewers/LiveModuleDocument.tsx +31 -133
  61. package/src/kit/components/asset-viewers/LutAssetDocument.tsx +10 -12
  62. package/src/kit/components/asset-viewers/ModelAssetDocument.tsx +4 -8
  63. package/src/kit/components/asset-viewers/Object3DPreview.tsx +13 -64
  64. package/src/kit/components/asset-viewers/QuarksAssetDocument.tsx +6 -10
  65. package/src/kit/components/asset-viewers/ShaderAssetDocument.tsx +48 -65
  66. package/src/kit/components/asset-viewers/shader-source.ts +144 -0
  67. package/src/kit/components/asset-viewers/three-asset-viewers.tsx +2 -14
  68. package/src/kit/components/object3d-contribution-surfaces.tsx +5 -3
  69. package/src/kit/components/stage-keyboard.tsx +6 -12
  70. package/src/kit/components/stage-overlay-set.tsx +11 -20
  71. package/src/kit/components/stage-presence-markers.ts +9 -32
  72. package/src/kit/components/stage-transform-chrome.ts +4 -4
  73. package/src/kit/components/stage-transform-tools.tsx +7 -14
  74. package/src/kit/components/standard-viewport-dressing.ts +47 -167
  75. package/src/kit/components/world-root-binding.ts +2 -7
  76. package/src/kit/constraint-helper.ts +1 -1
  77. package/src/kit/document-renderer-session.ts +114 -0
  78. package/src/kit/editor-presentation-activity.ts +58 -0
  79. package/src/kit/editor-shell-store.ts +31 -403
  80. package/src/kit/editor-viewport.ts +486 -1762
  81. package/src/kit/entity-lod.ts +3 -16
  82. package/src/kit/entity-object.ts +1 -39
  83. package/src/kit/hierarchy-mark-reader.ts +13 -30
  84. package/src/kit/instanced-presentation.ts +1 -47
  85. package/src/kit/live-gesture-lock.ts +35 -0
  86. package/src/kit/live-module-source.ts +12 -52
  87. package/src/kit/model-thumbnail.ts +7 -35
  88. package/src/kit/play-camera-flight.ts +7 -16
  89. package/src/kit/projection/three.ts +13 -366
  90. package/src/kit/retained-document-states.ts +91 -0
  91. package/src/kit/scene-document-viewport.ts +3 -3
  92. package/src/kit/scene-framing.ts +11 -152
  93. package/src/kit/scene-view-fog.ts +2 -47
  94. package/src/kit/spatial-handle-visuals.ts +19 -28
  95. package/src/kit/stories/three-story-model.ts +5 -33
  96. package/src/kit/three-canvas-render.ts +3 -4
  97. package/src/kit/three-hierarchy-row-media.ts +3 -11
  98. package/src/kit/three-inspection-media.ts +6 -6
  99. package/src/kit/three-integration.ts +18 -18
  100. package/src/kit/three-state.ts +2 -19
  101. package/src/kit/three-viewport/camera-fit.ts +2 -6
  102. package/src/kit/three-viewport/selection-brackets.ts +5 -20
  103. package/src/kit/three-viewport/selection-outline.ts +4 -56
  104. package/src/kit/three-viewport/skeleton-helper.ts +1 -32
  105. package/src/kit/three-viewport/source-color.ts +1 -33
  106. package/src/kit/three-viewport/studio-environment.ts +4 -55
  107. package/src/kit/three-viewport-presentation.ts +23 -0
  108. package/src/kit/transport-strip.tsx +147 -0
  109. package/src/kit/viewport-actions.ts +8 -50
  110. package/src/kit/viewport-authoring-policy.ts +5 -52
  111. package/src/kit/viewport-commands.ts +28 -57
  112. package/src/kit/viewport-hotkeys.ts +29 -31
  113. package/src/kit/viewport-status-facet.ts +4 -5
  114. package/src/loader.ts +8 -71
  115. package/src/object3d-contributions.ts +18 -34
  116. package/src/viewport/clip-planes.ts +4 -32
  117. package/src/viewport/content-bounds.ts +13 -142
  118. package/src/viewport/editor-layers.ts +4 -26
  119. package/src/viewport/environment.ts +1 -1
  120. package/src/viewport/preview-renderer.ts +7 -45
  121. package/src/viewport/renderer-ownership.ts +14 -62
  122. package/src/viewport-api.ts +14 -33
  123. package/src/viewport-door.ts +16 -86
  124. package/contributions/three-integration.service.ts +0 -13
  125. package/src/adapter/hierarchy-marks.ts +0 -156
  126. package/src/adapter/ingest/scene-capture.ts +0 -843
  127. package/src/adapter/ingest/structural-ids.ts +0 -139
  128. package/src/adapter/ingest/visible-capture-window.ts +0 -302
  129. package/src/adapter/renderer-config.ts +0 -115
  130. package/src/asset-parse-error.ts +0 -29
  131. package/src/host-hierarchy-objects.ts +0 -31
  132. package/src/kit/authoring/design-time-settle.ts +0 -343
  133. package/src/kit/camera-authoring.ts +0 -175
@@ -12,18 +12,14 @@ export type EditorHelperType =
12
12
  | 'cameras'
13
13
  | 'audio'
14
14
  | 'colliders'
15
- // Joint anchor/axis/limit gizmos (W2a) — distinct from 'colliders' so the
16
- // viewport's per-type visibility filter can toggle them independently.
15
+ // Joint anchor, axis and limit gizmos have their own visibility toggle, independent of colliders.
17
16
  | 'joints'
18
- // LOD distance rings (W2b) — one ring per `mesh.lod` level, radius = the
19
- // level's camera-distance threshold in world units.
17
+ // LOD distance rings use each level’s world-space camera-distance threshold as their radius.
20
18
  | 'lod'
21
19
  | 'splines'
22
- // 'particles' tags the LIVE three.quarks emitter Object3D itself (so the
23
- // editor's helper machinery skips disposing it); 'particle-shape' tags the
24
- // editor's emitter-shape WIREFRAME (W1b) — they must stay distinct: sweeping
25
- // "particles"-tagged children would detach the running emitter, and quarks
26
- // self-disposes any system whose emitter leaves the scene.
20
+ // Keep live three.quarks emitters distinct from editor-owned particle-shape wireframes. The
21
+ // helper sweep must preserve emitters because quarks disposes a system whose emitter leaves the
22
+ // scene.
27
23
  | 'particles'
28
24
  | 'particle-shape'
29
25
  | 'pivot'
@@ -33,7 +29,7 @@ export type EditorHelperType =
33
29
  | 'trigger-volumes'
34
30
  | 'skeletons'
35
31
  // A package's own helper kind, shown through the editor's viewport door
36
- // (the viewport door's `setViewportHelper`): the host lists the kinds it toggles by name,
32
+ // (through the viewport door): the host lists the kinds it toggles by name,
37
33
  // any other follows the master Helpers toggle.
38
34
  | (string & {});
39
35
 
@@ -43,8 +39,6 @@ export interface ObjectMarkSchema {
43
39
  skeletonEnabled: boolean;
44
40
  engineInternal: boolean;
45
41
  editorHelper: boolean;
46
- volterComponentRoot: string;
47
- volterBuiltInternal: boolean;
48
42
  }
49
43
 
50
44
  export const ObjectMarkKeys = {
@@ -53,8 +47,6 @@ export const ObjectMarkKeys = {
53
47
  skeletonEnabled: 'skeletonEnabled',
54
48
  engineInternal: 'engineInternal',
55
49
  editorHelper: 'editorHelper',
56
- volterComponentRoot: 'volterComponentRoot',
57
- volterBuiltInternal: 'volterBuiltInternal',
58
50
  } as const satisfies Record<keyof ObjectMarkSchema, string>;
59
51
 
60
52
  export function getObjectMark<K extends keyof ObjectMarkSchema>(
@@ -1,113 +1,6 @@
1
- /**
2
- * Typed registry for `Object3D.userData` keys used by the engine and editor.
3
- *
4
- * Three.js lets any code stash arbitrary values on `object.userData`, and over
5
- * time the engine + editor accumulated ~20 stringly-typed keys that together
6
- * form a *parallel object model* — meaning carried entirely in undocumented
7
- * string literals. This module is the ONE documented place that lists every
8
- * such key, its value type, and what it means. All engine/editor-internal
9
- * `userData` access for these keys must go through {@link getUserData} /
10
- * {@link setUserData} / {@link hasUserData} / {@link deleteUserData} so the key
11
- * strings live in exactly one place and are type-checked at every call site.
12
- * Structural readers may use the narrow `object-marks` accessors instead;
13
- * this registry includes that module's canonical keys and value schema.
14
- *
15
- * NOTE: This covers engine/editor *internal* keys only. User game code in the
16
- * example template may stash its own ad-hoc keys; those are out of scope.
17
- *
18
- * ## Key reference
19
- *
20
- * Serialization / identity (load-bearing — editor identity):
21
- * - `entityId` — the entity's stable string id. Presence also marks an
22
- * Object3D as a "real" entity (vs editor helper/env object).
23
- *
24
- * Runtime scene-query metadata (read by `scene-query.ts` and `scene-index.ts`):
25
- * - `tags` — string[] tags for `queryByTag` and the live index.
26
- * - `attributes` — per-object `Record<string, string|number|boolean>`
27
- * of game-defined attributes (P2 `observe`). Written
28
- * through `SceneIndex.setAttribute`, which emits an
29
- * `attributechanged` signal; JSON-simple values only,
30
- * so an attribute survives serialization unchanged.
31
- *
32
- * Runtime gameplay metadata:
33
- * - `forward` — `[x,y,z]` model-space visual forward exported as
34
- * ordinary glTF extras for asset-facing validation.
35
- * - `navRole` — `'walkable' | 'obstacle'` navmesh role; collected at runtime.
36
- * - `pivot` — `[x,y,z]` local-space pivot for rotate/scale-around-pivot.
37
- * - `splineCurve` — resolved THREE curve for a spline-following object.
38
- * - `_camera` — THREE.Camera owned by a camera entity (collected on load).
39
- * - `_particleSystem` — three.quarks ParticleSystem (scene-sync registration + census).
40
- * - `gaussianSplat` — native Spark splat metadata used for renderer discovery,
41
- * inspector facts, bounds, and deterministic disposal.
42
- *
43
- * Disposal contract:
44
- * - `__sharedGeometry` — `true` when a mesh's geometry is shared/cached and MUST
45
- * NOT be disposed by per-object cleanup (P0.2 contract).
46
- *
47
- * Editor shading overrides (editor-only, see `scene-sync.ts`):
48
- * - `__shadeOrig` — original material(s) stashed before a shading override.
49
- * - `__shadeUnlit` — generated unlit material(s) for the unlit view mode.
50
- *
51
- * Unmodified-game ingestion (editor-only, see `editor/src/ingest/`):
52
- * - `__ingest` — `true` on every Object3D minted by ingestion; routes
53
- * edits to live mutation.
54
- * - `__ingestNextId` — monotonic id counter stashed on the captured Scene
55
- * root so re-reflects keep minting unique ids.
56
- *
57
- * Editor scene-graph tagging (editor-only):
58
- * - `engineInternal` — `true` on engine-owned infrastructure in the game scene
59
- * (particle BatchedRenderer, debug-draw + its subtree).
60
- * The editor's play-mode hierarchy skips these so they
61
- * don't show up as selectable "entities".
62
- * - `authoringRoot` — explicit opt-in for a runtime descendant to become
63
- * its own authoring object instead of a part of the
64
- * nearest ancestor owner.
65
- * - `authoringInstance` — source OID of the custom-component callsite that
66
- * rendered this existing Object3D. Used to group
67
- * implementation parts without adding wrappers.
68
- * - `authoringLabel` — authored literal name (or component tag fallback)
69
- * for that source-backed component instance.
70
- * - `authoringComponent` — exact source component identity for portable-CSF
71
- * association; unlike the label, never user-facing copy.
72
- * - `authoringDocument` — generic reference to the project-tool document
73
- * that owns this scene instance's authored asset.
74
- * - `authoringHierarchyId`, `authoringHierarchyParentId`,
75
- * `authoringHierarchyOrder` — source-owned semantic identity, parent identity,
76
- * and sibling ordinal shared across native surfaces.
77
- * - `editorHelper` — `true` for editor-only helper objects (gizmos, wireframes).
78
- *
79
- * Hierarchy-presentation convention (written by GAME code, read by the editor —
80
- * see `../adapter/hierarchy-marks.ts`, which is the ONLY place these two are
81
- * read/written from; the `volter` prefix marks them as the HOST's namespace on a
82
- * node a game owns, unlike every other key above, which the engine/editor also
83
- * write):
84
- * - `volterComponentRoot` — display name of the component instance this subtree
85
- * IS. The node renders as one collapsed, expandable
86
- * row named for it.
87
- * - `volterBuiltInternal` — `true` on the ROOT of a subtree runtime code
88
- * CONSTRUCTED rather than authored (skeleton bones,
89
- * particle renderers). Subtree-scoped: everything
90
- * below a marked node is built-internal too.
91
- * - `editorHelperType` — which kind of helper (lights/particles/pivot/navmesh/...).
92
- * - `editorIcon` — `true` for editor billboard icon sprites.
93
- * - `skeletonVisible` — per-entity editor preference for its bone overlay.
94
- * - `skeletonEnabled` — resolved visibility preference on a skeleton helper.
95
- * - `envObject` — `true` for environment objects (ambient light, etc.).
96
- * - `reflectionProbe` — live project-owned reflection probe projected from
97
- * JSX props for renderer/editor integration.
98
- * - `triggerVolume` — plain `{ radius }` a GAME writes on the node that IS
99
- * a trigger volume, so the editor can draw its ring
100
- * (see `../adapter/trigger-volume.ts`).
101
- * - `constraints` — live project-owned spatial constraints projected
102
- * into the shared Inspector and viewport.
103
- * - `authoringSubject` — transient identity/inspection for an ecosystem-native
104
- * subject represented by an Object3D proxy.
105
- * - `splineControlPoint` — index of a spline control-point drag handle.
106
- * - `vcDirIdx` — view-cube face direction index (0=+X,1=-X,2=+Y,...).
107
- */
1
+ /** Typed Object3D.userData keys shared by the engine and editor. Internal accesses use getUserData, setUserData and hasUserData; structural consumers may use object-marks accessors. Project-defined extras remain outside this registry. */
108
2
 
109
3
  import type * as THREE from 'three';
110
- import type { ParticleSystem } from 'three.quarks';
111
4
  import type { ConstraintMark } from '../adapter/constraint';
112
5
  import type { Object3DAuthoringSubjectMark } from '../adapter/object3d-authoring-subject';
113
6
  import type { ReflectionProbeMark } from '../adapter/reflection-probe';
@@ -125,27 +18,14 @@ export interface UserDataSchema extends ObjectMarkSchema {
125
18
  forward: [number, number, number];
126
19
  tags: string[];
127
20
  attributes: Record<string, string | number | boolean>;
128
- navRole: 'walkable' | 'obstacle';
129
21
  pivot: [number, number, number];
130
- splineCurve: THREE.Curve<THREE.Vector3>;
131
- splineControlPoint: number;
132
22
  _camera: THREE.Camera;
133
- _particleSystem: ParticleSystem;
134
23
  gaussianSplat: { src: string; numSplats: number };
135
- __sharedGeometry: boolean;
136
- __shadeOrig: THREE.Material | THREE.Material[];
137
- __shadeUnlit: THREE.Material[];
138
24
  __ingest: boolean;
139
- __ingestNextId: number;
140
25
  authoringRoot: boolean;
141
26
  authoringInstance: string;
142
27
  authoringLabel: string;
143
28
  authoringComponent: string;
144
- authoringDocument: {
145
- readonly kind: 'project-tool';
146
- readonly name: string;
147
- readonly title: string;
148
- };
149
29
  /** Project-owned identity used to join authored hierarchy rows across adapter roots. */
150
30
  authoringHierarchyId: string;
151
31
  /** Project-owned parent identity when the authored parent can live on another surface. */
@@ -165,9 +45,8 @@ export interface UserDataSchema extends ObjectMarkSchema {
165
45
  export type UserDataKey = keyof UserDataSchema;
166
46
 
167
47
  /**
168
- * Canonical name → literal `userData` string key. The accessor names match the
169
- * raw string keys 1:1 (this is the documented set referenced by the AC), so the
170
- * literal strings are owned here or by the included structural mark registry.
48
+ * Canonical accessor names map to literal userData keys owned here or by the structural mark
49
+ * registry.
171
50
  */
172
51
  export const UserDataKeys = {
173
52
  ...ObjectMarkKeys,
@@ -175,23 +54,14 @@ export const UserDataKeys = {
175
54
  forward: 'forward',
176
55
  tags: 'tags',
177
56
  attributes: 'attributes',
178
- navRole: 'navRole',
179
57
  pivot: 'pivot',
180
- splineCurve: 'splineCurve',
181
- splineControlPoint: 'splineControlPoint',
182
58
  _camera: '_camera',
183
- _particleSystem: '_particleSystem',
184
59
  gaussianSplat: 'gaussianSplat',
185
- __sharedGeometry: '__sharedGeometry',
186
- __shadeOrig: '__shadeOrig',
187
- __shadeUnlit: '__shadeUnlit',
188
60
  __ingest: '__ingest',
189
- __ingestNextId: '__ingestNextId',
190
61
  authoringRoot: 'authoringRoot',
191
62
  authoringInstance: 'authoringInstance',
192
63
  authoringLabel: 'authoringLabel',
193
64
  authoringComponent: 'authoringComponent',
194
- authoringDocument: 'authoringDocument',
195
65
  authoringHierarchyId: 'authoringHierarchyId',
196
66
  authoringHierarchyParentId: 'authoringHierarchyParentId',
197
67
  authoringHierarchyOrder: 'authoringHierarchyOrder',
@@ -228,8 +98,3 @@ export function setUserData<K extends UserDataKey>(
228
98
  export function hasUserData(obj: THREE.Object3D | null | undefined, key: UserDataKey): boolean {
229
99
  return obj ? obj.userData[UserDataKeys[key]] !== undefined : false;
230
100
  }
231
-
232
- /** Remove a `userData` key. */
233
- export function deleteUserData(obj: THREE.Object3D, key: UserDataKey): void {
234
- delete obj.userData[UserDataKeys[key]];
235
- }
@@ -1,33 +1,13 @@
1
1
  /**
2
- * WHAT A THREE WORLD CAN SHOW AT A TIME — the stage transport's subjects for
3
- * an ordinary three/R3F stage (WORK.md §The stage transport and the animation
4
- * door, Step 3).
5
- *
6
- * TWO DISCOVERIES, because a three world carries clips in two different
7
- * places and only one of them is the obvious one:
8
- *
9
- * 1. `object.animations` — what a GLTF's own `AnimationClip[]` lands in when
10
- * something assigns it. A subject here owns a NEW `AnimationMixer` on that
11
- * object, because nothing else is playing it.
12
- * 2. `liveMixerFor(object)` — the mixer the world's OWN code made for it
13
- * (`new THREE.AnimationMixer(…)`, drei's `useAnimations`), found through
14
- * the served animation stamp (`@volter/editor-threejs/animation/live-mixers`).
15
- * A subject here drives THAT mixer; minting a second mixer over the same
16
- * object would give one skeleton two writers.
17
- *
18
- * DISCOVERY 2 IS NOT OPTIONAL, and this is the measurement that says so:
19
- * three's `GLTFLoader` never assigns `scene.animations`, so an R3F character
20
- * loaded with `useGLTF` has an EMPTY `animations` array on its Object3D. A
21
- * scan that looked only at `animations` would find nothing on exactly the
22
- * worlds a person most wants to scrub.
23
- *
24
- * THE MIXER IS THREE'S OWN and every call here is three's own API — this is a
25
- * subject over `AnimationMixer`, never a wrapper around it. `setTime` is the
26
- * one write; the transport owns the position.
2
+ * Stage-transport subjects for three.js animation. Discover object.animations and
3
+ * liveMixerFor(object): standalone clip arrays get a new AnimationMixer, while a mixer owned by
4
+ * the world is borrowed. GLTFLoader does not assign scene.animations, so live mixer discovery also
5
+ * reaches clips loaded through useGLTF and useAnimations. Subjects seek through
6
+ * AnimationMixer.setTime; the transport owns playback position.
27
7
  */
28
8
  import { liveMixerFor, subscribeLiveMixers } from '@volter/editor-threejs/animation/live-mixers';
29
9
  import * as THREE from 'three';
30
- import type { StageTransport } from '@volter/sdk/kit/animation/stage-transport';
10
+ import type { StageTransport } from '@volter/sdk/animation/stage-transport';
31
11
 
32
12
  /** The display rate a three clip is shown at. Three clips carry a duration in
33
13
  * SECONDS and no rate of their own, so the transport needs one to convert a
@@ -51,8 +31,9 @@ function attachOwnClips(object: THREE.Object3D, transport: StageTransport): (()
51
31
  const clips = object.animations;
52
32
  if (clips.length === 0) return null;
53
33
  const mixer = new THREE.AnimationMixer(object);
54
- let current = clips[0];
55
- if (!current) return null;
34
+ const initial = clips[0];
35
+ if (!initial) return null;
36
+ let current: THREE.AnimationClip = initial;
56
37
 
57
38
  const show = (clip: THREE.AnimationClip): void => {
58
39
  mixer.stopAllAction();
@@ -66,9 +47,9 @@ function attachOwnClips(object: THREE.Object3D, transport: StageTransport): (()
66
47
  const detach = transport.attach({
67
48
  id: object.uuid,
68
49
  label: object.name || current.name || 'Clip',
69
- range: () => ({ start: 0, end: current?.duration ?? 0, fps: THREE_DISPLAY_FPS }),
50
+ range: () => ({ start: 0, end: current.duration, fps: THREE_DISPLAY_FPS }),
70
51
  seek: (seconds) => {
71
- mixer.setTime(clamp(seconds, 0, current?.duration ?? 0));
52
+ mixer.setTime(clamp(seconds, 0, current.duration));
72
53
  },
73
54
  clips: () => clips.map((clip) => ({ id: clip.name, label: clip.name })),
74
55
  setClip: (id) => {
@@ -91,8 +72,9 @@ function attachOwnClips(object: THREE.Object3D, transport: StageTransport): (()
91
72
  function attachInspected(object: THREE.Object3D, transport: StageTransport): (() => void) | null {
92
73
  const inspection = liveMixerFor(object);
93
74
  if (!inspection) return null;
94
- let current = inspection.clips.values().next().value;
95
- if (!current) return null;
75
+ const initial = inspection.clips.values().next().value;
76
+ if (!initial) return null;
77
+ let current: THREE.AnimationClip = initial;
96
78
 
97
79
  const show = (clip: THREE.AnimationClip): void => {
98
80
  inspection.mixer.stopAllAction();
@@ -105,9 +87,9 @@ function attachInspected(object: THREE.Object3D, transport: StageTransport): (()
105
87
  const detach = transport.attach({
106
88
  id: object.uuid,
107
89
  label: object.name || current.name || 'Animation',
108
- range: () => ({ start: 0, end: current?.duration ?? 0, fps: THREE_DISPLAY_FPS }),
90
+ range: () => ({ start: 0, end: current.duration, fps: THREE_DISPLAY_FPS }),
109
91
  seek: (seconds) => {
110
- inspection.mixer.setTime(clamp(seconds, 0, current?.duration ?? 0));
92
+ inspection.mixer.setTime(clamp(seconds, 0, current.duration));
111
93
  },
112
94
  clips: () => [...inspection.clips.entries()].map(([name, clip]) => ({ id: name, label: clip.name || name })),
113
95
  setClip: (id) => {
@@ -140,8 +122,8 @@ export function scanClipSubjects(
140
122
  root: THREE.Object3D | (() => THREE.Object3D | null),
141
123
  transport: StageTransport,
142
124
  options: {
143
- /** Rescan when the game registers or drops a mixer, while this answers true. */
144
- readonly rescanOnLiveMixers?: () => boolean;
125
+ /** Rescan when the game registers or drops a mixer. */
126
+ readonly rescanOnLiveMixers?: boolean;
145
127
  } = {},
146
128
  ): ClipSubjectScan {
147
129
  const attached = new Map<string, () => void>();
@@ -172,12 +154,7 @@ export function scanClipSubjects(
172
154
  };
173
155
 
174
156
  refresh();
175
- const rescanWhen = options.rescanOnLiveMixers;
176
- const stopMixers = rescanWhen
177
- ? subscribeLiveMixers(() => {
178
- if (rescanWhen()) refresh();
179
- })
180
- : () => {};
157
+ const stopMixers = options.rescanOnLiveMixers ? subscribeLiveMixers(refresh) : () => {};
181
158
 
182
159
  return {
183
160
  refresh,
@@ -0,0 +1,161 @@
1
+
2
+
3
+ export const ASSET_COMPARE_VIEWS = ['front', 'side'] as const;
4
+ export type AssetCompareView = (typeof ASSET_COMPARE_VIEWS)[number];
5
+
6
+ /** Both models are scaled to this height, feet at y=0, centered on x=z=0. */
7
+ export const COMPARE_TARGET_HEIGHT = 1;
8
+ /**
9
+ * Fixed orthographic half-height around the model’s mid-height, with a 10% margin for reproducible
10
+ * silhouette comparisons.
11
+ */
12
+ export const COMPARE_FRAME_HALF_HEIGHT = 0.55;
13
+ export const COMPARE_FRAME_CENTER_Y = COMPARE_TARGET_HEIGHT / 2;
14
+
15
+ /** Silhouettes render as a flat unlit white override on solid black, so the
16
+ * mask threshold sits mid-range — antialiased edge pixels land on the side
17
+ * of the ramp they dominate. */
18
+ export const SILHOUETTE_LUMA_THRESHOLD = 128;
19
+
20
+ /** Overlay legend (RGB). The overlay is machine-generated review evidence:
21
+ * every pixel is exactly one of these four colors. */
22
+ export const OVERLAY_BACKGROUND_RGB = [24, 26, 31] as const;
23
+ export const OVERLAY_ASSET_ONLY_RGB = [235, 137, 52] as const; // orange — the asset under test
24
+ export const OVERLAY_REF_ONLY_RGB = [56, 189, 248] as const; // cyan — the reference
25
+ export const OVERLAY_OVERLAP_RGB = [236, 240, 238] as const; // near-white — agreement
26
+
27
+ /**
28
+ * Binary silhouette mask from RGBA pixels of a white-on-black render: a
29
+ * pixel belongs to the silhouette when any color channel crosses the
30
+ * threshold. Returns one byte (0|1) per pixel.
31
+ */
32
+ export function silhouetteMaskFromRgba(rgba: ArrayLike<number>, pixelCount: number): Uint8Array {
33
+ if (rgba.length < pixelCount * 4) {
34
+ throw new Error(
35
+ `Silhouette mask expected ${pixelCount * 4} RGBA bytes but received ${rgba.length}.`,
36
+ );
37
+ }
38
+ const mask = new Uint8Array(pixelCount);
39
+ for (let pixel = 0; pixel < pixelCount; pixel++) {
40
+ const offset = pixel * 4;
41
+ const value =
42
+ rgba[offset]! >= SILHOUETTE_LUMA_THRESHOLD ||
43
+ rgba[offset + 1]! >= SILHOUETTE_LUMA_THRESHOLD ||
44
+ rgba[offset + 2]! >= SILHOUETTE_LUMA_THRESHOLD;
45
+ mask[pixel] = value ? 1 : 0;
46
+ }
47
+ return mask;
48
+ }
49
+
50
+ export interface MaskOverlapStats {
51
+ intersection: number;
52
+ union: number;
53
+ aCount: number;
54
+ bCount: number;
55
+ /** intersection / union; 0 when the union is empty (guard emptiness upstream). */
56
+ iou: number;
57
+ }
58
+
59
+ /** Silhouette intersection-over-union between two same-length binary masks. */
60
+ export function maskIoU(a: Uint8Array, b: Uint8Array): MaskOverlapStats {
61
+ if (a.length !== b.length) {
62
+ throw new Error(`Mask IoU requires equal-size masks (${a.length} vs ${b.length}).`);
63
+ }
64
+ let intersection = 0;
65
+ let aCount = 0;
66
+ let bCount = 0;
67
+ for (let index = 0; index < a.length; index++) {
68
+ const inA = a[index] !== 0;
69
+ const inB = b[index] !== 0;
70
+ if (inA) aCount++;
71
+ if (inB) bCount++;
72
+ if (inA && inB) intersection++;
73
+ }
74
+ const union = aCount + bCount - intersection;
75
+ return { intersection, union, aCount, bCount, iou: union === 0 ? 0 : intersection / union };
76
+ }
77
+
78
+ /**
79
+ * The compare overlay: both silhouettes on one image in visibly distinct
80
+ * colors — asset-only orange, reference-only cyan, agreement near-white on a
81
+ * dark background. Returns opaque RGBA bytes ready for an ImageData.
82
+ */
83
+ export function overlayRgba(
84
+ assetMask: Uint8Array,
85
+ refMask: Uint8Array,
86
+ ): Uint8ClampedArray<ArrayBuffer> {
87
+ if (assetMask.length !== refMask.length) {
88
+ throw new Error(
89
+ `Overlay requires equal-size masks (${assetMask.length} vs ${refMask.length}).`,
90
+ );
91
+ }
92
+ const out = new Uint8ClampedArray(assetMask.length * 4);
93
+ for (let pixel = 0; pixel < assetMask.length; pixel++) {
94
+ const inAsset = assetMask[pixel] !== 0;
95
+ const inRef = refMask[pixel] !== 0;
96
+ const color =
97
+ inAsset && inRef
98
+ ? OVERLAY_OVERLAP_RGB
99
+ : inAsset
100
+ ? OVERLAY_ASSET_ONLY_RGB
101
+ : inRef
102
+ ? OVERLAY_REF_ONLY_RGB
103
+ : OVERLAY_BACKGROUND_RGB;
104
+ const offset = pixel * 4;
105
+ out[offset] = color[0];
106
+ out[offset + 1] = color[1];
107
+ out[offset + 2] = color[2];
108
+ out[offset + 3] = 255;
109
+ }
110
+ return out;
111
+ }
112
+
113
+ /** A structurally valid forward vector ([x,y,z], finite, non-degenerate in
114
+ * the ground plane) or null — used to read GLB `extras`/`userData.forward`
115
+ * without trusting arbitrary content. */
116
+ export function parseForwardVector(value: unknown): [number, number, number] | null {
117
+ if (!Array.isArray(value) || value.length !== 3) return null;
118
+ const [x, y, z] = value as unknown[];
119
+ if (typeof x !== 'number' || typeof y !== 'number' || typeof z !== 'number') return null;
120
+ if (!Number.isFinite(x) || !Number.isFinite(y) || !Number.isFinite(z)) return null;
121
+ if (Math.hypot(x, z) < 1e-6) return null; // vertical/zero forward cannot define a facing
122
+ return [x, y, z];
123
+ }
124
+
125
+ /**
126
+ * The Y rotation (radians) that turns a model whose forward is `forward`
127
+ * to face +Z — the direction the fixed front camera looks from. Facing is a
128
+ * ground-plane property; the vector's y component is ignored.
129
+ */
130
+ export function forwardYawRadians(forward: readonly [number, number, number]): number {
131
+ const [x, , z] = forward;
132
+ if (!Number.isFinite(x) || !Number.isFinite(z) || Math.hypot(x, z) < 1e-6) {
133
+ throw new Error(`Cannot derive a facing from forward vector [${forward.join(', ')}].`);
134
+ }
135
+ return Math.atan2(x, z);
136
+ }
137
+
138
+ export interface NormalizedPlacement {
139
+ scale: number;
140
+ /** Applied AFTER the scale: ground at y=0, footprint centered at x=z=0. */
141
+ position: [number, number, number];
142
+ }
143
+
144
+ /**
145
+ * Bounding-box height normalization (the decided compare framing): scale the
146
+ * model so its bounds stand `COMPARE_TARGET_HEIGHT` tall, then place it with its
147
+ * ground plane at y=0 and its bounds center on the x=0/z=0 axis.
148
+ */
149
+ export function normalizedPlacement(
150
+ min: readonly [number, number, number],
151
+ max: readonly [number, number, number],
152
+ ): NormalizedPlacement {
153
+ const height = max[1] - min[1];
154
+ if (!Number.isFinite(height) || height <= 0) {
155
+ throw new Error(`Compare normalization needs positive model height, got ${height}.`);
156
+ }
157
+ const scale = COMPARE_TARGET_HEIGHT / height;
158
+ const centerX = (min[0] + max[0]) / 2;
159
+ const centerZ = (min[2] + max[2]) / 2;
160
+ return { scale, position: [-centerX * scale, -min[1] * scale, -centerZ * scale] };
161
+ }
@@ -1,23 +1,9 @@
1
1
  /**
2
- * B8.4 — the Asset Lab COMPARE surface (the editor's `screenshot <model.glb>
3
- * --compare <ref.glb>` command): renders the project asset AND a caller-supplied reference GLB
4
- * with matched orthographic front + side framing, then scores their
5
- * silhouettes (IoU) and composes review overlays, so an agent can
6
- * numerically converge a procedural character toward a reference.
7
- *
8
- * Framing is DELIBERATELY bounding-box based (equal-height normalization,
9
- * shared ground plane, centered footprint) rather than the humanoid shot
10
- * set's Mixamo-skeleton anchors: the reference may carry any rig (UE joint
11
- * names, no rig at all), and the bone-anchored path throws on missing joints
12
- * by design. Both models are yaw-normalized to FACE the front camera —
13
- * forward read from GLB `extras`/`userData.forward` when present (our
14
- * generated rigs persist `[0,0,-1]`), else the glTF +Z convention, with an
15
- * explicit reference override for models whose extras lie.
16
- *
17
- * Pure scoring math lives in `asset-compare-core.ts` (unit-tested in node);
18
- * this module owns only loading, scene assembly, and WebGL capture. Fixed
19
- * cameras + fixed resolution + flat unlit override ⇒ identical IoU numbers
20
- * across runs (a stated acceptance gate).
2
+ * Asset Lab comparison capture for screenshot --compare: load an asset and reference GLB, normalize
3
+ * their height and forward direction, capture matched orthographic front and side views, and
4
+ * compose silhouette IoU scores and overlays. Bounding-box framing supports arbitrary rigs.
5
+ * asset-compare-core.ts owns the scoring math; this module owns loading, scene assembly and WebGL
6
+ * capture.
21
7
  */
22
8
 
23
9
  import { markHostRenderer } from '@volter/editor-threejs/viewport/renderer-ownership';
@@ -32,7 +18,7 @@ import {
32
18
  normalizedPlacement,
33
19
  overlayRgba,
34
20
  silhouetteMaskFromRgba,
35
- } from '@volter/sdk/kit/asset-compare-core';
21
+ } from './asset-compare-core';
36
22
  import {
37
23
  checkedDimension,
38
24
  createAssetPreviewSnapshot,
@@ -45,10 +31,7 @@ import {
45
31
  readModelForward,
46
32
  } from './asset-preview';
47
33
 
48
- export type { AssetCompareView } from '@volter/sdk/kit/asset-compare-core';
49
- // The forward detection moved to `asset-preview.ts` (the shot sets need it
50
- // too); re-exported here so compare-mode consumers keep their import path.
51
- export { readModelForward } from './asset-preview';
34
+ export type { AssetCompareView } from './asset-compare-core';
52
35
 
53
36
  export interface AssetCompareImage {
54
37
  base64: string;
@@ -85,9 +68,7 @@ const SILHOUETTE_BACKGROUND = 0x000000;
85
68
  const SILHOUETTE_FOREGROUND = 0xffffff;
86
69
  const COMPARE_CAMERA_DISTANCE = 10;
87
70
 
88
- /** The base64-GLB decoder moved to `asset-preview.ts` (`parseGlbBytesModel`)
89
- * when the module-look lane needed the same wire-carried bytes; this keeps
90
- * compare's own error wording. */
71
+ /** Parse the reference GLB using comparison-specific error wording. */
91
72
  function parseRefGlb(base64: string): Promise<THREE.Object3D> {
92
73
  return parseGlbBytesModel(base64, 'Compare reference GLB');
93
74
  }
@@ -5,7 +5,7 @@
5
5
  * viewport's relay verbs (`viewport-commands.ts`).
6
6
  */
7
7
  import type { EditorCommandMessage, EditorCommandResult } from '@volter/sdk/commands';
8
- import { parseForwardVector } from '@volter/sdk/kit/asset-compare-core';
8
+ import { parseForwardVector } from './asset-compare-core';
9
9
  import { captureEntityComparePreview, captureModelComparePreview } from './asset-compare';
10
10
  import {
11
11
  type AssetPreviewBackground,
@@ -20,8 +20,8 @@ import {
20
20
  captureSourceReviewShotSetModelPreview,
21
21
  parseShotSetDefinition,
22
22
  } from './asset-preview';
23
- import { activeDocumentAuthoring } from '@volter/sdk/kit/authoring/shell-document-ops';
24
- import { parseCameraChoice, parsePoseChoice } from '@volter/sdk/kit/capture-camera-pose';
23
+ import { activeDocumentAuthoring } from '@volter/sdk/authoring/shell-document-ops';
24
+ import { parseCameraChoice, parsePoseChoice } from './capture-camera-pose';
25
25
  import type { EditorShellStore } from './editor-shell-store';
26
26
  import { entityObject3D } from './entity-object';
27
27
 
@@ -32,9 +32,8 @@ export async function handleAssetPreviewCommand(
32
32
  const assetPath = cmd['assetPath'];
33
33
  const entityId = cmd['entityId'];
34
34
  // The THIRD source: a GLB that travels IN the command rather than being
35
- // fetched or looked up — the module-look lane (`project.bake.preview`)
36
- // builds an Object3D in Node, where there is no GPU, and hands the editor
37
- // the exported bytes. Counted rather than XOR-ed because there are now more
35
+ // fetched or looked up — a Node caller that built an Object3D, where there
36
+ // is no GPU, hands the editor the exported bytes. Counted rather than XOR-ed because there are now more
38
37
  // than two sources and "exactly one" has to stay exactly one.
39
38
  const glbBase64 = cmd['glbBase64'];
40
39
  const sourceCount =
@@ -97,9 +96,8 @@ export async function handleAssetPreviewCommand(
97
96
  'definition, compare, or stage "scene" — those legs stage their own cameras and poses.',
98
97
  };
99
98
  }
100
- // A project-defined labeled shot set travels WITH the command (the CLI
101
- // resolves `--shots <set>` through the registered
102
- // `project.<set>.previewShots` tool); validate the untrusted
99
+ // A caller-defined labeled shot set travels WITH the command (`live`'s
100
+ // `assetPreviewShots`); validate the untrusted
103
101
  // definition at the relay boundary so a malformed contribution fails with
104
102
  // a named reason instead of a deep three.js error.
105
103
  let shotSetDefinition: ReturnType<typeof parseShotSetDefinition> | undefined;
@@ -114,11 +112,11 @@ export async function handleAssetPreviewCommand(
114
112
  if (shots === 'source' && parsedForward === null) {
115
113
  return {
116
114
  ok: false,
117
- error: 'capture-asset-preview --shots source requires a non-degenerate forward [x,y,z].',
115
+ error: 'capture-asset-preview shots "source" requires a non-degenerate forward [x,y,z].',
118
116
  };
119
117
  }
120
118
  if (compare !== undefined) {
121
- // B8.4 — the compare mode (the editor's `screenshot <model.glb> --compare <ref.glb>` command).
119
+ // Compare mode serves the editor's `screenshot <model.glb> --compare <ref.glb>` command.
122
120
  // Validated here at the relay boundary so a malformed payload fails with
123
121
  // a named reason instead of a deep three.js error.
124
122
  if (shots !== undefined || shotSetDefinition !== undefined) {
@@ -142,8 +140,8 @@ export async function handleAssetPreviewCommand(
142
140
  // Wire-carried GLB bytes stand NOWHERE, so they take no scene stage and no
143
141
  // compare (whose reference is named some other way). An explicit `shotSet`
144
142
  // definition is a different matter and is served: a shot set stages the
145
- // subject itself, which is what lets the editor's `screenshot <module> --orbit <n>` command
146
- // circle a model that only ever existed as bytes. `shots: 'source'` stays
143
+ // subject itself, which is what lets a caller circle a model that only
144
+ // ever existed as bytes. `shots: 'source'` stays
147
145
  // out — it is the asset-path review set, keyed to a stored forward vector.
148
146
  if (typeof glbBase64 === 'string') {
149
147
  if (shots !== undefined || compare !== undefined) {
@@ -191,12 +189,9 @@ export async function handleAssetPreviewCommand(
191
189
  };
192
190
  }
193
191
  }
194
- // Resolve the screenshot subject through the SAME active-document adapter
195
- // that produced `status.entities`, drives Hierarchy/Inspector selection and
196
- // answers `frame-entity`. An adopted Play scene can own a stable OID in its
197
- // projection without publishing that object through the shell's edit-mode
198
- // `objectMap`; checking the map alone made the final door reject an id every
199
- // preceding door had just accepted.
192
+ // Resolve screenshot subjects through the active-document adapter used by status, Hierarchy,
193
+ // Inspector and frame-entity. An adopted scene’s projected ids need not appear in the shell’s
194
+ // edit-mode object map.
200
195
  const entityObject =
201
196
  typeof entityId === 'string'
202
197
  ? entityObject3D(activeDocumentAuthoring(store.shell), store.objectMap, entityId)