@vgai/engine 0.5.25 → 0.5.27

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 (48) hide show
  1. package/dist/adapter/adapter-module.d.ts +10 -9
  2. package/dist/adapter/adapter-module.d.ts.map +1 -1
  3. package/dist/adapter/adapter-module.js +9 -10
  4. package/dist/adapter/binding.d.ts +4 -2
  5. package/dist/adapter/binding.d.ts.map +1 -1
  6. package/dist/adapter/binding.js +1 -1
  7. package/dist/adapter/index.d.ts +2 -2
  8. package/dist/adapter/index.d.ts.map +1 -1
  9. package/dist/adapter/index.js +1 -1
  10. package/dist/adapter/ingest/structural-ids.d.ts +1 -1
  11. package/dist/adapter/ingest/structural-ids.js +1 -1
  12. package/dist/adapter/root-adapter.d.ts +31 -9
  13. package/dist/adapter/root-adapter.d.ts.map +1 -1
  14. package/dist/adapter/root-seam-contract.d.ts +6 -1
  15. package/dist/adapter/root-seam-contract.d.ts.map +1 -1
  16. package/dist/adapter/root-seam-contract.js +2 -1
  17. package/dist/adapter/system-adapter.d.ts +10 -0
  18. package/dist/adapter/system-adapter.d.ts.map +1 -1
  19. package/dist/ai/navigation.d.ts +11 -0
  20. package/dist/ai/navigation.d.ts.map +1 -1
  21. package/dist/ai/navigation.js +11 -0
  22. package/dist/canvas-react/pixi-react-root-factory.d.ts.map +1 -1
  23. package/dist/canvas-react/pixi-react-root-factory.js +12 -3
  24. package/dist/pixi/authoring.d.ts +3 -3
  25. package/dist/pixi/authoring.d.ts.map +1 -1
  26. package/dist/pixi/authoring.js +2 -2
  27. package/dist/runtime/create-runtime.d.ts +12 -11
  28. package/dist/runtime/create-runtime.d.ts.map +1 -1
  29. package/dist/runtime/create-runtime.js +16 -15
  30. package/dist/runtime/game.d.ts +10 -12
  31. package/dist/runtime/game.d.ts.map +1 -1
  32. package/dist/runtime/game.js +12 -15
  33. package/dist/runtime/render-control.d.ts.map +1 -1
  34. package/dist/runtime/render-control.js +6 -1
  35. package/package.json +1 -1
  36. package/src/adapter/adapter-module.ts +12 -13
  37. package/src/adapter/binding.ts +4 -2
  38. package/src/adapter/index.ts +3 -1
  39. package/src/adapter/ingest/structural-ids.ts +1 -1
  40. package/src/adapter/root-adapter.ts +31 -9
  41. package/src/adapter/root-seam-contract.ts +4 -3
  42. package/src/adapter/system-adapter.ts +10 -0
  43. package/src/ai/navigation.ts +24 -0
  44. package/src/canvas-react/pixi-react-root-factory.tsx +17 -3
  45. package/src/pixi/authoring.ts +3 -3
  46. package/src/runtime/create-runtime.ts +27 -26
  47. package/src/runtime/game.ts +21 -30
  48. package/src/runtime/render-control.ts +6 -1
@@ -2,7 +2,7 @@
2
2
 
3
3
  import type { AdapterSurface } from './adapter-surface';
