@vgai/engine 0.5.15 → 0.5.17
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 +229 -33
- package/dist/adapter/adapter-module.d.ts.map +1 -1
- package/dist/adapter/adapter-module.js +222 -50
- 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 -3
- package/dist/adapter/finders/scenes-from-entrypoint-selection.d.ts.map +1 -1
- package/dist/adapter/finders/scenes-from-entrypoint-selection.js +145 -27
- 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 +25 -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 +129 -0
- package/dist/adapter/root-seam-contract.d.ts.map +1 -0
- package/dist/adapter/root-seam-contract.js +37 -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 +11 -12
- 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 +129 -64
- 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/data/curve.d.ts +1 -1
- package/dist/data/curve.js +2 -2
- package/dist/dev/instruments.d.ts +4 -4
- package/dist/dev/instruments.js +4 -4
- package/dist/dev/register-render-vitals.d.ts +2 -2
- package/dist/dev/register-render-vitals.js +2 -2
- package/dist/dev/static-batch-advisor.d.ts +1 -1
- package/dist/dev/static-batch-advisor.js +2 -2
- 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 +64 -0
- package/dist/input/input-manager.d.ts.map +1 -1
- package/dist/input/input-manager.js +149 -0
- package/dist/manifest/load.d.ts +0 -3
- package/dist/manifest/load.d.ts.map +1 -1
- package/dist/manifest/load.js +0 -4
- package/dist/manifest/schema.d.ts +9 -11
- package/dist/manifest/schema.d.ts.map +1 -1
- package/dist/manifest/schema.js +2 -23
- package/dist/pixi/authoring.d.ts +33 -0
- package/dist/pixi/authoring.d.ts.map +1 -1
- package/dist/pixi/authoring.js +301 -2
- 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/react/world-state.d.ts +12 -0
- package/dist/react/world-state.d.ts.map +1 -1
- package/dist/react/world-state.js +14 -0
- 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/render/soft-particle-depth.d.ts +3 -3
- package/dist/render/soft-particle-depth.d.ts.map +1 -1
- package/dist/render/soft-particle-depth.js +8 -13
- package/dist/runtime/create-runtime.d.ts +25 -62
- package/dist/runtime/create-runtime.d.ts.map +1 -1
- package/dist/runtime/create-runtime.js +58 -105
- package/dist/runtime/dev-build.d.ts +37 -0
- package/dist/runtime/dev-build.d.ts.map +1 -0
- package/dist/runtime/dev-build.js +49 -0
- package/dist/runtime/game.d.ts +38 -72
- package/dist/runtime/game.d.ts.map +1 -1
- package/dist/runtime/game.js +40 -55
- 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 +0 -13
- package/dist/runtime/mount-manifest.d.ts.map +1 -1
- package/dist/runtime/mount-manifest.js +5 -25
- 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.d.ts.map +1 -1
- package/dist/world3d-react/engine-bridge.js +29 -4
- package/dist/world3d-react/index.d.ts +1 -1
- package/dist/world3d-react/index.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 +132 -28
- package/dist/world3d-react/r3f-root-factory.d.ts.map +1 -1
- package/dist/world3d-react/r3f-root-factory.js +14 -0
- package/dist/world3d-react/rapier-physics-bridge.d.ts +4 -4
- package/dist/world3d-react/rapier-physics-bridge.js +3 -3
- package/dist/world3d-react/world-context.d.ts +52 -3
- package/dist/world3d-react/world-context.d.ts.map +1 -1
- package/dist/world3d-react/world-context.js +94 -64
- package/dist-config/config.js.map +1 -1
- package/package.json +1 -1
- package/schemas/engine-capabilities.json +8 -8
- package/schemas/vgai-project.schema.json +1 -6
- package/src/adapter/adapter-module.ts +405 -72
- 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 +162 -36
- 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 +25 -56
- package/src/adapter/root-seam-contract.ts +57 -0
- package/src/adapter/seam-evidence.ts +336 -0
- package/src/adapter/setup-three-root-adapter.ts +13 -15
- 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 +143 -77
- package/src/canvas-react/pixi-react-root-factory.tsx +4 -0
- package/src/canvas-react/world-context.ts +26 -101
- package/src/data/curve.ts +2 -2
- package/src/dev/instruments.ts +4 -4
- package/src/dev/register-render-vitals.ts +2 -2
- package/src/dev/static-batch-advisor.ts +2 -2
- package/src/ecs/user-data.ts +5 -0
- package/src/input/input-manager.ts +145 -0
- package/src/manifest/load.ts +0 -7
- package/src/manifest/schema.ts +2 -25
- package/src/pixi/authoring.ts +329 -2
- package/src/pixi/ingest.ts +9 -2
- package/src/pixi/scene-capture.ts +20 -5
- package/src/react/world-state.tsx +15 -0
- package/src/render/particles-factory.ts +16 -6
- package/src/render/soft-particle-depth.ts +8 -13
- package/src/runtime/create-runtime.ts +79 -158
- package/src/runtime/dev-build.ts +49 -0
- package/src/runtime/game.ts +72 -113
- package/src/runtime/mount-game.ts +19 -1
- package/src/runtime/mount-manifest.ts +5 -37
- package/src/runtime/types.ts +22 -0
- package/src/setup/setup-renderer.ts +3 -4
- package/src/world3d-react/engine-bridge.ts +29 -4
- package/src/world3d-react/index.ts +1 -1
- package/src/world3d-react/r3f-adapter.tsx +140 -31
- package/src/world3d-react/r3f-root-factory.tsx +14 -0
- package/src/world3d-react/rapier-physics-bridge.tsx +4 -4
- package/src/world3d-react/world-context.ts +111 -87
- package/dist/runtime/dev-layers.d.ts +0 -34
- package/dist/runtime/dev-layers.d.ts.map +0 -1
- package/dist/runtime/dev-layers.js +0 -46
- package/src/runtime/dev-layers.ts +0 -46
package/src/pixi/authoring.ts
CHANGED
|
@@ -44,11 +44,264 @@ export type Overlay2D = Record<string, Override2D>;
|
|
|
44
44
|
function kindOf(o: Container): string {
|
|
45
45
|
const ctor = o.constructor?.name?.toLowerCase() ?? 'container';
|
|
46
46
|
if (ctor.includes('animatedsprite')) return 'animatedsprite';
|
|
47
|
+
if (ctor.includes('nineslice')) return 'nineslicesprite';
|
|
48
|
+
if (ctor.includes('tiling')) return 'tilingsprite';
|
|
47
49
|
if (ctor.includes('sprite')) return 'sprite';
|
|
48
50
|
if (ctor.includes('text')) return 'text';
|
|
51
|
+
if (ctor.includes('graphics')) return 'graphics';
|
|
49
52
|
return 'container';
|
|
50
53
|
}
|
|
51
54
|
|
|
55
|
+
/**
|
|
56
|
+
* Hierarchy row text — the Pixi sibling of three's `object.name || object.type`.
|
|
57
|
+
*
|
|
58
|
+
* Authored `.label` first. Then a GAME name: a field, a bag key, or a class
|
|
59
|
+
* that owns this `.view`. Stock Pixi types (`Sprite`, `Graphics`, `_Container`)
|
|
60
|
+
* are not names — they used to win and hide every owner. Then the texture
|
|
61
|
+
* alias (`Sprite.from('satellite')`). Then a remaining game constructor
|
|
62
|
+
* (`TitleScreen`, `PrimaryButton`). Bare `new Container()` still reads as
|
|
63
|
+
* `container` — plus its child count when it has children, because ten
|
|
64
|
+
* unnamed siblings that all read `container` name nothing at all; the
|
|
65
|
+
* editor loads games that never labelled anything and the panel still has
|
|
66
|
+
* to be legible. The count is DISPLAY only (row ids keep using raw
|
|
67
|
+
* `.label`) and rides only this last fallthrough: a row that earned a real
|
|
68
|
+
* name is never decorated.
|
|
69
|
+
*/
|
|
70
|
+
function displayLabel(o: Container): string {
|
|
71
|
+
const authored = authoredLabel(o);
|
|
72
|
+
if (authored) return authored;
|
|
73
|
+
const owner = viewOwnerLabel(o);
|
|
74
|
+
if (owner) return owner;
|
|
75
|
+
const asset = textureLabel(o);
|
|
76
|
+
if (asset) return asset;
|
|
77
|
+
const ctor = usefulCtorName(o);
|
|
78
|
+
if (ctor) return ctor;
|
|
79
|
+
const kind = kindOf(o);
|
|
80
|
+
const childCount = o.children?.length ?? 0;
|
|
81
|
+
return childCount > 0 ? `${kind} (${childCount})` : kind;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Pixi v8 stamps Sprite/Graphics `.label` with the class name. That is not an
|
|
86
|
+
* authored name.
|
|
87
|
+
*
|
|
88
|
+
* The minified-constructor guard is about the SAME leak: a shipped build's
|
|
89
|
+
* `_Container` becomes `e`, and that `e` is a class name wearing a label's
|
|
90
|
+
* clothes. It is only that leak when the label IS this object's own class
|
|
91
|
+
* name — `container.label = 'fx'` (or 'ui', 'hp') is a name the game
|
|
92
|
+
* actually authored, and a game gets to be terse.
|
|
93
|
+
*/
|
|
94
|
+
function authoredLabel(o: Container): string | undefined {
|
|
95
|
+
const authored = o.label?.trim();
|
|
96
|
+
if (!authored) return undefined;
|
|
97
|
+
if (isMinifiedCtor(authored) && authored !== o.constructor?.name) return authored;
|
|
98
|
+
if (isGenericPixiCtor(authored)) return undefined;
|
|
99
|
+
return authored;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function isMinifiedCtor(name: string): boolean {
|
|
103
|
+
return name.length <= 2 && name === name.toLowerCase();
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** Pixi's own display types. The shipped build prefixes them `_`. */
|
|
107
|
+
const STOCK_PIXI_CTOR = new Set([
|
|
108
|
+
'container',
|
|
109
|
+
'displayobject',
|
|
110
|
+
'sprite',
|
|
111
|
+
'animatedsprite',
|
|
112
|
+
'nineslicesprite',
|
|
113
|
+
'tilingsprite',
|
|
114
|
+
'text',
|
|
115
|
+
'bitmaptext',
|
|
116
|
+
'htmltext',
|
|
117
|
+
'graphics',
|
|
118
|
+
'mesh',
|
|
119
|
+
]);
|
|
120
|
+
|
|
121
|
+
function isGenericPixiCtor(name: string): boolean {
|
|
122
|
+
if (isMinifiedCtor(name) || name === 'Object' || name === 'Function') return true;
|
|
123
|
+
return STOCK_PIXI_CTOR.has(name.replace(/^_+/, '').toLowerCase());
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function textureLabel(object: Container): string | undefined {
|
|
127
|
+
const texture = (object as Container & { texture?: { label?: string } }).texture;
|
|
128
|
+
const raw = texture?.label?.trim();
|
|
129
|
+
if (!raw || /^(_?texture)?(empty|white)$/i.test(raw)) return undefined;
|
|
130
|
+
const leaf = raw.split('/').pop() ?? raw;
|
|
131
|
+
return leaf.replace(/\.[a-z0-9]+$/i, '') || undefined;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function usefulCtorName(value: object): string | undefined {
|
|
135
|
+
const name = value.constructor?.name;
|
|
136
|
+
if (!name || isGenericPixiCtor(name)) return undefined;
|
|
137
|
+
return name.replace(/^_+/, '') || undefined;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function fieldLabel(key: string): string | undefined {
|
|
141
|
+
const trimmed = key.replace(/^_+/, '');
|
|
142
|
+
return trimmed.length > 0 ? trimmed : undefined;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function isPixiDisplay(value: object): boolean {
|
|
146
|
+
return (
|
|
147
|
+
'addChild' in value &&
|
|
148
|
+
typeof (value as { addChild?: unknown }).addChild === 'function' &&
|
|
149
|
+
'children' in value
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Bubbo-style owner, identity only. TitleScreen keeps `this._cannon` and
|
|
155
|
+
* parents `this._cannon.view`. A grouping node is the field itself
|
|
156
|
+
* (`this._bottomAnimContainer`). A pool or atlas lives in a bag — Array,
|
|
157
|
+
* Map, Set, or an object literal (`_decor[]`, `allSystems`, `_parts[id]`).
|
|
158
|
+
* Scan each ancestor's own fields first — a flood walk of Pixi internals
|
|
159
|
+
* never reached `_cannon` inside the budget.
|
|
160
|
+
*/
|
|
161
|
+
function viewOwnerLabel(view: Container): string | undefined {
|
|
162
|
+
const seen = new WeakSet<object>();
|
|
163
|
+
let host: unknown = view.parent;
|
|
164
|
+
for (let hops = 0; hops < 8 && host && typeof host === 'object'; hops++) {
|
|
165
|
+
const found = labelFromHolder(host, view, seen) ?? findHeldOwner(host, view, 2, seen);
|
|
166
|
+
if (found) return found;
|
|
167
|
+
host = (host as { parent?: unknown }).parent;
|
|
168
|
+
}
|
|
169
|
+
return undefined;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function isGameFieldKey(key: string): boolean {
|
|
173
|
+
// Skip display-list indices (`0`, `1`) and Pixi internals (`children`).
|
|
174
|
+
return /^_?[A-Za-z][A-Za-zA-Z0-9]*$/.test(key) && !PIXI_INTERNAL_KEY.has(key);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const PIXI_INTERNAL_KEY = new Set([
|
|
178
|
+
'parent',
|
|
179
|
+
'children',
|
|
180
|
+
'transform',
|
|
181
|
+
'position',
|
|
182
|
+
'scale',
|
|
183
|
+
'pivot',
|
|
184
|
+
'skew',
|
|
185
|
+
'worldTransform',
|
|
186
|
+
'localTransform',
|
|
187
|
+
]);
|
|
188
|
+
|
|
189
|
+
function labelFromHolder(host: object, view: Container, seen: WeakSet<object>): string | undefined {
|
|
190
|
+
if (seen.has(host)) return undefined;
|
|
191
|
+
seen.add(host);
|
|
192
|
+
const asBag = nameInBag(host, view, undefined, seen);
|
|
193
|
+
if (asBag) return asBag;
|
|
194
|
+
for (const [key, value] of Object.entries(host as Record<string, unknown>)) {
|
|
195
|
+
if (!isGameFieldKey(key)) continue;
|
|
196
|
+
const named = nameIfHoldsView(host, key, value, view);
|
|
197
|
+
if (named) return named;
|
|
198
|
+
const nested = nameInBag(value, view, key, seen);
|
|
199
|
+
if (nested) return nested;
|
|
200
|
+
}
|
|
201
|
+
return undefined;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function nameInBag(
|
|
205
|
+
value: unknown,
|
|
206
|
+
view: Container,
|
|
207
|
+
fieldKey: string | undefined,
|
|
208
|
+
seen: WeakSet<object>,
|
|
209
|
+
): string | undefined {
|
|
210
|
+
const entries = bagEntries(value);
|
|
211
|
+
if (!entries) return undefined;
|
|
212
|
+
for (const [itemKey, item] of entries) {
|
|
213
|
+
const named = nameBagItem(value, item, usableBagKey(itemKey) ?? fieldKey, view, seen);
|
|
214
|
+
if (named) return named;
|
|
215
|
+
}
|
|
216
|
+
return undefined;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function nameBagItem(
|
|
220
|
+
bag: unknown,
|
|
221
|
+
item: unknown,
|
|
222
|
+
key: string | undefined,
|
|
223
|
+
view: Container,
|
|
224
|
+
seen: WeakSet<object>,
|
|
225
|
+
): string | undefined {
|
|
226
|
+
if (key) {
|
|
227
|
+
const named = nameIfHoldsView(
|
|
228
|
+
item && typeof item === 'object' ? item : (bag as object),
|
|
229
|
+
key,
|
|
230
|
+
item,
|
|
231
|
+
view,
|
|
232
|
+
);
|
|
233
|
+
if (named) return named;
|
|
234
|
+
} else if (item && typeof item === 'object' && (item as { view?: unknown }).view === view) {
|
|
235
|
+
const named = usefulCtorName(item);
|
|
236
|
+
if (named) return named;
|
|
237
|
+
}
|
|
238
|
+
if (item && typeof item === 'object' && !isPixiDisplay(item)) {
|
|
239
|
+
return labelFromHolder(item, view, seen);
|
|
240
|
+
}
|
|
241
|
+
return undefined;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function usableBagKey(key: string): string | undefined {
|
|
245
|
+
if (!key || /^\d+$/.test(key)) return undefined;
|
|
246
|
+
return isGameFieldKey(key) || key.includes('-') ? (fieldLabel(key) ?? key) : undefined;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function bagEntries(value: unknown): readonly [string, unknown][] | undefined {
|
|
250
|
+
if (!value || typeof value !== 'object') return undefined;
|
|
251
|
+
// Game arrays are allowed to be exotic subclasses. Do not dispatch through
|
|
252
|
+
// their overridable `map`: one shipped Pixi game returns its elements rather
|
|
253
|
+
// than `[key, value]` pairs there, which made hierarchy labeling throw while
|
|
254
|
+
// destructuring the result. The authoring seam treats game objects as
|
|
255
|
+
// untrusted observations, so normalize with indexed reads into our own array.
|
|
256
|
+
if (Array.isArray(value)) {
|
|
257
|
+
const entries: [string, unknown][] = [];
|
|
258
|
+
for (let index = 0; index < value.length; index++) {
|
|
259
|
+
entries.push([String(index), value[index]]);
|
|
260
|
+
}
|
|
261
|
+
return entries;
|
|
262
|
+
}
|
|
263
|
+
if (value instanceof Map) return [...value.entries()].map(([key, item]) => [String(key), item]);
|
|
264
|
+
if (value instanceof Set) return [...value].map((item, index) => [String(index), item]);
|
|
265
|
+
if (Object.getPrototypeOf(value) !== Object.prototype) return undefined;
|
|
266
|
+
return Object.entries(value as Record<string, unknown>);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
function nameIfHoldsView(
|
|
270
|
+
host: object,
|
|
271
|
+
key: string,
|
|
272
|
+
value: unknown,
|
|
273
|
+
view: Container,
|
|
274
|
+
): string | undefined {
|
|
275
|
+
if (value === view) {
|
|
276
|
+
const field = fieldLabel(key);
|
|
277
|
+
if (field && field.toLowerCase() !== 'view') {
|
|
278
|
+
return usefulCtorName(value as object) ?? field;
|
|
279
|
+
}
|
|
280
|
+
return usefulCtorName(value as object) ?? usefulCtorName(host) ?? field;
|
|
281
|
+
}
|
|
282
|
+
if (value && typeof value === 'object' && (value as { view?: unknown }).view === view) {
|
|
283
|
+
return usefulCtorName(value) ?? fieldLabel(key);
|
|
284
|
+
}
|
|
285
|
+
return undefined;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
function findHeldOwner(
|
|
289
|
+
held: object,
|
|
290
|
+
view: Container,
|
|
291
|
+
depth: number,
|
|
292
|
+
seen: WeakSet<object>,
|
|
293
|
+
): string | undefined {
|
|
294
|
+
const direct = labelFromHolder(held, view, seen);
|
|
295
|
+
if (direct) return direct;
|
|
296
|
+
if (depth <= 0) return undefined;
|
|
297
|
+
for (const value of Object.values(held as Record<string, unknown>)) {
|
|
298
|
+
if (!value || typeof value !== 'object' || isPixiDisplay(value)) continue;
|
|
299
|
+
const found = findHeldOwner(value, view, depth - 1, seen);
|
|
300
|
+
if (found) return found;
|
|
301
|
+
}
|
|
302
|
+
return undefined;
|
|
303
|
+
}
|
|
304
|
+
|
|
52
305
|
/**
|
|
53
306
|
* WHERE A ROW'S ID COMES FROM — the identity axis of the canvas authoring
|
|
54
307
|
* surface.
|
|
@@ -78,11 +331,52 @@ export const STRUCTURAL_CANVAS_IDENTITY: CanvasIdentity = {
|
|
|
78
331
|
idFor: (object, path) => `w2d:${path.join('/')}:${kindOf(object)}:${object.label ?? ''}`,
|
|
79
332
|
};
|
|
80
333
|
|
|
334
|
+
/** One remembered row name, with every input it was derived from. */
|
|
335
|
+
interface NameMemo {
|
|
336
|
+
readonly label: string;
|
|
337
|
+
readonly kind: string;
|
|
338
|
+
readonly parent: Container | null;
|
|
339
|
+
readonly type: unknown;
|
|
340
|
+
readonly own: string | null;
|
|
341
|
+
readonly texture: unknown;
|
|
342
|
+
readonly childCount: number;
|
|
343
|
+
}
|
|
344
|
+
|
|
81
345
|
export class AuthoringAdapter2D {
|
|
82
346
|
private readonly byId = new Map<string, Container>();
|
|
83
347
|
private overlay: Overlay2D = {};
|
|
84
348
|
private dirty = false;
|
|
85
349
|
private readonly identity: CanvasIdentity;
|
|
350
|
+
/**
|
|
351
|
+
* THE ROW-NAME MEMO — what makes a hierarchy read linear instead of a
|
|
352
|
+
* reflective flood walk per node per read.
|
|
353
|
+
*
|
|
354
|
+
* {@link displayLabel} is not a field read. For anything the game did not
|
|
355
|
+
* label itself it calls `viewOwnerLabel`, which climbs eight ancestors and
|
|
356
|
+
* reflects over each one's own fields, its bags, and two levels of whatever
|
|
357
|
+
* those hold — a deliberate search for the game object that owns this
|
|
358
|
+
* `.view`. Measured 2026-08-20 on a 1822-node `@pixi/react` canvas world,
|
|
359
|
+
* with the tab VISIBLE and the world rendering, one full projection walk
|
|
360
|
+
* cost ~800ms, essentially all of it here: once a world has rendered, the
|
|
361
|
+
* ancestors' own fields include Pixi's live render-group graph, so the
|
|
362
|
+
* search walks that too and finds nothing, every time, for every node.
|
|
363
|
+
*
|
|
364
|
+
* And it was asked four times per node per derivation — the projection
|
|
365
|
+
* walk, the conformance probe and the hierarchy facet each re-ask — which
|
|
366
|
+
* is how a status snapshot reached 115,433 `toNode` calls on one tree.
|
|
367
|
+
*
|
|
368
|
+
* ## What invalidates a remembered name
|
|
369
|
+
*
|
|
370
|
+
* Every input the derivation reads OFF THE OBJECT: its authored `.label`,
|
|
371
|
+
* its class, its texture, its parent, and how many children it has. Any of
|
|
372
|
+
* those changing re-derives — so a rename, a reparent, a retexture, and a
|
|
373
|
+
* sibling added or removed are all seen. What a remembered name does NOT
|
|
374
|
+
* re-notice is a game reassigning an ANCESTOR's field to point at this view
|
|
375
|
+
* after the fact, with the tree otherwise untouched; `viewOwnerLabel` is a
|
|
376
|
+
* naming heuristic, not a verdict, and the alternative is the 800ms walk
|
|
377
|
+
* above on every read of the tree.
|
|
378
|
+
*/
|
|
379
|
+
private readonly names = new WeakMap<Container, NameMemo>();
|
|
86
380
|
|
|
87
381
|
constructor(
|
|
88
382
|
private readonly root: Container,
|
|
@@ -143,12 +437,45 @@ export class AuthoringAdapter2D {
|
|
|
143
437
|
return this.byId.get(id) ?? null;
|
|
144
438
|
}
|
|
145
439
|
|
|
440
|
+
/** This object's row name, derived once and remembered until one of the
|
|
441
|
+
* inputs it is derived from changes. See {@link names}. */
|
|
442
|
+
private nameOf(o: Container): NameMemo {
|
|
443
|
+
const cached = this.names.get(o);
|
|
444
|
+
const parent = o.parent ?? null;
|
|
445
|
+
const type = o.constructor;
|
|
446
|
+
const own = o.label ?? null;
|
|
447
|
+
const texture = (o as Container & { texture?: unknown }).texture ?? null;
|
|
448
|
+
const childCount = o.children?.length ?? 0;
|
|
449
|
+
if (
|
|
450
|
+
cached &&
|
|
451
|
+
cached.parent === parent &&
|
|
452
|
+
cached.type === type &&
|
|
453
|
+
cached.own === own &&
|
|
454
|
+
cached.texture === texture &&
|
|
455
|
+
cached.childCount === childCount
|
|
456
|
+
) {
|
|
457
|
+
return cached;
|
|
458
|
+
}
|
|
459
|
+
const derived: NameMemo = {
|
|
460
|
+
label: displayLabel(o),
|
|
461
|
+
kind: kindOf(o),
|
|
462
|
+
parent,
|
|
463
|
+
type,
|
|
464
|
+
own,
|
|
465
|
+
texture,
|
|
466
|
+
childCount,
|
|
467
|
+
};
|
|
468
|
+
this.names.set(o, derived);
|
|
469
|
+
return derived;
|
|
470
|
+
}
|
|
471
|
+
|
|
146
472
|
private toNode(o: Container): EditorNode2D {
|
|
147
473
|
const self = o as Container & { __authId?: string };
|
|
474
|
+
const { label, kind } = this.nameOf(o);
|
|
148
475
|
return {
|
|
149
476
|
id: self.__authId ?? '',
|
|
150
|
-
label
|
|
151
|
-
kind
|
|
477
|
+
label,
|
|
478
|
+
kind,
|
|
152
479
|
parentId: (o.parent as (Container & { __authId?: string }) | null)?.__authId ?? null,
|
|
153
480
|
childIds: o.children.map((c) => (c as Container & { __authId?: string }).__authId ?? ''),
|
|
154
481
|
};
|
package/src/pixi/ingest.ts
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
import type { VisibleCaptureWindow } from '../adapter/ingest/visible-capture-window';
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
type CapturedRuntime2D,
|
|
4
|
+
installSceneCapture2D,
|
|
5
|
+
type SceneCapture2DHandle,
|
|
6
|
+
} from './scene-capture';
|
|
3
7
|
|
|
4
8
|
/** A registered unmodified PixiJS game (the Pixi analog of `IngestGame`). */
|
|
5
9
|
export interface IngestGame2D {
|
|
@@ -39,9 +43,12 @@ export async function mountIngestGame2D(
|
|
|
39
43
|
opts: {
|
|
40
44
|
captureTimeoutMs?: number;
|
|
41
45
|
onWait?: (wait: VisibleCaptureWindow | null) => void;
|
|
46
|
+
acceptCapture?: (runtime: CapturedRuntime2D) => boolean;
|
|
42
47
|
} = {},
|
|
43
48
|
): Promise<IngestMount2D> {
|
|
44
|
-
const capture = installSceneCapture2D(pixiNamespace
|
|
49
|
+
const capture = installSceneCapture2D(pixiNamespace, {
|
|
50
|
+
...(opts.acceptCapture ? { accept: opts.acceptCapture } : {}),
|
|
51
|
+
});
|
|
45
52
|
try {
|
|
46
53
|
await game.load();
|
|
47
54
|
} catch (err) {
|
|
@@ -79,12 +79,24 @@ interface PixiLike {
|
|
|
79
79
|
Application: { prototype: Record<string, unknown> };
|
|
80
80
|
}
|
|
81
81
|
|
|
82
|
+
export interface SceneCapture2DOptions {
|
|
83
|
+
/**
|
|
84
|
+
* Select the runtime owned by this mount when other Pixi Applications share
|
|
85
|
+
* the page (for example editor prefab previews). Rejected draws still count,
|
|
86
|
+
* but cannot satisfy {@link SceneCapture2DHandle.waitForCapture}.
|
|
87
|
+
*/
|
|
88
|
+
accept?: ((runtime: CapturedRuntime2D) => boolean) | undefined;
|
|
89
|
+
}
|
|
90
|
+
|
|
82
91
|
/**
|
|
83
92
|
* Install the render trap on `pixiNamespace.Application.prototype.render`. Pass the
|
|
84
93
|
* host's `pixi.js` namespace so a shared-instance game is trapped. Install once per
|
|
85
94
|
* ingest session; `uninstall()` on teardown.
|
|
86
95
|
*/
|
|
87
|
-
export function installSceneCapture2D(
|
|
96
|
+
export function installSceneCapture2D(
|
|
97
|
+
pixiNamespace: unknown,
|
|
98
|
+
options: SceneCapture2DOptions = {},
|
|
99
|
+
): SceneCapture2DHandle {
|
|
88
100
|
const PIXI = pixiNamespace as PixiLike;
|
|
89
101
|
const proto = PIXI.Application.prototype;
|
|
90
102
|
|
|
@@ -102,8 +114,11 @@ export function installSceneCapture2D(pixiNamespace: unknown): SceneCapture2DHan
|
|
|
102
114
|
drawCount++;
|
|
103
115
|
const stage = this['stage'];
|
|
104
116
|
if (!captured && stage) {
|
|
105
|
-
|
|
106
|
-
|
|
117
|
+
const candidate = { stage, renderer: this['renderer'], app: this };
|
|
118
|
+
if (options.accept?.(candidate) ?? true) {
|
|
119
|
+
captured = candidate;
|
|
120
|
+
for (const resolve of waiters.splice(0)) resolve(captured);
|
|
121
|
+
}
|
|
107
122
|
}
|
|
108
123
|
return realRender?.apply(this, args);
|
|
109
124
|
},
|
|
@@ -138,8 +153,8 @@ export function installSceneCapture2D(pixiNamespace: unknown): SceneCapture2DHan
|
|
|
138
153
|
reject(
|
|
139
154
|
new Error(
|
|
140
155
|
`pixi scene capture timed out after ${timeoutMs}ms of VISIBLE time ` +
|
|
141
|
-
`(${Math.round(captureWindow.elapsedHiddenMs())}ms
|
|
142
|
-
'
|
|
156
|
+
`(${Math.round(captureWindow.elapsedHiddenMs())}ms browser-suspended, which is ` +
|
|
157
|
+
'not counted because no frame can be presented) — the game never rendered, or it bundles its own ' +
|
|
143
158
|
'(un-shared) copy of pixi.js.',
|
|
144
159
|
),
|
|
145
160
|
);
|
|
@@ -70,6 +70,21 @@ export function useGame(): Game {
|
|
|
70
70
|
return game;
|
|
71
71
|
}
|
|
72
72
|
|
|
73
|
+
/**
|
|
74
|
+
* {@link useGame}'s non-throwing sibling: the `Game` when one hosts this tree,
|
|
75
|
+
* `null` otherwise.
|
|
76
|
+
*
|
|
77
|
+
* For components that legitimately mount BOTH under a running game and under a
|
|
78
|
+
* host that has none — the editor design-mounts a world to author it, and a
|
|
79
|
+
* story renders a component with no game at all. Those components must degrade
|
|
80
|
+
* (a dev/QA surface that simply has nothing to drive) rather than throw during
|
|
81
|
+
* render, which would take the whole world down with it. Everything else uses
|
|
82
|
+
* `useGame`, whose throw is the honest report of a wiring bug.
|
|
83
|
+
*/
|
|
84
|
+
export function useOptionalGame(): Game | null {
|
|
85
|
+
return useContext(GameContext);
|
|
86
|
+
}
|
|
87
|
+
|
|
73
88
|
/**
|
|
74
89
|
* Subscribe to a selected slice of game state. Re-renders only when the
|
|
75
90
|
* selected value changes: the selector re-runs at most once per completed
|
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
ApplyForce,
|
|
14
14
|
ChangeEmitDirection,
|
|
15
15
|
CircleEmitter,
|
|
16
|
+
type ColorGenerator,
|
|
16
17
|
ColorGeneratorFromJSON,
|
|
17
18
|
ColorOverLife,
|
|
18
19
|
ConeEmitter,
|
|
@@ -20,6 +21,7 @@ import {
|
|
|
20
21
|
type EmitterShape,
|
|
21
22
|
ForceOverLife,
|
|
22
23
|
FrameOverLife,
|
|
24
|
+
type FunctionColorGenerator,
|
|
23
25
|
GravityForce,
|
|
24
26
|
GridEmitter,
|
|
25
27
|
HemisphereEmitter,
|
|
@@ -345,9 +347,9 @@ export interface ParticleSystemResult {
|
|
|
345
347
|
* This is how a caller with fidelity a plain descriptor cannot express reaches
|
|
346
348
|
* the SAME factory instead of hand-building its own `new ParticleSystem(...)`:
|
|
347
349
|
* a foreign-engine carry (e.g. Godot CPUParticles) supplies its own
|
|
348
|
-
* `EmitterShape
|
|
349
|
-
* else — lifecycle, start values, emission,
|
|
350
|
-
* JSON.
|
|
350
|
+
* `EmitterShape`, start-colour generator, and instancing geometry here, and
|
|
351
|
+
* expresses everything else — lifecycle, scalar start values, emission,
|
|
352
|
+
* behaviors, material — as descriptor JSON.
|
|
351
353
|
*/
|
|
352
354
|
export interface ParticleSystemObjects {
|
|
353
355
|
/**
|
|
@@ -356,6 +358,12 @@ export interface ParticleSystemObjects {
|
|
|
356
358
|
* present it REPLACES `data.shape`.
|
|
357
359
|
*/
|
|
358
360
|
shape?: EmitterShape;
|
|
361
|
+
/**
|
|
362
|
+
* A pre-built start-colour generator for a per-particle draw the descriptor's
|
|
363
|
+
* JSON generator vocabulary cannot express. When present it REPLACES
|
|
364
|
+
* `data.startColor`.
|
|
365
|
+
*/
|
|
366
|
+
startColor?: ColorGenerator | FunctionColorGenerator;
|
|
359
367
|
/**
|
|
360
368
|
* The geometry instanced per particle in `RenderMode.Mesh`. A
|
|
361
369
|
* `BufferGeometry` is not JSON-serializable, so `renderMode: 'mesh'` requires
|
|
@@ -377,8 +385,9 @@ export interface ParticleSystemObjects {
|
|
|
377
385
|
* Create a three.quarks ParticleSystem from ParticlesDescriptor JSON data.
|
|
378
386
|
*
|
|
379
387
|
* `objects` carries the pieces a JSON descriptor cannot: a plugin emitter
|
|
380
|
-
* shape, a mesh-mode instancing geometry, and the
|
|
381
|
-
* optional and additive — a call with only `data`
|
|
388
|
+
* shape, a start-colour generator, a mesh-mode instancing geometry, and the
|
|
389
|
+
* default-sprite toggle. It is optional and additive — a call with only `data`
|
|
390
|
+
* behaves exactly as before.
|
|
382
391
|
*/
|
|
383
392
|
// biome-ignore lint/complexity/noExcessiveCognitiveComplexity: straightforward field-by-field mapping
|
|
384
393
|
export function createParticleSystemFromData(
|
|
@@ -410,7 +419,8 @@ export function createParticleSystemFromData(
|
|
|
410
419
|
if (data.startSpeed) params['startSpeed'] = valueGen(data.startSpeed);
|
|
411
420
|
if (data.startSize) params['startSize'] = valueGen(data.startSize);
|
|
412
421
|
if (data.startRotation) params['startRotation'] = valueGen(data.startRotation);
|
|
413
|
-
if (
|
|
422
|
+
if (objects?.startColor) params['startColor'] = objects.startColor;
|
|
423
|
+
else if (data.startColor) params['startColor'] = colorGen(data.startColor);
|
|
414
424
|
if (data.startTileIndex) params['startTileIndex'] = valueGen(data.startTileIndex);
|
|
415
425
|
|
|
416
426
|
// Trail mode requires startLength inside rendererEmitterSettings, not as a top-level param
|
|
@@ -91,9 +91,9 @@
|
|
|
91
91
|
* callers are `registerParticleSystem` / `unregisterParticleSystem`.
|
|
92
92
|
* - The render target and override material belong to the
|
|
93
93
|
* {@link SoftParticleDepthPass} instance and die with its `dispose()`. The
|
|
94
|
-
* pass
|
|
95
|
-
*
|
|
96
|
-
* allowed to end it.
|
|
94
|
+
* pass is owned by the renderer loop that calls it: an adapter mount for a
|
|
95
|
+
* runtime renderer, or the editor Scene viewport for its renderer. That
|
|
96
|
+
* owner's teardown is the one path allowed to end it.
|
|
97
97
|
*/
|
|
98
98
|
|
|
99
99
|
import * as THREE from 'three';
|
|
@@ -203,16 +203,11 @@ function drawsIntoDepth(object: THREE.Object3D): boolean {
|
|
|
203
203
|
* The three renderer members this prepass CANNOT do without — the render-target
|
|
204
204
|
* swap it draws through, and the buffer size it sizes that target from.
|
|
205
205
|
*
|
|
206
|
-
* A real `WebGLRenderer` has all of them. A
|
|
207
|
-
*
|
|
208
|
-
*
|
|
209
|
-
*
|
|
210
|
-
*
|
|
211
|
-
* not be settled at all. A prepass is an EFFECT: with no renderer to run it on,
|
|
212
|
-
* the right answer is the inert far-depth default every batch is already bound
|
|
213
|
-
* to (see {@link farDepthTexture}), which is pixel-identical to a batch that
|
|
214
|
-
* never asked for soft particles. So this degrades, loudly and once, instead of
|
|
215
|
-
* taking the frame down with it.
|
|
206
|
+
* A real `WebGLRenderer` has all of them. A design-time stand-in that only has
|
|
207
|
+
* to answer `render` may mount a world so another host can read it, but cannot
|
|
208
|
+
* run this effect. The editor does not pass that stand-in here: its real
|
|
209
|
+
* viewport renderer owns its own pass. Other incomplete hosts still degrade
|
|
210
|
+
* loudly and once instead of taking the frame down.
|
|
216
211
|
*/
|
|
217
212
|
const REQUIRED_RENDERER_MEMBERS = [
|
|
218
213
|
'getDrawingBufferSize',
|