@volter/editor-threejs 0.5.211 → 0.5.212

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (133) hide show
  1. package/NOTICE +7 -2
  2. package/README.md +8 -13
  3. package/contributions/animation-mixers.service.ts +2 -4
  4. package/contributions/animation-timeline.utility.tsx +1 -2
  5. package/contributions/three.page.ts +50 -0
  6. package/dist-node/serving.mjs +84 -3
  7. package/dist-node/shared-three.mjs +145 -0
  8. package/package.json +296 -16
  9. package/serving/animation-stamp.ts +1 -1
  10. package/serving/index.ts +22 -5
  11. package/serving/model-import-conversion.ts +6 -3
  12. package/serving/shared-three.ts +176 -0
  13. package/src/adapter/constraint.ts +1 -18
  14. package/src/adapter/object3d-authoring-subject.ts +1 -12
  15. package/src/adapter/reflection-probe.ts +1 -9
  16. package/src/adapter/three-contract.ts +6 -60
  17. package/src/animation/live-mixers.ts +0 -4
  18. package/src/api.ts +7 -0
  19. package/src/asset-loaders.ts +2 -178
  20. package/src/capture/photograph-sampling.ts +333 -0
  21. package/src/capture/scene.ts +1 -1
  22. package/src/ecs/object-marks.ts +6 -14
  23. package/src/ecs/user-data.ts +3 -138
  24. package/src/kit/animation/three-clips-subject.ts +19 -42
  25. package/src/kit/asset-compare-core.ts +161 -0
  26. package/src/kit/asset-compare.ts +8 -27
  27. package/src/kit/asset-preview-command.ts +14 -19
  28. package/src/kit/asset-preview-framing.ts +8 -67
  29. package/src/kit/asset-preview.ts +109 -342
  30. package/src/kit/asset-workflow/model-inspection.ts +11 -154
  31. package/src/kit/asset-workflow/preview-resource-lifetime.ts +25 -0
  32. package/src/kit/authoring/component-instance-root.ts +3 -39
  33. package/src/kit/authoring/live-object-transform.ts +1 -30
  34. package/src/kit/authoring/object3d-document-session-registry.ts +7 -15
  35. package/src/kit/authoring/object3d-document-session.ts +471 -208
  36. package/src/kit/authoring/shell-viewport-policy.ts +18 -24
  37. package/src/kit/authoring/source-object3d-authoring-adapter.ts +6 -48
  38. package/src/kit/authoring/three-projection-core.ts +11 -40
  39. package/src/kit/authoring/viewport-pick-context.ts +5 -12
  40. package/src/kit/authoring/viewport-raycast.ts +15 -93
  41. package/src/kit/authoring/world-hidden-viewport.ts +8 -61
  42. package/src/kit/capture-camera-pose.ts +68 -0
  43. package/src/kit/collaboration-presence.ts +41 -0
  44. package/src/kit/components/CameraInfo.tsx +4 -6
  45. package/src/kit/components/InspectorObjectPreview.tsx +3 -30
  46. package/src/kit/components/Object3DDocumentToolbar.css +75 -0
  47. package/src/kit/components/Object3DDocumentToolbar.tsx +18 -84
  48. package/src/kit/components/Object3DDocumentViewport.tsx +5 -8
  49. package/src/kit/components/StageHost.css +40 -0
  50. package/src/kit/components/StageHost.tsx +181 -404
  51. package/src/kit/components/StageOverlays.tsx +1 -2
  52. package/src/kit/components/StatsOverlay.tsx +5 -17
  53. package/src/kit/components/ViewportFurniture.tsx +27 -154
  54. package/src/kit/components/ViewportOverlay.tsx +13 -17
  55. package/src/kit/components/ViewportOverlaysMenu.tsx +177 -0
  56. package/src/kit/components/ViewportShadingMenu.tsx +2 -42
  57. package/src/kit/components/ViewportViewMenu.tsx +7 -18
  58. package/src/kit/components/asset-viewers/EntityModelDocument.tsx +4 -13
  59. package/src/kit/components/asset-viewers/EnvironmentAssetDocument.tsx +5 -5
  60. package/src/kit/components/asset-viewers/LiveModuleDocument.tsx +24 -102
  61. package/src/kit/components/asset-viewers/LutAssetDocument.tsx +6 -6
  62. package/src/kit/components/asset-viewers/Object3DPreview.tsx +10 -45
  63. package/src/kit/components/asset-viewers/QuarksAssetDocument.tsx +2 -2
  64. package/src/kit/components/asset-viewers/ShaderAssetDocument.tsx +11 -10
  65. package/src/kit/components/asset-viewers/shader-source.ts +144 -0
  66. package/src/kit/components/asset-viewers/three-asset-viewers.tsx +2 -14
  67. package/src/kit/components/object3d-contribution-surfaces.tsx +1 -1
  68. package/src/kit/components/stage-keyboard.tsx +6 -12
  69. package/src/kit/components/stage-overlay-set.tsx +11 -20
  70. package/src/kit/components/stage-presence-markers.ts +9 -32
  71. package/src/kit/components/stage-transform-chrome.ts +4 -4
  72. package/src/kit/components/stage-transform-tools.tsx +7 -14
  73. package/src/kit/components/standard-viewport-dressing.ts +47 -167
  74. package/src/kit/components/world-root-binding.ts +2 -7
  75. package/src/kit/constraint-helper.ts +1 -1
  76. package/src/kit/document-renderer-session.ts +114 -0
  77. package/src/kit/editor-presentation-activity.ts +58 -0
  78. package/src/kit/editor-shell-store.ts +31 -403
  79. package/src/kit/editor-viewport.ts +476 -1762
  80. package/src/kit/entity-lod.ts +3 -16
  81. package/src/kit/entity-object.ts +1 -39
  82. package/src/kit/hierarchy-mark-reader.ts +13 -30
  83. package/src/kit/instanced-presentation.ts +1 -47
  84. package/src/kit/live-gesture-lock.ts +35 -0
  85. package/src/kit/live-module-source.ts +12 -52
  86. package/src/kit/model-thumbnail.ts +7 -35
  87. package/src/kit/play-camera-flight.ts +7 -16
  88. package/src/kit/projection/three.ts +13 -366
  89. package/src/kit/retained-document-states.ts +91 -0
  90. package/src/kit/scene-document-viewport.ts +3 -3
  91. package/src/kit/scene-framing.ts +11 -152
  92. package/src/kit/scene-view-fog.ts +2 -47
  93. package/src/kit/spatial-handle-visuals.ts +19 -28
  94. package/src/kit/stories/three-story-model.ts +5 -33
  95. package/src/kit/three-canvas-render.ts +3 -4
  96. package/src/kit/three-hierarchy-row-media.ts +3 -11
  97. package/src/kit/three-inspection-media.ts +6 -6
  98. package/src/kit/three-integration.ts +18 -18
  99. package/src/kit/three-state.ts +2 -19
  100. package/src/kit/three-viewport/camera-fit.ts +2 -6
  101. package/src/kit/three-viewport/selection-brackets.ts +5 -20
  102. package/src/kit/three-viewport/selection-outline.ts +4 -56
  103. package/src/kit/three-viewport/skeleton-helper.ts +1 -32
  104. package/src/kit/three-viewport/source-color.ts +1 -33
  105. package/src/kit/three-viewport/studio-environment.ts +4 -55
  106. package/src/kit/three-viewport-presentation.ts +23 -0
  107. package/src/kit/transport-strip.tsx +147 -0
  108. package/src/kit/viewport-actions.ts +8 -50
  109. package/src/kit/viewport-authoring-policy.ts +5 -52
  110. package/src/kit/viewport-commands.ts +28 -57
  111. package/src/kit/viewport-hotkeys.ts +29 -31
  112. package/src/kit/viewport-status-facet.ts +4 -5
  113. package/src/kit/viewport-surface-status.tsx +44 -0
  114. package/src/loader.ts +8 -71
  115. package/src/object3d-contributions.ts +18 -34
  116. package/src/viewport/clip-planes.ts +4 -32
  117. package/src/viewport/content-bounds.ts +13 -142
  118. package/src/viewport/editor-layers.ts +4 -26
  119. package/src/viewport/environment.ts +1 -1
  120. package/src/viewport/preview-renderer.ts +7 -45
  121. package/src/viewport/renderer-ownership.ts +14 -62
  122. package/src/viewport-api.ts +14 -33
  123. package/src/viewport-door.ts +16 -86
  124. package/contributions/three-integration.service.ts +0 -13
  125. package/src/adapter/hierarchy-marks.ts +0 -156
  126. package/src/adapter/ingest/scene-capture.ts +0 -843
  127. package/src/adapter/ingest/structural-ids.ts +0 -139
  128. package/src/adapter/ingest/visible-capture-window.ts +0 -302
  129. package/src/adapter/renderer-config.ts +0 -115
  130. package/src/asset-parse-error.ts +0 -29
  131. package/src/host-hierarchy-objects.ts +0 -31
  132. package/src/kit/authoring/design-time-settle.ts +0 -343
  133. package/src/kit/camera-authoring.ts +0 -175
