@vgai/engine 0.5.13 → 0.5.15
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/dist/adapter/adapter-module.d.ts +447 -0
- package/dist/adapter/adapter-module.d.ts.map +1 -0
- package/dist/adapter/adapter-module.js +308 -0
- package/dist/adapter/authoring.d.ts +357 -10
- package/dist/adapter/authoring.d.ts.map +1 -1
- package/dist/adapter/authoring.js +87 -1
- package/dist/adapter/constraint.d.ts +9 -3
- package/dist/adapter/constraint.d.ts.map +1 -1
- package/dist/adapter/finders/finder-result.d.ts +28 -0
- package/dist/adapter/finders/finder-result.d.ts.map +1 -0
- package/dist/adapter/finders/finder-result.js +10 -0
- package/dist/adapter/finders/index.d.ts +38 -0
- package/dist/adapter/finders/index.d.ts.map +1 -0
- package/dist/adapter/finders/index.js +41 -0
- package/dist/adapter/finders/prefabs-from-stories.d.ts +64 -0
- package/dist/adapter/finders/prefabs-from-stories.d.ts.map +1 -0
- package/dist/adapter/finders/prefabs-from-stories.js +92 -0
- package/dist/adapter/finders/scenes-from-entrypoint-selection.d.ts +67 -0
- package/dist/adapter/finders/scenes-from-entrypoint-selection.d.ts.map +1 -0
- package/dist/adapter/finders/scenes-from-entrypoint-selection.js +226 -0
- package/dist/adapter/first-party-systems.d.ts.map +1 -1
- package/dist/adapter/first-party-systems.js +29 -0
- package/dist/adapter/index.d.ts +7 -3
- package/dist/adapter/index.d.ts.map +1 -1
- package/dist/adapter/index.js +5 -2
- package/dist/adapter/ingest/contract-system-adapters.d.ts +24 -1
- package/dist/adapter/ingest/contract-system-adapters.d.ts.map +1 -1
- package/dist/adapter/ingest/contract-system-adapters.js +100 -34
- package/dist/adapter/ingest/game-contract.d.ts +188 -2
- package/dist/adapter/ingest/game-contract.d.ts.map +1 -1
- package/dist/adapter/ingest/game-contract.js +107 -0
- package/dist/adapter/ingest/mount-readiness.d.ts +84 -0
- package/dist/adapter/ingest/mount-readiness.d.ts.map +1 -0
- package/dist/adapter/ingest/mount-readiness.js +81 -0
- package/dist/adapter/ingest/scene-capture.d.ts +91 -2
- package/dist/adapter/ingest/scene-capture.d.ts.map +1 -1
- package/dist/adapter/ingest/scene-capture.js +110 -18
- package/dist/adapter/ingest/structural-ids.d.ts +11 -5
- package/dist/adapter/ingest/structural-ids.d.ts.map +1 -1
- package/dist/adapter/ingest/structural-ids.js +18 -9
- package/dist/adapter/ingest/upstream-pin.d.ts +4 -2
- package/dist/adapter/ingest/upstream-pin.d.ts.map +1 -1
- package/dist/adapter/ingest/upstream-pin.js +4 -2
- package/dist/adapter/ingest/visible-capture-window.d.ts +116 -0
- package/dist/adapter/ingest/visible-capture-window.d.ts.map +1 -0
- package/dist/adapter/ingest/visible-capture-window.js +141 -0
- package/dist/adapter/loop-gate-report.d.ts +3 -4
- package/dist/adapter/loop-gate-report.d.ts.map +1 -1
- package/dist/adapter/loop-gate-report.js +3 -4
- package/dist/adapter/root-adapter.d.ts +22 -2
- package/dist/adapter/root-adapter.d.ts.map +1 -1
- package/dist/adapter/setup-three-root-adapter.d.ts.map +1 -1
- package/dist/adapter/setup-three-root-adapter.js +15 -0
- package/dist/adapter/system-adapter.d.ts +161 -2
- package/dist/adapter/system-adapter.d.ts.map +1 -1
- package/dist/adapter/system-adapter.js +18 -1
- package/dist/animation/camera-ownership.d.ts +4 -2
- package/dist/animation/camera-ownership.d.ts.map +1 -1
- package/dist/animation/camera-ownership.js +58 -1
- package/dist/animation/runtime-inspection.d.ts +70 -0
- package/dist/animation/runtime-inspection.d.ts.map +1 -0
- package/dist/animation/runtime-inspection.js +25 -0
- package/dist/animation/xstate-animation-binding.d.ts +9 -1
- package/dist/animation/xstate-animation-binding.d.ts.map +1 -1
- package/dist/animation/xstate-animation-binding.js +48 -1
- package/dist/asset-formats/material.d.ts +1 -1
- package/dist/asset-formats/mesh.d.ts +1 -1
- package/dist/asset-formats/particles.d.ts +13 -0
- package/dist/asset-formats/particles.d.ts.map +1 -1
- package/dist/asset-formats/particles.js +28 -0
- package/dist/behavior/xstate-inspection.d.ts +24 -0
- package/dist/behavior/xstate-inspection.d.ts.map +1 -0
- package/dist/behavior/xstate-inspection.js +25 -0
- package/dist/core/game-loop.d.ts +7 -5
- package/dist/core/game-loop.d.ts.map +1 -1
- package/dist/core/game-loop.js +39 -18
- package/dist/core/types.d.ts +11 -12
- package/dist/core/types.d.ts.map +1 -1
- package/dist/data/data-asset.js +1 -1
- package/dist/dev/instruments.d.ts.map +1 -1
- package/dist/dev/instruments.js +5 -1
- package/dist/dev/render-debug-adapter.d.ts +19 -2
- package/dist/dev/render-debug-adapter.d.ts.map +1 -1
- package/dist/dev/render-debug-adapter.js +10 -0
- package/dist/ecs/user-data.d.ts +10 -2
- package/dist/ecs/user-data.d.ts.map +1 -1
- package/dist/ecs/user-data.js +6 -2
- package/dist/input/binding-identity.d.ts +10 -0
- package/dist/input/binding-identity.d.ts.map +1 -0
- package/dist/input/binding-identity.js +36 -0
- package/dist/input/input-manager.d.ts +18 -185
- package/dist/input/input-manager.d.ts.map +1 -1
- package/dist/input/input-manager.js +78 -68
- package/dist/manifest/load.d.ts +14 -0
- package/dist/manifest/load.d.ts.map +1 -1
- package/dist/manifest/load.js +6 -1
- package/dist/manifest/schema.d.ts +18 -0
- package/dist/manifest/schema.d.ts.map +1 -1
- package/dist/manifest/schema.js +65 -8
- package/dist/pixi/authoring.d.ts +27 -0
- package/dist/pixi/authoring.d.ts.map +1 -1
- package/dist/pixi/authoring.js +10 -3
- package/dist/pixi/index.d.ts +1 -1
- package/dist/pixi/index.d.ts.map +1 -1
- package/dist/pixi/index.js +1 -1
- package/dist/pixi/ingest.d.ts +8 -0
- package/dist/pixi/ingest.d.ts.map +1 -1
- package/dist/pixi/ingest.js +10 -1
- package/dist/pixi/render-pass-bracket.d.ts +56 -0
- package/dist/pixi/render-pass-bracket.d.ts.map +1 -0
- package/dist/pixi/render-pass-bracket.js +98 -0
- package/dist/pixi/scene-capture.d.ts +32 -1
- package/dist/pixi/scene-capture.d.ts.map +1 -1
- package/dist/pixi/scene-capture.js +21 -9
- package/dist/pixi/system-adapters.d.ts +29 -0
- package/dist/pixi/system-adapters.d.ts.map +1 -1
- package/dist/pixi/system-adapters.js +27 -0
- package/dist/render/particles-factory.d.ts +16 -2
- package/dist/render/particles-factory.d.ts.map +1 -1
- package/dist/render/particles-factory.js +88 -5
- package/dist/render/soft-particle-depth.d.ts +149 -0
- package/dist/render/soft-particle-depth.d.ts.map +1 -0
- package/dist/render/soft-particle-depth.js +305 -0
- package/dist/render/voxel-surface.d.ts +117 -0
- package/dist/render/voxel-surface.d.ts.map +1 -0
- package/dist/render/voxel-surface.js +263 -0
- package/dist/runtime/create-runtime.js +7 -0
- package/dist/runtime/debug-bridge.d.ts +15 -15
- package/dist/runtime/debug-bridge.d.ts.map +1 -1
- package/dist/runtime/debug-bridge.js +27 -26
- package/dist/runtime/game.d.ts.map +1 -1
- package/dist/runtime/game.js +1 -2
- package/dist/setup/setup-particles.d.ts.map +1 -1
- package/dist/setup/setup-particles.js +9 -0
- package/dist/world3d-react/r3f-adapter.d.ts.map +1 -1
- package/dist/world3d-react/r3f-adapter.js +42 -3
- package/dist/world3d-react/rapier-physics-bridge.d.ts.map +1 -1
- package/dist/world3d-react/rapier-physics-bridge.js +201 -11
- package/dist-config/config.js.map +1 -1
- package/package.json +4 -2
- package/schemas/engine-capabilities.json +12 -12
- package/schemas/vgai-project.schema.json +42 -3
- package/src/adapter/adapter-module.ts +607 -0
- package/src/adapter/authoring.ts +440 -10
- package/src/adapter/constraint.ts +10 -3
- package/src/adapter/finders/finder-result.ts +29 -0
- package/src/adapter/finders/index.ts +62 -0
- package/src/adapter/finders/prefabs-from-stories.ts +142 -0
- package/src/adapter/finders/scenes-from-entrypoint-selection.ts +304 -0
- package/src/adapter/first-party-systems.ts +30 -0
- package/src/adapter/index.ts +32 -1
- package/src/adapter/ingest/contract-system-adapters.ts +116 -32
- package/src/adapter/ingest/game-contract.ts +265 -2
- package/src/adapter/ingest/mount-readiness.ts +140 -0
- package/src/adapter/ingest/scene-capture.ts +199 -22
- package/src/adapter/ingest/structural-ids.ts +21 -9
- package/src/adapter/ingest/upstream-pin.ts +4 -2
- package/src/adapter/ingest/visible-capture-window.ts +213 -0
- package/src/adapter/loop-gate-report.ts +3 -4
- package/src/adapter/root-adapter.ts +22 -2
- package/src/adapter/setup-three-root-adapter.ts +16 -0
- package/src/adapter/system-adapter.ts +188 -2
- package/src/animation/camera-ownership.ts +68 -3
- package/src/animation/runtime-inspection.ts +104 -0
- package/src/animation/xstate-animation-binding.ts +53 -1
- package/src/asset-formats/particles.ts +31 -0
- package/src/behavior/xstate-inspection.ts +51 -0
- package/src/core/game-loop.ts +39 -17
- package/src/core/types.ts +11 -12
- package/src/data/data-asset.ts +1 -1
- package/src/dev/instruments.ts +5 -1
- package/src/dev/render-debug-adapter.ts +22 -2
- package/src/ecs/user-data.ts +10 -2
- package/src/input/binding-identity.ts +37 -0
- package/src/input/input-manager.ts +93 -68
- package/src/manifest/load.ts +23 -2
- package/src/manifest/schema.ts +77 -8
- package/src/pixi/authoring.ts +37 -5
- package/src/pixi/index.ts +2 -0
- package/src/pixi/ingest.ts +15 -2
- package/src/pixi/render-pass-bracket.ts +117 -0
- package/src/pixi/scene-capture.ts +63 -13
- package/src/pixi/system-adapters.ts +41 -0
- package/src/render/particles-factory.ts +89 -6
- package/src/render/soft-particle-depth.ts +336 -0
- package/src/render/voxel-surface.ts +358 -0
- package/src/runtime/create-runtime.ts +7 -0
- package/src/runtime/debug-bridge.ts +32 -31
- package/src/runtime/game.ts +1 -2
- package/src/setup/setup-particles.ts +9 -0
- package/src/world3d-react/r3f-adapter.tsx +48 -2
- package/src/world3d-react/rapier-physics-bridge.tsx +249 -11
|
@@ -51,6 +51,7 @@ import { createCollisionSystem } from '../physics/collision-system';
|
|
|
51
51
|
import { createPhysicsRegistry } from '../physics/physics-registry';
|
|
52
52
|
import { createTransformWriter } from '../physics/transform-writer';
|
|
53
53
|
import { updateSceneLODs } from '../render/lod';
|
|
54
|
+
import { createSoftParticleDepthPass } from '../render/soft-particle-depth';
|
|
54
55
|
import {
|
|
55
56
|
disposeSparkRendererWhenIdle,
|
|
56
57
|
SPARK_DISCOVERY_INTERVAL_MS,
|
|
@@ -209,6 +210,10 @@ export class SetupThreeRootAdapter implements RootAdapter {
|
|
|
209
210
|
|
|
210
211
|
// --- Particles (three.quarks BatchedRenderer) ---
|
|
211
212
|
const particles = setupParticles(scene);
|
|
213
|
+
// Scene depth for any soft-particle system registered against this world.
|
|
214
|
+
// Inert (not one traversal, not one draw) until `registerParticleSystem`
|
|
215
|
+
// arms it — see `../render/soft-particle-depth.ts`.
|
|
216
|
+
const softParticleDepth = createSoftParticleDepthPass();
|
|
212
217
|
|
|
213
218
|
// --- Debug draw / assets ---
|
|
214
219
|
const debugDraw = createDebugDraw(scene);
|
|
@@ -311,6 +316,13 @@ export class SetupThreeRootAdapter implements RootAdapter {
|
|
|
311
316
|
// the assembled capture to a pending captureFrame() and restores the
|
|
312
317
|
// patched context — in the SAME finally as gpuTimer.end(). Spark is
|
|
313
318
|
// added before this so its draws fall inside the captured frame.
|
|
319
|
+
// Soft-particle scene depth goes in FRONT of the frame's own draw:
|
|
320
|
+
// the fade samples the geometry behind the particle, which cannot be
|
|
321
|
+
// the depth attachment the particle is drawn into. Deliberately
|
|
322
|
+
// OUTSIDE the capture and profiler brackets below — it is a prepass,
|
|
323
|
+
// not part of the frame's presentation, and a captured frame should
|
|
324
|
+
// show the draws the user sees.
|
|
325
|
+
softParticleDepth.render(renderer, scene, camera);
|
|
314
326
|
renderDebugWiring?.beforeRender();
|
|
315
327
|
// Issue #1504: CPU render submission, bracketed as its OWN profiler
|
|
316
328
|
// phase nested inside the frame's enclosing `render` phase. This is
|
|
@@ -838,6 +850,10 @@ export class SetupThreeRootAdapter implements RootAdapter {
|
|
|
838
850
|
renderVitals = null;
|
|
839
851
|
});
|
|
840
852
|
|
|
853
|
+
// This mount created the depth pass, so this mount ends it — the render
|
|
854
|
+
// target it holds is drawing-buffer sized and nothing else can reach it.
|
|
855
|
+
safeStep('softParticleDepth.dispose', () => softParticleDepth.dispose());
|
|
856
|
+
|
|
841
857
|
// Release analyser taps BEFORE the audio teardown below. A consumer
|
|
842
858
|
// (the editor's meter poll) should dispose its own handle, but Stop must
|
|
843
859
|
// never leak taps regardless.
|
|
@@ -14,8 +14,60 @@
|
|
|
14
14
|
import type * as THREE from 'three';
|
|
15
15
|
import type { RenderMemorySnapshot } from '../dev/render-memory';
|
|
16
16
|
import type { FrameCapture } from '../dev/webgl-frame-capture';
|
|
17
|
+
import type { PhysicsAdapter2D } from '../pixi/system-adapters';
|
|
17
18
|
import type { Transform, TransformOwner } from './transform';
|
|
18
19
|
|
|
20
|
+
export type PhysicsColliderShape =
|
|
21
|
+
| { readonly type: 'cuboid'; readonly halfExtents: readonly [number, number, number] }
|
|
22
|
+
| { readonly type: 'ball'; readonly radius: number }
|
|
23
|
+
| { readonly type: 'capsule'; readonly halfHeight: number; readonly radius: number };
|
|
24
|
+
|
|
25
|
+
/** Plain-data projection of one native 3D physics collider. The owning
|
|
26
|
+
* physics implementation keeps the actual collider; editor authoring only
|
|
27
|
+
* receives the geometry needed to inspect and draw it. */
|
|
28
|
+
export interface PhysicsColliderSnapshot {
|
|
29
|
+
readonly id: string;
|
|
30
|
+
readonly shape: PhysicsColliderShape;
|
|
31
|
+
readonly position: readonly [number, number, number];
|
|
32
|
+
readonly rotation: readonly [number, number, number, number];
|
|
33
|
+
/** Absolute world scale used when the native shape was created. Source
|
|
34
|
+
* dimensions divide by this to recover @react-three/rapier's local args. */
|
|
35
|
+
readonly scale: readonly [number, number, number];
|
|
36
|
+
readonly sensor: boolean;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export type PhysicsJointType =
|
|
40
|
+
| 'fixed'
|
|
41
|
+
| 'spherical'
|
|
42
|
+
| 'revolute'
|
|
43
|
+
| 'prismatic'
|
|
44
|
+
| 'rope'
|
|
45
|
+
| 'spring'
|
|
46
|
+
| 'generic';
|
|
47
|
+
|
|
48
|
+
/** Plain-data projection of one native 3D impulse joint. Anchors are exposed
|
|
49
|
+
* in both native body-local space (for persistence) and world space (for
|
|
50
|
+
* viewport instruments); the actual joint remains owned by the physics lib. */
|
|
51
|
+
export interface PhysicsJointSnapshot {
|
|
52
|
+
readonly id: string;
|
|
53
|
+
readonly type: PhysicsJointType;
|
|
54
|
+
readonly body1: string;
|
|
55
|
+
readonly body2: string;
|
|
56
|
+
readonly anchor1: readonly [number, number, number];
|
|
57
|
+
readonly anchor2: readonly [number, number, number];
|
|
58
|
+
readonly worldAnchor1: readonly [number, number, number];
|
|
59
|
+
readonly worldAnchor2: readonly [number, number, number];
|
|
60
|
+
readonly body1Position: readonly [number, number, number];
|
|
61
|
+
readonly body2Position: readonly [number, number, number];
|
|
62
|
+
readonly body1Rotation: readonly [number, number, number, number];
|
|
63
|
+
readonly body2Rotation: readonly [number, number, number, number];
|
|
64
|
+
/** The free axis in body-1 local/world space for revolute/prismatic joints. */
|
|
65
|
+
readonly axis?: readonly [number, number, number];
|
|
66
|
+
readonly worldAxis?: readonly [number, number, number];
|
|
67
|
+
readonly limits?: { readonly min: number; readonly max: number };
|
|
68
|
+
readonly contactsEnabled: boolean;
|
|
69
|
+
}
|
|
70
|
+
|
|
19
71
|
/**
|
|
20
72
|
* Physics coordination so the editor can stably edit an object a simulation
|
|
21
73
|
* would otherwise overwrite every frame: `freeze → apply → unfreeze`.
|
|
@@ -24,11 +76,36 @@ import type { Transform, TransformOwner } from './transform';
|
|
|
24
76
|
* Object3D keying made this seam structurally unreachable for any non-three
|
|
25
77
|
* world: a Pixi or React world could never expose a physics inspector, because
|
|
26
78
|
* the seam spoke a vocabulary it has no values in — which is why the Pixi
|
|
27
|
-
* surface grew a parallel
|
|
79
|
+
* surface grew a parallel {@link PhysicsAdapter2D} (`pixi/system-adapters.ts`)
|
|
28
80
|
* instead of implementing this. Substrate-specific resolution (id → the native
|
|
29
81
|
* object) is the IMPLEMENTER's job, done once at its own boundary.
|
|
82
|
+
*
|
|
83
|
+
* ## `keyedBy`, and why the physics slot's union is TAGGED
|
|
84
|
+
*
|
|
85
|
+
* `SystemAdapters['physics']` holds EITHER shape ({@link PhysicsCarrier}),
|
|
86
|
+
* because a carrier addresses the thing being edited in its surface's own
|
|
87
|
+
* vocabulary and the canvas lane's is the display object itself. But the two
|
|
88
|
+
* shapes carry the SAME four member names and are otherwise indistinguishable
|
|
89
|
+
* at runtime, while the registry that holds them
|
|
90
|
+
* (`editor/src/authoring/active-systems.ts`) is GAME-scoped and surface-blind —
|
|
91
|
+
* one object every editor panel reads. An untagged union would therefore be one
|
|
92
|
+
* nothing could narrow, and every three-lane consumer
|
|
93
|
+
* (`getActivePhysics()?.freeze(nodeId)`, `play-mode.ts`'s `physics.commit(id,
|
|
94
|
+
* transform)`) would have to reach it through a cast asserting a fact nobody
|
|
95
|
+
* checked.
|
|
96
|
+
*
|
|
97
|
+
* So `keyedBy` is a real discriminant, and it is REQUIRED on
|
|
98
|
+
* {@link PhysicsAdapter2D} while OPTIONAL here on purpose: node-id keying is
|
|
99
|
+
* this seam's original and — until the canvas lane — only vocabulary, so every
|
|
100
|
+
* existing implementer stays valid untouched, and the shape that needs telling
|
|
101
|
+
* apart is the one obliged to say so. {@link nodeKeyedPhysics} /
|
|
102
|
+
* {@link displayKeyedPhysics} are the two narrowings; nothing else should test
|
|
103
|
+
* the tag by hand.
|
|
30
104
|
*/
|
|
31
105
|
export interface PhysicsAdapter {
|
|
106
|
+
/** Optional tag; see the interface comment. `'node-id'` is the only value,
|
|
107
|
+
* and omitting it means the same thing. */
|
|
108
|
+
readonly keyedBy?: 'node-id';
|
|
32
109
|
/** Returns `'physics'` when a body drives this node, else another owner. */
|
|
33
110
|
ownerOf(nodeId: string): TransformOwner;
|
|
34
111
|
/** Pause the body driving `nodeId` (kinematic / sleep / detach) for editing. */
|
|
@@ -53,6 +130,21 @@ export interface PhysicsAdapter {
|
|
|
53
130
|
* geometry).
|
|
54
131
|
*/
|
|
55
132
|
contactPoints?(): Float32Array;
|
|
133
|
+
/** Optional native collider inspection for one authored node. Implementers
|
|
134
|
+
* return only shapes they can describe exactly; absence means the editor
|
|
135
|
+
* offers no collider component UI. */
|
|
136
|
+
colliders?(nodeId: string): readonly PhysicsColliderSnapshot[];
|
|
137
|
+
/** Optional gesture preview against the implementation's real collider.
|
|
138
|
+
* Source persistence remains the AuthoringAdapter's responsibility. */
|
|
139
|
+
previewCollider?(colliderId: string, shape: PhysicsColliderShape): void;
|
|
140
|
+
/** Optional native impulse-joint inspection for joints attached to one node. */
|
|
141
|
+
joints?(nodeId: string): readonly PhysicsJointSnapshot[];
|
|
142
|
+
/** Optional gesture preview against the implementation's real local anchor. */
|
|
143
|
+
previewJointAnchor?(
|
|
144
|
+
jointId: string,
|
|
145
|
+
endpoint: 0 | 1,
|
|
146
|
+
anchor: readonly [number, number, number],
|
|
147
|
+
): void;
|
|
56
148
|
}
|
|
57
149
|
|
|
58
150
|
export interface NetPeer {
|
|
@@ -377,11 +469,73 @@ export interface AudioAdapter {
|
|
|
377
469
|
* environment cannot meter (headless world with no real AudioContext) —
|
|
378
470
|
* the UI says so rather than showing frozen zeros. */
|
|
379
471
|
acquireMeters?(): AudioMeterHandle | null;
|
|
472
|
+
/** Optional: tap the real mixed output for a gameplay recording. The
|
|
473
|
+
* returned stream is the implementation's native browser audio stream; the
|
|
474
|
+
* caller must dispose the tap when recording ends. Absence means video can
|
|
475
|
+
* still be recorded, honestly without an audio track. */
|
|
476
|
+
acquireRecordingStream?(): AudioRecordingHandle | null;
|
|
380
477
|
/** Optional: the audio event ring (bounded, drops oldest). `sinceSeq`
|
|
381
478
|
* filters to `seq > sinceSeq` — the consumer's fence. */
|
|
382
479
|
audioEvents?(sinceSeq?: number): AudioDebugEvent[];
|
|
383
480
|
}
|
|
384
481
|
|
|
482
|
+
/** One temporary tap of an audio adapter's mixed output. */
|
|
483
|
+
export interface AudioRecordingHandle {
|
|
484
|
+
readonly stream: MediaStream;
|
|
485
|
+
dispose(): void;
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
/** One native camera known to a runtime camera controller.
|
|
489
|
+
*
|
|
490
|
+
* `id` is the controller's own stable key (a shot name, virtual-camera id,
|
|
491
|
+
* etc.). `nativeId` is OPTIONAL identity from the underlying scene object —
|
|
492
|
+
* Three.js implementations normally use `Object3D.uuid`; another adapter may
|
|
493
|
+
* use its renderer's equivalent. The editor uses it only to correlate an
|
|
494
|
+
* already-selected native camera with this read-only runtime projection. */
|
|
495
|
+
export interface CameraRuntimeCamera {
|
|
496
|
+
id: string;
|
|
497
|
+
label: string;
|
|
498
|
+
nativeId?: string;
|
|
499
|
+
/** Optional because ownership/cut controllers do not necessarily arbitrate
|
|
500
|
+
* by priority. A camera brain that does may expose the real value it uses. */
|
|
501
|
+
priority?: number;
|
|
502
|
+
enabled?: boolean;
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
/** A transition the controller is evaluating now. Plain data only: no
|
|
506
|
+
* renderer camera, tween, graph node, or editor-authored state crosses the
|
|
507
|
+
* seam. */
|
|
508
|
+
export interface CameraRuntimeTransition {
|
|
509
|
+
fromCameraId: string | null;
|
|
510
|
+
toCameraId: string;
|
|
511
|
+
durationSeconds: number;
|
|
512
|
+
elapsedSeconds: number;
|
|
513
|
+
progress: number;
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
/** Read-only runtime state for a selected native camera's Inspector section.
|
|
517
|
+
* Runtime follow/aim/collision stays project behavior; this snapshot merely
|
|
518
|
+
* reports what that behavior already decided. */
|
|
519
|
+
export interface CameraRuntimeSnapshot {
|
|
520
|
+
cameras: readonly CameraRuntimeCamera[];
|
|
521
|
+
activeCameraId: string | null;
|
|
522
|
+
/** The behavior/cinematic currently allowed to drive the rendered camera. */
|
|
523
|
+
activeControllerId: string | null;
|
|
524
|
+
ownershipDepth: number;
|
|
525
|
+
transition: CameraRuntimeTransition | null;
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
/** Camera-controller INTROSPECTION — not a camera graph or controller API.
|
|
529
|
+
*
|
|
530
|
+
* Games continue to author ordinary native cameras and drive them with their
|
|
531
|
+
* own TS/TSX. A controller registers this adapter only when it can truthfully
|
|
532
|
+
* expose its active camera, priority and blend state. Absence hides the
|
|
533
|
+
* runtime UI; the editor never infers a brain from scene structure. */
|
|
534
|
+
export interface CameraAdapter {
|
|
535
|
+
snapshot(): CameraRuntimeSnapshot;
|
|
536
|
+
subscribe(listener: () => void): Unsubscribe;
|
|
537
|
+
}
|
|
538
|
+
|
|
385
539
|
/** One tick-stamped debug event (`ctx.debug.emit`, spec §3.3): `tick`/`simT`
|
|
386
540
|
* are the engine's own counters AT EMISSION, so events and state reads
|
|
387
541
|
* correlate frame-exactly across every door (in-page bridge, relay, panels).
|
|
@@ -470,15 +624,47 @@ export interface RenderDebugAdapter {
|
|
|
470
624
|
memorySnapshot?(): RenderMemorySnapshot;
|
|
471
625
|
}
|
|
472
626
|
|
|
627
|
+
/**
|
|
628
|
+
* A physics carrier in EITHER surface's vocabulary — node-id keyed for a three
|
|
629
|
+
* world, display-object keyed for a canvas one. Tagged by `keyedBy`; see
|
|
630
|
+
* {@link PhysicsAdapter}'s comment for why, and use the two narrowings below
|
|
631
|
+
* rather than reading the tag directly.
|
|
632
|
+
*/
|
|
633
|
+
export type PhysicsCarrier = PhysicsAdapter | PhysicsAdapter2D;
|
|
634
|
+
|
|
635
|
+
/** The carrier a NODE-ID-keyed consumer may call, or `null` — including when
|
|
636
|
+
* the slot holds a display-keyed carrier, which such a consumer must not
|
|
637
|
+
* call and cannot correctly address. */
|
|
638
|
+
export function nodeKeyedPhysics(
|
|
639
|
+
carrier: PhysicsCarrier | null | undefined,
|
|
640
|
+
): PhysicsAdapter | null {
|
|
641
|
+
if (!carrier) return null;
|
|
642
|
+
return isDisplayKeyedPhysics(carrier) ? null : carrier;
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
/** The carrier a DISPLAY-keyed consumer may call, or `null`. Mirror of
|
|
646
|
+
* {@link nodeKeyedPhysics}. */
|
|
647
|
+
export function displayKeyedPhysics(
|
|
648
|
+
carrier: PhysicsCarrier | null | undefined,
|
|
649
|
+
): PhysicsAdapter2D | null {
|
|
650
|
+
if (!carrier) return null;
|
|
651
|
+
return isDisplayKeyedPhysics(carrier) ? carrier : null;
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
function isDisplayKeyedPhysics(carrier: PhysicsCarrier): carrier is PhysicsAdapter2D {
|
|
655
|
+
return carrier.keyedBy === 'display';
|
|
656
|
+
}
|
|
657
|
+
|
|
473
658
|
/**
|
|
474
659
|
* The set of optional subsystem providers a mounted game may expose. Absence of
|
|
475
660
|
* a provider means "capability not supported" — the editor degrades gracefully.
|
|
476
661
|
*/
|
|
477
662
|
export interface SystemAdapters {
|
|
478
|
-
physics?:
|
|
663
|
+
physics?: PhysicsCarrier;
|
|
479
664
|
networking?: NetworkingAdapter;
|
|
480
665
|
navigation?: NavigationAdapter;
|
|
481
666
|
audio?: AudioAdapter;
|
|
667
|
+
camera?: CameraAdapter;
|
|
482
668
|
debug?: DebugAdapter;
|
|
483
669
|
renderDebug?: RenderDebugAdapter;
|
|
484
670
|
}
|
|
@@ -1,4 +1,9 @@
|
|
|
1
1
|
import * as THREE from 'three';
|
|
2
|
+
import type {
|
|
3
|
+
CameraAdapter,
|
|
4
|
+
CameraRuntimeSnapshot,
|
|
5
|
+
CameraRuntimeTransition,
|
|
6
|
+
} from '../adapter/system-adapter';
|
|
2
7
|
import type { SystemPhaseName } from '../core/types';
|
|
3
8
|
import type { AnimationClock, ClockEvaluatorHandle } from './animation-clock';
|
|
4
9
|
import type { CameraOwnershipChangeEvent } from './theatre-director';
|
|
@@ -107,7 +112,8 @@ import type { CameraOwnershipChangeEvent } from './theatre-director';
|
|
|
107
112
|
* ## One unified transition model — cut is just a zero-duration blend
|
|
108
113
|
*
|
|
109
114
|
* Internally, `cut`/`blend`/`applyDirectorEvent` all record exactly one
|
|
110
|
-
* `FrameState` shape: `{ targetKey, fromPose, startTime,
|
|
115
|
+
* `FrameState` shape: `{ targetKey, fromTargetKey, fromPose, startTime,
|
|
116
|
+
* durationSeconds }`
|
|
111
117
|
* (`durationSeconds` is `0` for a cut). On EVERY clock crossing, the applied
|
|
112
118
|
* pose is a PURE function of `clock.time` and this fixed record — nothing is
|
|
113
119
|
* mutated as time passes:
|
|
@@ -236,6 +242,7 @@ export function lerpPose(from: CameraPose, to: CameraPose, t: number): CameraPos
|
|
|
236
242
|
*/
|
|
237
243
|
interface FrameState {
|
|
238
244
|
readonly targetKey: string;
|
|
245
|
+
readonly fromTargetKey: string | null;
|
|
239
246
|
readonly fromPose: CameraPose;
|
|
240
247
|
readonly startTime: number;
|
|
241
248
|
readonly durationSeconds: number;
|
|
@@ -280,7 +287,7 @@ export interface CameraOwnershipSystemOptions {
|
|
|
280
287
|
phase?: SystemPhaseName;
|
|
281
288
|
}
|
|
282
289
|
|
|
283
|
-
export interface CameraOwnershipSystem {
|
|
290
|
+
export interface CameraOwnershipSystem extends CameraAdapter {
|
|
284
291
|
/** Push a new ownership frame for `ownerId`, capturing `liveCamera`'s current pose as its restore point. Throws if `ownerId` is already on the stack. */
|
|
285
292
|
take(ownerId: string, options?: CameraOwnershipTakeOptions): void;
|
|
286
293
|
/** Hard-cut the live camera to `cameraKey`'s current pose. `ownerId` must be the current (top-of-stack) owner. */
|
|
@@ -312,7 +319,43 @@ export function createCameraOwnershipSystem(
|
|
|
312
319
|
const phase = options.phase ?? 'render';
|
|
313
320
|
|
|
314
321
|
const stack: OwnershipFrame[] = [];
|
|
322
|
+
const listeners = new Set<() => void>();
|
|
315
323
|
let disposed = false;
|
|
324
|
+
let transitionWasVisible = false;
|
|
325
|
+
|
|
326
|
+
function notify(): void {
|
|
327
|
+
for (const listener of listeners) listener();
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
function transitionSnapshot(frame: OwnershipFrame | undefined): CameraRuntimeTransition | null {
|
|
331
|
+
const state = frame?.state;
|
|
332
|
+
if (!state || state.durationSeconds <= 0) return null;
|
|
333
|
+
const elapsedSeconds = Math.max(0, clock.time - state.startTime);
|
|
334
|
+
const progress = Math.min(elapsedSeconds / state.durationSeconds, 1);
|
|
335
|
+
if (progress >= 1) return null;
|
|
336
|
+
return {
|
|
337
|
+
fromCameraId: state.fromTargetKey,
|
|
338
|
+
toCameraId: state.targetKey,
|
|
339
|
+
durationSeconds: state.durationSeconds,
|
|
340
|
+
elapsedSeconds,
|
|
341
|
+
progress,
|
|
342
|
+
};
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
function snapshot(): CameraRuntimeSnapshot {
|
|
346
|
+
const top = stack[stack.length - 1];
|
|
347
|
+
return {
|
|
348
|
+
cameras: Object.entries(cameras).map(([id, camera]) => ({
|
|
349
|
+
id,
|
|
350
|
+
label: camera.name || id,
|
|
351
|
+
nativeId: camera.uuid,
|
|
352
|
+
})),
|
|
353
|
+
activeCameraId: top?.state?.targetKey ?? null,
|
|
354
|
+
activeControllerId: top?.ownerId ?? null,
|
|
355
|
+
ownershipDepth: stack.length,
|
|
356
|
+
transition: transitionSnapshot(top),
|
|
357
|
+
};
|
|
358
|
+
}
|
|
316
359
|
|
|
317
360
|
function requireNotDisposed(action: string): void {
|
|
318
361
|
if (disposed) {
|
|
@@ -372,6 +415,7 @@ export function createCameraOwnershipSystem(
|
|
|
372
415
|
|
|
373
416
|
frame.state = {
|
|
374
417
|
targetKey: cameraKey,
|
|
418
|
+
fromTargetKey: frame.state?.targetKey ?? null,
|
|
375
419
|
fromPose: capturePose(liveCamera),
|
|
376
420
|
startTime: clock.time,
|
|
377
421
|
durationSeconds,
|
|
@@ -384,6 +428,8 @@ export function createCameraOwnershipSystem(
|
|
|
384
428
|
// hook's own 'preRender'-phase call site, once more from this module's
|
|
385
429
|
// later 'render'-phase evaluator).
|
|
386
430
|
evaluateFrame(frame);
|
|
431
|
+
transitionWasVisible = transitionSnapshot(frame) !== null;
|
|
432
|
+
notify();
|
|
387
433
|
}
|
|
388
434
|
|
|
389
435
|
function releaseFrame(frame: OwnershipFrame, completed: boolean): void {
|
|
@@ -396,7 +442,12 @@ export function createCameraOwnershipSystem(
|
|
|
396
442
|
|
|
397
443
|
const handle: ClockEvaluatorHandle = clock.subscribe(phase, () => {
|
|
398
444
|
const top = stack[stack.length - 1];
|
|
399
|
-
if (top)
|
|
445
|
+
if (top) {
|
|
446
|
+
evaluateFrame(top);
|
|
447
|
+
const transitionIsVisible = transitionSnapshot(top) !== null;
|
|
448
|
+
if (transitionIsVisible || transitionWasVisible) notify();
|
|
449
|
+
transitionWasVisible = transitionIsVisible;
|
|
450
|
+
}
|
|
400
451
|
});
|
|
401
452
|
|
|
402
453
|
return {
|
|
@@ -414,6 +465,7 @@ export function createCameraOwnershipSystem(
|
|
|
414
465
|
restorePose: capturePose(liveCamera),
|
|
415
466
|
state: undefined,
|
|
416
467
|
});
|
|
468
|
+
notify();
|
|
417
469
|
},
|
|
418
470
|
|
|
419
471
|
cut(ownerId, cameraKey) {
|
|
@@ -442,6 +494,8 @@ export function createCameraOwnershipSystem(
|
|
|
442
494
|
const top = requireTopOwner(ownerId, 'release');
|
|
443
495
|
stack.pop();
|
|
444
496
|
releaseFrame(top, releaseOptions.completed ?? false);
|
|
497
|
+
transitionWasVisible = false;
|
|
498
|
+
notify();
|
|
445
499
|
},
|
|
446
500
|
|
|
447
501
|
isOwner(ownerId) {
|
|
@@ -453,6 +507,14 @@ export function createCameraOwnershipSystem(
|
|
|
453
507
|
return stack.length;
|
|
454
508
|
},
|
|
455
509
|
|
|
510
|
+
snapshot,
|
|
511
|
+
|
|
512
|
+
subscribe(listener) {
|
|
513
|
+
if (disposed) return () => undefined;
|
|
514
|
+
listeners.add(listener);
|
|
515
|
+
return () => listeners.delete(listener);
|
|
516
|
+
},
|
|
517
|
+
|
|
456
518
|
dispose() {
|
|
457
519
|
if (disposed) return;
|
|
458
520
|
disposed = true;
|
|
@@ -460,7 +522,10 @@ export function createCameraOwnershipSystem(
|
|
|
460
522
|
const top = stack.pop();
|
|
461
523
|
if (top) releaseFrame(top, false);
|
|
462
524
|
}
|
|
525
|
+
transitionWasVisible = false;
|
|
463
526
|
handle.dispose();
|
|
527
|
+
notify();
|
|
528
|
+
listeners.clear();
|
|
464
529
|
},
|
|
465
530
|
};
|
|
466
531
|
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import type * as THREE from 'three';
|
|
2
|
+
import { deleteUserData, getUserData, setUserData } from '../ecs/user-data';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Live native animation playback for editor tools. Runtime state is read-only;
|
|
6
|
+
* an edit-time subject may additionally publish transient preview transport.
|
|
7
|
+
* This is instrumentation, not an animation asset or graph format.
|
|
8
|
+
*/
|
|
9
|
+
export interface AnimationRuntimeInspection {
|
|
10
|
+
readonly mixer: THREE.AnimationMixer;
|
|
11
|
+
readonly clips: ReadonlyMap<string, THREE.AnimationClip>;
|
|
12
|
+
readonly getSnapshot: () => AnimationRuntimeSnapshot;
|
|
13
|
+
readonly previewTransport?: AnimationPreviewTransport;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Optional controls for an isolated Asset Lab preview clock. A live gameplay
|
|
18
|
+
* runtime deliberately omits this: the editor may observe live playback, but
|
|
19
|
+
* it must not pause or seek the game through an animation inspector.
|
|
20
|
+
*/
|
|
21
|
+
export interface AnimationPreviewTransport {
|
|
22
|
+
readonly getSnapshot: () => AnimationPreviewTransportSnapshot;
|
|
23
|
+
readonly setPlaying: (playing: boolean) => void;
|
|
24
|
+
readonly setLoop: (loop: boolean) => void;
|
|
25
|
+
readonly setSpeed: (speed: number) => void;
|
|
26
|
+
readonly seek: (time: number) => void;
|
|
27
|
+
/** Select a source clip for isolated preview; `null` resumes controller output. */
|
|
28
|
+
readonly setClip: (name: string | null) => void;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface AnimationPreviewTransportSnapshot {
|
|
32
|
+
readonly clip: string;
|
|
33
|
+
readonly time: number;
|
|
34
|
+
readonly playing: boolean;
|
|
35
|
+
readonly loop: boolean;
|
|
36
|
+
readonly speed: number;
|
|
37
|
+
readonly mode: 'controller' | 'clip';
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface AnimationRuntimeSnapshot {
|
|
41
|
+
readonly layers: readonly AnimationRuntimeLayer[];
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface AnimationRuntimeLayer {
|
|
45
|
+
readonly id: string;
|
|
46
|
+
readonly state?: string;
|
|
47
|
+
readonly weight: number;
|
|
48
|
+
readonly blendMode: 'override' | 'additive';
|
|
49
|
+
readonly boneMask?: {
|
|
50
|
+
readonly include?: readonly string[] | undefined;
|
|
51
|
+
readonly exclude?: readonly string[] | undefined;
|
|
52
|
+
};
|
|
53
|
+
readonly actions: readonly AnimationRuntimeAction[];
|
|
54
|
+
readonly blendSpace?: AnimationRuntimeBlendSpace;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export interface AnimationRuntimeAction {
|
|
58
|
+
readonly name: string;
|
|
59
|
+
readonly clip: THREE.AnimationClip;
|
|
60
|
+
readonly action: THREE.AnimationAction;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export interface AnimationRuntimeBlendSpace {
|
|
64
|
+
readonly type: '1D' | '2D' | 'direct';
|
|
65
|
+
readonly parameter: string;
|
|
66
|
+
readonly parameterY?: string;
|
|
67
|
+
readonly values: ReadonlyMap<string, number | boolean>;
|
|
68
|
+
readonly samples: readonly {
|
|
69
|
+
readonly clip: string;
|
|
70
|
+
readonly x: number;
|
|
71
|
+
readonly y?: number;
|
|
72
|
+
readonly weight: number;
|
|
73
|
+
}[];
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
let version = 0;
|
|
77
|
+
const listeners = new Set<() => void>();
|
|
78
|
+
|
|
79
|
+
function notify(): void {
|
|
80
|
+
version++;
|
|
81
|
+
for (const listener of listeners) listener();
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function subscribeAnimationRuntimeInspections(listener: () => void): () => void {
|
|
85
|
+
listeners.add(listener);
|
|
86
|
+
return () => listeners.delete(listener);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function animationRuntimeInspectionsVersion(): number {
|
|
90
|
+
return version;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function attachAnimationRuntimeInspection(
|
|
94
|
+
owner: THREE.Object3D,
|
|
95
|
+
inspection: AnimationRuntimeInspection,
|
|
96
|
+
): () => void {
|
|
97
|
+
setUserData(owner, '_animationRuntime', inspection);
|
|
98
|
+
notify();
|
|
99
|
+
return () => {
|
|
100
|
+
if (getUserData(owner, '_animationRuntime') !== inspection) return;
|
|
101
|
+
deleteUserData(owner, '_animationRuntime');
|
|
102
|
+
notify();
|
|
103
|
+
};
|
|
104
|
+
}
|
|
@@ -4,6 +4,7 @@ import type { createSystemRunner } from '../core/system-runner';
|
|
|
4
4
|
import { deleteUserData, getUserData, setUserData } from '../ecs/user-data';
|
|
5
5
|
import type { BlendTreeDef } from './anim-graph-types';
|
|
6
6
|
import { evaluateBlendTree } from './blend-node';
|
|
7
|
+
import { attachAnimationRuntimeInspection } from './runtime-inspection';
|
|
7
8
|
import {
|
|
8
9
|
type AnimationBoneMask,
|
|
9
10
|
type AnimationMetaStateNodeLike,
|
|
@@ -125,6 +126,10 @@ export interface XStateAnimationBinding {
|
|
|
125
126
|
/** Advance native actions/mixer. Register this in the engine animation phase. */
|
|
126
127
|
tick: (dt: number) => void;
|
|
127
128
|
readonly actor: XStateAnimationActor;
|
|
129
|
+
/** Live numeric/boolean values after the binding's optional parameter selector.
|
|
130
|
+
* This is the exact parameter surface used by blend evaluation, exposed so
|
|
131
|
+
* editor instrumentation never guesses by reading actor context directly. */
|
|
132
|
+
getParameters: () => ReadonlyMap<string, number | boolean>;
|
|
128
133
|
/** Current native composition, useful to the editor and game diagnostics. */
|
|
129
134
|
getActiveLayers: () => readonly XStateAnimationLayerState[];
|
|
130
135
|
/** Unsubscribe, stop/uncache actions, and remove owner inspection data. Idempotent. */
|
|
@@ -245,7 +250,11 @@ function dominantOf(weights: WeightedClip[]): WeightedClip {
|
|
|
245
250
|
}
|
|
246
251
|
|
|
247
252
|
/**
|
|
248
|
-
*
|
|
253
|
+
* Optional integration that maps a native XState actor to native Three
|
|
254
|
+
* actions. XState remains a behavior primitive; this bridge is useful when a
|
|
255
|
+
* project deliberately chooses to let one behavior statechart emit animation
|
|
256
|
+
* policy, but it is not the engine's animation model. Animation inspection is
|
|
257
|
+
* published separately through `AnimationRuntimeInspection`.
|
|
249
258
|
*
|
|
250
259
|
* One active animated state is allowed per named layer. Parallel XState
|
|
251
260
|
* regions therefore compose lower-body locomotion, upper-body weapon states,
|
|
@@ -358,6 +367,7 @@ export function bindXStateAnimation(
|
|
|
358
367
|
|
|
359
368
|
const activeLayers = new Map<string, ActiveState>();
|
|
360
369
|
let disposed = false;
|
|
370
|
+
let detachRuntimeInspection = () => {};
|
|
361
371
|
|
|
362
372
|
function silence(weights: WeightedClip[], except: string | undefined, duration: number): void {
|
|
363
373
|
for (const weighted of weights) {
|
|
@@ -475,6 +485,7 @@ export function bindXStateAnimation(
|
|
|
475
485
|
const binding: XStateAnimationBinding = {
|
|
476
486
|
tick,
|
|
477
487
|
actor,
|
|
488
|
+
getParameters: () => readParameters(actor.getSnapshot().context),
|
|
478
489
|
getActiveLayers: () =>
|
|
479
490
|
[...activeLayers.values()].map((active) => ({
|
|
480
491
|
layer: active.layer,
|
|
@@ -491,6 +502,7 @@ export function bindXStateAnimation(
|
|
|
491
502
|
dispose(): void {
|
|
492
503
|
if (disposed) return;
|
|
493
504
|
disposed = true;
|
|
505
|
+
detachRuntimeInspection();
|
|
494
506
|
options.systems?.remove('animation', tick);
|
|
495
507
|
subscription.unsubscribe();
|
|
496
508
|
for (const { action, clip } of actionCache.values()) {
|
|
@@ -505,6 +517,46 @@ export function bindXStateAnimation(
|
|
|
505
517
|
}
|
|
506
518
|
},
|
|
507
519
|
};
|
|
520
|
+
detachRuntimeInspection = attachAnimationRuntimeInspection(owner, {
|
|
521
|
+
mixer,
|
|
522
|
+
clips,
|
|
523
|
+
getSnapshot: () => ({
|
|
524
|
+
layers: [...activeLayers.values()].map((active) => {
|
|
525
|
+
const parameters = readParameters(actor.getSnapshot().context);
|
|
526
|
+
const blendSpace = isBlendTreeAnimationMeta(active.meta)
|
|
527
|
+
? {
|
|
528
|
+
type: active.meta.blendTree.type,
|
|
529
|
+
parameter: active.meta.blendTree.parameter,
|
|
530
|
+
...(active.meta.blendTree.type === '2D'
|
|
531
|
+
? { parameterY: active.meta.blendTree.parameterY }
|
|
532
|
+
: {}),
|
|
533
|
+
values: parameters,
|
|
534
|
+
samples: active.meta.blendTree.children.map((child) => ({
|
|
535
|
+
clip: child.clip,
|
|
536
|
+
x: child.threshold,
|
|
537
|
+
...(child.thresholdY === undefined ? {} : { y: child.thresholdY }),
|
|
538
|
+
weight:
|
|
539
|
+
actionCache
|
|
540
|
+
.get(actionKey(active.stateId, active.meta, child.clip))
|
|
541
|
+
?.action.getEffectiveWeight() ?? 0,
|
|
542
|
+
})),
|
|
543
|
+
}
|
|
544
|
+
: undefined;
|
|
545
|
+
return {
|
|
546
|
+
id: active.layer,
|
|
547
|
+
state: active.stateId,
|
|
548
|
+
weight: layerWeightOf(active.meta),
|
|
549
|
+
blendMode: blendModeOf(active.meta),
|
|
550
|
+
...(active.meta.boneMask ? { boneMask: active.meta.boneMask } : {}),
|
|
551
|
+
actions: active.natural.map((weighted) => {
|
|
552
|
+
const cached = actionCache.get(weighted.actionKey)!;
|
|
553
|
+
return { name: weighted.clip, clip: cached.clip, action: cached.action };
|
|
554
|
+
}),
|
|
555
|
+
...(blendSpace ? { blendSpace } : {}),
|
|
556
|
+
};
|
|
557
|
+
}),
|
|
558
|
+
}),
|
|
559
|
+
});
|
|
508
560
|
setUserData(owner, '_xstateAnimation', binding);
|
|
509
561
|
notifyInspectionLifecycle();
|
|
510
562
|
options.systems?.add('animation', tick);
|
|
@@ -290,11 +290,42 @@ export const RenderModeSchema = z
|
|
|
290
290
|
|
|
291
291
|
// --- Particle Material (our format — asset paths, not Three.js UUIDs) ---
|
|
292
292
|
|
|
293
|
+
/**
|
|
294
|
+
* The sprite `map`'s SAMPLER — how the image is addressed outside [0,1], interpolated, and
|
|
295
|
+
* mip-chained. Separate from the URL because two emitters can name one image and want different
|
|
296
|
+
* sampling of it; the factory caches per URL *and* sampler for exactly that reason.
|
|
297
|
+
*
|
|
298
|
+
* An absent field leaves three's own default alone (clamp-to-edge, linear, mipmapped, anisotropy
|
|
299
|
+
* 1) rather than restating it — the same "absent means the loader's default" contract the rest of
|
|
300
|
+
* this descriptor keeps. `map`'s COLOUR SPACE is deliberately not here: a `map` is always a colour
|
|
301
|
+
* texture, so the factory states sRGB unconditionally (see `particles-factory.ts`).
|
|
302
|
+
*/
|
|
303
|
+
const ParticleMapSamplerSchema = z
|
|
304
|
+
.object({
|
|
305
|
+
wrap: z
|
|
306
|
+
.enum(['clamp', 'repeat', 'mirror'])
|
|
307
|
+
.optional()
|
|
308
|
+
.describe('How UVs outside [0,1] address the sprite image (three wrapS/wrapT)'),
|
|
309
|
+
filter: z
|
|
310
|
+
.enum(['linear', 'nearest'])
|
|
311
|
+
.optional()
|
|
312
|
+
.describe('Texel interpolation — nearest keeps a pixel-art sprite crisp'),
|
|
313
|
+
mipmaps: z.boolean().optional().describe('Generate and sample a mip chain'),
|
|
314
|
+
anisotropy: z
|
|
315
|
+
.number()
|
|
316
|
+
.optional()
|
|
317
|
+
.describe('Anisotropic filtering samples, for sprites viewed at grazing angles'),
|
|
318
|
+
})
|
|
319
|
+
.describe('Sprite texture sampler state');
|
|
320
|
+
|
|
293
321
|
const ParticleMaterialSchema = z
|
|
294
322
|
.object({
|
|
295
323
|
type: z.enum(['basic', 'standard']).optional().describe('Particle material shading model'),
|
|
296
324
|
color: z.string().optional().describe('Particle color as CSS hex string'),
|
|
297
325
|
map: z.string().optional().describe('Path to particle sprite texture'),
|
|
326
|
+
mapSampler: ParticleMapSamplerSchema.optional().describe(
|
|
327
|
+
'Sampler state applied to the sprite texture named by `map`',
|
|
328
|
+
),
|
|
298
329
|
blending: z
|
|
299
330
|
.enum(['normal', 'additive'])
|
|
300
331
|
.optional()
|