@volter/editor-threejs 0.5.210 → 0.5.212
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/NOTICE +7 -2
- package/README.md +8 -13
- package/contributions/animation-mixers.service.ts +2 -4
- package/contributions/animation-timeline.utility.tsx +1 -2
- package/contributions/three.page.ts +50 -0
- package/dist-node/serving.mjs +84 -3
- package/dist-node/shared-three.mjs +145 -0
- package/package.json +296 -16
- package/serving/animation-stamp.ts +1 -1
- package/serving/index.ts +22 -5
- package/serving/model-import-conversion.ts +6 -3
- package/serving/shared-three.ts +176 -0
- package/src/adapter/constraint.ts +1 -18
- package/src/adapter/object3d-authoring-subject.ts +1 -12
- package/src/adapter/reflection-probe.ts +1 -9
- package/src/adapter/three-contract.ts +6 -60
- package/src/animation/live-mixers.ts +0 -4
- package/src/api.ts +7 -0
- package/src/asset-loaders.ts +2 -178
- package/src/capture/photograph-sampling.ts +333 -0
- package/src/capture/scene.ts +1 -1
- package/src/ecs/object-marks.ts +6 -14
- package/src/ecs/user-data.ts +3 -138
- package/src/kit/animation/three-clips-subject.ts +19 -42
- package/src/kit/asset-compare-core.ts +161 -0
- package/src/kit/asset-compare.ts +8 -27
- package/src/kit/asset-preview-command.ts +14 -19
- package/src/kit/asset-preview-framing.ts +8 -67
- package/src/kit/asset-preview.ts +109 -342
- package/src/kit/asset-workflow/model-inspection.ts +11 -154
- package/src/kit/asset-workflow/preview-resource-lifetime.ts +25 -0
- package/src/kit/authoring/component-instance-root.ts +3 -39
- package/src/kit/authoring/live-object-transform.ts +1 -30
- package/src/kit/authoring/object3d-document-session-registry.ts +7 -15
- package/src/kit/authoring/object3d-document-session.ts +471 -208
- package/src/kit/authoring/shell-viewport-policy.ts +18 -24
- package/src/kit/authoring/source-object3d-authoring-adapter.ts +6 -48
- package/src/kit/authoring/three-projection-core.ts +11 -40
- package/src/kit/authoring/viewport-pick-context.ts +5 -12
- package/src/kit/authoring/viewport-raycast.ts +15 -93
- package/src/kit/authoring/world-hidden-viewport.ts +8 -61
- package/src/kit/capture-camera-pose.ts +68 -0
- package/src/kit/collaboration-presence.ts +41 -0
- package/src/kit/components/CameraInfo.tsx +4 -6
- package/src/kit/components/InspectorObjectPreview.tsx +3 -30
- package/src/kit/components/Object3DDocumentToolbar.css +75 -0
- package/src/kit/components/Object3DDocumentToolbar.tsx +18 -84
- package/src/kit/components/Object3DDocumentViewport.tsx +5 -8
- package/src/kit/components/StageHost.css +40 -0
- package/src/kit/components/StageHost.tsx +181 -404
- package/src/kit/components/StageOverlays.tsx +1 -2
- package/src/kit/components/StatsOverlay.tsx +5 -17
- package/src/kit/components/ViewportFurniture.tsx +27 -154
- package/src/kit/components/ViewportOverlay.tsx +13 -17
- package/src/kit/components/ViewportOverlaysMenu.tsx +177 -0
- package/src/kit/components/ViewportShadingMenu.tsx +2 -42
- package/src/kit/components/ViewportViewMenu.tsx +7 -18
- package/src/kit/components/asset-viewers/EntityModelDocument.tsx +4 -13
- package/src/kit/components/asset-viewers/EnvironmentAssetDocument.tsx +5 -5
- package/src/kit/components/asset-viewers/LiveModuleDocument.tsx +24 -102
- package/src/kit/components/asset-viewers/LutAssetDocument.tsx +6 -6
- package/src/kit/components/asset-viewers/Object3DPreview.tsx +10 -45
- package/src/kit/components/asset-viewers/QuarksAssetDocument.tsx +2 -2
- package/src/kit/components/asset-viewers/ShaderAssetDocument.tsx +11 -10
- package/src/kit/components/asset-viewers/shader-source.ts +144 -0
- package/src/kit/components/asset-viewers/three-asset-viewers.tsx +2 -14
- package/src/kit/components/object3d-contribution-surfaces.tsx +1 -1
- package/src/kit/components/stage-keyboard.tsx +6 -12
- package/src/kit/components/stage-overlay-set.tsx +11 -20
- package/src/kit/components/stage-presence-markers.ts +9 -32
- package/src/kit/components/stage-transform-chrome.ts +4 -4
- package/src/kit/components/stage-transform-tools.tsx +7 -14
- package/src/kit/components/standard-viewport-dressing.ts +47 -167
- package/src/kit/components/world-root-binding.ts +2 -7
- package/src/kit/constraint-helper.ts +1 -1
- package/src/kit/document-renderer-session.ts +114 -0
- package/src/kit/editor-presentation-activity.ts +58 -0
- package/src/kit/editor-shell-store.ts +31 -403
- package/src/kit/editor-viewport.ts +476 -1762
- package/src/kit/entity-lod.ts +3 -16
- package/src/kit/entity-object.ts +1 -39
- package/src/kit/hierarchy-mark-reader.ts +13 -30
- package/src/kit/instanced-presentation.ts +1 -47
- package/src/kit/live-gesture-lock.ts +35 -0
- package/src/kit/live-module-source.ts +12 -52
- package/src/kit/model-thumbnail.ts +7 -35
- package/src/kit/play-camera-flight.ts +7 -16
- package/src/kit/projection/three.ts +13 -366
- package/src/kit/retained-document-states.ts +91 -0
- package/src/kit/scene-document-viewport.ts +3 -3
- package/src/kit/scene-framing.ts +11 -152
- package/src/kit/scene-view-fog.ts +2 -47
- package/src/kit/spatial-handle-visuals.ts +19 -28
- package/src/kit/stories/three-story-model.ts +5 -33
- package/src/kit/three-canvas-render.ts +3 -4
- package/src/kit/three-hierarchy-row-media.ts +3 -11
- package/src/kit/three-inspection-media.ts +6 -6
- package/src/kit/three-integration.ts +18 -18
- package/src/kit/three-state.ts +2 -19
- package/src/kit/three-viewport/camera-fit.ts +2 -6
- package/src/kit/three-viewport/selection-brackets.ts +5 -20
- package/src/kit/three-viewport/selection-outline.ts +4 -56
- package/src/kit/three-viewport/skeleton-helper.ts +1 -32
- package/src/kit/three-viewport/source-color.ts +1 -33
- package/src/kit/three-viewport/studio-environment.ts +4 -55
- package/src/kit/three-viewport-presentation.ts +23 -0
- package/src/kit/transport-strip.tsx +147 -0
- package/src/kit/viewport-actions.ts +8 -50
- package/src/kit/viewport-authoring-policy.ts +5 -52
- package/src/kit/viewport-commands.ts +28 -57
- package/src/kit/viewport-hotkeys.ts +29 -31
- package/src/kit/viewport-status-facet.ts +4 -5
- package/src/kit/viewport-surface-status.tsx +44 -0
- package/src/loader.ts +8 -71
- package/src/object3d-contributions.ts +18 -34
- package/src/viewport/clip-planes.ts +4 -32
- package/src/viewport/content-bounds.ts +13 -142
- package/src/viewport/editor-layers.ts +4 -26
- package/src/viewport/environment.ts +1 -1
- package/src/viewport/preview-renderer.ts +7 -45
- package/src/viewport/renderer-ownership.ts +14 -62
- package/src/viewport-api.ts +14 -33
- package/src/viewport-door.ts +16 -86
- package/contributions/three-integration.service.ts +0 -13
- package/src/adapter/hierarchy-marks.ts +0 -156
- package/src/adapter/ingest/scene-capture.ts +0 -843
- package/src/adapter/ingest/structural-ids.ts +0 -139
- package/src/adapter/ingest/visible-capture-window.ts +0 -302
- package/src/adapter/renderer-config.ts +0 -115
- package/src/asset-parse-error.ts +0 -29
- package/src/host-hierarchy-objects.ts +0 -31
- package/src/kit/authoring/design-time-settle.ts +0 -343
- package/src/kit/camera-authoring.ts +0 -175
package/src/kit/entity-lod.ts
CHANGED
|
@@ -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
|
|
6
|
+
isNestedAuthoringNode: (object: THREE.Object3D) => boolean,
|
|
18
7
|
): THREE.LOD | null {
|
|
19
|
-
//
|
|
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) {
|
package/src/kit/entity-object.ts
CHANGED
|
@@ -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
|
-
* `
|
|
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
|
-
* `
|
|
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/
|
|
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
|
|
25
|
-
|
|
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
|
-
|
|
33
|
-
|
|
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.
|
|
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, '
|
|
58
|
-
const
|
|
59
|
-
if (typeof
|
|
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
|
-
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
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/
|
|
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
|
-
*
|
|
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
|
-
*
|
|
103
|
-
*
|
|
104
|
-
*
|
|
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
|
-
*
|
|
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/
|
|
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 =
|
|
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
|
|
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
|
|
527
|
-
*
|
|
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/
|
|
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/
|
|
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
|
|
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
|
|
156
|
-
* each frame so the hand-off converges on whatever the
|
|
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
|
|
191
|
+
* so the viewport still orbits correctly during play. */
|
|
201
192
|
function settle(): void {
|
|
202
193
|
const viewport = viewportRig();
|
|
203
194
|
const active = _flight;
|