@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.
- package/NOTICE +7 -2
- package/README.md +8 -13
- package/contributions/animation-mixers.service.ts +2 -4
- package/contributions/animation-timeline.utility.tsx +1 -2
- package/contributions/three.page.ts +50 -0
- package/dist-node/serving.mjs +84 -3
- package/dist-node/shared-three.mjs +145 -0
- package/package.json +296 -16
- package/serving/animation-stamp.ts +1 -1
- package/serving/index.ts +22 -5
- package/serving/model-import-conversion.ts +6 -3
- package/serving/shared-three.ts +176 -0
- package/src/adapter/constraint.ts +1 -18
- package/src/adapter/object3d-authoring-subject.ts +1 -12
- package/src/adapter/reflection-probe.ts +1 -9
- package/src/adapter/three-contract.ts +6 -60
- package/src/animation/live-mixers.ts +0 -4
- package/src/api.ts +7 -0
- package/src/asset-loaders.ts +2 -178
- package/src/capture/photograph-sampling.ts +333 -0
- package/src/capture/scene.ts +1 -1
- package/src/ecs/object-marks.ts +6 -14
- package/src/ecs/user-data.ts +3 -138
- package/src/kit/animation/three-clips-subject.ts +19 -42
- package/src/kit/asset-compare-core.ts +161 -0
- package/src/kit/asset-compare.ts +8 -27
- package/src/kit/asset-preview-command.ts +14 -19
- package/src/kit/asset-preview-framing.ts +8 -67
- package/src/kit/asset-preview.ts +109 -342
- package/src/kit/asset-workflow/model-inspection.ts +11 -154
- package/src/kit/asset-workflow/preview-resource-lifetime.ts +25 -0
- package/src/kit/authoring/component-instance-root.ts +3 -39
- package/src/kit/authoring/live-object-transform.ts +1 -30
- package/src/kit/authoring/object3d-document-session-registry.ts +7 -15
- package/src/kit/authoring/object3d-document-session.ts +471 -208
- package/src/kit/authoring/shell-viewport-policy.ts +18 -24
- package/src/kit/authoring/source-object3d-authoring-adapter.ts +6 -48
- package/src/kit/authoring/three-projection-core.ts +11 -40
- package/src/kit/authoring/viewport-pick-context.ts +5 -12
- package/src/kit/authoring/viewport-raycast.ts +15 -93
- package/src/kit/authoring/world-hidden-viewport.ts +8 -61
- package/src/kit/capture-camera-pose.ts +68 -0
- package/src/kit/collaboration-presence.ts +41 -0
- package/src/kit/components/CameraInfo.tsx +4 -6
- package/src/kit/components/InspectorObjectPreview.tsx +3 -30
- package/src/kit/components/Object3DDocumentToolbar.css +75 -0
- package/src/kit/components/Object3DDocumentToolbar.tsx +18 -84
- package/src/kit/components/Object3DDocumentViewport.tsx +5 -8
- package/src/kit/components/StageHost.css +40 -0
- package/src/kit/components/StageHost.tsx +181 -404
- package/src/kit/components/StageOverlays.tsx +1 -2
- package/src/kit/components/StatsOverlay.tsx +5 -17
- package/src/kit/components/ViewportFurniture.tsx +27 -154
- package/src/kit/components/ViewportOverlay.tsx +13 -17
- package/src/kit/components/ViewportOverlaysMenu.tsx +177 -0
- package/src/kit/components/ViewportShadingMenu.tsx +2 -42
- package/src/kit/components/ViewportViewMenu.tsx +7 -18
- package/src/kit/components/asset-viewers/EntityModelDocument.tsx +4 -13
- package/src/kit/components/asset-viewers/EnvironmentAssetDocument.tsx +5 -5
- package/src/kit/components/asset-viewers/LiveModuleDocument.tsx +24 -102
- package/src/kit/components/asset-viewers/LutAssetDocument.tsx +6 -6
- package/src/kit/components/asset-viewers/Object3DPreview.tsx +10 -45
- package/src/kit/components/asset-viewers/QuarksAssetDocument.tsx +2 -2
- package/src/kit/components/asset-viewers/ShaderAssetDocument.tsx +11 -10
- package/src/kit/components/asset-viewers/shader-source.ts +144 -0
- package/src/kit/components/asset-viewers/three-asset-viewers.tsx +2 -14
- package/src/kit/components/object3d-contribution-surfaces.tsx +1 -1
- package/src/kit/components/stage-keyboard.tsx +6 -12
- package/src/kit/components/stage-overlay-set.tsx +11 -20
- package/src/kit/components/stage-presence-markers.ts +9 -32
- package/src/kit/components/stage-transform-chrome.ts +4 -4
- package/src/kit/components/stage-transform-tools.tsx +7 -14
- package/src/kit/components/standard-viewport-dressing.ts +47 -167
- package/src/kit/components/world-root-binding.ts +2 -7
- package/src/kit/constraint-helper.ts +1 -1
- package/src/kit/document-renderer-session.ts +114 -0
- package/src/kit/editor-presentation-activity.ts +58 -0
- package/src/kit/editor-shell-store.ts +31 -403
- package/src/kit/editor-viewport.ts +476 -1762
- package/src/kit/entity-lod.ts +3 -16
- package/src/kit/entity-object.ts +1 -39
- package/src/kit/hierarchy-mark-reader.ts +13 -30
- package/src/kit/instanced-presentation.ts +1 -47
- package/src/kit/live-gesture-lock.ts +35 -0
- package/src/kit/live-module-source.ts +12 -52
- package/src/kit/model-thumbnail.ts +7 -35
- package/src/kit/play-camera-flight.ts +7 -16
- package/src/kit/projection/three.ts +13 -366
- package/src/kit/retained-document-states.ts +91 -0
- package/src/kit/scene-document-viewport.ts +3 -3
- package/src/kit/scene-framing.ts +11 -152
- package/src/kit/scene-view-fog.ts +2 -47
- package/src/kit/spatial-handle-visuals.ts +19 -28
- package/src/kit/stories/three-story-model.ts +5 -33
- package/src/kit/three-canvas-render.ts +3 -4
- package/src/kit/three-hierarchy-row-media.ts +3 -11
- package/src/kit/three-inspection-media.ts +6 -6
- package/src/kit/three-integration.ts +18 -18
- package/src/kit/three-state.ts +2 -19
- package/src/kit/three-viewport/camera-fit.ts +2 -6
- package/src/kit/three-viewport/selection-brackets.ts +5 -20
- package/src/kit/three-viewport/selection-outline.ts +4 -56
- package/src/kit/three-viewport/skeleton-helper.ts +1 -32
- package/src/kit/three-viewport/source-color.ts +1 -33
- package/src/kit/three-viewport/studio-environment.ts +4 -55
- package/src/kit/three-viewport-presentation.ts +23 -0
- package/src/kit/transport-strip.tsx +147 -0
- package/src/kit/viewport-actions.ts +8 -50
- package/src/kit/viewport-authoring-policy.ts +5 -52
- package/src/kit/viewport-commands.ts +28 -57
- package/src/kit/viewport-hotkeys.ts +29 -31
- package/src/kit/viewport-status-facet.ts +4 -5
- package/src/kit/viewport-surface-status.tsx +44 -0
- package/src/loader.ts +8 -71
- package/src/object3d-contributions.ts +18 -34
- package/src/viewport/clip-planes.ts +4 -32
- package/src/viewport/content-bounds.ts +13 -142
- package/src/viewport/editor-layers.ts +4 -26
- package/src/viewport/environment.ts +1 -1
- package/src/viewport/preview-renderer.ts +7 -45
- package/src/viewport/renderer-ownership.ts +14 -62
- package/src/viewport-api.ts +14 -33
- package/src/viewport-door.ts +16 -86
- package/contributions/three-integration.service.ts +0 -13
- package/src/adapter/hierarchy-marks.ts +0 -156
- package/src/adapter/ingest/scene-capture.ts +0 -843
- package/src/adapter/ingest/structural-ids.ts +0 -139
- package/src/adapter/ingest/visible-capture-window.ts +0 -302
- package/src/adapter/renderer-config.ts +0 -115
- package/src/asset-parse-error.ts +0 -29
- package/src/host-hierarchy-objects.ts +0 -31
- package/src/kit/authoring/design-time-settle.ts +0 -343
- package/src/kit/camera-authoring.ts +0 -175
|
@@ -1,51 +1,26 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
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/
|
|
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/
|
|
41
|
-
import { editorConsole } from '@volter/sdk/
|
|
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/
|
|
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
|
-
|
|
40
|
+
interface BackdropStops {
|
|
66
41
|
readonly bottom: THREE.Color;
|
|
67
42
|
readonly top: THREE.Color;
|
|
68
43
|
}
|
|
69
44
|
|
|
70
|
-
/**
|
|
71
|
-
|
|
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
|
-
//
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
212
|
-
//
|
|
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
|
-
|
|
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
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
readonly
|
|
264
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
359
|
-
|
|
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
|
-
*
|
|
377
|
-
*
|
|
378
|
-
*
|
|
379
|
-
*
|
|
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
|
-
*
|
|
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
|
|
576
|
-
*
|
|
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
|
-
//
|
|
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
|
|
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
|
-
*
|
|
777
|
-
*
|
|
778
|
-
*
|
|
779
|
-
*
|
|
780
|
-
*
|
|
781
|
-
* (
|
|
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
|
|
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/
|
|
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
|
|
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
|
+
}
|