@volter/editor-threejs 0.5.210 → 0.5.212

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 +5 -8
  49. package/src/kit/components/StageHost.css +40 -0
  50. package/src/kit/components/StageHost.tsx +181 -404
  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 +4 -13
  59. package/src/kit/components/asset-viewers/EnvironmentAssetDocument.tsx +5 -5
  60. package/src/kit/components/asset-viewers/LiveModuleDocument.tsx +24 -102
  61. package/src/kit/components/asset-viewers/LutAssetDocument.tsx +6 -6
  62. package/src/kit/components/asset-viewers/Object3DPreview.tsx +10 -45
  63. package/src/kit/components/asset-viewers/QuarksAssetDocument.tsx +2 -2
  64. package/src/kit/components/asset-viewers/ShaderAssetDocument.tsx +11 -10
  65. package/src/kit/components/asset-viewers/shader-source.ts +144 -0
  66. package/src/kit/components/asset-viewers/three-asset-viewers.tsx +2 -14
  67. package/src/kit/components/object3d-contribution-surfaces.tsx +1 -1
  68. package/src/kit/components/stage-keyboard.tsx +6 -12
  69. package/src/kit/components/stage-overlay-set.tsx +11 -20
  70. package/src/kit/components/stage-presence-markers.ts +9 -32
  71. package/src/kit/components/stage-transform-chrome.ts +4 -4
  72. package/src/kit/components/stage-transform-tools.tsx +7 -14
  73. package/src/kit/components/standard-viewport-dressing.ts +47 -167
  74. package/src/kit/components/world-root-binding.ts +2 -7
  75. package/src/kit/constraint-helper.ts +1 -1
  76. package/src/kit/document-renderer-session.ts +114 -0
  77. package/src/kit/editor-presentation-activity.ts +58 -0
  78. package/src/kit/editor-shell-store.ts +31 -403
  79. package/src/kit/editor-viewport.ts +476 -1762
  80. package/src/kit/entity-lod.ts +3 -16
  81. package/src/kit/entity-object.ts +1 -39
  82. package/src/kit/hierarchy-mark-reader.ts +13 -30
  83. package/src/kit/instanced-presentation.ts +1 -47
  84. package/src/kit/live-gesture-lock.ts +35 -0
  85. package/src/kit/live-module-source.ts +12 -52
  86. package/src/kit/model-thumbnail.ts +7 -35
  87. package/src/kit/play-camera-flight.ts +7 -16
  88. package/src/kit/projection/three.ts +13 -366
  89. package/src/kit/retained-document-states.ts +91 -0
  90. package/src/kit/scene-document-viewport.ts +3 -3
  91. package/src/kit/scene-framing.ts +11 -152
  92. package/src/kit/scene-view-fog.ts +2 -47
  93. package/src/kit/spatial-handle-visuals.ts +19 -28
  94. package/src/kit/stories/three-story-model.ts +5 -33
  95. package/src/kit/three-canvas-render.ts +3 -4
  96. package/src/kit/three-hierarchy-row-media.ts +3 -11
  97. package/src/kit/three-inspection-media.ts +6 -6
  98. package/src/kit/three-integration.ts +18 -18
  99. package/src/kit/three-state.ts +2 -19
  100. package/src/kit/three-viewport/camera-fit.ts +2 -6
  101. package/src/kit/three-viewport/selection-brackets.ts +5 -20
  102. package/src/kit/three-viewport/selection-outline.ts +4 -56
  103. package/src/kit/three-viewport/skeleton-helper.ts +1 -32
  104. package/src/kit/three-viewport/source-color.ts +1 -33
  105. package/src/kit/three-viewport/studio-environment.ts +4 -55
  106. package/src/kit/three-viewport-presentation.ts +23 -0
  107. package/src/kit/transport-strip.tsx +147 -0
  108. package/src/kit/viewport-actions.ts +8 -50
  109. package/src/kit/viewport-authoring-policy.ts +5 -52
  110. package/src/kit/viewport-commands.ts +28 -57
  111. package/src/kit/viewport-hotkeys.ts +29 -31
  112. package/src/kit/viewport-status-facet.ts +4 -5
  113. package/src/kit/viewport-surface-status.tsx +44 -0
  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
@@ -2,47 +2,41 @@
2
2
  * THE SHELL'S ANSWERS to `ViewportAuthoringPolicy` — the composition the
