@volter/editor-threejs 0.5.211 → 0.5.213

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 +6 -11
  49. package/src/kit/components/StageHost.css +40 -0
  50. package/src/kit/components/StageHost.tsx +192 -439
  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 +6 -17
  59. package/src/kit/components/asset-viewers/EnvironmentAssetDocument.tsx +9 -11
  60. package/src/kit/components/asset-viewers/LiveModuleDocument.tsx +31 -133
  61. package/src/kit/components/asset-viewers/LutAssetDocument.tsx +10 -12
  62. package/src/kit/components/asset-viewers/ModelAssetDocument.tsx +4 -8
  63. package/src/kit/components/asset-viewers/Object3DPreview.tsx +13 -64
  64. package/src/kit/components/asset-viewers/QuarksAssetDocument.tsx +6 -10
  65. package/src/kit/components/asset-viewers/ShaderAssetDocument.tsx +48 -65
  66. package/src/kit/components/asset-viewers/shader-source.ts +144 -0
  67. package/src/kit/components/asset-viewers/three-asset-viewers.tsx +2 -14
  68. package/src/kit/components/object3d-contribution-surfaces.tsx +5 -3
  69. package/src/kit/components/stage-keyboard.tsx +6 -12
  70. package/src/kit/components/stage-overlay-set.tsx +11 -20
  71. package/src/kit/components/stage-presence-markers.ts +9 -32
  72. package/src/kit/components/stage-transform-chrome.ts +4 -4
  73. package/src/kit/components/stage-transform-tools.tsx +7 -14
  74. package/src/kit/components/standard-viewport-dressing.ts +47 -167
  75. package/src/kit/components/world-root-binding.ts +2 -7
  76. package/src/kit/constraint-helper.ts +1 -1
  77. package/src/kit/document-renderer-session.ts +114 -0
  78. package/src/kit/editor-presentation-activity.ts +58 -0
  79. package/src/kit/editor-shell-store.ts +31 -403
  80. package/src/kit/editor-viewport.ts +486 -1762
  81. package/src/kit/entity-lod.ts +3 -16
  82. package/src/kit/entity-object.ts +1 -39
  83. package/src/kit/hierarchy-mark-reader.ts +13 -30
  84. package/src/kit/instanced-presentation.ts +1 -47
  85. package/src/kit/live-gesture-lock.ts +35 -0
  86. package/src/kit/live-module-source.ts +12 -52
  87. package/src/kit/model-thumbnail.ts +7 -35
  88. package/src/kit/play-camera-flight.ts +7 -16
  89. package/src/kit/projection/three.ts +13 -366
  90. package/src/kit/retained-document-states.ts +91 -0
  91. package/src/kit/scene-document-viewport.ts +3 -3
  92. package/src/kit/scene-framing.ts +11 -152
  93. package/src/kit/scene-view-fog.ts +2 -47
  94. package/src/kit/spatial-handle-visuals.ts +19 -28
  95. package/src/kit/stories/three-story-model.ts +5 -33
  96. package/src/kit/three-canvas-render.ts +3 -4
  97. package/src/kit/three-hierarchy-row-media.ts +3 -11
  98. package/src/kit/three-inspection-media.ts +6 -6
  99. package/src/kit/three-integration.ts +18 -18
  100. package/src/kit/three-state.ts +2 -19
  101. package/src/kit/three-viewport/camera-fit.ts +2 -6
  102. package/src/kit/three-viewport/selection-brackets.ts +5 -20
  103. package/src/kit/three-viewport/selection-outline.ts +4 -56
  104. package/src/kit/three-viewport/skeleton-helper.ts +1 -32
  105. package/src/kit/three-viewport/source-color.ts +1 -33
  106. package/src/kit/three-viewport/studio-environment.ts +4 -55
  107. package/src/kit/three-viewport-presentation.ts +23 -0
  108. package/src/kit/transport-strip.tsx +147 -0
  109. package/src/kit/viewport-actions.ts +8 -50
  110. package/src/kit/viewport-authoring-policy.ts +5 -52
  111. package/src/kit/viewport-commands.ts +28 -57
  112. package/src/kit/viewport-hotkeys.ts +29 -31
  113. package/src/kit/viewport-status-facet.ts +4 -5
  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,
@@ -1,5 +1,5 @@
1
1
  import { threeStateOf } from '../../three-state';
2
- import { themeVars } from '@volter/sdk/widgets';
2
+ import { StateSurface } from '@volter/sdk/widgets';
3
3
  import { liveMixerFor } from '@volter/editor-threejs/animation/live-mixers';
4
4
  import { useCallback, useSyncExternalStore } from 'react';
5
5
  import type * as THREE from 'three';