4
4
  import type {
5
- MountedPixiRoot,
5
+ MountedCanvasRoot,
6
6
  MountedReactRoot,
7
7
  MountedRootBase,
8
8
  MountedThreeRoot,
@@ -36,7 +36,7 @@ export const ROOT_STATE_OBSERVER_SHAPE = defineSeamShape<RootStateObserver>()({
36
36
  });
37
37
 
38
38
  type ThreeSurface = Omit<MountedThreeRoot, keyof MountedRootBase>;
39
- type CanvasSurface = Omit<MountedPixiRoot, keyof MountedRootBase>;
39
+ type CanvasSurface = Omit<MountedCanvasRoot, keyof MountedRootBase>;
40
40
  type DomSurface = Omit<MountedReactRoot, keyof MountedRootBase>;
41
41
 
42
42
  export const MOUNTED_THREE_SURFACE_SHAPE = defineSeamShape<ThreeSurface>()({
@@ -48,7 +48,8 @@ export const MOUNTED_THREE_SURFACE_SHAPE = defineSeamShape<ThreeSurface>()({
48
48
 
49
49
  export const MOUNTED_CANVAS_SURFACE_SHAPE = defineSeamShape<CanvasSurface>()({
50
50
  kind: { optional: false, kind: 'value', required: 'shape' },
51
- stage: { optional: false, kind: 'value', required: 'effect' },
51
+ canvas: { optional: false, kind: 'value', required: 'effect' },
52
+ substrate: { optional: false, kind: 'value', required: 'effect' },
52
53
  });
53
54
 
54
55
  export const MOUNTED_DOM_SURFACE_SHAPE = defineSeamShape<DomSurface>()({
@@ -357,6 +357,16 @@ export interface NavCrowdAgentState {
357
357
  velocity: NavPoint;
358
358
  radius: number;
359
359
  height: number;
360
+ /** The crowd's own agent state. `invalid` is the expressible FAILURE — the
361
+ * agent is off the navmesh (a bad spawn or teleport) and will not move;
362
+ * `offmesh` is an off-mesh connection traversal. Absent on an adapter that
363
+ * predates this field — the editor draws such agents as walking. */
364
+ state?: 'invalid' | 'walking' | 'offmesh';
365
+ /** The agent's current move target, when the implementation reports one. */
366
+ target?: NavPoint;
367
+ /** The local path-corridor corners the agent is steering along — the
368
+ * REQUESTED PATH the debug draw projects. Empty or absent when idle. */
369
+ corners?: NavPoint[];
360
370
  }
361
371
 
362
372
  /**
@@ -31,8 +31,22 @@ export interface CrowdAgentSnapshot {
31
31
  velocity: Vector3Like;
32
32
  radius: number;
33
33
  height: number;
34
+ /** Detour's own agent state. `invalid` is the crowd's expressible FAILURE —
35
+ * the agent is not on the navmesh (a bad spawn or teleport) and will not
36
+ * move until re-placed; `offmesh` is an off-mesh connection traversal. */
37
+ state: 'invalid' | 'walking' | 'offmesh';
38
+ /** The agent's current move target — detour's own answer, reported as the
39
+ * fact it is. Whether a request is ACTIVE is what {@link corners} says. */
40
+ target: Vector3Like;
41
+ /** The local path-corridor corners toward the target — the REQUESTED PATH
42
+ * the crowd is steering along right now (detour keeps a local corridor, not
43
+ * the whole route). Empty when the agent has nowhere to go. */
44
+ corners: Vector3Like[];
34
45
  }
35
46
 
47
+ /** Detour `CrowdAgentState` values, named. */
48
+ const AGENT_STATES = ['invalid', 'walking', 'offmesh'] as const;
49
+
36
50
  export interface NavMeshBuildParams {
37
51
  cellSize?: number | undefined;
38
52
  cellHeight?: number | undefined;
@@ -176,11 +190,21 @@ export class NavMeshManager {
176
190
  return this.crowd.getAgents().map((a) => {
177
191
  const p = a.interpolatedPosition;
178
192
  const v = a.velocity();
193
+ const t = a.target();
194
+ // An invalid agent has no corridor, and detour's accessors are not
195
+ // guaranteed meaningful off the mesh — report the failure state with an
196
+ // empty path rather than reading through it.
197
+ const state = AGENT_STATES[a.state()] ?? 'invalid';
198
+ const corners =
199
+ state === 'invalid' ? [] : a.corners().map((c) => ({ x: c.x, y: c.y, z: c.z }));
179
200
  return {
180
201
  position: { x: p.x, y: p.y, z: p.z },
181
202
  velocity: { x: v.x, y: v.y, z: v.z },
182
203
  radius: a.radius,
183
204
  height: a.height,
205
+ state,
206
+ target: { x: t.x, y: t.y, z: t.z },
207
+ corners,
184
208
  };
185
209
  });
186
210
  }
@@ -80,6 +80,20 @@ type PixiReactRoot = ReturnType<typeof createRoot>;
80
80
  interface CanvasEntryModuleExports {
81
81
  /** The one shape: the world IS a component. */
82
82
  readonly default?: ComponentType;
83
+ /** Imperative canvas substrates export their own host adapter. This is the
84
+ * same explicit boundary the three entry adjudicator accepts; gameplay
85
+ * modules remain ecosystem-native. */
86
+ readonly adapter?: unknown;
87
+ }
88
+
89
+ function isCanvasRootAdapter(candidate: unknown): candidate is RootAdapter<'canvas'> {
90
+ if (candidate === null || typeof candidate !== 'object') return false;
91
+ const value = candidate as Record<string, unknown>;
92
+ return (
93
+ typeof value['id'] === 'string' &&
94
+ value['id'].length > 0 &&
95
+ typeof value['mount'] === 'function'
96
+ );
83
97
  }
84
98
 
85
99
  /**
@@ -274,9 +288,8 @@ function canvasWorldAdapter(id: string, component: ComponentType): RootAdapter<'
274
288
 
275
289
  return {
276
290
  kind: 'canvas',
277
- // The reconciler's real root container — the same `PIXI.Container`
278
- // `RootInstance.pixiStage()` hands the editor.
279
- stage: mountedApp.stage,
291
+ canvas,
292
+ substrate: { name: 'pixi', root: mountedApp.stage },
280
293
  drivesOwnLoop: false,
281
294
  systems: systemAdapters,
282
295
  disposeComplete,
@@ -337,6 +350,7 @@ export function resolveCanvasEntryAdapter(
337
350
  rootId: string,
338
351
  ): RootAdapter<'canvas'> | null {
339
352
  const mod = entryModule as CanvasEntryModuleExports | undefined;
353
+ if (isCanvasRootAdapter(mod?.adapter)) return mod.adapter;
340
354
  if (typeof mod?.default === 'function') return canvasWorldAdapter(rootId, mod.default);
341
355
  return null;
342
356
  }
@@ -2,7 +2,7 @@ import type { Container } from 'pixi.js';
2
2
 
3
3
  /**
4
4
  * AuthoringAdapter-2D — the Pixi analog of the editor AuthoringAdapter /
5
- * LiveThreeAuthoringAdapter, implemented DIRECTLY over a live PixiJS display tree
5
+ * ThreeAuthoringAdapter, implemented DIRECTLY over a live PixiJS display tree
6
6
  * (no fabricated document — the anti-shim rule). Used by the editor to
7
7
  * inspect + edit Pixi display-tree entities (first-party OR an ingested unmodified game),
8
8
  * with stable structural-path ids so edits re-bind after the tree rebuilds.
@@ -668,7 +668,7 @@ export class AuthoringAdapter2D {
668
668
  * it into the in-memory overlay so a later `serializeOverlay()`/save preserves it
669
669
  * and a later `refresh()` re-applies it again. Ids absent from the current tree
670
670
  * are skipped (orphaned, not an error) — the 2D analog of
671
- * `LiveThreeAuthoringAdapter.applyOverlay` (T3.2 slice 3). Used both for the initial
671
+ * `ThreeAuthoringAdapter.applyOverlay` (T3.2 slice 3). Used both for the initial
672
672
  * reload (constructor `opts.overlay`) and for `PersistenceProvider.applyExternal`
673
673
  * (re-apply a changed overlay file without a full remount).
674
674
  */
@@ -699,7 +699,7 @@ export class AuthoringAdapter2D {
699
699
  for (const [id, ov] of Object.entries(this.overlay)) this.applyOverride(id, ov);
700
700
  this.dirty = true;
701
701
  }
702
- /** Clear the dirty flag after a successful save (mirrors LiveThreeAuthoringAdapter). */
702
+ /** Clear the dirty flag after a successful save (mirrors ThreeAuthoringAdapter). */
703
703
  markClean(): void {
704
704
  this.dirty = false;
705
705
  }
@@ -3,7 +3,7 @@ import { assertNever } from '../adapter/adapter-surface';
3
3
  import { createRootBinding, type RootBinding, type RootDeclaration } from '../adapter/binding';
4
4
  import type { CanvasHostContext, DomHostContext, ThreeHostContext } from '../adapter/host-context';
5
5
  import type {
6
- MountedPixiRoot,
6
+ MountedCanvasRoot,
7
7
  MountedReactRoot,
8
8
  MountedRoot,
9
9
  MountedThreeRoot,
@@ -100,16 +100,17 @@ function bindRoot(
100
100
  }
101
101
 
102
102
  /**
103
- * Register a canvas world onto the Game shell — the pixi analog of
103
+ * Register a canvas world onto the Game shell — the canvas analog of
104
104
  * {@link registerThreeRoot}. `adapter` is the seam's own
105
105
  * `RootAdapter<'canvas'>`; `RootInstance.adapter` itself only needs `.id`
106
106
  * (`AdapterHandle`, `runtime/game.ts`), so this passes through with zero cast.
107
- * `stage` is the one field every `MountedPixiRoot` actually carries.
107
+ * The substrate root remains opaque here; the editor dispatches from the
108
+ * mount's explicit `substrate.name` declaration.
108
109
  */
109
- export function registerPixiRoot(
110
+ export function registerCanvasRoot(
110
111
  game: GameInternal,
111
112
  adapter: RootAdapter<'canvas'>,
112
- mounted: MountedPixiRoot,
113
+ mounted: MountedCanvasRoot,
113
114
  opts?: RegisterRootOptions,
114
115
  ): RootInstance {
115
116
  const id = opts?.id ?? 'main';
@@ -120,7 +121,7 @@ export function registerPixiRoot(
120
121
  pausable,
121
122
  adapter,
122
123
  mounted,
123
- stage: mounted.stage,
124
+ canvasRoot: mounted.substrate.root,
124
125
  binding: bindRoot(game, opts?.declaration, { surface: 'canvas', adapter }, mounted, pausable),
125
126
  });
126
127
  game.registerRoot(world);
@@ -129,13 +130,13 @@ export function registerPixiRoot(
129
130
 
130
131
  /**
131
132
  * Register a React world onto the Game shell — the DOM
132
- * analog of {@link registerThreeRoot}/{@link registerPixiRoot}. A react
133
+ * analog of {@link registerThreeRoot}/{@link registerCanvasRoot}. A react
133
134
  * world has no `frame` hooks (react's own
134
135
  * `createRoot` schedules its commits; `GameInternal.runFrame` correctly
135
136
  * leaves a world with no `frame` untouched by its opaque-`update` fallback
136
137
  * too, since `MountedReactGame` declares no `update`). `adapter` is typed
137
138
  * as the real {@link ReactRootAdapter} shape (same reasoning as
138
- * `registerPixiRoot`'s doc comment above) — `RootInstance.adapter` only
139
+ * `registerCanvasRoot`'s doc comment above) — `RootInstance.adapter` only
139
140
  * needs `.id`, so this passes through with zero cast.
140
141
  */
141
142
  export function registerReactRoot(
@@ -202,10 +203,10 @@ export interface ThreeRootMountSpec extends RootMountSpecBase {
202
203
 
203
204
  /**
204
205
  * `adapter` is the SEAM's `RootAdapter<'canvas'>` — a canvas root's mount
205
- * honestly returns only a `MountedPixiRoot`, and naming the real contract
206
+ * honestly returns a `MountedCanvasRoot`, and naming the real contract
206
207
  * here is what keeps the resolver cast-free.
207
208
  */
208
- export interface PixiRootMountSpec extends RootMountSpecBase {
209
+ export interface CanvasRootMountSpec extends RootMountSpecBase {
209
210
  readonly kind: 'canvas';
210
211
  readonly adapter: RootAdapter<'canvas'>;
211
212
  }
@@ -234,7 +235,7 @@ export interface PixiRootMountSpec extends RootMountSpecBase {
234
235
  * `kind`/`container` satisfy `MountedReactRoot` (`adapter/
235
236
  * root-adapter.ts`) — `container` is the SAME `DomHostContext.container` the
236
237
  * adapter's `mount` was handed (identity matters, mirroring `threeScene()`/
237
- * `pixiStage()`'s "same instance the adapter mounted" contract); every
238
+ * `canvasRoot()`'s "same instance the adapter mounted" contract); every
238
239
  * `ReactRootAdapter` implementer echoes it back here so `mounted` alone
239
240
  * (with no separately-threaded `container`) satisfies the union
240
241
  * `RootInstanceInit.mounted`/`RootInstance.mounted` with zero cast.
@@ -259,14 +260,14 @@ export interface ReactRootAdapter extends RootAdapter<'dom'> {
259
260
  mount(host: DomHostContext): Promise<MountedReactGame>;
260
261
  }
261
262
 
262
- /** Same reasoning as {@link PixiRootMountSpec} above: the seam's
263
+ /** Same reasoning as {@link CanvasRootMountSpec} above: the seam's
263
264
  * `RootAdapter<'dom'>` is what a DOM root actually guarantees. */
264
265
  export interface ReactRootMountSpec extends RootMountSpecBase {
265
266
  readonly kind: 'dom';
266
267
  readonly adapter: RootAdapter<'dom'>;
267
268
  }
268
269
 
269
- export type RootMountSpec = ThreeRootMountSpec | PixiRootMountSpec | ReactRootMountSpec;
270
+ export type RootMountSpec = ThreeRootMountSpec | CanvasRootMountSpec | ReactRootMountSpec;
270
271
 
271
272
  /**
272
273
  * Mount N adapter roots (three + canvas + react)
@@ -450,7 +451,7 @@ interface OneRootResult {
450
451
 
451
452
  /** Shared per-world mount inputs, computed once in `createRootsGameRuntime`'s
452
453
  * loop (canvas stacking + one dpr) and threaded into whichever of
453
- * `mountOneThreeRoot`/`mountOnePixiRoot` this world's `kind` needs — split
454
+ * `mountOneThreeRoot`/`mountOneCanvasRoot` this world's `kind` needs — split
454
455
  * out so the orchestrating loop itself stays a simple dispatch. */
455
456
  interface OneRootContext {
456
457
  readonly game: GameInternal;
@@ -469,7 +470,7 @@ interface OneRootContext {
469
470
  /** Mount one three `RootMountSpec` (canvas + renderer construction, per
470
471
  * D5 §1/§4, then `registerThreeRoot`). Split out of
471
472
  * `createRootsGameRuntime` purely to keep that function's own branching
472
- * simple — see `mountOnePixiRoot` for the canvas sibling. */
473
+ * simple — see `mountOneCanvasRoot` for the canvas sibling. */
473
474
  async function mountOneThreeRoot(
474
475
  spec: ThreeRootMountSpec,
475
476
  ctx: OneRootContext,
@@ -539,7 +540,7 @@ async function mountOneThreeRoot(
539
540
  }
540
541
 
541
542
  /** Mount one canvas `RootMountSpec` (via its own `CanvasHostContext`, per D5
542
- * §1/§3/§4, then `registerPixiRoot`) — the pixi sibling of
543
+ * §1/§3/§4, then `registerCanvasRoot`) — the canvas sibling of
543
544
  * `mountOneThreeRoot` above.
544
545
  *
545
546
  * Unlike `mountOneThreeRoot`, this needs no explicit `canvas.style.width`/
@@ -556,8 +557,8 @@ async function mountOneThreeRoot(
556
557
  * already calls `session.resize()` unconditionally right after mount, so
557
558
  * this never surfaces as a lasting bug the way three' buffer-only resize
558
559
  * did (never self-healing, by design — see above). */
559
- async function mountOnePixiRoot(
560
- spec: PixiRootMountSpec,
560
+ async function mountOneCanvasRoot(
561
+ spec: CanvasRootMountSpec,
561
562
  ctx: Omit<OneRootContext, 'assets'>,
562
563
  ): Promise<OneRootResult> {
563
564
  const { game, canvas, w, h, dpr, headless, isBottom } = ctx;
@@ -572,7 +573,7 @@ async function mountOnePixiRoot(
572
573
  preserveDrawingBuffer: true,
573
574
  };
574
575
  const mounted = await spec.adapter.mount(pixiHost);
575
- registerPixiRoot(game, spec.adapter, mounted, {
576
+ registerCanvasRoot(game, spec.adapter, mounted, {
576
577
  id: spec.id,
577
578
  pausable: spec.pausable,
578
579
  declaration: spec.declaration,
@@ -596,7 +597,7 @@ async function mountOnePixiRoot(
596
597
 
597
598
  /**
598
599
  * Mount one React `RootMountSpec` — the DOM sibling of
599
- * `mountOneThreeRoot`/`mountOnePixiRoot`. Unlike its canvas-backed siblings
600
+ * `mountOneThreeRoot`/`mountOneCanvasRoot`. Unlike its canvas-backed siblings
600
601
  * this returns NO `routerEntry`: a react world's DOM-root layer participates in
601
602
  * D5's z-order/box stacking (the caller still creates and positions its `<div>`
602
603
  * exactly like a canvas — see `createRootsGameRuntime`'s stack-building loop)
@@ -655,7 +656,7 @@ async function mountOneReactRoot(
655
656
  * canvas for three/canvas, a DOM-root `<div>` layer for react — stacked
656
657
  * per D5 §1, z-order/ties exactly matching
657
658
  * `manifest/load.ts`'s sort, ONE `Game`, and registers every world onto it
658
- * via `registerThreeRoot`/`registerPixiRoot`/`registerReactRoot` — the
659
+ * via `registerThreeRoot`/`registerCanvasRoot`/`registerReactRoot` — the
659
660
  * SAME wiring `test/create-runtime-worlds.test.ts` proves for the
660
661
  * three/canvas pair. Worlds MOUNT in `roots` ARRAY order ("manifest
661
662
  * declaration order" — the frame/registration axis), independent of
@@ -720,14 +721,14 @@ interface MountAllRootsResult {
720
721
  * Mount + register every world spec, in ARRAY (declaration) order — split
721
722
  * out of `createRootsGameRuntime` purely to keep that function's own
722
723
  * cyclomatic complexity down (E4, same reason `mountOneThreeRoot`/
723
- * `mountOnePixiRoot` are already split out). React roots contribute NO
724
+ * `mountOneCanvasRoot` are already split out). React roots contribute NO
724
725
  * router entry ("DOM layers need no entry in the router's hit-test loop"):
725
726
  * their layer's own `pointer-events` discipline handles claim/fall-through
726
727
  * natively, with zero router involvement (see `mountOneReactRoot`'s doc
727
728
  * comment).
728
729
  */
729
730
  async function mountAllRootSpecs(
730
- mountSpecs: (ThreeRootMountSpec | PixiRootMountSpec | ReactRootMountSpec)[],
731
+ mountSpecs: (ThreeRootMountSpec | CanvasRootMountSpec | ReactRootMountSpec)[],
731
732
  bottomId: string | undefined,
732
733
  inputs: MountAllRootsInputs,
733
734
  ): Promise<MountAllRootsResult> {
@@ -751,7 +752,7 @@ async function mountAllRootSpecs(
751
752
  if (spec.kind === 'three') {
752
753
  ({ mountedEntry, routerEntry } = await mountOneThreeRoot(spec, oneCtx));
753
754
  } else if (spec.kind === 'canvas') {
754
- ({ mountedEntry, routerEntry } = await mountOnePixiRoot(spec, oneCtx));
755
+ ({ mountedEntry, routerEntry } = await mountOneCanvasRoot(spec, oneCtx));
755
756
  } else {
756
757
  // Exhaustiveness guard (§7.4-2): 'react' was already handled by the
757
758
  // early `continue` above, so only a hypothetical 4th `AdapterSurface` can
@@ -876,7 +877,7 @@ async function createRootsGameRuntime(config: RootsRuntimeConfig): Promise<GameS
876
877
  if (specs.length === 0) {
877
878
  throw new Error('createGameRuntime: `roots` must contain at least one root.');
878
879
  }
879
- const mountSpecs = specs as (ThreeRootMountSpec | PixiRootMountSpec | ReactRootMountSpec)[];
880
+ const mountSpecs = specs as (ThreeRootMountSpec | CanvasRootMountSpec | ReactRootMountSpec)[];
880
881
 
881
882
  const w = Math.max(
882
883
  1,
@@ -992,7 +993,7 @@ async function createRootsGameRuntime(config: RootsRuntimeConfig): Promise<GameS
992
993
  // --- Mount + register every world, in ARRAY (declaration) order. ---
993
994
  // Split out into its own top-level function purely to keep
994
995
  // `createRootsGameRuntime`'s own cyclomatic complexity down (E4) — same
995
- // reason `mountOneThreeRoot`/`mountOnePixiRoot` are already split out
996
+ // reason `mountOneThreeRoot`/`mountOneCanvasRoot` are already split out
996
997
  // below.
997
998
  let mounted: MountAllRootsResult;
998
999
  try {
@@ -5,11 +5,6 @@
5
5
  * RootInstance retains its own mounted surface and optional capabilities.
6
6
  */
7
7
 
8
- // TYPE-ONLY pixi import: `game.ts` must never value-import
9
- // `pixi.js` — a value import would pull the Pixi runtime into 3D-only
10
- // bundles that never touch a canvas root. Every pixi-typed field below is erased
11
- // at compile time; nothing here constructs or calls into Pixi.
12
- import type * as PIXI from 'pixi.js';
13
8
  import type * as THREE from 'three';
14
9
  import type { AdapterSurface as AdapterSurfaceLeaf } from '../adapter/adapter-surface';
15
10
  import type { RootBinding } from '../adapter/binding';
@@ -180,21 +175,20 @@ export interface RootInstance {
180
175
  * gain — see `AdapterHandle` below. */
181
176
  readonly adapter: AdapterHandle;
182
177
  /** The live mounted surface — one of `MountedRoot`'s kind-tagged shapes
183
- * (T7.5; `MountedThreeRoot` for a three world, `MountedPixiRoot` for
178
+ * (T7.5; `MountedThreeRoot` for a three world, `MountedCanvasRoot` for
184
179
  * canvas, `MountedReactRoot` for react). */
185
180
  readonly mounted: MountedRoot;
186
181
  /** Kind-narrowed accessor: throws a descriptive error when this world is
187
182
  * not a three world. */
188
183
  threeScene(): THREE.Scene;
189
- /** Kind-narrowed accessor for canvas roots (T7.3): returns the stage
190
- * passed to `createRootInstance` for a `'canvas'`-kind world. Throws
191
- * descriptively for a non-pixijs world, or a canvas world built without
192
- * a `stage` (see `RootInstanceInit.stage`). */
193
- pixiStage(): PIXI.Container;
184
+ /** Kind-narrowed accessor for canvas roots: returns the native substrate
185
+ * root the adapter mounted. The host keeps it opaque; a substrate adapter
186
+ * that declared the matching name narrows `T`. */
187
+ canvasRoot<T = unknown>(): T;
194
188
  /** Kind-narrowed accessor for DOM roots: returns the
195
189
  * DOM-root layer `<div>` the host mounted this world's react tree into
196
190
  * (the SAME element passed as `container` to `createRootInstance` —
197
- * identity matters, mirroring `threeScene()`/`pixiStage()`'s "same
191
+ * identity matters, mirroring `threeScene()`/`canvasRoot()`'s "same
198
192
  * instance the adapter mounted" contract). Throws descriptively for a
199
193
  * non-react world, or a react world built without a `container` (see
200
194
  * `RootInstanceInit.container`). */
@@ -275,9 +269,9 @@ export interface RootInstanceInit {
275
269
  readonly mounted: MountedRoot;
276
270
  /** Required when `kind === 'three'` — backs `threeScene()`. */
277
271
  readonly scene?: THREE.Scene | undefined;
278
- /** Required when `kind === 'canvas'` — backs `pixiStage()` (T7.3 slice 2,
279
- * symmetric with `scene` for three above). */
280
- readonly stage?: PIXI.Container | undefined;
272
+ /** Required when `kind === 'canvas'` — backs `canvasRoot()`, symmetric with
273
+ * `scene` for three above. */
274
+ readonly canvasRoot?: unknown;
281
275
  /** Required when `kind === 'dom'` — backs `reactRoot()` (T6.2 slice 1,
282
276
  * symmetric with `scene`/`stage` above): the DOM-root layer `<div>` this
283
277
  * world's react tree is mounted into. */
@@ -295,7 +289,7 @@ export interface RootInstanceInit {
295
289
  /**
296
290
  * Build a `RootInstance` whose kind-narrowed accessors throw descriptively
297
291
  * on kind mismatch. This is the one place that builds
298
- * `threeScene`/`pixiStage`, so every world (however it's constructed, in this
292
+ * `threeScene`/`canvasRoot`, so every world (however it's constructed, in this
299
293
  * slice or later ones) gets identical throw behavior.
300
294
  */
301
295
  export function createRootInstance(init: RootInstanceInit): RootInstance {
@@ -306,7 +300,7 @@ export function createRootInstance(init: RootInstanceInit): RootInstance {
306
300
  adapter,
307
301
  mounted,
308
302
  scene,
309
- stage,
303
+ canvasRoot,
310
304
  container,
311
305
  physics,
312
306
  collisions,
@@ -327,13 +321,13 @@ export function createRootInstance(init: RootInstanceInit): RootInstance {
327
321
  );
328
322
  }
329
323
 
330
- // Symmetric check for canvas (T7.3 slice 2) — a canvas world with no stage
331
- // has nothing for `pixiStage()` to return; fail loudly here, at
324
+ // Symmetric check for canvas — a canvas world with no native substrate root
325
+ // has nothing for `canvasRoot()` to return; fail loudly here, at
332
326
  // construction, same as the three/scene check above.
333
- if (kind === 'canvas' && !stage) {
327
+ if (kind === 'canvas' && (canvasRoot === undefined || canvasRoot === null)) {
334
328
  throw new Error(
335
- `RootInstance "${id}" (kind: canvas): a canvas world requires a stage — ` +
336
- 'pass `stage` in RootInstanceInit.',
329
+ `RootInstance "${id}" (kind: canvas): a canvas world requires a native substrate root — ` +
330
+ 'pass `canvasRoot` in RootInstanceInit.',
337
331
  );
338
332
  }
339
333
 
@@ -405,23 +399,20 @@ export function createRootInstance(init: RootInstanceInit): RootInstance {
405
399
  }
406
400
  return scene;
407
401
  },
408
- pixiStage(): PIXI.Container {
409
- // The `!stage` branch a canvas world could hit here is now unreachable
410
- // (T7.3 slice 2): construction above throws for `kind === 'canvas'`
411
- // with no `stage`, symmetric with `threeScene()`'s `scene` guard.
412
- if (kind !== 'canvas' || !stage) {
402
+ canvasRoot<T = unknown>(): T {
403
+ if (kind !== 'canvas' || canvasRoot === undefined || canvasRoot === null) {
413
404
  throw new Error(
414
- `RootInstance "${id}" (kind: ${kind}): pixiStage() requested but this world is not ` +
405
+ `RootInstance "${id}" (kind: ${kind}): canvasRoot() requested but this world is not ` +
415
406
  'a canvas world',
416
407
  );
417
408
  }
418
- return stage;
409
+ return canvasRoot as T;
419
410
  },
420
411
  reactRoot(): HTMLElement {
421
412
  // The `!container` branch a react world could hit here is now
422
413
  // unreachable (T6.2 slice 1): construction above throws for
423
414
  // `kind === 'dom'` with no `container`, symmetric with
424
- // `threeScene()`/`pixiStage()`'s guards.
415
+ // `threeScene()`/`canvasRoot()`'s guards.
425
416
  if (kind !== 'dom' || !container) {
426
417
  throw new Error(
427
418
  `RootInstance "${id}" (kind: ${kind}): reactRoot() requested but this world is not ` +
@@ -469,7 +469,12 @@ function countRoots(game: Game): PerfRootCount[] {
469
469
  if (world.kind === 'three') {
470
470
  counts.push({ id: world.id, kind: world.kind, ...countRootGraph(world.threeScene()) });
471
471
  } else if (world.kind === 'canvas') {
472
- counts.push({ id: world.id, kind: world.kind, ...countRootGraph(world.pixiStage()) });
472
+ // Node counting is a substrate projection, not a surface fact. Pixi's
473
+ // native tree exposes `children`; other canvas substrates are omitted
474
+ // until their adapter declares an honest counter.
475
+ if (world.mounted.kind === 'canvas' && world.mounted.substrate.name === 'pixi') {
476
+ counts.push({ id: world.id, kind: world.kind, ...countRootGraph(world.canvasRoot()) });
477
+ }
473
478
  }
474
479
  }
475
480
  return counts;