@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,25 +1,19 @@
1
1
  /**
2
- * EditorShellStore — the shell store's THREE half: the adopted scene and its object map, the
2
+ * EditorShellStore — the shell store's THREE half: the scene and its object map, the
3
3
  * renderer and camera, viewport tools (transform, snapping, pivot, gizmo), helper and shading
4
- * state, LOD pins, capture and the periodic editor-state save. It extends `ShellStore`
5
- * (`shell-store.ts`), the media-neutral half the kit's other modules type against, and leaves
6
- * the kit for `@volter/editor-threejs` with the viewport (ARCHITECTURE.md §The plan, unit 3).
4
+ * state, LOD pins, capture and the periodic editor-state save. It wraps the media-neutral
5
+ * SDK `ShellStore` that the kit's other modules type against.
7
6
  * It knows `THREE.Object3D`s and ids, never a document model: an adapter's document is that
8
- * adapter's private business (`authoring-inversion-guard.test.ts` is the tripwire).
7
+ * adapter's private business.
9
8
  */
10
9
 
11
- import type { ViewportTab } from '@volter/sdk';
12
10
  import { viewportCaptureOutputPass } from '@volter/editor-threejs/capture/output-pass';
13
- import { isEditorOwnedObject } from '@volter/editor-threejs/viewport/editor-layers';
14
- import type { WorldRendererConfig } from '@volter/editor-threejs/adapter/renderer-config';
15
11
  import { getUserData, setUserData } from '@volter/editor-threejs/ecs/user-data';
16
12
  import type { ViewportShadingMode } from '@volter/editor-threejs/render/viewport-shading';
17
13
  import * as THREE from 'three';
18
- import type { BatchedRenderer } from 'three.quarks';
19
14
  import { findEntityLod } from './entity-lod';
20
- import { entityIdOf } from './entity-object';
21
- import { ShellStore } from '@volter/sdk/kit/shell-store';
22
- export type { NotifyScope, PlayEditRegime } from '@volter/sdk/kit/shell-store';
15
+ import { ShellStore } from '@volter/sdk/shell-store';
16
+ export type { NotifyScope } from '@volter/sdk/shell-store';
23
17
  import { withSceneFogNeutralized } from './scene-view-fog';
24
18
 
25
19
  /** The store's persistence collaborator — installed by the shell, never
@@ -30,74 +24,17 @@ export interface EditorStatePersistence {
30
24
  }
31
25
 
32
26
  /**
33
- * WHICH TOOL THE SHELF HAS ARMED — four that draw a transform gizmo, and one
34
- * that draws none.
35
- *
36
- * `'select'` is Blender's Select Box, and it is a TOOL rather than an absence:
37
- * its shelf opens on it (`space_toolsystem_toolbar.py`,
38
- * `_defs_view3d_generic.select_box` first in the Object Mode `_tools_default`;
39
- * photographed at the engine's pin in `gizmo-select-box.png`), so a selected
40
- * object shows its outline and nothing else until Move / Rotate / Scale /
41
- * Transform is armed. Before it there was no "no gizmo" state at all — this
42
- * store's default was `'combined'` and every selection drew three tools'
43
- * handles at once — which is why it is a member here rather than a flag beside
44
- * it. `editor-viewport.ts` detaches on it, through the same branch a
45
- * non-writable selection already took.
46
- *
47
- * Which member a STAGE is born with is its presentation's (`interaction.bootTool`,
48
- * `kit/viewport-presentation`); arming one is always a click in the shelf.
27
+ * Re-export the shell’s transform-tool choices. Select shows selection outlines without a transform
28
+ * gizmo; Move, Rotate, Scale and Transform arm their corresponding handles. Stage presentation
29
+ * supplies interaction.bootTool.
49
30
  */
50
- export type { GizmoAnchor, PivotMode, TransformMode, TransformSpace } from '@volter/sdk/kit/shell-store';
31
+ export type { GizmoAnchor, PivotMode, TransformMode, TransformSpace } from '@volter/sdk/shell-store';
51
32
 
