@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
@@ -5,17 +5,7 @@ import ts from "typescript";
5
5
  // serving/animation-live-module.ts
6
6
  var ANIMATION_LIVE_MODULE_ID = "virtual:volter-three-animation-live";
7
7
  var animationLiveModuleSource = `
8
- const live = (globalThis[Symbol.for('volter.three.animation.live')] ??= {
9
- mixers: new Map(),
10
- listeners: new Set(),
11
- version: 0,
12
- });
13
- function notify() {
14
- live.version++;
15
- for (const listener of live.listeners) {
16
- try { listener(); } catch (error) { console.error(error); }
17
- }
18
- }
8
+ const live = (globalThis[Symbol.for('volter.three.animation.live')] ??= { mixers: new Map() });
19
9
  export function __volterMixer(mixer, key) {
20
10
  if (!mixer || typeof mixer.clipAction !== 'function') return mixer;
21
11
  if (mixer.__volterMixerKey) return mixer;
@@ -28,10 +18,8 @@ export function __volterMixer(mixer, key) {
28
18
  if (action) {
29
19
  const played = action.getClip();
30
20
  const target = action.getRoot();
31
- let changed = false;
32
- if (played && !entry.clips.has(played.name)) { entry.clips.set(played.name, played); changed = true; }
33
- if (target && !entry.roots.has(target)) { entry.roots.add(target); changed = true; }
34
- if (changed) notify();
21
+ if (played && !entry.clips.has(played.name)) entry.clips.set(played.name, played);
22
+ if (target) entry.roots.add(target);
35
23
  }
36
24
  return action;
37
25
  };
@@ -40,10 +28,8 @@ export function __volterMixer(mixer, key) {
40
28
  const result = uncacheRoot.call(this, root);
41
29
  entry.roots.delete(root);
42
30
  if (root === this.getRoot()) live.mixers.delete(this);
43
- notify();
44
31
  return result;
45
32
  };
46
- notify();
47
33
  return mixer;
48
34
  }
49
35
  /** drei's \`useAnimations\` result: its mixer, and the clips it was handed. */
@@ -52,11 +38,9 @@ export function __volterAnimations(result, key) {
52
38
  __volterMixer(result.mixer, key);
53
39
  const entry = live.mixers.get(result.mixer);
54
40
  if (entry && Array.isArray(result.clips)) {
55
- let changed = false;
56
41
  for (const clip of result.clips) {
57
- if (clip && clip.name && !entry.clips.has(clip.name)) { entry.clips.set(clip.name, clip); changed = true; }
42
+ if (clip && clip.name && !entry.clips.has(clip.name)) entry.clips.set(clip.name, clip);
58
43
  }
59
- if (changed) notify();
60
44
  }
61
45
  return result;
62
46
  }
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@volter/editor-threejs",
3
3
  "publisher": "volter",
4
4
  "author": "Volter AI, Inc.",
5
- "version": "0.5.212",
5
+ "version": "0.5.214",
6
6
  "engines": {
7
7
  "vscode": "*"
8
8
  },
@@ -83,7 +83,6 @@
83
83
  "./kit/components/stage-transform-chrome": "./src/kit/components/stage-transform-chrome.ts",
84
84
  "./kit/components/stage-transform-tools": "./src/kit/components/stage-transform-tools.tsx",
85
85
  "./kit/components/standard-viewport-dressing": "./src/kit/components/standard-viewport-dressing.ts",
86
- "./kit/components/world-root-binding": "./src/kit/components/world-root-binding.ts",
87
86
  "./kit/constraint-helper": "./src/kit/constraint-helper.ts",
88
87
  "./kit/editor-shell-store": "./src/kit/editor-shell-store.ts",
89
88
  "./kit/editor-viewport": "./src/kit/editor-viewport.ts",
@@ -95,7 +94,6 @@
95
94
  "./kit/model-thumbnail": "./src/kit/model-thumbnail.ts",
96
95
  "./kit/projection/three": "./src/kit/projection/three.ts",
97
96
  "./kit/reflection-probe-helper": "./src/kit/reflection-probe-helper.ts",
98
- "./kit/scene-document-viewport": "./src/kit/scene-document-viewport.ts",
99
97
  "./kit/scene-framing": "./src/kit/scene-framing.ts",
100
98
  "./kit/scene-view-fog": "./src/kit/scene-view-fog.ts",
101
99
  "./kit/spatial-handle-visuals": "./src/kit/spatial-handle-visuals.ts",
@@ -416,11 +414,11 @@
416
414
  }
417
415
  },
418
416
  "dependencies": {
419
- "@volter/project": "0.5.212",
417
+ "@volter/project": "0.5.214",
420
418
  "@sparkjsdev/spark": "^2.1.0",
421
419
  "three.quarks": "^0.16.0",
422
420
  "typescript": "5.9.3",
423
- "@volter/sdk": "0.5.212",
421
+ "@volter/sdk": "0.5.214",
424
422
  "postprocessing": "6.39.5",
425
423
  "quarks.core": "0.16.0",
426
424
  "@fortawesome/free-solid-svg-icons": "^7.2.0"
@@ -12,17 +12,7 @@
12
12
  export const ANIMATION_LIVE_MODULE_ID = 'virtual:volter-three-animation-live';
13
13
 
14
14
  export const animationLiveModuleSource = `
