@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
@@ -1,53 +1,12 @@
1
- /**
2
- * WHAT THE GIZMO VIEWPORT ASKS THE SHELL — the one collaborator
3
- * `editor-viewport.ts` declares and the shell installs at boot.
4
- *
5
- * ARCHITECTURE-CORE §Editor chrome, "The viewport stack is separable": a
6
- * surface below the shell never imports the shell's transport, a world
7
- * authoring adapter, or project discovery. The viewport was importing six
8
- * `authoring/` modules to ask four questions —
9
- *
10
- * · which adapter am I driving, and which one do the shared panels drive?
11
- * · what is under the pointer?
12
- * · which native root owns the current selection, and is it painted here?
13
- * · which three root is world-hidden, and how do I apply that?
14
- *
15
- * — and every one of those answers is a COMPOSITE fact. `layered-pick.ts`
16
- * walks `CompositeAuthoringAdapter.childAdapters()` in manifest z-order;
17
- * `panel-authoring.ts` reaches the workspace-document registry;
18
- * `world-hidden-viewport.ts` and `viewport-tool-context.ts` resolve the
19
- * composite's live three child. The viewport itself knows about none of that:
20
- * it raycasts a scene and drags a gizmo. Measured, those six edges were 96 of
21
- * its 193 files.
22
- *
23
- * ## The default is the NO-SHELL answer, and it is deliberate
24
- *
25
- * {@link DEFAULT_VIEWPORT_AUTHORING_POLICY} is what a viewport mounted with no
26
- * shell above it (a bounded host; a fixture) gets: no composite exists,
27
- * so there are no layers to walk, no other panel to agree with, no world-group
28
- * eye to honour. Each default states that outright rather than half-answering.
29
- * The ONE default that is real logic is {@link adapterOnlyToolOwner} — the
30
- * structural single-adapter fallback `resolveViewportToolContext` already used
31
- * for non-composite adapters, which lives here now and is imported BACK by
32
- * `authoring/viewport-tool-context.ts` so there is one copy of it.
33
- *
34
- * In the editor the shell policy is installed once by `EditorProvider`
35
- * (`EditorContext.tsx`), before any viewport mounts — including the Asset Lab
36
- * document's own, which is why that document keeps today's behaviour exactly.
37
- * The install site is the PROVIDER rather than `the world root's stage` because a
38
- * workspace can have an Asset Lab document open with no scene the world root's stage
39
- * mounted at all, and that document's viewport must not silently fall to the
40
- * defaults.
41
- */
1
+ /** The shell installs the viewport’s authoring policy: adapter resolution, picking, selection ownership and world-hidden roots. Asset documents can use this seam without importing shell discovery or transport modules. */
42
2
 
43
3
  import type { AuthoringAdapter } from '@volter/project/adapter';
44
- import { adapterOnlyToolOwner, type ViewportToolOwner } from '@volter/sdk/kit/authoring/viewport-tool-owner';
4
+ import { adapterOnlyToolOwner, type ViewportToolOwner } from '@volter/sdk/authoring/viewport-tool-owner';
45
5
 
46
6
  export { adapterOnlyToolOwner, type ViewportToolOwner };
47
7
  import type * as THREE from 'three';
48
- import { makeNoAuthoringAdapter } from '@volter/sdk/kit/authoring/no-authoring-adapter';
49
- import type { ShellStore } from '@volter/sdk/kit/shell-store';
50
-
8
+ import { makeNoAuthoringAdapter } from '@volter/sdk/authoring/no-authoring-adapter';
9
+ import type { ShellStore } from '@volter/sdk/shell-store';
51
10
 
