@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
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { ToolObject3DPreviewContext, ToolObject3DPreviewExtension } from '../../../object3d-contributions';
|
|
2
|
-
import { themeVars } from '@volter/sdk/widgets';
|
|
2
|
+
import { StateSurface, themeVars } from '@volter/sdk/widgets';
|
|
3
3
|
import { useEffect, useRef, useState } from 'react';
|
|
4
4
|
import * as THREE from 'three';
|
|
5
5
|
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
|
|
@@ -294,24 +294,8 @@ export function Object3DPreview({
|
|
|
294
294
|
background: themeVars.surface.inset,
|
|
295
295
|
}}
|
|
296
296
|
>
|
|
297
|
-
{
|
|
298
|
-
|
|
299
|
-
Loading…
|
|
300
|
-
</span>
|
|
301
|
-
) : null}
|
|
302
|
-
{!active ? (
|
|
303
|
-
<span style={{ position: 'absolute', padding: 10, color: themeVars.content.muted }}>
|
|
304
|
-
Preview suspended while inactive.
|
|
305
|
-
</span>
|
|
306
|
-
) : null}
|
|
307
|
-
{error ? (
|
|
308
|
-
<div
|
|
309
|
-
role="alert"
|
|
310
|
-
style={{ position: 'absolute', padding: 10, color: themeVars.semantic.danger }}
|
|
311
|
-
>
|
|
312
|
-
{error}
|
|
313
|
-
</div>
|
|
314
|
-
) : null}
|
|
297
|
+
{!active ? <StateSurface fill compact title="Preview suspended while inactive." /> : null}
|
|
298
|
+
{error ? <StateSurface fill compact tone="error" title={`${displayName} could not be previewed`} detail={error} /> : null}
|
|
315
299
|
</div>
|
|
316
300
|
);
|
|
317
301
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { ToolObject3DDocumentAuthoringFactory } from '../../../object3d-contributions';
|
|
2
|
-
import {
|
|
2
|
+
import { StateSurface } from '@volter/sdk/widgets';
|
|
3
|
+
import { Progress } from '@volter/sdk/progress';
|
|
3
4
|
import type {
|
|
4
5
|
AuthoringAdapter,
|
|
5
6
|
InspectorProvider,
|
|
@@ -503,14 +504,9 @@ export function QuarksAssetDocument({
|
|
|
503
504
|
|
|
504
505
|
if (error)
|
|
505
506
|
return (
|
|
506
|
-
<
|
|
507
|
-
{error}
|
|
508
|
-
</div>
|
|
509
|
-
);
|
|
510
|
-
if (!artifact)
|
|
511
|
-
return (
|
|
512
|
-
<div style={{ padding: 20, color: themeVars.content.muted }}>Loading {displayName}…</div>
|
|
507
|
+
<StateSurface tone="error" title={`Could not load ${displayName}`} detail={error} />
|
|
513
508
|
);
|
|
509
|
+
if (!artifact) return <Progress documentId={documentId} title={`Loading ${displayName}`} />;
|
|
514
510
|
return (
|
|
515
511
|
<Object3DDocumentViewport
|
|
516
512
|
documentId={documentId}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { editorHost } from '@volter/sdk/host';
|
|
2
2
|
import { faCode, faSliders, faTriangleExclamation } from '@fortawesome/free-solid-svg-icons';
|
|
3
|
-
import { Checkbox, ColorInput, NumberInput, Select, themeVars } from '@volter/sdk/widgets';
|
|
3
|
+
import { Checkbox, ColorInput, EditorBanner, NumberInput, Select, spaceVar, StateSurface, Text, 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 { readProjectTextFile } from '@volter/sdk/api/project-source';
|
|
@@ -449,6 +450,14 @@ function UniformControlRow({
|
|
|
449
450
|
);
|
|
450
451
|
}
|
|
451
452
|
|
|
453
|
+
/** One diagnostic as a line: where (the file, its line and column) and what. */
|
|
454
|
+
function diagnosticLine(diagnostic: ShaderDiagnostic, files: ShaderFiles): string {
|
|
455
|
+
const file = diagnostic.stage === 'program' ? null : files[diagnostic.stage];
|
|
456
|
+
const where = file?.available ? basename(file.path) : diagnostic.stage;
|
|
457
|
+
const at = diagnostic.line ? `:${diagnostic.line}${diagnostic.column ? `:${diagnostic.column}` : ''}` : '';
|
|
458
|
+
return `${where}${at} · ${diagnostic.message}`;
|
|
459
|
+
}
|
|
460
|
+
|
|
452
461
|
function DiagnosticList({
|
|
453
462
|
diagnostics,
|
|
454
463
|
files,
|
|
@@ -457,37 +466,21 @@ function DiagnosticList({
|
|
|
457
466
|
files: ShaderFiles;
|
|
458
467
|
}) {
|
|
459
468
|
if (diagnostics.length === 0) {
|
|
460
|
-
return
|
|
461
|
-
<div style={{ color: themeVars.semantic.success, fontSize: 11, padding: 4 }}>
|
|
462
|
-
Shader program compiled successfully.
|
|
463
|
-
</div>
|
|
464
|
-
);
|
|
469
|
+
return <StateSurface inline tone="success" title="Shader program compiled successfully." />;
|
|
465
470
|
}
|
|
466
471
|
return (
|
|
467
|
-
<div style={{ display: 'flex', flexDirection: 'column', gap:
|
|
468
|
-
{diagnostics.map((diagnostic, index) =>
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
lineHeight: 1.45,
|
|
480
|
-
}}
|
|
481
|
-
>
|
|
482
|
-
{file?.available ? basename(file.path) : diagnostic.stage}
|
|
483
|
-
{diagnostic.line
|
|
484
|
-
? `:${diagnostic.line}${diagnostic.column ? `:${diagnostic.column}` : ''}`
|
|
485
|
-
: ''}
|
|
486
|
-
{' · '}
|
|
487
|
-
{diagnostic.message}
|
|
488
|
-
</div>
|
|
489
|
-
);
|
|
490
|
-
})}
|
|
472
|
+
<div style={{ display: 'flex', flexDirection: 'column', gap: spaceVar[2] }}>
|
|
473
|
+
{diagnostics.map((diagnostic, index) => (
|
|
474
|
+
<Text
|
|
475
|
+
key={`${diagnostic.stage}:${diagnostic.line ?? 0}:${diagnostic.message}:${index}`}
|
|
476
|
+
as="div"
|
|
477
|
+
variant="code"
|
|
478
|
+
selectable
|
|
479
|
+
tone={diagnostic.severity === 'warning' ? 'warning' : 'danger'}
|
|
480
|
+
>
|
|
481
|
+
{diagnosticLine(diagnostic, files)}
|
|
482
|
+
</Text>
|
|
483
|
+
))}
|
|
491
484
|
</div>
|
|
492
485
|
);
|
|
493
486
|
}
|
|
@@ -589,17 +582,18 @@ export function ShaderAssetDocument({
|
|
|
589
582
|
order: PROPERTIES_SECTION_ORDER,
|
|
590
583
|
body: {
|
|
591
584
|
kind: 'custom',
|
|
592
|
-
|
|
585
|
+
// (shown once the files are read: the document passes its sections only then)
|
|
586
|
+
render: () => files && (
|
|
593
587
|
<div
|
|
594
588
|
style={{ display: 'flex', flexDirection: 'column', gap: 5, padding: 4, fontSize: 10 }}
|
|
595
589
|
>
|
|
596
590
|
<span>
|
|
597
|
-
Vertex · {
|
|
598
|
-
{
|
|
591
|
+
Vertex · {basename(files.vertex.path)}
|
|
592
|
+
{!files.vertex.available ? ' (preview fallback)' : ''}
|
|
599
593
|
</span>
|
|
600
594
|
<span>
|
|
601
|
-
Fragment · {
|
|
602
|
-
{
|
|
595
|
+
Fragment · {basename(files.fragment.path)}
|
|
596
|
+
{!files.fragment.available ? ' (preview fallback)' : ''}
|
|
603
597
|
</span>
|
|
604
598
|
<span style={{ color: themeVars.content.muted }}>
|
|
605
599
|
Ordinary sibling GLSL files; runtime composition stays in TS/TSX.
|
|
@@ -658,9 +652,7 @@ export function ShaderAssetDocument({
|
|
|
658
652
|
title={basename(assetPath)}
|
|
659
653
|
status="Shader unavailable"
|
|
660
654
|
>
|
|
661
|
-
<
|
|
662
|
-
{loadError}
|
|
663
|
-
</div>
|
|
655
|
+
<StateSurface tone="error" title={`Could not load ${basename(assetPath)}`} detail={loadError} />
|
|
664
656
|
</AssetEditorShell>
|
|
665
657
|
);
|
|
666
658
|
}
|
|
@@ -671,9 +663,8 @@ export function ShaderAssetDocument({
|
|
|
671
663
|
active={active}
|
|
672
664
|
type="shader"
|
|
673
665
|
title={basename(assetPath)}
|
|
674
|
-
status="Loading GLSL"
|
|
675
666
|
>
|
|
676
|
-
<
|
|
667
|
+
<Progress documentId={documentId} title={`Loading ${basename(assetPath)}`} />
|
|
677
668
|
</AssetEditorShell>
|
|
678
669
|
);
|
|
679
670
|
}
|
|
@@ -721,22 +712,13 @@ export function ShaderAssetDocument({
|
|
|
721
712
|
</Select>
|
|
722
713
|
</div>
|
|
723
714
|
{diagnostics.length > 0 && (
|
|
724
|
-
<
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
right: 12,
|
|
729
|
-
bottom: 12,
|
|
730
|
-
maxHeight: 120,
|
|
731
|
-
overflow: 'auto',
|
|
732
|
-
padding: 8,
|
|
733
|
-
border: `1px solid ${themeVars.semantic.danger}`,
|
|
734
|
-
borderRadius: 6,
|
|
735
|
-
background: themeVars.surface.overlay,
|
|
736
|
-
}}
|
|
715
|
+
<EditorBanner
|
|
716
|
+
tone={diagnostics.some((diagnostic) => diagnostic.severity !== 'warning') ? 'error' : 'warning'}
|
|
717
|
+
style={{ position: 'absolute', left: 0, right: 0, bottom: 0 }}
|
|
718
|
+
detail={diagnostics.map((diagnostic) => diagnosticLine(diagnostic, files)).join('\n')}
|
|
737
719
|
>
|
|
738
|
-
|
|
739
|
-
</
|
|
720
|
+
{`${diagnostics.length} issue${diagnostics.length === 1 ? '' : 's'}`}
|
|
721
|
+
</EditorBanner>
|
|
740
722
|
)}
|
|
741
723
|
</div>
|
|
742
724
|
</AssetEditorShell>
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { announceDocumentStage } from '@volter/sdk/document-viewports';
|
|
2
|
+
import { Progress } from '@volter/sdk/progress';
|
|
2
3
|
import { lazy, Suspense, useEffect } from 'react';
|
|
3
4
|
import type { ToolObject3DAuthoringProps, ToolObject3DPreviewProps } from '../../object3d-contributions';
|
|
4
5
|
|
|
@@ -12,7 +13,8 @@ const Authoring = lazy(async () => ({ default: (await import('./StageHost')).Too
|
|
|
12
13
|
|
|
13
14
|
export function Object3DPreviewSurface(props: ToolObject3DPreviewProps) {
|
|
14
15
|
return (
|
|
15
|
-
|
|
16
|
+
// (a preview card reports no wait: it is a card in a panel or a catalog, neither a document nor a view)
|
|
17
|
+
<Suspense fallback={null}>
|
|
16
18
|
<Preview {...props} />
|
|
17
19
|
</Suspense>
|
|
18
20
|
);
|
|
@@ -26,7 +28,7 @@ export function Object3DAuthoringSurface(props: ToolObject3DAuthoringProps) {
|
|
|
26
28
|
// `workspace.document` contributions do not, and which is which belongs to the PROJECT.
|
|
27
29
|
useEffect(() => announceDocumentStage(props.documentId), [props.documentId]);
|
|
28
30
|
return (
|
|
29
|
-
<Suspense fallback={<
|
|
31
|
+
<Suspense fallback={<Progress documentId={props.documentId} title="Loading 3D authoring surface" />}>
|
|
30
32
|
<Authoring {...props} />
|
|
31
33
|
</Suspense>
|
|
32
34
|
);
|
|
@@ -3,8 +3,7 @@
|
|
|
3
3
|
* document's chrome (`@volter/sdk/document-viewports`): what the
|
|
4
4
|
* host's transform tools drive on it (`stage-context.ts`'s one answer), the tool
|
|
5
5
|
* strip on its shelf and the gizmo's header wells. Every tool writes THIS
|
|
6
|
-
* stage's own store (`stage-store-registry.ts`)
|
|
7
|
-
* the session store, so the Scene document is unchanged either way.
|
|
6
|
+
* stage's own store (`stage-store-registry.ts`).
|
|
8
7
|
*/
|
|
9
8
|
import { type ViewportInteraction, viewPresentation } from '@volter/sdk/viewport-presentation';
|
|
10
9
|
import { useEffect, useSyncExternalStore } from 'react';
|
|
@@ -99,35 +99,11 @@ export class EditorShellStore {
|
|
|
99
99
|
this._statePersistence = persistence;
|
|
100
100
|
}
|
|
101
101
|
|
|
102
|
-
/** The game camera lookup the last `focusOnScene` carried, read by the world stage while its
|
|
103
|
-
* auto-frame window is open. */
|
|
104
|
-
get sceneCameraLookup(): (() => THREE.Camera | null) | null {
|
|
105
|
-
return this._sceneCameraLookup;
|
|
106
|
-
}
|
|
107
|
-
protected _sceneCameraLookup: (() => THREE.Camera | null) | null = null;
|
|
108
|
-
|
|
109
|
-
/**
|
|
110
|
-
* Frame the whole active scene — the first thing a reader should see when a
|
|
111
|
-
* world they did not author opens. The viewport keeps
|
|
112
|
-
* asking until the world HAS content, because a game builds itself
|
|
113
|
-
* asynchronously; any camera input from the reader cancels it (see
|
|
114
|
-
* `the world root's stage`). A no-op for a scene the reader already has a camera on.
|
|
115
|
-
*
|
|
116
|
-
* `gameCamera` is a LIVE LOOKUP, not a camera: a game's own camera
|
|
117
|
-
* may not exist yet on the frame it is captured on, so the viewport asks
|
|
118
|
-
* again while its window is open. When one turns up, its view is adopted
|
|
119
|
-
* instead of any measurement — see `scene-framing.ts`.
|
|
120
|
-
*/
|
|
121
|
-
focusOnScene(gameCamera?: () => THREE.Camera | null): void {
|
|
122
|
-
this._sceneCameraLookup = gameCamera ?? null;
|
|
123
|
-
this.shell.focusOnScene();
|
|
124
|
-
}
|
|
125
|
-
|
|
126
102
|
/**
|
|
127
103
|
* Live viewport camera pose, read straight from the bound Three.js camera +
|
|
128
104
|
* orbit target (`bindScene`/`setOrbitTarget`).
|
|
129
105
|
* Null only when nothing has bound a camera yet (e.g. a headless store in a
|
|
130
|
-
* unit test that never mounted
|
|
106
|
+
* unit test that never mounted a stage).
|
|
131
107
|
*/
|
|
132
108
|
get cameraPose(): { position: THREE.Vector3; target: THREE.Vector3; fov: number } | null {
|
|
133
109
|
if (!this._camera) return null;
|
|
@@ -326,7 +302,7 @@ export class EditorShellStore {
|
|
|
326
302
|
* Fresh, on-demand viewport capture (unthrottled, unlike the periodic
|
|
327
303
|
* autosave thumbnail above) — backs the `capture-viewport` relay command.
|
|
328
304
|
* Returns null when no renderer/scene/camera is bound yet (e.g. before
|
|
329
|
-
*
|
|
305
|
+
* a stage has mounted).
|
|
330
306
|
*/
|
|
331
307
|
captureViewportImage(
|
|
332
308
|
size: number | { width: number; height: number } = EditorShellStore.THUMBNAIL_SIZE,
|
|
@@ -477,6 +477,14 @@ const _gridQuat = new THREE.Quaternion();
|
|
|
477
477
|
const _gridEye = new THREE.Vector3();
|
|
478
478
|
const _gridUp = new THREE.Vector3(0, 1, 0);
|
|
479
479
|
|
|
480
|
+
/**
|
|
481
|
+
* What the floor's overlays (its grid and its axes) are drawn with: a surface in their own plane, a level's ground at
|
|
482
|
+
* 0, shows them on it, as Blender's grid passes its depth test on a tie (`DRW_STATE_DEPTH_LESS_EQUAL`,
|
|
483
|
+
* overlay_grid.hh). three's depth test passes a tie too; the offset keeps it where the two surfaces' depths round
|
|
484
|
+
* apart, which would split it pixel by pixel.
|
|
485
|
+
*/
|
|
486
|
+
const ON_THE_FLOOR = { polygonOffset: true, polygonOffsetFactor: -1, polygonOffsetUnits: -1 } as const;
|
|
487
|
+
|
|
480
488
|
/**
|
|
481
489
|
* The floor grid as a shader over one plane (`extent` metres square, y = 0).
|
|
482
490
|
* Lines are `fwidth`-antialiased so they keep one on-screen width at every
|
|
@@ -604,6 +612,7 @@ function createFloorGrid(extent: number): THREE.Mesh<THREE.PlaneGeometry, THREE.
|
|
|
604
612
|
transparent: true,
|
|
605
613
|
depthWrite: false,
|
|
606
614
|
side: THREE.DoubleSide,
|
|
615
|
+
...ON_THE_FLOOR,
|
|
607
616
|
});
|
|
608
617
|
const mesh = new THREE.Mesh(geometry, material);
|
|
609
618
|
mesh.name = 'editor-floor-grid';
|
|
@@ -1000,6 +1009,7 @@ export class EditorViewport {
|
|
|
1000
1009
|
opacity: 1,
|
|
1001
1010
|
depthWrite: false,
|
|
1002
1011
|
toneMapped: false,
|
|
1012
|
+
...ON_THE_FLOOR,
|
|
1003
1013
|
}),
|
|
1004
1014
|
);
|
|
1005
1015
|
// Share the floor's grazing fade. LineMaterial interpolates the segment endpoints with
|
|
@@ -1422,7 +1432,7 @@ export class EditorViewport {
|
|
|
1422
1432
|
return this._scene;
|
|
1423
1433
|
}
|
|
1424
1434
|
|
|
1425
|
-
/** Camera that paints the
|
|
1435
|
+
/** Camera that paints the stage: the free editor camera. */
|
|
1426
1436
|
get renderCamera(): THREE.Camera {
|
|
1427
1437
|
return this.freeCamera;
|
|
1428
1438
|
}
|
|
@@ -22,7 +22,6 @@ import { editorHost } from '@volter/sdk/host';
|
|
|
22
22
|
import { onShellStore } from '@volter/sdk/shell-store-door';
|
|
23
23
|
import { registerThreeCanvasRender } from './three-canvas-render';
|
|
24
24
|
import { registerThreeHierarchyRowMedia } from './three-hierarchy-row-media';
|
|
25
|
-
import { registerThreePlayCameraFlight } from './play-camera-flight';
|
|
26
25
|
import { registerThreeInspectionMedia } from './three-inspection-media';
|
|
27
26
|
import { threeStateOf } from './three-state';
|
|
28
27
|
import { installViewportAuthoringPolicy } from './viewport-authoring-policy';
|
|
@@ -68,7 +67,6 @@ export function ensureThreeIntegration(): () => void {
|
|
|
68
67
|
reportViewportStatus(),
|
|
69
68
|
registerThreeCanvasRender(),
|
|
70
69
|
registerThreeHierarchyRowMedia(),
|
|
71
|
-
registerThreePlayCameraFlight(),
|
|
72
70
|
registerRendererResourceCounts('hostLive', liveHostRendererCount),
|
|
73
71
|
registerRendererResourceCounts('interactive', interactiveViewportRendererCounts),
|
|
74
72
|
registerRendererResourceCounts('inspectorPreview', inspectorPreviewRendererCounts),
|
package/src/viewport-api.ts
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* THE AUTHORED THREE VIEWPORT'S PUBLIC SHAPE — what a lane that mounts something reaches through
|
|
3
|
-
* `viewport-door.ts`:
|
|
4
|
-
* shows and every mounted stage.
|
|
3
|
+
* `viewport-door.ts`: every mounted stage, its rig, its frame loop and the helpers it shows.
|
|
5
4
|
*/
|
|
6
5
|
import type * as THREE from 'three';
|
|
7
6
|
import type { StageFrameCostReading } from '@volter/sdk';
|
|
@@ -35,9 +34,7 @@ export interface ViewportRig {
|
|
|
35
34
|
|
|
36
35
|
/**
|
|
37
36
|
* ONE MOUNTED 3D STAGE, named by the document it draws. Every 3D document
|
|
38
|
-
* mounts a stage of its own
|
|
39
|
-
* to a particular one, where the viewport door's singular members
|
|
40
|
-
* are the door to whichever is primary.
|
|
37
|
+
* mounts a stage of its own; this is the door to a particular one.
|
|
41
38
|
*/
|
|
42
39
|
export interface ViewportStage {
|
|
43
40
|
/** The workspace document this stage draws. */
|
|
@@ -50,14 +47,8 @@ export interface ViewportStage {
|
|
|
50
47
|
}
|
|
51
48
|
|
|
52
49
|
/**
|
|
53
|
-
* THE AUTHORED VIEWPORT, as a lane that mounts something reaches it
|
|
54
|
-
*
|
|
55
|
-
* viewport renders is the host's; a lane that finds no rig (a headless tab, a
|
|
56
|
-
* project with no Three root) mounts without one.
|
|
57
|
-
*
|
|
58
|
-
* THE SINGULAR MEMBERS ARE THE PRIMARY STAGE — the FOCUSED stage, else the
|
|
59
|
-
* first bound. A camera flight wants exactly that one, which is why it reads
|
|
60
|
-
* here and never enumerates.
|
|
50
|
+
* THE AUTHORED VIEWPORT, as a lane that mounts something reaches it. Which
|
|
51
|
+
* native surface the viewport renders is the host's.
|
|
61
52
|
*
|
|
62
53
|
* {@link stages} is EVERY mounted 3D stage. A helper is an `Object3D` and an
|
|
63
54
|
* `Object3D` has one parent, so there is no "set it once for all stages": a
|
|
@@ -66,9 +57,6 @@ export interface ViewportStage {
|
|
|
66
57
|
* it made that pass.
|
|
67
58
|
*/
|
|
68
59
|
export interface ViewportDoor {
|
|
69
|
-
rig(): ViewportRig | null;
|
|
70
|
-
/** Runs after the viewport's own per-frame update; the return unsubscribes. */
|
|
71
|
-
onFrame(fn: (dtSeconds: number) => void): () => void;
|
|
72
60
|
/**
|
|
73
61
|
* Show an editor-only helper object of `kind` in the authored viewport (a
|
|
74
62
|
* baked navmesh's debug mesh), replacing the previous one of that kind;
|
package/src/viewport-door.ts
CHANGED
|
@@ -2,14 +2,11 @@
|
|
|
2
2
|
* Registry for document stages' viewport rigs, helper displays and frame callbacks. Each mounted
|
|
3
3
|
* 3D stage binds under its document id and runs its frame hook from its own RAF loop.
|
|
4
4
|
*
|
|
5
|
-
* `viewportRig` and `onViewportFrame` follow the primary stage: the focused document's bound
|
|
6
|
-
* stage, else the first bound. The camera-flight seam reads that rig and uses its frame clock.
|
|
7
5
|
* `viewportStages()` exposes every bound stage so helpers can be attached to each separately.
|
|
8
6
|
* Stage subscriptions and helpers survive canvas remounts under the same document id.
|
|
9
7
|
*/
|
|
10
8
|
|
|
11
9
|
import type { ViewportDoor, ViewportRig, ViewportStage } from './viewport-api';
|
|
12
|
-
import { activeWorkspaceDocumentId } from '@volter/sdk/workspace-document-registry';
|
|
13
10
|
|
|
14
11
|
/** An editor-only helper object, typed by the host door it is shown through. */
|
|
15
12
|
type HelperObject = Parameters<ViewportDoor['setHelper']>[1];
|
|
@@ -24,8 +21,7 @@ interface Binding {
|
|
|
24
21
|
readonly helpers: Map<string, HelperObject>;
|
|
25
22
|
}
|
|
26
23
|
|
|
27
|
-
/** Every bound stage, keyed by the document it draws
|
|
28
|
-
* "the first bound" is a real answer. */
|
|
24
|
+
/** Every bound stage, keyed by the document it draws, in bind order. */
|
|
29
25
|
const bound = new Map<string, Binding>();
|
|
30
26
|
/** Retain frame subscribers and helper assignments by document across viewport remounts and before binding. */
|
|
31
27
|
const stageStateByDocument = new Map<
|
|
@@ -47,23 +43,8 @@ function stageStateFor(documentId: string): {
|
|
|
47
43
|
}
|
|
48
44
|
return state;
|
|
49
45
|
}
|
|
50
|
-
/** Listeners on the singular `onViewportFrame`: they follow the PRIMARY
|
|
51
|
-
* stage, so they are held here and fanned in rather than added to a
|
|
52
|
-
* binding's own set, which would strand them on a stage that stops being
|
|
53
|
-
* primary (or unmounts). */
|
|
54
|
-
const primaryFrameListeners = new Set<(dtSeconds: number) => void>();
|
|
55
46
|
const stageListeners = new Set<(stages: readonly ViewportStage[]) => void>();
|
|
56
47
|
|
|
57
|
-
/** THE PRIMARY STAGE: focused > first bound. */
|
|
58
|
-
function primary(): Binding | null {
|
|
59
|
-
const focused = activeWorkspaceDocumentId();
|
|
60
|
-
if (focused !== null) {
|
|
61
|
-
const match = bound.get(focused);
|
|
62
|
-
if (match) return match;
|
|
63
|
-
}
|
|
64
|
-
return bound.values().next().value ?? null;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
48
|
let stagesCache: readonly ViewportStage[] | null = null;
|
|
68
49
|
|
|
69
50
|
function notifyStages(): void {
|
|
@@ -121,20 +102,6 @@ export function bindViewportRig(
|
|
|
121
102
|
};
|
|
122
103
|
}
|
|
123
104
|
|
|
124
|
-
export function viewportRig(): ViewportRig | null {
|
|
125
|
-
return primary()?.rig ?? null;
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
/** Runs after the PRIMARY stage's own per-frame update, and RE-TARGETS: the
|
|
129
|
-
* listener follows whichever stage is primary, not the one that was primary
|
|
130
|
-
* when it subscribed. */
|
|
131
|
-
export function onViewportFrame(fn: (dtSeconds: number) => void): () => void {
|
|
132
|
-
primaryFrameListeners.add(fn);
|
|
133
|
-
return () => {
|
|
134
|
-
primaryFrameListeners.delete(fn);
|
|
135
|
-
};
|
|
136
|
-
}
|
|
137
|
-
|
|
138
105
|
/** Every bound stage, in bind order. */
|
|
139
106
|
export function viewportStages(): readonly ViewportStage[] {
|
|
140
107
|
if (!stagesCache)
|
|
@@ -156,12 +123,9 @@ export function onViewportStages(fn: (stages: readonly ViewportStage[]) => void)
|
|
|
156
123
|
};
|
|
157
124
|
}
|
|
158
125
|
|
|
159
|
-
/** A stage's RAF loop calls this after its own update each frame
|
|
160
|
-
* own listeners always run; the primary-stage listeners run here only when
|
|
161
|
-
* this stage IS the primary. */
|
|
126
|
+
/** A stage's RAF loop calls this after its own update each frame, running the stage's own listeners. */
|
|
162
127
|
export function runViewportFrame(documentId: string, dtSeconds: number): void {
|
|
163
128
|
const binding = bound.get(documentId);
|
|
164
129
|
if (!binding) return;
|
|
165
130
|
for (const fn of binding.frameListeners) fn(dtSeconds);
|
|
166
|
-
if (primary() === binding) for (const fn of primaryFrameListeners) fn(dtSeconds);
|
|
167
131
|
}
|
|
@@ -1,59 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* THE WORLD ROOT'S CONTENT BINDING, as the stage host sees it.
|
|
3
|
-
*
|
|
4
|
-
* A stage shows either one Object3D a caller builds (`build` content) or the
|
|
5
|
-
* project's world (`world-root` content): the manifest's roots mounted by a
|
|
6
|
-
* design session, drawn by the world's own image pipeline, with the presenter
|
|
7
|
-
* Play adopts live roots through. The host owns the surface's place, the frame
|
|
8
|
-
* session, the per-stage door, the furniture and the overlays for both. What
|
|
9
|
-
* mounts a world is the contributing package's — it runs the game runtime,
|
|
10
|
-
* which a modeling host never carries — so the package hands the host this
|
|
11
|
-
* binding and the host loads it only for a stage that shows a world.
|
|
12
|
-
*/
|
|
13
|
-
import type { ComponentType } from 'react';
|
|
14
|
-
import type * as THREE from 'three';
|
|
15
|
-
import type { EditorShellStore } from '../editor-shell-store';
|
|
16
|
-
import type { EditorStats } from '@volter/sdk/editor-runtime';
|
|
17
|
-
import type { EditorViewport } from '../editor-viewport';
|
|
18
|
-
|
|
19
|
-
export interface WorldRootStageOptions {
|
|
20
|
-
/** The session's store: the world is the session's own subject. */
|
|
21
|
-
readonly store: EditorShellStore;
|
|
22
|
-
readonly documentId: string;
|
|
23
|
-
readonly container: HTMLDivElement;
|
|
24
|
-
readonly canvas: HTMLCanvasElement;
|
|
25
|
-
readonly renderer: THREE.WebGLRenderer;
|
|
26
|
-
readonly stats: EditorStats;
|
|
27
|
-
readonly onMountStatus: (status: 'mounting' | 'ready') => void;
|
|
28
|
-
readonly onRootIds: (rootIds: readonly string[]) => void;
|
|
29
|
-
/** This stage's frame hook on the per-stage viewport door. */
|
|
30
|
-
readonly runFrame: (deltaSeconds: number) => void;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
export interface WorldRootStage {
|
|
34
|
-
readonly scene: THREE.Scene;
|
|
35
|
-
readonly viewport: EditorViewport;
|
|
36
|
-
frame(timeMs: number, resumed: boolean): void;
|
|
37
|
-
setHelper(kind: string, object: THREE.Object3D | null): void;
|
|
38
|
-
dispose(): void;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
export interface WorldRootOverlayProps {
|
|
42
|
-
readonly store: EditorShellStore;
|
|
43
|
-
/** The stage's own document — its stage context is read by this id. */
|
|
44
|
-
readonly documentId: string;
|
|
45
|
-
readonly mountStatus: 'mounting' | 'ready';
|
|
46
|
-
readonly rootIds: readonly string[];
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
export interface WorldRootStageBinding {
|
|
50
|
-
/** The world's own drawing surface, built to the world's render settings. */
|
|
51
|
-
mountWorldRootSurface(
|
|
52
|
-
canvasHost: HTMLDivElement,
|
|
53
|
-
container: HTMLDivElement,
|
|
54
|
-
displayName: string,
|
|
55
|
-
): { canvas: HTMLCanvasElement; renderer: THREE.WebGLRenderer; lease: null };
|
|
56
|
-
installWorldRootStage(options: WorldRootStageOptions): WorldRootStage;
|
|
57
|
-
/** What the stage shows over a world: its mount state and camera authoring. */
|
|
58
|
-
readonly Overlays: ComponentType<WorldRootOverlayProps>;
|
|
59
|
-
}
|