@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.
Files changed (29) hide show
  1. package/dist-node/serving.mjs +4 -20
  2. package/package.json +3 -5
  3. package/serving/animation-live-module.ts +4 -20
  4. package/src/animation/live-mixers.ts +1 -9
  5. package/src/kit/animation/three-clips-subject.ts +10 -50
  6. package/src/kit/authoring/component-instance-root.ts +2 -3
  7. package/src/kit/components/Object3DDocumentToolbar.tsx +1 -2
  8. package/src/kit/components/Object3DDocumentViewport.tsx +2 -4
  9. package/src/kit/components/StageHost.tsx +78 -208
  10. package/src/kit/components/ViewportViewMenu.tsx +1 -1
  11. package/src/kit/components/asset-viewers/EntityModelDocument.tsx +2 -4
  12. package/src/kit/components/asset-viewers/EnvironmentAssetDocument.tsx +4 -6
  13. package/src/kit/components/asset-viewers/LiveModuleDocument.tsx +7 -31
  14. package/src/kit/components/asset-viewers/LutAssetDocument.tsx +4 -6
  15. package/src/kit/components/asset-viewers/ModelAssetDocument.tsx +4 -8
  16. package/src/kit/components/asset-viewers/Object3DPreview.tsx +3 -19
  17. package/src/kit/components/asset-viewers/QuarksAssetDocument.tsx +4 -8
  18. package/src/kit/components/asset-viewers/ShaderAssetDocument.tsx +37 -55
  19. package/src/kit/components/object3d-contribution-surfaces.tsx +4 -2
  20. package/src/kit/components/stage-transform-tools.tsx +1 -2
  21. package/src/kit/editor-shell-store.ts +2 -26
  22. package/src/kit/editor-viewport.ts +11 -1
  23. package/src/kit/three-integration.ts +0 -2
  24. package/src/viewport-api.ts +4 -16
  25. package/src/viewport-door.ts +2 -38
  26. package/src/kit/components/world-root-binding.ts +0 -59
  27. package/src/kit/play-camera-flight.ts +0 -291
  28. package/src/kit/scene-document-viewport.ts +0 -51
  29. 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
- {loading && !error ? (
298
- <span style={{ position: 'absolute', padding: 10, color: themeVars.content.muted }}>
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 { themeVars } from '@volter/sdk/widgets';
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
- <div role="alert" style={{ padding: 20, color: themeVars.semantic.danger }}>
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: 6, padding: 4 }}>
468
- {diagnostics.map((diagnostic, index) => {
469
- const file = diagnostic.stage === 'program' ? null : files[diagnostic.stage];
470
- return (
471
- <div
472
- key={`${diagnostic.stage}:${diagnostic.line ?? 0}:${diagnostic.message}:${index}`}
473
- style={{
474
- color:
475
- diagnostic.severity === 'warning'
476
- ? themeVars.semantic.warning
477
- : themeVars.semantic.danger,
478
- fontSize: 10,
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
- render: () => (
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 · {files ? basename(files.vertex.path) : 'Loading…'}
598
- {files && !files.vertex.available ? ' (preview fallback)' : ''}
591
+ Vertex · {basename(files.vertex.path)}
592
+ {!files.vertex.available ? ' (preview fallback)' : ''}
599
593
  </span>
600
594
  <span>
601
- Fragment · {files ? basename(files.fragment.path) : 'Loading…'}
602
- {files && !files.fragment.available ? ' (preview fallback)' : ''}
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
- <div role="alert" style={{ padding: 20, color: themeVars.semantic.danger }}>
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
- <div style={{ padding: 20, color: themeVars.content.muted }}>Loading shader files…</div>
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
- <div
725
- style={{
726
- position: 'absolute',
727
- left: 12,
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
- <DiagnosticList diagnostics={diagnostics} files={files} />
739
- </div>
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
- <Suspense fallback={<div style={{ minHeight: 240 }}>Loading 3D preview…</div>}>
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={<div style={{ minHeight: 240 }}>Loading 3D authoring surface…</div>}>
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`); the world root's stage runs on
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 `the world root's stage`).
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
- * `the world root's stage` has mounted).
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 Scene document: the free editor camera. */
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),
@@ -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`: the rig a camera flight drives, the frame loop it rides, the helpers it
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 ; this is the door
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: the rig
54
- * a camera flight drives and the frame loop it rides. Which native surface the
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;
@@ -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; insertion-ordered, so
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. The stage's
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
- }