15
- const live = (globalThis[Symbol.for('volter.three.animation.live')] ??= {
16
- mixers: new Map(),
17
- listeners: new Set(),
18
- version: 0,
19
- });
20
- function notify() {
21
- live.version++;
22
- for (const listener of live.listeners) {
23
- try { listener(); } catch (error) { console.error(error); }
24
- }
25
- }
15
+ const live = (globalThis[Symbol.for('volter.three.animation.live')] ??= { mixers: new Map() });
26
16
  export function __volterMixer(mixer, key) {
27
17
  if (!mixer || typeof mixer.clipAction !== 'function') return mixer;
28
18
  if (mixer.__volterMixerKey) return mixer;
@@ -35,10 +25,8 @@ export function __volterMixer(mixer, key) {
35
25
  if (action) {
36
26
  const played = action.getClip();
37
27
  const target = action.getRoot();
38
- let changed = false;
39
- if (played && !entry.clips.has(played.name)) { entry.clips.set(played.name, played); changed = true; }
40
- if (target && !entry.roots.has(target)) { entry.roots.add(target); changed = true; }
41
- if (changed) notify();
28
+ if (played && !entry.clips.has(played.name)) entry.clips.set(played.name, played);
29
+ if (target) entry.roots.add(target);
42
30
  }
43
31
  return action;
44
32
  };
@@ -47,10 +35,8 @@ export function __volterMixer(mixer, key) {
47
35
  const result = uncacheRoot.call(this, root);
48
36
  entry.roots.delete(root);
49
37
  if (root === this.getRoot()) live.mixers.delete(this);
50
- notify();
51
38
  return result;
52
39
  };
53
- notify();
54
40
  return mixer;
55
41
  }
56
42
  /** drei's \`useAnimations\` result: its mixer, and the clips it was handed. */
@@ -59,11 +45,9 @@ export function __volterAnimations(result, key) {
59
45
  __volterMixer(result.mixer, key);
60
46
  const entry = live.mixers.get(result.mixer);
61
47
  if (entry && Array.isArray(result.clips)) {
62
- let changed = false;
63
48
  for (const clip of result.clips) {
64
- if (clip && clip.name && !entry.clips.has(clip.name)) { entry.clips.set(clip.name, clip); changed = true; }
49
+ if (clip && clip.name && !entry.clips.has(clip.name)) entry.clips.set(clip.name, clip);
65
50
  }
66
- if (changed) notify();
67
51
  }
68
52
  return result;
69
53
  }
