@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
@@ -1,64 +1,11 @@
1
- /**
2
- * What the editor camera should look at when it adopts a world nobody authored
3
- * here — the two answers behind `EditorViewport.focusOnScene`.
4
- *
5
- * ## The measured defect
6
- *
7
- * Auto-frame framed the game's ENTIRE content AABB. Measured on a Cuberun
8
- * mount: a 2,000-radius BackSide skybox sphere, a sun mesh at `z = -2000` and a
9
- * hyperspace prop at `z ≈ -5365` stretched that AABB to ~8,400 units deep, so
10
- * the fit distance landed the camera 12,870 units away from a ~1,000-unit
11
- * gameplay corridor. 93% of the Scene viewport was black, and the one visible
12
- * thing was the OUTSIDE of the skybox. The same world framed at working
13
- * distance is vivid and correct. (Descent measured the same looseness at a
14
- * smaller scale: a camera at ~2,580 for an ~800-unit world.)
15
- *
16
- * The union is the wrong instrument because a backdrop is not content the
17
- * reader is looking FOR — it is content that exists to be far away. One prop
18
- * 5,000 units out costs the whole framing, and no amount of care in the fit
19
- * math can recover it.
20
- *
21
- * ## The two answers, in order
22
- *
23
- * 1. {@link viewFromGameCamera} — a game's author already decided the
24
- * interesting view, so seeding the orbit camera from the world matrix of a
25
- * camera the game itself placed opens the Scene tab where the game looks.
26
- * {@link pickGameCamera} decides which camera that is.
27
- * 2. {@link frameableContentBounds} — the fallback for a game whose camera we
28
- * cannot name. It trims outliers off the union before framing.
29
- *
30
- * ## Why gap-trimming and not a percentile
31
- *
32
- * A blanket percentile (frame the inner 90% of content) would tighten the
33
- * framing of EVERY world, including the uniform ones that are framed correctly
34
- * today. This trims only across a spatial GAP: a face of the bounding box moves
35
- * inward only when a small share of boxes sits beyond a void spanning a large
36
- * fraction of the current diagonal. A uniformly-filled world has no such gap,
37
- * so it comes back byte-identical to the plain union — that identity is the
38
- * first property the tests pin.
39
- */
1
+ /** Frame adopted worlds from useful content bounds: trim enclosing backdrops and isolated distant clusters while retaining uniformly distributed content. */
40
2
 
41
- import { isEditorOwnedObject } from '@volter/editor-threejs/viewport/editor-layers';
42
3
  import * as THREE from 'three';
43
4
 
44
- /** Tuning for {@link frameableContentBounds}. Defaults are the shipped values. */
45
- export interface FrameableBoundsOptions {
46
- /** Largest share of the boxes all trimming together may discard. */
47
- maxOutlierFraction?: number;
48
- /** A gap must span this share of the current content diagonal to be a gap. */
49
- minGapFraction?: number;
50
- /** Below this many boxes there is no distribution to judge; nothing trims. */
51
- minSampleSize?: number;
52
- /** Cap on trim passes, so cost stays bounded on a huge world. */
53
- maxPasses?: number;
54
- }
55
-
56
- const DEFAULTS: Required<FrameableBoundsOptions> = {
57
- maxOutlierFraction: 0.1,
58
- minGapFraction: 0.2,
59
- minSampleSize: 8,
60
- maxPasses: 8,
61
- };
5
+ const MAX_OUTLIER_FRACTION = 0.1;
6
+ const MIN_GAP_FRACTION = 0.2;
7
+ const MIN_SAMPLE_SIZE = 8;
8
+ const MAX_TRIM_PASSES = 8;
62
9
 
63
10
  /** Orbit pivot distance when there is no content to measure one from. */
64
11
  const EMPTY_CONTENT_PIVOT_DISTANCE = 10;
