@vgai/engine 0.5.13 → 0.5.14

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.
Files changed (192) hide show
  1. package/dist/adapter/adapter-module.d.ts +447 -0
  2. package/dist/adapter/adapter-module.d.ts.map +1 -0
  3. package/dist/adapter/adapter-module.js +308 -0
  4. package/dist/adapter/authoring.d.ts +357 -10
  5. package/dist/adapter/authoring.d.ts.map +1 -1
  6. package/dist/adapter/authoring.js +87 -1
  7. package/dist/adapter/constraint.d.ts +9 -3
  8. package/dist/adapter/constraint.d.ts.map +1 -1
  9. package/dist/adapter/finders/finder-result.d.ts +28 -0
  10. package/dist/adapter/finders/finder-result.d.ts.map +1 -0
  11. package/dist/adapter/finders/finder-result.js +10 -0
  12. package/dist/adapter/finders/index.d.ts +38 -0
  13. package/dist/adapter/finders/index.d.ts.map +1 -0
  14. package/dist/adapter/finders/index.js +41 -0
  15. package/dist/adapter/finders/prefabs-from-stories.d.ts +64 -0
  16. package/dist/adapter/finders/prefabs-from-stories.d.ts.map +1 -0
  17. package/dist/adapter/finders/prefabs-from-stories.js +92 -0
  18. package/dist/adapter/finders/scenes-from-entrypoint-selection.d.ts +67 -0
  19. package/dist/adapter/finders/scenes-from-entrypoint-selection.d.ts.map +1 -0
  20. package/dist/adapter/finders/scenes-from-entrypoint-selection.js +226 -0
  21. package/dist/adapter/first-party-systems.d.ts.map +1 -1
  22. package/dist/adapter/first-party-systems.js +29 -0
  23. package/dist/adapter/index.d.ts +7 -3
  24. package/dist/adapter/index.d.ts.map +1 -1
  25. package/dist/adapter/index.js +5 -2
  26. package/dist/adapter/ingest/contract-system-adapters.d.ts +24 -1
  27. package/dist/adapter/ingest/contract-system-adapters.d.ts.map +1 -1
  28. package/dist/adapter/ingest/contract-system-adapters.js +100 -34
  29. package/dist/adapter/ingest/game-contract.d.ts +188 -2
  30. package/dist/adapter/ingest/game-contract.d.ts.map +1 -1
  31. package/dist/adapter/ingest/game-contract.js +107 -0
  32. package/dist/adapter/ingest/mount-readiness.d.ts +84 -0
  33. package/dist/adapter/ingest/mount-readiness.d.ts.map +1 -0
  34. package/dist/adapter/ingest/mount-readiness.js +81 -0
  35. package/dist/adapter/ingest/scene-capture.d.ts +91 -2
  36. package/dist/adapter/ingest/scene-capture.d.ts.map +1 -1
  37. package/dist/adapter/ingest/scene-capture.js +110 -18
  38. package/dist/adapter/ingest/structural-ids.d.ts +11 -5
  39. package/dist/adapter/ingest/structural-ids.d.ts.map +1 -1
  40. package/dist/adapter/ingest/structural-ids.js +18 -9
  41. package/dist/adapter/ingest/upstream-pin.d.ts +4 -2
  42. package/dist/adapter/ingest/upstream-pin.d.ts.map +1 -1
  43. package/dist/adapter/ingest/upstream-pin.js +4 -2
  44. package/dist/adapter/ingest/visible-capture-window.d.ts +116 -0
  45. package/dist/adapter/ingest/visible-capture-window.d.ts.map +1 -0
  46. package/dist/adapter/ingest/visible-capture-window.js +141 -0
  47. package/dist/adapter/loop-gate-report.d.ts +3 -4
  48. package/dist/adapter/loop-gate-report.d.ts.map +1 -1
  49. package/dist/adapter/loop-gate-report.js +3 -4
  50. package/dist/adapter/root-adapter.d.ts +22 -2
  51. package/dist/adapter/root-adapter.d.ts.map +1 -1
  52. package/dist/adapter/setup-three-root-adapter.d.ts.map +1 -1
  53. package/dist/adapter/setup-three-root-adapter.js +15 -0
  54. package/dist/adapter/system-adapter.d.ts +161 -2
  55. package/dist/adapter/system-adapter.d.ts.map +1 -1
  56. package/dist/adapter/system-adapter.js +18 -1
  57. package/dist/animation/camera-ownership.d.ts +4 -2
  58. package/dist/animation/camera-ownership.d.ts.map +1 -1
  59. package/dist/animation/camera-ownership.js +58 -1
  60. package/dist/animation/runtime-inspection.d.ts +70 -0
  61. package/dist/animation/runtime-inspection.d.ts.map +1 -0
  62. package/dist/animation/runtime-inspection.js +25 -0
  63. package/dist/animation/xstate-animation-binding.d.ts +9 -1
  64. package/dist/animation/xstate-animation-binding.d.ts.map +1 -1
  65. package/dist/animation/xstate-animation-binding.js +48 -1
  66. package/dist/asset-formats/material.d.ts +1 -1
  67. package/dist/asset-formats/mesh.d.ts +1 -1
  68. package/dist/asset-formats/particles.d.ts +13 -0
  69. package/dist/asset-formats/particles.d.ts.map +1 -1
  70. package/dist/asset-formats/particles.js +28 -0
  71. package/dist/behavior/xstate-inspection.d.ts +24 -0
  72. package/dist/behavior/xstate-inspection.d.ts.map +1 -0
  73. package/dist/behavior/xstate-inspection.js +25 -0
  74. package/dist/core/game-loop.d.ts +7 -5
  75. package/dist/core/game-loop.d.ts.map +1 -1
  76. package/dist/core/game-loop.js +39 -18
  77. package/dist/core/types.d.ts +11 -12
  78. package/dist/core/types.d.ts.map +1 -1
  79. package/dist/data/data-asset.js +1 -1
  80. package/dist/dev/instruments.d.ts.map +1 -1
  81. package/dist/dev/instruments.js +5 -1
  82. package/dist/dev/render-debug-adapter.d.ts +19 -2
  83. package/dist/dev/render-debug-adapter.d.ts.map +1 -1
  84. package/dist/dev/render-debug-adapter.js +10 -0
  85. package/dist/ecs/user-data.d.ts +10 -2
  86. package/dist/ecs/user-data.d.ts.map +1 -1
  87. package/dist/ecs/user-data.js +6 -2
  88. package/dist/input/binding-identity.d.ts +10 -0
  89. package/dist/input/binding-identity.d.ts.map +1 -0
  90. package/dist/input/binding-identity.js +36 -0
  91. package/dist/input/input-manager.d.ts +18 -185
  92. package/dist/input/input-manager.d.ts.map +1 -1
  93. package/dist/input/input-manager.js +78 -68
  94. package/dist/manifest/load.d.ts +14 -0
  95. package/dist/manifest/load.d.ts.map +1 -1
  96. package/dist/manifest/load.js +6 -1
  97. package/dist/manifest/schema.d.ts +18 -0
  98. package/dist/manifest/schema.d.ts.map +1 -1
  99. package/dist/manifest/schema.js +64 -7
  100. package/dist/pixi/authoring.d.ts +27 -0
  101. package/dist/pixi/authoring.d.ts.map +1 -1
  102. package/dist/pixi/authoring.js +10 -3
  103. package/dist/pixi/index.d.ts +1 -1
  104. package/dist/pixi/index.d.ts.map +1 -1
  105. package/dist/pixi/index.js +1 -1
  106. package/dist/pixi/ingest.d.ts +8 -0
  107. package/dist/pixi/ingest.d.ts.map +1 -1
  108. package/dist/pixi/ingest.js +10 -1
  109. package/dist/pixi/render-pass-bracket.d.ts +56 -0
  110. package/dist/pixi/render-pass-bracket.d.ts.map +1 -0
  111. package/dist/pixi/render-pass-bracket.js +98 -0
  112. package/dist/pixi/scene-capture.d.ts +32 -1
  113. package/dist/pixi/scene-capture.d.ts.map +1 -1
  114. package/dist/pixi/scene-capture.js +21 -9
  115. package/dist/pixi/system-adapters.d.ts +29 -0
  116. package/dist/pixi/system-adapters.d.ts.map +1 -1
  117. package/dist/pixi/system-adapters.js +27 -0
  118. package/dist/render/particles-factory.d.ts +16 -2
  119. package/dist/render/particles-factory.d.ts.map +1 -1
  120. package/dist/render/particles-factory.js +88 -5
  121. package/dist/render/soft-particle-depth.d.ts +149 -0
  122. package/dist/render/soft-particle-depth.d.ts.map +1 -0
  123. package/dist/render/soft-particle-depth.js +305 -0
  124. package/dist/render/voxel-surface.d.ts +117 -0
  125. package/dist/render/voxel-surface.d.ts.map +1 -0
  126. package/dist/render/voxel-surface.js +263 -0
  127. package/dist/runtime/create-runtime.js +7 -0
  128. package/dist/runtime/debug-bridge.d.ts +15 -15
  129. package/dist/runtime/debug-bridge.d.ts.map +1 -1
  130. package/dist/runtime/debug-bridge.js +27 -26
  131. package/dist/runtime/game.d.ts.map +1 -1
  132. package/dist/runtime/game.js +1 -2
  133. package/dist/setup/setup-particles.d.ts.map +1 -1
  134. package/dist/setup/setup-particles.js +9 -0
  135. package/dist/world3d-react/r3f-adapter.d.ts.map +1 -1
  136. package/dist/world3d-react/r3f-adapter.js +42 -3
  137. package/dist/world3d-react/rapier-physics-bridge.d.ts.map +1 -1
  138. package/dist/world3d-react/rapier-physics-bridge.js +201 -11
  139. package/dist-config/config.js.map +1 -1
  140. package/package.json +4 -2
  141. package/schemas/engine-capabilities.json +12 -12
  142. package/schemas/vgai-project.schema.json +41 -2
  143. package/src/adapter/adapter-module.ts +607 -0
  144. package/src/adapter/authoring.ts +440 -10
  145. package/src/adapter/constraint.ts +10 -3
  146. package/src/adapter/finders/finder-result.ts +29 -0
  147. package/src/adapter/finders/index.ts +62 -0
  148. package/src/adapter/finders/prefabs-from-stories.ts +142 -0
  149. package/src/adapter/finders/scenes-from-entrypoint-selection.ts +304 -0
  150. package/src/adapter/first-party-systems.ts +30 -0
  151. package/src/adapter/index.ts +32 -1
  152. package/src/adapter/ingest/contract-system-adapters.ts +116 -32
  153. package/src/adapter/ingest/game-contract.ts +265 -2
  154. package/src/adapter/ingest/mount-readiness.ts +140 -0
  155. package/src/adapter/ingest/scene-capture.ts +199 -22
  156. package/src/adapter/ingest/structural-ids.ts +21 -9
  157. package/src/adapter/ingest/upstream-pin.ts +4 -2
  158. package/src/adapter/ingest/visible-capture-window.ts +213 -0
  159. package/src/adapter/loop-gate-report.ts +3 -4
  160. package/src/adapter/root-adapter.ts +22 -2
  161. package/src/adapter/setup-three-root-adapter.ts +16 -0
  162. package/src/adapter/system-adapter.ts +188 -2
  163. package/src/animation/camera-ownership.ts +68 -3
  164. package/src/animation/runtime-inspection.ts +104 -0
  165. package/src/animation/xstate-animation-binding.ts +53 -1
  166. package/src/asset-formats/particles.ts +31 -0
  167. package/src/behavior/xstate-inspection.ts +51 -0
  168. package/src/core/game-loop.ts +39 -17
  169. package/src/core/types.ts +11 -12
  170. package/src/data/data-asset.ts +1 -1
  171. package/src/dev/instruments.ts +5 -1
  172. package/src/dev/render-debug-adapter.ts +22 -2
  173. package/src/ecs/user-data.ts +10 -2
  174. package/src/input/binding-identity.ts +37 -0
  175. package/src/input/input-manager.ts +93 -68
  176. package/src/manifest/load.ts +23 -2
  177. package/src/manifest/schema.ts +76 -7
  178. package/src/pixi/authoring.ts +37 -5
  179. package/src/pixi/index.ts +2 -0
  180. package/src/pixi/ingest.ts +15 -2
  181. package/src/pixi/render-pass-bracket.ts +117 -0
  182. package/src/pixi/scene-capture.ts +63 -13
  183. package/src/pixi/system-adapters.ts +41 -0
  184. package/src/render/particles-factory.ts +89 -6
  185. package/src/render/soft-particle-depth.ts +336 -0
  186. package/src/render/voxel-surface.ts +358 -0
  187. package/src/runtime/create-runtime.ts +7 -0
  188. package/src/runtime/debug-bridge.ts +32 -31
  189. package/src/runtime/game.ts +1 -2
  190. package/src/setup/setup-particles.ts +9 -0
  191. package/src/world3d-react/r3f-adapter.tsx +48 -2
  192. 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 `PhysicsAdapter2D` (`pixi/system-adapters.ts`)
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?: PhysicsAdapter;
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, durationSeconds }`
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) evaluateFrame(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
- * Bind a native XState actor to native Three actions.
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()