@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,7 +1,7 @@
|
|
|
1
1
|
import { optionalThreeStateOf, threeStateOf } from '../three-state';
|
|
2
|
-
import { ShellStore } from '@volter/sdk/
|
|
3
|
-
import type { ToolObject3DAuthoringProps, ToolObject3DDocumentAuthoring, ToolObject3DPreviewSource
|
|
4
|
-
import { invalidateStages, stageGeneration } from '@volter/sdk/
|
|
2
|
+
import { ShellStore } from '@volter/sdk/shell-store';
|
|
3
|
+
import type { ToolObject3DAuthoringProps, ToolObject3DDocumentAuthoring, ToolObject3DPreviewSource } from '../../object3d-contributions';
|
|
4
|
+
import { invalidateStages, stageGeneration } from '@volter/sdk/stage-invalidation';
|
|
5
5
|
import type { StageTransportSnapshot } from '@volter/sdk/host';
|
|
6
6
|
import { EditorIcon, editorIcons, IconButton, themeVars } from '@volter/sdk/widgets';
|
|
7
7
|
import type { AuthoringAdapter } from '@volter/project/adapter';
|
|
@@ -25,9 +25,9 @@ import {
|
|
|
25
25
|
} from 'react';
|
|
26
26
|
import * as THREE from 'three';
|
|
27
27
|
import { DocumentScene } from '../../render/document-scene';
|
|
28
|
-
import { registerStageTransport, StageTransport } from '@volter/sdk/
|
|
28
|
+
import { registerStageTransport, StageTransport } from '@volter/sdk/animation/stage-transport';
|
|
29
29
|
import { scanClipSubjects } from '../animation/three-clips-subject';
|
|
30
|
-
import { liveGestureActive, whenLiveGestureIdle } from '
|
|
30
|
+
import { liveGestureActive, whenLiveGestureIdle } from '../live-gesture-lock';
|
|
31
31
|
import {
|
|
32
32
|
type Object3DDocumentPresentationState,
|
|
33
33
|
Object3DDocumentSession,
|
|
@@ -37,39 +37,36 @@ import {
|
|
|
37
37
|
} from '../authoring/object3d-document-session-registry';
|
|
38
38
|
import { Object3DGestureController } from '../authoring/object3d-gesture-controller';
|
|
39
39
|
import { SourceObject3DAuthoringAdapter } from '../authoring/source-object3d-authoring-adapter';
|
|
40
|
-
import { registerDesignTimeSurface } from '@volter/sdk/
|
|
41
|
-
import { DocumentRendererSession } from '
|
|
42
|
-
import { useOptionalEditorStats, useOptionalEditorStore } from '@volter/sdk/
|
|
40
|
+
import { registerDesignTimeSurface } from '@volter/sdk/coverage/design-time-surfaces';
|
|
41
|
+
import { DocumentRendererSession } from '../document-renderer-session';
|
|
42
|
+
import { useOptionalEditorStats, useOptionalEditorStore } from '@volter/sdk/editor-runtime';
|
|
43
43
|
import type { EditorShellStore } from '../editor-shell-store';
|
|
44
44
|
import { EditorViewport } from '../editor-viewport';
|
|
45
45
|
import {
|
|
46
46
|
lookDeclaresViewportColors,
|
|
47
47
|
nativeViewportLook,
|
|
48
48
|
subscribeNativeSelectionTheme,
|
|
49
|
-
} from '@volter/sdk/
|
|
50
|
-
import {
|
|
51
|
-
|
|
52
|
-
object3DDocumentWritePolicy,
|
|
53
|
-
} from '@volter/sdk/kit/object3d-document-write-policy';
|
|
54
|
-
import { registerPerformanceSource } from '@volter/sdk/kit/performance-sources';
|
|
49
|
+
} from '@volter/sdk/native-selection-style';
|
|
50
|
+
import type { AssetDocumentPersistenceSession } from '@volter/sdk/authoring/document-persistence-types';
|
|
51
|
+
import { assetDocumentWritePolicy } from '@volter/sdk/asset-document-write-policy';
|
|
55
52
|
import {
|
|
56
53
|
assetSubjectApplies,
|
|
57
54
|
documentStageContext,
|
|
58
55
|
focusedStageStore,
|
|
59
56
|
identityRowApplies,
|
|
60
57
|
studioStageApplies,
|
|
61
|
-
} from '@volter/sdk/
|
|
62
|
-
import { registerStageStore } from '@volter/sdk/
|
|
63
|
-
import { announceDocumentStage, registerDocumentViewport } from '@volter/sdk/
|
|
58
|
+
} from '@volter/sdk/stage-context';
|
|
59
|
+
import { registerStageStore } from '@volter/sdk/stage-store-registry';
|
|
60
|
+
import { announceDocumentStage, registerDocumentViewport } from '@volter/sdk/document-viewports';
|
|
64
61
|
import { sceneDocumentViewport } from '../scene-document-viewport';
|
|
65
62
|
import { threeStageTransformChrome } from './stage-transform-chrome';
|
|
66
|
-
import { RetainedDocumentStates } from '
|
|
63
|
+
import { RetainedDocumentStates } from '../retained-document-states';
|
|
67
64
|
import { perspectiveDistanceToFitBox } from '../three-viewport/camera-fit';
|
|
68
65
|
import {
|
|
69
66
|
acquireInteractiveViewportRenderer,
|
|
70
67
|
type InteractiveViewportRendererLease,
|
|
71
68
|
} from '../three-viewport/interactive-renderer';
|
|
72
|
-
import type { ThreeViewportProjection } from '
|
|
69
|
+
import type { ThreeViewportProjection } from '../three-viewport-presentation';
|
|
73
70
|
import {
|
|
74
71
|
activeViewportBreakdownDocumentId,
|
|
75
72
|
markViewportConstructReady,
|
|
@@ -77,9 +74,9 @@ import {
|
|
|
77
74
|
markViewportFirstRenderStart,
|
|
78
75
|
markViewportRafResume,
|
|
79
76
|
markViewportReactActive,
|
|
80
|
-
|
|
77
|
+
markViewportFirstRender,
|
|
81
78
|
recordViewportFirstFrame,
|
|
82
|
-
} from '@volter/sdk/
|
|
79
|
+
} from '@volter/sdk/viewport-activation-timings';
|
|
83
80
|
import { bindViewportRig, runViewportFrame } from '../../viewport-door';
|
|
84
81
|
import type { StageFrameCost } from '../../viewport-api';
|
|
85
82
|
import {
|
|
@@ -87,11 +84,11 @@ import {
|
|
|
87
84
|
openWorkspaceDocuments,
|
|
88
85
|
registerWorkspaceDocumentSelection,
|
|
89
86
|
subscribeWorkspaceDocuments,
|
|
90
|
-
} from '@volter/sdk/
|
|
91
|
-
import { AssetEditorShell } from '@volter/sdk/
|
|
87
|
+
} from '@volter/sdk/workspace-document-registry';
|
|
88
|
+
import { AssetEditorShell } from '@volter/sdk/components/AssetEditorShell';
|
|
92
89
|
import { StageOverlays } from './StageOverlays';
|
|
93
90
|
import type { WorldRootStageBinding } from './world-root-binding';
|
|
94
|
-
import { TransportStrip } from '
|
|
91
|
+
import { TransportStrip } from '../transport-strip';
|
|
95
92
|
|
|
96
93
|
// The stage's own keyboard actions, behind the same lazy boundary as the
|
|
97
94
|
// overlays and for the same reason — a bounded host pays for neither
|
|
@@ -108,8 +105,8 @@ import {
|
|
|
108
105
|
watchPaletteBackdrop,
|
|
109
106
|
} from './standard-viewport-dressing';
|
|
110
107
|
import { ViewportFurniture } from './ViewportFurniture';
|
|
111
|
-
import { OBJECT3D_SURFACE_BUILDING, ViewportSurfaceStatus } from '
|
|
112
|
-
import { workspaceHistoryService } from '@volter/sdk/
|
|
108
|
+
import { OBJECT3D_SURFACE_BUILDING, ViewportSurfaceStatus } from '../viewport-surface-status';
|
|
109
|
+
import { workspaceHistoryService } from '@volter/sdk/components/workspace-history';
|
|
113
110
|
import {
|
|
114
111
|
bindViewPresentation,
|
|
115
112
|
DOCUMENT_STUDIO_PRESET,
|
|
@@ -123,11 +120,12 @@ import {
|
|
|
123
120
|
viewPresentation,
|
|
124
121
|
viewPresentationSnapshot,
|
|
125
122
|
type ViewportDrawMode,
|
|
126
|
-
} from '@volter/sdk/
|
|
127
|
-
import { subscribeEnvironmentImages } from '@volter/sdk/
|
|
123
|
+
} from '@volter/sdk/viewport-presentation';
|
|
124
|
+
import { subscribeEnvironmentImages } from '@volter/sdk/environment-images';
|
|
128
125
|
import { StagePresentationRig } from './standard-viewport-dressing';
|
|
129
126
|
import { threeStoreForHost } from '../three-state';
|
|
130
127
|
import { threeObject } from '../../adapter/three-contract';
|
|
128
|
+
import './StageHost.css';
|
|
131
129
|
|
|
132
130
|
/** The kind of stage a document's view is, for its starting presentation: the document's own
|
|
133
131
|
* kind, its id's prefix inside the workspace's `document:` wrapper
|
|
@@ -138,13 +136,12 @@ const stageKindOf = (documentId: string): string => {
|
|
|
138
136
|
};
|
|
139
137
|
|
|
140
138
|
/** WHERE THIS DOCUMENT'S BYTES GO — the one collaborator the shell installs
|
|
141
|
-
* (`
|
|
139
|
+
* (`asset-document-write-policy.ts`). The tier's source recorder, the
|
|
142
140
|
* project file history and the thumbnail manifest all live behind it, so the
|
|
143
|
-
* Asset Lab 3D document carries no editor transport in its own closure
|
|
144
|
-
* (ARCHITECTURE-CORE §Editor chrome, "the viewport stack is separable").
|
|
141
|
+
* Asset Lab 3D document carries no editor transport in its own closure.
|
|
145
142
|
* With no shell above it every member refuses by name, which is the same
|
|
146
143
|
* refusal a read-only tier already produced. */
|
|
147
|
-
const projectWrites = () =>
|
|
144
|
+
const projectWrites = () => assetDocumentWritePolicy();
|
|
148
145
|
|
|
149
146
|
/**
|
|
150
147
|
* How far a BARE dock-panel stage bleeds past the box a person sees, so the
|
|
@@ -157,34 +154,6 @@ const projectWrites = () => object3DDocumentWritePolicy();
|
|
|
157
154
|
*/
|
|
158
155
|
const STAGE_BLEED_PX = 12;
|
|
159
156
|
|
|
160
|
-
function boxFromFrameBounds(
|
|
161
|
-
bounds:
|
|
162
|
-
| {
|
|
163
|
-
readonly min: readonly [number, number, number];
|
|
164
|
-
readonly max: readonly [number, number, number];
|
|
165
|
-
}
|
|
166
|
-
| undefined,
|
|
167
|
-
): THREE.Box3 | null {
|
|
168
|
-
if (!bounds) return null;
|
|
169
|
-
const box = new THREE.Box3(
|
|
170
|
-
new THREE.Vector3(bounds.min[0], bounds.min[1], bounds.min[2]),
|
|
171
|
-
new THREE.Vector3(bounds.max[0], bounds.max[1], bounds.max[2]),
|
|
172
|
-
);
|
|
173
|
-
return box.isEmpty() ? null : box;
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
/**
|
|
177
|
-
* Per-document overrides of the standard viewport dressing
|
|
178
|
-
* (`standard-viewport-dressing.ts`) — explicit opt-outs/opt-ins, never
|
|
179
|
-
* re-implementations. Omitted means the standard look.
|
|
180
|
-
*
|
|
181
|
-
* THE SHAPE IS THE SDK'S (`ToolViewportDressing`), because a contributed
|
|
182
|
-
* document is what asks for it: `@volter/editor-blender`'s Model document hands over
|
|
183
|
-
* its view-locked studio through this door. One declaration, so the public
|
|
184
|
-
* surface and the host cannot drift.
|
|
185
|
-
*/
|
|
186
|
-
export type StandardDressingProps = ToolViewportDressing;
|
|
187
|
-
|
|
188
157
|
/** What an editor-side source lane gets to build its authoring adapter over
|
|
189
158
|
* the document's own store and scene. */
|
|
190
159
|
export interface SourceDocumentAuthoringContext {
|
|
@@ -246,63 +215,23 @@ export interface Object3DDocumentViewportProps
|
|
|
246
215
|
* SOURCE (a mounted R3F composition's stamped JSX, today). The caller that
|
|
247
216
|
* knows the source lane supplies the adapter; this document constructs
|
|
248
217
|
* none of them, so it stays mountable without any source lane in its
|
|
249
|
-
* closure
|
|
250
|
-
* separable"). Raw model/artifact documents omit this and remain honestly
|
|
218
|
+
* closure. Raw model/artifact documents omit this and remain honestly
|
|
251
219
|
* read-only through the native source adapter. Consulted only when the
|
|
252
220
|
* project `authoring` factory declined. MUST be referentially stable for
|
|
253
221
|
* the document's lifetime, like `build`: it is an activation dependency.
|
|
254
222
|
*/
|
|
255
223
|
readonly sourceAuthoring?: SourceDocumentAuthoringFactory;
|
|
256
|
-
/**
|
|
257
|
-
*
|
|
258
|
-
*
|
|
259
|
-
* `own` (the default) constructs one for this mount — the right answer for a
|
|
260
|
-
* document that lives as long as its panel does. `inspector-preview` draws
|
|
261
|
-
* through the shared inspector-preview renderer
|
|
262
|
-
* (`inspector-preview-renderer.ts`): that lane REMOUNTS PER SELECTION, and a
|
|
263
|
-
* renderer construction there is ~1.9 s of frozen main thread plus a cold
|
|
264
|
-
* shader cache and a fresh PMREM bake, every time you click something.
|
|
265
|
-
*/
|
|
224
|
+
/** Use the shared inspector-preview renderer for mounts replaced per selection; other
|
|
225
|
+
* document mounts own their renderer and shader cache for their lifetime. */
|
|
266
226
|
readonly rendererLane?: 'own' | 'inspector-preview';
|
|
267
|
-
/**
|
|
268
|
-
* Double-click on a picked node — the document's "open THIS" gesture,
|
|
269
|
-
* reported with the node's own `Object3D` so the owner can resolve whatever
|
|
270
|
-
* it means by it. Single click stays plain selection; a document that
|
|
271
|
-
* declares no handler has no open gesture at all.
|
|
272
|
-
*
|
|
273
|
-
* Sole caller: the `3D` board, where an exhibit opens that
|
|
274
|
-
* story's own turntable document (`three-board/ThreeBoardDocument.tsx`).
|
|
275
|
-
*/
|
|
276
|
-
readonly onOpenNode?: (object: THREE.Object3D) => void;
|
|
277
|
-
/**
|
|
278
|
-
* When set, Frame with no selection and the view presets fit THIS box
|
|
279
|
-
* instead of `contentWorldBounds(root)`. The 3D board passes the union of
|
|
280
|
-
* each exhibit's presence as its true-scale overview.
|
|
281
|
-
*/
|
|
282
|
-
readonly frameBounds?: {
|
|
283
|
-
readonly min: readonly [number, number, number];
|
|
284
|
-
readonly max: readonly [number, number, number];
|
|
285
|
-
};
|
|
286
|
-
/** Optional first-paint frame distinct from the document's Frame fallback.
|
|
287
|
-
* The 3D component board opens on one readable exhibit, while clearing the
|
|
288
|
-
* selection and pressing Frame still restores its true-scale overview. */
|
|
289
|
-
readonly openingFrameBounds?: {
|
|
290
|
-
readonly min: readonly [number, number, number];
|
|
291
|
-
readonly max: readonly [number, number, number];
|
|
292
|
-
};
|
|
293
227
|
/** Multiplies the opening fit distance; `1` fills the view. */
|
|
294
228
|
readonly openingFit?: number;
|
|
295
229
|
/** The kind of stage this view is, for its starting presentation
|
|
296
|
-
* (`@volter/sdk/
|
|
230
|
+
* (`@volter/sdk/viewport-presentation`): the document's own kind (`'model'`).
|
|
297
231
|
* Without it the kind is read off the document id's prefix. */
|
|
298
232
|
readonly stageKind?: string;
|
|
299
233
|
}
|
|
300
234
|
|
|
301
|
-
/**
|
|
302
|
-
* Distinguishes the chromeless mounts of ONE document from each other and from
|
|
303
|
-
* that document's viewport, so each publishes its own design-time surface
|
|
304
|
-
* instead of overwriting a sibling's (see the registration below).
|
|
305
|
-
*/
|
|
306
235
|
/** The draw modes a view's presentation carries (`ViewportDrawMode`). */
|
|
307
236
|
const VIEW_DRAW_MODES: readonly ViewportDrawMode[] = [
|
|
308
237
|
'solid',
|
|
@@ -316,6 +245,11 @@ const VIEW_DRAW_MODES: readonly ViewportDrawMode[] = [
|
|
|
316
245
|
'overdraw',
|
|
317
246
|
];
|
|
318
247
|
|
|
248
|
+
/**
|
|
249
|
+
* Distinguishes the chromeless mounts of ONE document from each other and from
|
|
250
|
+
* that document's viewport, so each publishes its own design-time surface
|
|
251
|
+
* instead of overwriting a sibling's (see the registration below).
|
|
252
|
+
*/
|
|
319
253
|
let chromelessSurfaceSequence = 0;
|
|
320
254
|
|
|
321
255
|
interface RetainedObject3DStageState {
|
|
@@ -375,11 +309,11 @@ function mountViewportSurface(
|
|
|
375
309
|
lane: 'own' | 'inspector-preview',
|
|
376
310
|
displayName: string,
|
|
377
311
|
initialSize: { readonly width: number; readonly height: number },
|
|
378
|
-
onContextLost
|
|
312
|
+
onContextLost: () => void,
|
|
379
313
|
): {
|
|
380
314
|
canvas: HTMLCanvasElement;
|
|
381
315
|
renderer: THREE.WebGLRenderer;
|
|
382
|
-
lease: InspectorPreviewLease | InteractiveViewportRendererLease
|
|
316
|
+
lease: InspectorPreviewLease | InteractiveViewportRendererLease;
|
|
383
317
|
} {
|
|
384
318
|
if (lane === 'inspector-preview') {
|
|
385
319
|
const lease = acquireInspectorPreviewRenderer();
|
|
@@ -406,7 +340,6 @@ function mountViewportSurface(
|
|
|
406
340
|
* Source lifetimes borrow it; closing waits for any asynchronous gesture
|
|
407
341
|
* cleanup to return its borrow before releasing the context. */
|
|
408
342
|
|
|
409
|
-
|
|
410
343
|
interface DocumentContentBinding {
|
|
411
344
|
readonly scene: THREE.Scene;
|
|
412
345
|
settle(): Promise<void>;
|
|
@@ -416,7 +349,7 @@ interface DocumentContentBinding {
|
|
|
416
349
|
/** Persistent editor state. Source revisions own only their content binding. */
|
|
417
350
|
class Object3DDocumentHost {
|
|
418
351
|
readonly scene = new DocumentScene();
|
|
419
|
-
/** This document's store,
|
|
352
|
+
/** This document's store, supplied by its owner. */
|
|
420
353
|
readonly store: EditorShellStore;
|
|
421
354
|
readonly cleanups: Array<() => void> = [];
|
|
422
355
|
readonly rendererSession = new DocumentRendererSession(
|
|
@@ -427,7 +360,7 @@ class Object3DDocumentHost {
|
|
|
427
360
|
session: Object3DDocumentSession | null = null;
|
|
428
361
|
dressing: StandardViewportDressing | null = null;
|
|
429
362
|
/** The stage's lighting, tone and exposure, from its view's presentation
|
|
430
|
-
* (`standard-viewport-dressing.ts`,
|
|
363
|
+
* (`standard-viewport-dressing.ts`, `@volter/sdk/viewport-presentation`). */
|
|
431
364
|
presentationRig: StagePresentationRig | null = null;
|
|
432
365
|
/** Re-apply the view's presentation (after the viewport and session exist). */
|
|
433
366
|
applyPresentation: (() => void) | null = null;
|
|
@@ -487,18 +420,15 @@ class Object3DDocumentHost {
|
|
|
487
420
|
return this.closed;
|
|
488
421
|
}
|
|
489
422
|
|
|
490
|
-
/** THIS STAGE's transport — the one holder of the content-time door below.
|
|
491
|
-
|
|
492
|
-
* `playState` and nothing else (`animation/stage-transport.ts`). */
|
|
493
|
-
readonly transport: StageTransport;
|
|
423
|
+
/** THIS STAGE's transport — the one holder of the content-time door below. */
|
|
424
|
+
readonly transport = new StageTransport();
|
|
494
425
|
|
|
495
426
|
constructor(
|
|
496
427
|
readonly lane: 'own' | 'inspector-preview',
|
|
497
|
-
readonly surface: ReturnType<typeof mountViewportSurface>,
|
|
498
|
-
store
|
|
428
|
+
readonly surface: ReturnType<typeof mountViewportSurface> | ReturnType<WorldRootStageBinding['mountWorldRootSurface']>,
|
|
429
|
+
store: EditorShellStore,
|
|
499
430
|
) {
|
|
500
|
-
this.store = store
|
|
501
|
-
this.transport = new StageTransport(this.store.shell);
|
|
431
|
+
this.store = store;
|
|
502
432
|
}
|
|
503
433
|
|
|
504
434
|
borrow(): () => void {
|
|
@@ -529,7 +459,7 @@ class Object3DDocumentHost {
|
|
|
529
459
|
private releaseIfClosed(): void {
|
|
530
460
|
if (!this.closed || this.users !== 0 || this.released) return;
|
|
531
461
|
this.released = true;
|
|
532
|
-
const { lease,
|
|
462
|
+
const { lease, canvas } = this.surface;
|
|
533
463
|
for (const cleanup of [
|
|
534
464
|
...this.cleanups.splice(0),
|
|
535
465
|
() => this.session?.dispose(),
|
|
@@ -537,9 +467,7 @@ class Object3DDocumentHost {
|
|
|
537
467
|
() => this.rendererSession.dispose(),
|
|
538
468
|
() => this.dressing?.dispose(),
|
|
539
469
|
() => canvas.removeAttribute('data-volter-root-surface'),
|
|
540
|
-
|
|
541
|
-
? [() => lease.release()]
|
|
542
|
-
: [() => renderer.dispose(), () => renderer.forceContextLoss(), () => canvas.remove()]),
|
|
470
|
+
() => lease?.release(),
|
|
543
471
|
]) {
|
|
544
472
|
try {
|
|
545
473
|
cleanup();
|
|
@@ -551,33 +479,7 @@ class Object3DDocumentHost {
|
|
|
551
479
|
}
|
|
552
480
|
}
|
|
553
481
|
|
|
554
|
-
/**
|
|
555
|
-
* Editor-owned authoring host for a project-supplied native Object3D factory.
|
|
556
|
-
* Project code supplies only source construction; this component owns the
|
|
557
|
-
* real EditorViewport, document-scoped AuthoringAdapter, selection, framing,
|
|
558
|
-
* animation preview, resize, and teardown.
|
|
559
|
-
*
|
|
560
|
-
* ## TIME ON THIS SURFACE — the one policy, inherited by everything mounted here
|
|
561
|
-
*
|
|
562
|
-
* This host is the shared stack: Asset Lab documents, story turntables, 3D board
|
|
563
|
-
* exhibits and the inspector's preview card all render through it. So the
|
|
564
|
-
* Edit-static rule is enforced HERE, once, rather than at each of them:
|
|
565
|
-
*
|
|
566
|
-
* - PRESENTATION time always advances — orbit damping, the view cube, the
|
|
567
|
-
* standard dressing, the editor's own helpers and overlays. It is
|
|
568
|
-
* editor-owned and it moves while the editor is in Edit.
|
|
569
|
-
* - CONTENT time does NOT. The source's own `update(dt)` and any clip a
|
|
570
|
-
* subject shows move only through the stage TRANSPORT, and only while a
|
|
571
|
-
* human is holding it (`animation/stage-transport.ts`, WORK.md §The stage
|
|
572
|
-
* transport and the animation door). A mounted
|
|
573
|
-
* document plays nothing on its own: that is the defect this closes (a GLB
|
|
574
|
-
* train animating in the Asset Lab while every instrument called the editor
|
|
575
|
-
* still, owner find 2026-08-15).
|
|
576
|
-
*
|
|
577
|
-
* Whatever content time this surface has reached is published as its content
|
|
578
|
-
* clock (`coverage/design-time-surfaces.ts`), so the Edit-static vitals row
|
|
579
|
-
* measures THIS surface by name instead of speaking only for the Scene view.
|
|
580
|
-
*/
|
|
482
|
+
/** Shared Object3D authoring host: owns viewport, adapter, selection, framing, resize and disposal. Presentation time advances independently; source update and clips advance only through the stage transport. Publish content time for this surface. */
|
|
581
483
|
export function Object3DDocumentViewport({
|
|
582
484
|
documentId,
|
|
583
485
|
sourcePath,
|
|
@@ -601,9 +503,6 @@ export function Object3DDocumentViewport({
|
|
|
601
503
|
sourceAuthoring,
|
|
602
504
|
dressing,
|
|
603
505
|
rendererLane = 'own',
|
|
604
|
-
onOpenNode,
|
|
605
|
-
frameBounds,
|
|
606
|
-
openingFrameBounds,
|
|
607
506
|
openingFit,
|
|
608
507
|
openingView,
|
|
609
508
|
presentation: documentPresentation,
|
|
@@ -624,40 +523,12 @@ export function Object3DDocumentViewport({
|
|
|
624
523
|
if (chromeless) return;
|
|
625
524
|
return announceDocumentStage(documentId);
|
|
626
525
|
}, [documentId, chromeless]);
|
|
627
|
-
//
|
|
628
|
-
// about the workspace's document, and `stage-context.ts` is where every
|
|
629
|
-
// stage capability asks it (ARCHITECTURE-CORE §One stage). OPTIONAL because
|
|
630
|
-
// this host is a bounded-host surface — `@volter/editor-blender`'s Model document
|
|
631
|
-
// mounts it with no `EditorProvider` above it, and the throwing hook took
|
|
632
|
-
// that document off the screen with "useEditorStore must be used within
|
|
633
|
-
// <EditorProvider>" (measured live, 2026-09-18). No shell, no shell frame.
|
|
526
|
+
// Shell React context is optional for a bounded stage. Shell overlays mount only when that provider is present.
|
|
634
527
|
const shellStore = optionalThreeStateOf(useOptionalEditorStore());
|
|
635
|
-
/**
|
|
636
|
-
* THE STORE THE STAGE CONTEXT IS ASKED AGAINST — the session's own, reached
|
|
637
|
-
* without a React provider.
|
|
638
|
-
*
|
|
639
|
-
* `optionalThreeStateOf(useOptionalEditorStore())` above answers a NARROWER question than the
|
|
640
|
-
* context needs: it is whether the shell's own React tree is above this
|
|
641
|
-
* host, and for every package-contributed document it is not.
|
|
642
|
-
* `@volter/editor-blender`'s Model document mounts this host through `ToolHost`, so the
|
|
643
|
-
* context store is null there — and reading only it left `stageCtx` null,
|
|
644
|
-
* which made EVERY condition in `stage-context.ts` unanswerable for that
|
|
645
|
-
* document. That is a capability decided by where a document's React tree
|
|
646
|
-
* happens to mount, which is the origin-deciding reading §One stage
|
|
647
|
-
* retired. Without the context, the studio arm of `studioStageApplies`
|
|
648
|
-
* could never fire for a model no matter what the look declared.
|
|
649
|
-
*
|
|
650
|
-
* `shellStoreForHost()` supplies the session store that
|
|
651
|
-
* `focusedStageStore()` falls back to. It is deliberately NOT used for the
|
|
652
|
-
* overlay set or the shell readout below: those are the SHELL's estate and
|
|
653
|
-
* their condition is the shell's React presence, exactly as §One stage
|
|
654
|
-
* unit 3 measured it — a stage that is not inside the shell has no shell
|
|
655
|
-
* overlays. That gap is real and named, not closed here: unit 1's bar is
|
|
656
|
-
* that the Model document's Blender frame does not move.
|
|
657
|
-
*/
|
|
528
|
+
/** Use the host’s session store for stage conditions even when ToolHost mounts outside the shell’s React tree. Shell overlays still require the shell provider. */
|
|
658
529
|
const contextStore = shellStore ?? threeStoreForHost();
|
|
659
530
|
/**
|
|
660
|
-
* WHAT THIS STAGE IS SHOWING, asked once
|
|
531
|
+
* WHAT THIS STAGE IS SHOWING, asked once.
|
|
661
532
|
* `chrome` is the host's own answer and nothing infers it: an EMBEDDED
|
|
662
533
|
* preview has no workspace document at all, which is exactly why the
|
|
663
534
|
* subject would come back `unknown` for one.
|
|
@@ -668,10 +539,8 @@ export function Object3DDocumentViewport({
|
|
|
668
539
|
/**
|
|
669
540
|
* THE STUDIO PRESENTATION — the alpha clear, the suppressed palette
|
|
670
541
|
* backdrop, the identity-row frame and the asset flavour of this stage's
|
|
671
|
-
* provenance. It
|
|
672
|
-
*
|
|
673
|
-
* mount that passed `documentKind="asset"` is a story, an artifact or an
|
|
674
|
-
* embedded preview, and each still answers true.
|
|
542
|
+
* provenance. It follows the stage context and look; stories, artifacts
|
|
543
|
+
* and embedded previews each use studio presentation.
|
|
675
544
|
*/
|
|
676
545
|
// The LOOK is one of `studioStageApplies`'s live inputs (a data subject takes
|
|
677
546
|
// the studio only where the look leaves the backdrop to the editor), and the
|
|
@@ -695,11 +564,8 @@ export function Object3DDocumentViewport({
|
|
|
695
564
|
const hasShell = stageCtx !== null && identityRowApplies(stageCtx);
|
|
696
565
|
const hasShellRef = useRef(hasShell);
|
|
697
566
|
hasShellRef.current = hasShell;
|
|
698
|
-
//
|
|
699
|
-
//
|
|
700
|
-
// source's kind. It is the studio presentation MINUS the data arm — a mesh
|
|
701
|
-
// module opens in an isolation scene but is not a derived asset — and was
|
|
702
|
-
// read off `studioStage` while the two were the same answer.
|
|
567
|
+
// Use the asset-stage flavor for the source adapter’s document kind and performance source.
|
|
568
|
+
// Studio presentation also applies to source modules, which are not derived assets.
|
|
703
569
|
const assetSubject = stageCtx !== null && assetSubjectApplies(stageCtx);
|
|
704
570
|
const assetSubjectRef = useRef(assetSubject);
|
|
705
571
|
assetSubjectRef.current = assetSubject;
|
|
@@ -775,15 +641,6 @@ export function Object3DDocumentViewport({
|
|
|
775
641
|
documentHostRef.current = null;
|
|
776
642
|
};
|
|
777
643
|
}, [documentId, rendererLane, retainedState, surfaceAttached, worldRoot]);
|
|
778
|
-
// Read through a ref: the one big lifetime effect below owns the canvas
|
|
779
|
-
// listener, and re-running it on a new handler identity would tear the whole
|
|
780
|
-
// renderer down and rebuild it.
|
|
781
|
-
const onOpenNodeRef = useRef(onOpenNode);
|
|
782
|
-
onOpenNodeRef.current = onOpenNode;
|
|
783
|
-
const frameBoundsRef = useRef(frameBounds);
|
|
784
|
-
frameBoundsRef.current = frameBounds;
|
|
785
|
-
const openingFrameBoundsRef = useRef(openingFrameBounds);
|
|
786
|
-
openingFrameBoundsRef.current = openingFrameBounds;
|
|
787
644
|
const openingViewRef = useRef(openingView);
|
|
788
645
|
openingViewRef.current = openingView;
|
|
789
646
|
const documentPresentationRef = useRef(documentPresentation);
|
|
@@ -817,14 +674,13 @@ export function Object3DDocumentViewport({
|
|
|
817
674
|
root: THREE.Object3D;
|
|
818
675
|
animations: readonly THREE.AnimationClip[];
|
|
819
676
|
} | null>(null);
|
|
820
|
-
const persistenceSessionRef = useRef<Promise<
|
|
677
|
+
const persistenceSessionRef = useRef<Promise<AssetDocumentPersistenceSession> | null>(null);
|
|
821
678
|
const [error, setError] = useState<string | null>(null);
|
|
822
679
|
const [surfaceStatus, setSurfaceStatus] = useState<'building' | 'ready'>('building');
|
|
823
680
|
// A world root's own mount state and the binding that draws its overlays.
|
|
824
681
|
const [worldRootBinding, setWorldRootBinding] = useState<WorldRootStageBinding | null>(null);
|
|
825
682
|
const [designMountStatus, setDesignMountStatus] = useState<'mounting' | 'ready'>('mounting');
|
|
826
683
|
const [designRootIds, setDesignRootIds] = useState<readonly string[]>([]);
|
|
827
|
-
const cameraPreviewRef = useRef<HTMLDivElement | null>(null);
|
|
828
684
|
const [projection, setProjection] = useState<ThreeViewportProjection>('perspective');
|
|
829
685
|
// The live authoring adapter this document's inspection composes from. It
|
|
830
686
|
// only exists once the graph is built, so it is state rather than a ref:
|
|
@@ -864,7 +720,8 @@ export function Object3DDocumentViewport({
|
|
|
864
720
|
const canvasHost = canvasHostRef.current;
|
|
865
721
|
if (!container || !canvasHost || documentHostRef.current) return;
|
|
866
722
|
const stats = shellStatsRef.current;
|
|
867
|
-
|
|
723
|
+
const store = threeStoreForHost();
|
|
724
|
+
if (!stats || !store) {
|
|
868
725
|
// The world root IS the shell's own subject — its readout, its
|
|
869
726
|
// adoption stack, its transport. A bounded host has none of that.
|
|
870
727
|
setError('The world root needs the editor shell above it; this host has none.');
|
|
@@ -880,7 +737,7 @@ export function Object3DDocumentViewport({
|
|
|
880
737
|
// The world is the session's own subject: Play adoption, ingest, the
|
|
881
738
|
// project history and the host door's hierarchy facet all address the
|
|
882
739
|
// SESSION store, so this stage runs on it rather than a private one.
|
|
883
|
-
|
|
740
|
+
store,
|
|
884
741
|
);
|
|
885
742
|
documentHostRef.current = host;
|
|
886
743
|
host.cleanups.push(registerStageStore(documentId, host.store.shell));
|
|
@@ -898,7 +755,6 @@ export function Object3DDocumentViewport({
|
|
|
898
755
|
canvas: host.surface.canvas,
|
|
899
756
|
renderer: host.surface.renderer,
|
|
900
757
|
stats,
|
|
901
|
-
cameraPreview: () => cameraPreviewRef.current,
|
|
902
758
|
onMountStatus: setDesignMountStatus,
|
|
903
759
|
onRootIds: setDesignRootIds,
|
|
904
760
|
runFrame: (delta) => runViewportFrame(documentId, delta),
|
|
@@ -910,21 +766,16 @@ export function Object3DDocumentViewport({
|
|
|
910
766
|
settle: () => Promise.resolve(),
|
|
911
767
|
dispose: async () => stage.dispose(),
|
|
912
768
|
};
|
|
913
|
-
//
|
|
914
|
-
//
|
|
915
|
-
// throttled, and not while Play drives time.
|
|
916
|
-
// The scene the stage DRAWS: an R3F world renders the session store's
|
|
917
|
-
// scene, not the stage's own (`world-root-stage.ts`), and scanning the
|
|
918
|
-
// latter found no subject on a world whose enemies each had a mixer.
|
|
769
|
+
// Rescan clip subjects on throttled store changes as the world tree mounts and updates.
|
|
770
|
+
// Inspect the scene the stage draws, including a session store’s scene.
|
|
919
771
|
const clipScan = scanClipSubjects(() => host.store.scene ?? stage.scene, host.transport, {
|
|
920
772
|
// A game makes its mixers as its world runs, not when the store changes.
|
|
921
|
-
rescanOnLiveMixers:
|
|
773
|
+
rescanOnLiveMixers: true,
|
|
922
774
|
});
|
|
923
775
|
let lastClipScan = 0;
|
|
924
776
|
host.cleanups.push(() => clipScan.dispose());
|
|
925
777
|
host.cleanups.push(
|
|
926
778
|
host.store.shell.subscribe(() => {
|
|
927
|
-
if (host.store.shell.playState !== 'stopped') return;
|
|
928
779
|
const now = performance.now();
|
|
929
780
|
if (now - lastClipScan < CLIP_RESCAN_INTERVAL_MS) return;
|
|
930
781
|
lastClipScan = now;
|
|
@@ -939,9 +790,10 @@ export function Object3DDocumentViewport({
|
|
|
939
790
|
camera: stage.viewport.camera,
|
|
940
791
|
drawCamera: () => stage.viewport.renderCamera,
|
|
941
792
|
orbit: stage.viewport.orbitControls,
|
|
793
|
+
projection: stage.viewport.projectionOwner,
|
|
942
794
|
scene: stage.scene,
|
|
795
|
+
renderer: host.surface.renderer,
|
|
943
796
|
},
|
|
944
|
-
stage.present,
|
|
945
797
|
stage.setHelper,
|
|
946
798
|
{ documentId },
|
|
947
799
|
),
|
|
@@ -1015,7 +867,7 @@ export function Object3DDocumentViewport({
|
|
|
1015
867
|
const { renderer, canvas, lease } = host.surface;
|
|
1016
868
|
const store = host.store;
|
|
1017
869
|
// This stage's store, reachable by the shared panels through
|
|
1018
|
-
// `focusedStageStore()
|
|
870
|
+
// `focusedStageStore()`. A
|
|
1019
871
|
// CHROMELESS preview is not a stage anything can focus — it has no
|
|
1020
872
|
// workspace document — so it registers nothing.
|
|
1021
873
|
if (!chromeless) {
|
|
@@ -1038,7 +890,7 @@ export function Object3DDocumentViewport({
|
|
|
1038
890
|
const documentState = { root: source.root, animations: source.root.animations };
|
|
1039
891
|
let defaultAdapter: SourceObject3DAuthoringAdapter | null = null;
|
|
1040
892
|
let projectAuthoring: ToolObject3DDocumentAuthoring | null = null;
|
|
1041
|
-
let activePersistenceSession: Promise<
|
|
893
|
+
let activePersistenceSession: Promise<AssetDocumentPersistenceSession> | null = null;
|
|
1042
894
|
let interactionExtension: ReturnType<NonNullable<typeof interaction>['setup']> | null = null;
|
|
1043
895
|
let gestureController: Object3DGestureController | null = null;
|
|
1044
896
|
let pointerDownListener: ((event: PointerEvent) => void) | null = null;
|
|
@@ -1109,8 +961,6 @@ export function Object3DDocumentViewport({
|
|
|
1109
961
|
// animation binding writes through — checksum-guarded whole-file
|
|
1110
962
|
// replace, recorded in canonical history — never the resource
|
|
1111
963
|
// route, which refuses executable source by rule.
|
|
1112
|
-
if (!document)
|
|
1113
|
-
throw new Error('Project history is unavailable; the edit was not saved.');
|
|
1114
964
|
const history = workspaceHistoryService();
|
|
1115
965
|
if (!history) {
|
|
1116
966
|
throw new Error('Project history is unavailable; the edit was not saved.');
|
|
@@ -1133,7 +983,7 @@ export function Object3DDocumentViewport({
|
|
|
1133
983
|
if (!persistence) {
|
|
1134
984
|
throw new Error('This Asset Lab document has no persistence binding.');
|
|
1135
985
|
}
|
|
1136
|
-
if (!activePersistenceSession
|
|
986
|
+
if (!activePersistenceSession) {
|
|
1137
987
|
throw new Error('Project history is unavailable; the edit was not saved.');
|
|
1138
988
|
}
|
|
1139
989
|
// The pipe's ack for THIS commit: `persisted` is whether a byte moved
|
|
@@ -1181,11 +1031,8 @@ export function Object3DDocumentViewport({
|
|
|
1181
1031
|
});
|
|
1182
1032
|
defaultAdapter.dispose();
|
|
1183
1033
|
}
|
|
1184
|
-
//
|
|
1185
|
-
//
|
|
1186
|
-
// persistence baseline only after that factory has adopted the root;
|
|
1187
|
-
// opening it before authoring made every clean rig mount reject with
|
|
1188
|
-
// "Object3D is not a project rig artifact."
|
|
1034
|
+
// Open the persistence baseline after the authoring factory adopts the root and
|
|
1035
|
+
// establishes any module-local artifact its serializers read.
|
|
1189
1036
|
activePersistenceSession =
|
|
1190
1037
|
persistence && history
|
|
1191
1038
|
? projectWrites().openPersistence({
|
|
@@ -1214,7 +1061,8 @@ export function Object3DDocumentViewport({
|
|
|
1214
1061
|
// operator into every `toneMapped` program.
|
|
1215
1062
|
renderer.toneMapping = dressingToneMapping ?? THREE.ACESFilmicToneMapping;
|
|
1216
1063
|
renderer.shadowMap.enabled = true;
|
|
1217
|
-
|
|
1064
|
+
// PCFShadowMap supplies the soft-shadow filter supported by this three.js release.
|
|
1065
|
+
renderer.shadowMap.type = THREE.PCFShadowMap;
|
|
1218
1066
|
renderer.info.autoReset = false;
|
|
1219
1067
|
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
|
|
1220
1068
|
if (studioStage) renderer.setClearColor(0x000000, 0);
|
|
@@ -1235,8 +1083,6 @@ export function Object3DDocumentViewport({
|
|
|
1235
1083
|
!studioStage &&
|
|
1236
1084
|
background === undefined &&
|
|
1237
1085
|
dressingBackground !== false,
|
|
1238
|
-
// The stage's lights are the presentation rig's, below.
|
|
1239
|
-
keyLight: false,
|
|
1240
1086
|
grid: dressingGrid === true,
|
|
1241
1087
|
content: source.root,
|
|
1242
1088
|
});
|
|
@@ -1334,7 +1180,7 @@ export function Object3DDocumentViewport({
|
|
|
1334
1180
|
const idOf = (start: THREE.Object3D | null): string | null => {
|
|
1335
1181
|
for (let object = start; object; object = object.parent) {
|
|
1336
1182
|
const id =
|
|
1337
|
-
presentation?.hierarchy.
|
|
1183
|
+
presentation?.hierarchy.idForNativeObject?.(object) ?? current.hierarchy.idForNativeObject?.(object);
|
|
1338
1184
|
if (id) return id;
|
|
1339
1185
|
}
|
|
1340
1186
|
return null;
|
|
@@ -1382,25 +1228,10 @@ export function Object3DDocumentViewport({
|
|
|
1382
1228
|
// The same expression that places the DOM furniture, for the one
|
|
1383
1229
|
// piece of furniture that is drawn on the canvas instead.
|
|
1384
1230
|
chromeInsetPx: chromeless || hasShell || fillContainer ? 0 : STAGE_BLEED_PX,
|
|
1385
|
-
//
|
|
1386
|
-
// lights itself, and the editor's design-time rig answers exactly
|
|
1387
|
-
// the same question. MEASURED on the Model stage before this
|
|
1388
|
-
// existed: the rig's ambient 0.5 + directional 1.0 sit on
|
|
1389
|
-
// `EDITOR_LAYER`, but three filters lights by the CAMERA's layers
|
|
1390
|
-
// (`WebGLRenderer.projectObject`) and the one camera that draws
|
|
1391
|
-
// this stage enables that layer to see the grid and the gizmo —
|
|
1392
|
-
// so the layer scopes them away from nothing at all and they lit
|
|
1393
|
-
// the content.
|
|
1231
|
+
// View presentation supplies the lighting; disable the viewport's built-in rig.
|
|
1394
1232
|
lightRig: false,
|
|
1395
1233
|
});
|
|
1396
1234
|
if (dressingGrid === true) host.viewport.grid.removeFromParent();
|
|
1397
|
-
const open = (event: MouseEvent) => {
|
|
1398
|
-
const id = pick(event.clientX, event.clientY);
|
|
1399
|
-
const object = id && host.adapter ? threeObject(host.adapter.hierarchy, id) : null;
|
|
1400
|
-
if (object) onOpenNodeRef.current?.(object);
|
|
1401
|
-
};
|
|
1402
|
-
container.addEventListener('dblclick', open);
|
|
1403
|
-
host.cleanups.push(() => container.removeEventListener('dblclick', open));
|
|
1404
1235
|
}
|
|
1405
1236
|
const viewport = host.viewport;
|
|
1406
1237
|
if (dressingViewLocked) {
|
|
@@ -1492,19 +1323,7 @@ export function Object3DDocumentViewport({
|
|
|
1492
1323
|
event.stopPropagation();
|
|
1493
1324
|
};
|
|
1494
1325
|
pointerDownListener = (event) => {
|
|
1495
|
-
//
|
|
1496
|
-
// listeners sit on the CONTAINER in the capture phase, so they run
|
|
1497
|
-
// before that control's own handler and a `stopPropagation` there
|
|
1498
|
-
// would be too late: pressing the rolled-back-operation notice's
|
|
1499
|
-
// Dismiss also started a box-select on the mesh (measured — the
|
|
1500
|
-
// notice's text changed from the failed op to "Box select").
|
|
1501
|
-
// The viewport's navigation cluster (`ViewportFurniture`) is the
|
|
1502
|
-
// same case and was missing here: its Pan button drives the camera
|
|
1503
|
-
// from `onPointerDown`, which React dispatches at the root, so
|
|
1504
|
-
// claiming the press here meant `startPan` never ran and the
|
|
1505
|
-
// control was inert (measured — the camera floats were identical
|
|
1506
|
-
// before and after a full pointer gesture, while the cluster's
|
|
1507
|
-
// `onClick` buttons beside it moved the camera every time).
|
|
1326
|
+
// Let controls painted over the stage own their pointer presses before the container’s capture listeners begin selection.
|
|
1508
1327
|
if (
|
|
1509
1328
|
event.target instanceof Element &&
|
|
1510
1329
|
event.target.closest('[role="alert"], [role="toolbar"]') !== null
|
|
@@ -1566,7 +1385,6 @@ export function Object3DDocumentViewport({
|
|
|
1566
1385
|
const previousPersistence = persistenceSessionRef.current;
|
|
1567
1386
|
const previousInteraction = interactionExtensionRef.current;
|
|
1568
1387
|
const previousBackground = host.session?.neutralBackgroundTexture();
|
|
1569
|
-
const previousLights = host.dressing.lights.map((light) => light.visible);
|
|
1570
1388
|
const cleanupCount = host.cleanups.length;
|
|
1571
1389
|
const wasInitialized = host.initialized;
|
|
1572
1390
|
rollback = () => {
|
|
@@ -1587,7 +1405,7 @@ export function Object3DDocumentViewport({
|
|
|
1587
1405
|
store.objectMap.clear();
|
|
1588
1406
|
for (const [id, object] of previousObjects) store.objectMap.set(id, object);
|
|
1589
1407
|
if (previous)
|
|
1590
|
-
store.bindScene(previous.scene, renderer, viewport.
|
|
1408
|
+
store.bindScene(previous.scene, renderer, viewport.camera);
|
|
1591
1409
|
if (previousRoot && previousAdapter)
|
|
1592
1410
|
host.session?.replaceContent(previousRoot, previousAdapter);
|
|
1593
1411
|
documentStateRef.current = previousDocument;
|
|
@@ -1597,9 +1415,6 @@ export function Object3DDocumentViewport({
|
|
|
1597
1415
|
setDocumentAdapter(previousAdapter);
|
|
1598
1416
|
if (previousBackground !== undefined)
|
|
1599
1417
|
host.session?.setNeutralBackground(previousBackground);
|
|
1600
|
-
host.dressing?.lights.forEach((light, index) => {
|
|
1601
|
-
light.visible = previousLights[index] ?? true;
|
|
1602
|
-
});
|
|
1603
1418
|
if (previousRoot) {
|
|
1604
1419
|
host.dressing?.frameContent(previousRoot);
|
|
1605
1420
|
host.presentationRig?.placeFloor(previousRoot);
|
|
@@ -1612,7 +1427,7 @@ export function Object3DDocumentViewport({
|
|
|
1612
1427
|
host.adapter = adapter;
|
|
1613
1428
|
host.presentationAdapter = presentationAdapter;
|
|
1614
1429
|
host.content = binding;
|
|
1615
|
-
store.bindScene(scene, renderer, viewport.
|
|
1430
|
+
store.bindScene(scene, renderer, viewport.camera);
|
|
1616
1431
|
store.setOrbitTarget(viewport.orbitControls.target);
|
|
1617
1432
|
store.objectMap.clear();
|
|
1618
1433
|
scene.traverse((object) => {
|
|
@@ -1622,12 +1437,10 @@ export function Object3DDocumentViewport({
|
|
|
1622
1437
|
// too. A Blender document names its objects only once the engine's outliner rows
|
|
1623
1438
|
// arrive, often after this bind; the presentation ids exist from the first frame.
|
|
1624
1439
|
const id =
|
|
1625
|
-
presentationAdapter?.hierarchy.
|
|
1440
|
+
presentationAdapter?.hierarchy.idForNativeObject?.(object) ?? adapter.hierarchy.idForNativeObject?.(object);
|
|
1626
1441
|
if (id) store.objectMap.set(id, object);
|
|
1627
1442
|
});
|
|
1628
1443
|
if (!host.session) {
|
|
1629
|
-
const overviewFrame = boxFromFrameBounds(frameBoundsRef.current);
|
|
1630
|
-
const openingFrame = boxFromFrameBounds(openingFrameBoundsRef.current) ?? overviewFrame;
|
|
1631
1444
|
// The view's camera (`ViewportCamera`): its field of view, and the direction it opens
|
|
1632
1445
|
// from when the document states none.
|
|
1633
1446
|
const viewCamera = viewPresentation(documentId).camera;
|
|
@@ -1642,7 +1455,7 @@ export function Object3DDocumentViewport({
|
|
|
1642
1455
|
.copy(target)
|
|
1643
1456
|
.addScaledVector(new THREE.Vector3(...viewCamera.opening).normalize(), distance);
|
|
1644
1457
|
}
|
|
1645
|
-
|
|
1458
|
+
viewport.focusOn(source.root);
|
|
1646
1459
|
if (stated) {
|
|
1647
1460
|
const target = new THREE.Vector3(...stated.target);
|
|
1648
1461
|
const direction = new THREE.Vector3(...stated.direction).normalize();
|
|
@@ -1653,7 +1466,7 @@ export function Object3DDocumentViewport({
|
|
|
1653
1466
|
if (stated.up) viewport.camera.up.set(...stated.up);
|
|
1654
1467
|
viewport.orbitControls.target.copy(target);
|
|
1655
1468
|
} else if (statedDirection) {
|
|
1656
|
-
const box =
|
|
1469
|
+
const box = contentWorldBounds(source.root);
|
|
1657
1470
|
const center = box.getCenter(new THREE.Vector3());
|
|
1658
1471
|
const direction = new THREE.Vector3(cameraX, cameraY, cameraZ).normalize();
|
|
1659
1472
|
const distance =
|
|
@@ -1674,7 +1487,6 @@ export function Object3DDocumentViewport({
|
|
|
1674
1487
|
: background === undefined
|
|
1675
1488
|
? host.dressing.backgroundTexture
|
|
1676
1489
|
: new THREE.Color(background),
|
|
1677
|
-
overviewFrame,
|
|
1678
1490
|
);
|
|
1679
1491
|
host.session.selectionOutlineEnabled = !shared && selectionOutlineRef.current;
|
|
1680
1492
|
host.session.setCameraViewSource(cameraViewRef.current ?? null);
|
|
@@ -1686,13 +1498,8 @@ export function Object3DDocumentViewport({
|
|
|
1686
1498
|
const session = host.session;
|
|
1687
1499
|
host.cleanups.push(
|
|
1688
1500
|
watchPaletteBackdrop(() => {
|
|
1689
|
-
//
|
|
1690
|
-
//
|
|
1691
|
-
// palette's flat viewport background when it names one, the
|
|
1692
|
-
// dressing's gradient re-derived for the palette otherwise.
|
|
1693
|
-
// Painting the flat colour straight onto the scene left it there
|
|
1694
|
-
// when the next palette named none (Plotter's paper stayed under
|
|
1695
|
-
// Classic).
|
|
1501
|
+
// Refresh the session's neutral backdrop from the palette's flat background,
|
|
1502
|
+
// or a newly derived gradient when the palette names no flat background.
|
|
1696
1503
|
const flat = nativeViewportLook(canvas).background;
|
|
1697
1504
|
const previous = host.defaultBackground;
|
|
1698
1505
|
host.defaultBackground =
|
|
@@ -1771,12 +1578,7 @@ export function Object3DDocumentViewport({
|
|
|
1771
1578
|
if (viewPresentation(documentId).drawMode !== mode)
|
|
1772
1579
|
setViewPresentation(documentId, { drawMode: mode as ViewportDrawMode });
|
|
1773
1580
|
};
|
|
1774
|
-
//
|
|
1775
|
-
// View), so the file's statement is where the session starts. Otherwise a mode the person
|
|
1776
|
-
// chose and the view restored is theirs. Only a view with neither is given the session's.
|
|
1777
|
-
// Then the view takes the session's mode either way: a stored choice older than the file's
|
|
1778
|
-
// statement would otherwise keep lighting the view for the mode it no longer draws (the
|
|
1779
|
-
// Blender presenter dressed in Solid's materials under a Material Preview cell).
|
|
1581
|
+
// Initialize draw mode from the document’s file, then the restored view, then the session default. Synchronize the view to the resulting session mode.
|
|
1780
1582
|
const restoredMode = ownLayer?.drawMode ?? viewPresentationSnapshot(documentId).drawMode;
|
|
1781
1583
|
if (restoredMode !== undefined && viewModes.has(restoredMode) && host.session)
|
|
1782
1584
|
host.session.setMode(restoredMode);
|
|
@@ -1900,7 +1702,8 @@ export function Object3DDocumentViewport({
|
|
|
1900
1702
|
? host.darkenContentLights()
|
|
1901
1703
|
: 0;
|
|
1902
1704
|
rig.update(documentSession.camera());
|
|
1903
|
-
|
|
1705
|
+
// A sampled photograph draws many times; it is one draw to report.
|
|
1706
|
+
if (documentSession.photographSampleIndex() === 0) reportViewDraw(documentId, {
|
|
1904
1707
|
source: drawSource,
|
|
1905
1708
|
presetId: rig.presetId(),
|
|
1906
1709
|
presetLights: rig.lightsVisible(),
|
|
@@ -1919,24 +1722,21 @@ export function Object3DDocumentViewport({
|
|
|
1919
1722
|
host.syncHostScene?.();
|
|
1920
1723
|
const mode = documentSession.presentation().mode;
|
|
1921
1724
|
const navigation = interactive && ['solid', 'preview', 'clay', 'unlit', 'matcap'].includes(mode);
|
|
1922
|
-
return source.prepareDraw?.(documentSession.camera(), {interactive: navigation, height: renderer.domElement.height, multiDraw: renderer.extensions.has('WEBGL_multi_draw'), renderer});
|
|
1725
|
+
return source.prepareDraw?.(documentSession.camera(), {interactive: navigation, height: renderer.domElement.height, multiDraw: renderer.extensions.has('WEBGL_multi_draw'), renderer, drawMode: mode, pixelScale: documentSession.drawPixelScale(), photograph: documentSession.photographing()});
|
|
1923
1726
|
});
|
|
1924
1727
|
// This loop draws the compass and every other overlay pass over the
|
|
1925
1728
|
// document's own render, so it — and only it — can serve the chrome
|
|
1926
1729
|
// door a frame that matches the screen.
|
|
1927
1730
|
documentSession.setPresentsFrames(true);
|
|
1928
|
-
//
|
|
1929
|
-
//
|
|
1930
|
-
// had here is gone for good: subjects seek THEMSELVES, so content time
|
|
1931
|
-
// is only ever the bookkeeping plus the source's own update.
|
|
1731
|
+
// Advance content-time bookkeeping and the source’s update. Animation subjects seek
|
|
1732
|
+
// themselves through their own transport.
|
|
1932
1733
|
const advanceContent = (deltaSeconds: number): void => {
|
|
1933
1734
|
if (disposed || !(deltaSeconds > 0) || !Number.isFinite(deltaSeconds)) return;
|
|
1934
1735
|
host.contentSeconds += deltaSeconds;
|
|
1935
1736
|
host.transportAdvances++;
|
|
1936
1737
|
source.update?.(deltaSeconds);
|
|
1937
1738
|
};
|
|
1938
|
-
//
|
|
1939
|
-
// root's stage rescans (below) because its tree arrives later.
|
|
1739
|
+
// Scan this source's animation subjects once. World-root stages rescan as their trees update.
|
|
1940
1740
|
const clipScan = scanClipSubjects(source.root, host.transport);
|
|
1941
1741
|
host.cleanups.push(() => clipScan.dispose());
|
|
1942
1742
|
const retainedTransport = retainedState.transport;
|
|
@@ -1951,37 +1751,41 @@ export function Object3DDocumentViewport({
|
|
|
1951
1751
|
retainedState.transport = null;
|
|
1952
1752
|
}
|
|
1953
1753
|
let previousTime = performance.now();
|
|
1954
|
-
//
|
|
1955
|
-
// (`ToolObject3DPreviewSource.onChange`) is drawn only when something
|
|
1956
|
-
// changed; one that does not is drawn every frame, as before. The loop
|
|
1957
|
-
// itself still runs every frame -- transport, controls, flights and
|
|
1958
|
-
// presence advance there -- and what is skipped is the render: the
|
|
1959
|
-
// scene, the composer's passes and the compass. MEASURED before: an
|
|
1960
|
-
// idle Model document spent 43% of the main thread drawing the same
|
|
1961
|
-
// picture (4 s at load ~30: 372 frames, 1.7 s in the loop, 0.18 s of
|
|
1962
|
-
// it WebGL calls, the rest three's render and the outline passes).
|
|
1963
|
-
//
|
|
1964
|
-
// What draws a frame: the content's own announcement; the stage
|
|
1965
|
-
// store; pointer, wheel and key input on the stage (hover and gizmo
|
|
1966
|
-
// highlights follow the pointer); the editor acting on a stage from
|
|
1967
|
-
// outside those (`stage-invalidation.ts`); a moved camera or a resized
|
|
1968
|
-
// surface; playback; a flight or a pending photograph
|
|
1969
|
-
// (`needsFrame`); another participant's markers; particle systems;
|
|
1970
|
-
// and an interaction extension that refines every frame.
|
|
1754
|
+
// Draw on announced content changes, stage state or interaction, camera/size changes, playback, flights, captures, presence, particles and active interaction extensions. Sources without onChange draw each frame; the presentation loop always advances.
|
|
1971
1755
|
const drawsOnChange = typeof source.onChange === 'function';
|
|
1972
1756
|
let dirty = true;
|
|
1757
|
+
// Every signal, counted: a signal that arrives while a frame draws (a source answering its
|
|
1758
|
+
// own draw) is not cleared by that frame, and the count is the content's REVISION, which a
|
|
1759
|
+
// live accumulation's sum belongs to.
|
|
1760
|
+
let changeCount = 0;
|
|
1973
1761
|
const markDirty = (): void => {
|
|
1974
1762
|
dirty = true;
|
|
1763
|
+
changeCount++;
|
|
1764
|
+
};
|
|
1765
|
+
// A pointer moving over the stage draws a frame (hover highlights follow it), but it is a
|
|
1766
|
+
// change of the picture only when it changes what the stage draws: the furniture's hover
|
|
1767
|
+
// and drag state (`EditorViewport.interactionState`) or the camera, both compared below.
|
|
1768
|
+
// A press, a release, the wheel and keys still count as changes, and so does a move with
|
|
1769
|
+
// a button held: a drag moves things whether or not what it moves signals (a gizmo drag in
|
|
1770
|
+
// a Blender view writes the object's matrix in place). An active drag or gesture is a
|
|
1771
|
+
// change on every frame too (`changedSinceDrawn`).
|
|
1772
|
+
let inputPending = false;
|
|
1773
|
+
const markInput = (event: Event): void => {
|
|
1774
|
+
if ((event as PointerEvent).buttons) markDirty();
|
|
1775
|
+
else inputPending = true;
|
|
1975
1776
|
};
|
|
1976
1777
|
if (drawsOnChange) {
|
|
1977
1778
|
const stopSource = source.onChange!(markDirty);
|
|
1978
1779
|
const stopStore = store.shell.subscribe(markDirty);
|
|
1979
|
-
const
|
|
1980
|
-
|
|
1780
|
+
const changes = ['pointerdown', 'pointerup', 'wheel', 'keydown', 'keyup'] as const;
|
|
1781
|
+
const moves = ['pointermove', 'pointerleave'] as const;
|
|
1782
|
+
for (const type of changes) container.addEventListener(type, markDirty, { capture: true, passive: true });
|
|
1783
|
+
for (const type of moves) container.addEventListener(type, markInput, { capture: true, passive: true });
|
|
1981
1784
|
stopDrawSignals = () => {
|
|
1982
1785
|
stopSource();
|
|
1983
1786
|
stopStore();
|
|
1984
|
-
for (const type of
|
|
1787
|
+
for (const type of changes) container.removeEventListener(type, markDirty, { capture: true });
|
|
1788
|
+
for (const type of moves) container.removeEventListener(type, markInput, { capture: true });
|
|
1985
1789
|
};
|
|
1986
1790
|
}
|
|
1987
1791
|
let drawnGeneration = -1;
|
|
@@ -1992,13 +1796,36 @@ export function Object3DDocumentViewport({
|
|
|
1992
1796
|
let drawnBackground: unknown = undefined;
|
|
1993
1797
|
let drawnToneMapping: THREE.ToneMapping | null = null;
|
|
1994
1798
|
let drawnExposure = Number.NaN;
|
|
1995
|
-
|
|
1799
|
+
let drawnInteraction = '';
|
|
1800
|
+
// The signals already taken when the frame being drawn began.
|
|
1801
|
+
let changeCountAtDraw = 0;
|
|
1802
|
+
let generationAtDraw = -1;
|
|
1803
|
+
// What a draw mirrors into the stage's scene (the host's world, its tone), BY VALUE: a
|
|
1804
|
+
// backdrop colour is handed over as a fresh instance on every draw, so identity would read
|
|
1805
|
+
// every draw as a change. A still frame that changes it is followed by a changed one.
|
|
1806
|
+
const valueOf = (value: THREE.Color | THREE.Texture | null): string =>
|
|
1807
|
+
!value ? '-' : (value as THREE.Color).isColor ? `#${(value as THREE.Color).getHexString()}` : (value as THREE.Texture).uuid;
|
|
1808
|
+
const mirroredState = (): readonly unknown[] => {
|
|
1809
|
+
const fog = host.scene.fog as (THREE.Fog & THREE.FogExp2) | null;
|
|
1810
|
+
return [
|
|
1811
|
+
valueOf(host.scene.background as THREE.Color | THREE.Texture | null),
|
|
1812
|
+
valueOf(host.scene.environment),
|
|
1813
|
+
fog ? `${fog.color.getHexString()}|${fog.near}|${fog.far}|${fog.density}` : '-',
|
|
1814
|
+
host.scene.backgroundIntensity, host.scene.backgroundBlurriness, host.scene.environmentIntensity,
|
|
1815
|
+
renderer.toneMapping, renderer.toneMappingExposure,
|
|
1816
|
+
];
|
|
1817
|
+
};
|
|
1818
|
+
// What changed since the last drawn frame. A frame drawn with nothing changed (a
|
|
1819
|
+
// presented-frame request, the live view's accumulation going on) is a still one.
|
|
1820
|
+
const changedSinceDrawn = (advanced: number): boolean => {
|
|
1996
1821
|
if (!drawsOnChange || dirty || advanced > 0) return true;
|
|
1997
1822
|
if (stageGeneration() !== drawnGeneration) return true;
|
|
1998
|
-
if (
|
|
1823
|
+
if (host.presence?.live()) return true;
|
|
1999
1824
|
if (interactionExtension?.prepareFrame || interactionExtension?.update) return true;
|
|
2000
1825
|
if (((viewport.batchedRenderer as unknown as { batches?: readonly unknown[] }).batches?.length ?? 0) > 0)
|
|
2001
1826
|
return true;
|
|
1827
|
+
if (viewport.interacting() || gestureController?.hasActiveGesture()) return true;
|
|
1828
|
+
if (viewport.interactionState() !== drawnInteraction) return true;
|
|
2002
1829
|
const camera = documentSession.camera();
|
|
2003
1830
|
camera.updateMatrixWorld();
|
|
2004
1831
|
renderer.getDrawingBufferSize(currentSize);
|
|
@@ -2010,8 +1837,10 @@ export function Object3DDocumentViewport({
|
|
|
2010
1837
|
renderer.toneMappingExposure !== drawnExposure;
|
|
2011
1838
|
};
|
|
2012
1839
|
const noteDrawn = (): void => {
|
|
2013
|
-
dirty =
|
|
2014
|
-
|
|
1840
|
+
dirty = changeCount !== changeCountAtDraw;
|
|
1841
|
+
inputPending = false;
|
|
1842
|
+
drawnGeneration = generationAtDraw;
|
|
1843
|
+
drawnInteraction = viewport.interactionState();
|
|
2015
1844
|
const camera = documentSession.camera();
|
|
2016
1845
|
drawnCamera.copy(camera.matrixWorld);
|
|
2017
1846
|
drawnProjection.copy(camera.projectionMatrix);
|
|
@@ -2022,15 +1851,12 @@ export function Object3DDocumentViewport({
|
|
|
2022
1851
|
};
|
|
2023
1852
|
let navigationMeasurement: THREE.Quaternion | null = null;
|
|
2024
1853
|
const animate = (time: number, resumed: boolean) => {
|
|
2025
|
-
if (disposed
|
|
1854
|
+
if (disposed) return;
|
|
2026
1855
|
const activeRenderer = renderer;
|
|
2027
1856
|
const activeViewport = viewport;
|
|
2028
|
-
const profiler = documentSession?.profiler;
|
|
2029
|
-
profiler?.beginFrame();
|
|
2030
1857
|
if (resumed) previousTime = time;
|
|
2031
1858
|
const delta = Math.min((time - previousTime) / 1000, 0.1);
|
|
2032
1859
|
previousTime = time;
|
|
2033
|
-
profiler?.beginPhase();
|
|
2034
1860
|
// CONTENT TIME moves ONLY through the stage transport, and only
|
|
2035
1861
|
// while a human is holding it: `tick` is inert unless playing and
|
|
2036
1862
|
// the editor drives, and it returns exactly the seconds it advanced.
|
|
@@ -2038,8 +1864,6 @@ export function Object3DDocumentViewport({
|
|
|
2038
1864
|
if (advanced > 0) advanceContent(advanced);
|
|
2039
1865
|
interactionExtension?.prepareFrame?.(delta);
|
|
2040
1866
|
interactionExtension?.update?.(delta);
|
|
2041
|
-
profiler?.endPhase('animation');
|
|
2042
|
-
profiler?.beginPhase();
|
|
2043
1867
|
host.presence?.syncMarkers(delta);
|
|
2044
1868
|
viewport.update(delta);
|
|
2045
1869
|
// This stage's frame hook. After viewport.update, the same order the
|
|
@@ -2052,12 +1876,15 @@ export function Object3DDocumentViewport({
|
|
|
2052
1876
|
camera.updateMatrixWorld(true);
|
|
2053
1877
|
}
|
|
2054
1878
|
viewport.batchedRenderer.update(delta);
|
|
2055
|
-
|
|
2056
|
-
|
|
2057
|
-
|
|
1879
|
+
// THE LIVE VIEW ACCUMULATES while nothing changes, one sample a frame, as Blender's
|
|
1880
|
+
// viewport does (`Object3DDocumentSession.renderViewport`'s `still`).
|
|
1881
|
+
const changed = resumed || navigationMeasurement !== null || changedSinceDrawn(advanced);
|
|
1882
|
+
if (!changed && !inputPending && !documentSession.needsFrame() && !documentSession.accumulating()) {
|
|
2058
1883
|
return;
|
|
2059
1884
|
}
|
|
2060
|
-
|
|
1885
|
+
changeCountAtDraw = changeCount;
|
|
1886
|
+
generationAtDraw = stageGeneration();
|
|
1887
|
+
const mirroredBefore = changed ? null : mirroredState();
|
|
2061
1888
|
activeRenderer.info.reset();
|
|
2062
1889
|
const timeFirstRender =
|
|
2063
1890
|
!firstFrameGateRef.current && activeViewportBreakdownDocumentId() === documentId;
|
|
@@ -2072,33 +1899,30 @@ export function Object3DDocumentViewport({
|
|
|
2072
1899
|
// Photographs and the default frame-cost reading draw the
|
|
2073
1900
|
// full resident scene. Only navigation may use source-owned
|
|
2074
1901
|
// temporary geometry; the source restores it after the draw.
|
|
2075
|
-
documentSession.renderViewport(
|
|
1902
|
+
documentSession.renderViewport(
|
|
1903
|
+
delta,
|
|
1904
|
+
navigationMeasurement !== null || (!resumed && !documentSession.needsFrame()),
|
|
1905
|
+
!changed,
|
|
1906
|
+
`${changeCountAtDraw}/${generationAtDraw}`,
|
|
1907
|
+
);
|
|
2076
1908
|
activeViewport.renderViewCube(activeRenderer);
|
|
2077
1909
|
});
|
|
2078
1910
|
} finally {
|
|
2079
1911
|
host.scene.overrideMaterial = presentationOverride;
|
|
2080
1912
|
}
|
|
1913
|
+
// A still frame whose draw changed what the stage mirrors starts the sum over next frame.
|
|
1914
|
+
if (mirroredBefore && mirroredState().some((value, index) => value !== mirroredBefore[index])) markDirty();
|
|
2081
1915
|
// Inside the frame that drew them — the canvas has no
|
|
2082
1916
|
// preserveDrawingBuffer, so this is the only moment its pixels exist.
|
|
2083
1917
|
documentSession.servePresentedFrame();
|
|
2084
1918
|
noteDrawn();
|
|
2085
1919
|
if (!firstFrameGateRef.current) {
|
|
2086
1920
|
firstFrameGateRef.current = true;
|
|
2087
|
-
if (timeFirstRender)
|
|
1921
|
+
if (timeFirstRender) markViewportFirstRender(Date.now() - tFirstRender);
|
|
2088
1922
|
recordViewportFirstFrame(documentId);
|
|
2089
1923
|
setSurfaceStatus('ready');
|
|
2090
1924
|
}
|
|
2091
|
-
|
|
2092
|
-
gpuMs: null,
|
|
2093
|
-
drawCalls: activeRenderer.info.render.calls,
|
|
2094
|
-
triangles: activeRenderer.info.render.triangles,
|
|
2095
|
-
geometries: activeRenderer.info.memory.geometries,
|
|
2096
|
-
textures: activeRenderer.info.memory.textures,
|
|
2097
|
-
});
|
|
2098
|
-
profiler?.endPhase('render');
|
|
2099
|
-
profiler?.endFrame();
|
|
2100
|
-
// THE SHELL'S READOUT IS THE FOCUSED STAGE'S (ARCHITECTURE-CORE
|
|
2101
|
-
// §One stage unit 4). `CameraInfo` and `StatsOverlay` read one
|
|
1925
|
+
// THE SHELL'S READOUT IS THE FOCUSED STAGE'S. `CameraInfo` and `StatsOverlay` read one
|
|
2102
1926
|
// struct the shell owns; the stage the panels are following fills
|
|
2103
1927
|
// it, so a model or prefab document reports ITS frame and ITS
|
|
2104
1928
|
// camera. Inactive documents leave the shared readout alone.
|
|
@@ -2124,8 +1948,7 @@ export function Object3DDocumentViewport({
|
|
|
2124
1948
|
* resumed so it draws, run back to back and each waited out on the GPU with a one-pixel
|
|
2125
1949
|
* read, which a real frame never does; the first frame warms and is not counted.
|
|
2126
1950
|
*/
|
|
2127
|
-
const measureFrameCost = (frames: number, quality: 'full' | 'navigation'
|
|
2128
|
-
if (!renderer) throw new Error('This stage has no renderer yet.');
|
|
1951
|
+
const measureFrameCost = (frames: number, quality: 'full' | 'navigation'): StageFrameCost => {
|
|
2129
1952
|
const activeRenderer = renderer;
|
|
2130
1953
|
const gl = activeRenderer.getContext();
|
|
2131
1954
|
const pixel = new Uint8Array(4);
|
|
@@ -2170,17 +1993,17 @@ export function Object3DDocumentViewport({
|
|
|
2170
1993
|
sceneTriangles += Math.floor(count / 3) * ((mesh as THREE.InstancedMesh).count ?? 1);
|
|
2171
1994
|
});
|
|
2172
1995
|
const sorted = [...times].sort((a, b) => a - b);
|
|
2173
|
-
const at = (q: number) => sorted[Math.min(sorted.length - 1, Math.floor(q * sorted.length))]
|
|
1996
|
+
const at = (q: number) => sorted[Math.min(sorted.length - 1, Math.floor(q * sorted.length))]!;
|
|
2174
1997
|
const size = activeRenderer.getDrawingBufferSize(new THREE.Vector2());
|
|
2175
1998
|
const ratio = activeRenderer.getPixelRatio();
|
|
2176
1999
|
const round = (value: number) => Math.round(value * 100) / 100;
|
|
2177
|
-
const median = (values: number[]) => round(values.sort((a, b) => a - b)[Math.floor(values.length / 2)]
|
|
2000
|
+
const median = (values: number[]) => round(values.sort((a, b) => a - b)[Math.floor(values.length / 2)]!);
|
|
2178
2001
|
return {
|
|
2179
2002
|
frames,
|
|
2180
2003
|
quality,
|
|
2181
2004
|
medianMs: round(at(0.5)),
|
|
2182
2005
|
p95Ms: round(at(0.95)),
|
|
2183
|
-
minMs: round(sorted[0]
|
|
2006
|
+
minMs: round(sorted[0]!),
|
|
2184
2007
|
submitMedianMs: median(submitTimes),
|
|
2185
2008
|
completionWaitMedianMs: median(completionTimes),
|
|
2186
2009
|
width: size.x,
|
|
@@ -2197,13 +2020,7 @@ export function Object3DDocumentViewport({
|
|
|
2197
2020
|
if (!host.initialized) {
|
|
2198
2021
|
if (!chromeless) {
|
|
2199
2022
|
// --- The viewport door (viewport-door.ts) ---
|
|
2200
|
-
//
|
|
2201
|
-
// (ARCHITECTURE-CORE §One stage), and it binds under the id of the
|
|
2202
|
-
// document it draws, so an SDK reader reaches THIS stage's rig,
|
|
2203
|
-
// helper sink and frame loop through the viewport door's `viewportStages()`.
|
|
2204
|
-
// It presents no live roots yet — Play's adoption is the world
|
|
2205
|
-
// root's, and moves onto this host in unit 3 — so the presenter
|
|
2206
|
-
// declines every root rather than pretending to a subject.
|
|
2023
|
+
// Bind this document's rig, helper sink and frame loop. Its presenter declines live roots.
|
|
2207
2024
|
// A CHROMELESS mount (the inspector's object preview) binds
|
|
2208
2025
|
// nothing: it is a thumbnail of a document, not a stage of its own,
|
|
2209
2026
|
// and several can be alive for one document id at once.
|
|
@@ -2213,23 +2030,17 @@ export function Object3DDocumentViewport({
|
|
|
2213
2030
|
camera: viewport.camera,
|
|
2214
2031
|
drawCamera: () => host.session?.camera() ?? viewport.camera,
|
|
2215
2032
|
orbit: viewport.orbitControls,
|
|
2033
|
+
projection: viewport.projectionOwner,
|
|
2216
2034
|
scene: host.scene,
|
|
2035
|
+
renderer: host.surface.renderer,
|
|
2217
2036
|
frameCost: (frames, quality) => measureFrameCost(frames, quality),
|
|
2218
2037
|
},
|
|
2219
|
-
() => null,
|
|
2220
2038
|
(kind, object) => viewport.setHelper(kind, object),
|
|
2221
2039
|
{ documentId },
|
|
2222
2040
|
),
|
|
2223
2041
|
);
|
|
2224
|
-
//
|
|
2225
|
-
//
|
|
2226
|
-
// It is a capability with a condition — a stage painting a three
|
|
2227
|
-
// surface — and a `build` stage always paints one, so every
|
|
2228
|
-
// document with chrome mounts it and a prefab story shows the
|
|
2229
|
-
// same camera frusta, selection boxes and pointer rays the Scene
|
|
2230
|
-
// does (ARCHITECTURE-CORE §One stage; WORK.md §Presence and the
|
|
2231
|
-
// substrate, presence unit 4). Dynamically imported: it reaches
|
|
2232
|
-
// the collaboration client, which a bounded host must not pay for.
|
|
2042
|
+
// Mount participant camera frusta, selection boxes and pointer rays on chromed three.js
|
|
2043
|
+
// stages. Load the collaboration client dynamically so bounded hosts do not import it.
|
|
2233
2044
|
void import('./stage-presence-markers').then((markers) => {
|
|
2234
2045
|
if (host.isClosed || documentHostRef.current !== host || host.presence) return;
|
|
2235
2046
|
const binding = markers.bindStagePresenceMarkers({
|
|
@@ -2259,24 +2070,13 @@ export function Object3DDocumentViewport({
|
|
|
2259
2070
|
host.cleanups.push(
|
|
2260
2071
|
registerObject3DDocumentSession(documentSession, threeStageTransformChrome(documentId)),
|
|
2261
2072
|
);
|
|
2262
|
-
host.cleanups.push(
|
|
2263
|
-
registerPerformanceSource({
|
|
2264
|
-
id: documentId,
|
|
2265
|
-
label: displayName,
|
|
2266
|
-
kind: assetSubject ? 'asset' : 'source',
|
|
2267
|
-
profiler: documentSession.profiler,
|
|
2268
|
-
}),
|
|
2269
|
-
);
|
|
2270
2073
|
}
|
|
2271
2074
|
// EXACTLY ONE registrar of this document's workspace selection: the
|
|
2272
2075
|
// identity-row shell does it through its own `selection` prop, so
|
|
2273
|
-
// the host does it wherever that shell is absent. The
|
|
2274
|
-
//
|
|
2275
|
-
//
|
|
2276
|
-
//
|
|
2277
|
-
// document with NEITHER registrar — the Hierarchy said "No
|
|
2278
|
-
// authoring adapter" and the mesh inspector emptied (measured live
|
|
2279
|
-
// on the models scaffold, 2026-09-18).
|
|
2076
|
+
// the host does it wherever that shell is absent. The shell and the
|
|
2077
|
+
// studio are different answers (a data subject has the studio),
|
|
2078
|
+
// so `!studioStage` would leave the Model document
|
|
2079
|
+
// with NEITHER registrar.
|
|
2280
2080
|
if (!chromeless && !hasShell) {
|
|
2281
2081
|
host.cleanups.push(
|
|
2282
2082
|
registerWorkspaceDocumentSelection(documentId, () => ({
|
|
@@ -2296,12 +2096,8 @@ export function Object3DDocumentViewport({
|
|
|
2296
2096
|
transportAdvances: () => host.transportAdvances,
|
|
2297
2097
|
}),
|
|
2298
2098
|
);
|
|
2299
|
-
//
|
|
2300
|
-
//
|
|
2301
|
-
// never did, which is why Frame (`F`) was inert on a prefab or a
|
|
2302
|
-
// model — the hotkey pushes `focus-selection` onto the stage's own
|
|
2303
|
-
// store and nothing was listening (ARCHITECTURE-CORE §One stage:
|
|
2304
|
-
// the capability is present wherever its condition holds).
|
|
2099
|
+
// Listen to the document stage’s viewport action bus so commands such as focus-selection
|
|
2100
|
+
// reach the stage’s own store.
|
|
2305
2101
|
host.cleanups.push(
|
|
2306
2102
|
store.shell.onViewportAction((action) => {
|
|
2307
2103
|
switch (action.type) {
|
|
@@ -2469,6 +2265,8 @@ export function Object3DDocumentViewport({
|
|
|
2469
2265
|
}
|
|
2470
2266
|
// A world root keeps its clock while another document holds focus.
|
|
2471
2267
|
session.setActive(active || worldRoot);
|
|
2268
|
+
// A hidden stage keeps no live accumulation (its targets are the drawing buffer's size).
|
|
2269
|
+
if (!active) documentHostRef.current?.session?.releaseLive();
|
|
2472
2270
|
}, [active, documentId, worldRoot]);
|
|
2473
2271
|
|
|
2474
2272
|
// The stage's own handle for the capabilities that drive it directly (the
|
|
@@ -2518,12 +2316,7 @@ export function Object3DDocumentViewport({
|
|
|
2518
2316
|
style={{ position: 'absolute', inset: 0, minHeight: 0, overflow: 'hidden' }}
|
|
2519
2317
|
>
|
|
2520
2318
|
<ViewportSurface canvasHostRef={canvasHostRef} />
|
|
2521
|
-
{/*
|
|
2522
|
-
presets, Frame, pivot, snap. Beside the stage rather than inside
|
|
2523
|
-
the overlay set, because a package-contributed document mounts
|
|
2524
|
-
with no shell above it and reached none of them
|
|
2525
|
-
(`stage-keyboard.tsx`). The registry holds ONE stage's set, so the
|
|
2526
|
-
condition is the ACTIVE document's stage. */}
|
|
2319
|
+
{/* The active document's stage owns transform, view, Frame, pivot and snap keys. */}
|
|
2527
2320
|
{active && stageHandle && !chromeless ? (
|
|
2528
2321
|
<Suspense fallback={null}>
|
|
2529
2322
|
<LazyStageKeyboard stage={stageHandle} />
|
|
@@ -2547,18 +2340,11 @@ export function Object3DDocumentViewport({
|
|
|
2547
2340
|
<worldRootBinding.Overlays
|
|
2548
2341
|
store={shellStore}
|
|
2549
2342
|
documentId={documentId}
|
|
2550
|
-
cameraPreviewRef={cameraPreviewRef}
|
|
2551
2343
|
mountStatus={designMountStatus}
|
|
2552
2344
|
rootIds={designRootIds}
|
|
2553
2345
|
/>
|
|
2554
2346
|
) : null}
|
|
2555
|
-
{/*
|
|
2556
|
-
write the history guarded) — every writer of `error` restores the
|
|
2557
|
-
prior state, so the surface below is live and must stay visible.
|
|
2558
|
-
It used to mount a full-bleed opaque curtain with no dismiss: the
|
|
2559
|
-
stage read as dead until the session restarted (measured). A
|
|
2560
|
-
notice over the stage, dismissible, and cleared by the next
|
|
2561
|
-
gesture that succeeds. */}
|
|
2347
|
+
{/* Show a dismissible error notice over any stage that remains available. */}
|
|
2562
2348
|
{error && (
|
|
2563
2349
|
<div
|
|
2564
2350
|
role="alert"
|
|
@@ -2604,13 +2390,7 @@ export function Object3DDocumentViewport({
|
|
|
2604
2390
|
{OBJECT3D_SURFACE_BUILDING}
|
|
2605
2391
|
</ViewportSurfaceStatus>
|
|
2606
2392
|
)}
|
|
2607
|
-
{/*
|
|
2608
|
-
above) so the rendered image fills it edge to edge. The furniture is
|
|
2609
|
-
CHROME, not image: it has to sit inside the panel the person can
|
|
2610
|
-
actually see, so it gets that bleed back. Without this the view text
|
|
2611
|
-
started at page x=-6 — the "U" of "User Perspective" was clipped off
|
|
2612
|
-
the window — and the navigation cluster's right edge landed 4px past
|
|
2613
|
-
the panel, under the Inspector's section rail (both measured). */}
|
|
2393
|
+
{/* Inset furniture by the canvas bleed so chrome stays inside the visible panel. */}
|
|
2614
2394
|
<div
|
|
2615
2395
|
style={{
|
|
2616
2396
|
position: 'absolute',
|
|
@@ -2654,9 +2434,6 @@ export function Object3DDocumentViewport({
|
|
|
2654
2434
|
>
|
|
2655
2435
|
<div className="volter-object3d-asset-workspace">
|
|
2656
2436
|
{viewport}
|
|
2657
|
-
{/* Where the deleted preview strip was. It draws nothing until this
|
|
2658
|
-
stage's transport has a subject, so a document with nothing to show
|
|
2659
|
-
at a time is unchanged. */}
|
|
2660
2437
|
<TransportStrip transport={documentHostRef.current?.transport ?? null} />
|
|
2661
2438
|
</div>
|
|
2662
2439
|
</AssetEditorShell>
|