@forgeax/engine-vfx-render 0.1.6 → 0.1.7

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@forgeax/engine-vfx-render",
3
- "version": "0.1.6",
3
+ "version": "0.1.7",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",
@@ -22,13 +22,13 @@
22
22
  "LICENSE"
23
23
  ],
24
24
  "dependencies": {
25
- "@forgeax/engine-assets-runtime": "0.1.6",
26
- "@forgeax/engine-ecs": "0.1.6",
27
- "@forgeax/engine-math": "0.1.6",
28
- "@forgeax/engine-render": "0.1.6",
29
- "@forgeax/engine-scene": "0.1.6",
30
- "@forgeax/engine-types": "0.1.6",
31
- "@forgeax/engine-vfx": "0.1.6"
25
+ "@forgeax/engine-assets-runtime": "0.1.7",
26
+ "@forgeax/engine-ecs": "0.1.7",
27
+ "@forgeax/engine-math": "0.1.7",
28
+ "@forgeax/engine-render": "0.1.7",
29
+ "@forgeax/engine-scene": "0.1.7",
30
+ "@forgeax/engine-types": "0.1.7",
31
+ "@forgeax/engine-vfx": "0.1.7"
32
32
  },
33
33
  "devDependencies": {},
34
34
  "forgeax": {
@@ -1,4 +1,5 @@
1
1
  import { World } from '@forgeax/engine-ecs';
2
+ import type { Asset, MaterialAsset } from '@forgeax/engine-types';
2
3
  import {
3
4
  ParticleEffectPlayer,
4
5
  VFX_GPU_RUNTIME_RESOURCE_KEY,
@@ -45,6 +46,89 @@ describe('GPU VFX public host', () => {
45
46
  });
46
47
  });
47
48
 
