@vgai/engine 0.5.14 → 0.5.16
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 +228 -17
- package/dist/adapter/adapter-module.d.ts.map +1 -1
- package/dist/adapter/adapter-module.js +224 -42
- package/dist/adapter/authoring-seam-contract.d.ts +530 -0
- package/dist/adapter/authoring-seam-contract.d.ts.map +1 -0
- package/dist/adapter/authoring-seam-contract.js +162 -0
- package/dist/adapter/authoring.d.ts +237 -50
- package/dist/adapter/authoring.d.ts.map +1 -1
- package/dist/adapter/authoring.js +2 -1
- package/dist/adapter/entrypoint-selection-source.d.ts +35 -0
- package/dist/adapter/entrypoint-selection-source.d.ts.map +1 -0
- package/dist/adapter/entrypoint-selection-source.js +151 -0
- package/dist/adapter/finders/scenes-from-entrypoint-selection.d.ts +7 -1
- package/dist/adapter/finders/scenes-from-entrypoint-selection.d.ts.map +1 -1
- package/dist/adapter/finders/scenes-from-entrypoint-selection.js +142 -21
- package/dist/adapter/first-party-systems.d.ts +13 -3
- package/dist/adapter/first-party-systems.d.ts.map +1 -1
- package/dist/adapter/first-party-systems.js +10 -3
- package/dist/adapter/index.d.ts +7 -2
- package/dist/adapter/index.d.ts.map +1 -1
- package/dist/adapter/index.js +4 -0
- package/dist/adapter/ingest/contract-system-adapters.d.ts +1 -1
- package/dist/adapter/ingest/contract-system-adapters.d.ts.map +1 -1
- package/dist/adapter/ingest/contract-system-adapters.js +13 -25
- package/dist/adapter/ingest/game-contract-seams.d.ts +214 -0
- package/dist/adapter/ingest/game-contract-seams.d.ts.map +1 -0
- package/dist/adapter/ingest/game-contract-seams.js +62 -0
- package/dist/adapter/ingest/game-contract.d.ts +5 -3
- package/dist/adapter/ingest/game-contract.d.ts.map +1 -1
- package/dist/adapter/ingest/game-contract.js +4 -1
- package/dist/adapter/ingest/merge-debug-adapters.d.ts +52 -0
- package/dist/adapter/ingest/merge-debug-adapters.d.ts.map +1 -0
- package/dist/adapter/ingest/merge-debug-adapters.js +141 -0
- package/dist/adapter/ingest/observation-debug-adapter.d.ts +56 -0
- package/dist/adapter/ingest/observation-debug-adapter.d.ts.map +1 -0
- package/dist/adapter/ingest/observation-debug-adapter.js +131 -0
- package/dist/adapter/ingest/scene-capture.d.ts +38 -13
- package/dist/adapter/ingest/scene-capture.d.ts.map +1 -1
- package/dist/adapter/ingest/scene-capture.js +101 -24
- package/dist/adapter/ingest/visible-capture-window.d.ts +22 -5
- package/dist/adapter/ingest/visible-capture-window.d.ts.map +1 -1
- package/dist/adapter/ingest/visible-capture-window.js +46 -7
- package/dist/adapter/native-debug-module.d.ts +43 -0
- package/dist/adapter/native-debug-module.d.ts.map +1 -0
- package/dist/adapter/native-debug-module.js +230 -0
- package/dist/adapter/object3d-authoring-subject.d.ts +32 -0
- package/dist/adapter/object3d-authoring-subject.d.ts.map +1 -0
- package/dist/adapter/object3d-authoring-subject.js +25 -0
- package/dist/adapter/root-adapter.d.ts +21 -56
- package/dist/adapter/root-adapter.d.ts.map +1 -1
- package/dist/adapter/root-adapter.js +4 -20
- package/dist/adapter/root-seam-contract.d.ts +124 -0
- package/dist/adapter/root-seam-contract.d.ts.map +1 -0
- package/dist/adapter/root-seam-contract.js +36 -0
- package/dist/adapter/seam-evidence.d.ts +118 -0
- package/dist/adapter/seam-evidence.d.ts.map +1 -0
- package/dist/adapter/seam-evidence.js +231 -0
- package/dist/adapter/setup-three-root-adapter.d.ts +2 -3
- package/dist/adapter/setup-three-root-adapter.d.ts.map +1 -1
- package/dist/adapter/setup-three-root-adapter.js +2 -3
- package/dist/adapter/system-adapter.d.ts +6 -1
- package/dist/adapter/system-adapter.d.ts.map +1 -1
- package/dist/adapter/system-seam-contract.d.ts +354 -0
- package/dist/adapter/system-seam-contract.d.ts.map +1 -0
- package/dist/adapter/system-seam-contract.js +95 -0
- package/dist/animation/xstate-animation-binding.d.ts +6 -0
- package/dist/animation/xstate-animation-binding.d.ts.map +1 -1
- package/dist/animation/xstate-animation-binding.js +142 -4
- package/dist/asset-formats/collider.d.ts +1 -1
- package/dist/asset-formats/mesh.d.ts +2 -2
- package/dist/asset-formats/particles.d.ts +1 -1
- package/dist/canvas-react/engine-bridge.d.ts +6 -6
- package/dist/canvas-react/engine-bridge.d.ts.map +1 -1
- package/dist/canvas-react/engine-bridge.js +2 -2
- package/dist/canvas-react/index.d.ts +2 -2
- package/dist/canvas-react/index.d.ts.map +1 -1
- package/dist/canvas-react/index.js +1 -1
- package/dist/canvas-react/pixi-react-adapter.d.ts +15 -9
- package/dist/canvas-react/pixi-react-adapter.d.ts.map +1 -1
- package/dist/canvas-react/pixi-react-adapter.js +43 -29
- package/dist/canvas-react/pixi-react-root-factory.d.ts.map +1 -1
- package/dist/canvas-react/pixi-react-root-factory.js +4 -0
- package/dist/canvas-react/world-context.d.ts +22 -12
- package/dist/canvas-react/world-context.d.ts.map +1 -1
- package/dist/canvas-react/world-context.js +12 -51
- package/dist/ecs/user-data.d.ts +5 -0
- package/dist/ecs/user-data.d.ts.map +1 -1
- package/dist/ecs/user-data.js +3 -0
- package/dist/input/input-manager.d.ts +49 -0
- package/dist/input/input-manager.d.ts.map +1 -1
- package/dist/input/input-manager.js +127 -0
- package/dist/manifest/schema.d.ts +9 -9
- package/dist/manifest/schema.js +1 -1
- package/dist/pixi/authoring.d.ts.map +1 -1
- package/dist/pixi/authoring.js +220 -1
- package/dist/pixi/ingest.d.ts +2 -1
- package/dist/pixi/ingest.d.ts.map +1 -1
- package/dist/pixi/ingest.js +4 -2
- package/dist/pixi/scene-capture.d.ts +9 -1
- package/dist/pixi/scene-capture.d.ts.map +1 -1
- package/dist/pixi/scene-capture.js +9 -6
- package/dist/render/particles-factory.d.ts +13 -6
- package/dist/render/particles-factory.d.ts.map +1 -1
- package/dist/render/particles-factory.js +6 -3
- package/dist/runtime/create-runtime.d.ts +23 -46
- package/dist/runtime/create-runtime.d.ts.map +1 -1
- package/dist/runtime/create-runtime.js +22 -67
- package/dist/runtime/game.d.ts +29 -71
- package/dist/runtime/game.d.ts.map +1 -1
- package/dist/runtime/game.js +15 -54
- package/dist/runtime/mount-game.d.ts.map +1 -1
- package/dist/runtime/mount-game.js +12 -1
- package/dist/runtime/mount-manifest.d.ts.map +1 -1
- package/dist/runtime/mount-manifest.js +4 -5
- package/dist/runtime/types.d.ts +18 -0
- package/dist/runtime/types.d.ts.map +1 -1
- package/dist/setup/setup-renderer.d.ts +3 -4
- package/dist/setup/setup-renderer.d.ts.map +1 -1
- package/dist/setup/setup-renderer.js +3 -4
- package/dist/world3d-react/engine-bridge.d.ts +1 -1
- package/dist/world3d-react/engine-bridge.js +1 -1
- package/dist/world3d-react/r3f-adapter.d.ts +7 -0
- package/dist/world3d-react/r3f-adapter.d.ts.map +1 -1
- package/dist/world3d-react/r3f-adapter.js +50 -21
- package/dist/world3d-react/r3f-root-factory.d.ts.map +1 -1
- package/dist/world3d-react/r3f-root-factory.js +4 -0
- package/dist/world3d-react/world-context.d.ts +9 -3
- package/dist/world3d-react/world-context.d.ts.map +1 -1
- package/dist/world3d-react/world-context.js +5 -18
- package/package.json +1 -1
- package/schemas/engine-capabilities.json +8 -8
- package/schemas/vgai-project.schema.json +1 -1
- package/src/adapter/adapter-module.ts +407 -50
- package/src/adapter/authoring-seam-contract.ts +211 -0
- package/src/adapter/authoring.ts +242 -50
- package/src/adapter/entrypoint-selection-source.ts +159 -0
- package/src/adapter/finders/scenes-from-entrypoint-selection.ts +159 -28
- package/src/adapter/first-party-systems.ts +17 -3
- package/src/adapter/index.ts +51 -1
- package/src/adapter/ingest/contract-system-adapters.ts +26 -30
- package/src/adapter/ingest/game-contract-seams.ts +85 -0
- package/src/adapter/ingest/game-contract.ts +9 -4
- package/src/adapter/ingest/merge-debug-adapters.ts +193 -0
- package/src/adapter/ingest/observation-debug-adapter.ts +158 -0
- package/src/adapter/ingest/scene-capture.ts +126 -23
- package/src/adapter/ingest/visible-capture-window.ts +63 -9
- package/src/adapter/native-debug-module.ts +307 -0
- package/src/adapter/object3d-authoring-subject.ts +50 -0
- package/src/adapter/root-adapter.ts +21 -56
- package/src/adapter/root-seam-contract.ts +56 -0
- package/src/adapter/seam-evidence.ts +336 -0
- package/src/adapter/setup-three-root-adapter.ts +4 -6
- package/src/adapter/system-adapter.ts +6 -1
- package/src/adapter/system-seam-contract.ts +121 -0
- package/src/animation/xstate-animation-binding.ts +151 -4
- package/src/canvas-react/engine-bridge.ts +6 -6
- package/src/canvas-react/index.ts +2 -1
- package/src/canvas-react/pixi-react-adapter.tsx +55 -34
- package/src/canvas-react/pixi-react-root-factory.tsx +4 -0
- package/src/canvas-react/world-context.ts +26 -101
- package/src/ecs/user-data.ts +5 -0
- package/src/input/input-manager.ts +122 -0
- package/src/manifest/schema.ts +1 -1
- package/src/pixi/authoring.ts +236 -1
- package/src/pixi/ingest.ts +9 -2
- package/src/pixi/scene-capture.ts +20 -5
- package/src/render/particles-factory.ts +16 -6
- package/src/runtime/create-runtime.ts +41 -106
- package/src/runtime/game.ts +41 -111
- package/src/runtime/mount-game.ts +19 -1
- package/src/runtime/mount-manifest.ts +4 -5
- package/src/runtime/types.ts +22 -0
- package/src/setup/setup-renderer.ts +3 -4
- package/src/world3d-react/engine-bridge.ts +1 -1
- package/src/world3d-react/r3f-adapter.tsx +64 -24
- package/src/world3d-react/r3f-root-factory.tsx +4 -0
- package/src/world3d-react/world-context.ts +11 -36
|
@@ -4,7 +4,10 @@ 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 {
|
|
7
|
+
import {
|
|
8
|
+
type AnimationPreviewTransport,
|
|
9
|
+
attachAnimationRuntimeInspection,
|
|
10
|
+
} from './runtime-inspection';
|
|
8
11
|
import {
|
|
9
12
|
type AnimationBoneMask,
|
|
10
13
|
type AnimationMetaStateNodeLike,
|
|
@@ -109,6 +112,12 @@ export interface XStateAnimationBindingOptions {
|
|
|
109
112
|
* omitted, the caller owns ticking (useful for standalone Three tests).
|
|
110
113
|
*/
|
|
111
114
|
systems?: Pick<ReturnType<typeof createSystemRunner>, 'add' | 'remove'>;
|
|
115
|
+
/**
|
|
116
|
+
* Publish a seekable transport for an isolated design-time mount such as a
|
|
117
|
+
* CSF story. Never enable this on the live game binding: runtime inspection
|
|
118
|
+
* stays read-only there, while the story deliberately owns this preview.
|
|
119
|
+
*/
|
|
120
|
+
previewTransport?: boolean;
|
|
112
121
|
}
|
|
113
122
|
|
|
114
123
|
export interface XStateAnimationLayerState {
|
|
@@ -368,6 +377,89 @@ export function bindXStateAnimation(
|
|
|
368
377
|
const activeLayers = new Map<string, ActiveState>();
|
|
369
378
|
let disposed = false;
|
|
370
379
|
let detachRuntimeInspection = () => {};
|
|
380
|
+
let previewClip: string | null = null;
|
|
381
|
+
let previewAction: THREE.AnimationAction | null = null;
|
|
382
|
+
let previewPlaying = true;
|
|
383
|
+
let previewLoop = true;
|
|
384
|
+
let previewSpeed = 1;
|
|
385
|
+
|
|
386
|
+
function controllerPreviewSubject(): {
|
|
387
|
+
clip: string;
|
|
388
|
+
action: THREE.AnimationAction | null;
|
|
389
|
+
} {
|
|
390
|
+
const layers = [activeLayers.get('base'), ...activeLayers.values()].filter(
|
|
391
|
+
(active, index, all): active is ActiveState =>
|
|
392
|
+
Boolean(active) && all.indexOf(active) === index,
|
|
393
|
+
);
|
|
394
|
+
for (const active of layers) {
|
|
395
|
+
const weighted = [...active.natural].sort((left, right) => {
|
|
396
|
+
const leftWeight = actionCache.get(left.actionKey)?.action.getEffectiveWeight() ?? 0;
|
|
397
|
+
const rightWeight = actionCache.get(right.actionKey)?.action.getEffectiveWeight() ?? 0;
|
|
398
|
+
return rightWeight - leftWeight;
|
|
399
|
+
})[0];
|
|
400
|
+
if (weighted) {
|
|
401
|
+
return {
|
|
402
|
+
clip: weighted.clip,
|
|
403
|
+
action: actionCache.get(weighted.actionKey)?.action ?? null,
|
|
404
|
+
};
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
return { clip: clips.keys().next().value ?? '', action: null };
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
function actionIsControllerOwned(action: THREE.AnimationAction): boolean {
|
|
411
|
+
for (const cached of actionCache.values()) {
|
|
412
|
+
if (cached.action === action) return true;
|
|
413
|
+
}
|
|
414
|
+
return false;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
function releasePreviewAction(): void {
|
|
418
|
+
const action = previewAction;
|
|
419
|
+
if (!action) return;
|
|
420
|
+
action.stop();
|
|
421
|
+
if (!actionIsControllerOwned(action)) {
|
|
422
|
+
mixer.uncacheAction(action.getClip(), action.getRoot());
|
|
423
|
+
}
|
|
424
|
+
previewAction = null;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
function enterClipPreview(name: string): void {
|
|
428
|
+
const clip = clips.get(name);
|
|
429
|
+
if (!clip) return;
|
|
430
|
+
releasePreviewAction();
|
|
431
|
+
previewClip = name;
|
|
432
|
+
for (const cached of actionCache.values()) cached.action.setEffectiveWeight(0);
|
|
433
|
+
const action = mixer.clipAction(clip).reset().play();
|
|
434
|
+
action.setEffectiveWeight(1);
|
|
435
|
+
action.setEffectiveTimeScale(1);
|
|
436
|
+
action.setLoop(previewLoop ? THREE.LoopRepeat : THREE.LoopOnce, previewLoop ? Infinity : 1);
|
|
437
|
+
action.clampWhenFinished = !previewLoop;
|
|
438
|
+
previewAction = action;
|
|
439
|
+
mixer.update(0);
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
function restoreControllerPreview(): void {
|
|
443
|
+
releasePreviewAction();
|
|
444
|
+
previewClip = null;
|
|
445
|
+
const snapshot = actor.getSnapshot();
|
|
446
|
+
reconcile(snapshot);
|
|
447
|
+
const context = snapshot.context;
|
|
448
|
+
for (const cached of actionCache.values()) cached.action.setEffectiveWeight(0);
|
|
449
|
+
for (const active of activeLayers.values()) {
|
|
450
|
+
active.crossfadeRemaining = 0;
|
|
451
|
+
active.natural = weightsOf(active.stateId, active.meta, context);
|
|
452
|
+
const loop = isBlendTreeAnimationMeta(active.meta) ? true : active.meta.loop;
|
|
453
|
+
const speed = isBlendTreeAnimationMeta(active.meta) ? 1 : active.meta.speed;
|
|
454
|
+
const layerWeight = layerWeightOf(active.meta);
|
|
455
|
+
for (const weighted of active.natural) {
|
|
456
|
+
ensureAction(weighted.actionKey, loop, speed, layerWeight)
|
|
457
|
+
.play()
|
|
458
|
+
.setEffectiveWeight(weighted.weight * layerWeight);
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
mixer.update(0);
|
|
462
|
+
}
|
|
371
463
|
|
|
372
464
|
function silence(weights: WeightedClip[], except: string | undefined, duration: number): void {
|
|
373
465
|
for (const weighted of weights) {
|
|
@@ -465,23 +557,76 @@ export function bindXStateAnimation(
|
|
|
465
557
|
const initialSnapshot = actor.getSnapshot();
|
|
466
558
|
reconcile(initialSnapshot);
|
|
467
559
|
const subscription = actor.subscribe((snapshot) => {
|
|
468
|
-
|
|
560
|
+
// Isolated clip review deliberately holds behavior output. The actor may
|
|
561
|
+
// keep receiving story args, but its latest state is reconciled only when
|
|
562
|
+
// the user returns to Controller.
|
|
563
|
+
if (!disposed && !previewClip) reconcile(snapshot);
|
|
469
564
|
});
|
|
470
565
|
|
|
471
566
|
function tick(dt: number): void {
|
|
472
567
|
if (disposed) return;
|
|
568
|
+
if (options.previewTransport && !previewPlaying) return;
|
|
569
|
+
const elapsed = dt * (options.previewTransport ? previewSpeed : 1);
|
|
570
|
+
if (options.previewTransport && previewClip && previewAction) {
|
|
571
|
+
mixer.update(elapsed);
|
|
572
|
+
const clip = clips.get(previewClip);
|
|
573
|
+
if (!previewLoop && clip && previewAction.time >= clip.duration) previewPlaying = false;
|
|
574
|
+
return;
|
|
575
|
+
}
|
|
473
576
|
const context = actor.getSnapshot().context;
|
|
474
577
|
for (const active of activeLayers.values()) {
|
|
475
578
|
if (active.crossfadeRemaining > 0) {
|
|
476
|
-
active.crossfadeRemaining = Math.max(0, active.crossfadeRemaining -
|
|
579
|
+
active.crossfadeRemaining = Math.max(0, active.crossfadeRemaining - elapsed);
|
|
477
580
|
if (active.crossfadeRemaining <= 0) applyLiveBlendWeights(active, context);
|
|
478
581
|
} else {
|
|
479
582
|
applyLiveBlendWeights(active, context);
|
|
480
583
|
}
|
|
481
584
|
}
|
|
482
|
-
mixer.update(
|
|
585
|
+
mixer.update(elapsed);
|
|
483
586
|
}
|
|
484
587
|
|
|
588
|
+
const previewTransport: AnimationPreviewTransport | undefined = options.previewTransport
|
|
589
|
+
? {
|
|
590
|
+
getSnapshot: () => {
|
|
591
|
+
const controller = controllerPreviewSubject();
|
|
592
|
+
return {
|
|
593
|
+
clip: previewClip ?? controller.clip,
|
|
594
|
+
time: previewClip ? (previewAction?.time ?? 0) : (controller.action?.time ?? 0),
|
|
595
|
+
playing: previewPlaying,
|
|
596
|
+
loop: previewLoop,
|
|
597
|
+
speed: previewSpeed,
|
|
598
|
+
mode: previewClip ? 'clip' : 'controller',
|
|
599
|
+
};
|
|
600
|
+
},
|
|
601
|
+
setPlaying(playing) {
|
|
602
|
+
previewPlaying = playing;
|
|
603
|
+
},
|
|
604
|
+
setLoop(loop) {
|
|
605
|
+
previewLoop = loop;
|
|
606
|
+
if (!previewClip) enterClipPreview(controllerPreviewSubject().clip);
|
|
607
|
+
previewAction?.setLoop(loop ? THREE.LoopRepeat : THREE.LoopOnce, loop ? Infinity : 1);
|
|
608
|
+
if (previewAction) previewAction.clampWhenFinished = !loop;
|
|
609
|
+
},
|
|
610
|
+
setSpeed(speed) {
|
|
611
|
+
previewSpeed = THREE.MathUtils.clamp(speed, 0.05, 4);
|
|
612
|
+
},
|
|
613
|
+
seek(time) {
|
|
614
|
+
if (!previewClip) enterClipPreview(controllerPreviewSubject().clip);
|
|
615
|
+
const clip = previewClip ? clips.get(previewClip) : undefined;
|
|
616
|
+
if (!clip || !previewAction) return;
|
|
617
|
+
previewAction.time = THREE.MathUtils.clamp(time, 0, Math.max(clip.duration, 0));
|
|
618
|
+
mixer.update(0);
|
|
619
|
+
},
|
|
620
|
+
setClip(name) {
|
|
621
|
+
if (name === null) {
|
|
622
|
+
if (previewClip) restoreControllerPreview();
|
|
623
|
+
return;
|
|
624
|
+
}
|
|
625
|
+
enterClipPreview(name);
|
|
626
|
+
},
|
|
627
|
+
}
|
|
628
|
+
: undefined;
|
|
629
|
+
|
|
485
630
|
const binding: XStateAnimationBinding = {
|
|
486
631
|
tick,
|
|
487
632
|
actor,
|
|
@@ -503,6 +648,7 @@ export function bindXStateAnimation(
|
|
|
503
648
|
if (disposed) return;
|
|
504
649
|
disposed = true;
|
|
505
650
|
detachRuntimeInspection();
|
|
651
|
+
releasePreviewAction();
|
|
506
652
|
options.systems?.remove('animation', tick);
|
|
507
653
|
subscription.unsubscribe();
|
|
508
654
|
for (const { action, clip } of actionCache.values()) {
|
|
@@ -520,6 +666,7 @@ export function bindXStateAnimation(
|
|
|
520
666
|
detachRuntimeInspection = attachAnimationRuntimeInspection(owner, {
|
|
521
667
|
mixer,
|
|
522
668
|
clips,
|
|
669
|
+
...(previewTransport ? { previewTransport } : {}),
|
|
523
670
|
getSnapshot: () => ({
|
|
524
671
|
layers: [...activeLayers.values()].map((active) => {
|
|
525
672
|
const parameters = readParameters(actor.getSnapshot().context);
|
|
@@ -14,20 +14,20 @@
|
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
16
|
import { createContext, useContext } from 'react';
|
|
17
|
-
import type {
|
|
17
|
+
import type { CanvasGameContext } from './world-context';
|
|
18
18
|
|
|
19
19
|
export interface EngineBridgeValue {
|
|
20
|
-
/** This root's `
|
|
20
|
+
/** This root's `CanvasGameContext` — `input`, `debug`, `random`, `clock`, `game`,
|
|
21
21
|
* `roots`, `systems`, `registerSystemAdapter`. Present so ordinary
|
|
22
22
|
* components can reach engine services with a HOOK (see
|
|
23
23
|
* {@link useGameContext}). */
|
|
24
|
-
readonly ctx?:
|
|
24
|
+
readonly ctx?: CanvasGameContext | undefined;
|
|
25
25
|
}
|
|
26
26
|
|
|
27
27
|
export const EngineBridge = createContext<EngineBridgeValue | null>(null);
|
|
28
28
|
|
|
29
29
|
/**
|
|
30
|
-
* This canvas root's `
|
|
30
|
+
* This canvas root's `CanvasGameContext`, for use inside any component in the tree.
|
|
31
31
|
*
|
|
32
32
|
* An ordinary component does what a model would write:
|
|
33
33
|
*
|
|
@@ -36,7 +36,7 @@ export const EngineBridge = createContext<EngineBridgeValue | null>(null);
|
|
|
36
36
|
* const { input } = useGameContext();
|
|
37
37
|
* const ref = useRef<Sprite>(null);
|
|
38
38
|
* useTick(({ deltaMS }) => {
|
|
39
|
-
* if (input
|
|
39
|
+
* if (input?.isPressed('move_right')) ref.current!.x += speed * (deltaMS / 1000);
|
|
40
40
|
* });
|
|
41
41
|
* return <pixiSprite ref={ref} texture={Texture.WHITE} />;
|
|
42
42
|
* }
|
|
@@ -46,7 +46,7 @@ export const EngineBridge = createContext<EngineBridgeValue | null>(null);
|
|
|
46
46
|
* `createPixiReactAdapter` — absent context is a wiring bug, and a silent
|
|
47
47
|
* `null` would surface as a mystery `undefined` deep in a tick callback.
|
|
48
48
|
*/
|
|
49
|
-
export function useGameContext():
|
|
49
|
+
export function useGameContext(): CanvasGameContext {
|
|
50
50
|
const bridge = useContext(EngineBridge);
|
|
51
51
|
if (!bridge?.ctx) {
|
|
52
52
|
throw new Error(
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
* - `PixiPrimitive` / `adoptNow` — the canvas surface's `<primitive object={…}>`:
|
|
23
23
|
* render a container the game already owns, and place a spawn in the
|
|
24
24
|
* display tree ahead of the commit that renders it.
|
|
25
|
-
* - `useGameContext()` — this root's `
|
|
25
|
+
* - `useGameContext()` — this root's `CanvasGameContext` (`input`, `debug`,
|
|
26
26
|
* `random`, `clock`, `game`, `roots`) from inside any component in the tree.
|
|
27
27
|
* Behavior in a canvas world is components + hooks, never a class under a
|
|
28
28
|
* string key.
|
|
@@ -43,6 +43,7 @@ export {
|
|
|
43
43
|
} from './pixi-react-adapter';
|
|
44
44
|
export { pixiReactRootFactory, resolveCanvasEntryAdapter } from './pixi-react-root-factory';
|
|
45
45
|
export {
|
|
46
|
+
type CanvasGameContext,
|
|
46
47
|
type CanvasRootContextOptions,
|
|
47
48
|
type CanvasRootRuntime,
|
|
48
49
|
createCanvasRootContext,
|
|
@@ -6,20 +6,20 @@
|
|
|
6
6
|
* this returns a `MountedPixiRoot` (`../adapter/root-adapter.ts`) whose `stage`
|
|
7
7
|
* is the REAL `PIXI.Container` the reconciler renders into.
|
|
8
8
|
*
|
|
9
|
-
* This is the canvas surface's
|
|
9
|
+
* This is the canvas surface's sibling of `world3d-react`'s
|
|
10
10
|
* `createR3FAdapter`, with `@pixi/react` where that one has
|
|
11
|
-
* react-three-fiber.
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
11
|
+
* react-three-fiber. The explicit factory still supplies the engine context
|
|
12
|
+
* used by the current first-party Canvas examples while their app-owned input
|
|
13
|
+
* and debugger migration remains tracked in `docs/WORK.md`; `useTick` hooks
|
|
14
|
+
* stay registered on Pixi's real ticker throughout that migration.
|
|
15
15
|
*
|
|
16
16
|
* ## The loop contract
|
|
17
17
|
*
|
|
18
18
|
* `Application` is initialized with `autoStart: false` and `sharedTicker:
|
|
19
|
-
* false`, so it owns a ticker that is NEVER started and therefore
|
|
20
|
-
* requests an animation frame. `mounted.update(dt)` is the only thing
|
|
21
|
-
* advances
|
|
22
|
-
* world freeze exactly when the host freezes it, and what makes
|
|
19
|
+
* false`, so it owns a native Pixi ticker that is NEVER started and therefore
|
|
20
|
+
* never requests an animation frame. `mounted.update(dt)` is the only thing
|
|
21
|
+
* that advances that ticker, and it feeds GAME time, not wall time — which is
|
|
22
|
+
* what makes the world freeze exactly when the host freezes it, and what makes
|
|
23
23
|
* `game.waitSimTime` work in a hidden tab (a hidden tab has no rAF; the engine
|
|
24
24
|
* loop's own catch-up still calls `update`, and Pixi has no independent clock
|
|
25
25
|
* to disagree with it).
|
|
@@ -38,9 +38,11 @@
|
|
|
38
38
|
import { createRoot, extend } from '@pixi/react';
|
|
39
39
|
import type { Application, ApplicationOptions } from 'pixi.js';
|
|
40
40
|
import * as PIXI from 'pixi.js';
|
|
41
|
-
import { createElement, type ReactNode, useLayoutEffect } from 'react';
|
|
41
|
+
import { createElement, Fragment, type ReactNode, useEffect, useLayoutEffect } from 'react';
|
|
42
42
|
import type { CanvasHostContext, MountedPixiRoot, RootAdapter } from '../adapter';
|
|
43
|
+
import type { SystemAdapters } from '../adapter/system-adapter';
|
|
43
44
|
import { WorldProvider } from '../react/world-state';
|
|
45
|
+
import { getDebugRegistry } from '../runtime/debug-registry';
|
|
44
46
|
import { EngineBridge, type EngineBridgeValue } from './engine-bridge';
|
|
45
47
|
import { createCanvasRootContext } from './world-context';
|
|
46
48
|
|
|
@@ -77,6 +79,12 @@ export interface CreatePixiReactAdapterOptions {
|
|
|
77
79
|
* actions). A missing/invalid map degrades loudly without failing the
|
|
78
80
|
* mount — see `world-context.ts`. */
|
|
79
81
|
readonly inputMapPath?: string | null | undefined;
|
|
82
|
+
/**
|
|
83
|
+
* Transitional compatibility for component trees that still consume the
|
|
84
|
+
* engine context. Native trees opt out once their input, observation and
|
|
85
|
+
* debugger state are application-owned.
|
|
86
|
+
*/
|
|
87
|
+
readonly engineRuntime?: boolean | undefined;
|
|
80
88
|
/**
|
|
81
89
|
* Extra `Application.init` options merged UNDER this adapter's own — e.g.
|
|
82
90
|
* `{ antialias: false }`, `{ roundPixels: true }`. The loop-critical ones
|
|
@@ -98,21 +106,21 @@ export interface CreatePixiReactAdapterOptions {
|
|
|
98
106
|
export function createPixiReactAdapter(
|
|
99
107
|
options: CreatePixiReactAdapterOptions,
|
|
100
108
|
): RootAdapter<'canvas'> {
|
|
101
|
-
const { id, content, inputMapPath, applicationOptions } = options;
|
|
109
|
+
const { id, content, inputMapPath, engineRuntime = true, applicationOptions } = options;
|
|
102
110
|
|
|
103
111
|
return {
|
|
104
112
|
id,
|
|
105
113
|
|
|
106
114
|
async mount(host: CanvasHostContext): Promise<MountedPixiRoot> {
|
|
107
|
-
const runtime = createCanvasRootContext(host, { id, inputMapPath });
|
|
108
|
-
const bridge: EngineBridgeValue = { ctx: runtime.ctx };
|
|
115
|
+
const runtime = engineRuntime ? createCanvasRootContext(host, { id, inputMapPath }) : null;
|
|
116
|
+
const bridge: EngineBridgeValue | null = runtime ? { ctx: runtime.ctx } : null;
|
|
109
117
|
// Actions must exist before any component's first tick reads them — wait
|
|
110
118
|
// for the (never-rejecting) input-map load before the first commit.
|
|
111
|
-
await runtime
|
|
119
|
+
await runtime?.inputMapReady;
|
|
112
120
|
|
|
113
121
|
const canvas = host.canvas;
|
|
114
122
|
if (MOUNTED_CANVASES.has(canvas)) {
|
|
115
|
-
runtime
|
|
123
|
+
runtime?.dispose();
|
|
116
124
|
throw new Error(
|
|
117
125
|
`createPixiReactAdapter: root "${id}" was handed a canvas this lane has already ` +
|
|
118
126
|
'mounted. @pixi/react keys its reconciler roots off the canvas element, so re-using ' +
|
|
@@ -129,6 +137,10 @@ export function createPixiReactAdapter(
|
|
|
129
137
|
const committed = new Promise<void>((resolve) => {
|
|
130
138
|
resolveCommitted = resolve;
|
|
131
139
|
});
|
|
140
|
+
let resolveEffectsReady!: () => void;
|
|
141
|
+
const effectsReady = new Promise<void>((resolve) => {
|
|
142
|
+
resolveEffectsReady = resolve;
|
|
143
|
+
});
|
|
132
144
|
let resolveUnmounted!: () => void;
|
|
133
145
|
const unmounted = new Promise<void>((resolve) => {
|
|
134
146
|
resolveUnmounted = resolve;
|
|
@@ -152,22 +164,28 @@ export function createPixiReactAdapter(
|
|
|
152
164
|
resolveCommitted();
|
|
153
165
|
return resolveUnmounted;
|
|
154
166
|
}, []);
|
|
167
|
+
// This component is the last sibling. Reaching its passive effect
|
|
168
|
+
// means the initial tree's useDebugProvider/useDebugCommand effects
|
|
169
|
+
// have registered, so mount() can truthfully back Play's ack.
|
|
170
|
+
useEffect(resolveEffectsReady, []);
|
|
155
171
|
return null;
|
|
156
172
|
}
|
|
157
173
|
|
|
158
|
-
const game = runtime
|
|
174
|
+
const game = runtime?.ctx.game;
|
|
159
175
|
// The React seam every dom root already has — `useWorldState`,
|
|
160
176
|
// `useDebugProvider`/`useDebugCommand`/`useDebugEmit`
|
|
161
177
|
// (`../react/world-state`) — works identically inside a canvas root
|
|
162
178
|
// because a canvas root IS React. `host.game` is genuinely absent in
|
|
163
179
|
// bare/foreign hosts, so the provider is conditional and those hooks
|
|
164
180
|
// stay the inert no-ops they already document themselves to be.
|
|
165
|
-
const
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
181
|
+
const world = createElement(Fragment, null, content, createElement(Lifecycle));
|
|
182
|
+
const element = bridge
|
|
183
|
+
? createElement(
|
|
184
|
+
EngineBridge.Provider,
|
|
185
|
+
{ value: bridge },
|
|
186
|
+
game ? createElement(WorldProvider, { game }, world) : world,
|
|
187
|
+
)
|
|
188
|
+
: world;
|
|
171
189
|
|
|
172
190
|
const root: PixiReactRoot = createRoot(canvas);
|
|
173
191
|
|
|
@@ -193,7 +211,7 @@ export function createPixiReactAdapter(
|
|
|
193
211
|
// creates the renderer) and only then hands the tree to the reconciler.
|
|
194
212
|
// A crash inside init rejects here, which is the loud failure we want.
|
|
195
213
|
const app: Application = await root.render(element, initOptions);
|
|
196
|
-
runtime
|
|
214
|
+
runtime?.setApplication(app);
|
|
197
215
|
|
|
198
216
|
// A reconcile-time crash (e.g. an un-extended component) surfaces as an
|
|
199
217
|
// uncaught window error and the first commit never lands — without this
|
|
@@ -207,7 +225,7 @@ export function createPixiReactAdapter(
|
|
|
207
225
|
} catch {
|
|
208
226
|
/* the world never fully came up; teardown failure must not mask why */
|
|
209
227
|
}
|
|
210
|
-
runtime
|
|
228
|
+
runtime?.dispose();
|
|
211
229
|
reject(new Error(`createPixiReactAdapter: root "${id}" — ${why}`));
|
|
212
230
|
};
|
|
213
231
|
const onError = (event: ErrorEvent) =>
|
|
@@ -228,7 +246,7 @@ export function createPixiReactAdapter(
|
|
|
228
246
|
globalThis.removeEventListener?.('error', onError);
|
|
229
247
|
};
|
|
230
248
|
globalThis.addEventListener?.('error', onError);
|
|
231
|
-
void committed.then(() => {
|
|
249
|
+
void Promise.all([committed, effectsReady]).then(() => {
|
|
232
250
|
cleanup();
|
|
233
251
|
resolve();
|
|
234
252
|
});
|
|
@@ -243,6 +261,9 @@ export function createPixiReactAdapter(
|
|
|
243
261
|
app.ticker.lastTime = 0;
|
|
244
262
|
let elapsedMs = 0;
|
|
245
263
|
let disposed = false;
|
|
264
|
+
const registry = host.game ? getDebugRegistry(host.game) : null;
|
|
265
|
+
const systemAdapters: SystemAdapters =
|
|
266
|
+
runtime?.systemAdapters ?? (registry ? { debug: registry.adapter } : {});
|
|
246
267
|
|
|
247
268
|
return {
|
|
248
269
|
kind: 'canvas',
|
|
@@ -250,17 +271,17 @@ export function createPixiReactAdapter(
|
|
|
250
271
|
// `RootInstance.pixiStage()` hands the editor.
|
|
251
272
|
stage: app.stage,
|
|
252
273
|
drivesOwnLoop: false,
|
|
253
|
-
// Adapter surface:
|
|
254
|
-
//
|
|
255
|
-
|
|
274
|
+
// Adapter surface: the host's shared debug registry. A native component
|
|
275
|
+
// tree registers nothing; its entry-module `debug` export is projected
|
|
276
|
+
// after mount by the shared native adapter.
|
|
277
|
+
systems: systemAdapters,
|
|
256
278
|
update(dt: number): void {
|
|
257
279
|
if (disposed) return;
|
|
258
280
|
elapsedMs += dt * 1000;
|
|
259
|
-
//
|
|
260
|
-
// the
|
|
261
|
-
//
|
|
262
|
-
|
|
263
|
-
runtime.systems.run(dt);
|
|
281
|
+
// The optional legacy runtime drains first when explicitly enabled;
|
|
282
|
+
// the native path has none. The real Pixi ticker remains the one
|
|
283
|
+
// scheduler `useTick` observes, and the host only gates/advances it.
|
|
284
|
+
runtime?.systems.run(dt);
|
|
264
285
|
app.ticker.update(elapsedMs);
|
|
265
286
|
},
|
|
266
287
|
resize(width: number, height: number): void {
|
|
@@ -282,7 +303,7 @@ export function createPixiReactAdapter(
|
|
|
282
303
|
// `removeView` stays false — the canvas belongs to the HOST.
|
|
283
304
|
void root.render(null, initOptions);
|
|
284
305
|
void unmounted.then(() => app.destroy(false, { children: true }));
|
|
285
|
-
runtime
|
|
306
|
+
runtime?.dispose();
|
|
286
307
|
},
|
|
287
308
|
};
|
|
288
309
|
},
|
|
@@ -66,6 +66,10 @@ export function resolveCanvasEntryAdapter(
|
|
|
66
66
|
return createPixiReactAdapter({
|
|
67
67
|
id: rootId,
|
|
68
68
|
content: createElement(mod.default),
|
|
69
|
+
// The supported Canvas portfolio still consumes the compatibility
|
|
70
|
+
// context. Removing it belongs to the tracked component-root migration;
|
|
71
|
+
// disabling it here first made every current default-exported game fail
|
|
72
|
+
// to mount before that migration had happened.
|
|
69
73
|
});
|
|
70
74
|
}
|
|
71
75
|
return null;
|