@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.
- 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 +6 -11
- package/src/kit/components/StageHost.css +40 -0
- package/src/kit/components/StageHost.tsx +192 -439
- 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 +6 -17
- package/src/kit/components/asset-viewers/EnvironmentAssetDocument.tsx +9 -11
- package/src/kit/components/asset-viewers/LiveModuleDocument.tsx +31 -133
- package/src/kit/components/asset-viewers/LutAssetDocument.tsx +10 -12
- package/src/kit/components/asset-viewers/ModelAssetDocument.tsx +4 -8
- package/src/kit/components/asset-viewers/Object3DPreview.tsx +13 -64
- package/src/kit/components/asset-viewers/QuarksAssetDocument.tsx +6 -10
- package/src/kit/components/asset-viewers/ShaderAssetDocument.tsx +48 -65
- 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 +5 -3
- 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 +486 -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/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,49 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* The projection unit is the RUNTIME OBJECT. Nothing here reads source, an AST
|
|
7
|
-
* or a document; a walk answers from the graph that is actually mounted, so an
|
|
8
|
-
* empty projection over a mounted world is a defect rather than a fact.
|
|
9
|
-
*
|
|
10
|
-
* ## What this owns, and what it deliberately does not
|
|
11
|
-
*
|
|
12
|
-
* OWNS — the four things all three three-surface authoring classes used to
|
|
13
|
-
* spell separately:
|
|
14
|
-
*
|
|
15
|
-
* - the DFS itself ({@link walkThreeGraph}), including the editor-furniture
|
|
16
|
-
* skip and the fold-into-the-nearest-admitted-ancestor rule,
|
|
17
|
-
* - IDENTITY MINTING, parameterized by scheme ({@link oidIdentity},
|
|
18
|
-
* {@link structuralIdentity}, {@link sourceOidIdentity},
|
|
19
|
-
* {@link documentPathIdentity}) — the id FORMATS are the schemes', the walk
|
|
20
|
-
* that applies them is one,
|
|
21
|
-
* - the INDEX both directions ({@link ThreeProjector.objects} /
|
|
22
|
-
* {@link ThreeProjector.idsByObject}) plus the `userData.entityId` stamp
|
|
23
|
-
* that makes the viewport's raycast, the gizmo and `store.objectMap` agree
|
|
24
|
-
* with the walk,
|
|
25
|
-
* - PICKING as a derived view over that index
|
|
26
|
-
* ({@link ThreeProjector.pick}).
|
|
27
|
-
*
|
|
28
|
-
* DOES NOT own the LANE-SPECIFIC hierarchy VIEW built on top. The R3F source
|
|
29
|
-
* lane collapses a component instance's interior into one row; the live lane
|
|
30
|
-
* resolves the same question over projected nodes with different top-level
|
|
31
|
-
* semantics. Both read this projection and derive their rows from it; merging
|
|
32
|
-
* the two collapse rules would be inventing a third, not extracting a shared
|
|
33
|
-
* one.
|
|
34
|
-
*
|
|
35
|
-
* BOUNDS are not here either, because they were never in the authoring classes:
|
|
36
|
-
* `@volter/editor-threejs/viewport/content-bounds` (`contentWorldBounds`) is the bounds walk
|
|
37
|
-
* every consumer uses, and it takes the `Object3D` this index resolves.
|
|
2
|
+
* Project a live Object3D graph into one traversal, two-way object/id index and picker. Skip editor
|
|
3
|
+
* furniture, fold omitted nodes into admitted ancestors, and mint identities using the supplied
|
|
4
|
+
* scheme. Authoring adapters derive their hierarchy rows and write targets from this projection;
|
|
5
|
+
* viewport/content-bounds owns the separate bounds walk.
|
|
38
6
|
*/
|
|
39
7
|
|
|
40
8
|
import type { EditorNode } from '@volter/project/adapter';
|
|
41
9
|
import { isEditorOwnedObject } from '@volter/editor-threejs/viewport/editor-layers';
|
|
42
10
|
import { constraintsOf } from '@volter/editor-threejs/adapter/constraint';
|
|
43
|
-
import {
|
|
44
|
-
CAMERA_ID,
|
|
45
|
-
collectStructuralIds,
|
|
46
|
-
} from '@volter/editor-threejs/adapter/ingest/structural-ids';
|
|
47
11
|
import { object3DAuthoringSubjectOf } from '@volter/editor-threejs/adapter/object3d-authoring-subject';
|
|
48
12
|
import { reflectionProbeOf } from '@volter/editor-threejs/adapter/reflection-probe';
|
|
49
13
|
import { getUserData } from '@volter/editor-threejs/ecs/user-data';
|
|
@@ -52,17 +16,16 @@ import { authoringOidOf, occurrenceId } from '../authoring/component-instance-ro
|
|
|
52
16
|
import { raycastCandidates, raycastPick } from '../authoring/viewport-raycast';
|
|
53
17
|
import type { EditorShellStore } from '../editor-shell-store';
|
|
54
18
|
import { entityIdOf, stampEntityId } from '../entity-object';
|
|
55
|
-
import type { ProjectedNode, Projection } from '@volter/sdk/
|
|
19
|
+
import type { ProjectedNode, Projection } from '@volter/sdk/projection-types';
|
|
56
20
|
|
|
57
|
-
export type { ProjectedNode, Projection } from '@volter/sdk/
|
|
21
|
+
export type { ProjectedNode, Projection } from '@volter/sdk/projection-types';
|
|
58
22
|
|
|
59
23
|
/** One projected authoring node over a live three graph. */
|
|
60
24
|
export type ThreeNode = ProjectedNode<THREE.Object3D>;
|
|
61
25
|
|
|
62
26
|
/**
|
|
63
27
|
* Reflection stats for one walk. Surfaced by the adapters' `refresh()` for
|
|
64
|
-
* proof/telemetry
|
|
65
|
-
* deliberately a fresh measurement each walk rather than a mount-time snapshot,
|
|
28
|
+
* proof/telemetry — deliberately a fresh measurement each walk rather than a mount-time snapshot,
|
|
66
29
|
* because a running world keeps streaming objects in.
|
|
67
30
|
*/
|
|
68
31
|
export interface ThreeWalkStats {
|
|
@@ -76,9 +39,7 @@ export interface ThreeWalkStats {
|
|
|
76
39
|
* object drawing N units — so `count`/`meshes` (and the hierarchy rows built
|
|
77
40
|
* from the same nodes) report 1 where a reader sees N, with no error
|
|
78
41
|
* anywhere, because both numbers are individually correct. These two fields
|
|
79
|
-
*
|
|
80
|
-
* instanced-content check reads exactly this pair
|
|
81
|
-
* (`packages/editor/src/doctor/detection-gaps.ts`).
|
|
42
|
+
* make that difference visible to an instanced-content inspection.
|
|
82
43
|
*/
|
|
83
44
|
readonly instancedMeshes: number;
|
|
84
45
|
readonly instances: number;
|
|
@@ -189,19 +150,7 @@ function tally(nodes: ReadonlyMap<string, ThreeNode>): ThreeWalkStats {
|
|
|
189
150
|
return { count: nodes.size, meshes, lights, ...instancedTally(nodes) };
|
|
190
151
|
}
|
|
191
152
|
|
|
192
|
-
/**
|
|
193
|
-
* THE WALK. One DFS over a live scene, projecting it into authoring nodes and
|
|
194
|
-
* stamping `userData.entityId` on each, so the viewport raycast/gizmo and
|
|
195
|
-
* `EditorShellStore.objectMap` key off the same identity the walk minted.
|
|
196
|
-
*
|
|
197
|
-
* Every scheme that goes through here addresses a graph the editor is entitled
|
|
198
|
-
* to stamp. The one scheme that is not ({@link structuralIdentity}, over
|
|
199
|
-
* foreign objects) keeps identity in the projection instead and does not use
|
|
200
|
-
* this walk at all — see #1965.
|
|
201
|
-
*
|
|
202
|
-
* Idempotent for every scheme that reuses an existing stamp: re-walking a scene
|
|
203
|
-
* re-derives the same ids for the same objects.
|
|
204
|
-
*/
|
|
153
|
+
/** Project a live scene in one DFS and stamp entityId so raycast, gizmo and objectMap share the same identities. Reused stamps make repeated walks stable. */
|
|
205
154
|
export function walkThreeGraph(scene: THREE.Scene, scheme: ThreeIdentityScheme): ThreeProjection {
|
|
206
155
|
const nodes = new Map<string, ThreeNode>();
|
|
207
156
|
const rootIds: string[] = [];
|
|
@@ -237,15 +186,7 @@ export function walkThreeGraph(scene: THREE.Scene, scheme: ThreeIdentityScheme):
|
|
|
237
186
|
return { nodes, rootIds, stats: tally(nodes) };
|
|
238
187
|
}
|
|
239
188
|
|
|
240
|
-
/**
|
|
241
|
-
* The scheme half of every three-lane entity id (`r3f:<worldId>:<oid>`).
|
|
242
|
-
*
|
|
243
|
-
* Spelled ONCE because it is read as well as written: the editor shell tests a
|
|
244
|
-
* focused id with `startsWith` to decide whether the three lane owns it
|
|
245
|
-
* (`components/DefaultEditorLayout.tsx`), so a literal changed at one of the
|
|
246
|
-
* mint sites and not at the reader is a silent loss of that decision rather
|
|
247
|
-
* than a compile error.
|
|
248
|
-
*/
|
|
189
|
+
/** Shared scheme for Three-lane entity ids, r3f:<worldId>:<oid>. */
|
|
249
190
|
export const THREE_ENTITY_ID_SCHEME = 'r3f:';
|
|
250
191
|
|
|
251
192
|
/** `r3f:<worldId>:<oid>` — the one place the three lane's id shape is built. */
|
|
@@ -253,297 +194,6 @@ export function threeEntityId(worldId: string, oid: string): string {
|
|
|
253
194
|
return `${THREE_ENTITY_ID_SCHEME}${worldId}:${oid}`;
|
|
254
195
|
}
|
|
255
196
|
|
|
256
|
-
/**
|
|
257
|
-
* The LIVE OID scheme. A node stamped by the served TSX's `__volterOid` transform
|
|
258
|
-
* keeps `r3f:<worldId>:<oid>`, so selection survives Edit→Play; anything a
|
|
259
|
-
* source location cannot claim gets a session-stable `live:<worldId>:<n>`.
|
|
260
|
-
*/
|
|
261
|
-
function liveOidScheme(worldId: string): ThreeIdentityScheme {
|
|
262
|
-
const oidOccurrences = new Map<string, number>();
|
|
263
|
-
const reservedOccurrenceOwners = new Map<string, THREE.Object3D>();
|
|
264
|
-
const occurrenceBelongsTo = (id: string, base: string): boolean =>
|
|
265
|
-
id === base || (id.startsWith(`${base}#`) && /^\d+$/u.test(id.slice(base.length + 1)));
|
|
266
|
-
|
|
267
|
-
const reserveExistingOccurrences = (scene: THREE.Scene): void => {
|
|
268
|
-
reservedOccurrenceOwners.clear();
|
|
269
|
-
const visit = (object: THREE.Object3D): void => {
|
|
270
|
-
if (isEditorOwnedObject(object)) return;
|
|
271
|
-
const sourceOid = authoringOidOf(object);
|
|
272
|
-
const stamped = entityIdOf(object);
|
|
273
|
-
if (sourceOid !== undefined && stamped !== undefined) {
|
|
274
|
-
const base = threeEntityId(worldId, sourceOid);
|
|
275
|
-
if (occurrenceBelongsTo(stamped, base) && !reservedOccurrenceOwners.has(stamped)) {
|
|
276
|
-
reservedOccurrenceOwners.set(stamped, object);
|
|
277
|
-
}
|
|
278
|
-
}
|
|
279
|
-
for (const child of object.children) visit(child);
|
|
280
|
-
};
|
|
281
|
-
for (const child of scene.children) visit(child);
|
|
282
|
-
};
|
|
283
|
-
|
|
284
|
-
const reusableStampedIdentity = (
|
|
285
|
-
object: THREE.Object3D,
|
|
286
|
-
sourceOid: string | undefined,
|
|
287
|
-
): string | undefined => {
|
|
288
|
-
const stamped = entityIdOf(object);
|
|
289
|
-
if (stamped === undefined || sourceOid === undefined) return stamped;
|
|
290
|
-
const base = threeEntityId(worldId, sourceOid);
|
|
291
|
-
if (!occurrenceBelongsTo(stamped, base)) return stamped;
|
|
292
|
-
return reservedOccurrenceOwners.get(stamped) === object ? stamped : undefined;
|
|
293
|
-
};
|
|
294
|
-
|
|
295
|
-
const claimSourceIdentity = (object: THREE.Object3D, sourceOid: string): string => {
|
|
296
|
-
const base = threeEntityId(worldId, sourceOid);
|
|
297
|
-
let occurrence = oidOccurrences.get(sourceOid) ?? 0;
|
|
298
|
-
let identity = occurrenceId(base, occurrence);
|
|
299
|
-
while (reservedOccurrenceOwners.has(identity)) {
|
|
300
|
-
occurrence += 1;
|
|
301
|
-
identity = occurrenceId(base, occurrence);
|
|
302
|
-
}
|
|
303
|
-
oidOccurrences.set(sourceOid, occurrence + 1);
|
|
304
|
-
reservedOccurrenceOwners.set(identity, object);
|
|
305
|
-
return identity;
|
|
306
|
-
};
|
|
307
|
-
|
|
308
|
-
return {
|
|
309
|
-
begin: (scene) => {
|
|
310
|
-
oidOccurrences.clear();
|
|
311
|
-
reserveExistingOccurrences(scene);
|
|
312
|
-
},
|
|
313
|
-
identify: (object, context) => {
|
|
314
|
-
// Count every source-stamped runtime node in native traversal order,
|
|
315
|
-
// including projected-away wrappers/parts, so surviving authoring roots
|
|
316
|
-
// keep the same id across Edit and Play (mirrors the R3F design adapter's
|
|
317
|
-
// duplicate-OID suffixing exactly).
|
|
318
|
-
const sourceOid = authoringOidOf(object);
|
|
319
|
-
const stamped = reusableStampedIdentity(object, sourceOid);
|
|
320
|
-
if (stamped !== undefined) return stamped;
|
|
321
|
-
// Two live objects carrying one old occurrence stamp is the precise
|
|
322
|
-
// collision the reservation pass repairs. The first object in the
|
|
323
|
-
// current graph keeps the address; this later one receives the first
|
|
324
|
-
// free occurrence and is restamped by walkThreeGraph.
|
|
325
|
-
const sourceIdentity =
|
|
326
|
-
sourceOid === undefined ? undefined : claimSourceIdentity(object, sourceOid);
|
|
327
|
-
// ADMISSION. Three independent grounds, and the FIRST is what makes a
|
|
328
|
-
// play mount project the world the author wrote:
|
|
329
|
-
//
|
|
330
|
-
// - A SOURCE IDENTITY. The served TSX stamped this element, so it is a
|
|
331
|
-
// node the author authored — `<object3D name='Coins'>` as much as the
|
|
332
|
-
// `<Coin/>` instances inside it. Ownership by an ancestor is beside the
|
|
333
|
-
// point: an authored node is not its owner's implementation detail.
|
|
334
|
-
// Without this, a fresh play mount (where nothing is pre-stamped with
|
|
335
|
-
// an `entityId` yet) admitted only the nodes whose `authoringInstance`
|
|
336
|
-
// differed from their parent's — so `Stage`'s whole interior collapsed
|
|
337
|
-
// and its 49 component instances rendered FLAT, with the `Coins` and
|
|
338
|
-
// `Enemies` groups that really parent them gone from the panel.
|
|
339
|
-
// - NO OWNER ABOVE. An unstamped world (an ingest, an adopted graph) has
|
|
340
|
-
// no source identities at all; its outermost objects are the rows, and
|
|
341
|
-
// everything under them stays folded in as parts.
|
|
342
|
-
// - AN EXPLICIT OPT-IN. `userData.authoringRoot` is how a runtime
|
|
343
|
-
// descendant of such a world asks to be a row of its own.
|
|
344
|
-
//
|
|
345
|
-
// What stays folded is exactly what has none of the three: the parts a
|
|
346
|
-
// library builds under an authored node (GLTF meshes, bones, a light's
|
|
347
|
-
// `target`), which the mark convention folds by classification instead.
|
|
348
|
-
if (
|
|
349
|
-
sourceIdentity !== undefined ||
|
|
350
|
-
!context.ownedByAncestor ||
|
|
351
|
-
getUserData(object, 'authoringRoot') === true
|
|
352
|
-
) {
|
|
353
|
-
return sourceIdentity ?? `live:${worldId}:${mintedIdCounter++}`;
|
|
354
|
-
}
|
|
355
|
-
return null;
|
|
356
|
-
},
|
|
357
|
-
};
|
|
358
|
-
}
|
|
359
|
-
|
|
360
|
-
/**
|
|
361
|
-
* Walk a live scene under the OID scheme. Exported because scene ADOPTION runs
|
|
362
|
-
* the same walk the adapter does (`viewport-root-presentation.ts`), and the two must
|
|
363
|
-
* agree about every id.
|
|
364
|
-
*/
|
|
365
|
-
export function projectThreeScene(scene: THREE.Scene, worldId: string): ThreeProjection {
|
|
366
|
-
return walkThreeGraph(scene, liveOidScheme(worldId));
|
|
367
|
-
}
|
|
368
|
-
|
|
369
|
-
/**
|
|
370
|
-
* Stamp identities on a live scene and return the id → Object3D map the store
|
|
371
|
-
* adopts as its `objectMap`. The adapter's own index is built by the same walk,
|
|
372
|
-
* so the two can never disagree about an id.
|
|
373
|
-
*/
|
|
374
|
-
export function stampThreeIdentities(
|
|
375
|
-
scene: THREE.Scene,
|
|
376
|
-
worldId: string,
|
|
377
|
-
): Map<string, THREE.Object3D> {
|
|
378
|
-
const { nodes } = projectThreeScene(scene, worldId);
|
|
379
|
-
const map = new Map<string, THREE.Object3D>();
|
|
380
|
-
for (const [id, node] of nodes) map.set(id, node.object);
|
|
381
|
-
return map;
|
|
382
|
-
}
|
|
383
|
-
|
|
384
|
-
/**
|
|
385
|
-
* The OID identity (see {@link liveOidScheme}), for a LIVE world.
|
|
386
|
-
*
|
|
387
|
-
* `camera` is the SAME allowance {@link structuralIdentity} makes and for the
|
|
388
|
-
* same reason: a captured world's render camera often is not a child of the
|
|
389
|
-
* scene it renders, and a camera that is not a node is not selectable,
|
|
390
|
-
* inspectable, or reachable by the viewport's framing. It joins as an extra
|
|
391
|
-
* root under the fixed {@link CAMERA_ID} only when the walk did not already
|
|
392
|
-
* meet it as a scene child — an R3F world usually authors its camera inside the
|
|
393
|
-
* tree, and admitting it twice would put the same object on two rows.
|
|
394
|
-
*/
|
|
395
|
-
export function oidIdentity(
|
|
396
|
-
worldId: string,
|
|
397
|
-
options: { readonly camera?: THREE.Camera | undefined } = {},
|
|
398
|
-
): ThreeIdentity {
|
|
399
|
-
const camera = options.camera;
|
|
400
|
-
return {
|
|
401
|
-
incremental: { kind: 'live-oid', worldId },
|
|
402
|
-
project: (scene) => {
|
|
403
|
-
const projection = projectThreeScene(scene, worldId);
|
|
404
|
-
if (!camera || [...projection.nodes.values()].some((node) => node.object === camera)) {
|
|
405
|
-
return projection;
|
|
406
|
-
}
|
|
407
|
-
stampEntityId(camera, CAMERA_ID);
|
|
408
|
-
projection.nodes.set(CAMERA_ID, {
|
|
409
|
-
object: camera,
|
|
410
|
-
id: CAMERA_ID,
|
|
411
|
-
parentId: null,
|
|
412
|
-
childIds: [],
|
|
413
|
-
});
|
|
414
|
-
projection.rootIds.push(CAMERA_ID);
|
|
415
|
-
return { ...projection, stats: { ...projection.stats, count: projection.nodes.size } };
|
|
416
|
-
},
|
|
417
|
-
};
|
|
418
|
-
}
|
|
419
|
-
|
|
420
|
-
export interface StructuralIdentityOptions {
|
|
421
|
-
/**
|
|
422
|
-
* A render camera that is NOT a scene child — surfaced as an extra root node
|
|
423
|
-
* under the fixed {@link CAMERA_ID} so it is selectable and inspectable. A
|
|
424
|
-
* captured world's camera usually lives outside the tree it renders.
|
|
425
|
-
*/
|
|
426
|
-
readonly camera?: THREE.Camera | undefined;
|
|
427
|
-
}
|
|
428
|
-
|
|
429
|
-
/**
|
|
430
|
-
* The STRUCTURAL-PATH scheme. Deterministic from the scene's shape (position in
|
|
431
|
-
* the tree + three.js type + name), so the same id re-binds to the same object
|
|
432
|
-
* after the world rebuilds part of its graph within a session. This is the
|
|
433
|
-
* scheme for a graph whose source was never stamped, and it pairs with the
|
|
434
|
-
* creation-site anchors the persistence backend writes through.
|
|
435
|
-
*
|
|
436
|
-
* The walk itself lives in `@volter/editor-threejs/adapter/ingest/structural-ids` — it is
|
|
437
|
-
* the engine's own ingest walk and stays there; this provider is the projection
|
|
438
|
-
* hop, turning its pure flat id→object map into the parent/child node graph
|
|
439
|
-
* every authoring provider reads, WITHOUT stamping the foreign objects.
|
|
440
|
-
*/
|
|
441
|
-
export function structuralIdentity(options: StructuralIdentityOptions = {}): ThreeIdentity {
|
|
442
|
-
const camera = options.camera;
|
|
443
|
-
return {
|
|
444
|
-
project: (scene) => {
|
|
445
|
-
const walk = collectStructuralIds(scene, camera);
|
|
446
|
-
// Reverse the walk's own map rather than storing identity on the foreign
|
|
447
|
-
// objects. A re-walk after a rebuild therefore has no stale stamps to
|
|
448
|
-
// collide with live ids or parent under the wrong object.
|
|
449
|
-
const idByObject = new Map<THREE.Object3D, string>();
|
|
450
|
-
for (const [id, object] of walk.byId) idByObject.set(object, id);
|
|
451
|
-
const nodes = new Map<string, ThreeNode>();
|
|
452
|
-
const rootIds: string[] = [];
|
|
453
|
-
for (const [id, object] of walk.byId) {
|
|
454
|
-
const parent = object.parent;
|
|
455
|
-
const parentId = (parent && parent !== scene ? idByObject.get(parent) : undefined) ?? null;
|
|
456
|
-
// `childIds` is filtered to the walk's own membership: editor furniture
|
|
457
|
-
// parked in the world's scene is skipped by `assignStructuralIds`, and
|
|
458
|
-
// it must not reappear as a hierarchy row through a parent's children.
|
|
459
|
-
const childIds = object.children
|
|
460
|
-
.map((child) => idByObject.get(child))
|
|
461
|
-
.filter((childId): childId is string => childId !== undefined);
|
|
462
|
-
nodes.set(id, { object, id, parentId, childIds });
|
|
463
|
-
if (parentId === null) rootIds.push(id);
|
|
464
|
-
}
|
|
465
|
-
return {
|
|
466
|
-
nodes,
|
|
467
|
-
rootIds,
|
|
468
|
-
stats: {
|
|
469
|
-
count: walk.count,
|
|
470
|
-
meshes: walk.meshes,
|
|
471
|
-
lights: walk.lights,
|
|
472
|
-
...instancedTally(nodes),
|
|
473
|
-
},
|
|
474
|
-
};
|
|
475
|
-
},
|
|
476
|
-
};
|
|
477
|
-
}
|
|
478
|
-
|
|
479
|
-
/**
|
|
480
|
-
* The SOURCE OID identity, for a first-party R3F world whose TSX the editor
|
|
481
|
-
* serves: every object is a node (the lane's hierarchy view does its own
|
|
482
|
-
* collapsing on top), addressed by its authored callsite where it has one and
|
|
483
|
-
* by its structural path where it does not.
|
|
484
|
-
*
|
|
485
|
-
* It also reports the two OID-lane facts the walk is the only place able to
|
|
486
|
-
* measure, refreshed on every `project`:
|
|
487
|
-
*
|
|
488
|
-
* - `instanceRoots` — how many times each authored element RENDERS, counted at
|
|
489
|
-
* instance ROOTS only (an object with no ancestor carrying the same
|
|
490
|
-
* identity). Every host element inside a component definition carries that
|
|
491
|
-
* instance's stamp, so counting stamped objects would report a component as
|
|
492
|
-
* rendering ~50 times instead of once.
|
|
493
|
-
* - `representatives` — the FIRST live object that resolved to each oid: the
|
|
494
|
-
* occurrence that keeps the bare address, and the one every `…#n`
|
|
495
|
-
* occurrence's write authority is decided against.
|
|
496
|
-
*/
|
|
497
|
-
export interface SourceOidIdentity extends ThreeIdentity {
|
|
498
|
-
readonly instanceRoots: ReadonlyMap<string, number>;
|
|
499
|
-
readonly representatives: ReadonlyMap<string, THREE.Object3D>;
|
|
500
|
-
}
|
|
501
|
-
|
|
502
|
-
export function sourceOidIdentity(worldId: string): SourceOidIdentity {
|
|
503
|
-
let instanceRoots: ReadonlyMap<string, number> = new Map();
|
|
504
|
-
let representatives: ReadonlyMap<string, THREE.Object3D> = new Map();
|
|
505
|
-
return {
|
|
506
|
-
get instanceRoots() {
|
|
507
|
-
return instanceRoots;
|
|
508
|
-
},
|
|
509
|
-
get representatives() {
|
|
510
|
-
return representatives;
|
|
511
|
-
},
|
|
512
|
-
project: (scene) => {
|
|
513
|
-
const oidOccurrence = new Map<string, number>();
|
|
514
|
-
const roots = new Map<string, number>();
|
|
515
|
-
const reps = new Map<string, THREE.Object3D>();
|
|
516
|
-
/** Authoring identities of the objects ON the current DFS path. */
|
|
517
|
-
const ancestorIdentities = new Set<string>();
|
|
518
|
-
const openedBy = new Map<THREE.Object3D, string>();
|
|
519
|
-
const projection = walkThreeGraph(scene, {
|
|
520
|
-
identify: (object, context) => {
|
|
521
|
-
const oid = authoringOidOf(object);
|
|
522
|
-
if (oid === undefined) return threeEntityId(worldId, `@${context.path.join('.')}`);
|
|
523
|
-
const n = oidOccurrence.get(oid) ?? 0;
|
|
524
|
-
oidOccurrence.set(oid, n + 1);
|
|
525
|
-
if (n === 0) reps.set(oid, object);
|
|
526
|
-
if (!ancestorIdentities.has(oid)) {
|
|
527
|
-
roots.set(oid, (roots.get(oid) ?? 0) + 1);
|
|
528
|
-
ancestorIdentities.add(oid);
|
|
529
|
-
openedBy.set(object, oid);
|
|
530
|
-
}
|
|
531
|
-
return occurrenceId(threeEntityId(worldId, oid), n);
|
|
532
|
-
},
|
|
533
|
-
leave: (object) => {
|
|
534
|
-
const opened = openedBy.get(object);
|
|
535
|
-
if (opened === undefined) return;
|
|
536
|
-
ancestorIdentities.delete(opened);
|
|
537
|
-
openedBy.delete(object);
|
|
538
|
-
},
|
|
539
|
-
});
|
|
540
|
-
instanceRoots = roots;
|
|
541
|
-
representatives = reps;
|
|
542
|
-
return projection;
|
|
543
|
-
},
|
|
544
|
-
};
|
|
545
|
-
}
|
|
546
|
-
|
|
547
197
|
/**
|
|
548
198
|
* The DOCUMENT-PATH identity, for a native Object3D graph opened as a document
|
|
549
199
|
* (a loaded model, a project source graph): document-scoped structural ids over
|
|
@@ -580,10 +230,7 @@ export function nativeKindOf(object: THREE.Object3D): EditorNode['kind'] {
|
|
|
580
230
|
if ((object as THREE.Bone).isBone) return 'bone';
|
|
581
231
|
if ((object as THREE.Light).isLight) return 'light';
|
|
582
232
|
if ((object as THREE.Camera).isCamera) return 'camera';
|
|
583
|
-
//
|
|
584
|
-
// flag to read like the lines above, and an `Audio`/`AudioListener` carries
|
|
585
|
-
// no geometry, so without this it fell through to the generic `object`
|
|
586
|
-
// circle and a world's sound was the one thing the hierarchy could not name.
|
|
233
|
+
// Audio nodes expose their kind through type rather than an isAudio flag or geometry.
|
|
587
234
|
if (
|
|
588
235
|
object.type === 'Audio' ||
|
|
589
236
|
object.type === 'PositionalAudio' ||
|
|
@@ -886,13 +533,13 @@ export class ThreeProjector {
|
|
|
886
533
|
private pickProjection():
|
|
887
534
|
| {
|
|
888
535
|
objects: Iterable<THREE.Object3D>;
|
|
889
|
-
|
|
536
|
+
idForNativeObject: (object: THREE.Object3D) => string | null;
|
|
890
537
|
}
|
|
891
538
|
| undefined {
|
|
892
539
|
if (this.options.pickScope === 'store-object-map') return undefined;
|
|
893
540
|
return {
|
|
894
541
|
objects: this.objectsById.values(),
|
|
895
|
-
|
|
542
|
+
idForNativeObject: (object) => this.idOf(object),
|
|
896
543
|
};
|
|
897
544
|
}
|
|
898
545
|
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
interface DisposableSource {
|
|
2
|
+
dispose(): void;
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
export interface RetainedDocumentState {
|
|
6
|
+
readonly documentId: string;
|
|
7
|
+
inUse: boolean;
|
|
8
|
+
source: DisposableSource | null;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/** CPU state belongs to an open document, not to the lifetime of its last pane. */
|
|
12
|
+
export class RetainedDocumentStates<T extends RetainedDocumentState> {
|
|
13
|
+
private readonly states = new Map<string, T[]>();
|
|
14
|
+
private readonly generations = new WeakMap<T, number>();
|
|
15
|
+
private unsubscribe: (() => void) | null = null;
|
|
16
|
+
|
|
17
|
+
constructor(
|
|
18
|
+
private readonly create: (documentId: string) => T,
|
|
19
|
+
private readonly isOpen: (documentId: string) => boolean,
|
|
20
|
+
private readonly subscribe: (changed: () => void) => () => void,
|
|
21
|
+
) {}
|
|
22
|
+
|
|
23
|
+
claim(documentId: string): T {
|
|
24
|
+
const states = this.states.get(documentId) ?? [];
|
|
25
|
+
let state = states.find(candidate => !candidate.inUse);
|
|
26
|
+
if (!state) {
|
|
27
|
+
state = this.create(documentId);
|
|
28
|
+
states.push(state);
|
|
29
|
+
this.states.set(documentId, states);
|
|
30
|
+
}
|
|
31
|
+
state.inUse = true;
|
|
32
|
+
this.generations.set(state, this.generation(state) + 1);
|
|
33
|
+
this.unsubscribe ??= this.subscribe(() => this.discardClosed());
|
|
34
|
+
return state;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
generation(state: T): number {
|
|
38
|
+
return this.generations.get(state) ?? 0;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
release(state: T, generation = this.generation(state)): void {
|
|
42
|
+
if (this.generation(state) !== generation) return;
|
|
43
|
+
state.inUse = false;
|
|
44
|
+
// React cleanup may precede the registry's close notification. The
|
|
45
|
+
// subscription also handles closing a pane that was already unmounted.
|
|
46
|
+
queueMicrotask(() => this.discardClosed());
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** A gesture may finish releasing its source after its document has closed
|
|
50
|
+
* or its state slot has been claimed by a replacement pane. */
|
|
51
|
+
retain(state: T, source: NonNullable<T['source']>, generation: number): void {
|
|
52
|
+
if (
|
|
53
|
+
!this.isOpen(state.documentId) ||
|
|
54
|
+
!this.states.get(state.documentId)?.includes(state) ||
|
|
55
|
+
this.generation(state) !== generation
|
|
56
|
+
) {
|
|
57
|
+
source.dispose();
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
const previous = state.source;
|
|
61
|
+
state.source = source;
|
|
62
|
+
if (previous !== source) previous?.dispose();
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
private discardClosed(): void {
|
|
66
|
+
const discarded: T[] = [];
|
|
67
|
+
for (const [documentId, states] of this.states) {
|
|
68
|
+
if (this.isOpen(documentId)) continue;
|
|
69
|
+
const remaining = states.filter(state => state.inUse);
|
|
70
|
+
discarded.push(...states.filter(state => !state.inUse));
|
|
71
|
+
if (remaining.length) this.states.set(documentId, remaining);
|
|
72
|
+
else this.states.delete(documentId);
|
|
73
|
+
}
|
|
74
|
+
if (this.states.size === 0) {
|
|
75
|
+
this.unsubscribe?.();
|
|
76
|
+
this.unsubscribe = null;
|
|
77
|
+
}
|
|
78
|
+
// Remove ownership first, even if a disposer throws or publishes a change.
|
|
79
|
+
const failures: unknown[] = [];
|
|
80
|
+
for (const state of discarded) {
|
|
81
|
+
const source = state.source;
|
|
82
|
+
state.source = null;
|
|
83
|
+
try {
|
|
84
|
+
source?.dispose();
|
|
85
|
+
} catch (error) {
|
|
86
|
+
failures.push(error);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
if (failures.length) throw new AggregateError(failures, 'Retained document cleanup failed');
|
|
90
|
+
}
|
|
91
|
+
}
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* THE SCENE'S VIEWPORT (`@volter/sdk/
|
|
2
|
+
* THE SCENE'S VIEWPORT (`@volter/sdk/document-viewports`): the world
|
|
3
3
|
* stage draws the Scene through the session store's own camera, shading and
|
|
4
4
|
* grid, and frames the shell's selection. It keeps no selection of its own, so a
|
|
5
5
|
* view's selection is the shell's.
|
|
6
6
|
*/
|
|
7
|
-
import type { DocumentViewport } from '@volter/sdk/
|
|
7
|
+
import type { DocumentViewport } from '@volter/sdk/document-viewports';
|
|
8
8
|
import type { EditorShellStore } from './editor-shell-store';
|
|
9
|
-
import { viewGridVisible } from '@volter/sdk/
|
|
9
|
+
import { viewGridVisible } from '@volter/sdk/viewport-presentation';
|
|
10
10
|
|
|
11
11
|
type ShadingMode = Parameters<EditorShellStore['setShadingMode']>[0];
|
|
12
12
|
|