@@ -10,12 +10,12 @@ import {
10
10
  Tooltip,
11
11
  } from '@volter/sdk/widgets';
12
12
  import { useEffect, useRef, useState, useSyncExternalStore } from 'react';
13
- import type { ShellStore } from '@volter/sdk/kit/shell-store';
13
+ import type { ShellStore } from '@volter/sdk/shell-store';
14
14
  import {
15
15
  setThreeViewportProjection,
16
16
  subscribeThreeViewportPresentation,
17
17
  threeViewportPresentation,
18
- } from '@volter/sdk/kit/three-viewport-presentation';
18
+ } from '../three-viewport-presentation';
19
19
  import {
20
20
  object3DDocumentSession,
21
21
  object3DDocumentSessionsVersion,
@@ -25,22 +25,11 @@ import {
25
25
  const NO_SESSION_SUBSCRIBE = () => () => {};
26
26
  const ZERO = () => 0;
27
27
 
28
- /** The standard Scene-view camera menu: projection and axis views in one
29
- * place. These are editor-camera choices only. */
30
- /** The stage's projection + axis-view presets. `shell` is the STAGE's own
31
- * store's shell, so a preset drives the stage the control is mounted over
32
- * (ARCHITECTURE-CORE §One stage unit 4).
33
- *
34
- * A DOCUMENT STAGE IS PAINTED BY ITS SESSION (`Object3DDocumentSession.camera()`), and the
35
- * editor-wide projection (`setThreeViewportProjection`) reaches only the world root's stage
36
- * (`world-root-stage.ts` is its one subscriber): over a document the Perspective and
37
- * Orthographic buttons changed nothing, the defect `ViewportFurniture`'s projection toggle
38
- * records. With `documentId` naming a stage that has a session, the menu drives that session,
39
- * as the document toolbar's view menu does; the world root keeps the editor-wide value.
40
- *
41
- * `label` draws the trigger as the view's NAME (the look's `stage.chrome.viewName`: Godot's
42
- * "⋮ Perspective", Unreal's "Perspective" pill) instead of the camera icon; `kebab` puts
43
- * Godot's three dots before it. */
28
+ /**
29
+ * Editor-camera projection and axis presets for the stage’s own shell. A documentId targets its
30
+ * Object3DDocumentSession; the world root uses the editor-wide projection. label shows the view
31
+ * name instead of a camera icon, and kebab prefixes the label with three dots.
32
+ */
44
33
  export function ViewportViewMenu({
45
34
  shell,
46
35
  documentId,
@@ -8,7 +8,7 @@ import {
8
8
  createAssetPreviewSnapshot,
9
9
  disposeAssetPreviewSnapshot,
10
10
  } from '../../asset-preview';
11
- import { useEditorStore } from '@volter/sdk/kit/editor-runtime';
11
+ import { useEditorStore } from '@volter/sdk/editor-runtime';
12
12
  import { Object3DDocumentViewport } from '../StageHost';
13
13
 
14
14
  function entityClips(source: THREE.Object3D): THREE.AnimationClip[] {
@@ -21,17 +21,8 @@ function entityClips(source: THREE.Object3D): THREE.AnimationClip[] {
21
21
  return [...discovered];
22
22
  }
23
23
 
24
- /**
25
- * Isolated native projection of a live `Object3D`; the source graph is never
26
- * reparented (`createAssetPreviewSnapshot` copies it).
27
- *
28
- * The object comes in already resolved, so this component has no way to fail
29
- * for a thing that is on screen — it is the honest shared body of both the
30
- * Asset Lab's entity document ({@link EntityModelDocument}, which resolves the
31
- * scene entity and owns the "no longer available" case) and the inspector's
32
- * preview section (`components/InspectorObjectPreview.tsx`), whose subject the
33
- * composer has already proved previewable.
34
- */
24
+ /** Isolated projection of an already-resolved Object3D, shared by entity documents and Inspector
25
+ * previews. Snapshot the source so its graph stays parented to its owner. */
35
26
  export function Object3DSnapshotDocument({
36
27
  documentId,
37
28
  source,
@@ -76,7 +67,7 @@ export function Object3DSnapshotDocument({
76
67
  chromeless={chromeless}
77
68
  // The chromeless host is the inspector's preview, which remounts on
78
69
  // every selection — it draws through the shared renderer instead of
79
- // building one per click (`inspector-preview-renderer.ts`). A full
70
+ // building one per click. A full
80
71
  // document lives as long as its panel and keeps its own.
81
72
  rendererLane={chromeless ? 'inspector-preview' : 'own'}
82
73
  assetType="entity"
@@ -1,3 +1,4 @@
1
+ import { editorHost } from '@volter/sdk/host';
1
2
  import { faCloudSun, faSliders } from '@fortawesome/free-solid-svg-icons';
2
3
  import { NumberInput, themeVars } from '@volter/sdk/widgets';
3
4
  import { useEffect, useMemo, useRef, useState } from 'react';
@@ -5,10 +6,9 @@ import * as THREE from 'three';
5
6
  import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
6
7
  import { EXRLoader } from 'three/addons/loaders/EXRLoader.js';
7
8
  import { HDRLoader } from 'three/addons/loaders/HDRLoader.js';
8
- import type { InspectionSection } from '@volter/sdk/kit/inspection-model';
9
- import { CONTRIBUTED_SECTION_ORDER, PROPERTIES_SECTION_ORDER } from '@volter/sdk/kit/inspection-model';
10
- import { AssetEditorShell } from '@volter/sdk/kit/components/AssetEditorShell';
11
- import { subscribeProjectAsset } from '@volter/sdk/kit/components/asset-editor-persistence';
9
+ import type { InspectionSection } from '@volter/sdk/inspection-model';
10
+ import { CONTRIBUTED_SECTION_ORDER, PROPERTIES_SECTION_ORDER } from '@volter/sdk/inspection-model';
11
+ import { AssetEditorShell } from '@volter/sdk/components/AssetEditorShell';
12
12
 
13
13
  interface EnvironmentImage {
14
14
  readonly texture: THREE.DataTexture;
@@ -296,7 +296,7 @@ export function EnvironmentAssetDocument({
296
296
 
297
297
  useEffect(() => {
298
298
  if (!active) return;
299
- return subscribeProjectAsset(assetPath, () => setRevision((value) => value + 1));
299
+ return editorHost().files.subscribeAsset(assetPath, () => setRevision((value) => value + 1));
300
300
  }, [active, assetPath]);
301
301
 
302
302
  useEffect(() => {
@@ -1,57 +1,16 @@
1
1
  /**
2
- * THE LIVE MODELING VIEW (docs/BLENDER-PARITY.md §Live modeling view, Gap 1):
3
- * a project TypeScript module that builds an `Object3D`, opened as an Asset
4
- * Lab document that RE-EXECUTES ON EVERY SAVE with the camera untouched —
5
- * "watch it model".
2
+ * Live Object3D document for authored project modules. Probe the export before mounting a viewport;
3
+ * modules with other shapes use the caller's source viewer.
6
4
  *
7
- * WHAT IS NEW HERE IS ONE THING: the re-execution. Everything else is the
8
- * surface every other Object3D document already opens on
9
- * (`StageHost.tsx`'s `Object3DDocumentViewport` — orbit, pick,
10
- * framing, the document toolbar, the ordinary Object3D inspector subject), and
11
- * the contract it resolves is the bake lane's, unchanged
12
- * (`live-module-source.ts`).
5
+ * A stable build callback returns one owned Group. Rebuilds replace its child, preserving the
6
+ * stage's camera, renderer and adapter. This component owns child disposal; the viewport's returned
7
+ * graph disposer is empty so the graph has one teardown path.
13
8
  *
14
- * ## Why the camera survives a rebuild: ONE ROOT, SWAPPED CHILDREN
15
- *
16
- * `Object3DDocumentViewport` re-runs its whole lifetime effect — renderer,
17
- * scene, camera, adapter — whenever the `build` callback's IDENTITY changes.
18
- * That is how the parametric Builder documents rebuild
19
- * (`catalog/project-source/editor/tools/builder-document.tsx`: a param edit makes
20
- * a new `build`, the viewport tears down and stands back up), and it is
21
- * exactly why those documents snap the camera back to `cameraDirection` on
22
- * every slider nudge. A modeling loop cannot do that: the whole point is to
23
- * keep looking at the model from where you put yourself while it changes
24
- * underneath you.
25
- *
26
- * So `build` here is STABLE FOR THE DOCUMENT'S LIFETIME. It returns a
27
- * container `THREE.Group` this component owns, and a rebuild swaps that
28
- * container's single CHILD. The viewport never learns a rebuild happened; the
29
- * camera, the renderer and the GPU state are never touched. This is the
30
- * "OWNED graph" shape `ToolObject3DAuthoringProps.build` documents (the same
31
- * one the 3D board and the story turntable take, for the same reason: the
32
- * graph is async to create), so the returned `dispose` is deliberately empty
33
- * and THIS component's effect is the one teardown path.
34
- *
35
- * ## The probe decides which document this is
36
- *
37
- * Detection is structural, never a filename convention — the quarks precedent
38
- * (`JsonAssetDocument.tsx`). The module is imported and its export resolved
39
- * ONCE before anything mounts; only an `Object3D` gets the live viewport, and
40
- * anything else falls through to the `fallback` the caller passed (the
41
- * ordinary source viewer). Mounting the viewport first and retreating would
42
- * flash a 3D panel at every `.ts` file in the project.
43
- *
44
- * ## A broken save is never a blank pane
45
- *
46
- * Once live, a failing rebuild KEEPS THE LAST GOOD MODEL on screen, dimmed,
47
- * under a banner carrying the message and stack. The document does not
48
- * unmount, the workspace does not crash, and the next good save clears the
49
- * banner in place. A failure at FIRST open has no last-good root to keep, so
50
- * it shows the source viewer with the same banner above it and recovers to the
51
- * live view when the module compiles.
9
+ * A failed rebuild keeps the last good model dimmed under a diagnostic banner. A failure before
10
+ * the first model uses the source viewer with that banner. A successful build clears the failure.
52
11
  */
53
12
 
54
- import { beginLiveModuleRevision } from '@volter/sdk/session/project-module-url';
13
+ import { beginLiveModuleRevision } from '@volter/sdk/project-module-url';
55
14
  import { themeVars } from '@volter/sdk/widgets';
56
15
  import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
57
16
  import * as THREE from 'three';
@@ -60,7 +19,7 @@ import {
60
19
  object3DDocumentSession,
61
20
  registerObject3DDocumentPreparation,
62
21
  } from '../../authoring/object3d-document-session-registry';
63
- import { editorConsole } from '@volter/sdk/kit/editor-console';
22
+ import { editorConsole } from '@volter/sdk/editor-console';
64
23
  import {
65
24
  buildLiveModuleObject3D,
66
25
  LiveModuleError,
@@ -71,18 +30,12 @@ import {
71
30
  projectModuleChangeMatches,
72
31
  projectModuleTransformError,
73
32
  subscribeProjectModuleChange,
74
- } from '@volter/sdk/kit/project-module-changes';
75
- import { notifyWorkspaceDocumentSelectionChanged } from '@volter/sdk/kit/workspace-document-registry';
33
+ } from '@volter/sdk/project-module-changes';
34
+ import { notifyWorkspaceDocumentSelectionChanged } from '@volter/sdk/workspace-document-registry';
76
35
  import { Object3DDocumentViewport } from '../Object3DDocumentViewport';
77
36
  import { STANDARD_COMPONENT_CAMERA_DIRECTION } from '../standard-viewport-dressing';
78
37
 
79
- /**
80
- * The asset-document host (`components/asset-documents.tsx`) hands its content
81
- * a plain BLOCK container of the panel's full height, not a flex parent — so
82
- * `flex: 1` on this component's own root measures ZERO and the viewport
83
- * canvas comes up 1241×0 (measured, 2026-08-29). Height 100% is what fills it;
84
- * the flex column below it is this component's own layout.
85
- */
38
+ /** Fill the asset host’s block container by height; this component owns the flex column inside it. */
86
39
  const FILL = {
87
40
  height: '100%',
88
41
  width: '100%',
@@ -91,33 +44,21 @@ const FILL = {
91
44
  position: 'relative',
92
45
  } as const;
93
46
 
94
- /** The one growing child inside {@link FILL}. Both axes, because a flex ROW
95
- * child with no grow measures 1×388 (also measured) — the mirror of the
96
- * block-parent trap above. */
47
+ /** Grow this child along both axes within FILL. */
97
48
  const FILL_CHILD = { flex: 1, minWidth: 0, minHeight: 0, display: 'flex' } as const;
98
49
 
99
- /** A project module is a live-model document, a plain source file, or not
100
- * decided yet. `fallback` covers the last two the same way — the caller's
101
- * ordinary viewer — and `reason` is why. */
50
+ /** Whether this module is being probed, showing a live model, or using its ordinary source viewer. */
102
51
  type ProbeState =
103
52
  | { readonly status: 'probing' }
104
- | { readonly status: 'live'; readonly exportName: string }
105
- | { readonly status: 'fallback'; readonly reason: string };
53
+ | { readonly status: 'live' }
54
+ | { readonly status: 'fallback' };
106
55
 
107
56
  interface BuildFailure {
108
57
  readonly message: string;
109
58
  readonly stack?: string | undefined;
110
59
  }
111
60
 
112
- /**
113
- * What the banner says. A transform failure PREFERS the server's own reason:
114
- * the browser's `import()` rejection for a module the dev server refused to
115
- * transform is the opaque `Failed to fetch dynamically imported module: <url>`
116
- * (measured), while Vite reported `Unexpected ";"` at line:col with a source
117
- * frame on the HMR channel — and the second one is the only one a person
118
- * mid-edit can act on. The rejection then becomes the detail line, because it
119
- * is still the truth about what this document tried to do.
120
- */
61
+ /** Prefer the dev server’s transform diagnostic to the opaque import rejection; retain the rejection as detail. */
121
62
  function describeRefusal(refusal: LiveModuleRefusal, modulePath: string): BuildFailure {
122
63
  if (refusal.kind !== 'import-failed') return { message: refusal.message };
123
64
  const transform = projectModuleTransformError(modulePath);
@@ -140,8 +81,7 @@ export function LiveModuleDocument({
140
81
  readonly modulePath: string;
141
82
  readonly displayName: string;
142
83
  readonly active: boolean;
143
- /** Shown when this module is not a model module (and while one that WAS is
144
- * failing to import at first open). */
84
+ /** Shown while probing, for non-model exports, or when the first import fails. */
145
85
  readonly fallback: React.ReactNode;
146
86
  }) {
147
87
  const [probe, setProbe] = useState<ProbeState>({ status: 'probing' });
@@ -185,19 +125,7 @@ export function LiveModuleDocument({
185
125
  mounted = root;
186
126
  mountedDispose = dispose;
187
127
  container.add(root);
188
- // THE CLIPS RIDE THE CONTAINER TOO. `live-module-source.ts` writes a
189
- // build result's `animations` onto the built root "so the clip list
190
- // travels with the graph the document mounts and every reader downstream
191
- // (the Object3D document session, its animation transport) finds it where
192
- // it finds a GLTF's" — but the graph this document MOUNTS is the stable
193
- // container, and every one of those readers reads `root.animations` on
194
- // the document root exactly (`ToolObject3DAuthoring`'s
195
- // `source.root.animations.length > 0` gate builds the mixer and the
196
- // transport; `model-inspection.ts`'s `root.animations.map` builds the
197
- // Animation section). Measured on a rigged module with two looping clips:
198
- // no transport, no Animation section, `editor.inspect()` listing only
199
- // Preview/Geometry/Rig/Materials/Source. Copying the reference
200
- // here is what makes the container the same subject the child is.
128
+ // Copy animations to the stable container, which is the document root inspected by the transport and Animation section.
201
129
  container.animations = root.animations;
202
130
  // The hierarchy/inspector project the live graph on every render, so the
203
131
  // swap is visible the moment the panels re-render — but nothing else
@@ -209,7 +137,7 @@ export function LiveModuleDocument({
209
137
  // Not a model module, and never was: this is an ordinary source file.
210
138
  // Silent by design — the project is full of them.
211
139
  if (refusal.kind === 'not-a-model' && !hasLastGood) {
212
- setProbe({ status: 'fallback', reason: refusal.message });
140
+ setProbe({ status: 'fallback' });
213
141
  return;
214
142
  }
215
143
  const described = describeRefusal(refusal, modulePath);
@@ -218,7 +146,7 @@ export function LiveModuleDocument({
218
146
  // surface, not document-local noise. It retires on the next page load
219
147
  // that does not reproduce it (`server/console-ledger.ts`).
220
148
  editorConsole.error(`${modulePath}: ${described.message}`, 'live-module');
221
- if (!hasLastGood) setProbe({ status: 'fallback', reason: refusal.message });
149
+ if (!hasLastGood) setProbe({ status: 'fallback' });
222
150
  };
223
151
 
224
152
  /** A save that lands mid-import supersedes this one; its own swap wins. */
@@ -247,7 +175,7 @@ export function LiveModuleDocument({
247
175
  captureFailure = null;
248
176
  clearProjectModuleTransformError(modulePath);
249
177
  setFailure(null);
250
- setProbe({ status: 'live', exportName: built.exportName });
178
+ setProbe({ status: 'live' });
251
179
  };
252
180
 
253
181
  const rebuild = (): Promise<void> => {
@@ -278,14 +206,8 @@ export function LiveModuleDocument({
278
206
  };
279
207
  }, [container, documentId, modulePath, projectRoot]);
280
208
 
281
- /**
282
- * FRAME ONCE, on the first model this document ever shows — a rebuild must
283
- * never move the camera, which is the one thing this document exists to
284
- * avoid. The viewport registers its session in its own mount effect, a
285
- * render AFTER the state flip that mounts it, so this waits for the session
286
- * rather than assuming it: bounded, because "no session after a second" is a
287
- * mount failure, not something to poll forever.
288
- */
209
+ /** Frame the first built model once the viewport session mounts, waiting at most 61 animation-frame turns.
210
+ * Rebuilds retain the person's camera. */
289
211
  useEffect(() => {
290
212
  if (probe.status !== 'live' || framedRef.current) return;
291
213
  let frame = 0;
@@ -1,14 +1,14 @@
1
+ import { editorHost } from '@volter/sdk/host';
1
2
  import { faCircleHalfStroke, faSliders } from '@fortawesome/free-solid-svg-icons';
2
3
  import { Checkbox, NumberInput, themeVars } from '@volter/sdk/widgets';
3
4
  import { EffectComposer, EffectPass, LUT3DEffect, RenderPass } from 'postprocessing';
4
5
  import { useEffect, useMemo, useRef, useState } from 'react';
5
6
  import * as THREE from 'three';
6
7
  import { LUTCubeLoader } from 'three/addons/loaders/LUTCubeLoader.js';
7
- import { readProjectTextFile } from '@volter/sdk/kit/editor-api';
8
- import type { InspectionSection } from '@volter/sdk/kit/inspection-model';
9
- import { CONTRIBUTED_SECTION_ORDER, PROPERTIES_SECTION_ORDER } from '@volter/sdk/kit/inspection-model';
10
- import { AssetEditorShell } from '@volter/sdk/kit/components/AssetEditorShell';
11
- import { subscribeProjectAsset } from '@volter/sdk/kit/components/asset-editor-persistence';
8
+ import { readProjectTextFile } from '@volter/sdk/api/project-source';
9
+ import type { InspectionSection } from '@volter/sdk/inspection-model';
10
+ import { CONTRIBUTED_SECTION_ORDER, PROPERTIES_SECTION_ORDER } from '@volter/sdk/inspection-model';
11
+ import { AssetEditorShell } from '@volter/sdk/components/AssetEditorShell';
12
12
 
13
13
  interface CubeLut {
14
14
  readonly title: string | null;
@@ -282,7 +282,7 @@ export function LutAssetDocument({
282
282
 
283
283
  useEffect(() => {
284
284
  if (!active) return;
285
- return subscribeProjectAsset(assetPath, () => setRevision((value) => value + 1));
285
+ return editorHost().files.subscribeAsset(assetPath, () => setRevision((value) => value + 1));
286
286
  }, [active, assetPath]);
287
287
 
288
288
  useEffect(() => {
@@ -6,30 +6,21 @@ import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
6
6
  import {
7
7
  createOwnedObjectPreviewSnapshot,
8
8
  createPerspectiveAssetPreviewCamera,
9
- disposeProjectAssetModel,
10
- loadProjectAssetModel,
11
9
  measureAssetPreview,
12
10
  type NativeObjectPreviewSource,
13
11
  type OwnedObjectPreviewSnapshot,
14
12
  } from '../../asset-preview';
15
- import { registerPreviewResource } from '@volter/sdk/kit/asset-workflow/preview-resource-lifetime';
16
- import { registerDesignTimeSurface } from '@volter/sdk/kit/coverage/design-time-surfaces';
13
+ import { registerPreviewResource } from '../../asset-workflow/preview-resource-lifetime';
14
+ import { registerDesignTimeSurface } from '@volter/sdk/coverage/design-time-surfaces';
17
15
  import {
18
16
  isEditorPresentationActive,
19
17
  subscribeEditorPresentationActivity,
20
- } from '@volter/sdk/kit/editor-presentation-activity';
21
- import {
22
- createModelPreviewSource,
23
- loadModelThumbnailObject,
24
- modelThumbnailFormat,
25
- } from '../../model-thumbnail';
18
+ } from '../../editor-presentation-activity';
26
19
 
27
20
  export interface Object3DPreviewProps {
28
- readonly assetPath?: string | undefined;
29
- readonly materialPath?: string | undefined;
30
21
  readonly displayName?: string | undefined;
31
22
  readonly active?: boolean | undefined;
32
- readonly sourceFactory?: (() => NativeObjectPreviewSource) | undefined;
23
+ readonly sourceFactory: () => NativeObjectPreviewSource;
33
24
  readonly showSkeleton?: boolean | undefined;
34
25
  readonly background?: THREE.ColorRepresentation | undefined;
35
26
  readonly cameraDirection?: readonly [number, number, number] | undefined;
@@ -65,8 +56,6 @@ let previewCardSequence = 0;
65
56
 
66
57
  /** Compact, non-authoring Object3D preview for cards, catalogs, and thumbnails. */
67
58
  export function Object3DPreview({
68
- assetPath,
69
- materialPath,
70
59
  displayName = 'Object3D preview',
71
60
  active = true,
72
61
  sourceFactory,
@@ -103,9 +92,8 @@ export function Object3DPreview({
103
92
  const scene = new THREE.Scene();
104
93
  const camera = new THREE.PerspectiveCamera(35, 1, 0.01, 1000);
105
94
  const controls = new OrbitControls(camera, renderer.domElement);
106
- const clock = new THREE.Clock();
95
+ const timer = new THREE.Timer();
107
96
  let mixer: THREE.AnimationMixer | null = null;
108
- let ownedModel: THREE.Object3D | null = null;
109
97
  let ownedFactory: OwnedObjectPreviewSnapshot | null = null;
110
98
  let extension: ToolObject3DPreviewExtension | undefined;
111
99
  let sparkRenderer: SparkRenderer | null = null;
@@ -192,29 +180,8 @@ export function Object3DPreview({
192
180
 
193
181
  const load = async () => {
194
182
  try {
195
- if (sourceFactory) {
196
- ownedFactory = createOwnedObjectPreviewSnapshot(sourceFactory);
197
- await install(ownedFactory.root);
198
- return;
199
- }
200
- if (!assetPath) throw new Error('Compact Object3D preview has no source.');
201
- const format = modelThumbnailFormat(assetPath);
202
- if (!format) throw new Error(`No native Three.js loader supports ${assetPath}.`);
203
- const loaded =
204
- format === 'glb' || format === 'gltf' || format === 'spz'
205
- ? await loadProjectAssetModel(assetPath, controller.signal)
206
- : await loadModelThumbnailObject({
207
- url: assetPath,
208
- format,
209
- ...(materialPath ? { materialUrl: materialPath } : {}),
210
- });
211
- ownedModel = createModelPreviewSource(loaded);
212
- if (controller.signal.aborted || disposed) {
213
- disposeProjectAssetModel(ownedModel);
214
- ownedModel = null;
215
- return;
216
- }
217
- await install(ownedModel);
183
+ ownedFactory = createOwnedObjectPreviewSnapshot(sourceFactory);
184
+ await install(ownedFactory.root);
218
185
  } catch (caught) {
219
186
  if (!controller.signal.aborted && !disposed) {
220
187
  setError(caught instanceof Error ? caught.message : String(caught));
@@ -245,7 +212,8 @@ export function Object3DPreview({
245
212
  // PRESENTATION ONLY. The mixer is never advanced here — see
246
213
  // `poseAtFirstFrame`; `delta` drives the project's own presentation
247
214
  // extension and the orbit damping, both editor-owned.
248
- const delta = Math.min(clock.getDelta(), 0.1);
215
+ timer.update();
216
+ const delta = Math.min(timer.getDelta(), 0.1);
249
217
  controls.update();
250
218
  if (sparkRenderer) scene.add(sparkRenderer);
251
219
  try {
@@ -261,7 +229,7 @@ export function Object3DPreview({
261
229
  cancelAnimationFrame(frame);
262
230
  frame = 0;
263
231
  } else if (!disposed && frame === 0) {
264
- clock.getDelta();
232
+ timer.update();
265
233
  render();
266
234
  }
267
235
  });
@@ -293,7 +261,6 @@ export function Object3DPreview({
293
261
  } else material?.dispose();
294
262
  }
295
263
  if (ownedFactory) ownedFactory.dispose();
296
- if (ownedModel) disposeProjectAssetModel(ownedModel);
297
264
  if (container.contains(renderer.domElement)) container.removeChild(renderer.domElement);
298
265
  releaseResizeObserver();
299
266
  releaseAnimationFrame();
@@ -301,11 +268,9 @@ export function Object3DPreview({
301
268
  };
302
269
  }, [
303
270
  active,
304
- assetPath,
305
271
  background,
306
272
  cameraDirection,
307
273
  exposure,
308
- materialPath,
309
274
  setupPreview,
310
275
  showGrid,
311
276
  showSkeleton,
@@ -16,7 +16,7 @@ import { useCallback, useEffect, useMemo, useState } from 'react';
16
16
  import type * as THREE from 'three';
17
17
  import { type ParticleSystem, QuarksLoader } from 'three.quarks';
18
18
  import { quarksParticleSystems } from '../../authoring/quarks-particle-systems';
19
- import { projectOutputPath } from '@volter/sdk/kit/project-provenance';
19
+ import { projectPathOfServedAsset } from '@volter/sdk/project/output-roots';
20
20
  import { Object3DDocumentViewport } from '../StageHost';
21
21
  import { threeObject } from '../../../adapter/three-contract';
22
22
 
@@ -445,7 +445,7 @@ export async function loadNativeQuarksJsonSource(
445
445
  return null;
446
446
  }
447
447
  return isNativeQuarksJson(json)
448
- ? { text, json, projectPath: projectOutputPath(assetPath) }
448
+ ? { text, json, projectPath: projectPathOfServedAsset(assetPath) }
449
449
  : null;
450
450
  }
451
451
 
@@ -1,12 +1,12 @@
1
+ import { editorHost } from '@volter/sdk/host';
1
2
  import { faCode, faSliders, faTriangleExclamation } from '@fortawesome/free-solid-svg-icons';
2
3
  import { Checkbox, ColorInput, NumberInput, Select, themeVars } from '@volter/sdk/widgets';
3
4
  import { useEffect, useMemo, useRef, useState } from 'react';
4
5
  import * as THREE from 'three';
5
- import { readProjectTextFile } from '@volter/sdk/kit/editor-api';
6
- import type { InspectionSection } from '@volter/sdk/kit/inspection-model';
7
- import { CONTRIBUTED_SECTION_ORDER, PROPERTIES_SECTION_ORDER } from '@volter/sdk/kit/inspection-model';
8
- import { AssetEditorShell } from '@volter/sdk/kit/components/AssetEditorShell';
9
- import { subscribeProjectAsset } from '@volter/sdk/kit/components/asset-editor-persistence';
6
+ import { readProjectTextFile } from '@volter/sdk/api/project-source';
7
+ import type { InspectionSection } from '@volter/sdk/inspection-model';
8
+ import { CONTRIBUTED_SECTION_ORDER, PROPERTIES_SECTION_ORDER } from '@volter/sdk/inspection-model';
9
+ import { AssetEditorShell } from '@volter/sdk/components/AssetEditorShell';
10
10
  import {
11
11
  FALLBACK_FRAGMENT_SHADER,
12
12
  FALLBACK_VERTEX_SHADER,
@@ -18,7 +18,7 @@ import {
18
18
  type ShaderUniformDescriptor,
19
19
  shaderCompanionPath,
20
20
  shaderStageFromPath,
21
- } from '@volter/sdk/kit/components/asset-viewers/shader-source';
21
+ } from './shader-source';
22
22
 
23
23
  type PreviewGeometry = 'sphere' | 'box' | 'plane';
24
24
  type UniformPreviewValue = number | boolean | string | number[];
@@ -267,7 +267,7 @@ function ShaderPreview({
267
267
  const observer = new ResizeObserver(resize);
268
268
  observer.observe(host);
269
269
 
270
- const clock = new THREE.Clock();
270
+ const timer = new THREE.Timer();
271
271
  try {
272
272
  renderer.compile(scene, camera);
273
273
  renderer.render(scene, camera);
@@ -294,7 +294,8 @@ function ShaderPreview({
294
294
 
295
295
  const render = () => {
296
296
  if (disposed) return;
297
- const elapsed = clock.getElapsedTime();
297
+ timer.update();
298
+ const elapsed = timer.getElapsed();
298
299
  for (const descriptor of descriptors) {
299
300
  const uniform = uniforms[descriptor.name];
300
301
  if (!uniform) continue;
@@ -535,8 +536,8 @@ export function ShaderAssetDocument({
535
536
  const reload = () => setFileRevision((revision) => revision + 1);
536
537
  const companionPath = shaderCompanionPath(assetPath);
537
538
  const unsubscribers = [
538
- subscribeProjectAsset(assetPath, reload),
539
- ...(companionPath ? [subscribeProjectAsset(companionPath, reload)] : []),
539
+ editorHost().files.subscribeAsset(assetPath, reload),
540
+ ...(companionPath ? [editorHost().files.subscribeAsset(companionPath, reload)] : []),
540
541
  ];
541
542
  return () => {
542
543
  for (const unsubscribe of unsubscribers) unsubscribe();