52
11
  export interface ViewportAuthoringPolicy {
53
12
  /** The adapter this viewport drives when its host injected none. */
@@ -84,7 +43,6 @@ export interface ViewportAuthoringPolicy {
84
43
  suppressRootEnvironment(scene: THREE.Scene): void;
85
44
  }
86
45
 
87
-
88
46
  /** Memoized per store, because the viewport calls `activeAuthoring` per gesture
89
47
  * and adapter IDENTITY keys the selection scope and the handle cache. Mirrors
90
48
  * `authoring/active-adapter.ts`'s own per-store memo. */
@@ -115,7 +73,7 @@ export const DEFAULT_VIEWPORT_AUTHORING_POLICY: ViewportAuthoringPolicy = {
115
73
  return true;
116
74
  },
117
75
  threeSurfaceShowing(_store, adapter) {
118
- return !adapter.rects && typeof adapter.hierarchy.object3D === 'function';
76
+ return !adapter.rects && typeof adapter.hierarchy.nativeObject === 'function';
119
77
  },
120
78
  threeViewportRootId() {
121
79
  return null;
@@ -147,8 +105,3 @@ export function installViewportAuthoringPolicy(policy: ViewportAuthoringPolicy):
147
105
  export function viewportAuthoringPolicy(): ViewportAuthoringPolicy {
148
106
  return _policy;
149
107
  }
150
-
151
- /** Test seam: drop the installed policy. */
152
- export function __resetViewportAuthoringPolicyForTest(): void {
153
- _policy = DEFAULT_VIEWPORT_AUTHORING_POLICY;
154
- }
@@ -2,34 +2,26 @@
2
2
  * THE THREE VIEWPORT'S RELAY VERBS — framing, the camera, view presets, the
3
3
  * open Object3D document's orbit/turntable/frame, the viewport photograph and
4
4
  * the display and transform toggles. They are the Three viewport's, so they
5
- * reach the relay as a command contribution (`@volter/sdk/commands`,
6
- * `command-registry.ts`) rather than as rows of the kit's own table: a
7
- * composition with no Three viewport has none of them. Each budget travelled
8
- * with its verb from `command-table.ts`.
5
+ * are editor-threejs's Code-OSS commands `volter.<verb>` (`@volter/sdk/commands`),
6
+ * registered by its page half (`../../contributions/three.page.ts`), rather than
7
+ * rows of the kit's own table. Each command declares its budget in contributes.commands.
9
8
  */
10
- import type {
11
- CommandContribution,
12
- CommandDerivedRefresh,
13
- CommandSpec,
14
- EditorCommandMessage,
15
- EditorCommandResult,
16
- } from '@volter/sdk/commands';
17
- import { commandLine } from '@volter/sdk/kit/product-command';
18
- import { activeWorkspaceDocumentId } from '@volter/sdk/kit/workspace-document-registry';
9
+ import type { EditorCommandMessage, EditorCommandResult, VerbHandler } from '@volter/sdk/commands';
10
+ import { commandLine } from '@volter/sdk/product-command';
11
+ import { activeWorkspaceDocumentId } from '@volter/sdk/workspace-document-registry';
19
12
  import { object3DDocumentSession } from './authoring/object3d-document-session-registry';
20
- import { activeDocumentAuthoring } from '@volter/sdk/kit/authoring/shell-document-ops';
13
+ import { activeDocumentAuthoring } from '@volter/sdk/authoring/shell-document-ops';
21
14
  import { handleAssetPreviewCommand } from './asset-preview-command';
22
- import { captureSizeFromCommand } from '@volter/sdk/kit/capture-size';
15
+ import { captureSizeFromCommand } from '@volter/sdk/capture-size';
23
16
  import type { EditorShellStore, HelperVisibility } from './editor-shell-store';
24
- import { editorHost } from '@volter/sdk/host';
25
17
  import { entityObject3D } from './entity-object';
26
18
  import { VIEWPORT_SHADING_MODES } from '../render/viewport-shading';
27
- import { focusedStageStore } from '@volter/sdk/kit/stage-context';
19
+ import { focusedStageStore } from '@volter/sdk/stage-context';
28
20
  import {
29
21
  setViewGridVisible,
30
22
  startingPresentation,
31
23
  viewPresentationBinding,
32
- } from '@volter/sdk/kit/viewport-presentation';
24
+ } from '@volter/sdk/viewport-presentation';
33
25
  import { threeStoreForHost } from './three-state';
34
26
  import { viewportStages } from '../viewport-door';
35
27
  import { threeStateOf } from './three-state';
@@ -39,20 +31,14 @@ function activeObject3DDocumentSession() {
39
31
  return documentId ? object3DDocumentSession(documentId) : null;
40
32
  }
41
33
 
42
- /** One verb over the session's Three store. */
34
+ /** One verb over the session's Three store. Its budget and derivation are its `contributes.commands` entry. */
43
35
  function verb(
44
36
  run: (store: EditorShellStore, cmd: EditorCommandMessage) => EditorCommandResult | Promise<EditorCommandResult>,
45
- timeoutMs?: number,
46
- derivedRefresh: CommandDerivedRefresh = 'none',
47
- ): CommandSpec {
48
- return {
49
- ...(timeoutMs === undefined ? {} : { timeoutMs }),
50
- derivedRefresh,
51
- handle: (cmd) => {
52
- const store = threeStoreForHost();
53
- if (!store) return { ok: false, error: `${cmd.type} needs an editor session; none has opened yet.` };
54
- return run(store, cmd);
55
- },
37
+ ): VerbHandler {
38
+ return (cmd) => {
39
+ const store = threeStoreForHost();
40
+ if (!store) return { ok: false, error: `${cmd.type} needs an editor session; none has opened yet.` };
41
+ return run(store, cmd);
56
42
  };
57
43
  }
58
44
 
@@ -60,7 +46,7 @@ const OK: EditorCommandResult = { ok: true };
60
46
 
61
47
  type ShadingMode = Parameters<EditorShellStore['setShadingMode']>[0];
62
48
 
63
- export const viewportCommands: CommandContribution['commands'] = {
49
+ export const viewportVerbs: Readonly<Record<string, VerbHandler>> = {
64
50
  'focus-entity': verb((store, cmd) => {
65
51
  if (!activeObject3DDocumentSession()?.frameIds([cmd['id'] as string])) {
66
52
  store.shell.focusOnEntity(cmd['id'] as string);
@@ -107,9 +93,7 @@ export const viewportCommands: CommandContribution['commands'] = {
107
93
  return OK;
108
94
  }),
109
95
  'view-preset': verb((store, cmd) => {
110
- // An absent or unknown preset is refused BY NAME: unguarded it reached
111
- // `cameraPresetDirection` and threw a TypeError that named neither the
112
- // command nor the vocabulary (measured live, 2026-09-18).
96
+ // Refuse absent or unknown camera presets with the accepted vocabulary.
113
97
  const requested = cmd['preset'];
114
98
  const presets = ['top', 'front', 'right', 'perspective'] as const;
115
99
  if (typeof requested !== 'string' || !(presets as readonly string[]).includes(requested)) {
@@ -154,8 +138,8 @@ export const viewportCommands: CommandContribution['commands'] = {
154
138
  // the move's own wall clock. Both refuse a length past 60 s themselves. There
155
139
  // is no Scene fallback: the Scene viewport's camera answers to
156
140
  // `view-preset`/`set-camera` and has no framed subject to circle.
157
- 'document-orbit': verb((_store, cmd) => documentMove(cmd), 120_000),
158
- 'document-turntable': verb((_store, cmd) => documentMove(cmd), 120_000),
141
+ 'document-orbit': verb((_store, cmd) => documentMove(cmd)),
142
+ 'document-turntable': verb((_store, cmd) => documentMove(cmd)),
159
143
  'document-frame': verb((_store, cmd) => {
160
144
  const documentSession = activeObject3DDocumentSession();
161
145
  if (!documentSession) {
@@ -210,22 +194,11 @@ export const viewportCommands: CommandContribution['commands'] = {
210
194
  if (!measure)
211
195
  return { ok: false, error: `document-frame-cost: the stage for ${JSON.stringify(stage.documentId)} has no frame of its own to measure.` };
212
196
  return { ok: true, data: { stage: stage.documentId, ...measure(frames, quality) } };
213
- }, 120_000),
197
+ }),
214
198
  'capture-viewport': verb(async (store, cmd) => {
215
199
  // Fresh, unthrottled on-demand capture (see EditorShellStore.captureViewportImage).
216
200
  const requested = captureSizeFromCommand(cmd);
217
201
  if ('error' in requested) return { ok: false, error: requested.error };
218
- // AN ADOPTED SCENE IS PRESENTED BY ITS ADOPTER. The store's renderer and
219
- // camera are its own viewport's; re-rendering a scene another document
220
- // adopted through them came back white.
221
- if (store.hasAdoptedScene) {
222
- try {
223
- const capture = await editorHost().documents.captureActive(requested.size);
224
- return { ok: true, data: { base64: capture.base64, mimeType: capture.mimeType } };
225
- } catch (error) {
226
- return { ok: false, error: error instanceof Error ? error.message : String(error) };
227
- }
228
- }
229
202
  const dataUrl = store.captureViewportImage(requested.size);
230
203
  if (!dataUrl) {
231
204
  // The refusal names the MECHANISM and the door that does answer: this door
@@ -235,21 +208,20 @@ export const viewportCommands: CommandContribution['commands'] = {
235
208
  ok: false,
236
209
  error:
237
210
  "This door photographs the editor's own three.js Scene viewport, and nothing has bound one " +
238
- '(no renderer/scene/camera). A canvas-surface world (a first-party canvas root, or a ' +
239
- 'PixiJS/Phaser/Babylon ingest) never binds it — it draws on its own canvas in the Game ' +
240
- 'document. Capture that through `capture-active-document` (`editor.captureActiveDocument()`) ' +
241
- `or the running game through \`bridge-screenshot\` (${commandLine('screenshot')}).`,
211
+ '(no renderer/scene/camera). Capture a document-owned canvas through ' +
212
+ '`capture-active-document` (`editor.captureActiveDocument()`) ' +
213
+ `or the running game through ${commandLine('capture --region play')}.`,
242
214
  };
243
215
  }
244
216
  const comma = dataUrl.indexOf(',');
245
217
  const base64 = comma >= 0 ? dataUrl.slice(comma + 1) : dataUrl;
246
218
  return { ok: true, data: { base64, mimeType: 'image/png' } };
247
- }, undefined, 'if-content-changed'),
219
+ }),
248
220
  // The Asset Lab's photographs; the budget covers a multi-shot set's renders.
249
- 'capture-asset-preview': verb((store, cmd) => handleAssetPreviewCommand(store, cmd), 30_000, 'if-content-changed'),
221
+ 'capture-asset-preview': verb((store, cmd) => handleAssetPreviewCommand(store, cmd)),
250
222
  // Display — set semantics (only toggle when the value differs).
251
223
  'set-grid': verb((_store, cmd) => {
252
- // The ACTIVE view's grid switch (`kit/viewport-presentation`), one per view.
224
+ // The ACTIVE view's grid switch (`@volter/sdk/viewport-presentation`), one per view.
253
225
  const viewId = activeWorkspaceDocumentId();
254
226
  if (!viewId || !viewPresentationBinding(viewId)) {
255
227
  return { ok: false, error: 'set-grid: the active document draws no grid.' };
@@ -272,8 +244,7 @@ export const viewportCommands: CommandContribution['commands'] = {
272
244
  }),
273
245
  'set-shading-mode': verb((store, cmd) => {
274
246
  const documentSession = activeObject3DDocumentSession();
275
- // A MODE THE VIEWPORT DOES NOT DRAW IS REFUSED BY NAME: an unknown one reached the renderer
276
- // and every frame after it threw (`editor.shading('material')`).
247
+ // Refuse modes the active viewport does not support before changing renderer state.
277
248
  const mode = cmd['mode'];
278
249
  const known: readonly string[] = documentSession
279
250
  ? [...VIEWPORT_SHADING_MODES, 'uv', 'vertex-colors']
@@ -7,11 +7,11 @@
7
7
  * W/E/R under `volter`, G/R/S under `blender`.
8
8
  */
9
9
  import { editorHost } from '@volter/sdk/host';
10
- import { getActiveScope, installHotkeys, setActiveScope } from '@volter/sdk/kit/hotkeys';
11
- import { showTransientHint } from '@volter/sdk/kit/transient-hint';
10
+ import { getActiveScope, installHotkeys, setActiveScope } from '@volter/sdk/hotkeys';
11
+ import { showTransientHint } from '@volter/sdk/transient-hint';
12
12
  import type { EditorShellStore, TransformMode } from './editor-shell-store';
13
13
  import type { EditorViewport } from './editor-viewport';
14
- import { requestTransformMode } from '@volter/sdk/kit/transform-mode-request';
14
+ import { requestTransformMode } from '@volter/sdk/transform-mode-request';
15
15
 
16
16
  export function registerViewportHotkeys(
17
17
  store: EditorShellStore,
@@ -23,18 +23,17 @@ export function registerViewportHotkeys(
23
23
  if (!viewport.isFlying) requestTransformMode(store.shell, mode);
24
24
  };
25
25
  const unbind = keyboard.bindActions([
26
- { id: 'transform.select', scope: 'stage', run: transformMode('select') },
27
- { id: 'transform.combined', scope: 'stage', run: transformMode('combined') },
28
- { id: 'transform.translate', scope: 'stage', run: transformMode('translate') },
29
- { id: 'transform.rotate', scope: 'stage', run: transformMode('rotate') },
30
- { id: 'transform.scale', scope: 'stage', run: transformMode('scale') },
26
+ { id: 'transform.select', run: transformMode('select') },
27
+ { id: 'transform.combined', run: transformMode('combined') },
28
+ { id: 'transform.translate', run: transformMode('translate') },
29
+ { id: 'transform.rotate', run: transformMode('rotate') },
30
+ { id: 'transform.scale', run: transformMode('scale') },
31
31
  // Snap has a key and says what it did: a human build session asked "how
32
32
  // do you toggle the snap" with the magnet button on screen the whole time.
33
33
  // The hint names the ACTIVE keymap's key, never a remembered literal.
34
34
  // Holding Ctrl/⌘ during a drag snaps temporarily (editor-viewport.ts).
35
35
  {
36
36
  id: 'viewport.toggleSnap',
37
- scope: 'stage',
38
37
  run: () => {
39
38
  store.shell.toggleSnap();
40
39
  const { translate, rotate, scale } = store.shell.snapValues;
@@ -46,36 +45,35 @@ export function registerViewportHotkeys(
46
45
  );
47
46
  },
48
47
  },
49
- { id: 'viewport.frameSelection', scope: 'stage', run: () => store.shell.focusOnSelection() },
48
+ { id: 'viewport.frameSelection', run: () => store.shell.focusOnSelection() },
50
49
  {
51
50
  id: 'viewport.cyclePivot',
52
- scope: 'stage',
53
51
  run: () => {
54
52
  const pivotModes = ['active-element', 'median-point', 'individual-origins'] as const;
55
53
  const idx = pivotModes.indexOf(store.shell.pivotMode);
56
54
  store.shell.setPivotMode(pivotModes[(idx + 1) % pivotModes.length]!);
57
55
  },
58
56
  },
59
- { id: 'view.top', scope: 'stage', run: () => store.shell.setViewPreset('top') },
60
- { id: 'view.front', scope: 'stage', run: () => store.shell.setViewPreset('front') },
61
- { id: 'view.right', scope: 'stage', run: () => store.shell.setViewPreset('right') },
62
- { id: 'view.bottom', scope: 'stage', run: () => store.shell.setViewPreset('bottom') },
63
- { id: 'view.back', scope: 'stage', run: () => store.shell.setViewPreset('back') },
64
- { id: 'view.left', scope: 'stage', run: () => store.shell.setViewPreset('left') },
65
- { id: 'view.orbitLeft', scope: 'stage', run: () => store.shell.stepView('orbit-left') },
66
- { id: 'view.orbitRight', scope: 'stage', run: () => store.shell.stepView('orbit-right') },
67
- { id: 'view.orbitUp', scope: 'stage', run: () => store.shell.stepView('orbit-up') },
68
- { id: 'view.orbitDown', scope: 'stage', run: () => store.shell.stepView('orbit-down') },
69
- { id: 'view.opposite', scope: 'stage', run: () => store.shell.stepView('opposite') },
70
- { id: 'view.rollLeft', scope: 'stage', run: () => store.shell.stepView('roll-left') },
71
- { id: 'view.rollRight', scope: 'stage', run: () => store.shell.stepView('roll-right') },
72
- { id: 'view.all', scope: 'stage', run: () => store.shell.frameAll() },
73
- { id: 'view.zoomIn', scope: 'stage', run: () => store.shell.zoomView(1) },
74
- { id: 'view.zoomOut', scope: 'stage', run: () => store.shell.zoomView(-1) },
75
- { id: 'view.perspective', scope: 'stage', run: () => store.shell.setViewPreset('perspective') },
76
- { id: 'view.projection', scope: 'stage', run: () => store.shell.toggleProjection() },
77
- { id: 'view.camera', scope: 'stage', run: () => store.shell.toggleCameraView() },
78
- { id: 'viewport.snapToFloor', scope: 'stage', run: () => viewport.snapSelectionToFloor() },
57
+ { id: 'view.top', run: () => store.shell.setViewPreset('top') },
58
+ { id: 'view.front', run: () => store.shell.setViewPreset('front') },
59
+ { id: 'view.right', run: () => store.shell.setViewPreset('right') },
60
+ { id: 'view.bottom', run: () => store.shell.setViewPreset('bottom') },
61
+ { id: 'view.back', run: () => store.shell.setViewPreset('back') },
62
+ { id: 'view.left', run: () => store.shell.setViewPreset('left') },
63
+ { id: 'view.orbitLeft', run: () => store.shell.stepView('orbit-left') },
64
+ { id: 'view.orbitRight', run: () => store.shell.stepView('orbit-right') },
65
+ { id: 'view.orbitUp', run: () => store.shell.stepView('orbit-up') },
66
+ { id: 'view.orbitDown', run: () => store.shell.stepView('orbit-down') },
67
+ { id: 'view.opposite', run: () => store.shell.stepView('opposite') },
68
+ { id: 'view.rollLeft', run: () => store.shell.stepView('roll-left') },
69
+ { id: 'view.rollRight', run: () => store.shell.stepView('roll-right') },
70
+ { id: 'view.all', run: () => store.shell.frameAll() },
71
+ { id: 'view.zoomIn', run: () => store.shell.zoomView(1) },
72
+ { id: 'view.zoomOut', run: () => store.shell.zoomView(-1) },
73
+ { id: 'view.perspective', run: () => store.shell.setViewPreset('perspective') },
74
+ { id: 'view.projection', run: () => store.shell.toggleProjection() },
75
+ { id: 'view.camera', run: () => store.shell.toggleCameraView() },
76
+ { id: 'viewport.snapToFloor', run: () => viewport.snapSelectionToFloor() },
79
77
  ]);
80
78
 
81
79
  const onPointerDown = () => setActiveScope('viewport');
@@ -2,14 +2,13 @@
2
2
  * THE VIEWPORT'S STATUS KEYS, reported by the Three set as a status facet
3
3
  * (`host.session.reportFacet`): grid, helpers, stats, shading, helper visibility,
4
4
  * the armed transform tool, space and snap, and the camera — each for the stage
5
- * the person is looking at (`focusedStageStore`, ARCHITECTURE-CORE §One stage
6
- * unit 4), with an Object3D document's own presentation where it has one.
5
+ * the person is looking at , with an Object3D document's own presentation where it has one.
7
6
  */
8
7
  import { editorHost } from '@volter/sdk/host';
9
- import { activeWorkspaceDocumentId } from '@volter/sdk/kit/workspace-document-registry';
8
+ import { activeWorkspaceDocumentId } from '@volter/sdk/workspace-document-registry';
10
9
  import { object3DDocumentSession } from './authoring/object3d-document-session-registry';
11
- import { focusedStageStore } from '@volter/sdk/kit/stage-context';
12
- import { viewGridVisible } from '@volter/sdk/kit/viewport-presentation';
10
+ import { focusedStageStore } from '@volter/sdk/stage-context';
11
+ import { viewGridVisible } from '@volter/sdk/viewport-presentation';
13
12
  import { threeStoreForHost } from './three-state';
14
13
  import { threeStateOf } from './three-state';
15
14
 
package/src/loader.ts CHANGED
@@ -3,7 +3,7 @@
3
3
  *
4
4
  * Provides a single LoadingManager (for Three.js loaders) and resolveUrl()
5
5
  * (for raw fetch calls) so that all engine asset requests go through one
6
- * configurable URL prefix. Call setAssetPrefix() once at runtime startup.
6
+ * configurable URL prefix.
7
7
  *
8
8
  * Three.js loaders constructed with `loadingManager` automatically rewrite
9
9
  * URLs via setURLModifier(). For raw fetch() calls, wrap the URL with
@@ -34,18 +34,12 @@ function rewriteUrl(url: string): string {
34
34
  export const loadingManager = new THREE.LoadingManager();
35
35
  loadingManager.setURLModifier(rewriteUrl);
36
36
 
37
- /** Pre-configured TextureLoader using the shared manager. */
38
- export const textureLoader = new THREE.TextureLoader(loadingManager);
39
-
40
37
  /**
41
38
  * Shared DRACOLoader for decoding DRACO-compressed GLTF/GLB meshes.
42
39
  *
43
- * The decoder wasm/js is vendored under
44
- * packages/game-editor/template/public/jsm/libs/draco/gltf/ and served at the
45
- * absolute runtime path '/jsm/libs/draco/gltf/' (DRACOLoader appends
46
- * draco_wasm_wrapper.js / draco_decoder.wasm to this path). This is an
47
- * absolute path, so it deliberately does NOT go through the asset prefix —
48
- * the decoder is engine infrastructure, not a scene asset.
40
+ * Decoder wasm/js is served at '/jsm/libs/draco/gltf/'. DRACOLoader
41
+ * appends draco_wasm_wrapper.js / draco_decoder.wasm to this absolute path,
42
+ * independently of the scene asset prefix.
49
43
  */
50
44
  export const dracoLoader = new DRACOLoader(loadingManager);
51
45
  dracoLoader.setDecoderPath('/jsm/libs/draco/gltf/');
@@ -53,77 +47,20 @@ dracoLoader.setDecoderPath('/jsm/libs/draco/gltf/');
53
47
  /**
54
48
  * Shared KTX2Loader for decoding KHR_texture_basisu (KTX2/Basis) textures.
55
49
  *
56
- * Vendored and served EXACTLY like the DRACO decoder above — the Basis
57
- * transcoder js/wasm live in repo-root `public/jsm/libs/basis/` (the editor)
58
- * and `packages/game-editor/template/public/jsm/libs/basis/` (a scaffolded game's
59
- * own build), pinned in `vendor/upstream-assets.lock.json`, and KTX2Loader
60
- * appends `basis_transcoder.js` / `.wasm` to this absolute path.
61
- *
62
- * Without this wiring, the Asset Budget's "Compress textures · KTX2" output
63
- * is a GLB nothing in the engine can load — which is why the op and this
64
- * loader landed together.
50
+ * The host serves basis_transcoder.js and basis_transcoder.wasm at
51
+ * '/jsm/libs/basis/'. KTX2Loader appends the filenames to this absolute path.
65
52
  */
66
53
  export const ktx2Loader = new KTX2Loader(loadingManager);
67
54
  ktx2Loader.setTranscoderPath('/jsm/libs/basis/');
68
55
 
69
- let ktx2SupportDetected = false;
70
-
71
- /**
72
- * Teach the shared KTX2Loader which compressed formats THIS GPU accepts.
73
- *
74
- * KTX2Loader refuses to transcode until it has seen a renderer, and the
75
- * answer is a property of the page's GPU rather than of any one renderer —
76
- * so the FIRST renderer to exist supplies it and every later one is a no-op.
77
- * Called by the engine's own renderer setup (`setup/setup-renderer.ts`) and
78
- * by the editor's viewport, which between them cover every surface that
79
- * parses a GLB.
80
- *
81
- * This sits on the MOUNT path, so it must never throw for a renderer it
82
- * cannot question. `detectSupport` reads `renderer.extensions.has/get` (or
83
- * `renderer.hasFeature` on a WebGPU renderer) — surface a headless stand-in
84
- * (the test harnesses' fake renderers, any non-WebGL host) does not have.
85
- * Such a renderer is skipped WITHOUT latching, so the first renderer that
86
- * can actually answer still configures the loader for the whole page.
87
- */
88
- export function detectKtx2Support(renderer: THREE.WebGLRenderer): void {
89
- if (ktx2SupportDetected) return;
90
- const probe = renderer as unknown as
91
- | {
92
- isWebGPURenderer?: boolean;
93
- hasFeature?: unknown;
94
- extensions?: { has?: unknown; get?: unknown };
95
- }
96
- | null
97
- | undefined;
98
- const canAnswer =
99
- probe != null &&
100
- (probe.isWebGPURenderer === true
101
- ? typeof probe.hasFeature === 'function'
102
- : typeof probe.extensions?.has === 'function' && typeof probe.extensions?.get === 'function');
103
- if (!canAnswer) return;
104
- ktx2Loader.detectSupport(renderer);
105
- ktx2SupportDetected = true;
106
- }
107
-
108
56
  /** Pre-configured GLTFLoader using the shared manager, with DRACO decoding wired in. */
109
57
  export const gltfLoader = new GLTFLoader(loadingManager);
110
58
  gltfLoader.setDRACOLoader(dracoLoader);
111
59
  gltfLoader.setKTX2Loader(ktx2Loader);
112
- // EXT_meshopt_compression decoding (three's own bundled decoder — inline
113
- // wasm, no fetch). Without this, a meshopt-compressed GLB — including the
114
- // output of the editor's Asset Budget "Compress · meshopt" action (W4a M3) —
115
- // throws at load time in every runtime and the editor viewport. Symmetric
116
- // with the DRACO wiring above (CB2): the ONE shared loader owns codec setup.
60
+ // Configure the shared loader’s bundled meshopt decoder for EXT_meshopt_compression, alongside its
61
+ // DRACO decoder.
117
62
  gltfLoader.setMeshoptDecoder(MeshoptDecoder);
118
63
 
119
- /**
120
- * Set the URL prefix prepended to relative asset paths.
121
- * Called once by createGameRuntime(). Defaults to '/'.
122
- */
123
- export function setAssetPrefix(prefix: string): void {
124
- _prefix = prefix.endsWith('/') ? prefix : prefix ? `${prefix}/` : '/';
125
- }
126
-
127
64
  /**
128
65
  * Resolve a relative asset URL using the current prefix.
129
66
  * Use this for raw fetch() calls — Three.js loaders using
@@ -8,11 +8,11 @@
8
8
  */
9
9
 
10
10
  import type { AuthoringAdapter } from '@volter/project/adapter';
11
+ import type { SourceObject3DAuthoringAdapter } from './kit/authoring/source-object3d-authoring-adapter';
11
12
  import type {
12
13
  DocumentPersistenceBinding,
13
- DocumentPersistenceResource,
14
- } from '@volter/sdk/kit/authoring/object3d-document-persistence';
15
- import type { PresentationLayer } from '@volter/sdk/kit/viewport-presentation';
14
+ } from '@volter/sdk/authoring/document-persistence-types';
15
+ import type { PresentationLayer } from '@volter/sdk/viewport-presentation';
16
16
  import type { ComponentType } from 'react';
17
17
  import type {
18
18
  AnimationClip,
@@ -44,22 +44,23 @@ export interface ToolObject3DPreviewSource {
44
44
  readonly hierarchyRoots?: readonly Object3D[];
45
45
  readonly animations?: readonly AnimationClip[];
46
46
  /**
47
- * The source's OWN tick, and the host drives it EXACTLY ONCE per mount — for
48
- * the bounded design-time settle that gives a physics-owned body the pose it
49
- * actually rests in (`authoring/design-time-settle.ts`). It is never a frame
50
- * loop: content time does not advance on a design-time surface, so a source
51
- * that animates from here shows its settled pose and then holds it. Play is
52
- * where a world runs.
47
+ * The source's OWN tick: the stage's transport calls it as content time
48
+ * advances (`StageHost`'s `advanceContent`). Content time does not advance on
49
+ * its own on a design-time surface; Play is where a world runs.
53
50
  */
54
51
  update?(deltaSeconds: number): void;
55
52
  /** Prepare camera-dependent draw buffers immediately before rendering.
56
53
  * Does not advance content time or mutate the authored document. An optional
57
54
  * returned function restores temporary draw state, including after errors.
58
55
  * Interactive draws may use disposable navigation geometry; omitted/false
59
- * always means full detail (captures and default frame-cost readings). */
60
- prepareDraw?(camera: Camera, options?: {interactive: boolean; height: number; multiDraw?: boolean; renderer?: WebGLRenderer}): void | (() => void);
56
+ * always means full detail (captures and default frame-cost readings).
57
+ * `drawMode` is the stage's draw mode for this draw, `pixelScale` its target pixels per
58
+ * output pixel (the device pixel ratio on screen, the supersampling factor in a photograph),
59
+ * and `photograph` says the draw is a capture, which gets one chance: nothing in it may
60
+ * settle on a later frame. */
61
+ prepareDraw?(camera: Camera, options?: {interactive: boolean; height: number; multiDraw?: boolean; renderer?: WebGLRenderer; drawMode?: string; pixelScale?: number; photograph?: boolean}): void | (() => void);
61
62
  /**
62
- * EVERY CHANGE TO WHAT THE SOURCE DRAWS, announced: the host calls
63
+ * EVERY CHANGE TO WHAT THE SOURCE DRAWS, announced: the source calls
63
64
  * `listener` whenever the source's graph, materials, textures or poses may
64
65
  * look different, and returns nothing from it. A source that offers this is
65
66
  * drawn only when something changed -- this, the camera, input on the
@@ -153,16 +154,8 @@ export interface ToolViewportDressing {
153
154
  */
154
155
  readonly viewLocked?: Object3D;
155
156
  /**
156
- * THE STAGE'S VIEW TRANSFORM — three's `WebGLRenderer.toneMapping`. Omitted
157
- * means the editor's own, ACES.
158
- *
159
- * A document whose source system states its own transform passes that one
160
- * instead: Blender's factory scene is AgX (`view_settings.view_transform`),
161
- * and this product's Blender RENDER path already photographs through
162
- * `AgXToneMapping` — so before this existed the viewport was the one
163
- * surface in the chain running a different curve from the thing it frames.
164
- * It is not a look preference: the curve decides how a shading range lands
165
- * on screen, and two curves over one radiance are two different pictures.
157
+ * The stage’s three.js tone-mapping transform; omitted uses ACES. Documents can supply their
158
+ * source system’s transform so viewport and render use the same curve, such as Blender’s AgX.
166
159
  */
167
160
  readonly toneMapping?: ToneMapping;
168
161
  }
@@ -213,7 +206,7 @@ export interface ToolObject3DAuthoringProps {
213
206
  /** Per-document overrides of the standard viewport dressing. */
214
207
  readonly dressing?: ToolViewportDressing;
215
208
  /** The kind of stage this view is, for its starting presentation
216
- * (`@volter/sdk/kit/viewport-presentation`): the document's own kind (`'model'`).
209
+ * (`@volter/sdk/viewport-presentation`): the document's own kind (`'model'`).
217
210
  * Without it the kind is read off the document id's prefix. */
218
211
  readonly stageKind?: string;
219
212
  /** How far the OPENING view stands back from a fit of the content: `1` fills the view (the
@@ -236,16 +229,10 @@ export interface ToolObject3DAuthoringProps {
236
229
  } | null;
237
230
  /**
238
231
  * THE DOCUMENT'S OWN PRESENTATION LAYER, over the stage's starting values and under a person's
239
- * choices (`kit/viewport-presentation`): what the file itself says about how it is seen — a
232
+ * choices (`@volter/sdk/viewport-presentation`): what the file itself says about how it is seen — a
240
233
  * `.blend`'s saved lens as `camera.fov`. Read when the document binds.
241
234
  */
242
235
  readonly presentation?: PresentationLayer | null;
243
- /**
244
- * Optional binding from live native clips back to ordinary project source.
245
- * Project code only serializes its own TypeScript shape; the editor owns the
246
- * checksum-guarded write and records it in the canonical project history.
247
- * Without this binding the animation workspace remains honestly read-only.
248
- */
249
236
  /**
250
237
  * Project-owned serialization for this authored document. Each serializer
251
238
  * returns the exact bytes of one ordinary project file; the host writes all
@@ -387,16 +374,13 @@ export interface ToolObject3DDocumentState {
387
374
  readonly animations: readonly AnimationClip[];
388
375
  }
389
376
 
390
- /** One project-owned artifact participating in an Asset Lab commit. */
391
- export type ToolObject3DDocumentResource = DocumentPersistenceResource<ToolObject3DDocumentState>;
392
-
393
377
  export type ToolObject3DDocumentPersistence = DocumentPersistenceBinding<ToolObject3DDocumentState>;
394
378
 
395
379
  export interface ToolObject3DDocumentAuthoringContext extends ToolObject3DDocumentState {
396
380
  readonly documentId: string;
397
381
  readonly sourcePath: string;
398
382
  readonly scene: Scene;
399
- readonly defaultAdapter: AuthoringAdapter;
383
+ readonly defaultAdapter: SourceObject3DAuthoringAdapter;
400
384
  /**
401
385
  * Serialize this project-owned document through its declared persistence
402
386
  * resources and record the change in canonical history. Rejects when the