@volter/editor-threejs 0.5.211 → 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,51 +1,26 @@
1
1
  /**
2
- * The STANDARD VIEWPORT dressing — the ONE chrome-and-dressing definition every
3
- * 3D document viewport inherits (owner-ratified contract). A document surface
4
- * (`Object3DDocumentViewport` and everything mounted on it: the 3D board,
5
- * story turntables, model/entity asset documents) gets the editor's standard
6
- * look from this module instead of instantiating its own background/lighting:
7
- *
8
- * - `scene.environment` baked from three's own `RoomEnvironment` through
9
- * `PMREMGenerator`, so PBR materials actually read;
10
- * - a subtle dark vertical-gradient backdrop (never a flat hex);
11
- * - one NEUTRAL directional key light for definition (it was warm; see the
12
- * measurement where it is constructed);
13
- * - an optional ground grid following the Scene viewport's grid conventions
14
- * (1 m cells, `0x999999`, `EDITOR_LAYER` — see `editor-viewport.ts`),
15
- * centred on the content and fading out with distance so it never
16
- * degenerates into convergence-line artifacts at the horizon.
17
- *
18
- * Everything created here is EDITOR CHROME: marked per `editor-layers.ts`'s
19
- * conventions (`userData.editorHelper`, and `EDITOR_LAYER` for the grid) so no
20
- * hierarchy walk, pick, or export ever mistakes it for content — and it is
21
- * never serialized, never touching project data (the anti-shim rule). It
22
- * exists only inside the document viewport's scene at view time.
23
- *
24
- * Ownership: {@link applyStandardViewportDressing} owns every object and
25
- * texture it creates, INCLUDING the {@link StandardEnvironment} handed to it
26
- * (PMREM render targets leak if not disposed). Its returned `dispose` is the
27
- * one teardown path; it is idempotent.
28
- *
29
- * The environment bake lives in `@volter/editor-threejs/viewport/environment`
30
- * because it is the one GL-bound step — it needs a live `WebGLRenderer`,
31
- * where the rest of the dressing is plain scene-graph work.
2
+ * Standard dressing for 3D document viewports: a supplied environment for PBR materials, gradient
3
+ * backdrop and optional ground grid centered on content. Mark all objects as
4
+ * editor furniture and keep them out of authored data. applyStandardViewportDressing owns its
5
+ * objects, textures and supplied StandardEnvironment, and returns an idempotent disposer. The
6
+ * renderer-dependent bake belongs to viewport/environment.
32
7
  */
33
8
 
34
- import { invalidateStages } from '@volter/sdk/kit/stage-invalidation';
9
+ import { invalidateStages } from '@volter/sdk/stage-invalidation';
35
10
  import { contentWorldBounds } from '@volter/editor-threejs/viewport/content-bounds';
36
11
  import { EDITOR_LAYER } from '@volter/editor-threejs/viewport/editor-layers';
37
12
  import { loadEnvironmentImage, type StandardEnvironment } from '@volter/editor-threejs/viewport/environment';
38
13
  import { setUserData } from '@volter/editor-threejs/ecs/user-data';
39
14
  import * as THREE from 'three';
40
- import { environmentImage } from '@volter/sdk/kit/environment-images';
41
- import { editorConsole } from '@volter/sdk/kit/editor-console';
15
+ import { environmentImage } from '@volter/sdk/environment-images';
16
+ import { editorConsole } from '@volter/sdk/editor-console';
42
17
  import {
43
18
  studioPreset,
44
19
  type StudioLight,
45
20
  type SceneTakeover,
46
21
  type StudioPreset,
47
22
  type ViewportPresentation,
48
- } from '@volter/sdk/kit/viewport-presentation';
23
+ } from '@volter/sdk/viewport-presentation';
49
24
 
