@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,24 +1,11 @@
1
1
  import type * as THREE from 'three';
2
- import { isEntityObject } from './entity-object';
3
2
 
4
- /**
5
- * The selected object when it is itself a live THREE.LOD (the native R3F
6
- * shape), otherwise the entity's live THREE.LOD preview object, or null while
7
- * an async asset is still loading. A subtree search skips child authoring
8
- * nodes so a nested LOD entity can never shadow this one's.
9
- *
10
- * Format-neutral: reads the caller's native-node membership answer and
11
- * Three's own `isLOD` marker, never a scene descriptor. The default retains
12
- * stamp-based compatibility for first-party callers. `editor-viewport.ts`'s
13
- * per-frame forced-LOD-level enforcement is the only caller.
14
- */
3
+ /** Resolve the selected native THREE.LOD, otherwise its live preview LOD. Skip child authoring nodes and return null while its asset loads. */
15
4
  export function findEntityLod(
16
5
  obj: THREE.Object3D,
17
- isNestedAuthoringNode: (object: THREE.Object3D) => boolean = isEntityObject,
6
+ isNestedAuthoringNode: (object: THREE.Object3D) => boolean,
18
7
  ): THREE.LOD | null {
19
- // Adapter-backed worlds can expose the native LOD itself as the selected
20
- // authored row (for example Drei Detailed), rather than wrapping it in the
21
- // old entity preview object this helper was originally extracted for.
8
+ // An adapter can expose its native LOD directly as the authored row.
22
9
  if ((obj as THREE.LOD).isLOD) return obj as THREE.LOD;
23
10
  const stack = [...obj.children];
24
11
  while (stack.length > 0) {
@@ -1,42 +1,4 @@
1
- /**
2
- * One answer to "which live `Object3D` is this entity id?", for every editor
3
- * surface that needs one.
4
- *
5
- * There are TWO places an id can resolve, and neither is a superset of the
6
- * other. The ACTIVE ADAPTER's `hierarchy.object3D` is authoritative: a
7
- * source-backed component may select one identity and render its transform on a
8
- * child, and an ADOPTED live scene (play mode) is walked by the adapter, so its
9
- * ids exist there and nowhere else. `EditorShellStore.objectMap` is the shell's
10
- * own map, which the adapter's answer falls back to.
11
- *
12
- * Asking only the store is how `editor.frame(id)` came to refuse an entity the
13
- * same session had just selected and drawn a cage around. Measured live
14
- * (2026-08-14, the translated platformer in play mode): `editor.status()`
15
- * listed `r3f:world:o4a7fjmzo`, `editor.select` on it put a bracket cage on the
16
- * coin — and `editor.frame` on that same id answered
17
- * `Entity not found: r3f:world:o4a7fjmzo`, because the adopted play scene's
18
- * nodes live in the adapter's walk and not in `objectMap`. A verb's EXISTENCE
19
- * CHECK has to be the resolver its ACTION uses, or the refusal is about a
20
- * different question than the one asked.
21
- *
22
- * ## The REVERSE direction lives here too, and that is the point
23
- *
24
- * `userData.entityId` is the stamp an adapter's walk writes when it ADMITS an
25
- * object as an authoring node (`projection/three.ts`,
26
- * `R3fSourceAuthoringAdapter.indexGraph`). Reading that stamp is the node → id
27
- * half of the very same question `entityObject3D` answers id → node, so both
28
- * halves are spelled once, here — and the surfaces that used to reach for
29
- * `getUserData(object, 'entityId')` themselves (the viewport gizmo and its
30
- * surface-snap, the raycast climb, the store's adoption index, the LOD walk)
31
- * ask this module instead.
32
- *
33
- * Presence of the stamp is also the ADMISSION answer: an object the walk
34
- * skipped (an editor helper, a library-built internal folded into its owner)
35
- * carries none, which is exactly what {@link isEntityObject} and
36
- * {@link nearestEntityObject} read. Do not re-derive that from a type test, a
37
- * name convention or a layer — those disagree with the walk, and a surface that
38
- * disagrees with the walk is the "one id, three answers" defect.
39
- */
1
+ /** Resolve an entity through the active adapter’s nativeObject, then the shell’s object map. Share the authoring walk’s ownership rules across editor surfaces. */
40
2
 
41
3
  import type { AuthoringAdapter } from '@volter/project/adapter';
42
4
  import { getUserData, setUserData } from '@volter/editor-threejs/ecs/user-data';
@@ -3,50 +3,37 @@
3
3
  *
4
4
  * `hierarchy-component-marks.ts` is deliberately pure — it takes a
5
5
  * {@link NodeMarkReader} and never learns what a node is made of — and
6
- * `GameHierarchy` is deliberately contract-only (rule zero: no 3D-library value
6
+ * `Hierarchy` is deliberately contract-only (rule zero: no 3D-library value
7
7
  * import). This module is the seam between them: it asks the adapter's OPTIONAL
8
- * `object3D` capability for the node and reads
9
- * `@volter/editor-threejs/adapter/hierarchy-marks`' two marks off it.
8
+ * `nativeObject` capability for the node and reads its component off it.
10
9
  *
11
10
  * An adapter with no `Object3D` seam (React, Pixi, DOM) yields {@link NO_MARKS},
12
11
  * which `componentMarkView` recognizes by identity and short-circuits on — so a
13
12
  * non-three surface pays nothing and renders byte-identically.
14
13
  */
15
14
 
15
+ import type * as THREE from 'three';
16
16
  import type { HierarchyProvider } from '@volter/project/adapter';
17
- import { componentRootName, isBuiltInternal } from '@volter/editor-threejs/adapter/hierarchy-marks';
18
17
  import { getUserData } from '@volter/editor-threejs/ecs/user-data';
19
18
  import { isComponentInstanceRoot } from './authoring/component-instance-root';
20
- import { NO_MARKS, type NodeMarkReader, type NodeMarks } from '@volter/sdk/kit/hierarchy-component-marks';
19
+ import { NO_MARKS, type NodeMarkReader, type NodeMarks } from '@volter/sdk/hierarchy-component-marks';
21
20
  import { threeObject } from '../adapter/three-contract';
22
21
 
23
22
  /** Project-local R3F source already carries its component ownership on the
24
- * native node. Read that existing stamp as the automatic equivalent of an
25
- * imperative `markComponentRoot`; explicit marks still win. */
26
- function componentName(object: Parameters<typeof componentRootName>[0]): string | undefined {
27
- const explicit = componentRootName(object);
23
+ * native node; that stamp is the component mark. */
24
+ function componentName(object: THREE.Object3D): string | undefined {
28
25
  // Some project-local components are the native authoring ENTITY constructor rather than a
29
26
  // reusable component boundary. Importers are the canonical case: `<UnityNode>` creates the
30
27
  // GameObject itself, while a generated `<Player>` prefab remains a component instance around
31
28
  // that node. `authoringRoot` is the existing generic declaration for exactly that distinction.
32
- // An explicit component mark still wins, so a prefab root can be both its own authoring object
33
- // and the boundary of the reusable prefab instance. A built-internal mark suppresses only the
34
- // automatic component classification: a React helper that renders implementation machinery is
35
- // still implementation, not a prefab merely because Fiber recorded its component boundary.
36
- if (
37
- explicit ||
38
- isBuiltInternal(object) ||
39
- getUserData(object, 'authoringRoot') === true ||
40
- !isComponentInstanceRoot(object)
41
- ) {
42
- return explicit;
29
+ if (getUserData(object, 'authoringRoot') === true || !isComponentInstanceRoot(object)) {
30
+ return undefined;
43
31
  }
44
32
  const authored = getUserData(object, 'authoringComponent');
45
33
  return typeof authored === 'string' && authored ? authored : undefined;
46
34
  }
47
-
48
35
  /**
49
- * A memoized reader over `hierarchy.object3D`.
36
+ * A memoized reader over `hierarchy.nativeObject`.
50
37
  *
51
38
  * The cache lives for the reader's lifetime — one render pass — because the
52
39
  * live graph it reads is re-walked every render anyway (`hierarchy.roots()` IS
@@ -54,19 +41,15 @@ function componentName(object: Parameters<typeof componentRootName>[0]): string
54
41
  * and the walk asks for the same id many times: once per `node()` projection,
55
42
  * again per internals probe.
56
43
  */
57
- export function markReaderFor(hierarchy: Pick<HierarchyProvider, 'object3D'>): NodeMarkReader {
58
- const object3D = hierarchy.object3D;
59
- if (typeof object3D !== 'function') return NO_MARKS;
44
+ export function markReaderFor(hierarchy: Pick<HierarchyProvider, 'nativeObject'>): NodeMarkReader {
45
+ const nativeObject = hierarchy.nativeObject;
46
+ if (typeof nativeObject !== 'function') return NO_MARKS;
60
47
  const cache = new Map<string, NodeMarks | undefined>();
61
48
  return (id) => {
62
49
  if (cache.has(id)) return cache.get(id);
63
50
  const object = threeObject(hierarchy, id);
64
51
  const marks: NodeMarks | undefined = object
65
- ? {
66
- componentRoot: componentName(object),
67
- authoredEntity: getUserData(object, 'authoringRoot') === true,
68
- builtInternal: isBuiltInternal(object),
69
- }
52
+ ? { componentRoot: componentName(object) }
70
53
  : undefined;
71
54
  cache.set(id, marks);
72
55
  return marks;
@@ -1,50 +1,4 @@
1
- /**
2
- * What an `InstancedMesh` IS to a reader looking at it — derived by measuring
3
- * the live object, never by guessing what the game meant.
4
- *
5
- * ## The two questions, and why the second one exists
6
- *
7
- * 1. **How many units does this row draw?** Every other count in the editor is
8
- * keyed on OBJECTS, and an `InstancedMesh` is one object drawing N units, so
9
- * a hierarchy row reports 1 where a reader sees 200 — both numbers correct,
10
- * no error anywhere. `ThreeWalkStats.instances`
11
- * (`projection/three.ts`) is the world-level half of the same
12
- * shortfall; this is the per-node half.
13
- *
14
- * 2. **Is this a WORLD-ANCHORED instanced system?** The measured case
15
- * (2026-08-16, the vendored racing-game): `Dust` and `Skid` write per-unit
16
- * matrices in WORLD coordinates — `wheels[2].current.getWorldPosition(v)` →
17
- * `setItemAt` — while the `instancedMesh` container sits at identity at the
18
- * world origin, deliberately, because a skid mark must stay where it was laid
19
- * (`vendor/games/racing-game/src/models/vehicle/Vehicle.tsx` renders both
20
- * OUTSIDE `<Chassis>` for exactly that reason). Selecting one put the
21
- * selection cage and the gizmo on (0,0,0) while every unit the reader can see
22
- * is out at the car — measured live: focusing `Dust` flew the camera inside
23
- * the canyon to two bracket marks at the origin, which reads as a broken
24
- * editor rather than as a correctly-drawn trail system.
25
- *
26
- * The class is the native-engine vocabulary (Unity calls it simulation space).
27
- * We do not read the game's intent to find it: the SIGNATURE is measurable and
28
- * three.js forces it — to write world coordinates into instance matrices the
29
- * container's own world matrix MUST be identity, so "identity container, content
30
- * elsewhere" is the pattern's fingerprint and not an inference about it.
31
- *
32
- * ## What "elsewhere" is measured against, and why it is not a threshold
33
- *
34
- * The reference volume is the container's OWN drawn geometry, sitting at its own
35
- * origin. That is the only intrinsic scale an instanced draw has, and it makes
36
- * the test tuning-free: content whose centre still falls inside the shape the
37
- * container itself draws is content the pivot is already on top of, and nothing
38
- * about that reads as broken.
39
- *
40
- * It also makes the answer honest at REST, which matters more than it looks.
41
- * `new InstancedMesh(geometry, material, count)` seeds every unit to IDENTITY
42
- * (three r180 `InstancedMesh.js` — the constructor's `setMatrixAt(i, _identity)`
43
- * loop), so a system that has placed nothing yet genuinely draws `count` copies
44
- * stacked on the container's origin. There is no world-space anchoring to see
45
- * there, and the class correctly does not fire until the game has actually put
46
- * content somewhere else.
47
- */
1
+ /** Report an InstancedMesh’s drawn unit count and whether its content is anchored in world space, using the live object’s instance matrices and bounds. */
48
2
 
49
3
  import * as THREE from 'three';
50
4
 
@@ -0,0 +1,35 @@
1
+ /** Hold absorb-by-remount swaps until continuous viewport gestures finish. The viewport sets the lock; the design session waits through whenLiveGestureIdle. */
2
+
3
+ let active = 0;
4
+ let waiters: Array<() => void> = [];
5
+
6
+ export function beginLiveGesture(): void {
7
+ active += 1;
8
+ }
9
+
10
+ export function endLiveGesture(): void {
11
+ active = Math.max(0, active - 1);
12
+ if (active === 0) {
13
+ const resolved = waiters;
14
+ waiters = [];
15
+ for (const resolve of resolved) resolve();
16
+ }
17
+ }
18
+
19
+ /** Whether any gesture (a drag, or a gesture's still-landing write) holds
20
+ * the lock right now — the swap's settle loop re-checks this after each
21
+ * wait, because a lock taken in the same task that released the previous
22
+ * one can race a waiter that already resolved. */
23
+ export function liveGestureActive(): boolean {
24
+ return active > 0;
25
+ }
26
+
27
+ /** Resolves immediately when no gesture is running, else on the current
28
+ * gesture's end. */
29
+ export function whenLiveGestureIdle(): Promise<void> {
30
+ return active === 0
31
+ ? Promise.resolve()
32
+ : new Promise((resolve) => {
33
+ waiters.push(resolve);
34
+ });
35
+ }
@@ -1,31 +1,11 @@
1
1
  /**
2
- * The module→`Object3D` step, IN THE BROWSER — the live twin of the bake
3
- * lane's Node-side `module-source.ts`
4
- * (`catalog/project-source/editor/tools/module-source.ts`), and deliberately the
5
- * SAME contract: a project TypeScript module whose export builds a native
6
- * `THREE.Object3D`. `project.bake.module` / `project.bake.preview` /
7
- * the editor's `screenshot <module>` command already define that contract; the live modeling
8
- * document opens the same thing, so a module that bakes opens, and a module
9
- * that opens bakes (docs/BLENDER-PARITY.md §The model file).
10
- *
11
- * TWO HALVES, ONE CONTRACT. Node imports a `file://` url and has no GPU; the
12
- * browser imports the dev server's `/@fs/` url and IS the GPU. Nothing else
13
- * differs, which is why this file re-states the resolution rather than
14
- * importing the tool's copy: that copy is `host: 'node'` capability source
15
- * (`node:fs/promises`, `node:url`) that a project's own tsconfig excludes from
16
- * its browser build. Sharing the module would drag Node into the editor
17
- * bundle; sharing the CONTRACT is what matters, and the contract is small
18
- * enough to be stated twice and obviously identical.
19
- *
20
- * DETECTION IS STRUCTURAL, not a filename convention — the quarks precedent
21
- * (`components/asset-viewers/JsonAssetDocument.tsx`: three.quarks names no
22
- * extension of its own, so the file's own structure is what says whether it is
23
- * a particle document). There is no `.model.ts`: a project `.ts`/`.tsx`
24
- * whose export RESOLVES to an `Object3D` is a model module, and everything
25
- * else falls back to the ordinary source viewer.
2
+ * Resolve a project module's export to a native THREE.Object3D in the browser. Import the module
3
+ * through the session's /@fs/ URL and use its default export, or its single named export. A builder
4
+ * may be asynchronous and return an Object3D or a result with root and optional animations/disposal.
5
+ * Detection is structural; modules with other exports use the ordinary source viewer.
26
6
  */
27
7
 
28
- import { liveModuleImportUrl } from '@volter/sdk/session/project-module-url';
8
+ import { liveModuleImportUrl } from '@volter/sdk/project-module-url';
29
9
  import type * as THREE from 'three';
30
10
 
31
11
  /** Why this module is not a live model module — the sentence the document
@@ -71,13 +51,8 @@ function isObject3D(value: unknown): value is THREE.Object3D {
71
51
  );
72
52
  }
73
53
 
74
- /**
75
- * Pick the export this module offers as its subject, matching the bake lane's
76
- * `exportName` default: the DEFAULT export, or — when there is no default —
77
- * the single named export. Two named exports and no default is ambiguous and
78
- * refuses by name rather than guessing; the bake verbs make the caller say
79
- * which one, and a document has nobody to ask.
80
- */
54
+ /** Choose the default export, or the single named export. Multiple named exports without a
55
+ * default refuse as ambiguous rather than guessing which subject the document should build. */
81
56
  export function pickLiveModuleExport(
82
57
  namespace: Record<string, unknown>,
83
58
  ): { readonly name: string; readonly value: unknown } | LiveModuleRefusal {
@@ -99,20 +74,9 @@ export function pickLiveModuleExport(
99
74
  }
100
75
 
101
76
  /**
102
- * THE TWO SHAPES A MODEL MODULE MAY PRODUCE — the same pair the Node twin
103
- * normalizes (`catalog/project-source/editor/tools/module-source.ts`'s
104
- * `asModuleBuild`), because a module that bakes must open and a module that
105
- * opens must bake:
106
- *
107
- * 1. a bare `THREE.Object3D`;
108
- * 2. a BUILD RESULT `{ root, animations?, dispose? }` — what every parametric
109
- * lib in the kit returns (`createBird`'s `BirdBuild` and its descendants),
110
- * and what `bakeObject3DSource`'s `build` callback has always taken.
111
- *
112
- * `animations` is written onto `root.animations`, three's own carrier for a
113
- * model's clips — so the clip list travels with the graph the document mounts
114
- * and every reader downstream (the Object3D document session, its animation
115
- * transport) finds it where it finds a GLTF's.
77
+ * Accept a bare Object3D or `{ root, animations?, dispose? }`. Supplied animations fill an empty
78
+ * root.animations list so clips travel with the graph the document and its transport inspect.
79
+ * A supplied disposer replaces generic graph disposal.
116
80
  */
117
81
  function asLiveModuleBuild(
118
82
  built: unknown,
@@ -150,12 +114,8 @@ const ACCEPTED_SHAPES =
150
114
  'a model module returns a THREE.Object3D, or a build result ' +
151
115
  '`{ root, animations?, dispose? }` whose `root` is one.';
152
116
 
153
- /**
154
- * Resolve a picked export to an `Object3D`. A FUNCTION is called (its result
155
- * may be a promise — the bake lane awaits it too); a VALUE is taken as-is, so
156
- * `export default new THREE.Group()` is as legitimate a model module as
157
- * `export default () => …`.
158
- */
117
+ /** Resolve a picked export to an Object3D: accept its value directly, or call a builder and
118
+ * await its result. Both `export default new THREE.Group()` and a builder are supported. */
159
119
  export async function resolveLiveModuleExport(
160
120
  name: string,
161
121
  value: unknown,
@@ -12,7 +12,7 @@
12
12
  * - Self-cleaning: each loaded object is disposed after the thumbnail is captured.
13
13
  */
14
14
 
15
- import { registerAssetThumbnailRenderer } from '@volter/sdk/kit/asset-thumbnails';
15
+ import { registerAssetThumbnailRenderer } from '@volter/sdk/asset-thumbnails';
16
16
  import type { SparkRenderer, SplatMesh } from '@sparkjsdev/spark';
17
17
  import { markHostRenderer } from '@volter/editor-threejs/viewport/renderer-ownership';
18
18
  import { loadSplat } from '@volter/editor-threejs/asset-loaders';
@@ -44,7 +44,6 @@ export type ModelThumbnailFormat =
44
44
  export interface ModelThumbnailSpec {
45
45
  url: string;
46
46
  format?: ModelThumbnailFormat;
47
- materialUrl?: string;
48
47
  background?: 'transparent' | 'neutral';
49
48
  output?: 'png' | 'webp';
50
49
  }
@@ -344,7 +343,7 @@ export async function loadModelThumbnailObject(
344
343
  }
345
344
  case 'obj': {
346
345
  const { OBJLoader } = await import('three/addons/loaders/OBJLoader.js');
347
- const materialUrl = spec.materialUrl ?? (await discoverObjMaterial(spec.url));
346
+ const materialUrl = await discoverObjMaterial(spec.url);
348
347
  const loader = new OBJLoader();
349
348
  if (materialUrl) {
350
349
  const { MTLLoader } = await import('three/addons/loaders/MTLLoader.js');
@@ -357,6 +356,8 @@ export async function loadModelThumbnailObject(
357
356
  case 'dae': {
358
357
  const { ColladaLoader } = await import('three/addons/loaders/ColladaLoader.js');
359
358
  const collada = await new ColladaLoader().loadAsync(spec.url);
359
+ // The loader answers null for a file that is not a COLLADA document (stated in r186's types).
360
+ if (!collada) throw new Error(`${spec.url} is not a COLLADA document`);
360
361
  const animations = (collada as unknown as { animations?: THREE.AnimationClip[] }).animations;
361
362
  if (animations) collada.scene.animations = [...animations];
362
363
  return collada.scene;
@@ -481,35 +482,6 @@ async function discoverObjMaterial(url: string): Promise<string | undefined> {
481
482
  }
482
483
  }
483
484
 
484
- /** Normalize an upstream/source preview image through the same thumbnail canvas. */
485
- export async function renderImageThumbnail(
486
- url: string,
487
- options: { output?: 'png' | 'webp'; background?: 'transparent' | 'neutral' } = {},
488
- ): Promise<string> {
489
- const image = await new Promise<HTMLImageElement>((resolve, reject) => {
490
- const element = new Image();
491
- element.onload = () => resolve(element);
492
- element.onerror = () => reject(new Error(`Failed to load preview image: ${url}`));
493
- element.src = url;
494
- });
495
- const canvas = document.createElement('canvas');
496
- canvas.width = SIZE;
497
- canvas.height = SIZE;
498
- const context = canvas.getContext('2d');
499
- if (!context) throw new Error('2D thumbnail canvas is unavailable.');
500
- if (options.background === 'neutral') {
501
- context.fillStyle = '#20242a';
502
- context.fillRect(0, 0, SIZE, SIZE);
503
- }
504
- const scale = Math.min(SIZE / image.naturalWidth, SIZE / image.naturalHeight);
505
- const width = image.naturalWidth * scale;
506
- const height = image.naturalHeight * scale;
507
- context.drawImage(image, (SIZE - width) / 2, (SIZE - height) / 2, width, height);
508
- return options.output === 'webp'
509
- ? canvas.toDataURL('image/webp', 0.86)
510
- : canvas.toDataURL('image/png');
511
- }
512
-
513
485
  /** Get (or create) the shared thumbnail renderer. */
514
486
  export function getModelThumbnailRenderer(): ModelThumbnailRenderer {
515
487
  if (!instance) instance = new ModelThumbnailRenderer();
@@ -518,13 +490,13 @@ export function getModelThumbnailRenderer(): ModelThumbnailRenderer {
518
490
 
519
491
  // A Fast Refresh invalidation creates a fresh module singleton. Release the
520
492
  // previous module's offscreen WebGL context first instead of silently leaking
521
- // one context per AssetBrowser edit.
493
+ // one context per edit.
522
494
  if (import.meta.hot) {
523
495
  import.meta.hot.dispose(() => instance?.dispose());
524
496
  }
525
497
 
526
- /** This renderer, as the asset browser's thumbnail for every model format it
527
- * loads (`@volter/sdk/kit/asset-thumbnails`). */
498
+ /** This renderer, as the asset thumbnail for every model format it loads
499
+ * (`@volter/sdk/asset-thumbnails`). */
528
500
  export function registerModelThumbnails(): () => void {
529
501
  return registerAssetThumbnailRenderer({
530
502
  accepts: (url) => modelThumbnailFormat(url) !== undefined,
@@ -1,13 +1,13 @@
1
1
  /**
2
2
  * THE PLAY ENTRY CAMERA FLIGHT — the Three integration's half of Play's immersive entry
3
- * (`@volter/sdk/kit/play-camera-flight`). The kit's transition owns the phases and the
3
+ * (`@volter/sdk/play-camera-flight`). The kit's transition owns the phases and the
4
4
  * chrome; this owns the camera: the editor viewport flies along a Catmull-Rom spline from its
5
5
  * preview pose to the AUTHORED game camera's pose, retargets to the game's live camera once it
6
6
  * boots so the cross-fade is pixel-continuous, and restores the preview pose when Play ends. The
7
7
  * flight reads the authored viewport's rig through the viewport door and rides its frame hook, so
8
8
  * flight pacing rides the editor's own frame clock.
9
9
  */
10
- import { registerPlayCameraFlight, type LiveCameraLookup } from '@volter/sdk/kit/play-camera-flight';
10
+ import { registerPlayCameraFlight, type LiveCameraLookup } from '@volter/sdk/play-camera-flight';
11
11
  import { onViewportFrame, viewportRig } from '../viewport-door';
12
12
  import * as THREE from 'three';
13
13
  import { threeStoreForHost } from './three-state';
@@ -105,17 +105,8 @@ function targetFromObject(
105
105
  }
106
106
 
107
107
  /**
108
- * Find the authored game camera in the CURRENT (pre-play) editor scene.
109
- *
110
- * A world's source IS its document, so there is no descriptor to read a
111
- * declared camera out of: the design session adopts the mounted fiber scene
112
- * into the store, which makes the authored camera (e.g. drei
113
- * `<PerspectiveCamera makeDefault>`) a live `THREE.Camera` in the mapped
114
- * object graphs. Take the first one outside editor-only (layer-31) and gizmo
115
- * branches, at its live world transform.
116
- *
117
- * `null` when no camera is found — play then keeps today's behavior (game
118
- * starts at the preview pose, no flight).
108
+ * Find the authored camera in the current editor scene outside editor-only and gizmo branches, and
109
+ * return its live world transform. Return null when the mapped object graph contains no camera.
119
110
  */
120
111
  export function resolveAuthoredCameraTarget(
121
112
  store: PlayCameraSourceStore,
@@ -152,8 +143,8 @@ interface ActiveFlight {
152
143
  readonly runtime: FlightRuntime;
153
144
  readonly snapshot: { position: THREE.Vector3; target: THREE.Vector3; up: THREE.Vector3; fov: number };
154
145
  readonly onLanded: () => void;
155
- /** Live game camera, registered at game-ready — the flight retargets to it
156
- * each frame so the hand-off converges on whatever the game actually does
146
+ /** Live camera, registered at live-ready — the flight retargets to it
147
+ * each frame so the hand-off converges on whatever the project actually does
157
148
  * with its camera during boot (follow rigs, SceneCamera components…). */
158
149
  getLiveCamera: LiveCameraLookup | null;
159
150
  /** Driving the camera: from `begin` until the game is on screen (`settle`) or Play ends. */
@@ -197,7 +188,7 @@ function begin(onLanded: () => void): boolean {
197
188
  }
198
189
 
199
190
  /** After the flight, leave OrbitControls in a sane state at the landed pose
200
- * so a `set-viewport-tab scene` during play still orbits correctly. */
191
+ * so the viewport still orbits correctly during play. */
201
192
  function settle(): void {
202
193
  const viewport = viewportRig();
203
194
  const active = _flight;