@vgai/engine 0.5.1 → 0.5.3
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/README.md +18 -11
- package/package.json +10 -6
- package/schemas/engine-api.json +1 -68
- package/schemas/engine-api.md +1 -32
- package/schemas/engine-capabilities.json +30 -42
- package/schemas/{vgai-game.schema.json → vgai-project.schema.json} +52 -34
- package/src/adapter/adapter-surface.ts +5 -5
- package/src/adapter/authoring.ts +168 -226
- package/src/adapter/colyseus-networking-adapter.ts +44 -5
- package/src/adapter/first-party-systems.ts +156 -42
- package/src/adapter/host-context.ts +177 -46
- package/src/adapter/index.ts +47 -51
- package/src/adapter/ingest/game-contract.ts +2 -2
- package/src/adapter/ingest/scene-capture.ts +18 -19
- package/src/adapter/ingest/structural-ids.ts +127 -0
- package/src/adapter/ingest/upstream-pin.ts +9 -12
- package/src/adapter/loop-gate-report.ts +11 -11
- package/src/adapter/rapier-physics-adapter.ts +27 -9
- package/src/adapter/root-adapter.ts +217 -0
- package/src/adapter/{vgai-scene-game-adapter.ts → setup-three-root-adapter.ts} +173 -351
- package/src/adapter/system-adapter.ts +80 -63
- package/src/ai/navigation.ts +1 -1
- package/src/animation/animation-clock.ts +1 -1
- package/src/animation/camera-ownership.ts +1 -2
- package/src/animation/cubic-spline-interpolant.ts +132 -0
- package/src/animation/theatre-clock-binding.ts +2 -2
- package/src/animation/theatre-object-binding.ts +4 -4
- package/src/animation/xstate-animation-binding.ts +75 -5
- package/src/{scene/schema → asset-formats}/camera.ts +2 -5
- package/src/{scene/schema → asset-formats}/collider.ts +2 -5
- package/src/asset-formats/index.ts +54 -0
- package/src/{scene/schema → asset-formats}/instances.ts +10 -6
- package/src/{scene/schema → asset-formats}/light.ts +3 -6
- package/src/{scene/schema → asset-formats}/material.ts +4 -7
- package/src/{scene/schema → asset-formats}/mesh.ts +4 -7
- package/src/asset-formats/parse.ts +39 -0
- package/src/{scene/schema → asset-formats}/particles.ts +3 -6
- package/src/{scene/schema/environment.ts → asset-formats/render-env.ts} +23 -86
- package/src/{scene/schema → asset-formats}/tuples.ts +1 -1
- package/src/{scene/asset-loaders.ts → asset-loaders.ts} +8 -9
- package/src/asset-parse-error.ts +33 -0
- package/src/{scene/asset-registry.ts → asset-registry.ts} +1 -1
- package/src/assets.ts +1 -1
- package/src/audio/wav-encode.ts +9 -9
- package/src/canvas-react/engine-bridge.ts +59 -0
- package/src/canvas-react/index.ts +50 -0
- package/src/canvas-react/pixi-primitive.tsx +202 -0
- package/src/canvas-react/pixi-react-adapter.tsx +290 -0
- package/src/canvas-react/pixi-react-root-factory.tsx +88 -0
- package/src/canvas-react/world-context.ts +328 -0
- package/src/core/frame-pacing.ts +100 -0
- package/src/core/game-loop.ts +50 -28
- package/src/core/seeded-random.ts +7 -7
- package/src/core/sim-clock.ts +388 -0
- package/src/core/system-runner.ts +17 -63
- package/src/core/types.ts +34 -15
- package/src/data/data-asset.ts +3 -3
- package/src/data/data-check-core.ts +6 -7
- package/src/data/data-ref.ts +11 -11
- package/src/data/vite-plugin-data.ts +10 -10
- package/src/{scene/defaults.ts → defaults.ts} +18 -40
- package/src/dev/render-debug-adapter.ts +1 -1
- package/src/dev/webgl-frame-capture.ts +1 -1
- package/src/ecs/scene-index.ts +439 -0
- package/src/ecs/scene-query.ts +43 -0
- package/src/{scene → ecs}/user-data.ts +17 -36
- package/src/index.ts +7 -9
- package/src/input/input-manager.ts +29 -32
- package/src/input/input-types.ts +2 -2
- package/src/input/schema.ts +5 -5
- package/src/loader.ts +57 -0
- package/src/manifest/editor-port.ts +69 -0
- package/src/manifest/filename.ts +49 -0
- package/src/manifest/index.ts +8 -2
- package/src/manifest/load-file.ts +11 -0
- package/src/manifest/load.ts +65 -77
- package/src/manifest/locate.ts +55 -0
- package/src/manifest/schema.ts +400 -233
- package/src/{scene → physics}/collider-dimensions.ts +3 -3
- package/src/physics/physics-registry.ts +1 -1
- package/src/{world2d/authoring-2d.ts → pixi/authoring.ts} +24 -11
- package/src/pixi/index.ts +43 -0
- package/src/{world2d/ingest-iframe-2d.ts → pixi/ingest-iframe.ts} +9 -9
- package/src/{world2d/ingest2d.ts → pixi/ingest.ts} +28 -28
- package/src/{world2d/physics2d-registry.ts → pixi/physics-registry.ts} +1 -1
- package/src/{world2d/scene-capture-2d.ts → pixi/scene-capture.ts} +3 -3
- package/src/{world2d/system-adapters-2d.ts → pixi/system-adapters.ts} +2 -2
- package/src/react/unmanaged-root-detector.ts +26 -1
- package/src/react/use-data.ts +5 -5
- package/src/react/use-selection.tsx +15 -42
- package/src/react/{game-state.tsx → world-state.tsx} +44 -46
- package/src/render/auto-batcher.ts +1 -2
- package/src/{scene → render}/instance-mesh.ts +1 -1
- package/src/{scene → render}/light-camera-factory.ts +14 -13
- package/src/render/lod.ts +17 -0
- package/src/{scene → render}/material-factory.ts +8 -7
- package/src/{scene → render}/particles-factory.ts +62 -12
- package/src/render/render-batch-system.ts +14 -41
- package/src/render/render-features.ts +1 -1
- package/src/render/render-settings.ts +1 -2
- package/src/render/spark-renderer-lifecycle.ts +1 -1
- package/src/runtime/create-runtime.ts +352 -463
- package/src/runtime/debug-bridge.ts +148 -93
- package/src/runtime/debug-registry.ts +79 -54
- package/src/runtime/dev-layers.ts +40 -0
- package/src/runtime/frame-selector-cache.ts +4 -4
- package/src/runtime/game.ts +618 -369
- package/src/runtime/gameplay-rng-trap.ts +6 -7
- package/src/runtime/input-router.ts +11 -11
- package/src/runtime/mount-game.ts +54 -55
- package/src/runtime/mount-manifest.ts +154 -150
- package/src/runtime/presentation.ts +141 -0
- package/src/runtime/render-audio-control.ts +64 -53
- package/src/runtime/render-control.ts +45 -51
- package/src/runtime/render-seed.ts +3 -4
- package/src/runtime/state-bridge.ts +17 -18
- package/src/runtime/types.ts +94 -61
- package/src/setup/setup-renderer.ts +11 -6
- package/src/world3d-react/engine-bridge.ts +46 -33
- package/src/world3d-react/index.ts +31 -26
- package/src/world3d-react/r3f-adapter.tsx +211 -96
- package/src/world3d-react/r3f-root-factory.tsx +91 -0
- package/src/world3d-react/renderer-config.ts +137 -0
- package/src/world3d-react/world-context.ts +141 -111
- package/schemas/entity2d.schema.json +0 -468
- package/schemas/prefab.schema.json +0 -9992
- package/schemas/scn2d.schema.json +0 -494
- package/schemas/vscn.schema.json +0 -10851
- package/src/adapter/game-adapter.ts +0 -164
- package/src/adapter/ingest/overlay-applier.ts +0 -207
- package/src/adapter/ingest/overlay-apply.ts +0 -168
- package/src/adapter/ingest/overlay-file.ts +0 -126
- package/src/adapter/ingest/overlay-report.ts +0 -176
- package/src/animation/gsap-registration.ts +0 -184
- package/src/audio/audio-introspection.ts +0 -290
- package/src/audio/index.ts +0 -39
- package/src/audio/tone-clock-binding.ts +0 -98
- package/src/audio/tone-context.ts +0 -175
- package/src/audio/tone-offline-render.ts +0 -167
- package/src/ecs/component-manager.ts +0 -814
- package/src/ecs/game-component.ts +0 -260
- package/src/ecs/hmr-swap-report.ts +0 -65
- package/src/physics/trigger-dispatch.ts +0 -97
- package/src/react/root-adapter.tsx +0 -49
- package/src/scene/asset-paths.ts +0 -121
- package/src/scene/asset-ref-check.ts +0 -248
- package/src/scene/component-registry.ts +0 -51
- package/src/scene/parse.ts +0 -204
- package/src/scene/scene-apply.ts +0 -407
- package/src/scene/scene-diff-schema.ts +0 -115
- package/src/scene/scene-diff-types.ts +0 -29
- package/src/scene/scene-loader.ts +0 -1526
- package/src/scene/scene-query.ts +0 -63
- package/src/scene/scene-types.ts +0 -33
- package/src/scene/scene-version.ts +0 -40
- package/src/scene/schema/animation.ts +0 -46
- package/src/scene/schema/audio.ts +0 -25
- package/src/scene/schema/entity-ref.ts +0 -78
- package/src/scene/schema/entity.ts +0 -189
- package/src/scene/schema/index.ts +0 -51
- package/src/scene/schema/joint.ts +0 -26
- package/src/scene/schema/physics.ts +0 -49
- package/src/scene/schema/scene-file.ts +0 -292
- package/src/scene/schema/shadow.ts +0 -24
- package/src/scene/schema/spline.ts +0 -21
- package/src/world2d/asset-paths2d.ts +0 -44
- package/src/world2d/capture-to-scene2d.ts +0 -52
- package/src/world2d/collision-2d.ts +0 -99
- package/src/world2d/entity2d-asset.ts +0 -22
- package/src/world2d/index.ts +0 -91
- package/src/world2d/physics2d-transform.ts +0 -173
- package/src/world2d/physics2d-units.ts +0 -10
- package/src/world2d/pixi-game-adapter.ts +0 -439
- package/src/world2d/pixi-surface.ts +0 -78
- package/src/world2d/scene2d-identity.ts +0 -49
- package/src/world2d/scene2d-loader.ts +0 -433
- package/src/world2d/schema/entity2d.ts +0 -163
- package/src/world2d/schema/physics2d.ts +0 -64
- package/src/world2d/schema/sprite.ts +0 -99
- package/src/world2d/schema/tilemap.ts +0 -39
- package/src/world2d/schema/tuples2d.ts +0 -25
- package/src/world2d/transform-writer-2d.ts +0 -42
- package/src/world2d/types.ts +0 -74
- package/src/world3d-react/behavior.tsx +0 -146
- /package/src/{scene → render}/mesh-shadow.ts +0 -0
package/src/data/data-ref.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* `file#key` refs — the resolvable half
|
|
2
|
+
* `file#key` refs — the resolvable half.
|
|
3
3
|
*
|
|
4
4
|
* Cross-asset references (`"enemies#goblin"`, `"tuning#economy.slotGrowth"`)
|
|
5
5
|
* have been DETECTED and VALIDATED since W5 (`data-check-core.ts`'s
|
|
@@ -24,13 +24,13 @@
|
|
|
24
24
|
* (it reads `target.get()` at CALL time, same doctrine as every other data
|
|
25
25
|
* read — "read every frame", never cache across frames).
|
|
26
26
|
*
|
|
27
|
-
* Deliberately NOT built (
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
27
|
+
* Deliberately NOT built (and the design note this module's history carries):
|
|
28
|
+
* eager resolution inside `defineData` (would change `get()`'s shipped output
|
|
29
|
+
* type and entangle HMR — an edit to `enemies.data.json` would have to
|
|
30
|
+
* re-notify every OTHER asset that references it), and a global
|
|
31
|
+
* `resolveRef(addressString)` (needs a runtime stem→handle registry that
|
|
32
|
+
* `src/data/assets.ts` — side-effect-free, config-load-time, schemas-only by
|
|
33
|
+
* its own module doc — must not become).
|
|
34
34
|
*/
|
|
35
35
|
|
|
36
36
|
import { z } from 'zod';
|
|
@@ -71,7 +71,7 @@ export function dataRef<Stem extends string>(target: Stem): z.ZodType<DataRef<St
|
|
|
71
71
|
.string()
|
|
72
72
|
.regex(refPattern(target), {
|
|
73
73
|
message:
|
|
74
|
-
`must be a "${target}#<key>" reference
|
|
74
|
+
`must be a "${target}#<key>" reference — ` +
|
|
75
75
|
`expected the target asset stem "${target}", e.g. "${target}#some-key".`,
|
|
76
76
|
})
|
|
77
77
|
.meta({ 'x-vgai-ref': target }) as z.ZodType<DataRef<Stem>>;
|
|
@@ -110,7 +110,7 @@ export function getRef<T>(target: DataHandle<Record<string, T>>, ref: string): T
|
|
|
110
110
|
const m = DATA_REF_PATTERN.exec(ref);
|
|
111
111
|
if (!m) {
|
|
112
112
|
throw new Error(
|
|
113
|
-
`getRef: "${ref}" is not a "file#key" reference
|
|
113
|
+
`getRef: "${ref}" is not a "file#key" reference — ` +
|
|
114
114
|
'expected a string shaped like "some-file#some-key".',
|
|
115
115
|
);
|
|
116
116
|
}
|
|
@@ -136,7 +136,7 @@ export function getRef<T>(target: DataHandle<Record<string, T>>, ref: string): T
|
|
|
136
136
|
throw new Error(
|
|
137
137
|
`getRef: dangling ref "${ref}" — "${missing}" does not exist` +
|
|
138
138
|
(targetStem ? ` in ${targetStem}.data.json` : '') +
|
|
139
|
-
'. Fix the key or remove the ref (refs are "file#key" strings
|
|
139
|
+
'. Fix the key or remove the ref (refs are "file#key" strings).',
|
|
140
140
|
);
|
|
141
141
|
}
|
|
142
142
|
let node: unknown = root;
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* `vgaiDataCheck` — build-path validation for data assets and project tools
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
2
|
+
* `vgaiDataCheck` — build-path validation for data assets and project tools (the
|
|
3
|
+
* enforcement half of W5). A Vite plugin for the PROJECT's own `vite.config.ts`
|
|
4
|
+
* (the template wires it; the editor dev server boots Vite from the ENGINE's
|
|
5
|
+
* config and never runs this — its surface is covered by `vgai doctor` instead).
|
|
6
|
+
* Build-only (`apply: 'build'`): in dev, `defineData`'s parse-on-load already
|
|
7
|
+
* fails loud in the console.
|
|
8
8
|
*
|
|
9
9
|
* Three checks, all failing the build loud:
|
|
10
10
|
*
|
|
@@ -206,7 +206,7 @@ export function collectDataCheckProblems(options: VgaiDataCheckOptions): {
|
|
|
206
206
|
`dangling data ref in "src/data/${dangling.inAsset}.data.json" at ${dangling.atPath}: ` +
|
|
207
207
|
`"${dangling.ref}" — "${dangling.missingSegment}" does not exist in ` +
|
|
208
208
|
`src/data/${dangling.targetAsset}.data.json. Fix the key or remove the ref ` +
|
|
209
|
-
'(refs are "file#key" strings
|
|
209
|
+
'(refs are "file#key" strings).',
|
|
210
210
|
);
|
|
211
211
|
}
|
|
212
212
|
|
|
@@ -263,8 +263,8 @@ export function vgaiDataCheck(options: VgaiDataCheckOptions): Plugin {
|
|
|
263
263
|
// ONE error carrying every problem — fail loud with the full picture,
|
|
264
264
|
// not a fix-one-rebuild-see-the-next loop.
|
|
265
265
|
this.error(
|
|
266
|
-
`data-asset validation failed (${errors.length} problem${errors.length === 1 ? '' : 's'}
|
|
267
|
-
|
|
266
|
+
`data-asset validation failed (${errors.length} problem${errors.length === 1 ? '' : 's'}` +
|
|
267
|
+
`):\n${errors.map((e) => ` - ${e}`).join('\n')}`,
|
|
268
268
|
);
|
|
269
269
|
}
|
|
270
270
|
},
|
|
@@ -279,7 +279,7 @@ export function vgaiDataCheck(options: VgaiDataCheckOptions): Plugin {
|
|
|
279
279
|
if (leaked.size > 0) {
|
|
280
280
|
this.error(
|
|
281
281
|
'project tool code reached the game build — tools are editor-only and must never ship ' +
|
|
282
|
-
'to players
|
|
282
|
+
'to players. Remove every game-code import of these ' +
|
|
283
283
|
`modules (tools may import game code, never the reverse):\n${[...leaked]
|
|
284
284
|
.map((id) => ` - ${id}`)
|
|
285
285
|
.join('\n')}`,
|
|
@@ -1,17 +1,22 @@
|
|
|
1
|
-
/**
|
|
1
|
+
/**
|
|
2
|
+
* Single source of truth for default values the engine's factories read.
|
|
3
|
+
*
|
|
4
|
+
* WO-8 deleted SEVEN groups — `transform`, `entity`, `audio`, `shadow`,
|
|
5
|
+
* `animation`, `fog`, `environment` — because every one of their readers was a
|
|
6
|
+
* `.vscn.json` consumer (`entity-factory.ts`, `scene-loader.ts`,
|
|
7
|
+
* `scene-sync.ts`, `scene-serializer.ts`, the per-section inspectors) and
|
|
8
|
+
* `defaults-source-scan.test.ts` reported them with ZERO `DEFAULTS.<group>`
|
|
9
|
+
* readers anywhere in engine/editor src or tests. That guard offers two remedies
|
|
10
|
+
* — wire the group to the factory that reads it, or delete it if truly unused —
|
|
11
|
+
* and there is no surviving factory to wire these to: `transform` restated
|
|
12
|
+
* THREE's own Object3D constructor values, `entity.locked`/ `pivot` were
|
|
13
|
+
* descriptor-only fields, and `fog`/`environment.envMapIntensity`/ `shadow` were
|
|
14
|
+
* the `.vscn` `environment` block that `render-env.ts` explicitly did NOT carry
|
|
15
|
+
* forward. Keeping them would have been exactly the dead documentation T1.13
|
|
16
|
+
* created this guard to prevent.
|
|
17
|
+
*
|
|
18
|
+
*/
|
|
2
19
|
export const DEFAULTS = {
|
|
3
|
-
transform: {
|
|
4
|
-
position: [0, 0, 0] as [number, number, number],
|
|
5
|
-
rotation: [0, 0, 0, 1] as [number, number, number, number],
|
|
6
|
-
scale: [1, 1, 1] as [number, number, number],
|
|
7
|
-
},
|
|
8
|
-
|
|
9
|
-
entity: {
|
|
10
|
-
visible: true,
|
|
11
|
-
locked: false,
|
|
12
|
-
pivot: [0, 0, 0] as [number, number, number],
|
|
13
|
-
},
|
|
14
|
-
|
|
15
20
|
material: {
|
|
16
21
|
type: 'standard' as const,
|
|
17
22
|
color: '#888888',
|
|
@@ -57,39 +62,12 @@ export const DEFAULTS = {
|
|
|
57
62
|
height: 1080,
|
|
58
63
|
},
|
|
59
64
|
|
|
60
|
-
audio: {
|
|
61
|
-
spatial: true,
|
|
62
|
-
volume: 1,
|
|
63
|
-
refDistance: 1,
|
|
64
|
-
rolloffFactor: 1,
|
|
65
|
-
maxDistance: 10000,
|
|
66
|
-
loop: false,
|
|
67
|
-
autoplay: false,
|
|
68
|
-
},
|
|
69
|
-
|
|
70
65
|
collider: {
|
|
71
66
|
cuboid: { halfExtents: [0.5, 0.5, 0.5] as [number, number, number] },
|
|
72
67
|
ball: { radius: 0.5 },
|
|
73
68
|
capsule: { radius: 0.25, halfHeight: 0.5 },
|
|
74
69
|
},
|
|
75
70
|
|
|
76
|
-
shadow: {
|
|
77
|
-
mapSize: 1024,
|
|
78
|
-
},
|
|
79
|
-
|
|
80
|
-
animation: {
|
|
81
|
-
loop: true,
|
|
82
|
-
},
|
|
83
|
-
|
|
84
|
-
fog: {
|
|
85
|
-
linear: { near: 1, far: 100 },
|
|
86
|
-
exponential: { density: 0.01 },
|
|
87
|
-
},
|
|
88
|
-
|
|
89
|
-
environment: {
|
|
90
|
-
envMapIntensity: 1.0,
|
|
91
|
-
},
|
|
92
|
-
|
|
93
71
|
toneMapping: {
|
|
94
72
|
// T1.13: the effective renderer default is ACES (see
|
|
95
73
|
// setup-renderer.ts's `createHostRenderer` + `applyScenePostProcessing`,
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* the WebGL2 frame capture (`webgl-frame-capture.ts`), a live three.js scene,
|
|
4
4
|
* and the render adapter's per-frame render pass.
|
|
5
5
|
*
|
|
6
|
-
* Split OUT of `
|
|
6
|
+
* Split OUT of `setup-three-root-adapter.ts` so the capture/attribution/restore
|
|
7
7
|
* flow is unit-testable in a headless Node test with a mock GL context and a
|
|
8
8
|
* plain scene — a real non-headless GPU mount (EffectComposer + WebGLRenderer)
|
|
9
9
|
* cannot run under vitest here, the same reason the adapter suite drives
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* WHY FIRST-PARTY, NOT spectorjs (recorded per the "use libraries directly, no
|
|
5
5
|
* wrappers" rule): the capture seam we need is the WebGL2 context this engine
|
|
6
6
|
* ALREADY owns end-to-end (`renderer.getContext()` in
|
|
7
|
-
* `
|
|
7
|
+
* `setup-three-root-adapter.ts`). spectorjs is absent from node_modules, and its
|
|
8
8
|
* actual value is a bundled inspector UI we would discard — adopting it imports
|
|
9
9
|
* ~2MB of library to keep ~10% of it, and it wraps the context with its own
|
|
10
10
|
* global patching model rather than the instance-shadow-and-restore discipline
|
|
@@ -0,0 +1,439 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `createSceneIndex` — the LIVE scene index (punchlist P2, `observe`).
|
|
3
|
+
*
|
|
4
|
+
* `scene-query.ts` answers "which objects carry tag X?" by walking the whole
|
|
5
|
+
* graph on every call, and it tells nobody when the answer changes. This
|
|
6
|
+
* module is the notified half: a tag/entity-id index kept current as objects
|
|
7
|
+
* join and leave the scene, plus the change signals a game needs so it never
|
|
8
|
+
* has to poll.
|
|
9
|
+
*
|
|
10
|
+
* ## The one event primitive is `EventTarget`
|
|
11
|
+
*
|
|
12
|
+
* There is no bus, no emitter class, no `subscribe()` returning a closure.
|
|
13
|
+
* `index.events` is a plain `EventTarget` dispatching `CustomEvent`s, and
|
|
14
|
+
* **unsubscribe is `AbortSignal`** — the platform's own answer:
|
|
15
|
+
*
|
|
16
|
+
* ```ts
|
|
17
|
+
* const ac = new AbortController();
|
|
18
|
+
* ctx.sceneIndex.events.addEventListener(
|
|
19
|
+
* 'tagadded',
|
|
20
|
+
* (e) => console.log((e as SceneIndexEvent<'tagadded'>).detail.object.name),
|
|
21
|
+
* { signal: ac.signal },
|
|
22
|
+
* );
|
|
23
|
+
* ac.abort(); // done — nothing to remember, nothing to leak
|
|
24
|
+
* ```
|
|
25
|
+
*
|
|
26
|
+
* {@link onSceneIndexEvent} is the same call with the detail typed for you.
|
|
27
|
+
* `index.eventsFor(obj)` is the per-object target (Roblox's `inst.Changed`):
|
|
28
|
+
* the same events, already filtered to one object, so a listener that only
|
|
29
|
+
* cares about one entity does not filter by hand.
|
|
30
|
+
*
|
|
31
|
+
* ## How the index stays live: three's own parent events, not a monkeypatch
|
|
32
|
+
*
|
|
33
|
+
* three r155+ dispatches `childadded`/`childremoved` on the **parent** from
|
|
34
|
+
* `Object3D.add`/`remove` (and therefore from `attach`, `clear`,
|
|
35
|
+
* `removeFromParent`, and R3F's reconciler, which all go through those two).
|
|
36
|
+
* The index subscribes to those two events on every object it indexes; on
|
|
37
|
+
* `childadded` it walks the new subtree, indexes it, and subscribes each
|
|
38
|
+
* node, and on `childremoved` it does the inverse. `Object3D.prototype` is
|
|
39
|
+
* NOT patched — nothing in this repo patches a library prototype.
|
|
40
|
+
*
|
|
41
|
+
* Consequences worth knowing:
|
|
42
|
+
*
|
|
43
|
+
* - **The quiet-frame cost is zero.** The index registers no system and runs
|
|
44
|
+
* no per-frame reconcile; it does work only when three tells it something
|
|
45
|
+
* moved. Nothing here ticks.
|
|
46
|
+
* - **Reparenting nets out to one entry.** `parent.add(obj)` removes `obj`
|
|
47
|
+
* from its old parent first, so a move inside the indexed tree fires
|
|
48
|
+
* `childremoved` then `childadded` — unindex, then index. The object ends
|
|
49
|
+
* up indexed exactly once (and a listener sees an `entityremoved` followed
|
|
50
|
+
* by an `entityadded`, which is the truth about what happened).
|
|
51
|
+
* - **Splicing `parent.children` by hand is invisible to the index**, because
|
|
52
|
+
* it is invisible to three as well. Use `add`/`remove`.
|
|
53
|
+
*
|
|
54
|
+
* ## Tags are stored where they always were
|
|
55
|
+
*
|
|
56
|
+
* `addTag`/`removeTag` write the existing `userData['tags']` array, so
|
|
57
|
+
* `queryByTag` keeps returning the truth and nothing has to be migrated. The
|
|
58
|
+
* index is the fast, notified path over the same storage. A game that writes
|
|
59
|
+
* `userData['tags']` by hand gets a stale index — that is not prevented, it is
|
|
60
|
+
* repaired: call {@link SceneIndex.reindex} on the object.
|
|
61
|
+
*
|
|
62
|
+
* ## Attributes
|
|
63
|
+
*
|
|
64
|
+
* `getAttribute`/`setAttribute`/`deleteAttribute` are typed accessors over ONE
|
|
65
|
+
* new reserved userData key, `attributes` (registered in `ecs/user-data.ts`,
|
|
66
|
+
* `Record<string, string | number | boolean>`). They emit `attributechanged`
|
|
67
|
+
* only when the value actually changes, so a re-set of the same value is
|
|
68
|
+
* silent.
|
|
69
|
+
*
|
|
70
|
+
* ## What this deliberately does NOT do
|
|
71
|
+
*
|
|
72
|
+
* - **No property-level `Changed` on transforms.** three does not notify on
|
|
73
|
+
* `position.set(...)`; faking it means diffing every transform every frame,
|
|
74
|
+
* and a signal that is really a per-frame poll is worse than no signal —
|
|
75
|
+
* it hides its own cost. Read the transform when you need it, or drive the
|
|
76
|
+
* change from the code that made it.
|
|
77
|
+
* - **No `Touched`.** Contact/trigger events are Rapier's, dispatched through
|
|
78
|
+
* `ctx.collisions.onCollision`. A second path would be a second truth.
|
|
79
|
+
* - **No 2D index.** This module indexes a three graph only.
|
|
80
|
+
*/
|
|
81
|
+
|
|
82
|
+
import type * as THREE from 'three';
|
|
83
|
+
import { deleteUserData, getUserData, setUserData } from './user-data';
|
|
84
|
+
|
|
85
|
+
/** The value types an attribute may hold — deliberately JSON-simple, so an
|
|
86
|
+
* attribute survives serialization and network replication unchanged. */
|
|
87
|
+
export type AttributeValue = string | number | boolean;
|
|
88
|
+
|
|
89
|
+
/** The `detail` payload of every scene-index event, keyed by event type.
|
|
90
|
+
* Every detail carries `object`, so a scene-wide listener filters without a
|
|
91
|
+
* second lookup. */
|
|
92
|
+
export interface SceneIndexEventDetails {
|
|
93
|
+
/** An object (and, one event each, its descendants) joined the indexed tree. */
|
|
94
|
+
entityadded: { object: THREE.Object3D };
|
|
95
|
+
/** An object (and, one event each, its descendants) left the indexed tree. */
|
|
96
|
+
entityremoved: { object: THREE.Object3D };
|
|
97
|
+
/** A tag was added via {@link SceneIndex.addTag}. */
|
|
98
|
+
tagadded: { object: THREE.Object3D; tag: string };
|
|
99
|
+
/** A tag was removed via {@link SceneIndex.removeTag}. */
|
|
100
|
+
tagremoved: { object: THREE.Object3D; tag: string };
|
|
101
|
+
/** An attribute's value changed (`value` is `undefined` after a delete;
|
|
102
|
+
* `previous` is `undefined` when it was previously unset). */
|
|
103
|
+
attributechanged: {
|
|
104
|
+
object: THREE.Object3D;
|
|
105
|
+
name: string;
|
|
106
|
+
value: AttributeValue | undefined;
|
|
107
|
+
previous: AttributeValue | undefined;
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** Every event type `index.events` / `index.eventsFor(obj)` dispatches. */
|
|
112
|
+
export type SceneIndexEventType = keyof SceneIndexEventDetails;
|
|
113
|
+
|
|
114
|
+
/** The `CustomEvent` a given scene-index event type dispatches. */
|
|
115
|
+
export type SceneIndexEvent<K extends SceneIndexEventType = SceneIndexEventType> = CustomEvent<
|
|
116
|
+
SceneIndexEventDetails[K]
|
|
117
|
+
>;
|
|
118
|
+
|
|
119
|
+
/** Event-type → event, for callers that want a typed `addEventListener` map. */
|
|
120
|
+
export type SceneIndexEventMap = { [K in SceneIndexEventType]: SceneIndexEvent<K> };
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* `addEventListener` with the detail typed — the ONLY convenience over the
|
|
124
|
+
* platform call, and it returns nothing on purpose: unsubscribe stays
|
|
125
|
+
* `AbortSignal` (`{ signal }`), never a returned closure.
|
|
126
|
+
*/
|
|
127
|
+
export function onSceneIndexEvent<K extends SceneIndexEventType>(
|
|
128
|
+
target: EventTarget,
|
|
129
|
+
type: K,
|
|
130
|
+
listener: (detail: SceneIndexEventDetails[K], event: SceneIndexEvent<K>) => void,
|
|
131
|
+
options?: { signal?: AbortSignal; once?: boolean },
|
|
132
|
+
): void {
|
|
133
|
+
target.addEventListener(
|
|
134
|
+
type,
|
|
135
|
+
(event) => {
|
|
136
|
+
const custom = event as SceneIndexEvent<K>;
|
|
137
|
+
listener(custom.detail, custom);
|
|
138
|
+
},
|
|
139
|
+
options,
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/** The live scene index — see this module's header. */
|
|
144
|
+
export interface SceneIndex {
|
|
145
|
+
/** The scene-wide signal target. Listen with `addEventListener(type, fn,
|
|
146
|
+
* { signal })`, or {@link onSceneIndexEvent} for a typed `detail`. */
|
|
147
|
+
readonly events: EventTarget;
|
|
148
|
+
/** The per-object signal target (Roblox's `inst.Changed`): the same events,
|
|
149
|
+
* filtered to `obj`. Created lazily and held weakly. */
|
|
150
|
+
eventsFor(obj: THREE.Object3D): EventTarget;
|
|
151
|
+
|
|
152
|
+
/** Every indexed object carrying `tag`, in index order. Never traverses. */
|
|
153
|
+
byTag(tag: string): readonly THREE.Object3D[];
|
|
154
|
+
/** The indexed object whose `userData['entityId']` is `id` (the first, in
|
|
155
|
+
* index order, if a project has minted the id twice). Never traverses. */
|
|
156
|
+
byEntityId(id: string): THREE.Object3D | undefined;
|
|
157
|
+
/** True when `obj` is currently in the indexed tree. */
|
|
158
|
+
has(obj: THREE.Object3D): boolean;
|
|
159
|
+
|
|
160
|
+
/** Add `tag` to `obj`'s `userData['tags']` and the index; emits `tagadded`.
|
|
161
|
+
* A no-op (and silent) when the tag is already present. Legal on an object
|
|
162
|
+
* that has not joined the scene yet — the tag is stored, and the index
|
|
163
|
+
* picks it up when the object is added. */
|
|
164
|
+
addTag(obj: THREE.Object3D, tag: string): void;
|
|
165
|
+
/** Remove `tag`; emits `tagremoved`. A no-op (and silent) when absent. */
|
|
166
|
+
removeTag(obj: THREE.Object3D, tag: string): void;
|
|
167
|
+
/** True when `obj` carries `tag` (reads `userData['tags']`, so it is
|
|
168
|
+
* truthful even for an object that is not in the scene yet). */
|
|
169
|
+
hasTag(obj: THREE.Object3D, tag: string): boolean;
|
|
170
|
+
|
|
171
|
+
/** Read one attribute (`undefined` when unset). */
|
|
172
|
+
getAttribute(obj: THREE.Object3D, name: string): AttributeValue | undefined;
|
|
173
|
+
/** Write one attribute; emits `attributechanged` only if the value changed. */
|
|
174
|
+
setAttribute(obj: THREE.Object3D, name: string, value: AttributeValue): void;
|
|
175
|
+
/** Delete one attribute; emits `attributechanged` with `value: undefined`. */
|
|
176
|
+
deleteAttribute(obj: THREE.Object3D, name: string): void;
|
|
177
|
+
|
|
178
|
+
/** Re-read `obj`'s subtree from `userData` — the escape hatch for code that
|
|
179
|
+
* wrote `userData['tags']`/`userData['entityId']` by hand. Indexes anything
|
|
180
|
+
* in the subtree that is not indexed yet (emitting `entityadded`) and
|
|
181
|
+
* re-syncs the tag/id entries of everything that is. */
|
|
182
|
+
reindex(obj: THREE.Object3D): void;
|
|
183
|
+
|
|
184
|
+
/** Drop every listener this index installed and forget everything it knows.
|
|
185
|
+
* Called from the owning root's teardown; safe to call twice. */
|
|
186
|
+
dispose(): void;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function readTags(obj: THREE.Object3D): string[] {
|
|
190
|
+
const tags = getUserData(obj, 'tags');
|
|
191
|
+
return Array.isArray(tags) ? tags : [];
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function readAttributes(obj: THREE.Object3D): Record<string, AttributeValue> | undefined {
|
|
195
|
+
const attributes = getUserData(obj, 'attributes');
|
|
196
|
+
return attributes && typeof attributes === 'object' ? attributes : undefined;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Build a live index over `root` and everything currently under it. One index
|
|
201
|
+
* per three root; the root adapter creates it and exposes it as
|
|
202
|
+
* `ctx.sceneIndex`.
|
|
203
|
+
*/
|
|
204
|
+
export function createSceneIndex(root: THREE.Object3D): SceneIndex {
|
|
205
|
+
const events = new EventTarget();
|
|
206
|
+
// Weak on purpose: the per-object target exists only while something else
|
|
207
|
+
// still references the object. The index keeps NO map from target back to
|
|
208
|
+
// object, so a removed-and-forgotten object can be collected with its target.
|
|
209
|
+
const objectTargets = new WeakMap<THREE.Object3D, EventTarget>();
|
|
210
|
+
|
|
211
|
+
const indexed = new Set<THREE.Object3D>();
|
|
212
|
+
const tagged = new Map<string, Set<THREE.Object3D>>();
|
|
213
|
+
// Arrays, not single values: a project that mints the same entityId twice
|
|
214
|
+
// must not blind `byEntityId` for the survivor when one of them is removed.
|
|
215
|
+
const byId = new Map<string, THREE.Object3D[]>();
|
|
216
|
+
// The tag/id snapshot each object was indexed under, so unindexing (and
|
|
217
|
+
// `reindex`) can undo exactly what indexing did even if `userData` has been
|
|
218
|
+
// rewritten by hand in between.
|
|
219
|
+
const indexedTags = new WeakMap<THREE.Object3D, string[]>();
|
|
220
|
+
const indexedIds = new WeakMap<THREE.Object3D, string>();
|
|
221
|
+
let disposed = false;
|
|
222
|
+
|
|
223
|
+
function emit<K extends SceneIndexEventType>(type: K, detail: SceneIndexEventDetails[K]): void {
|
|
224
|
+
// Two distinct CustomEvent instances: one Event object cannot carry two
|
|
225
|
+
// `target`s, and re-dispatching a dispatched event is a spec footgun.
|
|
226
|
+
events.dispatchEvent(new CustomEvent(type, { detail }));
|
|
227
|
+
const perObject = objectTargets.get(detail.object);
|
|
228
|
+
if (perObject) perObject.dispatchEvent(new CustomEvent(type, { detail }));
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function addToTagIndex(obj: THREE.Object3D, tag: string): void {
|
|
232
|
+
let set = tagged.get(tag);
|
|
233
|
+
if (!set) {
|
|
234
|
+
set = new Set();
|
|
235
|
+
tagged.set(tag, set);
|
|
236
|
+
}
|
|
237
|
+
set.add(obj);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function removeFromTagIndex(obj: THREE.Object3D, tag: string): void {
|
|
241
|
+
const set = tagged.get(tag);
|
|
242
|
+
if (!set) return;
|
|
243
|
+
set.delete(obj);
|
|
244
|
+
if (set.size === 0) tagged.delete(tag);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function addToIdIndex(obj: THREE.Object3D, id: string): void {
|
|
248
|
+
const list = byId.get(id);
|
|
249
|
+
if (list) {
|
|
250
|
+
if (!list.includes(obj)) list.push(obj);
|
|
251
|
+
} else {
|
|
252
|
+
byId.set(id, [obj]);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
function removeFromIdIndex(obj: THREE.Object3D, id: string): void {
|
|
257
|
+
const list = byId.get(id);
|
|
258
|
+
if (!list) return;
|
|
259
|
+
const at = list.indexOf(obj);
|
|
260
|
+
if (at !== -1) list.splice(at, 1);
|
|
261
|
+
if (list.length === 0) byId.delete(id);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
const onChildAdded = (event: { child: THREE.Object3D }): void => {
|
|
265
|
+
indexSubtree(event.child);
|
|
266
|
+
};
|
|
267
|
+
const onChildRemoved = (event: { child: THREE.Object3D }): void => {
|
|
268
|
+
unindexSubtree(event.child);
|
|
269
|
+
};
|
|
270
|
+
|
|
271
|
+
/** Point the tag entries at whatever `userData['tags']` says right now,
|
|
272
|
+
* undoing whatever the object was previously indexed under. */
|
|
273
|
+
function syncTags(obj: THREE.Object3D): void {
|
|
274
|
+
for (const tag of indexedTags.get(obj) ?? []) removeFromTagIndex(obj, tag);
|
|
275
|
+
const tags = [...new Set(readTags(obj))];
|
|
276
|
+
indexedTags.set(obj, tags);
|
|
277
|
+
for (const tag of tags) addToTagIndex(obj, tag);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/** Same, for the entity-id entry. */
|
|
281
|
+
function syncId(obj: THREE.Object3D): void {
|
|
282
|
+
const previous = indexedIds.get(obj);
|
|
283
|
+
if (previous !== undefined) removeFromIdIndex(obj, previous);
|
|
284
|
+
indexedIds.delete(obj);
|
|
285
|
+
const id = getUserData(obj, 'entityId');
|
|
286
|
+
if (typeof id === 'string' && id !== '') {
|
|
287
|
+
indexedIds.set(obj, id);
|
|
288
|
+
addToIdIndex(obj, id);
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
function indexOne(obj: THREE.Object3D): void {
|
|
293
|
+
if (indexed.has(obj)) return;
|
|
294
|
+
indexed.add(obj);
|
|
295
|
+
// three's EventDispatcher de-dupes an identical (type, listener) pair, so
|
|
296
|
+
// this is idempotent even if an object somehow arrives twice.
|
|
297
|
+
obj.addEventListener('childadded', onChildAdded);
|
|
298
|
+
obj.addEventListener('childremoved', onChildRemoved);
|
|
299
|
+
syncTags(obj);
|
|
300
|
+
syncId(obj);
|
|
301
|
+
emit('entityadded', { object: obj });
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
function unindexOne(obj: THREE.Object3D): void {
|
|
305
|
+
if (!indexed.has(obj)) return;
|
|
306
|
+
indexed.delete(obj);
|
|
307
|
+
obj.removeEventListener('childadded', onChildAdded);
|
|
308
|
+
obj.removeEventListener('childremoved', onChildRemoved);
|
|
309
|
+
|
|
310
|
+
for (const tag of indexedTags.get(obj) ?? []) removeFromTagIndex(obj, tag);
|
|
311
|
+
indexedTags.delete(obj);
|
|
312
|
+
const id = indexedIds.get(obj);
|
|
313
|
+
if (id !== undefined) {
|
|
314
|
+
removeFromIdIndex(obj, id);
|
|
315
|
+
indexedIds.delete(obj);
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
emit('entityremoved', { object: obj });
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
function indexSubtree(subtreeRoot: THREE.Object3D): void {
|
|
322
|
+
if (disposed) return;
|
|
323
|
+
subtreeRoot.traverse(indexOne);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
function unindexSubtree(subtreeRoot: THREE.Object3D): void {
|
|
327
|
+
if (disposed) return;
|
|
328
|
+
// `traverse` still works on a detached subtree: `remove` only broke the
|
|
329
|
+
// link to the parent, the children below are intact.
|
|
330
|
+
subtreeRoot.traverse(unindexOne);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
indexSubtree(root);
|
|
334
|
+
|
|
335
|
+
return {
|
|
336
|
+
events,
|
|
337
|
+
|
|
338
|
+
eventsFor(obj) {
|
|
339
|
+
let target = objectTargets.get(obj);
|
|
340
|
+
if (!target) {
|
|
341
|
+
target = new EventTarget();
|
|
342
|
+
objectTargets.set(obj, target);
|
|
343
|
+
}
|
|
344
|
+
return target;
|
|
345
|
+
},
|
|
346
|
+
|
|
347
|
+
byTag(tag) {
|
|
348
|
+
const set = tagged.get(tag);
|
|
349
|
+
return set ? [...set] : [];
|
|
350
|
+
},
|
|
351
|
+
|
|
352
|
+
byEntityId(id) {
|
|
353
|
+
return byId.get(id)?.[0];
|
|
354
|
+
},
|
|
355
|
+
|
|
356
|
+
has(obj) {
|
|
357
|
+
return indexed.has(obj);
|
|
358
|
+
},
|
|
359
|
+
|
|
360
|
+
addTag(obj, tag) {
|
|
361
|
+
const tags = readTags(obj);
|
|
362
|
+
if (tags.includes(tag)) return;
|
|
363
|
+
setUserData(obj, 'tags', [...tags, tag]);
|
|
364
|
+
if (indexed.has(obj)) {
|
|
365
|
+
indexedTags.set(obj, [...(indexedTags.get(obj) ?? []), tag]);
|
|
366
|
+
addToTagIndex(obj, tag);
|
|
367
|
+
}
|
|
368
|
+
emit('tagadded', { object: obj, tag });
|
|
369
|
+
},
|
|
370
|
+
|
|
371
|
+
removeTag(obj, tag) {
|
|
372
|
+
const tags = readTags(obj);
|
|
373
|
+
if (!tags.includes(tag)) return;
|
|
374
|
+
setUserData(
|
|
375
|
+
obj,
|
|
376
|
+
'tags',
|
|
377
|
+
tags.filter((t) => t !== tag),
|
|
378
|
+
);
|
|
379
|
+
if (indexed.has(obj)) {
|
|
380
|
+
indexedTags.set(
|
|
381
|
+
obj,
|
|
382
|
+
(indexedTags.get(obj) ?? []).filter((t) => t !== tag),
|
|
383
|
+
);
|
|
384
|
+
removeFromTagIndex(obj, tag);
|
|
385
|
+
}
|
|
386
|
+
emit('tagremoved', { object: obj, tag });
|
|
387
|
+
},
|
|
388
|
+
|
|
389
|
+
hasTag(obj, tag) {
|
|
390
|
+
return readTags(obj).includes(tag);
|
|
391
|
+
},
|
|
392
|
+
|
|
393
|
+
getAttribute(obj, name) {
|
|
394
|
+
return readAttributes(obj)?.[name];
|
|
395
|
+
},
|
|
396
|
+
|
|
397
|
+
setAttribute(obj, name, value) {
|
|
398
|
+
const attributes = readAttributes(obj);
|
|
399
|
+
const previous = attributes?.[name];
|
|
400
|
+
if (previous === value) return;
|
|
401
|
+
if (attributes) attributes[name] = value;
|
|
402
|
+
else setUserData(obj, 'attributes', { [name]: value });
|
|
403
|
+
emit('attributechanged', { object: obj, name, value, previous });
|
|
404
|
+
},
|
|
405
|
+
|
|
406
|
+
deleteAttribute(obj, name) {
|
|
407
|
+
const attributes = readAttributes(obj);
|
|
408
|
+
if (!attributes || !(name in attributes)) return;
|
|
409
|
+
const previous = attributes[name];
|
|
410
|
+
delete attributes[name];
|
|
411
|
+
if (Object.keys(attributes).length === 0) deleteUserData(obj, 'attributes');
|
|
412
|
+
emit('attributechanged', { object: obj, name, value: undefined, previous });
|
|
413
|
+
},
|
|
414
|
+
|
|
415
|
+
reindex(obj) {
|
|
416
|
+
if (disposed) return;
|
|
417
|
+
obj.traverse((node) => {
|
|
418
|
+
if (indexed.has(node)) {
|
|
419
|
+
syncTags(node);
|
|
420
|
+
syncId(node);
|
|
421
|
+
} else {
|
|
422
|
+
indexOne(node);
|
|
423
|
+
}
|
|
424
|
+
});
|
|
425
|
+
},
|
|
426
|
+
|
|
427
|
+
dispose() {
|
|
428
|
+
if (disposed) return;
|
|
429
|
+
disposed = true;
|
|
430
|
+
for (const obj of indexed) {
|
|
431
|
+
obj.removeEventListener('childadded', onChildAdded);
|
|
432
|
+
obj.removeEventListener('childremoved', onChildRemoved);
|
|
433
|
+
}
|
|
434
|
+
indexed.clear();
|
|
435
|
+
tagged.clear();
|
|
436
|
+
byId.clear();
|
|
437
|
+
},
|
|
438
|
+
};
|
|
439
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import type * as THREE from 'three';
|
|
2
|
+
import { getUserData } from './user-data';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Runtime queries over a loaded THREE scene graph (P1.6d).
|
|
6
|
+
*
|
|
7
|
+
* These helpers walk a THREE.Object3D graph and read entity metadata off
|
|
8
|
+
* `userData`, so gameplay code can find objects by tag or name without
|
|
9
|
+
* bookkeeping its own indexes.
|
|
10
|
+
*
|
|
11
|
+
* Canonical metadata locations:
|
|
12
|
+
* - tags: `userData['tags']` (string[])
|
|
13
|
+
* - name: `Object3D.name`
|
|
14
|
+
*
|
|
15
|
+
* These are one-shot walks that notify nobody. For a repeated query, or for a
|
|
16
|
+
* signal when the answer changes, use the LIVE index instead —
|
|
17
|
+
* `ctx.sceneIndex` (`./scene-index.ts`): it reads the same `userData['tags']`
|
|
18
|
+
* storage, answers without traversing, and dispatches `entityadded`/
|
|
19
|
+
* `entityremoved`/`tagadded`/`tagremoved` on an `EventTarget`.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
function getTags(obj: THREE.Object3D): string[] {
|
|
23
|
+
const direct = getUserData(obj, 'tags');
|
|
24
|
+
return Array.isArray(direct) ? direct : [];
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** All objects in the graph carrying the given tag (depth-first, document order). */
|
|
28
|
+
export function queryByTag(root: THREE.Object3D, tag: string): THREE.Object3D[] {
|
|
29
|
+
const matches: THREE.Object3D[] = [];
|
|
30
|
+
root.traverse((obj) => {
|
|
31
|
+
if (getTags(obj).includes(tag)) matches.push(obj);
|
|
32
|
+
});
|
|
33
|
+
return matches;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** All objects in the graph with the given name (depth-first, document order). */
|
|
37
|
+
export function queryByName(root: THREE.Object3D, name: string): THREE.Object3D[] {
|
|
38
|
+
const matches: THREE.Object3D[] = [];
|
|
39
|
+
root.traverse((obj) => {
|
|
40
|
+
if (obj.name === name) matches.push(obj);
|
|
41
|
+
});
|
|
42
|
+
return matches;
|
|
43
|
+
}
|