52
33
  export type ShadingMode = ViewportShadingMode;
53
34
 
54
- /**
55
- * What a store notification says CHANGED.
56
- *
57
- * `'selection'` is the narrow one and it is claimed by exactly five writers
58
- * (`select`, `selectMultiple`, `addToSelection`, `toggleSelection`,
59
- * `applySelectionBeforePresentation`), each of which touches
60
- * `_viewportSelections` and nothing else. `'content'` is the default and means
61
- * "assume anything may have changed", so a mutation that forgets to classify
62
- * itself is safe by construction — the failure mode of a wrong claim is a
63
- * stale panel, so the burden of proof sits on the narrow value.
64
- * See {@link EditorShellStore.contentVersion}.
65
- */
66
-
67
- /** Exact structural delta supplied by a live Three projection mutation. */
68
- export interface IngestObjectMapStructureDelta {
69
- readonly changedParentIds: ReadonlySet<string>;
70
- readonly rootsChanged: boolean;
71
- /** Exact projected membership changes; omission keeps legacy callers conservative. */
72
- readonly addedIds?: ReadonlySet<string>;
73
- readonly removedIds?: ReadonlySet<string>;
74
- }
75
-
76
- /** All projected parent rows changed after a previously observed gizmo epoch. */
77
- export interface IngestObjectMapChanges {
78
- readonly epoch: number;
79
- readonly changedParentIds: ReadonlySet<string>;
80
- }
81
-
82
- /** Exact object-map identities whose membership changed after an observed gizmo epoch. */
83
- export interface IngestObjectMapMembershipChanges {
84
- readonly epoch: number;
85
- readonly changedIds: ReadonlySet<string>;
86
- }
87
-
88
- /** Enough exact structural epochs to span deferred React consumers; older readers rebuild. */
89
- const OBJECT_MAP_MEMBERSHIP_HISTORY = 256;
90
-
35
+ export type { HelperVisibility } from '@volter/sdk/shell-store';
91
36
 
92
-
93
- /** The two viewport tabs, Edit · Play. Defined ONCE, in the SDK's shared
94
- * vocabulary (`@volter/sdk`), because the control API speaks it too;
95
- * re-exported here so editor modules keep one import site. */
96
- export type { ViewportTab };
97
-
98
- export type { HelperVisibility } from '@volter/sdk/kit/shell-store';
99
-
100
- export type { ViewportAction } from '@volter/sdk/kit/shell-store';
37
+ export type { ViewportAction } from '@volter/sdk/shell-store';
101
38
 