3
3
  * editor installs at boot (`EditorContext.tsx`'s `EditorProvider`).
4
4
  *
5
- * Every member here is a one-line delegation to the module that already owned
6
- * that answer. Nothing moved and nothing was reimplemented: the point of the
7
- * seam is only that `editor-viewport.ts` no longer imports these modules, so
5
+ * Every member here is a one-line delegation to the module that owns that
6
+ * answer. The point of the seam is only that `editor-viewport.ts` does not
7
+ * import these modules, so
8
8
  * the gizmo viewport does not carry the composite adapter, the workspace
9
9
  * document registry, or the layered-pick walk in its own closure
10
- * (ARCHITECTURE-CORE §Editor chrome, "the viewport stack is separable").
10
+ *.
11
11
  *
12
12
  * This file is the shell side of that boundary and is free to import all of
13
13
  * them.
14
14
  */
15
15
 
16
16
  import type { ViewportAuthoringPolicy } from '../viewport-authoring-policy';
17
- import { getActiveAuthoring } from '@volter/sdk/kit/authoring/active-adapter';
18
- import { CompositeAuthoringAdapter } from '@volter/sdk/kit/authoring/composite-authoring-adapter';
19
- import { pickTopmost } from '@volter/sdk/kit/authoring/layered-pick';
20
- import { resolvePanelAuthoring } from '@volter/sdk/kit/authoring/panel-authoring';
21
- import { isViewportToolContextVisible, resolveViewportToolContext } from '@volter/sdk/kit/authoring/viewport-tool-context';
22
- import {
23
- applyRootHiddenVisibility,
24
- isThreejsSurfaceVisible,
25
- resolveThreeViewportRootId,
26
- suppressRootEnvironment,
27
- } from './world-hidden-viewport';
17
+ import { getActiveAuthoring } from '@volter/sdk/authoring/active-adapter';
18
+ import { CompositeAuthoringAdapter } from '@volter/sdk/authoring/composite-authoring-adapter';
19
+ import { pickTopmost } from '@volter/sdk/authoring/layered-pick';
20
+ import { resolvePanelAuthoring } from '@volter/sdk/authoring/panel-authoring';
21
+ import { isViewportToolContextVisible, resolveViewportToolContext } from '@volter/sdk/authoring/viewport-tool-context';
22
+ import { isStageVisible, resolveStageRootId } from '@volter/sdk/authoring/stage-root';
23
+ import { applyRootHiddenVisibility, suppressRootEnvironment } from './world-hidden-viewport';
28
24
 
29
25
  export const SHELL_VIEWPORT_AUTHORING_POLICY: ViewportAuthoringPolicy = {
30
26
  activeAuthoring: (store) => getActiveAuthoring(store),
31
27
  panelAuthoring: (store) => resolvePanelAuthoring(store).adapter,
32
28
  pick: (store, clientX, clientY, intent) => pickTopmost(store, clientX, clientY, { intent }),
33
29
  toolOwner: (adapter, selectedIds) => resolveViewportToolContext(adapter, selectedIds),
34
- // Both halves of "is this owner painted here" — visibility says a three
35
- // surface is drawn at all, selection ownership says it is the world being
36
- // edited. The viewport ANDs the result with `kind === 'three'`, exactly as it
37
- // did when it called these two itself.
30
+ // Require both a visible three.js surface and ownership by the world being edited. The viewport
31
+ // also gates this result on a stage surface.
38
32
  toolOwnerPainted: (store, owner) =>
39
- isThreejsSurfaceVisible(store) && isViewportToolContextVisible(store, owner),
33
+ isStageVisible(store) && isViewportToolContextVisible(store, owner),
40
34
  threeSurfaceShowing: (store, adapter) =>
41
- isThreejsSurfaceVisible(store) &&
35
+ isStageVisible(store) &&
42
36
  (adapter instanceof CompositeAuthoringAdapter
43
- ? adapter.childAdapters().some((child) => child.kind === 'three')
44
- : !adapter.rects && typeof adapter.hierarchy.object3D === 'function'),
45
- threeViewportRootId: (store) => resolveThreeViewportRootId(store),
37
+ ? adapter.childAdapters().some((child) => child.kind === 'stage')
38
+ : !adapter.rects && typeof adapter.hierarchy.nativeObject === 'function'),
39
+ threeViewportRootId: (store) => resolveStageRootId(store),
46
40
  applyRootHiddenVisibility,
47
41
  suppressRootEnvironment,
48
42
  };
@@ -178,23 +178,7 @@ export class SourceObject3DAuthoringAdapter implements AuthoringAdapter {
178
178
  });
179
179
  }
180
180
 
181
- // ------------------------------------------------ graph mutated after mount
182
- //
183
- // The constructor's walk is a snapshot of the graph AT MOUNT, and one class
184
- // of document mutates its graph afterwards: the live module document
185
- // (`components/asset-viewers/LiveModuleDocument.tsx`) keeps a stable
186
- // container root and SWAPS ITS CHILD on every save, precisely so the
187
- // viewport's camera survives a rebuild. Measured 2026-08-29: the viewport
188
- // showed the rebuilt model while the hierarchy kept the mount-time walk, so
189
- // the document row lost its child and the picker addressed a disposed
190
- // object.
191
- //
192
- // The fix is `r3f-source-authoring-adapter.ts`'s, transcribed: re-index on
193
- // the GRAPH'S OWN structural events. `parent.add(child)` dispatches
194
- // `childadded` on the parent, so watching the scene plus every indexed
195
- // object covers every path a subtree can enter through, and no document has
196
- // to remember to tell this adapter anything. Editor furniture (gizmo and
197
- // helper attachments) is filtered so selection churn does not re-index.
181
+ // Re-index a live module’s stable root when its child graph changes. Ignore editor helpers so gizmo and selection attachments do not trigger another walk.
198
182
 
199
183
  private disposed = false;
