@vgai/engine 0.5.13 → 0.5.15
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/adapter/adapter-module.d.ts +447 -0
- package/dist/adapter/adapter-module.d.ts.map +1 -0
- package/dist/adapter/adapter-module.js +308 -0
- package/dist/adapter/authoring.d.ts +357 -10
- package/dist/adapter/authoring.d.ts.map +1 -1
- package/dist/adapter/authoring.js +87 -1
- package/dist/adapter/constraint.d.ts +9 -3
- package/dist/adapter/constraint.d.ts.map +1 -1
- package/dist/adapter/finders/finder-result.d.ts +28 -0
- package/dist/adapter/finders/finder-result.d.ts.map +1 -0
- package/dist/adapter/finders/finder-result.js +10 -0
- package/dist/adapter/finders/index.d.ts +38 -0
- package/dist/adapter/finders/index.d.ts.map +1 -0
- package/dist/adapter/finders/index.js +41 -0
- package/dist/adapter/finders/prefabs-from-stories.d.ts +64 -0
- package/dist/adapter/finders/prefabs-from-stories.d.ts.map +1 -0
- package/dist/adapter/finders/prefabs-from-stories.js +92 -0
- package/dist/adapter/finders/scenes-from-entrypoint-selection.d.ts +67 -0
- package/dist/adapter/finders/scenes-from-entrypoint-selection.d.ts.map +1 -0
- package/dist/adapter/finders/scenes-from-entrypoint-selection.js +226 -0
- package/dist/adapter/first-party-systems.d.ts.map +1 -1
- package/dist/adapter/first-party-systems.js +29 -0
- package/dist/adapter/index.d.ts +7 -3
- package/dist/adapter/index.d.ts.map +1 -1
- package/dist/adapter/index.js +5 -2
- package/dist/adapter/ingest/contract-system-adapters.d.ts +24 -1
- package/dist/adapter/ingest/contract-system-adapters.d.ts.map +1 -1
- package/dist/adapter/ingest/contract-system-adapters.js +100 -34
- package/dist/adapter/ingest/game-contract.d.ts +188 -2
- package/dist/adapter/ingest/game-contract.d.ts.map +1 -1
- package/dist/adapter/ingest/game-contract.js +107 -0
- package/dist/adapter/ingest/mount-readiness.d.ts +84 -0
- package/dist/adapter/ingest/mount-readiness.d.ts.map +1 -0
- package/dist/adapter/ingest/mount-readiness.js +81 -0
- package/dist/adapter/ingest/scene-capture.d.ts +91 -2
- package/dist/adapter/ingest/scene-capture.d.ts.map +1 -1
- package/dist/adapter/ingest/scene-capture.js +110 -18
- package/dist/adapter/ingest/structural-ids.d.ts +11 -5
- package/dist/adapter/ingest/structural-ids.d.ts.map +1 -1
- package/dist/adapter/ingest/structural-ids.js +18 -9
- package/dist/adapter/ingest/upstream-pin.d.ts +4 -2
- package/dist/adapter/ingest/upstream-pin.d.ts.map +1 -1
- package/dist/adapter/ingest/upstream-pin.js +4 -2
- package/dist/adapter/ingest/visible-capture-window.d.ts +116 -0
- package/dist/adapter/ingest/visible-capture-window.d.ts.map +1 -0
- package/dist/adapter/ingest/visible-capture-window.js +141 -0
- package/dist/adapter/loop-gate-report.d.ts +3 -4
- package/dist/adapter/loop-gate-report.d.ts.map +1 -1
- package/dist/adapter/loop-gate-report.js +3 -4
- package/dist/adapter/root-adapter.d.ts +22 -2
- package/dist/adapter/root-adapter.d.ts.map +1 -1
- package/dist/adapter/setup-three-root-adapter.d.ts.map +1 -1
- package/dist/adapter/setup-three-root-adapter.js +15 -0
- package/dist/adapter/system-adapter.d.ts +161 -2
- package/dist/adapter/system-adapter.d.ts.map +1 -1
- package/dist/adapter/system-adapter.js +18 -1
- package/dist/animation/camera-ownership.d.ts +4 -2
- package/dist/animation/camera-ownership.d.ts.map +1 -1
- package/dist/animation/camera-ownership.js +58 -1
- package/dist/animation/runtime-inspection.d.ts +70 -0
- package/dist/animation/runtime-inspection.d.ts.map +1 -0
- package/dist/animation/runtime-inspection.js +25 -0
- package/dist/animation/xstate-animation-binding.d.ts +9 -1
- package/dist/animation/xstate-animation-binding.d.ts.map +1 -1
- package/dist/animation/xstate-animation-binding.js +48 -1
- package/dist/asset-formats/material.d.ts +1 -1
- package/dist/asset-formats/mesh.d.ts +1 -1
- package/dist/asset-formats/particles.d.ts +13 -0
- package/dist/asset-formats/particles.d.ts.map +1 -1
- package/dist/asset-formats/particles.js +28 -0
- package/dist/behavior/xstate-inspection.d.ts +24 -0
- package/dist/behavior/xstate-inspection.d.ts.map +1 -0
- package/dist/behavior/xstate-inspection.js +25 -0
- package/dist/core/game-loop.d.ts +7 -5
- package/dist/core/game-loop.d.ts.map +1 -1
- package/dist/core/game-loop.js +39 -18
- package/dist/core/types.d.ts +11 -12
- package/dist/core/types.d.ts.map +1 -1
- package/dist/data/data-asset.js +1 -1
- package/dist/dev/instruments.d.ts.map +1 -1
- package/dist/dev/instruments.js +5 -1
- package/dist/dev/render-debug-adapter.d.ts +19 -2
- package/dist/dev/render-debug-adapter.d.ts.map +1 -1
- package/dist/dev/render-debug-adapter.js +10 -0
- package/dist/ecs/user-data.d.ts +10 -2
- package/dist/ecs/user-data.d.ts.map +1 -1
- package/dist/ecs/user-data.js +6 -2
- package/dist/input/binding-identity.d.ts +10 -0
- package/dist/input/binding-identity.d.ts.map +1 -0
- package/dist/input/binding-identity.js +36 -0
- package/dist/input/input-manager.d.ts +18 -185
- package/dist/input/input-manager.d.ts.map +1 -1
- package/dist/input/input-manager.js +78 -68
- package/dist/manifest/load.d.ts +14 -0
- package/dist/manifest/load.d.ts.map +1 -1
- package/dist/manifest/load.js +6 -1
- package/dist/manifest/schema.d.ts +18 -0
- package/dist/manifest/schema.d.ts.map +1 -1
- package/dist/manifest/schema.js +65 -8
- package/dist/pixi/authoring.d.ts +27 -0
- package/dist/pixi/authoring.d.ts.map +1 -1
- package/dist/pixi/authoring.js +10 -3
- package/dist/pixi/index.d.ts +1 -1
- package/dist/pixi/index.d.ts.map +1 -1
- package/dist/pixi/index.js +1 -1
- package/dist/pixi/ingest.d.ts +8 -0
- package/dist/pixi/ingest.d.ts.map +1 -1
- package/dist/pixi/ingest.js +10 -1
- package/dist/pixi/render-pass-bracket.d.ts +56 -0
- package/dist/pixi/render-pass-bracket.d.ts.map +1 -0
- package/dist/pixi/render-pass-bracket.js +98 -0
- package/dist/pixi/scene-capture.d.ts +32 -1
- package/dist/pixi/scene-capture.d.ts.map +1 -1
- package/dist/pixi/scene-capture.js +21 -9
- package/dist/pixi/system-adapters.d.ts +29 -0
- package/dist/pixi/system-adapters.d.ts.map +1 -1
- package/dist/pixi/system-adapters.js +27 -0
- package/dist/render/particles-factory.d.ts +16 -2
- package/dist/render/particles-factory.d.ts.map +1 -1
- package/dist/render/particles-factory.js +88 -5
- package/dist/render/soft-particle-depth.d.ts +149 -0
- package/dist/render/soft-particle-depth.d.ts.map +1 -0
- package/dist/render/soft-particle-depth.js +305 -0
- package/dist/render/voxel-surface.d.ts +117 -0
- package/dist/render/voxel-surface.d.ts.map +1 -0
- package/dist/render/voxel-surface.js +263 -0
- package/dist/runtime/create-runtime.js +7 -0
- package/dist/runtime/debug-bridge.d.ts +15 -15
- package/dist/runtime/debug-bridge.d.ts.map +1 -1
- package/dist/runtime/debug-bridge.js +27 -26
- package/dist/runtime/game.d.ts.map +1 -1
- package/dist/runtime/game.js +1 -2
- package/dist/setup/setup-particles.d.ts.map +1 -1
- package/dist/setup/setup-particles.js +9 -0
- package/dist/world3d-react/r3f-adapter.d.ts.map +1 -1
- package/dist/world3d-react/r3f-adapter.js +42 -3
- package/dist/world3d-react/rapier-physics-bridge.d.ts.map +1 -1
- package/dist/world3d-react/rapier-physics-bridge.js +201 -11
- package/dist-config/config.js.map +1 -1
- package/package.json +4 -2
- package/schemas/engine-capabilities.json +12 -12
- package/schemas/vgai-project.schema.json +42 -3
- package/src/adapter/adapter-module.ts +607 -0
- package/src/adapter/authoring.ts +440 -10
- package/src/adapter/constraint.ts +10 -3
- package/src/adapter/finders/finder-result.ts +29 -0
- package/src/adapter/finders/index.ts +62 -0
- package/src/adapter/finders/prefabs-from-stories.ts +142 -0
- package/src/adapter/finders/scenes-from-entrypoint-selection.ts +304 -0
- package/src/adapter/first-party-systems.ts +30 -0
- package/src/adapter/index.ts +32 -1
- package/src/adapter/ingest/contract-system-adapters.ts +116 -32
- package/src/adapter/ingest/game-contract.ts +265 -2
- package/src/adapter/ingest/mount-readiness.ts +140 -0
- package/src/adapter/ingest/scene-capture.ts +199 -22
- package/src/adapter/ingest/structural-ids.ts +21 -9
- package/src/adapter/ingest/upstream-pin.ts +4 -2
- package/src/adapter/ingest/visible-capture-window.ts +213 -0
- package/src/adapter/loop-gate-report.ts +3 -4
- package/src/adapter/root-adapter.ts +22 -2
- package/src/adapter/setup-three-root-adapter.ts +16 -0
- package/src/adapter/system-adapter.ts +188 -2
- package/src/animation/camera-ownership.ts +68 -3
- package/src/animation/runtime-inspection.ts +104 -0
- package/src/animation/xstate-animation-binding.ts +53 -1
- package/src/asset-formats/particles.ts +31 -0
- package/src/behavior/xstate-inspection.ts +51 -0
- package/src/core/game-loop.ts +39 -17
- package/src/core/types.ts +11 -12
- package/src/data/data-asset.ts +1 -1
- package/src/dev/instruments.ts +5 -1
- package/src/dev/render-debug-adapter.ts +22 -2
- package/src/ecs/user-data.ts +10 -2
- package/src/input/binding-identity.ts +37 -0
- package/src/input/input-manager.ts +93 -68
- package/src/manifest/load.ts +23 -2
- package/src/manifest/schema.ts +77 -8
- package/src/pixi/authoring.ts +37 -5
- package/src/pixi/index.ts +2 -0
- package/src/pixi/ingest.ts +15 -2
- package/src/pixi/render-pass-bracket.ts +117 -0
- package/src/pixi/scene-capture.ts +63 -13
- package/src/pixi/system-adapters.ts +41 -0
- package/src/render/particles-factory.ts +89 -6
- package/src/render/soft-particle-depth.ts +336 -0
- package/src/render/voxel-surface.ts +358 -0
- package/src/runtime/create-runtime.ts +7 -0
- package/src/runtime/debug-bridge.ts +32 -31
- package/src/runtime/game.ts +1 -2
- package/src/setup/setup-particles.ts +9 -0
- package/src/world3d-react/r3f-adapter.tsx +48 -2
- package/src/world3d-react/rapier-physics-bridge.tsx +249 -11
package/src/manifest/load.ts
CHANGED
|
@@ -62,6 +62,13 @@ export type ResolvedAdapter =
|
|
|
62
62
|
readonly assets: Record<string, string> | undefined;
|
|
63
63
|
readonly domStubs: string[] | undefined;
|
|
64
64
|
readonly captureTimeoutMs: number | undefined;
|
|
65
|
+
/**
|
|
66
|
+
* The component this root'''s WORLD is, in the game'''s own source (see the
|
|
67
|
+
* field'''s schema description). Present ⇒ Edit mounts THAT component
|
|
68
|
+
* alone as a still design-time document and Play mounts the game
|
|
69
|
+
* through `entry`; absent ⇒ `entry` is what both modes mount.
|
|
70
|
+
*/
|
|
71
|
+
readonly world?: { readonly entry: string; readonly export?: string | undefined } | undefined;
|
|
65
72
|
};
|
|
66
73
|
|
|
67
74
|
export interface ResolvedAdapterRoot {
|
|
@@ -86,7 +93,16 @@ export interface ResolvedGameManifest {
|
|
|
86
93
|
readonly engine: { readonly version: string };
|
|
87
94
|
/** Sorted by zOrder; ties broken by original array order (§3). */
|
|
88
95
|
readonly roots: readonly ResolvedAdapterRoot[];
|
|
89
|
-
readonly server:
|
|
96
|
+
readonly server:
|
|
97
|
+
| {
|
|
98
|
+
readonly room: string;
|
|
99
|
+
readonly module: string;
|
|
100
|
+
/** The declared endpoint, or `undefined` = the scaffold default (see
|
|
101
|
+
* the schema's own description; the editor resolves it against the
|
|
102
|
+
* page host, which the loader has no business knowing). */
|
|
103
|
+
readonly endpoint: string | undefined;
|
|
104
|
+
}
|
|
105
|
+
| undefined;
|
|
90
106
|
readonly resolution: { readonly width: number; readonly height: number } | undefined;
|
|
91
107
|
/** Construction-time renderer properties no world can declare for itself — see the schema. */
|
|
92
108
|
readonly rendering: { readonly antialias: boolean } | undefined;
|
|
@@ -218,6 +234,7 @@ function resolveAdapter(root: AdapterRoot): ResolvedAdapter {
|
|
|
218
234
|
assets: ingest.assets,
|
|
219
235
|
domStubs: ingest.domStubs,
|
|
220
236
|
captureTimeoutMs: ingest.captureTimeoutMs,
|
|
237
|
+
world: ingest.world,
|
|
221
238
|
};
|
|
222
239
|
}
|
|
223
240
|
|
|
@@ -312,7 +329,11 @@ export function loadGameManifest(raw: unknown): ResolvedGameManifest {
|
|
|
312
329
|
const roots = resolvedWithIndex.map(({ root }) => root);
|
|
313
330
|
|
|
314
331
|
const server = manifest.server
|
|
315
|
-
? {
|
|
332
|
+
? {
|
|
333
|
+
room: manifest.server.room,
|
|
334
|
+
module: manifest.server.module ?? DEFAULT_SERVER_MODULE,
|
|
335
|
+
endpoint: manifest.server.endpoint,
|
|
336
|
+
}
|
|
316
337
|
: undefined;
|
|
317
338
|
|
|
318
339
|
return {
|
package/src/manifest/schema.ts
CHANGED
|
@@ -154,8 +154,9 @@ export const RootAdapterSchema = z
|
|
|
154
154
|
'game`s entry has had ITS relative imports rewritten to whatever url the server ' +
|
|
155
155
|
'considers canonical for those files. Module identity is per-url, so the shim ' +
|
|
156
156
|
'silently binds a SECOND, freshly-evaluated copy of the whole game and every ' +
|
|
157
|
-
'provider reports a game that never started. Measured on three
|
|
158
|
-
'segments through the game`s graph, 0 through the shim`s,
|
|
157
|
+
'provider reports a game that never started. Measured on a vendored three ' +
|
|
158
|
+
'game: 285 world segments through the game`s own graph, 0 through the shim`s, ' +
|
|
159
|
+
'and no error anywhere.',
|
|
159
160
|
),
|
|
160
161
|
dataWriter: z
|
|
161
162
|
.string()
|
|
@@ -163,17 +164,18 @@ export const RootAdapterSchema = z
|
|
|
163
164
|
.describe(
|
|
164
165
|
"Project-relative path to a HOST-ADDED ES module that writes this game's own " +
|
|
165
166
|
'DATA file back — the sibling of a source write for a game whose authorable ' +
|
|
166
|
-
'truth is not source.
|
|
167
|
-
'binary records inside
|
|
167
|
+
'truth is not source. A level-based game places its enemies, pickups and ' +
|
|
168
|
+
'objectives in binary records inside a level file, reaching no source ' +
|
|
169
|
+
'literal, so an ' +
|
|
168
170
|
'authored move of one is an edit to that file and nothing else can write it. ' +
|
|
169
171
|
'The module declares two exports and the host knows nothing else about it: ' +
|
|
170
172
|
'`dataFile` (a project-relative string — the ONE file this writer edits) and ' +
|
|
171
173
|
'`planDataEdit(bytes, {record, property, baseline, next})`, which returns ' +
|
|
172
174
|
'`{changed: true, bytes}` or `{changed: false, reason}` and may be async. ' +
|
|
173
175
|
'It is loaded in the editor realm beside the game, so it may `import()` the ' +
|
|
174
|
-
"game's own modules to resolve what only a running game knows (
|
|
175
|
-
'writer asks the game`s own
|
|
176
|
-
'landed in, and refuses "outside the
|
|
176
|
+
"game's own modules to resolve what only a running game knows (a level " +
|
|
177
|
+
'writer asks the game`s own spatial lookup which cell a moved object ' +
|
|
178
|
+
'landed in, and refuses "outside the level" rather than writing data the game ' +
|
|
177
179
|
'would read back wrong). It NEVER fabricates: a record it cannot address, or ' +
|
|
178
180
|
'a property it does not model, is a named refusal. ' +
|
|
179
181
|
'An object is anchored to a record by carrying `userData.vgaiRecordIndex` — ' +
|
|
@@ -186,6 +188,23 @@ export const RootAdapterSchema = z
|
|
|
186
188
|
.describe(
|
|
187
189
|
'Path-substring -> served-URL rewrites for this ingested game (IngestGame.assets today)',
|
|
188
190
|
),
|
|
191
|
+
moduleAliases: z
|
|
192
|
+
.record(z.string(), z.string())
|
|
193
|
+
.optional()
|
|
194
|
+
.describe(
|
|
195
|
+
"The game's OWN bundler path aliases, restated as a mounting fact so its " +
|
|
196
|
+
'source resolves unedited. A game built with its own Vite/webpack config ' +
|
|
197
|
+
'routinely imports itself through an alias (`~/lib/config`, `@/store/x`); the ' +
|
|
198
|
+
"editor boots Vite from the ENGINE's config and never reads a game's own, so " +
|
|
199
|
+
'without this every such import is an unresolvable specifier and the mount dies ' +
|
|
200
|
+
'at transform time. Key = the specifier PREFIX exactly as the game writes it ' +
|
|
201
|
+
'("~/"); value = the repo-root-relative directory it means ' +
|
|
202
|
+
'("/vendor/games/<id>/src/"). SELF-SCOPING: the host honors an entry only for ' +
|
|
203
|
+
"importers inside the vendored game folder the value points into, so one game's " +
|
|
204
|
+
"alias can never capture another module's import. Rewriting the game's own " +
|
|
205
|
+
'imports instead would be a large non-seam-shaped diff against unmodified ' +
|
|
206
|
+
'source — this declares the fact rather than editing it away.',
|
|
207
|
+
),
|
|
189
208
|
domStubs: z
|
|
190
209
|
.array(z.string())
|
|
191
210
|
.optional()
|
|
@@ -200,6 +219,34 @@ export const RootAdapterSchema = z
|
|
|
200
219
|
'FAILS by name. A game that needs a long boot (streamed world assets, a menu ' +
|
|
201
220
|
'the player must clear) raises it; the default is 10s.',
|
|
202
221
|
),
|
|
222
|
+
world: z
|
|
223
|
+
.object({
|
|
224
|
+
entry: z
|
|
225
|
+
.string()
|
|
226
|
+
.describe(
|
|
227
|
+
"Project-relative path to the game's OWN module exporting that component",
|
|
228
|
+
),
|
|
229
|
+
export: z
|
|
230
|
+
.string()
|
|
231
|
+
.optional()
|
|
232
|
+
.describe(
|
|
233
|
+
'Named export to read from that module; omit when the module default-exports it',
|
|
234
|
+
),
|
|
235
|
+
})
|
|
236
|
+
.strict()
|
|
237
|
+
.optional()
|
|
238
|
+
.describe(
|
|
239
|
+
"The component this root's WORLD is, in the game's own source — a three root " +
|
|
240
|
+
'whose scene is a React/R3F component tree names it here. Two readers, and ' +
|
|
241
|
+
'they mount different things: the editor mounts THIS component alone as the ' +
|
|
242
|
+
"root's design-time document (a still scene — its frameloop runs ONE bounded " +
|
|
243
|
+
'settle at mount, so physics-owned bodies rest at their authored spawn, and ' +
|
|
244
|
+
'is then frozen forever; nothing moves in Edit), while `entry` ' +
|
|
245
|
+
'above stays what PLAY mounts (the whole game, its own composition, its own ' +
|
|
246
|
+
'HUD and loop). Declaring it is what lets a scene be a scene in Edit rather ' +
|
|
247
|
+
"than the running game's mount surfaces. A game whose world is not a " +
|
|
248
|
+
'component omits it and keeps mounting through `entry` in both modes.',
|
|
249
|
+
),
|
|
203
250
|
})
|
|
204
251
|
.strict()
|
|
205
252
|
.describe(
|
|
@@ -246,6 +293,19 @@ export const AdapterRootSchema = z
|
|
|
246
293
|
'Module exporting a world root — a TSX/R3F component or a setup()/adapter (three), or a ' +
|
|
247
294
|
'React component (dom). Required for every built-in adapter root.',
|
|
248
295
|
),
|
|
296
|
+
styles: z
|
|
297
|
+
.array(z.string())
|
|
298
|
+
.optional()
|
|
299
|
+
.describe(
|
|
300
|
+
"Project-root-relative CSS files this root's mount needs — the PAGE-level stylesheet a " +
|
|
301
|
+
'game written to own a browser tab keeps its layout in (an ingested game is the ' +
|
|
302
|
+
'standing case: its own entry imported the sheet, the host shim that replaces that ' +
|
|
303
|
+
'entry cannot, because `html`/`body`/`*` rules would restyle the editor). Declared ' +
|
|
304
|
+
'here, the editor serves each file rewritten to apply ONLY inside the container it ' +
|
|
305
|
+
"mounts the root's DOM in (`@scope`; packages/editor/server/scoped-game-css.ts), so " +
|
|
306
|
+
"the HUD is styled on the UI board's story cards and on the game surface without the " +
|
|
307
|
+
'editor seeing a single one of those rules. The files themselves are never modified.',
|
|
308
|
+
),
|
|
249
309
|
zOrder: z
|
|
250
310
|
.number()
|
|
251
311
|
.int()
|
|
@@ -338,7 +398,7 @@ export const LearnMetadataSchema = z
|
|
|
338
398
|
.optional()
|
|
339
399
|
.describe(
|
|
340
400
|
'Optional Learn-site lesson URL that teaches this project 1:1 ' +
|
|
341
|
-
'(e.g. https://vgai-learn.pages.dev/
|
|
401
|
+
'(e.g. https://vgai-learn.pages.dev/manual/physics/joints/)',
|
|
342
402
|
),
|
|
343
403
|
})
|
|
344
404
|
.strict()
|
|
@@ -481,6 +541,15 @@ export const GameManifestSchema = z
|
|
|
481
541
|
server: z
|
|
482
542
|
.object({
|
|
483
543
|
room: z.string().describe('Colyseus room name'),
|
|
544
|
+
endpoint: z
|
|
545
|
+
.string()
|
|
546
|
+
.optional()
|
|
547
|
+
.describe(
|
|
548
|
+
'WebSocket endpoint the editor reports for this room, e.g. "wss://rooms.example.com". ' +
|
|
549
|
+
'Omit to declare the scaffold default, `ws://<the page host>:2567` — the port ' +
|
|
550
|
+
"`server/colyseus-setup.ts` listens on. Stated here because the endpoint is the game's " +
|
|
551
|
+
'own deployment fact, not something the editor may guess',
|
|
552
|
+
),
|
|
484
553
|
module: z
|
|
485
554
|
.string()
|
|
486
555
|
.optional()
|
package/src/pixi/authoring.ts
CHANGED
|
@@ -49,16 +49,47 @@ function kindOf(o: Container): string {
|
|
|
49
49
|
return 'container';
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
+
/**
|
|
53
|
+
* WHERE A ROW'S ID COMES FROM — the identity axis of the canvas authoring
|
|
54
|
+
* surface.
|
|
55
|
+
*
|
|
56
|
+
* The tree walk is the same whatever authored the tree; what differs is
|
|
57
|
+
* whether the display objects carry an authored address. A first-party
|
|
58
|
+
* `@pixi/react` world's containers do (the `data-oid` the editor's transform
|
|
59
|
+
* stamps, which the reconciler lands on the instance), and keying on it is
|
|
60
|
+
* what makes selection survive a remount. A tree the editor did NOT author has
|
|
61
|
+
* no such address, so its ids are structural paths — stable for as long as the
|
|
62
|
+
* tree's shape is, which is the best that graph can honestly offer.
|
|
63
|
+
*
|
|
64
|
+
* Both are the SAME adapter over the SAME surface; only this collaborator
|
|
65
|
+
* changes. {@link STRUCTURAL_CANVAS_IDENTITY} is the default.
|
|
66
|
+
*/
|
|
67
|
+
export interface CanvasIdentity {
|
|
68
|
+
/** Called once at the start of every walk — where an implementation that
|
|
69
|
+
* disambiguates repeats (one JSX element rendered ninety times) resets its
|
|
70
|
+
* per-walk counters. */
|
|
71
|
+
beginWalk?(): void;
|
|
72
|
+
/** The id for one display object at one position in the tree. */
|
|
73
|
+
idFor(object: Container, path: readonly number[]): string;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** Deterministic structural-path ids: `w2d:<path>:<kind>:<label>`. */
|
|
77
|
+
export const STRUCTURAL_CANVAS_IDENTITY: CanvasIdentity = {
|
|
78
|
+
idFor: (object, path) => `w2d:${path.join('/')}:${kindOf(object)}:${object.label ?? ''}`,
|
|
79
|
+
};
|
|
80
|
+
|
|
52
81
|
export class AuthoringAdapter2D {
|
|
53
82
|
private readonly byId = new Map<string, Container>();
|
|
54
83
|
private overlay: Overlay2D = {};
|
|
55
84
|
private dirty = false;
|
|
85
|
+
private readonly identity: CanvasIdentity;
|
|
56
86
|
|
|
57
87
|
constructor(
|
|
58
88
|
private readonly root: Container,
|
|
59
|
-
opts: { overlay?: Overlay2D } = {},
|
|
89
|
+
opts: { overlay?: Overlay2D; identity?: CanvasIdentity } = {},
|
|
60
90
|
) {
|
|
61
91
|
if (opts.overlay) this.overlay = opts.overlay;
|
|
92
|
+
this.identity = opts.identity ?? STRUCTURAL_CANVAS_IDENTITY;
|
|
62
93
|
this.refresh();
|
|
63
94
|
}
|
|
64
95
|
|
|
@@ -73,20 +104,21 @@ export class AuthoringAdapter2D {
|
|
|
73
104
|
*/
|
|
74
105
|
reindex(): { count: number; sprites: number } {
|
|
75
106
|
this.byId.clear();
|
|
107
|
+
this.identity.beginWalk?.();
|
|
76
108
|
let count = 0;
|
|
77
109
|
let sprites = 0;
|
|
78
|
-
const visit = (o: Container, path:
|
|
79
|
-
const id =
|
|
110
|
+
const visit = (o: Container, path: readonly number[]): void => {
|
|
111
|
+
const id = this.identity.idFor(o, path);
|
|
80
112
|
(o as Container & { __authId?: string }).__authId = id;
|
|
81
113
|
this.byId.set(id, o);
|
|
82
114
|
count++;
|
|
83
115
|
if (kindOf(o).includes('sprite')) sprites++;
|
|
84
116
|
o.children.forEach((c, i) => {
|
|
85
|
-
visit(c as Container,
|
|
117
|
+
visit(c as Container, [...path, i]);
|
|
86
118
|
});
|
|
87
119
|
};
|
|
88
120
|
this.root.children.forEach((c, i) => {
|
|
89
|
-
visit(c as Container,
|
|
121
|
+
visit(c as Container, [i]);
|
|
90
122
|
});
|
|
91
123
|
return { count, sprites };
|
|
92
124
|
}
|
package/src/pixi/index.ts
CHANGED
|
@@ -10,10 +10,12 @@
|
|
|
10
10
|
|
|
11
11
|
export {
|
|
12
12
|
AuthoringAdapter2D,
|
|
13
|
+
type CanvasIdentity,
|
|
13
14
|
type EditorNode2D,
|
|
14
15
|
type Overlay2D,
|
|
15
16
|
type Override2D,
|
|
16
17
|
type Property2D,
|
|
18
|
+
STRUCTURAL_CANVAS_IDENTITY,
|
|
17
19
|
type Transform2DValue,
|
|
18
20
|
} from './authoring';
|
|
19
21
|
export { type IngestGame2D, type IngestMount2D, mountIngestGame2D } from './ingest';
|
package/src/pixi/ingest.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { VisibleCaptureWindow } from '../adapter/ingest/visible-capture-window';
|
|
1
2
|
import { installSceneCapture2D, type SceneCapture2DHandle } from './scene-capture';
|
|
2
3
|
|
|
3
4
|
/** A registered unmodified PixiJS game (the Pixi analog of `IngestGame`). */
|
|
@@ -25,11 +26,20 @@ export interface IngestMount2D {
|
|
|
25
26
|
* instance, runs the game's `load()`, waits for the game to render its first
|
|
26
27
|
* frame (capturing its live stage), and exposes loop gating + cleanup. A game
|
|
27
28
|
* whose pixi the trap cannot reach never captures, and the mount FAILS by name.
|
|
29
|
+
*
|
|
30
|
+
* `onWait` is the three lane's own seam (`ingest-root-adapter.ts` passes
|
|
31
|
+
* `setCaptureWait`): the capture window PARKS while the tab is hidden, and a
|
|
32
|
+
* parked wait is otherwise indistinguishable from a hung mount at every door.
|
|
33
|
+
* Threading it here is what lets `vgai status` say "waiting for the first
|
|
34
|
+
* visible frame — the tab is hidden" over a canvas ingest too.
|
|
28
35
|
*/
|
|
29
36
|
export async function mountIngestGame2D(
|
|
30
37
|
pixiNamespace: unknown,
|
|
31
38
|
game: IngestGame2D,
|
|
32
|
-
opts: {
|
|
39
|
+
opts: {
|
|
40
|
+
captureTimeoutMs?: number;
|
|
41
|
+
onWait?: (wait: VisibleCaptureWindow | null) => void;
|
|
42
|
+
} = {},
|
|
33
43
|
): Promise<IngestMount2D> {
|
|
34
44
|
const capture = installSceneCapture2D(pixiNamespace);
|
|
35
45
|
try {
|
|
@@ -41,7 +51,10 @@ export async function mountIngestGame2D(
|
|
|
41
51
|
|
|
42
52
|
const timeout = opts.captureTimeoutMs ?? game.captureTimeoutMs ?? 8000;
|
|
43
53
|
try {
|
|
44
|
-
const rt = await capture.waitForCapture(
|
|
54
|
+
const rt = await capture.waitForCapture({
|
|
55
|
+
timeoutMs: timeout,
|
|
56
|
+
...(opts.onWait ? { onWait: opts.onWait } : {}),
|
|
57
|
+
});
|
|
45
58
|
return {
|
|
46
59
|
stage: rt.stage,
|
|
47
60
|
capture,
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The render-pass BRACKET on the Pixi surface — the canvas-substrate answer to
|
|
3
|
+
* the question the three lane answers with the render accessor-trap's
|
|
4
|
+
* `setRenderPassHooks` (`adapter/ingest/scene-capture.ts`): *when does this
|
|
5
|
+
* renderer's pass begin and end?*
|
|
6
|
+
*
|
|
7
|
+
* ## Why the renderer's own runners, and not another wrap
|
|
8
|
+
*
|
|
9
|
+
* Pixi's `AbstractRenderer.render()` emits five runners in a fixed order —
|
|
10
|
+
* `prerender → renderStart → render → renderEnd → postrender` (verified in the
|
|
11
|
+
* installed pixi.js 8.19,
|
|
12
|
+
* `rendering/renderers/shared/system/AbstractRenderer.mjs`). EVERY GL draw the
|
|
13
|
+
* pass issues happens between the first and the last: the batcher draws under
|
|
14
|
+
* `render`, and even the back-buffer blit, the one draw that is easy to miss,
|
|
15
|
+
* runs under `renderStart`/`renderEnd` (`gl/GlBackBufferSystem.mjs`). So a
|
|
16
|
+
* listener added to `prerender` and `postrender` brackets the whole pass using
|
|
17
|
+
* the renderer's OWN published seam — no instance shadowing, no prototype
|
|
18
|
+
* patch, no ordering assumption beyond the one Pixi itself guarantees.
|
|
19
|
+
*
|
|
20
|
+
* `SystemRunner.add(item)` binds by METHOD NAME (`item[runnerName]` must
|
|
21
|
+
* exist) and `remove(item)` takes it back off, so installing and uninstalling
|
|
22
|
+
* is symmetric and touches nothing else on the renderer.
|
|
23
|
+
*
|
|
24
|
+
* ## Honest absence
|
|
25
|
+
*
|
|
26
|
+
* A renderer that publishes no such runners is not bracketed by guesswork:
|
|
27
|
+
* this returns `null`, and the caller treats that exactly like a missing
|
|
28
|
+
* WebGL2 context — the capability is absent, never faked.
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
import type { RenderPassHooks } from '../adapter/ingest/scene-capture';
|
|
32
|
+
|
|
33
|
+
/** The slice of a Pixi renderer this reads — structural, so a headless test
|
|
34
|
+
* hands in a plain object with two runners and nothing else. */
|
|
35
|
+
interface RunnerLike {
|
|
36
|
+
add(item: unknown): unknown;
|
|
37
|
+
remove(item: unknown): unknown;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
interface RendererWithRunners {
|
|
41
|
+
runners?: {
|
|
42
|
+
prerender?: RunnerLike;
|
|
43
|
+
postrender?: RunnerLike;
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function runnerPair(renderer: unknown): { pre: RunnerLike; post: RunnerLike } | null {
|
|
48
|
+
if (!renderer || typeof renderer !== 'object') return null;
|
|
49
|
+
const runners = (renderer as RendererWithRunners).runners;
|
|
50
|
+
const pre = runners?.prerender;
|
|
51
|
+
const post = runners?.postrender;
|
|
52
|
+
if (!pre || typeof pre.add !== 'function' || typeof pre.remove !== 'function') return null;
|
|
53
|
+
if (!post || typeof post.add !== 'function' || typeof post.remove !== 'function') return null;
|
|
54
|
+
return { pre, post };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Bracket a Pixi renderer's render pass with `hooks.before`/`hooks.after`.
|
|
59
|
+
*
|
|
60
|
+
* Returns the uninstall function, or `null` when this renderer publishes no
|
|
61
|
+
* `prerender`/`postrender` runners — the honest "cannot bracket this", which
|
|
62
|
+
* the caller reads as "no render-debug capability here".
|
|
63
|
+
*
|
|
64
|
+
* Neither hook may throw — the same contract the three lane's `RenderPassHooks`
|
|
65
|
+
* carries, and for a sharper reason here: these run as listeners on the
|
|
66
|
+
* renderer's own runners, so a throwing `before` aborts the game's frame AND
|
|
67
|
+
* skips the `postrender` emit that would have closed the pass. The assembly
|
|
68
|
+
* point (`ingest/ingest-render-debug.ts`) is where that contract is kept.
|
|
69
|
+
*/
|
|
70
|
+
export function installPixiRenderPassBracket(
|
|
71
|
+
renderer: unknown,
|
|
72
|
+
hooks: RenderPassHooks,
|
|
73
|
+
): (() => void) | null {
|
|
74
|
+
const pair = runnerPair(renderer);
|
|
75
|
+
if (!pair) return null;
|
|
76
|
+
|
|
77
|
+
// One object per runner: `SystemRunner.add` binds by method name, so the
|
|
78
|
+
// `prerender` listener and the `postrender` listener cannot be the same
|
|
79
|
+
// object unless it answers to both — keeping them separate makes each
|
|
80
|
+
// `remove()` exact.
|
|
81
|
+
const preListener = {
|
|
82
|
+
prerender() {
|
|
83
|
+
hooks.before();
|
|
84
|
+
},
|
|
85
|
+
};
|
|
86
|
+
const postListener = {
|
|
87
|
+
postrender() {
|
|
88
|
+
hooks.after();
|
|
89
|
+
},
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
pair.pre.add(preListener);
|
|
93
|
+
pair.post.add(postListener);
|
|
94
|
+
|
|
95
|
+
let uninstalled = false;
|
|
96
|
+
return () => {
|
|
97
|
+
if (uninstalled) return;
|
|
98
|
+
uninstalled = true;
|
|
99
|
+
pair.pre.remove(preListener);
|
|
100
|
+
pair.post.remove(postListener);
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* The captured Pixi renderer's real WebGL2 context, or `undefined`.
|
|
106
|
+
*
|
|
107
|
+
* `WebGLRenderer.gl` is the renderer's own published field — `GlContextSystem`
|
|
108
|
+
* assigns it (`this._renderer.gl = gl`,
|
|
109
|
+
* `rendering/renderers/gl/context/GlContextSystem.mjs`) as part of
|
|
110
|
+
* initialisation, and `WebGLRenderer.d.ts` declares it. A WebGPU renderer, or
|
|
111
|
+
* a renderer that has not initialised a context, simply has none — which is the
|
|
112
|
+
* answer, not an error.
|
|
113
|
+
*/
|
|
114
|
+
export function pixiRenderingContext(renderer: unknown): unknown {
|
|
115
|
+
if (!renderer || typeof renderer !== 'object') return undefined;
|
|
116
|
+
return (renderer as { gl?: unknown }).gl;
|
|
117
|
+
}
|
|
@@ -18,6 +18,27 @@
|
|
|
18
18
|
* all, and its mount fails by name.
|
|
19
19
|
*/
|
|
20
20
|
|
|
21
|
+
import {
|
|
22
|
+
documentVisibilityClock,
|
|
23
|
+
startVisibleCaptureWindow,
|
|
24
|
+
type VisibilityClock,
|
|
25
|
+
type VisibleCaptureWindow,
|
|
26
|
+
} from '../adapter/ingest/visible-capture-window';
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Options for {@link SceneCapture2DHandle.waitForCapture} — the same shape the
|
|
30
|
+
* three lane's `CaptureWaitOptions` has, because it is the same wait. Passing a
|
|
31
|
+
* bare number is `{ timeoutMs }`, which is what every existing caller does.
|
|
32
|
+
*/
|
|
33
|
+
export interface CaptureWait2DOptions {
|
|
34
|
+
/** The capture window, in VISIBLE milliseconds (default 10s). */
|
|
35
|
+
timeoutMs?: number | undefined;
|
|
36
|
+
/** Injected in tests; defaults to the document's own visibility. */
|
|
37
|
+
visibility?: VisibilityClock | undefined;
|
|
38
|
+
/** Live view of the wait while it runs, `null` the moment it ends. */
|
|
39
|
+
onWait?: ((wait: VisibleCaptureWindow | null) => void) | undefined;
|
|
40
|
+
}
|
|
41
|
+
|
|
21
42
|
/** A live Pixi runtime captured from an external PixiJS game. */
|
|
22
43
|
export interface CapturedRuntime2D {
|
|
23
44
|
/** The game's root stage Container. */
|
|
@@ -30,7 +51,24 @@ export interface CapturedRuntime2D {
|
|
|
30
51
|
|
|
31
52
|
export interface SceneCapture2DHandle {
|
|
32
53
|
readonly captured: CapturedRuntime2D | null;
|
|
33
|
-
|
|
54
|
+
/**
|
|
55
|
+
* Resolve once the game's stage is captured.
|
|
56
|
+
*
|
|
57
|
+
* The timeout is a budget of **VISIBLE** time, not wall-clock time — the same
|
|
58
|
+
* rule the three lane has had since `visible-capture-window.ts`, and for the
|
|
59
|
+
* same measured reason: a hidden document cannot render (the browser parks
|
|
60
|
+
* rAF), so counting hidden time against the game counts time it was not
|
|
61
|
+
* allowed to use. MEASURED on the bubbo-bubbo ingest with a plain
|
|
62
|
+
* `setTimeout` here: the mount succeeded when the auto-opened tab happened to
|
|
63
|
+
* be in front and died terminally 8s later when it was behind — "canvas
|
|
64
|
+
* ingest: game … rendered no capturable frame", with foregrounding the tab
|
|
65
|
+
* afterwards changing nothing. The normal HUMAN path is the backgrounded one.
|
|
66
|
+
*
|
|
67
|
+
* So the wait PARKS while hidden, the trap stays installed, and the first
|
|
68
|
+
* frame the tab draws after it comes forward is trapped exactly as it would
|
|
69
|
+
* have been at boot. Rejects only when the window is spent while VISIBLE.
|
|
70
|
+
*/
|
|
71
|
+
waitForCapture(options?: number | CaptureWait2DOptions): Promise<CapturedRuntime2D>;
|
|
34
72
|
getDrawCount(): number;
|
|
35
73
|
/** Pause/resume the captured game's own ticker (loop-host gating). */
|
|
36
74
|
setPaused(paused: boolean): void;
|
|
@@ -84,24 +122,36 @@ export function installSceneCapture2D(pixiNamespace: unknown): SceneCapture2DHan
|
|
|
84
122
|
if (paused) app.ticker.stop();
|
|
85
123
|
else app.ticker.start();
|
|
86
124
|
},
|
|
87
|
-
waitForCapture(
|
|
125
|
+
waitForCapture(options) {
|
|
126
|
+
const opts: CaptureWait2DOptions =
|
|
127
|
+
typeof options === 'number' ? { timeoutMs: options } : (options ?? {});
|
|
128
|
+
const timeoutMs = opts.timeoutMs ?? 10_000;
|
|
88
129
|
if (captured) return Promise.resolve(captured);
|
|
89
130
|
return new Promise<CapturedRuntime2D>((resolve, reject) => {
|
|
90
|
-
const
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
131
|
+
const captureWindow = startVisibleCaptureWindow({
|
|
132
|
+
budgetMs: timeoutMs,
|
|
133
|
+
clock: opts.visibility ?? documentVisibilityClock(),
|
|
134
|
+
onExpire: () => {
|
|
135
|
+
const i = waiters.indexOf(wrapped);
|
|
136
|
+
if (i >= 0) waiters.splice(i, 1);
|
|
137
|
+
opts.onWait?.(null);
|
|
138
|
+
reject(
|
|
139
|
+
new Error(
|
|
140
|
+
`pixi scene capture timed out after ${timeoutMs}ms of VISIBLE time ` +
|
|
141
|
+
`(${Math.round(captureWindow.elapsedHiddenMs())}ms hidden, which is not counted — ` +
|
|
142
|
+
'a hidden tab cannot render) — the game never rendered, or it bundles its own ' +
|
|
143
|
+
'(un-shared) copy of pixi.js.',
|
|
144
|
+
),
|
|
145
|
+
);
|
|
146
|
+
},
|
|
147
|
+
});
|
|
100
148
|
const wrapped = (rt: CapturedRuntime2D) => {
|
|
101
|
-
|
|
149
|
+
captureWindow.cancel();
|
|
150
|
+
opts.onWait?.(null);
|
|
102
151
|
resolve(rt);
|
|
103
152
|
};
|
|
104
153
|
waiters.push(wrapped);
|
|
154
|
+
opts.onWait?.(captureWindow);
|
|
105
155
|
});
|
|
106
156
|
},
|
|
107
157
|
uninstall() {
|
|
@@ -6,8 +6,20 @@ import type { Physics2DRegistry } from './physics-registry';
|
|
|
6
6
|
* Lets the editor coordinate editing of a Rapier-2D-driven entity WITHOUT owning the
|
|
7
7
|
* simulation: report transform ownership, then freeze → commit → unfreeze so a drag
|
|
8
8
|
* sticks instead of being stomped by the next physics step.
|
|
9
|
+
*
|
|
10
|
+
* KEYED BY THE DISPLAY OBJECT, and that is this surface's own vocabulary rather
|
|
11
|
+
* than a shortcut: the canvas gizmo's real caller
|
|
12
|
+
* (`editor/src/authoring/pixi-live-write-target.ts`) resolves node id → display
|
|
13
|
+
* itself before it asks, so there is no node-id hop on this lane at all.
|
|
14
|
+
*
|
|
15
|
+
* `keyedBy` is REQUIRED here and optional on the 3D {@link PhysicsAdapter} —
|
|
16
|
+
* see that interface's doc comment for why the union has to be tagged, and why
|
|
17
|
+
* the newcomer is the shape obliged to say so.
|
|
9
18
|
*/
|
|
10
19
|
export interface PhysicsAdapter2D {
|
|
20
|
+
/** Discriminates this shape from the node-id-keyed {@link PhysicsAdapter}
|
|
21
|
+
* inside `SystemAdapters['physics']`. */
|
|
22
|
+
readonly keyedBy: 'display';
|
|
11
23
|
ownerOf(display: Container): 'physics' | 'none';
|
|
12
24
|
freeze(display: Container): void;
|
|
13
25
|
commit(display: Container, position: [number, number], rotation: number): void;
|
|
@@ -18,6 +30,7 @@ export function createPhysicsAdapter2D(physics: Physics2DRegistry): PhysicsAdapt
|
|
|
18
30
|
// Remember each frozen body's prior type so unfreeze restores it.
|
|
19
31
|
const frozen = new WeakMap<Container, number>();
|
|
20
32
|
return {
|
|
33
|
+
keyedBy: 'display',
|
|
21
34
|
ownerOf(display) {
|
|
22
35
|
const refs = physics.get(display);
|
|
23
36
|
if (!refs) return 'none';
|
|
@@ -47,3 +60,31 @@ export function createPhysicsAdapter2D(physics: Physics2DRegistry): PhysicsAdapt
|
|
|
47
60
|
},
|
|
48
61
|
};
|
|
49
62
|
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* ONE carrier over several, each owning a DIFFERENT set of display objects.
|
|
66
|
+
*
|
|
67
|
+
* The canvas ingest mount is the case: the host always builds a registry-backed
|
|
68
|
+
* carrier for objects the editor itself created, and an ingested game may ALSO
|
|
69
|
+
* declare one over its own simulation's objects. Neither is a superset of the
|
|
70
|
+
* other and neither can answer for the other's objects, so the composition is
|
|
71
|
+
* ownership-routed rather than layered — `ownerOf` asks each in order and the
|
|
72
|
+
* FIRST to claim the object handles every other call for it. A display object
|
|
73
|
+
* nobody claims answers `'none'` and the freeze/commit/unfreeze trio no-ops,
|
|
74
|
+
* which is exactly what a single unclaiming carrier already does.
|
|
75
|
+
*
|
|
76
|
+
* Order is precedence: earlier carriers win a (never-expected) double claim,
|
|
77
|
+
* so the caller states its own preference instead of this function inventing
|
|
78
|
+
* one.
|
|
79
|
+
*/
|
|
80
|
+
export function composePhysicsAdapters2D(carriers: readonly PhysicsAdapter2D[]): PhysicsAdapter2D {
|
|
81
|
+
const ownerFor = (display: Container): PhysicsAdapter2D | null =>
|
|
82
|
+
carriers.find((carrier) => carrier.ownerOf(display) === 'physics') ?? null;
|
|
83
|
+
return {
|
|
84
|
+
keyedBy: 'display',
|
|
85
|
+
ownerOf: (display) => (ownerFor(display) ? 'physics' : 'none'),
|
|
86
|
+
freeze: (display) => ownerFor(display)?.freeze(display),
|
|
87
|
+
commit: (display, position, rotation) => ownerFor(display)?.commit(display, position, rotation),
|
|
88
|
+
unfreeze: (display) => ownerFor(display)?.unfreeze(display),
|
|
89
|
+
};
|
|
90
|
+
}
|