@volter/editor-threejs 0.5.212 → 0.5.214
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/dist-node/serving.mjs +4 -20
- package/package.json +3 -5
- package/serving/animation-live-module.ts +4 -20
- package/src/animation/live-mixers.ts +1 -9
- package/src/kit/animation/three-clips-subject.ts +10 -50
- package/src/kit/authoring/component-instance-root.ts +2 -3
- package/src/kit/components/Object3DDocumentToolbar.tsx +1 -2
- package/src/kit/components/Object3DDocumentViewport.tsx +2 -4
- package/src/kit/components/StageHost.tsx +78 -208
- package/src/kit/components/ViewportViewMenu.tsx +1 -1
- package/src/kit/components/asset-viewers/EntityModelDocument.tsx +2 -4
- package/src/kit/components/asset-viewers/EnvironmentAssetDocument.tsx +4 -6
- package/src/kit/components/asset-viewers/LiveModuleDocument.tsx +7 -31
- package/src/kit/components/asset-viewers/LutAssetDocument.tsx +4 -6
- package/src/kit/components/asset-viewers/ModelAssetDocument.tsx +4 -8
- package/src/kit/components/asset-viewers/Object3DPreview.tsx +3 -19
- package/src/kit/components/asset-viewers/QuarksAssetDocument.tsx +4 -8
- package/src/kit/components/asset-viewers/ShaderAssetDocument.tsx +37 -55
- package/src/kit/components/object3d-contribution-surfaces.tsx +4 -2
- package/src/kit/components/stage-transform-tools.tsx +1 -2
- package/src/kit/editor-shell-store.ts +2 -26
- package/src/kit/editor-viewport.ts +11 -1
- package/src/kit/three-integration.ts +0 -2
- package/src/viewport-api.ts +4 -16
- package/src/viewport-door.ts +2 -38
- package/src/kit/components/world-root-binding.ts +0 -59
- package/src/kit/play-camera-flight.ts +0 -291
- package/src/kit/scene-document-viewport.ts +0 -51
- package/src/kit/viewport-surface-status.tsx +0 -44
|
@@ -3,7 +3,7 @@ import { ShellStore } from '@volter/sdk/shell-store';
|
|
|
3
3
|
import type { ToolObject3DAuthoringProps, ToolObject3DDocumentAuthoring, ToolObject3DPreviewSource } from '../../object3d-contributions';
|
|
4
4
|
import { invalidateStages, stageGeneration } from '@volter/sdk/stage-invalidation';
|
|
5
5
|
import type { StageTransportSnapshot } from '@volter/sdk/host';
|
|
6
|
-
import { EditorIcon, editorIcons, IconButton, themeVars } from '@volter/sdk/widgets';
|
|
6
|
+
import { EditorBanner, EditorIcon, editorIcons, IconButton, themeVars } from '@volter/sdk/widgets';
|
|
7
7
|
import type { AuthoringAdapter } from '@volter/project/adapter';
|
|
8
8
|
import { contentWorldBounds } from '@volter/editor-threejs/viewport/content-bounds';
|
|
9
9
|
import { isInEditorOwnedSubtree } from '@volter/editor-threejs/viewport/editor-layers';
|
|
@@ -57,8 +57,7 @@ import {
|
|
|
57
57
|
studioStageApplies,
|
|
58
58
|
} from '@volter/sdk/stage-context';
|
|
59
59
|
import { registerStageStore } from '@volter/sdk/stage-store-registry';
|
|
60
|
-
import { announceDocumentStage
|
|
61
|
-
import { sceneDocumentViewport } from '../scene-document-viewport';
|
|
60
|
+
import { announceDocumentStage } from '@volter/sdk/document-viewports';
|
|
62
61
|
import { threeStageTransformChrome } from './stage-transform-chrome';
|
|
63
62
|
import { RetainedDocumentStates } from '../retained-document-states';
|
|
64
63
|
import { perspectiveDistanceToFitBox } from '../three-viewport/camera-fit';
|
|
@@ -87,7 +86,6 @@ import {
|
|
|
87
86
|
} from '@volter/sdk/workspace-document-registry';
|
|
88
87
|
import { AssetEditorShell } from '@volter/sdk/components/AssetEditorShell';
|
|
89
88
|
import { StageOverlays } from './StageOverlays';
|
|
90
|
-
import type { WorldRootStageBinding } from './world-root-binding';
|
|
91
89
|
import { TransportStrip } from '../transport-strip';
|
|
92
90
|
|
|
93
91
|
// The stage's own keyboard actions, behind the same lazy boundary as the
|
|
@@ -105,7 +103,7 @@ import {
|
|
|
105
103
|
watchPaletteBackdrop,
|
|
106
104
|
} from './standard-viewport-dressing';
|
|
107
105
|
import { ViewportFurniture } from './ViewportFurniture';
|
|
108
|
-
import {
|
|
106
|
+
import { Progress } from '@volter/sdk/progress';
|
|
109
107
|
import { workspaceHistoryService } from '@volter/sdk/components/workspace-history';
|
|
110
108
|
import {
|
|
111
109
|
bindViewPresentation,
|
|
@@ -113,10 +111,12 @@ import {
|
|
|
113
111
|
mergeLayers,
|
|
114
112
|
type PresentationLayer,
|
|
115
113
|
reportViewDraw,
|
|
114
|
+
setViewFraming,
|
|
116
115
|
setViewPresentation,
|
|
117
116
|
stageLightsPerMode,
|
|
118
117
|
startingPresentation,
|
|
119
118
|
subscribeViewportPresentation,
|
|
119
|
+
viewFraming,
|
|
120
120
|
viewPresentation,
|
|
121
121
|
viewPresentationSnapshot,
|
|
122
122
|
type ViewportDrawMode,
|
|
@@ -166,23 +166,6 @@ export type SourceDocumentAuthoringFactory = (
|
|
|
166
166
|
context: SourceDocumentAuthoringContext,
|
|
167
167
|
) => ToolObject3DDocumentAuthoring;
|
|
168
168
|
|
|
169
|
-
/** WHAT THE STAGE IS SHOWING. `build` is one Object3D the caller's `build()`
|
|
170
|
-
* constructs, in isolation; its kind supplies authoring behavior and chrome,
|
|
171
|
-
* and this host owns renderer lifetime, capture and disposal. `world-root` is
|
|
172
|
-
* the project's world: the contributing package's binding mounts the
|
|
173
|
-
* manifest's roots and presents the live roots Play adopts, and this host
|
|
174
|
-
* gives it the same surface place, frame session, per-stage door and
|
|
175
|
-
* overlays. A modeling host never loads a world binding. */
|
|
176
|
-
export type Object3DDocumentContent =
|
|
177
|
-
| { readonly kind: 'build' }
|
|
178
|
-
| { readonly kind: 'world-root'; load(): Promise<WorldRootStageBinding> };
|
|
179
|
-
|
|
180
|
-
/** Throttle for the world-root stage's clip rescan. A traverse per store
|
|
181
|
-
* notification would run on every selection change; 500 ms is fast enough
|
|
182
|
-
* that a streamed-in character becomes scrubbable while a person is still
|
|
183
|
-
* looking at it. */
|
|
184
|
-
const CLIP_RESCAN_INTERVAL_MS = 500;
|
|
185
|
-
|
|
186
169
|
/** Module-level so its identity is stable across renders, which
|
|
187
170
|
* `useSyncExternalStore` requires; the theme root is global, so no stage's
|
|
188
171
|
* own element narrows it. */
|
|
@@ -190,15 +173,7 @@ function subscribeThemeViewportGroup(onChange: () => void): () => void {
|
|
|
190
173
|
return subscribeNativeSelectionTheme(null, onChange);
|
|
191
174
|
}
|
|
192
175
|
|
|
193
|
-
export interface Object3DDocumentViewportProps
|
|
194
|
-
extends Omit<ToolObject3DAuthoringProps, 'build' | 'sourcePath'> {
|
|
195
|
-
/** Required by `build` content (and by the SDK door, where both stay
|
|
196
|
-
* mandatory); a `world-root` stage constructs nothing of its own and has no
|
|
197
|
-
* single source file — its content is the manifest's roots. */
|
|
198
|
-
readonly build?: ToolObject3DAuthoringProps['build'];
|
|
199
|
-
readonly sourcePath?: string;
|
|
200
|
-
/** See {@link Object3DDocumentContent}. Default `{ kind: 'build' }`. */
|
|
201
|
-
readonly content?: Object3DDocumentContent;
|
|
176
|
+
export interface Object3DDocumentViewportProps extends ToolObject3DAuthoringProps {
|
|
202
177
|
/** See `@volter/sdk`'s `ToolObject3DAuthoringProps.audit`. */
|
|
203
178
|
readonly audit?: boolean;
|
|
204
179
|
/** Reuse the canonical Asset Lab viewport with NONE of the document chrome —
|
|
@@ -425,7 +400,7 @@ class Object3DDocumentHost {
|
|
|
425
400
|
|
|
426
401
|
constructor(
|
|
427
402
|
readonly lane: 'own' | 'inspector-preview',
|
|
428
|
-
readonly surface: ReturnType<typeof mountViewportSurface
|
|
403
|
+
readonly surface: ReturnType<typeof mountViewportSurface>,
|
|
429
404
|
store: EditorShellStore,
|
|
430
405
|
) {
|
|
431
406
|
this.store = store;
|
|
@@ -511,7 +486,6 @@ export function Object3DDocumentViewport({
|
|
|
511
486
|
statistics,
|
|
512
487
|
subject,
|
|
513
488
|
gridScale,
|
|
514
|
-
content,
|
|
515
489
|
}: Object3DDocumentViewportProps) {
|
|
516
490
|
const viewStageKind = stageKind ?? stageKindOf(documentId);
|
|
517
491
|
// ANNOUNCE THE STAGE (`authoring/object3d-document-session-registry.ts`).
|
|
@@ -588,16 +562,10 @@ export function Object3DDocumentViewport({
|
|
|
588
562
|
return () => retainedObject3DStages.release(retained, generation);
|
|
589
563
|
}, [documentId]);
|
|
590
564
|
const retainedState = retainedRef.current;
|
|
591
|
-
// WHAT THIS STAGE SHOWS. A world root stays attached while hidden: Play
|
|
592
|
-
// adopts into it, and the play-entry flight rides its frame clock.
|
|
593
|
-
const worldRoot = content?.kind === 'world-root';
|
|
594
565
|
// Only a visible pane owns an interactive attachment. Hidden documents
|
|
595
566
|
// retain their state and return their renderer to the bounded pool.
|
|
596
|
-
const
|
|
597
|
-
|
|
598
|
-
worldRootLoadRef.current = content?.kind === 'world-root' ? content.load : null;
|
|
599
|
-
const [surfaceAttached, setSurfaceAttached] = useState(wantsSurface);
|
|
600
|
-
useEffect(() => setSurfaceAttached(wantsSurface), [wantsSurface]);
|
|
567
|
+
const [surfaceAttached, setSurfaceAttached] = useState(active);
|
|
568
|
+
useEffect(() => setSurfaceAttached(active), [active]);
|
|
601
569
|
// The selection silhouette is a LIVE prop: a modeling document turns it off
|
|
602
570
|
// the moment it enters its own sub-object mode and back on when it leaves,
|
|
603
571
|
// and the session applies that to the composer it has already built. The ref
|
|
@@ -620,7 +588,7 @@ export function Object3DDocumentViewport({
|
|
|
620
588
|
setSurfaceStatus('building');
|
|
621
589
|
return () => {
|
|
622
590
|
const host = documentHostRef.current;
|
|
623
|
-
if (host
|
|
591
|
+
if (host) {
|
|
624
592
|
// Inactive tabs stay open in the workspace. Their source remains
|
|
625
593
|
// mounted in retained CPU state while the expensive attachment goes
|
|
626
594
|
// back to the pool. A source revision or final close still disposes it.
|
|
@@ -640,7 +608,7 @@ export function Object3DDocumentViewport({
|
|
|
640
608
|
host?.close();
|
|
641
609
|
documentHostRef.current = null;
|
|
642
610
|
};
|
|
643
|
-
}, [documentId, rendererLane, retainedState, surfaceAttached
|
|
611
|
+
}, [documentId, rendererLane, retainedState, surfaceAttached]);
|
|
644
612
|
const openingViewRef = useRef(openingView);
|
|
645
613
|
openingViewRef.current = openingView;
|
|
646
614
|
const documentPresentationRef = useRef(documentPresentation);
|
|
@@ -677,10 +645,6 @@ export function Object3DDocumentViewport({
|
|
|
677
645
|
const persistenceSessionRef = useRef<Promise<AssetDocumentPersistenceSession> | null>(null);
|
|
678
646
|
const [error, setError] = useState<string | null>(null);
|
|
679
647
|
const [surfaceStatus, setSurfaceStatus] = useState<'building' | 'ready'>('building');
|
|
680
|
-
// A world root's own mount state and the binding that draws its overlays.
|
|
681
|
-
const [worldRootBinding, setWorldRootBinding] = useState<WorldRootStageBinding | null>(null);
|
|
682
|
-
const [designMountStatus, setDesignMountStatus] = useState<'mounting' | 'ready'>('mounting');
|
|
683
|
-
const [designRootIds, setDesignRootIds] = useState<readonly string[]>([]);
|
|
684
648
|
const [projection, setProjection] = useState<ThreeViewportProjection>('perspective');
|
|
685
649
|
// The live authoring adapter this document's inspection composes from. It
|
|
686
650
|
// only exists once the graph is built, so it is state rather than a ref:
|
|
@@ -707,122 +671,14 @@ export function Object3DDocumentViewport({
|
|
|
707
671
|
useEffect(() => {
|
|
708
672
|
let cancelled = false;
|
|
709
673
|
if (!surfaceAttached) return;
|
|
710
|
-
/**
|
|
711
|
-
* THE WORLD ROOT'S CONTENT BINDING. It shows the project's world rather
|
|
712
|
-
* than one constructed Object3D, so it brings its own surface, scene,
|
|
713
|
-
* composer and the presenter Play adopts through; everything else about
|
|
714
|
-
* the stage is the host's, exactly as it is for a prefab or a model.
|
|
715
|
-
*/
|
|
716
|
-
const installWorldRoot = async (
|
|
717
|
-
load: () => Promise<WorldRootStageBinding>,
|
|
718
|
-
): Promise<void> => {
|
|
719
|
-
const container = containerRef.current;
|
|
720
|
-
const canvasHost = canvasHostRef.current;
|
|
721
|
-
if (!container || !canvasHost || documentHostRef.current) return;
|
|
722
|
-
const stats = shellStatsRef.current;
|
|
723
|
-
const store = threeStoreForHost();
|
|
724
|
-
if (!stats || !store) {
|
|
725
|
-
// The world root IS the shell's own subject — its readout, its
|
|
726
|
-
// adoption stack, its transport. A bounded host has none of that.
|
|
727
|
-
setError('The world root needs the editor shell above it; this host has none.');
|
|
728
|
-
return;
|
|
729
|
-
}
|
|
730
|
-
const binding = await load();
|
|
731
|
-
if (cancelled || documentHostRef.current) return;
|
|
732
|
-
setWorldRootBinding(() => binding);
|
|
733
|
-
markViewportConstructStarted();
|
|
734
|
-
const host = new Object3DDocumentHost(
|
|
735
|
-
'own',
|
|
736
|
-
binding.mountWorldRootSurface(canvasHost, container, displayName),
|
|
737
|
-
// The world is the session's own subject: Play adoption, ingest, the
|
|
738
|
-
// project history and the host door's hierarchy facet all address the
|
|
739
|
-
// SESSION store, so this stage runs on it rather than a private one.
|
|
740
|
-
store,
|
|
741
|
-
);
|
|
742
|
-
documentHostRef.current = host;
|
|
743
|
-
host.cleanups.push(registerStageStore(documentId, host.store.shell));
|
|
744
|
-
host.cleanups.push(registerStageTransport(documentId, host.transport));
|
|
745
|
-
host.cleanups.push(
|
|
746
|
-
registerDocumentViewport(documentId, {
|
|
747
|
-
...sceneDocumentViewport(host.store, documentId),
|
|
748
|
-
...threeStageTransformChrome(documentId),
|
|
749
|
-
}),
|
|
750
|
-
);
|
|
751
|
-
const stage = binding.installWorldRootStage({
|
|
752
|
-
store: host.store,
|
|
753
|
-
documentId,
|
|
754
|
-
container,
|
|
755
|
-
canvas: host.surface.canvas,
|
|
756
|
-
renderer: host.surface.renderer,
|
|
757
|
-
stats,
|
|
758
|
-
onMountStatus: setDesignMountStatus,
|
|
759
|
-
onRootIds: setDesignRootIds,
|
|
760
|
-
runFrame: (delta) => runViewportFrame(documentId, delta),
|
|
761
|
-
});
|
|
762
|
-
host.viewport = stage.viewport;
|
|
763
|
-
host.frame = stage.frame;
|
|
764
|
-
host.content = {
|
|
765
|
-
scene: stage.scene,
|
|
766
|
-
settle: () => Promise.resolve(),
|
|
767
|
-
dispose: async () => stage.dispose(),
|
|
768
|
-
};
|
|
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.
|
|
771
|
-
const clipScan = scanClipSubjects(() => host.store.scene ?? stage.scene, host.transport, {
|
|
772
|
-
// A game makes its mixers as its world runs, not when the store changes.
|
|
773
|
-
rescanOnLiveMixers: true,
|
|
774
|
-
});
|
|
775
|
-
let lastClipScan = 0;
|
|
776
|
-
host.cleanups.push(() => clipScan.dispose());
|
|
777
|
-
host.cleanups.push(
|
|
778
|
-
host.store.shell.subscribe(() => {
|
|
779
|
-
const now = performance.now();
|
|
780
|
-
if (now - lastClipScan < CLIP_RESCAN_INTERVAL_MS) return;
|
|
781
|
-
lastClipScan = now;
|
|
782
|
-
clipScan.refresh();
|
|
783
|
-
}),
|
|
784
|
-
);
|
|
785
|
-
// This stage's presenter is the real one: Play adoption is a condition
|
|
786
|
-
// on the world-root binding, and a `build` stage declines every root.
|
|
787
|
-
host.cleanups.push(
|
|
788
|
-
bindViewportRig(
|
|
789
|
-
{
|
|
790
|
-
camera: stage.viewport.camera,
|
|
791
|
-
drawCamera: () => stage.viewport.renderCamera,
|
|
792
|
-
orbit: stage.viewport.orbitControls,
|
|
793
|
-
projection: stage.viewport.projectionOwner,
|
|
794
|
-
scene: stage.scene,
|
|
795
|
-
renderer: host.surface.renderer,
|
|
796
|
-
},
|
|
797
|
-
stage.setHelper,
|
|
798
|
-
{ documentId },
|
|
799
|
-
),
|
|
800
|
-
);
|
|
801
|
-
rendererSessionRef.current = host.rendererSession;
|
|
802
|
-
host.initialized = true;
|
|
803
|
-
markViewportConstructReady();
|
|
804
|
-
// The binding's own overlay reports mount progress.
|
|
805
|
-
setSurfaceStatus('ready');
|
|
806
|
-
// Always active: the play-entry flight rides this clock while the Game
|
|
807
|
-
// document holds focus; the stage skips frames by its own visibility.
|
|
808
|
-
host.rendererSession.setActive(true);
|
|
809
|
-
};
|
|
810
674
|
const install = async () => {
|
|
811
675
|
await documentHostRef.current?.content?.settle();
|
|
812
676
|
while (liveGestureActive()) await whenLiveGestureIdle();
|
|
813
677
|
if (cancelled) return;
|
|
814
|
-
const loadWorldRoot = worldRootLoadRef.current;
|
|
815
|
-
if (loadWorldRoot) return installWorldRoot(loadWorldRoot);
|
|
816
678
|
const container = containerRef.current;
|
|
817
679
|
const canvasHost = canvasHostRef.current;
|
|
818
680
|
if (!container || !canvasHost) return;
|
|
819
|
-
|
|
820
|
-
// `build` content is one Object3D the caller constructs from one file.
|
|
821
|
-
// Both are required by the SDK; validate callers at the runtime boundary.
|
|
822
|
-
setError('This stage shows `build` content and was given no `build()`/`sourcePath`.');
|
|
823
|
-
return;
|
|
824
|
-
}
|
|
825
|
-
let source: ReturnType<NonNullable<typeof build>>;
|
|
681
|
+
let source: ReturnType<typeof build>;
|
|
826
682
|
const retainedGeneration = retainedObject3DStages.generation(retainedState);
|
|
827
683
|
try {
|
|
828
684
|
source = retainedState.source ?? build();
|
|
@@ -877,6 +733,8 @@ export function Object3DDocumentViewport({
|
|
|
877
733
|
const history = workspaceHistoryService();
|
|
878
734
|
if (history) store.shell.attachHistory(history);
|
|
879
735
|
const shared = rendererLane === 'inspector-preview';
|
|
736
|
+
// Only the document's own view keeps its framing: a preview of it stands where it frames.
|
|
737
|
+
const recordsFraming = !shared && !chromeless;
|
|
880
738
|
const studioStage = studioStageRef.current;
|
|
881
739
|
const hasShell = hasShellRef.current;
|
|
882
740
|
const assetSubject = assetSubjectRef.current;
|
|
@@ -1440,6 +1298,7 @@ export function Object3DDocumentViewport({
|
|
|
1440
1298
|
presentationAdapter?.hierarchy.idForNativeObject?.(object) ?? adapter.hierarchy.idForNativeObject?.(object);
|
|
1441
1299
|
if (id) store.objectMap.set(id, object);
|
|
1442
1300
|
});
|
|
1301
|
+
const opening = !host.session;
|
|
1443
1302
|
if (!host.session) {
|
|
1444
1303
|
// The view's camera (`ViewportCamera`): its field of view, and the direction it opens
|
|
1445
1304
|
// from when the document states none.
|
|
@@ -1528,6 +1387,18 @@ export function Object3DDocumentViewport({
|
|
|
1528
1387
|
// After the pose, which draws in perspective: an orthographic view comes back as one.
|
|
1529
1388
|
viewport.setProjection(projection);
|
|
1530
1389
|
retainedState.camera = null;
|
|
1390
|
+
} else if (opening && recordsFraming) {
|
|
1391
|
+
// THE VIEW'S STATE OVER THE FILE'S: a document that was seen before opens where it was
|
|
1392
|
+
// last seen (`viewFraming`, restored from workspace storage, as Code-OSS restores an
|
|
1393
|
+
// editor's view state), in place of the opening view its file states.
|
|
1394
|
+
const framing = viewFraming(documentId);
|
|
1395
|
+
if (framing) {
|
|
1396
|
+
const [x, y, z] = framing.position;
|
|
1397
|
+
const [tx, ty, tz] = framing.target;
|
|
1398
|
+
const [ux, uy, uz] = framing.up;
|
|
1399
|
+
viewport.setPose({ x, y, z }, { x: tx, y: ty, z: tz }, undefined, { x: ux, y: uy, z: uz });
|
|
1400
|
+
host.session.setProjection(framing.projection);
|
|
1401
|
+
}
|
|
1531
1402
|
}
|
|
1532
1403
|
const documentSession = host.session;
|
|
1533
1404
|
documentStateRef.current = documentState;
|
|
@@ -1578,8 +1449,11 @@ export function Object3DDocumentViewport({
|
|
|
1578
1449
|
if (viewPresentation(documentId).drawMode !== mode)
|
|
1579
1450
|
setViewPresentation(documentId, { drawMode: mode as ViewportDrawMode });
|
|
1580
1451
|
};
|
|
1581
|
-
//
|
|
1582
|
-
|
|
1452
|
+
// The draw mode the view opens in: the view state (the person's last view of this document,
|
|
1453
|
+
// `document-view-states.ts`), else what the document's file states, else the session's default,
|
|
1454
|
+
// as Code-OSS restores an editor's view state over its input's defaults. The view then follows
|
|
1455
|
+
// the session.
|
|
1456
|
+
const restoredMode = viewPresentationSnapshot(documentId).drawMode ?? ownLayer?.drawMode;
|
|
1583
1457
|
if (restoredMode !== undefined && viewModes.has(restoredMode) && host.session)
|
|
1584
1458
|
host.session.setMode(restoredMode);
|
|
1585
1459
|
sessionToView();
|
|
@@ -1718,8 +1592,39 @@ export function Object3DDocumentViewport({
|
|
|
1718
1592
|
}
|
|
1719
1593
|
};
|
|
1720
1594
|
host.syncHostScene = syncHostScene;
|
|
1595
|
+
// WHERE THE VIEW STANDS, recorded as it moves (`setViewFraming`): when the orbit controls
|
|
1596
|
+
// report a move (`OrbitControls.update` dispatches `change` for any move, `setPose`'s
|
|
1597
|
+
// included), when the session changes (its projection), and before every draw. A door can
|
|
1598
|
+
// move the camera with no ordinary draw after it: measured, a pose set through
|
|
1599
|
+
// `editor.present` was not recorded by draws alone; that the page's tab was not on screen
|
|
1600
|
+
// is the likely cause, not measured. A photograph's draw (`captureImage`) leaves the view's
|
|
1601
|
+
// camera where it stands. During a photograph the projection recorded last stands, as a
|
|
1602
|
+
// guard: no caller switches the projection for a photograph today.
|
|
1603
|
+
let recordedFraming = '';
|
|
1604
|
+
let recordedProjection: 'perspective' | 'orthographic' | null = null;
|
|
1605
|
+
const recordFraming = (): void => {
|
|
1606
|
+
if (!recordsFraming) return;
|
|
1607
|
+
const position = viewport.camera.position.toArray();
|
|
1608
|
+
const target = viewport.orbitControls.target.toArray();
|
|
1609
|
+
const up = viewport.camera.up.toArray();
|
|
1610
|
+
const projection =
|
|
1611
|
+
documentSession.photographing() && recordedProjection !== null ? recordedProjection : documentSession.projection();
|
|
1612
|
+
const key = `${position}|${target}|${up}|${projection}`;
|
|
1613
|
+
if (key === recordedFraming) return;
|
|
1614
|
+
recordedFraming = key;
|
|
1615
|
+
recordedProjection = projection;
|
|
1616
|
+
setViewFraming(documentId, { position, target, up, projection });
|
|
1617
|
+
};
|
|
1618
|
+
const controls = viewport.orbitControls;
|
|
1619
|
+
controls.addEventListener('change', recordFraming);
|
|
1620
|
+
const stopSessionFraming = documentSession.subscribe(recordFraming);
|
|
1621
|
+
host.cleanups.push(() => {
|
|
1622
|
+
controls.removeEventListener('change', recordFraming);
|
|
1623
|
+
stopSessionFraming();
|
|
1624
|
+
});
|
|
1721
1625
|
documentSession.setBeforeRender((interactive) => {
|
|
1722
1626
|
host.syncHostScene?.();
|
|
1627
|
+
recordFraming();
|
|
1723
1628
|
const mode = documentSession.presentation().mode;
|
|
1724
1629
|
const navigation = interactive && ['solid', 'preview', 'clay', 'unlit', 'matcap'].includes(mode);
|
|
1725
1630
|
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()});
|
|
@@ -1736,7 +1641,7 @@ export function Object3DDocumentViewport({
|
|
|
1736
1641
|
host.transportAdvances++;
|
|
1737
1642
|
source.update?.(deltaSeconds);
|
|
1738
1643
|
};
|
|
1739
|
-
// Scan this source's animation subjects once.
|
|
1644
|
+
// Scan this source's animation subjects once.
|
|
1740
1645
|
const clipScan = scanClipSubjects(source.root, host.transport);
|
|
1741
1646
|
host.cleanups.push(() => clipScan.dispose());
|
|
1742
1647
|
const retainedTransport = retainedState.transport;
|
|
@@ -1867,8 +1772,8 @@ export function Object3DDocumentViewport({
|
|
|
1867
1772
|
host.presence?.syncMarkers(delta);
|
|
1868
1773
|
viewport.update(delta);
|
|
1869
1774
|
// This stage's frame hook. After viewport.update, the same order the
|
|
1870
|
-
// scene panel uses, so anything riding the loop
|
|
1871
|
-
//
|
|
1775
|
+
// scene panel uses, so anything riding the loop has final say over
|
|
1776
|
+
// the pose and orbit damping never fights it.
|
|
1872
1777
|
if (!chromeless) runViewportFrame(documentId, delta);
|
|
1873
1778
|
if (navigationMeasurement) {
|
|
1874
1779
|
const camera = documentSession.camera();
|
|
@@ -2102,7 +2007,6 @@ export function Object3DDocumentViewport({
|
|
|
2102
2007
|
store.shell.onViewportAction((action) => {
|
|
2103
2008
|
switch (action.type) {
|
|
2104
2009
|
case 'focus-selection':
|
|
2105
|
-
case 'focus-scene':
|
|
2106
2010
|
// The session frames the selection, else the whole subject,
|
|
2107
2011
|
// and says so on the console when there is nothing to frame.
|
|
2108
2012
|
if (!host.session?.frame() && host.session?.root)
|
|
@@ -2251,7 +2155,6 @@ export function Object3DDocumentViewport({
|
|
|
2251
2155
|
surfaceAttached,
|
|
2252
2156
|
sourceAuthoring,
|
|
2253
2157
|
sourcePath,
|
|
2254
|
-
worldRoot,
|
|
2255
2158
|
]);
|
|
2256
2159
|
|
|
2257
2160
|
useEffect(() => {
|
|
@@ -2263,11 +2166,10 @@ export function Object3DDocumentViewport({
|
|
|
2263
2166
|
firstFrameGateRef.current = false;
|
|
2264
2167
|
markViewportRafResume(documentId);
|
|
2265
2168
|
}
|
|
2266
|
-
|
|
2267
|
-
session.setActive(active || worldRoot);
|
|
2169
|
+
session.setActive(active);
|
|
2268
2170
|
// A hidden stage keeps no live accumulation (its targets are the drawing buffer's size).
|
|
2269
2171
|
if (!active) documentHostRef.current?.session?.releaseLive();
|
|
2270
|
-
}, [active, documentId
|
|
2172
|
+
}, [active, documentId]);
|
|
2271
2173
|
|
|
2272
2174
|
// The stage's own handle for the capabilities that drive it directly (the
|
|
2273
2175
|
// viewport hotkeys). `surfaceStatus` is what re-renders once the stage has
|
|
@@ -2336,44 +2238,12 @@ export function Object3DDocumentViewport({
|
|
|
2336
2238
|
chrome="document"
|
|
2337
2239
|
/>
|
|
2338
2240
|
) : null}
|
|
2339
|
-
{shellStore && worldRootBinding ? (
|
|
2340
|
-
<worldRootBinding.Overlays
|
|
2341
|
-
store={shellStore}
|
|
2342
|
-
documentId={documentId}
|
|
2343
|
-
mountStatus={designMountStatus}
|
|
2344
|
-
rootIds={designRootIds}
|
|
2345
|
-
/>
|
|
2346
|
-
) : null}
|
|
2347
2241
|
{/* Show a dismissible error notice over any stage that remains available. */}
|
|
2348
2242
|
{error && (
|
|
2349
|
-
<
|
|
2350
|
-
|
|
2351
|
-
style={{
|
|
2352
|
-
|
|
2353
|
-
insetInline: 0,
|
|
2354
|
-
top: 0,
|
|
2355
|
-
display: 'flex',
|
|
2356
|
-
justifyContent: 'center',
|
|
2357
|
-
padding: 'var(--volter-space-3)',
|
|
2358
|
-
pointerEvents: 'none',
|
|
2359
|
-
}}
|
|
2360
|
-
>
|
|
2361
|
-
<div
|
|
2362
|
-
className="volter-chrome-island"
|
|
2363
|
-
style={{
|
|
2364
|
-
display: 'flex',
|
|
2365
|
-
alignItems: 'center',
|
|
2366
|
-
gap: 'var(--volter-space-3)',
|
|
2367
|
-
maxWidth: '80%',
|
|
2368
|
-
padding: 'var(--volter-space-2) var(--volter-space-3)',
|
|
2369
|
-
borderRadius: 'var(--volter-radius-md)',
|
|
2370
|
-
border: `var(--volter-stroke-resting) solid ${themeVars.semantic.danger}`,
|
|
2371
|
-
background: themeVars.surface.raised,
|
|
2372
|
-
color: themeVars.semantic.danger,
|
|
2373
|
-
pointerEvents: 'auto',
|
|
2374
|
-
}}
|
|
2375
|
-
>
|
|
2376
|
-
<span>{error}</span>
|
|
2243
|
+
<EditorBanner
|
|
2244
|
+
tone="error"
|
|
2245
|
+
style={{ position: 'absolute', insetInline: 0, top: 0 }}
|
|
2246
|
+
actions={
|
|
2377
2247
|
<IconButton
|
|
2378
2248
|
size="compact"
|
|
2379
2249
|
aria-label="Dismiss"
|
|
@@ -2382,13 +2252,13 @@ export function Object3DDocumentViewport({
|
|
|
2382
2252
|
>
|
|
2383
2253
|
<EditorIcon icon={editorIcons.action.close} />
|
|
2384
2254
|
</IconButton>
|
|
2385
|
-
|
|
2386
|
-
|
|
2255
|
+
}
|
|
2256
|
+
>
|
|
2257
|
+
{error}
|
|
2258
|
+
</EditorBanner>
|
|
2387
2259
|
)}
|
|
2388
|
-
{!error && surfaceStatus === 'building' && (
|
|
2389
|
-
<
|
|
2390
|
-
{OBJECT3D_SURFACE_BUILDING}
|
|
2391
|
-
</ViewportSurfaceStatus>
|
|
2260
|
+
{!error && !chromeless && surfaceStatus === 'building' && (
|
|
2261
|
+
<Progress documentId={documentId} title="Preparing the viewport" />
|
|
2392
2262
|
)}
|
|
2393
2263
|
{/* Inset furniture by the canvas bleed so chrome stays inside the visible panel. */}
|
|
2394
2264
|
<div
|
|
@@ -27,7 +27,7 @@ const ZERO = () => 0;
|
|
|
27
27
|
|
|
28
28
|
/**
|
|
29
29
|
* Editor-camera projection and axis presets for the stage’s own shell. A documentId targets its
|
|
30
|
-
* Object3DDocumentSession;
|
|
30
|
+
* Object3DDocumentSession; without one, the editor-wide projection. label shows the view
|
|
31
31
|
* name instead of a camera icon, and kebab prefixes the label with three dots.
|
|
32
32
|
*/
|
|
33
33
|
export function ViewportViewMenu({
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { threeStateOf } from '../../three-state';
|
|
2
|
-
import {
|
|
2
|
+
import { StateSurface } from '@volter/sdk/widgets';
|
|
3
3
|
import { liveMixerFor } from '@volter/editor-threejs/animation/live-mixers';
|
|
4
4
|
import { useCallback, useSyncExternalStore } from 'react';
|
|
5
5
|
import type * as THREE from 'three';
|
|
@@ -94,9 +94,7 @@ export function EntityModelDocument({
|
|
|
94
94
|
|
|
95
95
|
if (!source)
|
|
96
96
|
return (
|
|
97
|
-
<
|
|
98
|
-
Scene entity {entityId} is no longer available.
|
|
99
|
-
</div>
|
|
97
|
+
<StateSurface tone="error" title={`Scene entity ${entityId} is no longer available`} />
|
|
100
98
|
);
|
|
101
99
|
return (
|
|
102
100
|
<Object3DSnapshotDocument
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { editorHost } from '@volter/sdk/host';
|
|
2
2
|
import { faCloudSun, faSliders } from '@fortawesome/free-solid-svg-icons';
|
|
3
|
-
import { NumberInput, themeVars } from '@volter/sdk/widgets';
|
|
3
|
+
import { NumberInput, StateSurface, themeVars } from '@volter/sdk/widgets';
|
|
4
|
+
import { Progress } from '@volter/sdk/progress';
|
|
4
5
|
import { useEffect, useMemo, useRef, useState } from 'react';
|
|
5
6
|
import * as THREE from 'three';
|
|
6
7
|
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
|
|
@@ -386,9 +387,7 @@ export function EnvironmentAssetDocument({
|
|
|
386
387
|
title={title}
|
|
387
388
|
status="HDR image unavailable"
|
|
388
389
|
>
|
|
389
|
-
<
|
|
390
|
-
{loadError}
|
|
391
|
-
</div>
|
|
390
|
+
<StateSurface tone="error" title={`Could not load ${title}`} detail={loadError} />
|
|
392
391
|
</AssetEditorShell>
|
|
393
392
|
);
|
|
394
393
|
}
|
|
@@ -400,9 +399,8 @@ export function EnvironmentAssetDocument({
|
|
|
400
399
|
active={active}
|
|
401
400
|
type="environment image"
|
|
402
401
|
title={title}
|
|
403
|
-
status="Loading HDR image"
|
|
404
402
|
>
|
|
405
|
-
<
|
|
403
|
+
<Progress documentId={documentId} title={`Loading ${title}`} />
|
|
406
404
|
</AssetEditorShell>
|
|
407
405
|
);
|
|
408
406
|
}
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
13
|
import { beginLiveModuleRevision } from '@volter/sdk/project-module-url';
|
|
14
|
-
import { themeVars } from '@volter/sdk/widgets';
|
|
14
|
+
import { EditorBanner, themeVars } from '@volter/sdk/widgets';
|
|
15
15
|
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
16
16
|
import * as THREE from 'three';
|
|
17
17
|
import { disposeProjectAssetModel } from '../../asset-preview';
|
|
@@ -281,37 +281,13 @@ export function LiveModuleDocument({
|
|
|
281
281
|
* and never a toast — the document itself has to say why it is stale. */
|
|
282
282
|
function BuildFailureBanner({ failure }: { readonly failure: BuildFailure }) {
|
|
283
283
|
return (
|
|
284
|
-
<
|
|
285
|
-
|
|
284
|
+
<EditorBanner
|
|
285
|
+
tone="error"
|
|
286
286
|
data-testid="live-module-error"
|
|
287
|
-
style={{
|
|
288
|
-
|
|
289
|
-
top: 8,
|
|
290
|
-
left: 8,
|
|
291
|
-
right: 8,
|
|
292
|
-
zIndex: 2,
|
|
293
|
-
maxHeight: '60%',
|
|
294
|
-
overflow: 'auto',
|
|
295
|
-
padding: '8px 10px',
|
|
296
|
-
borderRadius: themeVars.shape.small,
|
|
297
|
-
background: themeVars.surface.raised,
|
|
298
|
-
border: `1px solid ${themeVars.semantic.danger}`,
|
|
299
|
-
color: themeVars.semantic.danger,
|
|
300
|
-
fontFamily: themeVars.typography.mono,
|
|
301
|
-
fontSize: 11,
|
|
302
|
-
lineHeight: 1.5,
|
|
303
|
-
whiteSpace: 'pre-wrap',
|
|
304
|
-
wordBreak: 'break-word',
|
|
305
|
-
pointerEvents: 'auto',
|
|
306
|
-
}}
|
|
287
|
+
style={{ position: 'absolute', top: 0, left: 0, right: 0, zIndex: 2 }}
|
|
288
|
+
detail={failure.stack ? `${failure.message}\n${failure.stack}` : failure.message}
|
|
307
289
|
>
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
</div>
|
|
311
|
-
<div style={{ marginTop: 4 }}>{failure.message}</div>
|
|
312
|
-
{failure.stack ? (
|
|
313
|
-
<div style={{ marginTop: 6, color: themeVars.content.muted }}>{failure.stack}</div>
|
|
314
|
-
) : null}
|
|
315
|
-
</div>
|
|
290
|
+
This module did not build — showing the last good model.
|
|
291
|
+
</EditorBanner>
|
|
316
292
|
);
|
|
317
293
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { editorHost } from '@volter/sdk/host';
|
|
2
2
|
import { faCircleHalfStroke, faSliders } from '@fortawesome/free-solid-svg-icons';
|
|
3
|
-
import { Checkbox, NumberInput, themeVars } from '@volter/sdk/widgets';
|
|
3
|
+
import { Checkbox, NumberInput, StateSurface, themeVars } from '@volter/sdk/widgets';
|
|
4
|
+
import { Progress } from '@volter/sdk/progress';
|
|
4
5
|
import { EffectComposer, EffectPass, LUT3DEffect, RenderPass } from 'postprocessing';
|
|
5
6
|
import { useEffect, useMemo, useRef, useState } from 'react';
|
|
6
7
|
import * as THREE from 'three';
|
|
@@ -367,9 +368,7 @@ export function LutAssetDocument({
|
|
|
367
368
|
title={basename(assetPath)}
|
|
368
369
|
status="CUBE LUT unavailable"
|
|
369
370
|
>
|
|
370
|
-
<
|
|
371
|
-
{loadError}
|
|
372
|
-
</div>
|
|
371
|
+
<StateSurface tone="error" title={`Could not load ${basename(assetPath)}`} detail={loadError} />
|
|
373
372
|
</AssetEditorShell>
|
|
374
373
|
);
|
|
375
374
|
}
|
|
@@ -381,9 +380,8 @@ export function LutAssetDocument({
|
|
|
381
380
|
active={active}
|
|
382
381
|
type="color grade"
|
|
383
382
|
title={basename(assetPath)}
|
|
384
|
-
status="Loading CUBE LUT"
|
|
385
383
|
>
|
|
386
|
-
<
|
|
384
|
+
<Progress documentId={documentId} title={`Loading ${basename(assetPath)}`} />
|
|
387
385
|
</AssetEditorShell>
|
|
388
386
|
);
|
|
389
387
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { StateSurface } from '@volter/sdk/widgets';
|
|
2
|
+
import { Progress } from '@volter/sdk/progress';
|
|
2
3
|
import { useCallback, useEffect, useState } from 'react';
|
|
3
4
|
import type * as THREE from 'three';
|
|
4
5
|
import {
|
|
@@ -83,14 +84,9 @@ export function ModelAssetDocument({
|
|
|
83
84
|
}, [assetPath, model]);
|
|
84
85
|
if (error)
|
|
85
86
|
return (
|
|
86
|
-
<
|
|
87
|
-
{error}
|
|
88
|
-
</div>
|
|
89
|
-
);
|
|
90
|
-
if (!model)
|
|
91
|
-
return (
|
|
92
|
-
<div style={{ padding: 20, color: themeVars.content.muted }}>Loading {displayName}…</div>
|
|
87
|
+
<StateSurface tone="error" title={`Could not load ${displayName}`} detail={error} />
|
|
93
88
|
);
|
|
89
|
+
if (!model) return <Progress documentId={documentId} title={`Loading ${displayName}`} />;
|
|
94
90
|
return (
|
|
95
91
|
<Object3DDocumentViewport
|
|
96
92
|
documentId={documentId}
|