@@ -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"
@@ -103,9 +94,7 @@ export function EntityModelDocument({
103
94
 
104
95
  if (!source)
105
96
  return (
106
- <div role="alert" style={{ padding: 20, color: themeVars.semantic.danger }}>
107
- Scene entity {entityId} is no longer available.
108
- </div>
97
+ <StateSurface tone="error" title={`Scene entity ${entityId} is no longer available`} />
109
98
  );
110
99
  return (
111
100
  <Object3DSnapshotDocument
@@ -1,14 +1,15 @@
1
+ import { editorHost } from '@volter/sdk/host';
1
2
  import { faCloudSun, faSliders } from '@fortawesome/free-solid-svg-icons';
2
- import { NumberInput, themeVars } from '@volter/sdk/widgets';
3
+ import { NumberInput, StateSurface, themeVars } from '@volter/sdk/widgets';
4
+ import { Progress } from '@volter/sdk/progress';
3
5
  import { useEffect, useMemo, useRef, useState } from 'react';
4
6
  import * as THREE from 'three';
5
7
  import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
6
8
  import { EXRLoader } from 'three/addons/loaders/EXRLoader.js';
7
9
  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';
10
+ import type { InspectionSection } from '@volter/sdk/inspection-model';
11
+ import { CONTRIBUTED_SECTION_ORDER, PROPERTIES_SECTION_ORDER } from '@volter/sdk/inspection-model';
12
+ import { AssetEditorShell } from '@volter/sdk/components/AssetEditorShell';
12
13
 
13
14
  interface EnvironmentImage {
14
15
  readonly texture: THREE.DataTexture;
@@ -296,7 +297,7 @@ export function EnvironmentAssetDocument({
296
297
 
297
298
  useEffect(() => {
298
299
  if (!active) return;
299
- return subscribeProjectAsset(assetPath, () => setRevision((value) => value + 1));
300
+ return editorHost().files.subscribeAsset(assetPath, () => setRevision((value) => value + 1));
300
301
  }, [active, assetPath]);
301
302
 
302
303
  useEffect(() => {
@@ -386,9 +387,7 @@ export function EnvironmentAssetDocument({
386
387
  title={title}
387
388
  status="HDR image unavailable"
388
389
  >
389
- <div role="alert" style={{ padding: 20, color: themeVars.semantic.danger }}>
390
- {loadError}
391
- </div>
390
+ <StateSurface tone="error" title={`Could not load ${title}`} detail={loadError} />
392
391
  </AssetEditorShell>
393
392
  );
394
393
  }
@@ -400,9 +399,8 @@ export function EnvironmentAssetDocument({
400
399
  active={active}
401
400
  type="environment image"
402
401
  title={title}
403
- status="Loading HDR image"
404
402
  >
405
- <div style={{ padding: 20, color: themeVars.content.muted }}>Loading environment…</div>
403
+ <Progress documentId={documentId} title={`Loading ${title}`} />
406
404
  </AssetEditorShell>
407
405
  );
408
406
  }
@@ -1,58 +1,17 @@
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';
55
- import { themeVars } from '@volter/sdk/widgets';
13
+ import { beginLiveModuleRevision } from '@volter/sdk/project-module-url';
14
+ import { EditorBanner, themeVars } from '@volter/sdk/widgets';
56
15
  import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
57
16
  import * as THREE from 'three';
58
17
  import { disposeProjectAssetModel } from '../../asset-preview';
@@ -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;
@@ -359,37 +281,13 @@ export function LiveModuleDocument({
359
281
  * and never a toast — the document itself has to say why it is stale. */
360
282
  function BuildFailureBanner({ failure }: { readonly failure: BuildFailure }) {
361
283
  return (
362
- <div
363
- role="alert"
284
+ <EditorBanner
285
+ tone="error"
364
286
  data-testid="live-module-error"
365
- style={{
366
- position: 'absolute',
367
- top: 8,
368
- left: 8,
369
- right: 8,
370
- zIndex: 2,
371
- maxHeight: '60%',
372
- overflow: 'auto',
373
- padding: '8px 10px',
374
- borderRadius: themeVars.shape.small,
375
- background: themeVars.surface.raised,
376
- border: `1px solid ${themeVars.semantic.danger}`,
377
- color: themeVars.semantic.danger,
378
- fontFamily: themeVars.typography.mono,
379
- fontSize: 11,
380
- lineHeight: 1.5,
381
- whiteSpace: 'pre-wrap',
382
- wordBreak: 'break-word',
383
- pointerEvents: 'auto',
384
- }}
287
+ style={{ position: 'absolute', top: 0, left: 0, right: 0, zIndex: 2 }}
288
+ detail={failure.stack ? `${failure.message}\n${failure.stack}` : failure.message}
385
289
  >
386
- <div style={{ fontWeight: 600 }}>
387
- This module did not build — showing the last good model.
388
- </div>
389
- <div style={{ marginTop: 4 }}>{failure.message}</div>
390
- {failure.stack ? (
391
- <div style={{ marginTop: 6, color: themeVars.content.muted }}>{failure.stack}</div>
392
- ) : null}
393
- </div>
290
+ This module did not build — showing the last good model.
291
+ </EditorBanner>
394
292
  );
395
293
  }
@@ -1,14 +1,15 @@
1
+ import { editorHost } from '@volter/sdk/host';
1
2
  import { faCircleHalfStroke, faSliders } from '@fortawesome/free-solid-svg-icons';
2
- import { Checkbox, NumberInput, themeVars } from '@volter/sdk/widgets';
3
+ import { Checkbox, NumberInput, StateSurface, themeVars } from '@volter/sdk/widgets';
4
+ import { Progress } from '@volter/sdk/progress';
3
5
  import { EffectComposer, EffectPass, LUT3DEffect, RenderPass } from 'postprocessing';
4
6
  import { useEffect, useMemo, useRef, useState } from 'react';
5
7
  import * as THREE from 'three';
6
8
  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';
9
+ import { readProjectTextFile } from '@volter/sdk/api/project-source';
10
+ import type { InspectionSection } from '@volter/sdk/inspection-model';
11
+ import { CONTRIBUTED_SECTION_ORDER, PROPERTIES_SECTION_ORDER } from '@volter/sdk/inspection-model';
12
+ import { AssetEditorShell } from '@volter/sdk/components/AssetEditorShell';
12
13
 
13
14
  interface CubeLut {
14
15
  readonly title: string | null;
@@ -282,7 +283,7 @@ export function LutAssetDocument({
282
283
 
283
284
  useEffect(() => {
284
285
  if (!active) return;
285
- return subscribeProjectAsset(assetPath, () => setRevision((value) => value + 1));
286
+ return editorHost().files.subscribeAsset(assetPath, () => setRevision((value) => value + 1));
286
287
  }, [active, assetPath]);
287
288
 
288
289
  useEffect(() => {
@@ -367,9 +368,7 @@ export function LutAssetDocument({
367
368
  title={basename(assetPath)}
368
369
  status="CUBE LUT unavailable"
369
370
  >
370
- <div role="alert" style={{ padding: 20, color: themeVars.semantic.danger }}>
371
- {loadError}
372
- </div>
371
+ <StateSurface tone="error" title={`Could not load ${basename(assetPath)}`} detail={loadError} />
373
372
  </AssetEditorShell>
374
373
  );
375
374
  }
@@ -381,9 +380,8 @@ export function LutAssetDocument({
381
380
  active={active}
382
381
  type="color grade"
383
382
  title={basename(assetPath)}
384
- status="Loading CUBE LUT"
385
383
  >
386
- <div style={{ padding: 20, color: themeVars.content.muted }}>Loading color grade…</div>
384
+ <Progress documentId={documentId} title={`Loading ${basename(assetPath)}`} />
387
385
  </AssetEditorShell>
388
386
  );
389
387
  }
@@ -1,4 +1,5 @@
1
- import { themeVars } from '@volter/sdk/widgets';
1
+ import { StateSurface } from '@volter/sdk/widgets';
2
+ import { Progress } from '@volter/sdk/progress';
2
3
  import { useCallback, useEffect, useState } from 'react';
3
4
  import type * as THREE from 'three';
4
5
  import {
@@ -83,14 +84,9 @@ export function ModelAssetDocument({
83
84
  }, [assetPath, model]);
84
85
  if (error)
85
86
  return (
86
- <div role="alert" style={{ padding: 20, color: themeVars.semantic.danger }}>
87
- {error}
88
- </div>
89
- );
90
- if (!model)
91
- return (
92
- <div style={{ padding: 20, color: themeVars.content.muted }}>Loading {displayName}…</div>
87
+ <StateSurface tone="error" title={`Could not load ${displayName}`} detail={error} />
93
88
  );
89
+ if (!model) return <Progress documentId={documentId} title={`Loading ${displayName}`} />;
94
90
  return (
95
91
  <Object3DDocumentViewport
96
92
  documentId={documentId}
@@ -1,35 +1,26 @@
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';
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,
@@ -329,24 +294,8 @@ export function Object3DPreview({
329
294
  background: themeVars.surface.inset,
330
295
  }}
331
296
  >
332
- {loading && !error ? (
333
- <span style={{ position: 'absolute', padding: 10, color: themeVars.content.muted }}>
334
- Loading…
335
- </span>
336
- ) : null}
337
- {!active ? (
338
- <span style={{ position: 'absolute', padding: 10, color: themeVars.content.muted }}>
339
- Preview suspended while inactive.
340
- </span>
341
- ) : null}
342
- {error ? (
343
- <div
344
- role="alert"
345
- style={{ position: 'absolute', padding: 10, color: themeVars.semantic.danger }}
346
- >
347
- {error}
348
- </div>
349
- ) : 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}
350
299
  </div>
351
300
  );
352
301
  }