@@ -19,15 +19,13 @@ export interface LiveMixer {
19
19
 
20
20
  interface Registry {
21
21
  readonly mixers: Map<unknown, LiveMixer>;
22
- readonly listeners: Set<() => void>;
23
- version: number;
24
22
  }
25
23
 
26
24
  const REGISTRY = Symbol.for('volter.three.animation.live');
27
25
 
28
26
  function registry(): Registry {
29
27
  const holder = globalThis as unknown as Record<symbol, Registry | undefined>;
30
- return (holder[REGISTRY] ??= { mixers: new Map(), listeners: new Set(), version: 0 });
28
+ return (holder[REGISTRY] ??= { mixers: new Map() });
31
29
  }
32
30
 
33
31
  /** The live mixer that animates `object`, or null when the game animates it with none. */
@@ -43,9 +41,3 @@ export function liveMixerFor(object: THREE.Object3D): LiveMixer | null {
43
41
  export function liveMixers(): LiveMixer[] {
44
42
  return [...registry().mixers.values()];
45
43
  }
46
-
47
- export function subscribeLiveMixers(listener: () => void): () => void {
48
- const listeners = registry().listeners;
49
- listeners.add(listener);
50
- return () => listeners.delete(listener);
51
- }
@@ -5,7 +5,7 @@
5
5
  * reaches clips loaded through useGLTF and useAnimations. Subjects seek through
6
6
  * AnimationMixer.setTime; the transport owns playback position.
7
7
  */
8
- import { liveMixerFor, subscribeLiveMixers } from '@volter/editor-threejs/animation/live-mixers';
8
+ import { liveMixerFor } from '@volter/editor-threejs/animation/live-mixers';
9
9
  import * as THREE from 'three';
10
10
  import type { StageTransport } from '@volter/sdk/animation/stage-transport';
11
11
 
@@ -104,62 +104,22 @@ function attachInspected(object: THREE.Object3D, transport: StageTransport): (()
104
104
  return detach;
105
105
  }
106
106
 
107
- /**
108
- * One scan of a mounted root, and the handle that keeps it current.
109
- *
110
- * `refresh()` DIFFS by object uuid rather than re-attaching: a rescan of an
111
- * unchanged tree must not detach and re-attach every subject, because that
112
- * would reset which clip each one is showing under a person's hands. The
113
- * caller decides when to rescan (the stage host throttles it and skips it
114
- * while the world is driving time).
115
- */
107
+ /** One scan of a mounted root, and the handle that detaches what it attached. */
116
108
  export interface ClipSubjectScan {
117
- refresh(): void;
118
109
  dispose(): void;
119
110
  }
120
111
 
121
- export function scanClipSubjects(
122
- root: THREE.Object3D | (() => THREE.Object3D | null),
123
- transport: StageTransport,
124
- options: {
125
- /** Rescan when the game registers or drops a mixer. */
126
- readonly rescanOnLiveMixers?: boolean;
127
- } = {},
128
- ): ClipSubjectScan {
112
+ export function scanClipSubjects(root: THREE.Object3D, transport: StageTransport): ClipSubjectScan {
129
113
  const attached = new Map<string, () => void>();
130
-
131
- const refresh = (): void => {
132
- const seen = new Set<string>();
133
- // A getter for a stage whose rendered scene is not fixed at construction:
134
- // a world stage draws the session store's scene once the world mounts.
135
- const current = typeof root === 'function' ? root() : root;
136
- current?.traverse((object) => {
137
- if (attached.has(object.uuid)) {
138
- seen.add(object.uuid);
139
- return;
140
- }
141
- // The inspection wins when an object has both: it means a world binding
142
- // is already driving that skeleton, and a second mixer over it would
143
- // give one object two writers.
144
- const detach = attachInspected(object, transport) ?? attachOwnClips(object, transport);
145
- if (!detach) return;
146
- attached.set(object.uuid, detach);
147
- seen.add(object.uuid);
148
- });
149
- for (const [uuid, detach] of [...attached]) {
150
- if (seen.has(uuid)) continue;
151
- detach();
152
- attached.delete(uuid);
153
- }
154
- };
155
-
156
- refresh();
157
- const stopMixers = options.rescanOnLiveMixers ? subscribeLiveMixers(refresh) : () => {};
158
-
114
+ root.traverse((object) => {
115
+ // The project's own mixer wins when an object has both: the project's
116
+ // code is already driving that skeleton, and a second mixer over it would
117
+ // give one object two writers.
118
+ const detach = attachInspected(object, transport) ?? attachOwnClips(object, transport);
119
+ if (detach) attached.set(object.uuid, detach);
120
+ });
159
121
  return {
160
- refresh,
161
122
  dispose: () => {
162
- stopMixers();
163
123
  for (const detach of attached.values()) detach();
164
124
  attached.clear();
165
125
  },
@@ -105,9 +105,8 @@ function forwardedCallsiteOid(object: THREE.Object3D | null | undefined): string
105
105
  * same oid twice, and both mint the same shape of id: the first one seen in
106
106
  * depth-first order keeps the bare address, later ones get `#1`, `#2`, ….
107
107
  *
108
- * The suffix is spelled HERE and nowhere else: the minting walks
109
- * (`projection/three.ts`, `R3fSourceAuthoringAdapter.indexGraph`), the authority
110
- * check and the presence report all use this one.
108
+ * The suffix is spelled HERE and nowhere else; the minting walk
109
+ * (`projection/three.ts`) uses this one.
111
110
  */
112
111
  export function occurrenceId(baseId: string, occurrence: number): string {
113
112
  return occurrence === 0 ? baseId : `${baseId}#${occurrence}`;
@@ -112,8 +112,7 @@ export function Object3DDocumentToolbar({
112
112
  session?.getSnapshot ?? noSessionVersion,
113
113
  );
114
114
  // THIS STAGE's own store and its own helpers: the shared panels read
115
- // whichever store the FOCUSED document's stage runs on , and a toolbar is a control over ITS document, never over the
116
- // world root's.
115
+ // whichever store the FOCUSED document's stage runs on, and a toolbar is a control over ITS document.
117
116
  useSyncExternalStore(subscribeStageStores, stageStoresVersion, stageStoresVersion);
118
117
  const stage = stageStore(documentId);
119
118
  useSyncExternalStore(
@@ -9,7 +9,7 @@
9
9
  import { lazy, Suspense, useEffect } from 'react';
10
10
  import { announceDocumentStage } from '@volter/sdk/document-viewports';
11
11
  import type { Object3DDocumentViewportProps } from './StageHost';
12
- import { OBJECT3D_SURFACE_BUILDING, ViewportSurfaceStatus } from '../viewport-surface-status';
12
+ import { Progress } from '@volter/sdk/progress';
13
13
 
14
14
  const LazyObject3DDocumentViewport = lazy(async () => {
15
15
  const module = await import('./StageHost');
@@ -26,9 +26,7 @@ export function Object3DDocumentViewport(props: Object3DDocumentViewportProps) {
26
26
  return (
27
27
  <Suspense
28
28
  fallback={
29
- <ViewportSurfaceStatus testId="object3d-document-loading">
30
- {OBJECT3D_SURFACE_BUILDING}
31
- </ViewportSurfaceStatus>
29
+ props.chromeless ? null : <Progress documentId={props.documentId} title="Preparing the viewport" />
32
30
  }
33
31
  >
34
32
  <LazyObject3DDocumentViewport {...props} />