@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
|
@@ -1,31 +1,4 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* How far a Three authoring view's perspective camera can see.
|
|
3
|
-
*
|
|
4
|
-
* ## The measured defect this exists for
|
|
5
|
-
*
|
|
6
|
-
* The editor camera was constructed once, with `far = 1000`, and never
|
|
7
|
-
* adjusted. That is fine for the starter template (a cube on a 50-unit grid)
|
|
8
|
-
* and wrong for anything world-sized: framing a world several hundred units
|
|
9
|
-
* across puts the camera past the far plane, so every triangle is clipped and
|
|
10
|
-
* the Scene viewport is a flat, empty field. The failure is graded, which is
|
|
11
|
-
* what made it confusing — at a 454-unit framing the world draws; at 1099 only
|
|
12
|
-
* the fragments within 1000 survive; at 1276 nothing does. A game whose own
|
|
13
|
-
* camera declares `far = 10000` is stating the honest scale of its world, and
|
|
14
|
-
* the editor camera has to reach it.
|
|
15
|
-
*
|
|
16
|
-
* Nothing about that is ingest-specific — a first-party world larger than
|
|
17
|
-
* ~1000 units disappears the same way — so the fix is a property of the editor
|
|
18
|
-
* camera, not of the ingest route.
|
|
19
|
-
*
|
|
20
|
-
* ## Why grow `far` instead of just setting it large
|
|
21
|
-
*
|
|
22
|
-
* The depth buffer's precision is governed by `far / near`, not by `far`. A
|
|
23
|
-
* blanket `far = 100000` would give every small scene a 10^6 ratio and visible
|
|
24
|
-
* z-fighting. So `far` tracks the content the camera actually has to reach,
|
|
25
|
-
* and `near` is pushed out only as much as is needed to keep the ratio bounded
|
|
26
|
-
* — which, for anything at the template's scale, is not at all: the planes come
|
|
27
|
-
* back exactly `0.1 / 1000`, the values the camera has always had.
|
|
28
|
-
*/
|
|
1
|
+
/** Fit authoring-camera clip planes to content while bounding their ratio for depth precision. Retain base planes for ordinary small scenes. */
|
|
29
2
|
|
|
30
3
|
/** The camera's minimum near plane — also the exact value for any small scene. */
|
|
31
4
|
export const EDITOR_CAMERA_NEAR = 0.1;
|
|
@@ -34,7 +7,7 @@ export const EDITOR_CAMERA_NEAR = 0.1;
|
|
|
34
7
|
export const EDITOR_CAMERA_FAR = 1000;
|
|
35
8
|
|
|
36
9
|
/**
|
|
37
|
-
* Ceiling on `far / near`. 0.1
|
|
10
|
+
* Ceiling on `far / near`. 1000/0.1 is 10^4, so this leaves one doubling of
|
|
38
11
|
* headroom before `near` starts moving at all, and caps the depth-precision
|
|
39
12
|
* loss a very large world can inflict.
|
|
40
13
|
*/
|
|
@@ -47,9 +20,8 @@ const REACH_MARGIN = 1.25;
|
|
|
47
20
|
* Clip planes that reach `contentRadius` around a center `distanceToCenter`
|
|
48
21
|
* away, never tighter than the base planes.
|
|
49
22
|
*
|
|
50
|
-
* Pure and total: a non-finite or negative input
|
|
51
|
-
*
|
|
52
|
-
* could not be measured must still draw.
|
|
23
|
+
* Pure and total: a non-finite or negative input contributes zero to the reach.
|
|
24
|
+
* The remaining input still fits the content, and the base planes bound the result.
|
|
53
25
|
*/
|
|
54
26
|
export function fitClipPlanes(
|
|
55
27
|
distanceToCenter: number,
|
|
@@ -1,71 +1,16 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* World-space bounds of the GAME OBJECT a reader selected — the one walk every
|
|
3
|
-
* bounds consumer in the editor uses instead of `Box3.setFromObject`.
|
|
4
|
-
*
|
|
5
|
-
* ## Why `Box3.setFromObject` is the wrong instrument here
|
|
6
|
-
*
|
|
7
|
-
* `setFromObject` answers "what is the extent of this whole subtree", and a
|
|
8
|
-
* live game's subtree contains machinery as well as content. The marks in
|
|
9
|
-
* `@volter/editor-threejs/adapter/hierarchy-marks` already say which is which — a component
|
|
10
|
-
* WRAPS its functionality and MARKS the implementation it attaches — and the
|
|
11
|
-
* hierarchy panel and the drag rules already read them. Bounds is the third
|
|
12
|
-
* consumer, and it is the one where getting it wrong is visible on screen.
|
|
13
|
-
*
|
|
14
|
-
* The measurement that forced this (2026-08-14, the translated platformer's
|
|
15
|
-
* coins): a component root at `(12, 3, -5)` owning a 0.5 m coin mesh and, as a
|
|
16
|
-
* real child, its three.quarks `BatchedRenderer` with
|
|
17
|
-
* `matrixWorldAutoUpdate = false`. That renderer draws in WORLD space, so its
|
|
18
|
-
* identity `matrixWorld` is deliberate and correct for rendering — but its
|
|
19
|
-
* `VFXBatch` child carries the billboard template geometry, whose bounding box
|
|
20
|
-
* is `(-0.5, -0.5, 0)…(0.5, 0.5, 0)` and, at identity, sits AT THE WORLD
|
|
21
|
-
* ORIGIN. `setFromObject` unioned the two and returned
|
|
22
|
-
* `(-0.5, -0.5, -5.05)…(12.25, 3.25, 0)`: a selection cage stretching from the
|
|
23
|
-
* coin all the way back to the origin, on a coin that is 0.5 m across. The
|
|
24
|
-
* batch contributes that box whether or not a single particle is alive, so the
|
|
25
|
-
* cage was wrong from the first frame.
|
|
26
|
-
*
|
|
27
|
-
* That is the general shape, not a quarks quirk: ANY built-internal child whose
|
|
28
|
-
* world transform its owner writes by hand (`matrixWorldAutoUpdate = false` is
|
|
29
|
-
* the mechanism the marks' own header recommends) sits somewhere the subtree
|
|
30
|
-
* walk has no reason to expect, and pooled machinery routinely carries stale or
|
|
31
|
-
* template-sized geometry. Excluding implementation is the only rule that
|
|
32
|
-
* survives all of them.
|
|
33
|
-
*
|
|
34
|
-
* ## What is NOT excluded
|
|
35
|
-
*
|
|
36
|
-
* Editor furniture is deliberately still measured. `isEditorOwnedObject`
|
|
37
|
-
* matches `userData.engineInternal`, and the splat bounds proxy
|
|
38
|
-
* (`@volter/editor-threejs-runtime/asset-loaders`, `__volter_splat_bounds`) is engine-internal
|
|
39
|
-
* geometry that exists PRECISELY so generic focus/selection bounds can frame a
|
|
40
|
-
* Gaussian splat — a splat renders no `BufferGeometry` of its own. Excluding
|
|
41
|
-
* editor-owned nodes here would silently un-frame every splat. Consumers that
|
|
42
|
-
* want helpers out of their measurement (`_hasBoundableGeometry`, the asset
|
|
43
|
-
* preview's staging) filter for that themselves, on top of this walk.
|
|
44
|
-
*
|
|
45
|
-
* The selected object ITSELF is always measured, even when it is marked. A
|
|
46
|
-
* revealed internals row is selectable, and answering "where is it" with an
|
|
47
|
-
* empty box would be a refusal dressed as a measurement.
|
|
48
|
-
*/
|
|
1
|
+
/** Measure world-space content bounds for shared focus and selection consumers. Include editor-owned bound proxies such as Gaussian-splat geometry; callers may filter helpers separately. */
|
|
49
2
|
|
|
50
|
-
import { isBuiltInternal } from '@volter/editor-threejs/adapter/hierarchy-marks';
|
|
51
3
|
import * as THREE from 'three';
|
|
52
4
|
|
|
53
5
|
/**
|
|
54
|
-
* Visit `object` and every descendant
|
|
55
|
-
*
|
|
56
|
-
* Subtree-scoped, matching the mark's own contract: a marked node is skipped
|
|
57
|
-
* WITH everything under it, so a rig or a pool costs one check rather than one
|
|
58
|
-
* per bone. `object` itself is always visited — see the header.
|
|
6
|
+
* Visit `object` and every descendant, top-down.
|
|
59
7
|
*/
|
|
60
8
|
export function traverseContent(
|
|
61
9
|
object: THREE.Object3D,
|
|
62
10
|
visit: (node: THREE.Object3D) => void,
|
|
63
11
|
): void {
|
|
64
12
|
visit(object);
|
|
65
|
-
for (const child of object.children)
|
|
66
|
-
if (isBuiltInternal(child)) continue;
|
|
67
|
-
traverseContent(child, visit);
|
|
68
|
-
}
|
|
13
|
+
for (const child of object.children) traverseContent(child, visit);
|
|
69
14
|
}
|
|
70
15
|
|
|
71
16
|
/** Scratch for one node's transformed geometry box — this module is synchronous
|
|
@@ -73,31 +18,7 @@ export function traverseContent(
|
|
|
73
18
|
const nodeBox = new THREE.Box3();
|
|
74
19
|
const frameMatrix = new THREE.Matrix4();
|
|
75
20
|
|
|
76
|
-
/**
|
|
77
|
-
* Is this box a measurement at all?
|
|
78
|
-
*
|
|
79
|
-
* `Box3.isEmpty()` compares `max < min`, and EVERY comparison against `NaN` is
|
|
80
|
-
* false — so a box with a non-finite corner reports itself as a real,
|
|
81
|
-
* non-empty box, unions into every consumer, and turns the whole world's
|
|
82
|
-
* extent into `NaN`.
|
|
83
|
-
*
|
|
84
|
-
* That is not hypothetical and it is not the game's fault: a live world holds
|
|
85
|
-
* transforms that are not numbers yet. Measured on the racing-game mount
|
|
86
|
-
* (2026-08-15) — two of its meshes sit at `quaternion = (NaN, NaN, NaN, NaN)`
|
|
87
|
-
* while the game is PAUSED at frame zero, because a physics binding reads its
|
|
88
|
-
* worker's buffers before the worker has ever answered. The editor mounts an
|
|
89
|
-
* ingested game paused, so that is the exact state the first look measures.
|
|
90
|
-
* One such node poisoned all 71 boxes: the framing refused (`isEmpty()` false,
|
|
91
|
-
* fit distance `NaN`), the game-camera seed refused (`radius` `NaN`), and the
|
|
92
|
-
* Scene tab opened on the editor's boot pose — inside canyon geometry, a
|
|
93
|
-
* full-bleed rectangle of rock.
|
|
94
|
-
*
|
|
95
|
-
* The clip planes already made this decision for themselves
|
|
96
|
-
* (`viewport-clip-planes.ts`: "a camera whose bounds could not be measured
|
|
97
|
-
* must still draw"). Dropping the node HERE is the same decision made once, at
|
|
98
|
-
* the measurement, so framing, clipping and the selection cage all get the
|
|
99
|
-
* finite answer instead of each guarding separately.
|
|
100
|
-
*/
|
|
21
|
+
/** Reject non-finite corners before unioning a bounds box. Box3.isEmpty alone does not reject NaN, which would contaminate every derived extent. */
|
|
101
22
|
function isFiniteBox(box: THREE.Box3): boolean {
|
|
102
23
|
return (
|
|
103
24
|
Number.isFinite(box.min.x) &&
|
|
@@ -113,29 +34,7 @@ function isFiniteBox(box: THREE.Box3): boolean {
|
|
|
113
34
|
const instanceMatrix = new THREE.Matrix4();
|
|
114
35
|
const instanceBox = new THREE.Box3();
|
|
115
36
|
|
|
116
|
-
/**
|
|
117
|
-
* Union an `InstancedMesh`'s OWN drawn units into `target`, in world space.
|
|
118
|
-
*
|
|
119
|
-
* **Why not the object-level `boundingBox` three's own rule prefers here:
|
|
120
|
-
* `InstancedMesh.computeBoundingBox()` CACHES, and instanced content MOVES.**
|
|
121
|
-
* `boundingBox` starts `null`, is filled on the first request, and is never
|
|
122
|
-
* recomputed — three's docs say so outright ("You may need to recompute the
|
|
123
|
-
* bounding box if an instance is transformed"). A trail system that rewrites
|
|
124
|
-
* every instance matrix each frame (the racing-game's `Dust`/`Skid`) is
|
|
125
|
-
* therefore measured forever at whatever pose the editor first happened to ask
|
|
126
|
-
* about, and the per-frame selection cage — which exists to be glued to the
|
|
127
|
-
* thing — is glued to where the content USED to be. Nothing invalidates that
|
|
128
|
-
* cache, because nothing in the game's code knows the editor is looking.
|
|
129
|
-
*
|
|
130
|
-
* So the union is taken from the instance matrices as they stand, per call. The
|
|
131
|
-
* per-instance finite filter is the same decision {@link isFiniteBox} makes one
|
|
132
|
-
* level up and for the same reason: a live world holds transforms that are not
|
|
133
|
-
* numbers yet, and one such unit would otherwise turn the node's whole box into
|
|
134
|
-
* `NaN` — which `isEmpty()` cannot see.
|
|
135
|
-
*
|
|
136
|
-
* Cost is O(count) per call, which is exactly what `computeBoundingBox` costs;
|
|
137
|
-
* the callers that pay it per frame are scoped to the selection.
|
|
138
|
-
*/
|
|
37
|
+
/** Union each InstancedMesh unit from its current instance matrix and geometry bounds. Skip non-finite instances instead of relying on the mesh’s cached box. */
|
|
139
38
|
function expandByInstances(
|
|
140
39
|
target: THREE.Box3,
|
|
141
40
|
node: THREE.InstancedMesh,
|
|
@@ -157,16 +56,9 @@ function expandByInstances(
|
|
|
157
56
|
/**
|
|
158
57
|
* Union `node`'s OWN renderable extent into `target`, ignoring its children.
|
|
159
58
|
*
|
|
160
|
-
*
|
|
161
|
-
*
|
|
162
|
-
*
|
|
163
|
-
* it walks children, which is the whole thing this module exists to control — so
|
|
164
|
-
* the per-node half is spelled here, over three's public `boundingBox` /
|
|
165
|
-
* `computeBoundingBox` API. Same shape as `model-thumbnail.ts`'s
|
|
166
|
-
* `modelPreviewBounds`, which already needed a filtered walk for its own reason.
|
|
167
|
-
*
|
|
168
|
-
* `InstancedMesh` is the one node three's own rule gets wrong for a LIVE world;
|
|
169
|
-
* see {@link expandByInstances}.
|
|
59
|
+
* SkinnedMesh uses its geometry's bind-pose box; instanced meshes expand each unit.
|
|
60
|
+
* Other objects use their own boundingBox when available, then the geometry's box.
|
|
61
|
+
* The caller controls descendant traversal and editor-helper filtering.
|
|
170
62
|
*/
|
|
171
63
|
function expandByNodeGeometry(
|
|
172
64
|
target: THREE.Box3,
|
|
@@ -202,21 +94,7 @@ function localRenderableBox(
|
|
|
202
94
|
boundingBox?: THREE.Box3 | null;
|
|
203
95
|
computeBoundingBox?: () => void;
|
|
204
96
|
};
|
|
205
|
-
//
|
|
206
|
-
// baked in, so it is the GEOMETRY box that `expandByInstances` needs.
|
|
207
|
-
//
|
|
208
|
-
// A SkinnedMesh is measured by its GEOMETRY (bind-pose) box, and its
|
|
209
|
-
// mesh-level cache is deliberately never touched. `computeBoundingBox()` on
|
|
210
|
-
// a SkinnedMesh runs the CPU skinning path, and on the humanoid rigs it
|
|
211
|
-
// yields bone-WORLD-contaminated "local" bounds (measured live on the
|
|
212
|
-
// hosted editor: local box z≈7.8..8.2 for a body whose matrixWorld already
|
|
213
|
-
// carries z=8). Three CACHES that box on the mesh, and `SkinnedMesh.raycast`
|
|
214
|
-
// then uses it as an early-out gate against the LOCALIZED ray — so the one
|
|
215
|
-
// framing/thumbnail pass that touched a character made it click-through
|
|
216
|
-
// FOREVER: rays hit the floor behind while the body rendered right under
|
|
217
|
-
// the cursor (the enemy-torso click that selected the jump pad ten units
|
|
218
|
-
// behind it). The bind-pose box is the honest edit-mode answer here anyway
|
|
219
|
-
// — edit mode shows the authored pose.
|
|
97
|
+
// Use geometry bounds for instanced units and the bind-pose box for SkinnedMesh. Leave skinned mesh-level caches untouched so raycasting does not inherit bone-world-space bounds.
|
|
220
98
|
if ((node as THREE.SkinnedMesh).isSkinnedMesh) {
|
|
221
99
|
if (geometry.boundingBox === null) geometry.computeBoundingBox();
|
|
222
100
|
return geometry.boundingBox;
|
|
@@ -256,12 +134,7 @@ const collectBox = new THREE.Box3();
|
|
|
256
134
|
*/
|
|
257
135
|
export interface ContentNodeRecord {
|
|
258
136
|
readonly box: THREE.Box3;
|
|
259
|
-
/**
|
|
260
|
-
* Every material on this mesh has `depthWrite === false`. That is the
|
|
261
|
-
* authored "I am a volume, not a body" flag — measured on the lighthouse
|
|
262
|
-
* beam (`transparent`, `opacity: 0.16`, `depthWrite: false`) and absent
|
|
263
|
-
* from every body mesh of that prefab and of an opaque hull.
|
|
264
|
-
*/
|
|
137
|
+
/** Whether every material disables depthWrite, identifying a volume rather than an opaque body for this bounds walk. */
|
|
265
138
|
readonly overlay: boolean;
|
|
266
139
|
/**
|
|
267
140
|
* Every material on this mesh is `BackSide`. That is the authored
|
|
@@ -296,14 +169,12 @@ export function nodeIsEnclosure(node: THREE.Object3D): boolean {
|
|
|
296
169
|
* The union answers "how big is this world"; framing needs "where is this
|
|
297
170
|
* world's content", and those are different questions the moment a world has a
|
|
298
171
|
* backdrop — one skybox sphere or one far prop moves the union and moves
|
|
299
|
-
* nothing else
|
|
172
|
+
* nothing else. Only nodes that
|
|
300
173
|
* actually draw contribute a box, so groups, lights and cameras add nothing to
|
|
301
174
|
* the distribution.
|
|
302
175
|
*/
|
|
303
|
-
export function collectContentNodeRecords(
|
|
304
|
-
|
|
305
|
-
out: ContentNodeRecord[] = [],
|
|
306
|
-
): ContentNodeRecord[] {
|
|
176
|
+
export function collectContentNodeRecords(object: THREE.Object3D): ContentNodeRecord[] {
|
|
177
|
+
const out: ContentNodeRecord[] = [];
|
|
307
178
|
traverseContent(object, (node) => {
|
|
308
179
|
node.updateWorldMatrix(false, false);
|
|
309
180
|
collectBox.makeEmpty();
|
|
@@ -14,35 +14,13 @@ export const EDITOR_LAYER = 31;
|
|
|
14
14
|
export const EDITOR_SELECTION_LAYER = 30;
|
|
15
15
|
|
|
16
16
|
/**
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
* everything: the viewport's constructor-time furniture (`GridHelper`, the
|
|
21
|
-
* editor ambient/directional lights, the three.quarks `BatchedRenderer`, the
|
|
22
|
-
* TransformControls helper, the pivot dummy, the snap indicators) is marked
|
|
23
|
-
* ONLY with `layers.set(EDITOR_LAYER)`, while helpers added later
|
|
24
|
-
* (skeleton/physics/crowd/component gizmos) are marked ONLY with
|
|
25
|
-
* `userData.editorHelper`. A walk that checks one convention silently presents
|
|
26
|
-
* the other half as game content.
|
|
27
|
-
*
|
|
28
|
-
* That is the SimCity ledger's S-1 hierarchy defect: `projectThreeScene`
|
|
29
|
-
* checked only the userData flags, so when an ingest mount FAILED — leaving
|
|
30
|
-
* `store.scene` pointed at the editor viewport scene instead of a captured game
|
|
31
|
-
* scene — the hierarchy listed the editor's own grid, lights, BatchedRenderer,
|
|
32
|
-
* gizmo helper, pivot dummy and two snap indicators under the game world's id.
|
|
33
|
-
* A dead game was indistinguishable from a live one.
|
|
34
|
-
*
|
|
35
|
-
* This is the SAME predicate the engine's ingest walk already applies
|
|
36
|
-
* (`@volter/editor-threejs-runtime/adapter/ingest/structural-ids`'s `isEditorOnly`, layer half) plus
|
|
37
|
-
* the userData half `viewport-shading-boundary.ts` applies — stated once here
|
|
38
|
-
* so the two halves can never drift apart again.
|
|
17
|
+
* Identify editor furniture through either EDITOR_LAYER or the editorHelper userData mark.
|
|
18
|
+
* Constructor-time viewport objects and dynamically added helpers use these complementary marks;
|
|
19
|
+
* hierarchy, capture and export filters must recognize both.
|
|
39
20
|
*/
|
|
40
21
|
export function isEditorOwnedObject(object: THREE.Object3D): boolean {
|
|
41
22
|
return (
|
|
42
|
-
//
|
|
43
|
-
// ingest devtools hook supports. `Layers.isEnabled()` is newer than r129:
|
|
44
|
-
// calling it rejected an otherwise healthy Cuberun capture before the
|
|
45
|
-
// hierarchy could mount.
|
|
23
|
+
// The public layer mask identifies editor-only infrastructure.
|
|
46
24
|
(object.layers.mask & (1 << EDITOR_LAYER)) !== 0 ||
|
|
47
25
|
!!getObjectMark(object, 'editorHelper') ||
|
|
48
26
|
!!getObjectMark(object, 'engineInternal')
|
|
@@ -28,7 +28,7 @@ export function createStandardEnvironment(renderer: THREE.WebGLRenderer): Standa
|
|
|
28
28
|
}
|
|
29
29
|
|
|
30
30
|
/**
|
|
31
|
-
* An environment image (`@volter/sdk/
|
|
31
|
+
* An environment image (`@volter/sdk/environment-images`) as an equirectangular
|
|
32
32
|
* texture in scene-referred light. Read as half float: a sun past white stays bright, and half
|
|
33
33
|
* float filters linearly where full float cannot (phones).
|
|
34
34
|
*/
|
|
@@ -1,40 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* `Object3DDocumentViewport`) used to construct its own
|
|
8
|
-
* `THREE.WebGLRenderer` on mount, so SELECTING A THING built a whole GL stack.
|
|
9
|
-
* A renderer's constructor is a long chain of synchronous driver round trips
|
|
10
|
-
* (`WebGLCapabilities`/`getExtension`/`getParameter`), and its program cache
|
|
11
|
-
* starts empty, so every selection also recompiled the subject's shaders and
|
|
12
|
-
* re-baked the PMREM environment. A click profiled at ~1.9 s of frozen main
|
|
13
|
-
* thread, 3.5 s of self time in `gl.getParameter` alone. Sharing one renderer
|
|
14
|
-
* removes the construction entirely and lets the program cache and the IBL
|
|
15
|
-
* bake survive across selections.
|
|
16
|
-
*
|
|
17
|
-
* OWNERSHIP — the one place it is stated:
|
|
18
|
-
* - This module OWNS every canvas, `WebGLRenderer` and baked
|
|
19
|
-
* {@link StandardEnvironment} it creates. Nothing else may dispose them.
|
|
20
|
-
* - SHARERS are lease holders: {@link acquireInspectorPreviewRenderer} hands
|
|
21
|
-
* out a lease, the holder mounts `lease.canvas` in its own container and
|
|
22
|
-
* draws through `lease.renderer` for as long as it is mounted.
|
|
23
|
-
* - The ONE teardown path a holder has is {@link InspectorPreviewLease.release}
|
|
24
|
-
* — it returns the renderer to the pool (or disposes it, if it was an
|
|
25
|
-
* overflow lease that the pool does not keep). A holder never calls
|
|
26
|
-
* `renderer.dispose()`, and never disposes `lease.environment()`, whose
|
|
27
|
-
* handle is deliberately non-owning.
|
|
28
|
-
* - Per-selection SCENE resources (the snapshot's skeletons, the dressing's
|
|
29
|
-
* lights/grid/backdrop) stay the holder's, exactly as before: three's
|
|
30
|
-
* program cache lives on the renderer and is not touched by disposing the
|
|
31
|
-
* materials that populated it.
|
|
32
|
-
*
|
|
33
|
-
* Capacity is deliberately small. The inspector shows ONE live preview at a
|
|
34
|
-
* time by construction (`InspectionProjection.tsx` drops the disc to its glyph
|
|
35
|
-
* while the preview panel is open), so a second entry is slack for a handover
|
|
36
|
-
* frame, and anything beyond that is a transient renderer disposed on release
|
|
37
|
-
* rather than a growing pool.
|
|
2
|
+
* Lease pooled WebGL renderers for Inspector previews so program caches and environment bakes
|
|
3
|
+
* survive selection changes. This module owns each canvas, renderer and StandardEnvironment;
|
|
4
|
+
* holders mount and draw through the lease, then release it without disposing those shared
|
|
5
|
+
* resources. Per-selection scene resources belong to the holder. Keep a small idle pool and dispose
|
|
6
|
+
* overflow renderers on release.
|
|
38
7
|
*/
|
|
39
8
|
|
|
40
9
|
import * as THREE from 'three';
|
|
@@ -108,15 +77,8 @@ function disposeEntry(entry: PreviewRendererEntry): void {
|
|
|
108
77
|
entry.environment?.dispose();
|
|
109
78
|
entry.environment = null;
|
|
110
79
|
entry.renderer.dispose();
|
|
111
|
-
//
|
|
112
|
-
//
|
|
113
|
-
// contexts are a hard ~16-per-page budget the browser enforces by killing
|
|
114
|
-
// the OLDEST — which is the main viewport, so the symptom is a white
|
|
115
|
-
// viewport and "too many active WebGL contexts" far from the cause
|
|
116
|
-
// (runhuman pass 104). Every other renderer owner in this editor already
|
|
117
|
-
// pairs the two (`asset-preview.ts`, `components/world-root-stage.ts`); this
|
|
118
|
-
// shared pool did not, and it backs both the Inspector preview and the
|
|
119
|
-
// Object3D tool's viewport, so its transients leaked a context each.
|
|
80
|
+
// Release the WebGL context as well as GPU resources so discarded previews do not consume the
|
|
81
|
+
// browser’s context budget.
|
|
120
82
|
entry.renderer.forceContextLoss();
|
|
121
83
|
entry.canvas.remove();
|
|
122
84
|
}
|
|
@@ -1,67 +1,23 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* Why this exists: ingest's scene-capture trap
|
|
7
|
-
* (`@volter/editor-threejs-runtime/adapter/ingest/scene-capture`) is installed on the SHARED
|
|
8
|
-
* `three.WebGLRenderer.prototype` — that sharing is the whole mechanism, it is
|
|
9
|
-
* how an unmodified game's renderer gets instrumented without touching the
|
|
10
|
-
* game's source. But it means the editor's own renders arrive at the trap too,
|
|
11
|
-
* and the trap captures the first scene it sees. Any host renderer constructed
|
|
12
|
-
* after the trap installs (a thumbnail bake, an asset preview) could therefore
|
|
13
|
-
* be captured AS THE INGESTED GAME.
|
|
14
|
-
*
|
|
15
|
-
* That is not hypothetical: it is what made the "a game bundling its own three
|
|
16
|
-
* is DETECTED, never silently mistaken for a capture" spec pass or fail on
|
|
17
|
-
* timing alone. When it failed, the editor
|
|
18
|
-
* had adopted its own thumbnail scene as the game: a hierarchy of the editor's
|
|
19
|
-
* own lights, presented as the game's content.
|
|
20
|
-
*
|
|
21
|
-
* Marking is explicit at each construction site rather than inferred, because
|
|
22
|
-
* the honest question ("did the HOST make this renderer?") has no reliable
|
|
23
|
-
* signal at the trap: a host renderer and a game renderer are the same class,
|
|
24
|
-
* both constructed after install, and both render real scenes.
|
|
25
|
-
*/
|
|
26
|
-
|
|
27
|
-
const _hostRenderers = new WeakSet<object>();
|
|
28
|
-
|
|
29
|
-
/** Record `renderer` as the editor's own. Returns it, so it can wrap a `new`. */
|
|
30
|
-
/**
|
|
31
|
-
* WebGL contexts are a HARD, SMALL browser budget (order of sixteen per page),
|
|
32
|
-
* and this is the one chokepoint every editor-constructed renderer passes
|
|
33
|
-
* through — so it is where an editor that is quietly accumulating them can be
|
|
34
|
-
* caught naming its own culprit.
|
|
35
|
-
*
|
|
36
|
-
* A tester adding and deleting prefabs hit "too many active WebGL contexts"
|
|
37
|
-
* after five rounds and the whole viewport turned white, because the browser
|
|
38
|
-
* evicts the OLDEST context when the limit is reached and the oldest is the
|
|
39
|
-
* main viewport (runhuman pass 104). Three's warning comes from inside
|
|
40
|
-
* `three.module.js` and names nothing about who asked, which is what made it
|
|
41
|
-
* undiagnosable from the report.
|
|
42
|
-
*
|
|
43
|
-
* The count is LIVE: `dispose()` is wrapped so a renderer that is properly
|
|
44
|
-
* torn down stops counting. Past the budget the warning carries the creating
|
|
45
|
-
* stack, so the next sighting names the leak instead of the victim.
|
|
2
|
+
* Count live WebGLRenderers owned by the editor’s three.js integration, including offscreen
|
|
3
|
+
* previews. Wrapped dispose calls decrement the count. Warn with the creating stack near the
|
|
4
|
+
* browser’s context budget so the renderer owner can be located.
|
|
46
5
|
*/
|
|
47
6
|
const LIVE_RENDERER_WARN_AT = 8;
|
|
48
7
|
let liveHostRenderers = 0;
|
|
49
8
|
|
|
50
|
-
|
|
51
|
-
|
|
9
|
+
/** Count `renderer` as the editor's own. Returns it, so it can wrap a `new`. */
|
|
10
|
+
export function markHostRenderer<T extends { dispose(): void }>(renderer: T): T {
|
|
52
11
|
liveHostRenderers += 1;
|
|
53
|
-
const
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
return (dispose as (...a: unknown[]) => unknown).apply(this, args);
|
|
63
|
-
} as () => void;
|
|
64
|
-
}
|
|
12
|
+
const dispose = renderer.dispose;
|
|
13
|
+
let counted = true;
|
|
14
|
+
renderer.dispose = function wrappedDispose() {
|
|
15
|
+
if (counted) {
|
|
16
|
+
counted = false;
|
|
17
|
+
liveHostRenderers -= 1;
|
|
18
|
+
}
|
|
19
|
+
dispose.call(this);
|
|
20
|
+
};
|
|
65
21
|
if (liveHostRenderers >= LIVE_RENDERER_WARN_AT) {
|
|
66
22
|
// biome-ignore lint/suspicious/noConsole: the standing warning channel for a budget the browser enforces by destroying the viewport
|
|
67
23
|
console.warn(
|
|
@@ -80,7 +36,3 @@ export function liveHostRendererCount(): number {
|
|
|
80
36
|
return liveHostRenderers;
|
|
81
37
|
}
|
|
82
38
|
|
|
83
|
-
/** True when `renderer` is one the editor constructed for its own rendering. */
|
|
84
|
-
export function isHostRenderer(renderer: unknown): boolean {
|
|
85
|
-
return typeof renderer === 'object' && renderer !== null && _hostRenderers.has(renderer);
|
|
86
|
-
}
|
package/src/viewport-api.ts
CHANGED
|
@@ -1,21 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* THE AUTHORED THREE VIEWPORT'S PUBLIC SHAPE — what a lane that mounts something reaches through
|
|
3
3
|
* `viewport-door.ts`: the rig a camera flight drives, the frame loop it rides, the helpers it
|
|
4
|
-
* shows
|
|
5
|
-
* instance's roots (Play's adoption of the running scene).
|
|
4
|
+
* shows and every mounted stage.
|
|
6
5
|
*/
|
|
7
|
-
import type { MountedRoot } from '@volter/project/adapter/root-adapter';
|
|
8
6
|
import type * as THREE from 'three';
|
|
9
7
|
import type { StageFrameCostReading } from '@volter/sdk';
|
|
10
8
|
|
|
11
|
-
/** A live instance's roots, presented as the authored viewport's subject. */
|
|
12
|
-
export interface ViewportPresentation {
|
|
13
|
-
/** The root whose native subject the viewport shows. */
|
|
14
|
-
readonly worldId: string;
|
|
15
|
-
/** Restores the viewport's prior subject; idempotent. */
|
|
16
|
-
dispose(): void;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
9
|
/** What a frame of a stage costs, uncapped ({@link ViewportRig.frameCost}): the wire's reading
|
|
20
10
|
* without the stage's id, which the door adds. */
|
|
21
11
|
export type StageFrameCost = Omit<StageFrameCostReading, 'stage'>;
|
|
@@ -32,15 +22,20 @@ export interface ViewportRig {
|
|
|
32
22
|
* `requestAnimationFrame`, each waited out on the GPU, so the number is the frame's cost and
|
|
33
23
|
* not the display's refresh. Absent on a stage with no frame of its own.
|
|
34
24
|
*/
|
|
35
|
-
frameCost?(frames: number, quality
|
|
25
|
+
frameCost?(frames: number, quality: 'full' | 'navigation'): StageFrameCost;
|
|
36
26
|
readonly orbit: { readonly target: THREE.Vector3; enabled: boolean; update(): void };
|
|
27
|
+
/** While `owned`, the stage leaves the camera's projection (its clip planes) to whoever drives it:
|
|
28
|
+
* a running game. Absent on a stage that never fits its planes. */
|
|
29
|
+
readonly projection?: { owned: boolean };
|
|
37
30
|
/** The editor's own scene (helpers live on its editor layer). */
|
|
38
31
|
readonly scene: THREE.Scene;
|
|
32
|
+
/** The stage's one renderer, for a binding that draws a document's content into this stage's scene through it. */
|
|
33
|
+
readonly renderer: THREE.WebGLRenderer;
|
|
39
34
|
}
|
|
40
35
|
|
|
41
36
|
/**
|
|
42
37
|
* ONE MOUNTED 3D STAGE, named by the document it draws. Every 3D document
|
|
43
|
-
* mounts a stage of its own
|
|
38
|
+
* mounts a stage of its own ; this is the door
|
|
44
39
|
* to a particular one, where the viewport door's singular members
|
|
45
40
|
* are the door to whichever is primary.
|
|
46
41
|
*/
|
|
@@ -56,16 +51,13 @@ export interface ViewportStage {
|
|
|
56
51
|
|
|
57
52
|
/**
|
|
58
53
|
* THE AUTHORED VIEWPORT, as a lane that mounts something reaches it: the rig
|
|
59
|
-
* a camera flight drives
|
|
60
|
-
*
|
|
61
|
-
*
|
|
62
|
-
* host's; a lane that finds no rig (a headless tab, a project with no Three
|
|
63
|
-
* root) mounts without one.
|
|
54
|
+
* a camera flight drives and the frame loop it rides. Which native surface the
|
|
55
|
+
* viewport renders is the host's; a lane that finds no rig (a headless tab, a
|
|
56
|
+
* project with no Three root) mounts without one.
|
|
64
57
|
*
|
|
65
|
-
* THE SINGULAR MEMBERS ARE THE PRIMARY STAGE — the stage
|
|
66
|
-
*
|
|
67
|
-
*
|
|
68
|
-
* here and never enumerate.
|
|
58
|
+
* THE SINGULAR MEMBERS ARE THE PRIMARY STAGE — the FOCUSED stage, else the
|
|
59
|
+
* first bound. A camera flight wants exactly that one, which is why it reads
|
|
60
|
+
* here and never enumerates.
|
|
69
61
|
*
|
|
70
62
|
* {@link stages} is EVERY mounted 3D stage. A helper is an `Object3D` and an
|
|
71
63
|
* `Object3D` has one parent, so there is no "set it once for all stages": a
|
|
@@ -73,19 +65,8 @@ export interface ViewportStage {
|
|
|
73
65
|
* `onStages` is how a lane keeps up with stages that mount and unmount after
|
|
74
66
|
* it made that pass.
|
|
75
67
|
*/
|
|
76
|
-
/** A mounted adapter surface offered to the viewport. Execution, input and
|
|
77
|
-
* ticking stay with the caller; presentation requires only identity and mount.
|
|
78
|
-
* A game's richer root instance can satisfy this interface directly.
|
|
79
|
-
*/
|
|
80
|
-
export interface ViewportRoot {
|
|
81
|
-
readonly id: string;
|
|
82
|
-
readonly mounted: MountedRoot;
|
|
83
|
-
}
|
|
84
|
-
|
|
85
68
|
export interface ViewportDoor {
|
|
86
69
|
rig(): ViewportRig | null;
|
|
87
|
-
/** Null when the viewport has no subject among `roots` (no Three root). */
|
|
88
|
-
presentRoots(roots: readonly ViewportRoot[]): ViewportPresentation | null;
|
|
89
70
|
/** Runs after the viewport's own per-frame update; the return unsubscribes. */
|
|
90
71
|
onFrame(fn: (dtSeconds: number) => void): () => void;
|
|
91
72
|
/**
|