200
184
  dispose(): void {
@@ -299,7 +283,7 @@ export class SourceObject3DAuthoringAdapter implements AuthoringAdapter {
299
283
  return label;
300
284
  }
301
285
 
302
- readonly hierarchy: HierarchyProvider = {
286
+ readonly hierarchy: HierarchyProvider & Required<Pick<HierarchyProvider, 'nativeObject' | 'idForNativeObject'>> = {
303
287
  roots: () => [this.hierarchy.node(this.documentNodeId)!],
304
288
  node: (id) => {
305
289
  if (id === this.documentNodeId) {
@@ -323,8 +307,8 @@ export class SourceObject3DAuthoringAdapter implements AuthoringAdapter {
323
307
  const object = this.byId.get(id);
324
308
  return object ? this.view.node(object) : null;
325
309
  },
326
- object3D: (id) => this.byId.get(id) ?? null,
327
- idForObject3D: (object: THREE.Object3D) => this.idByObject.get(object) ?? null,
310
+ nativeObject: (id) => this.byId.get(id) ?? null,
311
+ idForNativeObject: (object: THREE.Object3D) => this.idByObject.get(object) ?? null,
328
312
  };
329
313
 
330
314
  readonly selection: SelectionProvider = {
@@ -345,33 +329,7 @@ export class SourceObject3DAuthoringAdapter implements AuthoringAdapter {
345
329
  private static readonly READ_ONLY_REASON =
346
330
  'This document shows what the module builds; its properties are read-only here. Edit the module.';
347
331
 
348
- /**
349
- * READ-ONLY, AND IT SAYS SO NOW. Every write door on this adapter is a
350
- * no-op and always was — `transforms.beginEdit`/`apply`/`endEdit` below and
351
- * `inspector.set` further down all return `undefined` — but it declared no
352
- * `editability`, which is the seam the shell asks BEFORE drawing a control
353
- * (`InspectorProvider.editability`'s own contract names "hierarchy eye/lock
354
- * controls"). Its default is `{ writable: true }`, so the shell believed
355
- * this projection and drew a live control over a surface that discards it.
356
- *
357
- * MEASURED 2026-09-19 in a cold models scaffold, which is what bought this:
358
- * the Outliner's EYE on a Model document lit, kept its `Hide` tooltip and
359
- * its `aria-pressed="false"`, and left the frame hash byte-identical —
360
- * `GameHierarchy.tsx` gates it on `visibleKnown && (editability?.writable ??
361
- * true)`, and `visible` is knowable here (it reads `object.visible`) even
362
- * though it is `readonly: true` in the very descriptor list below.
363
- *
364
- * `transforms` DELIBERATELY DOES NOT GET ONE, and that is a measurement
365
- * rather than an oversight. Its write doors are no-ops too, so the honest
366
- * answer is the same — but declaring it bought nothing and cost a mark:
367
- * `transform-mode-request.ts`, the one place that would have NAMED the
368
- * refusal, reads `store.selectedEntityIds` off the SHELL store while a
369
- * document stage keeps its selection in its own, so it returns before it
370
- * can speak and the host's Move/Rotate/Scale stayed silent either way —
371
- * while `GameHierarchy`'s `hierarchy-transform-lock` span immediately
372
- * appeared on every row, a glyph Blender's Outliner does not draw. Answer
373
- * it when something acts on the answer.
374
- */
332
+ /** This source projection is read-only. Expose editability where the shell consumes it instead of advertising writes the adapter discards. */
375
333
  private readonly readOnly = (): { writable: boolean; reason: string } => ({
376
334
  writable: false,
377
335
  reason: SourceObject3DAuthoringAdapter.READ_ONLY_REASON,
@@ -502,7 +460,7 @@ export class SourceObject3DAuthoringAdapter implements AuthoringAdapter {
502
460
  }
503
461
 
504
462
  get documentRootObject(): THREE.Object3D | null {
505
- const rootId = this.hierarchy.node(this.documentNodeId)?.childIds[0];
463
+ const rootId = this.hierarchy.node(this.documentNodeId)!.childIds[0];
506
464
  return rootId ? (this.byId.get(rootId) ?? null) : null;
507
465
  }
508
466
 
@@ -1,30 +1,8 @@
1
1
  /**
2
- * THE SHARED LIVE-`Object3D` AUTHORING PROJECTION — the derived views every
3
- * three-surface configuration reads instead of spelling for itself.
4
- *
5
- * The layer below is `../projection/three.ts`: it owns the walk, identity
6
- * minting, the two-way index and picking. This module owns what sits ON that
7
- * index and used to be duplicated across the configurations — the
8
- * transparent-wrapper collapse view (`CollapsedHierarchyView`), the transform
9
- * READ ({@link readLocalTransform}) and the store-selection adoption
10
- * ({@link StoreSelectionAdoption}).
11
- *
12
- * PICKING and BOUNDS are not here because they were never duplicated:
13
- * `ThreeProjector.pick`/`candidates` is already the one raycast, and
14
- * `@volter/editor-threejs/viewport/content-bounds` is the bounds walk. Duplicating either would
15
- * be a hop, not an extraction.
16
- *
17
- * The configurations are `three-authoring-adapter.ts` (play adoption and
18
- * every ingest mount), `r3f-source-authoring-adapter.ts` (edit write-back) and
19
- * `source-object3d-authoring-adapter.ts` (a native graph opened as a document).
20
- * They differ by IDENTITY PROVIDER and WRITE TARGET, never by where their graph
21
- * came from; `three-projection-inventory.test.ts` is the census that keeps it
22
- * that way.
23
- *
24
- * What is deliberately NOT here: the R3F lane's COMPONENT-INSTANCE collapse
25
- * (`isComponentBoundary` and the owner chain). That rule answers a different
26
- * question — which callsite rendered this — and merging it with the rule below
27
- * would be inventing a third, not extracting a shared one.
2
+ * Shared derived views over the live Object3D projection: transparent-wrapper collapse,
3
+ * local-transform reads and store-selection adoption. projection/three.ts owns graph traversal,
4
+ * identity, the two-way index and picking; viewport/content-bounds owns bounds. The source document
5
+ * adapter chooses identity and write targets.
28
6
  */
29
7
 
30
8
  import type { EditorNode, Transform } from '@volter/project/adapter';
@@ -32,7 +10,7 @@ import { isEditorOwnedObject } from '@volter/editor-threejs/viewport/editor-laye
32
10
  import { object3DAuthoringSubjectOf } from '@volter/editor-threejs/adapter/object3d-authoring-subject';
33
11
  import { getUserData } from '@volter/editor-threejs/ecs/user-data';
34
12
  import type * as THREE from 'three';
35
- import type { ShellStore } from '@volter/sdk/kit/shell-store';
13
+ import type { ShellStore } from '@volter/sdk/shell-store';
36
14
  import { nativeKindOf, type ThreeProjector } from '../projection/three';
37
15
  import { localTransformOf } from './live-object-transform';
38
16
 
@@ -46,7 +24,7 @@ export interface StoreSelectionAdoptionOptions {
46
24
  /** Runs with the OWNED ids whenever the adopted set changes, in either
47
25
  * direction. The Object3D-document configuration drives its nodes'
48
26
  * `selectionChanged` marks from here. */
49
- readonly onChange?: (ids: readonly string[]) => void;
27
+ readonly onChange: (ids: readonly string[]) => void;
50
28
  }
51
29
 
52
30
  function sameIds(a: readonly string[], b: readonly string[]): boolean {
@@ -54,14 +32,8 @@ function sameIds(a: readonly string[], b: readonly string[]): boolean {
54
32
  }
55
33
 
56
34
  /**
57
- * SELECTION, shared between the store and a configuration's own answer.
58
- *
59
- * `EditorViewport`'s additive/toggle paths mutate `ShellStore` directly
60
- * rather than going through the adapter, so a `SelectionProvider` that only
61
- * remembered what it was last told drifted from what the viewport had drawn.
62
- * Reading the store on every `get` is what keeps the hierarchy, the Inspector
63
- * and the viewport agreeing; comparing against the last adopted ids is what
64
- * stops that read from re-notifying on every render.
35
+ * Read selection from the store so direct viewport additive and toggle operations remain visible to
36
+ * the adapter. Compare with the last adopted ids to avoid notifying again on unchanged reads.
65
37
  */
66
38
  export class StoreSelectionAdoption {
67
39
  /** The store ids last adopted or published — the comparison basis. */
@@ -78,7 +50,7 @@ export class StoreSelectionAdoption {
78
50
  if (!sameIds(storeIds, this.adopted)) {
79
51
  this.adopted = storeIds;
80
52
  this.selected = storeIds;
81
- this.options.onChange?.(storeIds);
53
+ this.options.onChange(storeIds);
82
54
  }
83
55
  return [...this.selected];
84
56
  }
@@ -92,7 +64,7 @@ export class StoreSelectionAdoption {
92
64
  this.selected = [...selected];
93
65
  const storeIds = this.selected.filter(this.options.owns);
94
66
  this.adopted = storeIds;
95
- this.options.onChange?.(storeIds);
67
+ this.options.onChange(storeIds);
96
68
  this.options.store.selectMultiple(storeIds);
97
69
  }
98
70
  }
@@ -137,8 +109,7 @@ export function isTransparentWrapper(object: THREE.Object3D): boolean {
137
109
  object3DAuthoringSubjectOf(object) === null &&
138
110
  object.userData['oid'] === undefined &&
139
111
  getUserData(object, 'authoringInstance') === undefined &&
140
- getUserData(object, 'authoringRoot') !== true &&
141
- getUserData(object, 'volterComponentRoot') === undefined
112
+ getUserData(object, 'authoringRoot') !== true
142
113
  );
143
114
  }
144
115
 
@@ -1,17 +1,10 @@
1
1
  /**
2
- * Viewport pick context (B4, D12) — the camera/canvas seam that
3
- * `VolterSceneAuthoringAdapter.pickable.pick` needs but cannot own itself: both
4
- * live on `EditorViewport`, not on the adapter (the adapter only ever held a
5
- * `store` — see its own doc comment). A tiny module-level slot, same shape as
6
- * `./active-systems.ts`: `EditorViewport`'s constructor sets it once its
7
- * camera/canvas exist; its `dispose()` clears it.
8
- *
9
- * Absent (module load, a headless unit test that builds the adapter with no
10
- * live viewport, or after `dispose()`) ⇒ `pick()` returns `null` — an honest
11
- * degrade, never a throw.
2
+ * Camera, canvas and editor controls for adapter raycasting, owned by EditorViewport. A viewport
3
+ * configured to publish its context installs it after construction and clears it on disposal.
4
+ * Before a viewport binds or after it is disposed, picking has no context and returns null.
12
5
  */
13
6
 
14
- import { setViewportEditorControls, type ViewportEditorControls } from '@volter/sdk/kit/viewport-editor-controls';
7
+ import { setViewportEditorControls, type ViewportEditorControls } from '@volter/sdk/viewport-editor-controls';
15
8
  import type * as THREE from 'three';
16
9
 
17
10
  export interface ViewportPickContext {
@@ -21,7 +14,7 @@ export interface ViewportPickContext {
21
14
  * target/pole effectors). The full-cover selection overlay owns pointer
22
15
  * events in the project viewport, so it forwards them through this narrow
23
16
  * seam instead of duplicating Three raycasting or constraint semantics. */
24
- readonly editorControls?: ViewportEditorControls;
17
+ readonly editorControls: ViewportEditorControls;
25
18
 
26
19
  }
27
20
 
@@ -1,29 +1,9 @@
1
1
  /**
2
- * Shared three viewport raycast pick (B4, D12) — the adapter-AGNOSTIC
3
- * entity-under-the-pointer raycast, extracted verbatim from
4
- * `editor-viewport.ts`'s former `_raycastEntity` so EVERY threejs-backed
5
- * authoring adapter whose live objects sit in `store.objectMap` reuses the
6
- * exact same logic:
7
- * - first-party `VolterSceneAuthoringAdapter` (edit mode's focused world),
8
- * - `ThreeAuthoringAdapter` (any live three tree — bare ingest
9
- * mode AND the three child of a multi-world PLAY composite).
10
- *
11
- * Pre-B4, `_onPointerUp` ran this raycast UNCONDITIONALLY against
12
- * `store.objectMap` regardless of which adapter was active, so an ingest/
13
- * multi-world-play canvas click selected the object under the pointer. B4's
14
- * `pickTopmost` routes through the active adapter's `pickable` instead, so
15
- * that behavior only survives if each three adapter exposes a `pickable`
16
- * backed by THIS helper (the raycast depends
17
- * only on native Object3Ds plus the active adapter's object→id answer. The
18
- * default still reads `store.objectMap` and entity stamps for first-party
19
- * callers; foreign adapters inject their projection without mutating it.
20
- *
21
- * The camera/canvas live only on `EditorViewport`, reached through the
22
- * `viewport-pick-context.ts` seam (its ctor/`dispose()` set/clear it). Absent
23
- * (a headless unit test, or no live viewport mounted) ⇒ `null`, an honest
24
- * degrade rather than a throw. Uses its OWN `THREE.Raycaster` with
25
- * `layers.enableAll()` replicated so it hits gizmo-adjacent EDITOR_LAYER-tagged
26
- * geometry exactly like the original.
2
+ * Raycast live three.js authoring graphs through the active adapter’s object-to-id lookup. The
3
+ * default reads store.objectMap and entity stamps; document adapters can supply their projection.
4
+ * viewport-pick-context supplies the mounted viewport’s camera and canvas. Return null when no
5
+ * viewport is mounted, and enable all raycast layers to include selectable overlay-adjacent
6
+ * geometry.
27
7
  */
28
8
 
29
9
  import { isInEditorOwnedSubtree } from '@volter/editor-threejs/viewport/editor-layers';
@@ -36,25 +16,7 @@ function isBackdropMaterial(material: THREE.Material): boolean {
36
16
  return material.depthWrite === false && material.side === THREE.BackSide;
37
17
  }
38
18
 
39
- /**
40
- * Is this object the scene's BACKDROP rather than a thing in it?
41
- *
42
- * A sky dome / environment shell is painted behind everything
43
- * (`depthWrite: false`) and is only visible from INSIDE (`side: BackSide`).
44
- * Both flags are authored by the game itself, so this is the idiom read as
45
- * DATA — no name list, no per-project rule.
46
- *
47
- * Why picking must skip it: such a mesh ENCLOSES the camera, so every ray
48
- * eventually reaches it and the pick can never miss. Measured live
49
- * (2026-08-06, `examples/third-person`): all 64 points of an 8×8 grid over the
50
- * canvas returned a hit, the top-left "empty sky" among them, which is why
51
- * clicking empty space never cleared the selection — `_onPointerUp`'s
52
- * `selection.set([])` branch was simply unreachable in any scene with a sky.
53
- * A ray that reaches the backdrop has passed through every real thing in the
54
- * scene; that is a MISS, and the surface's no-selection subject is the honest
55
- * answer. The backdrop stays selectable from the hierarchy, where it is a row
56
- * like any other.
57
- */
19
+ /** Skip enclosing backdrops during viewport picking: BackSide meshes with depthWrite false surround the camera and otherwise turn every ray into a hit. Hierarchy selection still reaches them. */
58
20
  export function isBackdropObject(object: THREE.Object3D): boolean {
59
21
  const material = (object as Partial<THREE.Mesh>).material;
60
22
  if (!material) return false;
@@ -63,15 +25,7 @@ export function isBackdropObject(object: THREE.Object3D): boolean {
63
25
  : isBackdropMaterial(material);
64
26
  }
65
27
 
66
- /** Raycast `store.objectMap` and return the entity id under `(clientX,
67
- * clientY)`, or `null`. Skips editor-helper geometry, and skips ids `isLocked`
68
- * rejects — identical to the pre-B4 viewport raycast.
69
- *
70
- * `isLocked` is INJECTED rather than read off the store because "locked" is a
71
- * per-format notion: an adapter with a lock concept supplies its own check
72
- * (own flag OR any ancestor's — richer than what a generic
73
- * `inspector.get(id,'locked')` reproduces), while every live/foreign adapter
74
- * has no lock concept and omits it. */
28
+ /** Raycast objectMap at client coordinates, skipping helpers and ids rejected by the caller’s format-specific lock predicate. */
75
29
  export function raycastCandidates(
76
30
  store: EditorShellStore,
77
31
  clientX: number,
@@ -79,15 +33,15 @@ export function raycastCandidates(
79
33
  isLocked: (id: string) => boolean = () => false,
80
34
  projection?: {
81
35
  readonly objects: Iterable<THREE.Object3D>;
82
- readonly idForObject3D: (object: THREE.Object3D) => string | null;
36
+ readonly idForNativeObject: (object: THREE.Object3D) => string | null;
83
37
  },
84
38
  ): string[] {
85
39
  const ctx = getViewportPickContext();
86
40
  if (!ctx) return []; // no live viewport mounted — honest degrade
87
41
  const rect = ctx.canvas.getBoundingClientRect();
88
42
  // Chrome and other panes are not part of this viewport. Coverage also asks
89
- // about (0,0); previously that off-canvas query still skinned/raycast the
90
- // entire world twice after an interaction.
43
+ // about (0,0), and that off-canvas query must not skin/raycast the entire
44
+ // world.
91
45
  if (
92
46
  rect.width <= 0 ||
93
47
  rect.height <= 0 ||
@@ -117,15 +71,7 @@ export function raycastCandidates(
117
71
  return true;
118
72
  });
119
73
 
120
- // A SkinnedMesh with CACHED mesh-level bounds is a poisoned pick target:
121
- // `SkinnedMesh.raycast` gates on `this.boundingBox`/`boundingSphere`, and a
122
- // cache computed through the CPU skinning path carries bone-world
123
- // contamination on the humanoid rigs — the gate then misses the localized
124
- // ray forever and every click falls through the character (measured live:
125
- // 0 triangle hits with the cache present, 8 with it cleared, same ray, same
126
- // mesh). Clearing here immunizes the pick against every planter; the
127
- // raycast recomputes what it needs, and the framing walk no longer consumes
128
- // mesh-level skinned bounds at all (`content-bounds.ts`).
74
+ // Clear cached SkinnedMesh bounds before raycasting so the localized ray is tested against current geometry, not a stale bone-world-space cache.
129
75
  for (const root of entityObjects) {
130
76
  root.traverse((node) => {
131
77
  // Structural write: three's own `SkinnedMesh` fields hold `Box3 | null`
@@ -166,7 +112,7 @@ export function raycastCandidates(
166
112
  let eid: string | null = null;
167
113
  if (projection) {
168
114
  while (cursor && eid === null) {
169
- eid = projection.idForObject3D(cursor);
115
+ eid = projection.idForNativeObject(cursor);
170
116
  cursor = cursor.parent;
171
117
  }
172
118
  } else {
@@ -183,26 +129,7 @@ export function raycastCandidates(
183
129
  return hits;
184
130
  }
185
131
 
186
- /**
187
- * A CLICK THAT SELECTS NOTHING SAYS WHY — and only then.
188
- *
189
- * Repeated instances of one component are reported unselectable in the
190
- * viewport while the HIERARCHY addresses every one of them fine (runhuman
191
- * passes 100/109/110/111: "in order to move the one previous, I need to click
192
- * it from the hierarchy"). Identity is not the cause — a reproduction over
193
- * `sourceOidIdentity` mints distinct ids AND distinct stamps for four renders
194
- * of one definition element — so the failure is somewhere in this walk, and
195
- * the three numbers below separate its three possible causes:
196
- *
197
- * - `candidates: 0` — the object never reached `store.objectMap`;
198
- * - `candidates: N, rayHits: 0` — it is in the map but the ray missed it
199
- * (stale transform, detached subtree, wrong scene);
200
- * - `rayHits: N, ids: 0` — it was hit but every hit was filtered as editor
201
- * furniture/backdrop, or carried no stamp to resolve.
202
- *
203
- * Silent on every click that DOES select something, so an ordinary session
204
- * never sees it.
205
- */
132
+ /** Report genuine empty viewport picks with candidate, hit and filtered-hit counts to distinguish missing targets from identity and filtering failures. */
206
133
  function reportEmptyPick(
207
134
  hits: readonly string[],
208
135
  candidates: readonly THREE.Object3D[],
@@ -211,12 +138,7 @@ function reportEmptyPick(
211
138
  clientY: number,
212
139
  ): void {
213
140
  if (hits.length > 0) return;
214
- // The coverage CONFORMANCE PROBE exercises `editor.pickable.pick(0, 0)` on
215
- // a timer to audit the seam's shape (`coverage/authoring-seam-evidence`,
216
- // source: "conformance-probe") — twice every ~5s in a live session,
217
- // measured by stack capture. (0,0) is over the app chrome, never a real
218
- // viewport click, so the probe's synthetic misses must not narrate as user
219
- // clicks that selected nothing.
141
+ // Ignore (0, 0), which the shell uses for a synthetic conformance probe over app chrome.
220
142
  if (clientX === 0 && clientY === 0) return;
221
143
  // biome-ignore lint/suspicious/noConsole: diagnostic breadcrumb, same channel as the editor's other timing/stall notes
222
144
  console.info(
@@ -233,7 +155,7 @@ export function raycastPick(
233
155
  isLocked: (id: string) => boolean = () => false,
234
156
  projection?: {
235
157
  readonly objects: Iterable<THREE.Object3D>;
236
- readonly idForObject3D: (object: THREE.Object3D) => string | null;
158
+ readonly idForNativeObject: (object: THREE.Object3D) => string | null;
237
159
  },
238
160
  ): string | null {
239
161
  return raycastCandidates(store, clientX, clientY, isLocked, projection)[0] ?? null;
@@ -1,49 +1,13 @@
1
1
  /**
2
- * World-hidden eye (D9, `world-session-state.ts`) applied to the EDIT-mode
3
- * three viewport.
4
- *
5
- * Bug this fixes: the eye on a hierarchy world-group row (`world:<id>
6
- * (<kind>)`, shown for 2+ world manifests) toggles `isRootHidden(worldId)`
7
- * and culls the hierarchy subtree, but for a react/pixi world that's ALSO the
8
- * only thing that mattered — `design-time-layers.ts`'s `applySessionStyle`
9
- * sets `display:none` on that world's DOM layer. A three world has no DOM
10
- * layer; nothing else consumes `isRootHidden` for its actual
11
- * `THREE.Object3D`s, so without this module the eye is a viewport no-op.
12
- *
13
- * Session-local only — it writes no file and pushes no undo entry, matching
14
- * the DOM roots' own `display:none` semantics. Every call RECOMPUTES
15
- * `obj.visible` rather than remembering a previous value, which is what makes
16
- * it rebuild-proof: an adapter that re-renders its tree re-stamps
17
- * `obj.visible` from its own truth and would silently drop a one-shot
18
- * override, but the very next call here puts the world-hidden override back.
19
- * Callers must re-invoke this after every rebuild
20
- * AND after the toggle itself; `editor-viewport.ts`'s `syncFromStore()` is
21
- * exactly that seam — it already re-runs on every `store.subscribe` notify,
22
- * which covers both an adapter rebuild (which notifies) and
23
- * `GameHierarchy.tsx`'s toggle handler (`toggleRootHidden` +
24
- * `store.notifyIngestEdit()`).
25
- *
26
- * A composite's `store.objectMap`/live THREE.Scene holds the manifest's sole
27
- * three content root. `GameManifestSchema` rejects a second content root of
28
- * the same medium, so `resolveThreeViewportRootId` below only needs to name
29
- * that one world id rather than arbitrate per-object ownership.
2
+ * Apply hidden-world suppression to live three.js content and its scene environment.
30
3
  */
31
4
 
32
5
  import { getUserData } from '@volter/editor-threejs/ecs/user-data';
33
6
  import * as THREE from 'three';
34
- import { isRootHidden } from '@volter/sdk/kit/authoring/world-session-state';
35
- import { isThreejsSurfaceVisible, resolveThreeViewportRootId } from '@volter/sdk/kit/authoring/three-root';
7
+ import { isRootHidden } from '@volter/sdk/authoring/world-session-state';
36
8
 
37
- export { isThreejsSurfaceVisible, resolveThreeViewportRootId };
38
-
39
- /**
40
- * Apply the eye toggle to every object in `objectMap` (see module doc
41
- * comment for the full rebuild-proofing rationale). `threeRootId` is
42
- * accepted as a parameter (rather than re-resolved internally) so a caller
43
- * that already computed it once per call (e.g. for a cheap re-apply
44
- * signature) doesn't pay for `resolveThreeViewportRootId`'s composite walk
45
- * twice.
46
- */
9
+ /** Set every mapped object invisible when the identified stage root is hidden. Otherwise leave
10
+ * visibility unchanged. The caller supplies its resolved root id. */
47
11
  export function applyRootHiddenVisibility(
48
12
  objectMap: ReadonlyMap<string, THREE.Object3D>,
49
13
  threeRootId: string | null,
@@ -57,32 +21,15 @@ export function applyRootHiddenVisibility(
57
21
  }
58
22
 
59
23
  /**
60
- * The background an editor viewport with NO authored environment shows. A
24
+ * The background used while an entire world is hidden. A
61
25
  * single shared instance so the per-notify re-apply below never allocates.
62
26
  */
63
27
  const HIDDEN_WORLD_BACKGROUND = new THREE.Color('#aaaaaa');
64
28
 
65
29
  /**
66
- * The scene-level half of the eye: `applyRootHiddenVisibility` above only
67
- * covers entity `Object3D`s (the `objectMap`), but a three world also
68
- * renders things that live directly on the `THREE.Scene` — the authored
69
- * `background` color/skybox, `fog`, IBL `environment`, and the
70
- * `envObject`-tagged ambient light `applyEnvironment` adds. None of those are
71
- * entities, so without this function hiding the world leaves the viewport
72
- * painted in the world's background color — looking exactly like "the eye
73
- * didn't work".
74
- *
75
- * Same recompute-don't-remember contract as above: this only ever writes the
76
- * SUPPRESSED state, idempotently, and callers invoke it on every notify while
77
- * the world is hidden (an inspector environment edit mid-hide re-paints the
78
- * authored values via `_applyEnvironment` — the very next notify re-suppresses
79
- * them here). The restore path is `ShellStore.reapplyEnvironment()`, whose
80
- * truth is `_sceneMeta.environment` — never a snapshot taken here.
81
- *
82
- * Post-processing (bloom/vignette) is the composer's half: `the world root's stage`'s
83
- * `rebuildComposer` passes `undefined` instead of `store.environment` while
84
- * the world is hidden; `editor-viewport.ts` bumps `composerVersion` (via
85
- * `reapplyEnvironment`) on both hide/show edges so that rebuild actually runs.
30
+ * Clear the scene's background, fog and IBL environment, and hide envObject-tagged lights.
31
+ * The viewport policy calls this while the world is hidden. It writes only the suppressed state
32
+ * and stores no environment snapshot.
86
33
  */
87
34
  export function suppressRootEnvironment(scene: THREE.Scene): void {
88
35
  scene.background = HIDDEN_WORLD_BACKGROUND;
@@ -0,0 +1,68 @@
1
+ /**
2
+ * THE FREE CAPTURE CAMERA AND CLIP POSE, validated at the relay boundary —
3
+ * `--azimuth/--elevation/--distance` and `--clip/--time` as they arrive on a
4
+ * capture command.
5
+ *
6
+ * THE VERB NAMES ITSELF: the verb is a parameter, so a bad `--azimuth` refuses
7
+ * in the name of the command the caller ran.
8
+ */
9
+
10
+ /** Validate an untrusted `camera` payload: the parsed choice, `undefined` when
11
+ * absent, or the refusal message naming `verb`. */
12
+ export function parseCameraChoice(
13
+ verb: string,
14
+ value: unknown,
15
+ ): { azimuthDegrees: number; elevationDegrees: number; distance?: number } | undefined | string {
16
+ if (value === undefined) return undefined;
17
+ const record = value as {
18
+ azimuthDegrees?: unknown;
19
+ elevationDegrees?: unknown;
20
+ distance?: unknown;
21
+ } | null;
22
+ if (
23
+ record === null ||
24
+ typeof record !== 'object' ||
25
+ typeof record.azimuthDegrees !== 'number' ||
26
+ !Number.isFinite(record.azimuthDegrees) ||
27
+ typeof record.elevationDegrees !== 'number' ||
28
+ !Number.isFinite(record.elevationDegrees) ||
29
+ record.elevationDegrees < -90 ||
30
+ record.elevationDegrees > 90 ||
31
+ (record.distance !== undefined &&
32
+ (typeof record.distance !== 'number' ||
33
+ !Number.isFinite(record.distance) ||
34
+ record.distance <= 0))
35
+ ) {
36
+ return (
37
+ `${verb} camera must be {azimuthDegrees, elevationDegrees in [-90, 90], ` +
38
+ 'distance? > 0 meters}.'
39
+ );
40
+ }
41
+ return {
42
+ azimuthDegrees: record.azimuthDegrees,
43
+ elevationDegrees: record.elevationDegrees,
44
+ ...(record.distance === undefined ? {} : { distance: record.distance }),
45
+ };
46
+ }
47
+
48
+ /** Validate an untrusted `pose` payload: the parsed pose, `undefined` when
49
+ * absent, or the refusal message naming `verb`. */
50
+ export function parsePoseChoice(
51
+ verb: string,
52
+ value: unknown,
53
+ ): { clip: string; timeSeconds: number } | undefined | string {
54
+ if (value === undefined) return undefined;
55
+ const record = value as { clip?: unknown; timeSeconds?: unknown } | null;
56
+ if (
57
+ record === null ||
58
+ typeof record !== 'object' ||
59
+ typeof record.clip !== 'string' ||
60
+ record.clip.length === 0 ||
61
+ typeof record.timeSeconds !== 'number' ||
62
+ !Number.isFinite(record.timeSeconds) ||
63
+ record.timeSeconds < 0
64
+ ) {
65
+ return `${verb} pose must be {clip: non-empty string, timeSeconds >= 0}.`;
66
+ }
67
+ return { clip: record.clip, timeSeconds: record.timeSeconds };
68
+ }