50
25
  /**
51
26
  * Front-right three-quarter view for project-owned R3F components. Their
@@ -62,21 +37,13 @@ export const STANDARD_COMPONENT_CAMERA_DIRECTION = [0.8, 0.5, -1] as const;
62
37
  const GRADIENT_BOTTOM = new THREE.Color(0x1e2530);
63
38
  const GRADIENT_TOP = new THREE.Color(0x4c5b70);
64
39
 
65
- export interface BackdropStops {
40
+ interface BackdropStops {
66
41
  readonly bottom: THREE.Color;
67
42
  readonly top: THREE.Color;
68
43
  }
69
44
 
70
- /**
71
- * The backdrop's stops for the ACTIVE PALETTE: the ground line is the
72
- * palette's panel surface and the top is its raised surface, each blended
73
- * with the studio tint and lifted for ACES. Measured under the Blender
74
- * palette (panel #303030, raised #3d3d3d) the stage reads as Blender's grey
75
- * ground instead of a black void; under Classic Graphite it stays within a
76
- * few steps of the studio constants. Falls back to the studio constants
77
- * where no palette is installed (a bounded host, headless).
78
- */
79
- export function paletteBackdropStops(): BackdropStops {
45
+ /** Blend palette panel and raised surfaces with studio tint for the backdrop. Use studio constants when no palette is installed. */
46
+ function paletteBackdropStops(): BackdropStops {
80
47
  const root =
81
48
  typeof document === 'undefined' ? null : document.querySelector('[data-volter-palette]');
82
49
  if (!root) return { bottom: GRADIENT_BOTTOM.clone(), top: GRADIENT_TOP.clone() };
@@ -100,11 +67,7 @@ export function paletteBackdropStops(): BackdropStops {
100
67
  };
101
68
  const panel = read('--volter-surface-panel', GRADIENT_BOTTOM);
102
69
  const raised = read('--volter-surface-raised', GRADIENT_TOP);
103
- // A background texture is not tone-mapped, so what is authored here is
104
- // what the screen shows. The small lift keeps the ground a step above the
105
- // panel it sits beside (Blender's viewport is lighter than its editors).
106
- // Measured on screen under the Blender palette: ground #40, top #50, beside
107
- // Blender's own #3f–#4e.
70
+ // Background textures bypass tone mapping; lift the authored gradient slightly above the neighbouring panel.
108
71
  const bottom = panel.lerp(GRADIENT_BOTTOM, 0.25).lerp(new THREE.Color(0xffffff), 0.03);
109
72
  const top = raised.lerp(GRADIENT_TOP, 0.25).lerp(new THREE.Color(0xffffff), 0.04);
110
73
  return { bottom, top };
@@ -114,8 +77,7 @@ export function paletteBackdropStops(): BackdropStops {
114
77
  * Fires when the palette the backdrop follows changes. The theme installer
115
78
  * stamps its root with `data-volter-palette` / `data-volter-material` on every
116
79
  * switch, so the DOM is the contract here — a document stage never imports
117
- * the shell's preference store (its closure is pinned,
118
- * `scripts/validate-editor-closure.mjs`). Returns the unsubscribe.
80
+ * the shell's preference store. Returns the unsubscribe.
119
81
  */
120
82
  export function watchPaletteBackdrop(onChange: () => void): () => void {
121
83
  if (typeof document === 'undefined' || typeof MutationObserver === 'undefined') return () => {};
@@ -133,18 +95,13 @@ export function watchPaletteBackdrop(onChange: () => void): () => void {
133
95
  }
134
96
 
135
97
  /** One 1×N vertical-gradient strip; the renderer stretches it full-frame. */
136
- export function createGradientBackgroundTexture(
137
- stops: BackdropStops = paletteBackdropStops(),
138
- ): THREE.DataTexture {
98
+ export function createGradientBackgroundTexture(): THREE.DataTexture {
99
+ const stops = paletteBackdropStops();
139
100
  const height = 128;
140
101
  const data = new Uint8Array(height * 4);
141
102
  const color = new THREE.Color();
142
103
  for (let row = 0; row < height; row++) {
143
- // Row 0 is the BOTTOM of the texture, so the gradient runs ground → sky.
144
- // `Color` holds LINEAR components under colour management while the
145
- // texture below is declared sRGB, so the bytes are encoded on the way
146
- // out — written raw, every stop rendered several steps darker than
147
- // authored (measured: the stage read #050506 for a #1e2530 stop).
104
+ // Row zero is the texture bottom. Encode Color’s linear components to sRGB bytes for the declared texture colour space.
148
105
  color
149
106
  .copy(stops.bottom)
150
107
  .lerp(stops.top, row / (height - 1))
@@ -203,28 +160,9 @@ function gridDivisions(size: number): number {
203
160
  * reference; the band scales with `gridFrameForScene`, which already tracks
204
161
  * the content. Injection at `color_fragment` is where `LineBasicMaterial` has
205
162
  * `diffuseColor` in scope.
206
- *
207
- * Exported because the dressing OWNS the grid treatment: `editor-viewport.ts`
208
- * applies the same fade to its own editing grid, so no viewport anywhere ends
209
- * a grid on a hard aliasing rim.
210
163
  */
211
- export function applyGridDistanceFade(material: THREE.Material, gridSize: number): void {
212
- // The band ends at the grid's own extent: it exists so the floor never ends
213
- // on a hard rim, not to shrink the floor. The old 0.18/0.42 band was tuned
214
- // for a camera standing close — a document that opens further back (the
215
- // Model document's `openingFit`) photographed a floor with NO visible grid
216
- // at all, measured against Blender's uniform lattice.
217
- //
218
- // `gridSize` is `GridHelper`'s size, which is the grid's DIAMETER — it spans
219
- // ±size/2 — while the shader below compares against a RADIUS
220
- // (`length(vGridPlanePosition)`). Comparing the two directly put the whole
221
- // band outside the geometry: on a 400 m grid the fade started at 220 m when
222
- // the farthest point along either axis is 200 m, so alpha never left 1.0 and
223
- // the floor ended on exactly the hard rim this function exists to prevent
224
- // (measured against Blender: our horizon stepped from #3f3f3f to #4f4f4f in
225
- // four scanlines across the full stage width, Blender's held #3f3f3f-#41
226
- // throughout). The band is a fraction of the RADIUS, so it reaches zero at
227
- // the grid's edge, where the fade belongs.
164
+ function applyGridDistanceFade(material: THREE.Material, gridSize: number): void {
165
+ // Fade to zero at the grid’s radius (half GridHelper’s size), retaining a band proportional to that radius.
228
166
  const gridRadius = gridSize / 2;
229
167
  const fadeStart = gridRadius * 0.55;
230
168
  const fadeEnd = gridRadius;
@@ -251,30 +189,23 @@ export function applyGridDistanceFade(material: THREE.Material, gridSize: number
251
189
  material.customProgramCacheKey = () => `volter-grid-fade:${fadeStart}:${fadeEnd}`;
252
190
  }
253
191
 
254
- export interface StandardViewportDressingOptions {
192
+ interface StandardViewportDressingOptions {
255
193
  /**
256
194
  * The baked IBL from `@volter/editor-threejs/viewport/environment`; `null` opts the
257
195
  * scene out of an environment entirely. Ownership transfers here.
258
196
  */
259
197
  readonly environment: StandardEnvironment | null;
260
- /** Apply the vertical-gradient backdrop. Default `true`; a host whose
261
- * backdrop lives elsewhere (the Asset Lab's alpha-canvas studio stage, an
262
- * explicit SDK-contributed background) opts out. */
263
- readonly background?: boolean;
264
- /** Add the warm directional key light. Default `true`; a source that
265
- * authors its own lights opts out — authored lighting always wins. */
266
- readonly keyLight?: boolean;
267
- /** Add the ground grid (content standing on y=0 by design). Default `false`. */
268
- readonly grid?: boolean;
269
- readonly content?: THREE.Object3D;
198
+ /** Apply the vertical-gradient backdrop unless the stage paints its background elsewhere. */
199
+ readonly background: boolean;
200
+ /** Add the ground grid, centered on the content's footprint. */
201
+ readonly grid: boolean;
202
+ readonly content: THREE.Object3D;
270
203
  }
271
204
 
272
205
  export interface StandardViewportDressing {
273
206
  /** The gradient applied to `scene.background`, or `null` when opted out —
274
207
  * hosts hand it on (e.g. as a document session's neutral background). */
275
208
  readonly backgroundTexture: THREE.Texture | null;
276
- /** The dressing's lights, for hosts whose lighting presets retune them. */
277
- readonly lights: readonly THREE.Light[];
278
209
  frameContent(content: THREE.Object3D): void;
279
210
  /** The ONE teardown path for everything this dressing created. Idempotent. */
280
211
  dispose(): void;
@@ -285,9 +216,7 @@ export function applyStandardViewportDressing(
285
216
  scene: THREE.Scene,
286
217
  options: StandardViewportDressingOptions,
287
218
  ): StandardViewportDressing {
288
- const { environment, background = true, keyLight = true, grid = false } = options;
289
- const chrome: THREE.Object3D[] = [];
290
- const lights: THREE.Light[] = [];
219
+ const { environment, background, grid } = options;
291
220
  let backgroundTexture: THREE.DataTexture | null = null;
292
221
 
293
222
  if (environment) scene.environment = environment.texture;
@@ -297,22 +226,6 @@ export function applyStandardViewportDressing(
297
226
  scene.background = backgroundTexture;
298
227
  }
299
228
 
300
- if (keyLight) {
301
- // The kit's own key, warm, as the kit studio preset states it (`KIT_STUDIO_PRESET`). A
302
- // document stage is lit by its view's presentation instead (`StagePresentationRig`, and
303
- // `StageHost` turns this key off); a stage without a presentation still gets this one.
304
- const key = new THREE.DirectionalLight(0xfff3dd, 1.9);
305
- key.name = 'volter:standard-dressing-key';
306
- key.position.set(6, 10, -4);
307
- key.castShadow = true;
308
- // `userData.editorHelper` keeps it out of hierarchy walks and picks;
309
- // layers stay default so it lights layer-0 content under any camera.
310
- setUserData(key, 'editorHelper', true);
311
- scene.add(key);
312
- chrome.push(key);
313
- lights.push(key);
314
- }
315
-
316
229
  let gridHelper: THREE.GridHelper | null = null;
317
230
  let gridFrame = '';
318
231
  const frameContent = (content: THREE.Object3D) => {
@@ -324,7 +237,6 @@ export function applyStandardViewportDressing(
324
237
  if (gridHelper) {
325
238
  gridHelper.removeFromParent();
326
239
  gridHelper.dispose();
327
- chrome.splice(chrome.indexOf(gridHelper), 1);
328
240
  }
329
241
  // The Scene viewport's grid color (`editor-viewport.ts`), softened with
330
242
  // transparency so it reads as a ground reference under the gradient
@@ -343,23 +255,18 @@ export function applyStandardViewportDressing(
343
255
  helper.layers.set(EDITOR_LAYER);
344
256
  setUserData(helper, 'editorHelper', true);
345
257
  scene.add(helper);
346
- chrome.push(helper);
347
258
  };
348
- frameContent(options.content ?? scene);
259
+ frameContent(options.content);
349
260
 
350
261
  let disposed = false;
351
262
  return {
352
263
  backgroundTexture,
353
- lights,
354
264
  frameContent,
355
265
  dispose(): void {
356
266
  if (disposed) return;
357
267
  disposed = true;
358
- for (const object of chrome) {
359
- object.removeFromParent();
360
- // GridHelper frees its geometry+material; Light frees its shadow map.
361
- (object as { dispose?: () => void }).dispose?.();
362
- }
268
+ gridHelper?.removeFromParent();
269
+ gridHelper?.dispose();
363
270
  if (environment) {
364
271
  if (scene.environment === environment.texture) scene.environment = null;
365
272
  environment.dispose();
@@ -373,34 +280,15 @@ export function applyStandardViewportDressing(
373
280
  }
374
281
 
375
282
  /**
376
- * THE STAGE'S LIGHTING, FROM ITS PRESENTATION — the Three half of
377
- * `@volter/sdk/kit/viewport-presentation`. A 3D document stage owns one rig; it draws
378
- * the view's `studio` lighting (a preset's lights, world-fixed or camera-locked, and its
379
- * ambient), sets the renderer's tone mapper and exposure, and answers the image-based light's
380
- * strength. It replaces the lights that were fixed in code: the dressing's key and the
381
- * viewport's own ambient and directional (`docs/VIEWPORT-STAGE.md` §The ruling). It moves to
382
- * `@volter/editor-threejs` with the rest of the assembled viewport (ARCHITECTURE.md, the plan,
383
- * unit 3).
384
- *
385
- * A `studio` draw lights by its preset alone: the stage darkens the content's own lights for
386
- * that draw and restores them after it (`Object3DDocumentHost.darkenContentLights`). The
387
- * `document` preset is the document's own view-locked studio (Blender's Solid lights), which the
388
- * document manages together with its scene's lights.
389
- *
390
- * Not yet: the `preview` source (a sun and a sky) draws nothing, and a Blender document's
391
- * `scene` source draws unlit, because Blender's render lighting lives in the Blender engine
392
- * (`BlenderRuntimeView.setRendered`) rather than as lights in the document's content.
283
+ * Stage lighting from viewport-presentation: own the studio preset’s lights and ambient term, apply
284
+ * tone mapping and exposure, and report environment intensity. Studio drawing temporarily darkens
285
+ * authored lights. The document preset leaves its view-locked lighting and scene-light management
286
+ * to the document; Blender render lighting belongs to BlenderRuntimeView.setRendered.
393
287
  */
394
288
  const LIGHT_DISTANCE = 12;
395
289
 
396
- /**
397
- * GODOT'S FILMIC CURVE as three's one custom tone mapper: John Hable's curve with Godot's
398
- * exposure bias of 2, divided by the curve at the white point so white stays white
399
- * (`servers/rendering/renderer_rd/shaders/effects/tonemap.glsl`, `tonemap_filmic`, at
400
- * Godot's default `tonemap_white` of 1.0). three's own curves map white to about 0.8, which
401
- * drew Godot's sun grey. Installed once, into three's `CustomToneMapping` slot, which nothing
402
- * else on the page fills.
403
- */
290
+ /** Godot's Hable filmic curve, with exposure bias 2 and normalization at its default white point 1.
291
+ * Installed in three's CustomToneMapping slot. */
404
292
  const GODOT_FILMIC = /* glsl */ `
405
293
  vec3 volterHable( vec3 x ) {
406
294
  const float A = 0.22 * 4.0;
@@ -572,8 +460,8 @@ export class StagePresentationRig {
572
460
  new THREE.PlaneGeometry(FLOOR_EXTENT, FLOOR_EXTENT).rotateX(-Math.PI / 2),
573
461
  fadingFloorMaterial(),
574
462
  );
575
- /** The preview sky, built from its three colours: the background drawn behind the scene and
576
- * the environment that lights it, rebuilt only when the colours change. */
463
+ /** The preview's background and environment, keyed by image identity or procedural
464
+ * colours, curves, clouds and sun. */
577
465
  private sky: {
578
466
  key: string;
579
467
  /** The environment image drawn, or `null` for the procedural strip. */
@@ -612,7 +500,7 @@ export class StagePresentationRig {
612
500
  this.floor.name = 'volter:preview-floor';
613
501
  this.floor.userData['editorHelper'] = true;
614
502
  this.floor.receiveShadow = true;
615
- // Transparent now (its fade), so it is sorted with the grid: drawn first, under it.
503
+ // The fading floor is transparent and sorts with the grid; draw it first, beneath the grid.
616
504
  this.floor.renderOrder = -1;
617
505
  this.floor.visible = false;
618
506
  scene.add(this.floor);
@@ -649,8 +537,7 @@ export class StagePresentationRig {
649
537
  const altitude = THREE.MathUtils.degToRad(sun.altitude);
650
538
  const azimuth = THREE.MathUtils.degToRad(sun.azimuth);
651
539
  this.sun.color.set(sun.color);
652
- // Godot's energy as three's intensity, unscaled: the unit mapping is measured against
653
- // Godot's own frames, not assumed.
540
+ // Map Godot light energy directly to three intensity.
654
541
  this.sun.intensity = sun.enabled ? sun.energy : 0;
655
542
  this.sun.position
656
543
  .set(Math.sin(azimuth) * Math.cos(altitude), Math.sin(altitude), -Math.cos(azimuth) * Math.cos(altitude))
@@ -734,7 +621,7 @@ export class StagePresentationRig {
734
621
  this.pendingImage = { id: image.id, state: 'failed' };
735
622
  editorConsole.error(`Environment image "${image.id}" (${image.url}) did not load: ${String(error)}`, 'viewport');
736
623
  // The view still names it, so what stands is the procedural sky, never the last image.
737
- this.latestSky?.();
624
+ this.latestSky!();
738
625
  this.onReady?.();
739
626
  });
740
627
  }
@@ -773,20 +660,13 @@ export class StagePresentationRig {
773
660
  }
774
661
 
775
662
  /**
776
- * THE PREVIEW SKY, Godot's `ProceduralSkyMaterial`: above the horizon the horizon colour runs
777
- * to the top colour on the sky's top curve (Godot's 0.15), below it to the ground colour on its
778
- * ground curve (Godot's 0.02), mixed in linear light (`scene/resources/3d/sky_material.cpp`). THE SUN in
779
- * it follows the same material: inside the light's disc the sky is the sun's colour at its
780
- * energy, and out to `sun_angle_max` (30°) it returns to the sky on a curve of `sun_curve`
781
- * (0.15). A light with no angular size draws a half-degree disc.
782
- *
783
- * Drawn into a HALF-FLOAT equirectangular strip, because the sun is brighter than white: an 8-bit
784
- * strip clipped it to 1.0, which the tone curve draws as grey 202 (measured) where Godot's
785
- * sun blows out. Half, not full, float: a full-float strip cannot be linearly filtered where
786
- * `OES_texture_float_linear` is missing (common on phones) and would draw black. The strip is
787
- * both the backdrop and, prefiltered, the light; 1024 across,
788
- * because the sun's bright core is about 3° wide and drew as two pixels at 256. Placed by
789
- * three's equirectangular mapping (`atan(z, x)`, `asin(y)`); row 0 is straight down.
663
+ * Build the preview sky from Godot’s ProceduralSkyMaterial curves in linear light: horizon to top
664
+ * above the horizon, horizon to ground below it, and the light’s disc to sky across
665
+ * sun_angle_max, with a fixed half-degree disc. The half-float
666
+ * equirectangular strip preserves sun energy above white while supporting linear filtering
667
+ * without OES_texture_float_linear. Its 1024-pixel width resolves the bright core. Three’s
668
+ * mapping uses atan(z, x) and asin(y), with row zero pointing down. The strip supplies both the
669
+ * backdrop and the prefiltered lighting.
790
670
  */
791
671
  private buildSky(
792
672
  colours: ViewportPresentation['lighting']['preview']['environment']['sky'],
@@ -10,12 +10,11 @@
10
10
  * which a modeling host never carries — so the package hands the host this
11
11
  * binding and the host loads it only for a stage that shows a world.
12
12
  */
13
- import type { ComponentType, RefObject } from 'react';
13
+ import type { ComponentType } from 'react';
14
14
  import type * as THREE from 'three';
15
15
  import type { EditorShellStore } from '../editor-shell-store';
16
- import type { EditorStats } from '@volter/sdk/kit/editor-runtime';
16
+ import type { EditorStats } from '@volter/sdk/editor-runtime';
17
17
  import type { EditorViewport } from '../editor-viewport';
18
- import type { ViewportPresentation, ViewportRoot } from '../../viewport-api';
19
18
 
20
19
  export interface WorldRootStageOptions {
21
20
  /** The session's store: the world is the session's own subject. */
@@ -25,8 +24,6 @@ export interface WorldRootStageOptions {
25
24
  readonly canvas: HTMLCanvasElement;
26
25
  readonly renderer: THREE.WebGLRenderer;
27
26
  readonly stats: EditorStats;
28
- /** The camera-authoring preview pane, read per frame (a React ref). */
29
- readonly cameraPreview: () => HTMLDivElement | null;
30
27
  readonly onMountStatus: (status: 'mounting' | 'ready') => void;
31
28
  readonly onRootIds: (rootIds: readonly string[]) => void;
32
29
  /** This stage's frame hook on the per-stage viewport door. */
@@ -37,7 +34,6 @@ export interface WorldRootStage {
37
34
  readonly scene: THREE.Scene;
38
35
  readonly viewport: EditorViewport;
39
36
  frame(timeMs: number, resumed: boolean): void;
40
- present(roots: readonly ViewportRoot[]): ViewportPresentation | null;
41
37
  setHelper(kind: string, object: THREE.Object3D | null): void;
42
38
  dispose(): void;
43
39
  }
@@ -46,7 +42,6 @@ export interface WorldRootOverlayProps {
46
42
  readonly store: EditorShellStore;
47
43
  /** The stage's own document — its stage context is read by this id. */
48
44
  readonly documentId: string;
49
- readonly cameraPreviewRef: RefObject<HTMLDivElement | null>;
50
45
  readonly mountStatus: 'mounting' | 'ready';
51
46
  readonly rootIds: readonly string[];
52
47
  }
@@ -115,7 +115,7 @@ export class ConstraintHelper extends THREE.Group {
115
115
  }
116
116
 
117
117
  /** True when this helper is already the native viewport presentation for
118
- * the object. Such nodes must not also receive the legacy corner-bracket
118
+ * the object. Such nodes must not also receive the corner-bracket
119
119
  * fallback: the constraint lines/handle are their visible representation. */
120
120
  presents(object: THREE.Object3D): boolean {
121
121
  if (object === this.source) return true;
@@ -0,0 +1,114 @@
1
+ import {
2
+ isEditorPresentationActive,
3
+ subscribeEditorPresentationActivity,
4
+ } from './editor-presentation-activity';
5
+
6
+ /** Keep the renderer when a document deactivates: stop its loop and shrink the buffer while retaining geometry and program state. Dispose the context only when the document closes. */
7
+
8
+ export class DocumentRendererSession {
9
+ private rafId = 0;
10
+ private running = false;
11
+ private disposed = false;
12
+ private active = false;
13
+ private readonly unsubscribeActivity: () => void;
14
+
15
+ constructor(
16
+ private readonly frame: (time: number, resumed: boolean) => void,
17
+ private readonly releaseRenderer: () => void,
18
+ ) {
19
+ this.unsubscribeActivity = subscribeEditorPresentationActivity(() => this.syncActivity());
20
+ }
21
+
22
+ get isDisposed(): boolean {
23
+ return this.disposed;
24
+ }
25
+
26
+ get isRunning(): boolean {
27
+ return this.running;
28
+ }
29
+
30
+ /**
31
+ * Tab flip. `false` pauses the loop and keeps the GPU surface. `true`
32
+ * resumes it. Neither constructs nor disposes a renderer.
33
+ */
34
+ setActive(active: boolean): void {
35
+ if (this.disposed) return;
36
+ this.active = active;
37
+ this.syncActivity();
38
+ }
39
+
40
+ private syncActivity(): void {
41
+ if (this.active && isEditorPresentationActive()) this.start();
42
+ else this.stop();
43
+ }
44
+
45
+ /** Resizing clears the drawing buffer. Repaint immediately while visible;
46
+ * a suspended document redraws as soon as its presentation resumes. */
47
+ redraw(): void {
48
+ if (this.running && !this.disposed && isEditorPresentationActive()) {
49
+ this.guardedFrame(performance.now());
50
+ }
51
+ }
52
+
53
+ /** The one teardown path — document CLOSE / host unmount. */
54
+ dispose(): void {
55
+ this.stop();
56
+ if (this.disposed) return;
57
+ this.disposed = true;
58
+ this.unsubscribeActivity();
59
+ this.releaseRenderer();
60
+ }
61
+
62
+ private start(): void {
63
+ if (this.disposed || this.running) return;
64
+ this.running = true;
65
+ // A tab activation is a direct request to present this document. Draw one
66
+ // frame in the activation turn, then join the browser's animation clock.
67
+ // Waiting for the first rAF would make a visible tab inherit Chromium's
68
+ // background cadence (the 1 Hz signature) even when the page itself is
69
+ // visible, showing the stale canvas for that whole interval. Subsequent
70
+ // presentation remains ordinary rAF-driven work.
71
+ // Let content reset its delta baseline without advancing by the hidden interval.
72
+ this.guardedFrame(performance.now(), true);
73
+ if (!this.running || this.disposed) return;
74
+ this.rafId = requestAnimationFrame(this.tick);
75
+ }
76
+
77
+ /** The last frame's failure text, so a persistent fault narrates ONCE
78
+ * rather than sixty times a second, and a recovered loop resets. */
79
+ private lastFrameFailure: string | null = null;
80
+
81
+ /** Catch frame failures, report them through the document channel and schedule the next frame so presentation can recover. */
82
+ private guardedFrame(time: number, resumed = false): void {
83
+ try {
84
+ this.frame(time, resumed);
85
+ this.lastFrameFailure = null;
86
+ } catch (caught) {
87
+ const text = caught instanceof Error ? caught.message : String(caught);
88
+ if (text !== this.lastFrameFailure) {
89
+ this.lastFrameFailure = text;
90
+ // biome-ignore lint/suspicious/noConsole: a document frame that threw must narrate itself
91
+ console.error('[document renderer] a frame threw; the loop continues:', caught);
92
+ }
93
+ }
94
+ }
95
+
96
+ private stop(): void {
97
+ this.running = false;
98
+ if (this.rafId !== 0) {
99
+ cancelAnimationFrame(this.rafId);
100
+ this.rafId = 0;
101
+ }
102
+ }
103
+
104
+ private readonly tick = (time: number): void => {
105
+ if (!this.running || this.disposed) return;
106
+ if (!isEditorPresentationActive()) {
107
+ this.stop();
108
+ return;
109
+ }
110
+ this.guardedFrame(time);
111
+ if (!this.running || this.disposed) return;
112
+ this.rafId = requestAnimationFrame(this.tick);
113
+ };
114
+ }
@@ -0,0 +1,58 @@
1
+ /** Host visibility controls presentation work, never document ownership. */
2
+ interface PresentationActivity {
3
+ hostActive: boolean;
4
+ readonly listeners: Set<() => void>;
5
+ }
6
+
7
+ // Existing renderer sessions can survive a replacement of this module. Keep
8
+ // their subscriptions and the host's last answer on the same shared object.
9
+ /** Vite's HMR context when a dev server serves this module, typed here so the SDK carries no
10
+ * bundler types. */
11
+ const hot = (import.meta as ImportMeta & {
12
+ hot?: {
13
+ data?: { presentationActivity?: PresentationActivity };
14
+ dispose(callback: () => void): void;
15
+ };
16
+ }).hot;
17
+ const hotData = hot?.data;
18
+ const activity: PresentationActivity = hotData?.presentationActivity ?? {
19
+ hostActive: true,
20
+ listeners: new Set<() => void>(),
21
+ };
22
+ if (hotData) hotData.presentationActivity = activity;
23
+
24
+ export function isEditorPresentationActive(): boolean {
25
+ return activity.hostActive && (typeof document === 'undefined' || !document.hidden);
26
+ }
27
+ function changed(): void {
28
+ for (const listener of activity.listeners) listener();
29
+ }
30
+
31
+ const parentOrigin =
32
+ typeof document !== 'undefined' && document.referrer ? new URL(document.referrer).origin : null;
33
+
34
+ function receive(event: MessageEvent): void {
35
+ if (window.parent === window || event.source !== window.parent) return;
36
+ if (!parentOrigin || event.origin !== parentOrigin) return;
37
+ if (event.data?.type !== 'volter:embed-activity' || typeof event.data.active !== 'boolean') return;
38
+ if (activity.hostActive === event.data.active) return;
39
+ activity.hostActive = event.data.active;
40
+ changed();
41
+ }
42
+ export function subscribeEditorPresentationActivity(listener: () => void): () => void {
43
+ activity.listeners.add(listener);
44
+ return () => {
45
+ activity.listeners.delete(listener);
46
+ };
47
+ }
48
+ if (typeof window !== 'undefined') {
49
+ window.addEventListener('message', receive);
50
+ document.addEventListener('visibilitychange', changed);
51
+ if (window.parent !== window && parentOrigin) {
52
+ window.parent.postMessage({ type: 'volter:request-embed-activity' }, parentOrigin);
53
+ }
54
+ hot?.dispose(() => {
55
+ window.removeEventListener('message', receive);
56
+ document.removeEventListener('visibilitychange', changed);
57
+ });
58
+ }