@@ -122,25 +69,23 @@ function widestOuterGap(
122
69
  */
123
70
  export function frameableContentBounds(
124
71
  boxes: readonly THREE.Box3[],
125
- options: FrameableBoundsOptions = {},
126
72
  ): THREE.Box3 {
127
- const opts = { ...DEFAULTS, ...options };
128
73
  const content = boxes.filter((b) => !b.isEmpty());
129
74
  const all = content.map((_, i) => i);
130
75
  const full = unionOf(content, all);
131
- if (content.length < opts.minSampleSize) return full;
76
+ if (content.length < MIN_SAMPLE_SIZE) return full;
132
77
 
133
- let budget = Math.floor(content.length * opts.maxOutlierFraction);
78
+ let budget = Math.floor(content.length * MAX_OUTLIER_FRACTION);
134
79
  if (budget < 1) return full;
135
80
 
136
81
  const kept = new Set(all);
137
82
  let bounds = full;
138
83
  const size = new THREE.Vector3();
139
- for (let pass = 0; pass < opts.maxPasses && budget > 0; pass++) {
84
+ for (let pass = 0; pass < MAX_TRIM_PASSES && budget > 0; pass++) {
140
85
  const diagonal = bounds.getSize(size).length();
141
86
  if (!(diagonal > 0)) break;
142
87
  const cut = widestOuterGap(content, kept, budget);
143
- if (!cut || cut.gap <= opts.minGapFraction * diagonal) break;
88
+ if (!cut || cut.gap <= MIN_GAP_FRACTION * diagonal) break;
144
89
  for (const i of cut.discard) kept.delete(i);
145
90
  budget -= cut.discard.length;
146
91
  bounds = unionOf(content, kept);
@@ -154,21 +99,7 @@ export interface SeededView {
154
99
  target: THREE.Vector3;
155
100
  }
156
101
 
157
- /**
158
- * The editor orbit pose that reproduces `camera`'s own view.
159
- *
160
- * The eye is the game camera's world position and the view direction is its
161
- * own; the only thing invented is the orbit PIVOT, which is placed at the depth
162
- * of the content along that view direction — the point the reader is already
163
- * looking at, so the first orbit drag turns around the world rather than
164
- * around a point behind their head.
165
- *
166
- * Returns `null` when that pivot cannot be placed: the world sits BEHIND the
167
- * camera (a view showing none of the game is not worth adopting), or it sits
168
- * ON it, which leaves an orbit pivot at the eye — measured as a target
169
- * 4.6e-14 units out, an orbit control that can only spin in place. Both fall
170
- * back to {@link frameableContentBounds}.
171
- */
102
+ /** Adopt a camera’s world pose with an orbit pivot at visible content depth. Fall back to frameableContentBounds if the content is behind or at the eye. */
172
103
  export function viewFromGameCamera(camera: THREE.Camera, content: THREE.Box3): SeededView | null {
173
104
  const forward = camera.getWorldDirection(new THREE.Vector3());
174
105
  const position = new THREE.Vector3().setFromMatrixPosition(camera.matrixWorld);
@@ -223,43 +154,7 @@ function isVisibleInWorld(object: THREE.Object3D): boolean {
223
154
  return true;
224
155
  }
225
156
 
226
- /**
227
- * Does a view posed HERE show the world, or a surface pressed against the lens?
228
- *
229
- * The measured defect: a game's own camera is placed for the game's own moment
230
- * — inside a cockpit, behind a wall it is about to drive through, under
231
- * terrain that has not settled — and adopting it as the editor's opening view
232
- * can hand the reader a full-bleed rectangle of rock. The view is a perfectly
233
- * good gameplay camera and a useless first look, and nothing in the pose
234
- * itself says which.
235
- *
236
- * The instrument is geometric, not pixels: nine rays through the frustum, and
237
- * a view SHOWS THE WORLD when at least one of them either escapes (sky,
238
- * backdrop, open space) or reaches something farther than
239
- * {@link NEAR_FIELD_FRACTION} of what this view claims to be looking at. The
240
- * threshold is relative to the view's own pivot distance, so it needs no
241
- * world-scale constant and reads the same on a 10-unit room and a 10,000-unit
242
- * canyon.
243
- *
244
- * Deliberately conservative: EVERY sample must be blocked in the near field
245
- * before the view is refused, so an ordinary view with a prop in the middle or
246
- * ground filling its lower half still passes. A refusal here costs the reader
247
- * only the game's own camera — the caller falls back to
248
- * {@link frameableContentBounds}, which always shows something.
249
- *
250
- * Three's raycast honours `material.side` exactly as the renderer does, and
251
- * that alignment is the reason this instrument is honest rather than merely
252
- * cheap: a camera sealed inside a front-faced solid is invisible to BOTH — the
253
- * reader sees through the shell, the rays escape through it, and the view is
254
- * correctly not called blocked. The failure this catches is the one that is
255
- * actually on screen: front faces, filling the frame, right at the lens.
256
- *
257
- * (The screenshot lane measures the same failure from the other side, on
258
- * PIXELS, in `composite-screenshot.ts`. That instrument needs a drawn frame
259
- * and a GPU readback, and it cannot tell a legitimately flat picture from a
260
- * blocked one — which is why the decision to adopt a pose is made here, before
261
- * anything is drawn.)
262
- */
157
+ /** Check nine geometric frustum rays before adopting a camera pose, rejecting views blocked by a surface near the lens. */
263
158
  export function seededViewShowsWorld(
264
159
  camera: THREE.Camera,
265
160
  content: readonly THREE.Object3D[],
@@ -277,39 +172,3 @@ export function seededViewShowsWorld(
277
172
  return false;
278
173
  }
279
174
 
280
- /** Every camera in the game's own scene, in scene order, editor furniture and
281
- * its subtrees excluded. */
282
- function gameCameras(node: THREE.Object3D, out: THREE.Camera[] = []): THREE.Camera[] {
283
- if (isEditorOwnedObject(node)) return out;
284
- if ((node as THREE.Camera).isCamera) out.push(node as THREE.Camera);
285
- for (const child of node.children) gameCameras(child, out);
286
- return out;
287
- }
288
-
289
- /**
290
- * The camera whose view an ingest mount should adopt — one the game itself
291
- * CREATED AND PLACED, which means one that is in the game's own scene graph.
292
- *
293
- * The capture's own camera (the one the first `render(scene, camera)` used) is
294
- * preferred WHEN it is one of those, and is otherwise ignored, because on the
295
- * first rendered frame it can be a reconciler's placeholder rather than the
296
- * author's camera. Measured on Cuberun: the captured camera came back with an
297
- * identity world matrix at the origin while the game's own
298
- * `<PerspectiveCamera position={[0, 10, -10]}>` was sitting in the scene at
299
- * (0, 8, 3.5) looking down the corridor — seeding from the capture opened on a
300
- * flat wall of ground plane, seeding from the scene's camera opened on the
301
- * game.
302
- *
303
- * Editor furniture is never adopted: the capture trap sits on the shared
304
- * `WebGLRenderer.prototype`, so the editor's own cameras are the one thing
305
- * that must never come back from here.
306
- */
307
- export function pickGameCamera(
308
- captured: THREE.Camera | null | undefined,
309
- scene: THREE.Object3D,
310
- ): THREE.Camera | null {
311
- const cameras = gameCameras(scene);
312
- if (cameras.length === 0) return null;
313
- if (captured && cameras.includes(captured)) return captured;
314
- return cameras[0] as THREE.Camera;
315
- }
@@ -1,51 +1,6 @@
1
- import type * as THREE from 'three';
1
+ /** Neutralize scene fog while rendering the authoring view because its camera can sit outside the project camera’s intended fog range. Retain the fog object and shader variant. */
2
2
 
3
- /**
4
- * Why the Scene viewport draws a world's fog OUT.
5
- *
6
- * ## The measured defect
7
- *
8
- * Fog is calibrated in metres from THE GAME'S OWN CAMERA. The racing-game
9
- * mount declares `<fog args={['white', 0, 500]}>`, which is exactly right at
10
- * its chase camera 20 units behind the car and meaningless anywhere else: its
11
- * world is ~800 units across, so the editor framing that shows the whole track
12
- * sits ~700 units back, and every visible surface is past the fog's far plane.
13
- * Measured on that mount (2026-08-15): the framed Scene view is a featureless
14
- * white blob — the track, the canyon and the horizon are all the same white.
15
- *
16
- * The Scene camera is not the game's camera. It is placed by framing, by a
17
- * seed, or by the reader's own orbit, at whatever distance the world needs —
18
- * so a distance-calibrated effect authored for another camera is not a look,
19
- * it is an erasure.
20
- *
21
- * ## Why a view policy and not fog-aware framing
22
- *
23
- * The alternative was to have the framing respect the fog range — pick a
24
- * distance inside it. It cannot work, and the numbers say so rather than an
25
- * argument: this world's content diagonal is ~1,100 units against a fog far
26
- * plane of 500, so no distance both shows the track and stays readable. Any
27
- * fog-aware framing would have to stop showing the world to keep the fog,
28
- * which is backwards — the first look exists to show the world.
29
- *
30
- * So the fog goes, as a VIEW POLICY, in the same class as the shading modes:
31
- * presentation-only, restored before anything else can observe it, and applied
32
- * only to the editor's own draw. This is also the established answer in the
33
- * genre — Unity's Scene view renders fog OFF by default and offers it as a
34
- * scene-view effect toggle, for exactly this reason.
35
- *
36
- * The game's own look is not touched anywhere it belongs: the Game tab, play
37
- * mode and every exported build render the game's camera through the game's
38
- * fog. Only the authoring camera is exempted, and there is deliberately no
39
- * toggle for it — a Scene view that can be washed white by a setting is a
40
- * fault the reader has to diagnose, and nothing here is worth that.
41
- *
42
- * ## Why the parameters and not `scene.fog = null`
43
- *
44
- * `!!scene.fog` is part of three's shader-program cache key, so nulling it for
45
- * one draw and restoring it for the next compiles and keeps a SECOND program
46
- * variant for every material in the world. Neutralising the fog's own numbers
47
- * leaves the program identical and the fog term at zero.
48
- */
3
+ import type * as THREE from 'three';
49
4
 
50
5
  /** Far enough that no real depth reaches it; `near < far` keeps the linear
51
6
  * fog's `smoothstep(near, far, depth)` well-defined. */
@@ -13,11 +13,9 @@ export interface SpatialHandleBinding {
13
13
  readonly handle: SpatialDragHandle;
14
14
  }
15
15
 
16
- export interface SpatialHandleMesh extends THREE.Mesh {
17
- readonly isSpatialHandleMesh: true;
18
- }
16
+ export type SpatialHandleMesh = THREE.Mesh<THREE.SphereGeometry, THREE.MeshBasicMaterial>;
19
17
 
20
- export interface SpatialHandleVisualSet {
18
+ interface SpatialHandleVisualSet {
21
19
  readonly roots: THREE.Object3D[];
22
20
  readonly handles: SpatialHandleMesh[];
23
21
  }
@@ -47,10 +45,9 @@ function addLine(
47
45
  material: THREE.LineBasicMaterial,
48
46
  closed = false,
49
47
  ): void {
50
- const linePoints = closed && points.length > 0 ? [...points, points[0]!] : [...points];
48
+ const linePoints = closed ? [...points, points[0]!] : [...points];
51
49
  const line = new THREE.Line(new THREE.BufferGeometry().setFromPoints(linePoints), material);
52
50
  line.renderOrder = 997;
53
- line.layers.set(EDITOR_LAYER);
54
51
  if (material instanceof THREE.LineDashedMaterial) line.computeLineDistances();
55
52
  root.add(line);
56
53
  }
@@ -60,8 +57,8 @@ function circlePoints(
60
57
  radius: number,
61
58
  axisA: THREE.Vector3,
62
59
  axisB: THREE.Vector3,
63
- segments = 64,
64
60
  ): THREE.Vector3[] {
61
+ const segments = 64;
65
62
  const points: THREE.Vector3[] = [];
66
63
  for (let i = 0; i < segments; i++) {
67
64
  const angle = (i / segments) * Math.PI * 2;
@@ -244,7 +241,6 @@ function guideObject(guide: SpatialHandleGuide): THREE.Object3D {
244
241
  : guide.kind === 'box'
245
242
  ? boxGuide(guide)
246
243
  : capsuleGuide(guide);
247
- root.layers.set(EDITOR_LAYER);
248
244
  root.traverse((child) => child.layers.set(EDITOR_LAYER));
249
245
  return root;
250
246
  }
@@ -256,8 +252,7 @@ function handleMesh(nodeId: string, handle: SpatialDragHandle): SpatialHandleMes
256
252
  transparent: true,
257
253
  opacity: handle.writable ? 0.92 : 0.35,
258
254
  });
259
- const mesh = new THREE.Mesh(HANDLE_GEOMETRY, material) as unknown as SpatialHandleMesh;
260
- Object.defineProperty(mesh, 'isSpatialHandleMesh', { value: true });
255
+ const mesh = new THREE.Mesh(HANDLE_GEOMETRY, material);
261
256
  mesh.position.copy(vector(handle.position));
262
257
  mesh.renderOrder = 998;
263
258
  mesh.layers.set(EDITOR_LAYER);
@@ -282,15 +277,15 @@ export function createSpatialHandleVisuals(
282
277
  return { roots, handles };
283
278
  }
284
279
 
285
- export function spatialHandleBinding(mesh: SpatialHandleMesh): SpatialHandleBinding | null {
286
- return bindingByMesh.get(mesh) ?? null;
280
+ export function spatialHandleBinding(mesh: SpatialHandleMesh): SpatialHandleBinding {
281
+ return bindingByMesh.get(mesh)!;
287
282
  }
288
283
 
289
284
  export function setSpatialHandleHovered(mesh: SpatialHandleMesh, hovered: boolean): void {
290
- const binding = bindingByMesh.get(mesh);
291
- const material = mesh.material as THREE.MeshBasicMaterial;
285
+ const binding = spatialHandleBinding(mesh);
286
+ const material = mesh.material;
292
287
  material.color.set(
293
- hovered && binding?.handle.writable ? '#ffffff' : (binding?.handle.color ?? '#ffffff'),
288
+ hovered && binding.handle.writable ? '#ffffff' : binding.handle.color,
294
289
  );
295
290
  }
296
291
 
@@ -301,32 +296,28 @@ export function scaleSpatialHandle(mesh: SpatialHandleMesh, camera: THREE.Camera
301
296
 
302
297
  function disposeRenderable(
303
298
  child: THREE.Object3D,
304
- disposedGeometries: Set<THREE.BufferGeometry>,
305
299
  disposedMaterials: Set<THREE.Material>,
306
300
  ): void {
307
- const renderable = child as THREE.Line | THREE.Mesh;
301
+ const renderable = child as THREE.Object3D & {
302
+ geometry?: THREE.BufferGeometry;
303
+ material?: THREE.LineBasicMaterial | THREE.MeshBasicMaterial;
304
+ };
308
305
  const geometry = renderable.geometry;
309
- if (geometry && geometry !== HANDLE_GEOMETRY && !disposedGeometries.has(geometry)) {
306
+ if (geometry && geometry !== HANDLE_GEOMETRY) {
310
307
  geometry.dispose();
311
- disposedGeometries.add(geometry);
312
308
  }
313
- const materials = Array.isArray(renderable.material)
314
- ? renderable.material
315
- : renderable.material
316
- ? [renderable.material]
317
- : [];
318
- for (const material of materials) {
319
- if (disposedMaterials.has(material)) continue;
309
+ const material = renderable.material;
310
+ if (material) {
311
+ if (disposedMaterials.has(material)) return;
320
312
  material.dispose();
321
313
  disposedMaterials.add(material);
322
314
  }
323
315
  }
324
316
 
325
317
  export function disposeSpatialHandleVisuals(roots: readonly THREE.Object3D[]): void {
326
- const disposedGeometries = new Set<THREE.BufferGeometry>();
327
318
  const disposedMaterials = new Set<THREE.Material>();
328
319
  for (const root of roots) {
329
320
  root.removeFromParent();
330
- root.traverse((child) => disposeRenderable(child, disposedGeometries, disposedMaterials));
321
+ root.traverse((child) => disposeRenderable(child, disposedMaterials));
331
322
  }
332
323
  }
@@ -1,33 +1,5 @@
1
- /**
2
- * Shared, headless facts about a `three` STORY: whether a mounted story holds
3
- * three content, and how a story's preview thumbnail is keyed in an in-memory
4
- * cache. Kept pure and off the React tree so every one of them is
5
- * unit-testable with no renderer.
6
- *
7
- * Membership belongs to the CONSUMER, never to the registry, and it is
8
- * DECLARED (`story-declared-medium.ts`). The `3D Components` board admits
9
- * stories whose medium is `three`, the canvas board admits `canvas`, the
10
- * DOM design board admits `dom` ({@link domStoryBoardMembers}). An
11
- * undeclared story is a named gap and is not a candidate on any board —
12
- * nothing here mounts to guess. The project's story registry stays
13
- * deliberately unfiltered.
14
- */
15
-
16
- import type * as THREE from 'three';
17
- import { getProjectStoryRegions } from '@volter/sdk/kit/stories/project-story-regions';
18
- import { declaredStoryMedium, reportUndeclaredStoryMedium } from '@volter/sdk/kit/stories/story-declared-medium';
19
-
20
- /**
21
- * Whether a mounted story's root wrapper actually holds `three` content.
22
- *
23
- * A story that renders nothing (or renders only React fragments/null) commits
24
- * successfully into the wrapper group and yields an EMPTY `Object3D` — a
25
- * successful mount with nothing in it. A surface shows a story that rendered
26
- * something; "the mount succeeded" alone is not the test.
27
- */
28
- export function mountedStoryHasThreeContent(root: THREE.Object3D): boolean {
29
- return root.children.length > 0;
30
- }
1
+ import { editorHost } from '@volter/sdk/host';
2
+ import { declaredStoryMedium } from '@volter/sdk/stories/story-declared-medium';
31
3
 
32
4
  /**
33
5
  * The DOM design board's membership — every story DECLARED as `dom`.
@@ -36,7 +8,7 @@ export function mountedStoryHasThreeContent(root: THREE.Object3D): boolean {
36
8
  * mounts: medium is a declaration, and a last-resort mount would make
37
9
  * "residue 0" unfalsifiable (the orphan would land on this board).
38
10
  *
39
- * The board (`authoring/design-time-layers.ts`) renders each member through
11
+ * The DOM design board renders each member through
40
12
  * `react-dom`, so a three story landing on it renders `<group>`/`<primitive>`
41
13
  * as unknown DOM tags. Declaration is what keeps those stories off this
42
14
  * board; the project's story registry stays deliberately unfiltered.
@@ -49,7 +21,7 @@ export function domStoryBoardMembers<
49
21
  readonly tags?: readonly string[];
50
22
  },
51
23
  >(stories: readonly T[]): T[] {
52
- const regions = getProjectStoryRegions();
24
+ const regions = editorHost().stories.regions();
53
25
  const members: T[] = [];
54
26
  for (const story of stories) {
55
27
  const declared = declaredStoryMedium({ modulePath: story.modulePath, regions });
@@ -58,7 +30,7 @@ export function domStoryBoardMembers<
58
30
  continue;
59
31
  }
60
32
  if (declared.via === 'undeclared' && story.modulePath) {
61
- reportUndeclaredStoryMedium(story.modulePath, declared.reason);
33
+ editorHost().stories.reportUndeclaredMedium(story.modulePath, declared.reason);
62
34
  }
63
35
  }
64
36
  return members;
@@ -1,13 +1,12 @@
1
1
  /**
2
2
  * The Three set's canvases, rendered at a caller's scale
3
- * (`@volter/sdk/kit/canvas-frames`): the Scene viewport's presented frame
3
+ * (`@volter/sdk/canvas-frames`): the Scene viewport's presented frame
4
4
  * or a fresh render of it, and an Object3D document's presented frame or an
5
5
  * offscreen render of its subject. The editor-chrome photograph asks here, so a
6
6
  * 2x capture of a viewport gets a 2x render rather than a 1x one stretched.
7
7
  */
8
- import { registerCanvasRender } from '@volter/sdk/kit/canvas-frames';
8
+ import { presentedCanvasFrame, registerCanvasRender } from '@volter/sdk/canvas-frames';
9
9
  import { allObject3DDocumentSessions } from './authoring/object3d-document-session-registry';
10
- import { liveCanvasFrame } from '@volter/sdk/kit/live-canvas-frame';
11
10
  import { threeStoreForHost } from './three-state';
12
11
 
13
12
  async function imageOf(dataUrl: string): Promise<HTMLImageElement> {
@@ -22,7 +21,7 @@ export function registerThreeCanvasRender(): () => void {
22
21
  const store = threeStoreForHost();
23
22
  if (store && store.viewportCanvas() === canvas) {
24
23
  return (async () => {
25
- const presented = await liveCanvasFrame(canvas);
24
+ const presented = await presentedCanvasFrame(canvas);
26
25
  if (presented) return presented;
27
26
  const dataUrl = store.captureViewportImage(size);
28
27
  return dataUrl ? imageOf(dataUrl) : null;
@@ -1,10 +1,9 @@
1
1
  /**
2
2
  * The Three integration's answers to the hierarchy panel's row questions
3
- * (`@volter/sdk/kit/hierarchy-row-media`): an instanced mesh's row names the units it
4
- * draws, a node's component marks come off its `Object3D`, and the store's Three half logs which
5
- * projected parents its object map changed so the panel can patch rows.
3
+ * (`@volter/sdk/hierarchy-row-media`): an instanced mesh's row names the units it
4
+ * draws, and a node's component marks come off its `Object3D`.
6
5
  */
7
- import { registerHierarchyRowMedia } from '@volter/sdk/kit/hierarchy-row-media';
6
+ import { registerHierarchyRowMedia } from '@volter/sdk/hierarchy-row-media';
8
7
  import { entityObject3D } from './entity-object';
9
8
  import { markReaderFor } from './hierarchy-mark-reader';
10
9
  import { instancedRowDetail } from './instanced-presentation';
@@ -15,12 +14,5 @@ export function registerThreeHierarchyRowMedia(): () => void {
15
14
  detail: (adapter, nodeId, store) =>
16
15
  instancedRowDetail(entityObject3D(adapter, threeStateOf(store).objectMap, nodeId)),
17
16
  marks: (hierarchy) => markReaderFor(hierarchy),
18
- structureLog: (store) => {
19
- const three = threeStateOf(store);
20
- return {
21
- epoch: () => three.gizmoEpoch,
22
- parentChangesSince: (epoch) => three.ingestObjectMapChangesSince(epoch),
23
- };
24
- },
25
17
  });
26
18
  }
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * THE THREE.JS ANSWER TO "what does this node look like"
3
- * (`@volter/sdk/kit/inspection-node-media`): a selected node the shell's
3
+ * (`@volter/sdk/inspection-node-media`): a selected node the shell's
4
4
  * Three store resolves to a live `Object3D` is previewed as an isolated live
5
5
  * view of that object, a camera has no Preview section (its view is the
6
6
  * viewport's), an instanced mesh names its units, and a node in the object map
@@ -10,12 +10,12 @@
10
10
  import {
11
11
  type InspectionNodeMedia,
12
12
  registerInspectionNodeMedia,
13
- } from '@volter/sdk/kit/inspection-node-media';
13
+ } from '@volter/sdk/inspection-node-media';
14
14
  import { contentWorldBounds } from '@volter/editor-threejs/viewport/content-bounds';
15
15
  import { createElement, lazy } from 'react';
16
16
  import type * as THREE from 'three';
17
- import { openRegisteredDocument } from '@volter/sdk/kit/document-open-registry';
18
- import { InspectorPreviewBody } from '@volter/sdk/kit/components/inspector-preview-section';
17
+ import { openRegisteredDocument } from '@volter/sdk/document-open-registry';
18
+ import { InspectorPreviewBody } from '@volter/sdk/components/inspector-preview-section';
19
19
  import { entityObject3D } from './entity-object';
20
20
  import { describeInstancedPresentation } from './instanced-presentation';
21
21
  import { threeStoreForHost } from './three-state';
@@ -38,7 +38,7 @@ function objectMedia(
38
38
  (mode) =>
39
39
  createElement(InspectorPreviewBody, {
40
40
  picture: () =>
41
- createElement(InspectorObjectPreview, { previewKey, object, displayName, fill: true }),
41
+ createElement(InspectorObjectPreview, { previewKey, object, displayName }),
42
42
  previewKey,
43
43
  actions,
44
44
  ...(mode ? { mode } : {}),
@@ -62,7 +62,7 @@ export function registerThreeInspectionMedia(): () => void {
62
62
  ? {
63
63
  // The live `Object3D.name` is the only name there is.
64
64
  open: () =>
65
- openRegisteredDocument('asset-editor:entity', store.shell, {
65
+ openRegisteredDocument('asset-editor:entity', {
66
66
  entityId: nodeId,
67
67
  title: object.name || nodeId,
68
68
  }),
@@ -1,34 +1,31 @@
1
1
  /**
2
2
  * THE THREE INTEGRATION, INSTALLED — every registration the kit's Three viewport answers
3
- * through: the Object3D surfaces contributions mount, the viewport's relay verbs, its Asset Lab
3
+ * through: the Object3D surfaces contributions mount, its Asset Lab
4
4
  * viewers, the inspector's node media, model thumbnails, the viewport status facet,
5
5
  * canvas captures, the gizmo viewport's authoring policy, and the view state each session store's
6
6
  * Three half saves. The kit constructs none of it; the integration installs it once, and every
7
7
  * Three surface asks for it before it mounts (`ensureThreeIntegration`).
8
8
  */
9
- import { registerContributedCommands } from '@volter/sdk/kit/command-registry';
10
- import { saveThumbnail } from '@volter/sdk/kit/editor-api';
11
- import { registerHostHierarchyObjects } from '../host-hierarchy-objects';
12
- import { registerContributionSurfaces } from '@volter/sdk/kit/contribution-surfaces';
13
- import { registerRendererResourceCounts } from '@volter/sdk/kit/renderer-resource-counts';
9
+ import { saveThumbnail } from '@volter/sdk/api/project-open';
10
+ import { OUTLINER_DATA, OUTLINER_OBJECT, registerHierarchyKinds } from '@volter/sdk/hierarchy-kind-icon';
11
+ import { registerContributionSurfaces } from '@volter/sdk/contribution-surfaces';
12
+ import { registerRendererResourceCounts } from '@volter/sdk/renderer-resource-counts';
14
13
  import { interactiveViewportRendererCounts } from './three-viewport/interactive-renderer';
15
14
  import { inspectorPreviewRendererCounts } from '../viewport/preview-renderer';
16
15
  import { liveHostRendererCount } from '../viewport/renderer-ownership';
17
- import type * as THREE from 'three';
18
16
  import { SHELL_VIEWPORT_AUTHORING_POLICY } from './authoring/shell-viewport-policy';
19
17
  import { Object3DAuthoringSurface, Object3DPreviewSurface } from './components/object3d-contribution-surfaces';
20
18
  import { registerThreeAssetViewers } from './components/asset-viewers/three-asset-viewers';
21
19
  import type { EditorStatePersistence } from './editor-shell-store';
22
20
  import { registerModelThumbnails } from './model-thumbnail';
23
- import { writeProjectLocalSection } from '@volter/sdk/kit/project-local-state';
24
- import { onShellStore } from '@volter/sdk/kit/shell-store-door';
21
+ import { editorHost } from '@volter/sdk/host';
22
+ import { onShellStore } from '@volter/sdk/shell-store-door';
25
23
  import { registerThreeCanvasRender } from './three-canvas-render';
26
24
  import { registerThreeHierarchyRowMedia } from './three-hierarchy-row-media';
27
25
  import { registerThreePlayCameraFlight } from './play-camera-flight';
28
26
  import { registerThreeInspectionMedia } from './three-inspection-media';
29
- import { threeStateOf, threeStoreForHost } from './three-state';
27
+ import { threeStateOf } from './three-state';
30
28
  import { installViewportAuthoringPolicy } from './viewport-authoring-policy';
31
- import { viewportCommands } from './viewport-commands';
32
29
  import { reportViewportStatus } from './viewport-status-facet';
33
30
 
34
31
  /**
@@ -40,24 +37,31 @@ const hotData = (import.meta as ImportMeta & { hot?: { data: Record<string, unkn
40
37
  const VIEW_STATE_PERSISTENCE: EditorStatePersistence = (hotData?.['viewStatePersistence'] as
41
38
  | EditorStatePersistence
42
39
  | undefined) ?? {
43
- save: (state) => writeProjectLocalSection('view', state),
40
+ save: (state) => editorHost().projectLocalState.write('view', state),
44
41
  saveThumbnail,
45
42
  };
46
43
  if (hotData) hotData['viewStatePersistence'] = VIEW_STATE_PERSISTENCE;
47
44
 
48
- const NO_OBJECTS: ReadonlyMap<string, THREE.Object3D> = new Map();
49
45
 
50
46
  let installed: (() => void) | null = null;
51
47
 
52
48
  /** Install the integration; idempotent. Returns the teardown of the first install. */
53
49
  export function ensureThreeIntegration(): () => void {
54
50
  if (installed) return installed;
51
+ // The hierarchy glyphs, in place before the first hierarchy render a viewport brings (its rows are memoized). The
52
+ // core composite gives a `stage` world's group row the manifest surface as its kind (`groupNode`); that row draws
53
+ // the outliner's object glyph. A model document's root is a datablock, not an object carrying one `mesh-data` row
54
+ // off it, so the two kinds `source-object3d-authoring-adapter.ts` mints for it, `three-source` and `model-asset`,
55
+ // draw the data glyph, as `mesh-data` does: the Properties breadcrumb takes its glyph from the datablock, never
56
+ // from the open tab.
57
+ registerHierarchyKinds({
58
+ glyphs: { stage: OUTLINER_OBJECT, 'three-source': OUTLINER_DATA, 'model-asset': OUTLINER_DATA },
59
+ });
55
60
  const stops = [
56
61
  registerContributionSurfaces({
57
62
  Object3DPreview: Object3DPreviewSurface,
58
63
  Object3DAuthoring: Object3DAuthoringSurface,
59
64
  }),
60
- registerContributedCommands('three-viewport', viewportCommands),
61
65
  registerThreeAssetViewers(),
62
66
  registerThreeInspectionMedia(),
63
67
  registerModelThumbnails(),
@@ -65,10 +69,6 @@ export function ensureThreeIntegration(): () => void {
65
69
  registerThreeCanvasRender(),
66
70
  registerThreeHierarchyRowMedia(),
67
71
  registerThreePlayCameraFlight(),
68
- registerHostHierarchyObjects({
69
- object: (id) => threeStoreForHost()?.objectMap.get(id) ?? null,
70
- objects: () => threeStoreForHost()?.objectMap ?? NO_OBJECTS,
71
- }),
72
72
  registerRendererResourceCounts('hostLive', liveHostRendererCount),
73
73
  registerRendererResourceCounts('interactive', interactiveViewportRendererCounts),
74
74
  registerRendererResourceCounts('inspectorPreview', inspectorPreviewRendererCounts),
@@ -1,8 +1,7 @@
1
1
  /** A shell store's Three half (`threeStateOf`, beside the half in `editor-shell-store.ts`). */
2
- import type { ShellStore } from '@volter/sdk/kit/shell-store';
2
+ import type { ShellStore } from '@volter/sdk/shell-store';
3
3
  import { type EditorShellStore, threeStateOf } from './editor-shell-store';
4
- import { useEditorStore, useOptionalEditorStore } from '@volter/sdk/kit/editor-runtime';
5
- import { onShellStore, shellStoreForHost } from '@volter/sdk/kit/shell-store-door';
4
+ import { shellStoreForHost } from '@volter/sdk/shell-store-door';
6
5
 
7
6
  export { threeStateOf };
8
7
 
@@ -11,23 +10,7 @@ export function optionalThreeStateOf(store: ShellStore | null): EditorShellStore
11
10
  return store === null ? null : threeStateOf(store);
12
11
  }
13
12
 
14
- /** The session store's Three half (`threeStateOf`), for a component that reads the scene,
15
- * object map, camera or viewport tools. */
16
- export function useThreeEditorStore(): EditorShellStore {
17
- return threeStateOf(useEditorStore());
18
- }
19
-
20
- /** {@link useThreeEditorStore} outside a session answers `null`. */
21
- export function useOptionalThreeEditorStore(): EditorShellStore | null {
22
- return optionalThreeStateOf(useOptionalEditorStore());
23
- }
24
-
25
13
  /** The session store's Three half (`three-state.ts`), or `null` before a session opens. */
26
14
  export function threeStoreForHost(): EditorShellStore | null {
27
15
  return optionalThreeStateOf(shellStoreForHost());
28
16
  }
29
-
30
- /** {@link onShellStore} for a lane that works on the store's Three half. */
31
- export function onThreeStore(listener: (store: EditorShellStore) => void): () => void {
32
- return onShellStore((store) => listener(threeStateOf(store)));
33
- }