102
39
  export class EditorShellStore {
103
40
  /** Made by `threeStateOf`, once per shell store. */
@@ -105,55 +42,19 @@ export class EditorShellStore {
105
42
 
106
43
  // --- Scene graph references (set via bindScene) ---
107
44
  protected _scene: THREE.Scene | null = null;
108
- /** Adoption stack (see enterPlayScene): each frame is the scene state that
109
- * was active when an adoption replaced it, restored in LIFO order by
110
- * exitPlayScene. A STACK (not a single slot) because adoptions nest for
111
- * R3F projects: the design session adopts its fiber scene at design time,
112
- * and play adopts the live game scene OVER it while the play-entry
113
- * transition still needs the design scene alive — the design session then
114
- * unwinds its own frame from the middle via releaseAdoptedScene. */
115
- protected _adoptionStack: Array<{
116
- /** `null` when the adoption replaced no scene: a session with no Scene stage plays too. */
117
- scene: THREE.Scene | null;
118
- objectMap: Map<string, THREE.Object3D>;
119
- /** Opaque payload from {@link _captureAdoptionExtras} — a document half's
120
- * own per-adoption snapshot. The shell never inspects it. */
121
- extras: unknown;
122
- selection: Set<string>;
123
- /** The image config that was active BEFORE this adoption — same LIFO
124
- * discipline as `scene`, so a frame spliced out of the middle simply
125
- * stops being a restore point. See {@link _applyAdoptedImageConfig}. */
126
- imageConfig: WorldRendererConfig | undefined;
127
- }> = [];
128
45
  protected _objectMap = new Map<string, THREE.Object3D>();
129
46
  protected _renderer: THREE.WebGLRenderer | null = null;
130
47
  protected _camera: THREE.Camera | null = null;
131
- protected _batchedRenderer: BatchedRenderer | null = null;
132
48
  protected _lastThumbnailTime = 0;
133
49
  protected _lastEditorStateSaveTime = 0;
134
50
  protected _orbitTarget: THREE.Vector3 | null = null;
135
51
 
136
52
  // --- UI state (not stored in scene graph) ---
137
53
  /**
138
- * Bumped whenever gizmos must be re-applied without a structural signature
139
- * change (W3a B1): a single-entity rebuild (`_rebuildEntity` →
140
- * `updateEntityPreview`) disposes the old Object3D WITH all its helper
141
- * children, but leaves the objectMap identity/size and every toggle
142
- * untouched — the viewport's `gizmoSig` gate would otherwise skip both
143
- * `applyEntityGizmos` and the per-type visibility pass until an unrelated
144
- * event.
54
+ * Advance when object helpers must be reapplied without changing object-map identity, size or
55
+ * visibility toggles, including single-object rebuilds.
145
56
  */
146
57
  protected _gizmoEpoch = 0;
147
- /** Last structural epoch at which each projected parent's direct children changed. */
148
- protected _objectMapParentChangedAt = new Map<string, number>();
149
- /** A root change (or an unclassified legacy call) requires a full hierarchy read. */
150
- protected _objectMapFullChangeEpoch = 0;
151
- /** Last exact add/remove epoch for each projected id, independently consumed by the viewport. */
152
- protected _objectMapMembershipChangedAt = new Map<string, number>();
153
- /** Missing membership detail requires the viewport to reconcile its complete helper indexes. */
154
- protected _objectMapMembershipFullChangeEpoch = 0;
155
- /** Epochs older than this were compacted from the bounded membership history. */
156
- protected _objectMapMembershipHistoryFloor = 0;
157
58
  protected _showStats = false;
158
59
  protected _shadingMode: ShadingMode = 'solid';
159
60
  protected _statePersistence: EditorStatePersistence | null = null;
@@ -162,22 +63,16 @@ export class EditorShellStore {
162
63
  * Bumped only when post-processing-relevant state changes (environment / scene
163
64
  * structure / play-stop scene swap). The viewport rebuilds its EffectComposer
164
65
  * only when this changes — NOT on every notify — so a transform drag/scrub (which
165
- * notifies per frame but never touches these) no longer recompiles shaders 60×/s.
66
+ * notifies per frame but never touches these) never recompiles shaders 60×/s.
166
67
  */
167
68
  protected _composerVersion = 0;
168
69
  bindScene(
169
70
  scene: THREE.Scene,
170
71
  renderer?: THREE.WebGLRenderer,
171
- batchedRenderer?: BatchedRenderer | null,
172
72
  camera?: THREE.Camera,
173
73
  ): void {
174
- // A stage mounting while a scene is adopted binds the edit scene UNDER the adoption, where
175
- // Stop restores it; the adopted scene stays the active one.
176
- const base = this._adoptionStack[0];
177
- if (base) base.scene = scene;
178
- else this._scene = scene;
74
+ this._scene = scene;
179
75
  this._renderer = renderer ?? null;
180
- this._batchedRenderer = batchedRenderer ?? null;
181
76
  if (camera) this._camera = camera;
182
77
  }
183
78
 
@@ -187,18 +82,14 @@ export class EditorShellStore {
187
82
  }
188
83
 
189
84
  /** Update only the editor presentation camera (for perspective/orthographic
190
- * switching) without rebinding or replacing an adopted game scene. */
85
+ * switching) without rebinding the scene. */
191
86
  setViewportCamera(camera: THREE.Camera): void {
192
87
  this._camera = camera;
193
88
  }
194
89
 
195
-
196
90
  /**
197
- * Where view/tool state and the autosave thumbnail go. The shell installs
198
- * its transport here (`EditorContext` binds the server SDK); the store
199
- * itself names no endpoint, so a document-local store (Asset Lab) or a
200
- * fixture store simply never persists. Same collaborator shape as
201
- * `attachHistory`, same one-owner rule.
91
+ * Attach the shell’s view-state and thumbnail persistence transport. A document-local store may
92
+ * omit it. One collaborator owns persistence for the store.
202
93
  */
203
94
  attachStatePersistence(persistence: EditorStatePersistence): void {
204
95
  if (this._statePersistence === persistence) return;
@@ -217,12 +108,12 @@ export class EditorShellStore {
217
108
 
218
109
  /**
219
110
  * Frame the whole active scene — the first thing a reader should see when a
220
- * world they did not author is adopted (an ingest mount). The viewport keeps
111
+ * world they did not author opens. The viewport keeps
221
112
  * asking until the world HAS content, because a game builds itself
222
113
  * asynchronously; any camera input from the reader cancels it (see
223
114
  * `the world root's stage`). A no-op for a scene the reader already has a camera on.
224
115
  *
225
- * `gameCamera` is a LIVE LOOKUP, not a camera: an adopted game's own camera
116
+ * `gameCamera` is a LIVE LOOKUP, not a camera: a game's own camera
226
117
  * may not exist yet on the frame it is captured on, so the viewport asks
227
118
  * again while its window is open. When one turns up, its view is adopted
228
119
  * instead of any measurement — see `scene-framing.ts`.
@@ -232,10 +123,9 @@ export class EditorShellStore {
232
123
  this.shell.focusOnScene();
233
124
  }
234
125
 
235
-
236
126
  /**
237
127
  * Live viewport camera pose, read straight from the bound Three.js camera +
238
- * orbit target (`bindScene`/`setOrbitTarget` — wired by `world-root-stage.ts`).
128
+ * orbit target (`bindScene`/`setOrbitTarget`).
239
129
  * Null only when nothing has bound a camera yet (e.g. a headless store in a
240
130
  * unit test that never mounted `the world root's stage`).
241
131
  */
@@ -271,40 +161,6 @@ export class EditorShellStore {
271
161
  return this._gizmoEpoch;
272
162
  }
273
163
 
274
- /**
275
- * Return the exact projected parents changed after `epoch`, or `null` when a
276
- * root/unclassified change crossed that boundary and callers must rebuild.
277
- * Parent timestamps make this safe across React batching: no consumer drains
278
- * a shared queue, and several projectile commits collapse into one union.
279
- */
280
- ingestObjectMapChangesSince(epoch: number): IngestObjectMapChanges | null {
281
- if (epoch < 0 || epoch > this._gizmoEpoch || epoch < this._objectMapFullChangeEpoch) {
282
- return null;
283
- }
284
- const changedParentIds = new Set<string>();
285
- for (const [id, changedAt] of this._objectMapParentChangedAt) {
286
- if (changedAt > epoch) changedParentIds.add(id);
287
- }
288
- return { epoch: this._gizmoEpoch, changedParentIds };
289
- }
290
-
291
- /** Return exact added/removed identities since `epoch`, or `null` across an unclassified edit. */
292
- ingestObjectMapMembershipChangesSince(epoch: number): IngestObjectMapMembershipChanges | null {
293
- if (
294
- epoch < 0 ||
295
- epoch > this._gizmoEpoch ||
296
- epoch < this._objectMapMembershipFullChangeEpoch ||
297
- epoch < this._objectMapMembershipHistoryFloor
298
- ) {
299
- return null;
300
- }
301
- const changedIds = new Set<string>();
302
- for (const [id, changedAt] of this._objectMapMembershipChangedAt) {
303
- if (changedAt > epoch) changedIds.add(id);
304
- }
305
- return { epoch: this._gizmoEpoch, changedIds };
306
- }
307
-
308
164
  isSkeletonVisible(id: string): boolean {
309
165
  return Boolean(getUserData(this._objectMap.get(id), 'skeletonVisible'));
310
166
  }
@@ -326,10 +182,6 @@ export class EditorShellStore {
326
182
  get shadingMode(): ShadingMode {
327
183
  return this._shadingMode;
328
184
  }
329
- /** Notify subscribers after an ingest adapter mutated a live foreign object
330
- * (material/visibility). Runtime-only — no dirty/autosave, because the edit
331
- * lives on the live `Object3D` and nothing here owns a document. */
332
-
333
185
  /** Notify after a live adapter changed `objectMap` membership.
334
186
  *
335
187
  * Unlike an ordinary reflected-property edit, a membership change requires
@@ -337,191 +189,9 @@ export class EditorShellStore {
337
189
  * place, and a remove+add burst can keep its size unchanged, so neither map
338
190
  * identity nor size is a complete signal. `_gizmoEpoch` is the viewport's
339
191
  * existing explicit rebuild input; bump it only at this structural seam. */
340
- notifyIngestObjectMapEdit(delta?: IngestObjectMapStructureDelta): void {
192
+ notifyIngestObjectMapEdit(): void {
341
193
  this._gizmoEpoch++;
342
- if (delta === undefined || delta.rootsChanged) {
343
- this._objectMapFullChangeEpoch = this._gizmoEpoch;
344
- this._objectMapParentChangedAt.clear();
345
- } else {
346
- for (const id of delta.changedParentIds) {
347
- this._objectMapParentChangedAt.set(id, this._gizmoEpoch);
348
- }
349
- }
350
- if (delta?.addedIds === undefined || delta.removedIds === undefined) {
351
- this._objectMapMembershipFullChangeEpoch = this._gizmoEpoch;
352
- this._objectMapMembershipChangedAt.clear();
353
- } else {
354
- for (const id of delta.addedIds) this._objectMapMembershipChangedAt.set(id, this._gizmoEpoch);
355
- for (const id of delta.removedIds) {
356
- this._objectMapMembershipChangedAt.set(id, this._gizmoEpoch);
357
- }
358
- const historyFloor = Math.max(0, this._gizmoEpoch - OBJECT_MAP_MEMBERSHIP_HISTORY);
359
- if (historyFloor > this._objectMapMembershipHistoryFloor) {
360
- this._objectMapMembershipHistoryFloor = historyFloor;
361
- for (const [id, changedAt] of this._objectMapMembershipChangedAt) {
362
- if (changedAt <= historyFloor) this._objectMapMembershipChangedAt.delete(id);
363
- }
364
- }
365
- }
366
- const affectsShell =
367
- delta === undefined ||
368
- delta.rootsChanged ||
369
- [...(delta.addedIds ?? []), ...(delta.removedIds ?? [])].some((id) =>
370
- this.shell.selectedEntityIds.has(id),
371
- );
372
- this.shell.notifyChange('content', affectsShell ? 'shell' : 'object-map', false);
373
- }
374
-
375
- // --- Play mode scene swap ---
376
-
377
- /** Callback to sync transform edits to game ECS. Set by play-mode, cleared on exit.
378
- * Receives the NODE ID as well as the live object: the `PhysicsAdapter` seam
379
- * it ultimately feeds is id-keyed since P-4, and the id is already in hand
380
- * at the one call site — so nothing downstream has to map an object back. */
381
- protected _ecsSyncTransform: ((id: string, obj: THREE.Object3D) => void) | null = null;
382
-
383
- /** Set a callback that syncs editor transform changes to the game's ECS state. */
384
- setEcsSyncTransform(fn: ((id: string, obj: THREE.Object3D) => void) | null): void {
385
- this._ecsSyncTransform = fn;
386
- }
387
-
388
- /**
389
- * Swap the store's active scene to the game scene. Editor scene is preserved.
390
- *
391
- * Also snapshots whatever the active document half hands back from
392
- * {@link _captureAdoptionExtras}. A half that keeps mutating its LIVE
393
- * metadata during play (so its inspector fields keep working) must have
394
- * those edits discarded when play stops — otherwise a play-time edit
395
- * silently persists with `isDirty` false. `exitPlayScene` restores the
396
- * snapshot.
397
- */
398
- enterPlayScene(
399
- gameScene: THREE.Scene,
400
- imageConfig?: WorldRendererConfig | undefined,
401
- projectedObjects?: ReadonlyMap<string, THREE.Object3D>,
402
- ): void {
403
- this._adoptionStack.push({
404
- scene: this._scene,
405
- objectMap: this._objectMap,
406
- extras: this._captureAdoptionExtras(),
407
- selection: new Set(this.shell.selectedEntityIds),
408
- imageConfig: this._adoptedImageConfig,
409
- });
410
- this._scene = gameScene;
411
- this._applyAdoptedImageConfig(imageConfig);
412
-
413
- // The store INDEXES an adopted scene; it no longer models it. A foreign
414
- // adapter supplies its pure projection explicitly, so editor identity is
415
- // never written into the game's Object3Ds. Source-backed/adopted worlds
416
- // already carry native authoring stamps and retain the fallback walk.
417
- const gameMap = projectedObjects
418
- ? new Map(projectedObjects)
419
- : (() => {
420
- const stamped = new Map<string, THREE.Object3D>();
421
- const visit = (obj: THREE.Object3D): void => {
422
- if (isEditorOwnedObject(obj)) return;
423
- const eid = entityIdOf(obj);
424
- if (eid !== undefined) stamped.set(eid, obj);
425
- for (const child of obj.children) visit(child);
426
- };
427
- for (const child of gameScene.children) visit(child);
428
- return stamped;
429
- })();
430
- this._objectMap = gameMap;
431
-
432
- this.shell.applySelectionBeforePresentation(
433
- [...this.shell.selectedEntityIds].filter((id) => gameMap.has(id)),
434
- );
435
- // A viewport LOD pin must not follow the entity id into the adopted game
436
- // scene — there `WebGLRenderer.projectObject` owns level visibility (it
437
- // calls `lod.update(camera)` for every visible LOD whose `autoUpdate` is
438
- // still true, which is what a pin turns OFF), and per-frame enforcement
439
- // would fight it.
440
- this._lodForcedLevels.clear();
441
- this._composerVersion++; // scene swapped → composer must rebuild against the game scene
442
- this.shell.notifyChange();
443
- }
444
-
445
- /** Whether the active scene is one another document adopted (`enterPlayScene`). */
446
- get hasAdoptedScene(): boolean {
447
- return this._adoptionStack.length > 0;
448
- }
449
-
450
- /** Restore the previously-adopted scene (LIFO — see enterPlayScene). */
451
- exitPlayScene(): void {
452
- const frame = this._adoptionStack.pop();
453
- if (!frame) return;
454
- this._scene = frame.scene;
455
- this._objectMap = frame.objectMap;
456
- this._applyAdoptedImageConfig(frame.imageConfig);
457
- // Discard any play-time env/name/ui edits — restore the pre-play snapshot.
458
- this._restoreAdoptionExtras(frame.extras);
459
- this.shell.applySelectionBeforePresentation([...frame.selection]);
460
- this._composerVersion++; // scene swapped back → composer must rebuild against the editor scene
461
- this.shell.notifyChange();
462
- }
463
-
464
- /**
465
- * Release ONE adopter's adoption, wherever it sits in the stack. When
466
- * `scene` is the ACTIVE scene this is exactly `exitPlayScene`; when a later
467
- * adoption (play over the R3F design session) has already replaced it, the
468
- * frame that would restore `scene` is unlinked from the middle of the stack
469
- * instead — nothing visible changes now, and the LATER adopter's exit
470
- * restores the state underneath as if this adoption never happened. No-op
471
- * when `scene` was never adopted (or was already released).
472
- */
473
- releaseAdoptedScene(scene: THREE.Scene): void {
474
- if (this._scene === scene) {
475
- this.exitPlayScene();
476
- return;
477
- }
478
- const index = this._adoptionStack.findIndex((frame) => frame.scene === scene);
479
- if (index === -1) return;
480
- this._adoptionStack.splice(index, 1);
481
- }
482
-
483
- /**
484
- * THE ADOPTED CONTENT'S OWN COLOUR PIPELINE — what it is, not where it is
485
- * applied.
486
- *
487
- * A mounted world may report the pipeline it was authored for
488
- * (`MountedThreeRoot.rendererConfig`), and `applyWorldRendererConfig` puts it
489
- * on the renderer its own host handed it. In the editor those are two
490
- * DIFFERENT renderers: the design session mounts against a non-rasterizing
491
- * one (`authoring/design-time-renderer.ts`) and the viewport draws the
492
- * mounted scene with its own. So the declaration landed on a surface with no
493
- * pixels while the surface with pixels kept the engine's defaults — measured
494
- * on the translated Godot platformer, whose roughness-0 metal reads as chrome
495
- * under the editor's ACES while the game renders it under its own curve.
496
- *
497
- * The store only REMEMBERS the declaration and bumps `_composerVersion` when
498
- * it changes; `the world root's stage.rebuildComposer` is the one place that derives
499
- * the viewport renderer's whole configuration, and it applies this last. That
500
- * split is why there is no restore bookkeeping here: a rebuild always resets
501
- * the renderer to the engine defaults first, so "unwind" is just the next
502
- * rebuild seeing a different (or absent) config.
503
- */
504
- protected _adoptedImageConfig: WorldRendererConfig | undefined;
505
-
506
- /** What the active adopted content declared about the renderer that draws it,
507
- * or `undefined` when it declared nothing (leave the editor's own alone). */
508
- get adoptedImageConfig(): WorldRendererConfig | undefined {
509
- return this._adoptedImageConfig;
510
- }
511
-
512
- protected _applyAdoptedImageConfig(next: WorldRendererConfig | undefined): void {
513
- if (next === this._adoptedImageConfig) return;
514
- this._adoptedImageConfig = next;
515
- this._composerVersion++;
516
- }
517
-
518
- /** True while an ADOPTED live game scene is the active scene — every
519
- * `enterPlayScene` caller: play adoption, ingest mounts, and the R3F
520
- * design session. Adopted scenes own their lighting/rendering completely;
521
- * the viewport uses this to keep editor-fabricated presentation (the
522
- * helper light rig) out of them. */
523
- get isAdoptedSceneActive(): boolean {
524
- return this._adoptionStack.length > 0;
194
+ this.shell.notifyChange('content', 'shell', false);
525
195
  }
526
196
 
527
197
  // --- Vertex snap ---
@@ -543,25 +213,10 @@ export class EditorShellStore {
543
213
  * represented honestly. Both use the same offscreen render-target technique,
544
214
  * so neither needs `preserveDrawingBuffer` on the live WebGL canvas.
545
215
  *
546
- * THE COLOUR PIPELINE IS THE WHOLE REASON THERE ARE TWO TARGETS. Three
547
- * applies `renderer.toneMapping` and `renderer.outputColorSpace` ONLY when
548
- * the destination is the canvas: rendering into an ordinary
549
- * `WebGLRenderTarget` compiles every material with `NoToneMapping` and a
550
- * LINEAR output transfer (`WebGLPrograms.getParameters` and
551
- * `WebGLRenderer.setProgram` both gate on `currentRenderTarget === null`).
552
- * So the single-target version of this function answered "what does the
553
- * viewport look like" with a raw linear frame — measurably darker and
554
- * flatter than the pixels on screen, and immune to every tone-mapping
555
- * change (verified live: flipping the viewport to `NoToneMapping` moved the
556
- * captured PNG by 0 bytes). That made this door — the only look at the EDIT
557
- * viewport, and the source of every project thumbnail — unusable as
558
- * evidence about the image.
559
- *
560
- * The fix is three's own: render the scene into a HALF-FLOAT linear buffer
561
- * (so highlights survive to be tone-mapped) and resolve it through
562
- * `OutputPass`, which reads `toneMapping`/`toneMappingExposure`/
563
- * `outputColorSpace` off the renderer and applies exactly what the on-screen
564
- * frame gets.
216
+ * Render into a half-float linear target to preserve highlights, then resolve through
217
+ * `OutputPass` using the renderer’s toneMapping, toneMappingExposure and outputColorSpace.
218
+ * Three applies those settings directly only when the destination is the canvas, so the
219
+ * output pass gives captures the same colour transform as the on-screen viewport.
565
220
  */
566
221
  protected _renderViewportImage(
567
222
  size: number | { width: number; height: number },
@@ -711,8 +366,6 @@ export class EditorShellStore {
711
366
  shadingMode: this._shadingMode,
712
367
  };
713
368
 
714
- Object.assign(state, this._editorStateExtras());
715
-
716
369
  if (this._camera) {
717
370
  const cam = this._camera as THREE.PerspectiveCamera;
718
371
  state['camera'] = {
@@ -727,12 +380,9 @@ export class EditorShellStore {
727
380
  // --- Mutations ---
728
381
 
729
382
  /**
730
- * EDITOR-ONLY level-preview override (W2b): entity id → index into the
731
- * live THREE.LOD's (ascending-by-distance) levels to pin in the viewport.
732
- * Pure UI state — never written to the descriptor, so it can't leak into
733
- * the saved scene or the undo history. Enforced per frame by
734
- * `EditorViewport.update` (the glTF loads async and entity rebuilds replace
735
- * the THREE.LOD instance, so a one-shot apply would silently un-pin).
383
+ * Editor-only pinned LOD-level index per entity, ordered by distance. Keep this view state out of
384
+ * saved descriptors and undo history. EditorViewport reapplies it each frame as asynchronous
385
+ * loads and entity rebuilds replace LOD instances.
736
386
  */
737
387
  protected _lodForcedLevels = new Map<string, number>();
738
388
 
@@ -772,29 +422,7 @@ export class EditorShellStore {
772
422
  this.shell.notifyChange();
773
423
  }
774
424
 
775
- // --- Subclass hooks -------------------------------------------------------
776
- // The ONLY way the shell reaches into a document half. Each is a no-op here,
777
- // so the shell runs standalone; a subclass that owns a document overrides the
778
- // ones it needs (`store-notify-scope.test.ts` exercises the deferral hooks).
779
-
780
425
 
781
-
782
- /** Captured into the adoption frame by `enterPlayScene`, handed back by
783
- * `exitPlayScene`. A document half snapshots its metadata here so
784
- * play-time edits to it are discarded on exit. */
785
- protected _captureAdoptionExtras(): unknown {
786
- return undefined;
787
- }
788
-
789
- /** Restore counterpart of {@link _captureAdoptionExtras}. */
790
- protected _restoreAdoptionExtras(_extras: unknown): void {}
791
-
792
-
793
- /** Extra keys folded into the periodic editor-state save. A document half
794
- * contributes whatever it needs restored on the next boot. */
795
- protected _editorStateExtras(): Record<string, unknown> {
796
- return {};
797
- }
798
426
  }
799
427
 
800
428
  const threeStates = new WeakMap<ShellStore, EditorShellStore>();