49
+ it('preserves the asset registry receiver while planning a material projection', async () => {
50
+ const world = new World();
51
+ const lookups: string[] = [];
52
+ class StatefulAssets {
53
+ readonly material: MaterialAsset = { kind: 'material', values: {} };
54
+ readonly loaders = { registerPackLoader: () => {} };
55
+
56
+ lookup<T extends Asset = Asset>(guid: string): T | undefined {
57
+ if (this !== assets) throw new Error('asset registry receiver was lost');
58
+ lookups.push(guid);
59
+ return guid === 'material-guid' ? (this.material as T) : undefined;
60
+ }
61
+ }
62
+ const assets = new StatefulAssets();
63
+ const host = createVfxRuntimeHost({
64
+ camera: {
65
+ read: () => ({
66
+ position: new Float32Array(3),
67
+ right: new Float32Array([1, 0, 0]),
68
+ up: new Float32Array([0, 1, 0]),
69
+ viewProjection: new Float32Array([1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1]),
70
+ }),
71
+ },
72
+ });
73
+ expect(await host.attachWorld({ world, assets })).toMatchObject({ ok: true });
74
+
75
+ const intent = {
76
+ player: world.spawn().unwrap(),
77
+ fixedDelta: 1 / 60,
78
+ phaseTick: 0,
79
+ seed: 1,
80
+ playCycle: 0,
81
+ spawnCount: 1,
82
+ firstParticleId: 0,
83
+ reset: true,
84
+ channelInputs: [],
85
+ emitter: {
86
+ id: 'receiver-check',
87
+ capacity: 4,
88
+ backend: { required: 'gpu' },
89
+ space: 'world',
90
+ simulationWhenCulled: 'continue',
91
+ schedule: { rate: 0, bursts: [] },
92
+ bounds: { kind: 'sphere', center: [0, 0, 0], radius: 1 },
93
+ wgsl: '// receiver check',
94
+ reflection: { entryPoints: [], bindings: [] },
95
+ renderers: [{ kind: 'billboard', material: 'material-guid' }],
96
+ },
97
+ } as unknown as VfxGpuTickIntent;
98
+ const planned = host.feature.plan(
99
+ {
100
+ worlds: [
101
+ {
102
+ world,
103
+ runtime: {
104
+ isEmitterSessionEnabled: () => true,
105
+ setEmitterCameraVisibility: () => {},
106
+ markEventDispatched: () => {},
107
+ commit: () => {},
108
+ },
109
+ camera: {
110
+ position: new Float32Array(3),
111
+ right: new Float32Array([1, 0, 0]),
112
+ up: new Float32Array([0, 1, 0]),
113
+ viewProjection: new Float32Array([1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1]),
114
+ },
115
+ intents: [intent],
116
+ },
117
+ ],
118
+ frameNumber: 1,
119
+ } as never,
120
+ {
121
+ targets: [],
122
+ caps: {},
123
+ frame: { frameNumber: 1 },
124
+ generation: 1,
125
+ } as never,
126
+ );
127
+ expect(planned.ok).toBe(true);
128
+ expect(lookups).toEqual(['material-guid']);
129
+ await host.detachWorld({ world });
130
+ });
131
+
48
132
  it('fences preview controls to one attached host generation', async () => {
49
133
  const world = new World();
50
134
  const assets = {
@@ -1,11 +1,17 @@
1
- import type { AssetRegistry as LegacyAssetRegistry } from '@forgeax/engine-assets-runtime';
2
- import type { AssetRegistry as CoreAssetRegistry } from '@forgeax/engine-assets-runtime/internal';
3
- import {
4
- type AssetRegistryResolver,
5
- getAssetRegistryResolver,
6
- } from '@forgeax/engine-assets-runtime/internal';
1
+ import type {
2
+ AssetRegistryResolver,
3
+ AssetRegistry as LegacyAssetRegistry,
4
+ RuntimeAssetRegistry,
5
+ } from '@forgeax/engine-assets-runtime';
6
+ import { getAssetRegistryResolver } from '@forgeax/engine-assets-runtime';
7
7
  import { createWorldContext, type EntityHandle, type World } from '@forgeax/engine-ecs';
8
- import type { AssetDecoderLease, MaterialAsset, MeshAsset, Result } from '@forgeax/engine-types';
8
+ import type {
9
+ Asset,
10
+ AssetDecoderLease,
11
+ MaterialAsset,
12
+ MeshAsset,
13
+ Result,
14
+ } from '@forgeax/engine-types';
9
15
  import { err, ok } from '@forgeax/engine-types';
10
16
  import type {
11
17
  VfxDataInterfaceError,
@@ -32,7 +38,22 @@ import {
32
38
  type VfxDataInterfaceRegistry,
33
39
  } from './data-interface-providers.js';
34
40
 
35
- type AssetRegistry = LegacyAssetRegistry | CoreAssetRegistry;
41
+ /**
42
+ * Public authoring projection accepted by the VFX host. Editor owns the
43
+ * projection, while the Engine App still owns bytes/decoders/cache. Keeping
44
+ * this small structural seam public lets the Editor pass that projection
45
+ * without importing the runtime registry's internal module or fabricating a
46
+ * second registry identity.
47
+ */
48
+ export interface VfxAuthoringAssetRegistry {
49
+ readonly loaders: {
50
+ registerPackLoader(loader: unknown): void;
51
+ };
52
+ lookup<T extends Asset = Asset>(guid: string): T | undefined;
53
+ readonly load?: (guid: string, kind: string) => Promise<Result<unknown, unknown>>;
54
+ }
55
+
56
+ type AssetRegistry = LegacyAssetRegistry | RuntimeAssetRegistry | VfxAuthoringAssetRegistry;
36
57
 
37
58
  export interface VfxRuntimeHostOptions {
38
59
  readonly camera: ParticleRenderCameraSource;
@@ -113,7 +134,7 @@ export interface VfxRuntimeHost {
113
134
  }
114
135
 
115
136
  /** Install the VFX owner decoder into the core registry. */
116
- export function installVfxRuntimeDecoder(registry: CoreAssetRegistry): AssetDecoderLease {
137
+ export function installVfxRuntimeDecoder(registry: RuntimeAssetRegistry): AssetDecoderLease {
117
138
  return registry.installDecoder(vfxGpuEffectContribution.kind, vfxGpuEffectContribution.decoder);
118
139
  }
119
140
 
@@ -174,7 +195,12 @@ export function createVfxRuntimeHost(options: VfxRuntimeHostOptions): VfxRuntime
174
195
  const cached = attached.renderAssets.get(key);
175
196
  if (cached?.kind === kind) return cached as Kind extends 'material' ? MaterialAsset : MeshAsset;
176
197
  if ('lookup' in attached.assets) {
177
- const legacy = attached.assets.lookup(guid);
198
+ const lookup = attached.assets.lookup as (guid: string) => Asset | undefined;
199
+ // Preserve the registry receiver: the Engine-owned legacy class uses
200
+ // `this` to read its load-state/catalogue maps. The Editor facade may
201
+ // expose an arrow function, but calling through the owner is safe for
202
+ // both shapes and avoids turning a lookup into a swallowed plan error.
203
+ const legacy = lookup.call(attached.assets, guid);
178
204
  if (legacy?.kind === kind) {
179
205
  attached.renderAssets.set(key, legacy);
180
206
  return legacy as Kind extends 'material' ? MaterialAsset : MeshAsset;
@@ -185,10 +211,17 @@ export function createVfxRuntimeHost(options: VfxRuntimeHostOptions): VfxRuntime
185
211
  attached.renderAssets.set(key, resolved);
186
212
  return resolved as Kind extends 'material' ? MaterialAsset : MeshAsset;
187
213
  }
188
- if ('load' in attached.assets && !attached.pendingRenderAssets.has(key)) {
214
+ if (
215
+ 'load' in attached.assets &&
216
+ typeof attached.assets.load === 'function' &&
217
+ !attached.pendingRenderAssets.has(key)
218
+ ) {
189
219
  const epoch = attached.catalogEpoch;
190
- const request = attached.assets
191
- .load(guid, kind)
220
+ const load = attached.assets.load as (
221
+ guid: string,
222
+ kind: string,
223
+ ) => Promise<Result<MaterialAsset | MeshAsset, unknown>>;
224
+ const request = load(guid, kind)
192
225
  .then((result) => {
193
226
  if (!result.ok || worlds.get(world) !== attached || attached.catalogEpoch !== epoch)
194
227
  return;
@@ -327,10 +360,17 @@ export function createVfxRuntimeHost(options: VfxRuntimeHostOptions): VfxRuntime
327
360
  if (worlds.has(world)) return ok({ state: 'already-attached' });
328
361
  if (!registries.has(assets)) {
329
362
  try {
330
- if ('installDecoder' in assets) {
363
+ // The Editor facade deliberately exposes the legacy LoaderRegistry
364
+ // while projecting the Engine-owned App.assets instance. Prefer
365
+ // that public authoring seam before looking for the five-action
366
+ // runtime registry; otherwise a facade that happens to expose
367
+ // `load` would be misclassified as a decoder owner.
368
+ if ('loaders' in assets) {
369
+ assets.loaders.registerPackLoader(vfxGpuEffectPackLoader);
370
+ } else if ('installDecoder' in assets) {
331
371
  assets.installDecoder(vfxGpuEffectContribution.kind, vfxGpuEffectContribution.decoder);
332
372
  } else {
333
- assets.loaders.registerPackLoader(vfxGpuEffectPackLoader);
373
+ throw new TypeError('VFX assets registry exposes neither loaders nor installDecoder');
334
374
  }
335
375
  registries.add(assets);
336
376
  } catch (cause) {
@@ -362,7 +402,7 @@ export function createVfxRuntimeHost(options: VfxRuntimeHostOptions): VfxRuntime
362
402
  );
363
403
  }
364
404
  let resolver: AssetRegistryResolver | undefined;
365
- if ('installDecoder' in assets) {
405
+ if (!('loaders' in assets) && 'installDecoder' in assets) {
366
406
  try {
367
407
  resolver = getAssetRegistryResolver(assets);
368
408
  } catch {
package/src/index.ts CHANGED
@@ -46,6 +46,7 @@ export {
46
46
  createVfxDataInterfaceRegistry,
47
47
  } from './host/data-interface-providers.js';
48
48
  export type {
49
+ VfxAuthoringAssetRegistry,
49
50
  VfxRuntimeHost,
50
51
  VfxRuntimeHostControl,
51
52
  VfxRuntimeHostControlError,