@galfus/engine 0.0.0-stage → 0.31.1-alpha

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (80) hide show
  1. package/README.md +190 -2
  2. package/package.json +59 -4
  3. package/src/anchors/cache.ts +101 -0
  4. package/src/anchors/component.ts +123 -0
  5. package/src/anchors/hierarchy.ts +192 -0
  6. package/src/anchors/index.ts +8 -0
  7. package/src/anchors/solver.ts +247 -0
  8. package/src/anchors/strategy.ts +142 -0
  9. package/src/animation/blending.ts +163 -0
  10. package/src/animation/clip.ts +132 -0
  11. package/src/animation/coordinator.ts +568 -0
  12. package/src/animation/index.ts +7 -0
  13. package/src/animation/native-targets.ts +37 -0
  14. package/src/animation/player.ts +287 -0
  15. package/src/audio/index.ts +4 -0
  16. package/src/audio/manager.ts +371 -0
  17. package/src/collision/components.ts +211 -0
  18. package/src/collision/index.ts +16 -0
  19. package/src/collision/query2d.ts +167 -0
  20. package/src/collision/query3d.ts +170 -0
  21. package/src/ecs/component.ts +130 -0
  22. package/src/ecs/entity.ts +75 -0
  23. package/src/ecs/index.ts +9 -0
  24. package/src/ecs/intent.ts +162 -0
  25. package/src/ecs/metadata.ts +44 -0
  26. package/src/ecs/system.ts +224 -0
  27. package/src/ecs/world.ts +291 -0
  28. package/src/index.ts +19 -0
  29. package/src/interaction/focus.ts +327 -0
  30. package/src/interaction/index.ts +9 -0
  31. package/src/interaction/listener.ts +58 -0
  32. package/src/interaction/snapshot2d.ts +210 -0
  33. package/src/interaction/snapshot3d.ts +214 -0
  34. package/src/interaction/system.ts +544 -0
  35. package/src/interaction/types.ts +27 -0
  36. package/src/math/common.ts +67 -0
  37. package/src/math/index.ts +17 -0
  38. package/src/math/mat2d.ts +320 -0
  39. package/src/math/mat4.ts +576 -0
  40. package/src/math/quat.ts +438 -0
  41. package/src/math/vec2.ts +161 -0
  42. package/src/math/vec3.ts +242 -0
  43. package/src/math/vec4.ts +171 -0
  44. package/src/multiverse/coordinator.ts +540 -0
  45. package/src/multiverse/index.ts +13 -0
  46. package/src/multiverse/presentation-graph.ts +306 -0
  47. package/src/multiverse/presentation-links.ts +134 -0
  48. package/src/multiverse/realm.ts +49 -0
  49. package/src/platform/index.ts +2 -0
  50. package/src/platform/platform.ts +149 -0
  51. package/src/platform/window.ts +181 -0
  52. package/src/realm2d/components.ts +254 -0
  53. package/src/realm2d/index.ts +12 -0
  54. package/src/realm2d/presets/index.ts +36 -0
  55. package/src/realm2d/realm.ts +522 -0
  56. package/src/realm2d/sync.ts +284 -0
  57. package/src/realm2d/utils/index.ts +32 -0
  58. package/src/realm3d/components.ts +341 -0
  59. package/src/realm3d/geometry.ts +224 -0
  60. package/src/realm3d/index.ts +12 -0
  61. package/src/realm3d/presets/index.ts +67 -0
  62. package/src/realm3d/realm.ts +588 -0
  63. package/src/realm3d/resource-ids.ts +62 -0
  64. package/src/realm3d/skinning.ts +128 -0
  65. package/src/realm3d/sync.ts +442 -0
  66. package/src/realm3d/utils/index.ts +26 -0
  67. package/src/resources/index.ts +4 -0
  68. package/src/resources/manager.ts +92 -0
  69. package/src/runtime.ts +588 -0
  70. package/src/spatial/dirty_registry.ts +217 -0
  71. package/src/spatial/flags.ts +14 -0
  72. package/src/spatial/index.ts +6 -0
  73. package/src/spatial/metrics.ts +124 -0
  74. package/src/text/editor.ts +224 -0
  75. package/src/text/encoding.ts +38 -0
  76. package/src/text/index.ts +13 -0
  77. package/src/text/input.ts +160 -0
  78. package/src/text/keyboard.ts +50 -0
  79. package/src/text/svg-decorations.ts +147 -0
  80. package/src/types.ts +85 -0
@@ -0,0 +1,588 @@
1
+ /**
2
+ * High-level Realm3D manager combining an ECS World, Anchor solver, and core sync.
3
+ */
4
+
5
+ import type {
6
+ Camera3DDesc,
7
+ CameraLookAt3D,
8
+ CameraLookAt3DStateDesc,
9
+ EnvironmentRealm3DDesc,
10
+ GalfusEngine,
11
+ InstancedModel3DDesc,
12
+ Light3DDesc,
13
+ Material3DDesc,
14
+ Model3DDesc,
15
+ ParticleEmitter3DDesc,
16
+ Portal3DDesc,
17
+ TransitionConfig
18
+ } from '@galfus/transport-types';
19
+ import { assertValidLogicalId } from '@galfus/transport-types';
20
+ import type { Transform3DProps } from '../anchors/component';
21
+ import { HierarchyCoordinator } from '../anchors/hierarchy';
22
+ import { AnchorSolveSystem } from '../anchors/solver';
23
+ import type { AnimationCoordinator } from '../animation/coordinator';
24
+ import { createAnimationPlaybackSystem } from '../animation/player';
25
+ import { Collider3DComponent, type ColliderOptions } from '../collision/components';
26
+ import { raycast3D } from '../collision/query3d';
27
+ import type { EntityId } from '../ecs/entity';
28
+ import { RESERVED_PRIORITY_INTERNAL_RESOLVERS, type SystemContext } from '../ecs/system';
29
+ import { EcsWorld } from '../ecs/world';
30
+ import type { RealmConnector, RealmId, RealmKind } from '../multiverse/realm';
31
+ import { ResourceManager } from '../resources/manager';
32
+ import type { TransitionId } from '../types';
33
+ import {
34
+ createCamera3D,
35
+ createInstancedModel3D,
36
+ createLight3D,
37
+ createMesh3D,
38
+ createModel3D,
39
+ createParticleEmitter3D,
40
+ createPortal3D,
41
+ createSkin3D,
42
+ type GeometryResourceEntry,
43
+ type MaterialResourceEntry,
44
+ type Mesh3DComponent,
45
+ type Mesh3DDesc,
46
+ type Model3DComponent,
47
+ type Primitive3DKind
48
+ } from './components';
49
+ import {
50
+ type GeometryData,
51
+ type MorphTargetData,
52
+ packGeometryData,
53
+ prepareMorphTargets
54
+ } from './geometry';
55
+ import { resolveCameraId, resolveEmitterId, storeCameraLookAt } from './resource-ids';
56
+ import { SkinningSolveSystem } from './skinning';
57
+ import { type Realm3DResourceProvider, Realm3DSyncSystem } from './sync';
58
+
59
+ export class Realm3D implements RealmConnector, Realm3DResourceProvider {
60
+ readonly realmKind: RealmKind = '3d';
61
+ readonly realmId: RealmId;
62
+ readonly world = new EcsWorld();
63
+ readonly solver = new AnchorSolveSystem();
64
+ readonly skinningSystem: SkinningSolveSystem;
65
+ readonly syncSystem: Realm3DSyncSystem;
66
+ resources: ResourceManager = new ResourceManager();
67
+ readonly hierarchy: HierarchyCoordinator<'3d'>;
68
+
69
+ private readonly _geometries = new Map<number, GeometryResourceEntry>();
70
+ private readonly _pendingMorphWeights = new Map<number, Float32Array>();
71
+ private readonly _materials = new Map<number, MaterialResourceEntry>();
72
+ private _engine: GalfusEngine | null = null;
73
+ private _nextResourceLogicalId = 1;
74
+ private _preRenderSteppedFrame = -1;
75
+ private _disposed = false;
76
+
77
+ constructor(realmId: RealmId | number) {
78
+ this.realmId = realmId as RealmId;
79
+ assertValidLogicalId(this.realmId, 'RealmId');
80
+ this.hierarchy = new HierarchyCoordinator(this.world, '3d');
81
+ this.skinningSystem = new SkinningSolveSystem(this.realmId, this.solver.cache);
82
+ this.syncSystem = new Realm3DSyncSystem(this.realmId, this.solver.cache);
83
+ this._registerInternalSystems();
84
+ }
85
+
86
+ private _registerInternalSystems(): void {
87
+ this.world.addSystem('update', createAnimationPlaybackSystem(this), { kind: 'ordinary' });
88
+ this.world.addSystem('preRender', this.solver.createSystem(), { kind: 'anchor' });
89
+ this.world.addSystem(
90
+ 'preRender',
91
+ this.skinningSystem.createSystem(() => this._engine),
92
+ { kind: 'sync', priority: RESERVED_PRIORITY_INTERNAL_RESOLVERS }
93
+ );
94
+ }
95
+
96
+ get isDisposed(): boolean {
97
+ return this._disposed;
98
+ }
99
+
100
+ getGeometries(): Iterable<GeometryResourceEntry> {
101
+ return this._geometries.values();
102
+ }
103
+
104
+ getMaterials(): Iterable<MaterialResourceEntry> {
105
+ return this._materials.values();
106
+ }
107
+
108
+ allocateResourceId(): number {
109
+ const id = this._nextResourceLogicalId++;
110
+ assertValidLogicalId(id, 'ResourceId');
111
+ return id;
112
+ }
113
+
114
+ bindEngine(engine: GalfusEngine): void {
115
+ if (this._disposed) return;
116
+ this._engine = engine;
117
+ this.resources.bindEngine(engine);
118
+ engine.realm3dCreate(this.realmId);
119
+ }
120
+
121
+ destroyInstancedModel(instancedModelId: number): void {
122
+ this._engine?.realm3dDestroyInstancedModel(this.realmId, instancedModelId);
123
+ }
124
+
125
+ preRender(context: SystemContext): void {
126
+ if (this._disposed) return;
127
+ this.world.stepPreRender(context);
128
+ this._preRenderSteppedFrame = context.frameId;
129
+ }
130
+
131
+ sync(context: SystemContext): void {
132
+ if (this._disposed) return;
133
+ if (this._preRenderSteppedFrame !== context.frameId) {
134
+ this.preRender(context);
135
+ }
136
+ if (this._engine) {
137
+ this.syncSystem.sync(this.world, this._engine, this);
138
+ for (const [modelId, weights] of this._pendingMorphWeights) {
139
+ this._engine.realm3dSetModelMorphWeights(this.realmId, modelId, weights, weights.length);
140
+ }
141
+ this._pendingMorphWeights.clear();
142
+ }
143
+ }
144
+
145
+ postRender(context: SystemContext): void {
146
+ if (this._disposed) return;
147
+ this.world.stepPostRender(context);
148
+ }
149
+
150
+ reset(): void {
151
+ if (this._disposed) throw new Error('Cannot reset a disposed Realm3D.');
152
+ this.world.reset();
153
+ this.solver.cache.clear();
154
+ this._geometries.clear();
155
+ this._materials.clear();
156
+ this._pendingMorphWeights.clear();
157
+ this._preRenderSteppedFrame = -1;
158
+ this._registerInternalSystems();
159
+ if (this._engine) {
160
+ this.syncSystem.dispose(this._engine);
161
+ this._engine.realm3dDestroy(this.realmId);
162
+ this._engine.realm3dCreate(this.realmId);
163
+ }
164
+ }
165
+
166
+ dispose(): void {
167
+ if (this._disposed) return;
168
+ this._disposed = true;
169
+ this.syncSystem.dispose(this._engine);
170
+ if (this._engine) {
171
+ this._engine.realm3dDestroy(this.realmId);
172
+ }
173
+ this._engine = null;
174
+ this.world.reset();
175
+ this.solver.cache.clear();
176
+ this._geometries.clear();
177
+ this._materials.clear();
178
+ this._pendingMorphWeights.clear();
179
+ }
180
+
181
+ /** Adds a collider to an entity with a matching Model component. */
182
+ addCollider(
183
+ entityId: EntityId,
184
+ shape: import('../collision/components').ColliderShape3D,
185
+ options?: ColliderOptions
186
+ ): number {
187
+ if (!this.world.isAlive(entityId) || !this.world.hasComponent(entityId, 'Model3D'))
188
+ throw new Error('Collider requires a live entity with Model3D.');
189
+ const existing = this.world.getComponent<Collider3DComponent>(entityId, 'Collider3D');
190
+ const component = existing ?? new Collider3DComponent();
191
+ const id = this.allocateResourceId();
192
+ component.add(id, shape, options);
193
+ if (!existing) this.world.attachComponent(entityId, component);
194
+ return id;
195
+ }
196
+
197
+ raycast(
198
+ origin: [number, number, number],
199
+ direction: [number, number, number],
200
+ maxDistance: number,
201
+ mask = 0xffffffff
202
+ ) {
203
+ if (this._disposed) throw new Error('Cannot query a disposed Realm3D.');
204
+ this.solver.solve(this.world, { time: 0, dt: 0, frameId: 0 });
205
+ return raycast3D(this.world, origin, direction, maxDistance, mask);
206
+ }
207
+
208
+ destroyEntity(entityId: EntityId): boolean {
209
+ if (!this.world.isAlive(entityId)) return false;
210
+ const model = this.world.getComponent<Model3DComponent>(entityId, 'Model3D');
211
+ if (model) this._pendingMorphWeights.delete(model.modelId);
212
+ this.hierarchy.detach(entityId);
213
+ this.hierarchy.handleTargetDestroyed(entityId);
214
+ this.solver.cache.remove(entityId);
215
+ return this.world.destroyEntity(entityId);
216
+ }
217
+
218
+ flush(): void {
219
+ if (this._engine) {
220
+ this.solver.solve(this.world, { time: 0, dt: 0, frameId: 0, engine: this._engine });
221
+ this.syncSystem.sync(this.world, this._engine, this);
222
+ }
223
+ }
224
+
225
+ // Resource Management (Geometries & Materials outside ECS)
226
+ createMaterial(desc: Material3DDesc): { materialId: number } {
227
+ const materialId = this.allocateResourceId();
228
+ this._materials.set(materialId, { materialId, desc, isDirty: true });
229
+ return { materialId };
230
+ }
231
+
232
+ destroyMaterial(materialId: number): void {
233
+ this._materials.delete(materialId);
234
+ this._engine?.realm3dDestroyMaterial(this.realmId, materialId);
235
+ }
236
+
237
+ createGeometry(data: GeometryData | Primitive3DKind): { geometryId: number } {
238
+ const geometryId = this.allocateResourceId();
239
+ const primitive: Primitive3DKind =
240
+ 'positions' in data ? { kind: 'custom', ...packGeometryData(data) } : data;
241
+ this._geometries.set(geometryId, { geometryId, primitive, isDirty: true });
242
+ return { geometryId };
243
+ }
244
+
245
+ destroyGeometry(geometryId: number): void {
246
+ this._geometries.delete(geometryId);
247
+ this._engine?.realm3dDestroyGeometry(this.realmId, geometryId);
248
+ }
249
+
250
+ // Fluent Entity Spawning Helpers
251
+ private _attachModel(
252
+ entityId: EntityId,
253
+ id: number,
254
+ transform?: Transform3DProps
255
+ ): Model3DComponent {
256
+ const model = createModel3D(id);
257
+ if (transform) {
258
+ model.setTRS(transform.position, transform.rotation, transform.scale);
259
+ }
260
+ this.world.attachComponent(entityId, model);
261
+ return model;
262
+ }
263
+
264
+ /**
265
+ * Creates a pure spatial node with Model3DComponent (no Mesh attached).
266
+ */
267
+ createSpatialNode(transform?: Transform3DProps): EntityId {
268
+ const entityId = this.world.createEntity();
269
+ this._attachModel(entityId, this.allocateResourceId(), transform);
270
+ return entityId;
271
+ }
272
+
273
+ /**
274
+ * Creates a renderable entity with Model3DComponent and Mesh3DComponent attached.
275
+ */
276
+ createModel(desc: Model3DDesc, transform?: Transform3DProps): EntityId {
277
+ const entityId = this.world.createEntity();
278
+ const model = this._attachModel(entityId, this.allocateResourceId(), transform);
279
+ const mesh = createMesh3D(model.modelId, model.modelId, desc);
280
+ this.world.attachComponent(entityId, mesh);
281
+ return entityId;
282
+ }
283
+
284
+ /**
285
+ * Attaches a Mesh3D rendering component to an existing spatial node entity.
286
+ */
287
+ createMesh(entityId: EntityId, desc: Mesh3DDesc): Mesh3DComponent {
288
+ const model = this.world.getComponent<Model3DComponent>(entityId, 'Model3D');
289
+ if (!model) {
290
+ throw new Error(`Cannot attach Mesh3D: Entity ${entityId} does not have a Model3DComponent.`);
291
+ }
292
+ const meshId = this.allocateResourceId();
293
+ const mesh = createMesh3D(meshId, model.modelId, desc);
294
+ this.world.attachComponent(entityId, mesh);
295
+ return mesh;
296
+ }
297
+
298
+ createSkin(
299
+ modelEntityId: EntityId,
300
+ joints: EntityId[],
301
+ inverseBindMatrices: Float32Array
302
+ ): { entityId: EntityId; skinId: number } {
303
+ const entityId = this.world.createEntity(),
304
+ skinId = this.allocateResourceId();
305
+ this.world.attachComponent(
306
+ entityId,
307
+ createSkin3D(skinId, modelEntityId, joints, inverseBindMatrices)
308
+ );
309
+ return { entityId, skinId };
310
+ }
311
+
312
+ createCamera(
313
+ desc: Camera3DDesc,
314
+ transform?: Transform3DProps
315
+ ): { entityId: EntityId; cameraId: number } {
316
+ const entityId = this.world.createEntity(),
317
+ cameraId = this.allocateResourceId();
318
+ this._attachModel(entityId, cameraId, transform);
319
+ this.world.attachComponent(entityId, createCamera3D(cameraId, desc));
320
+ return { entityId, cameraId };
321
+ }
322
+
323
+ createLight(desc: Light3DDesc, transform?: Transform3DProps): EntityId {
324
+ const entityId = this.world.createEntity(),
325
+ lightId = this.allocateResourceId();
326
+ this._attachModel(entityId, lightId, transform);
327
+ this.world.attachComponent(entityId, createLight3D(lightId, desc));
328
+ return entityId;
329
+ }
330
+
331
+ createPrimitive(
332
+ primitive: Primitive3DKind,
333
+ materialId: number,
334
+ transform?: Transform3DProps
335
+ ): { entityId: EntityId; geometryId: number; modelId: number } {
336
+ const { geometryId } = this.createGeometry(primitive);
337
+ const entityId = this.createModel({ geometryId, materialId }, transform);
338
+ const model = this.world.getComponent<Model3DComponent>(entityId, 'Model3D')!;
339
+ return { entityId, geometryId, modelId: model.modelId };
340
+ }
341
+
342
+ createCube(size: number, materialId: number, transform?: Transform3DProps) {
343
+ return this.createPrimitive({ kind: 'cube', size }, materialId, transform);
344
+ }
345
+
346
+ createPlane(width: number, depth: number, materialId: number, transform?: Transform3DProps) {
347
+ return this.createPrimitive({ kind: 'plane', width, depth }, materialId, transform);
348
+ }
349
+
350
+ createSphere(
351
+ radius: number,
352
+ rings: number,
353
+ sectors: number,
354
+ materialId: number,
355
+ transform?: Transform3DProps
356
+ ) {
357
+ return this.createPrimitive({ kind: 'sphere', radius, rings, sectors }, materialId, transform);
358
+ }
359
+
360
+ createCylinder(r: number, h: number, s: number, matId: number, t?: Transform3DProps) {
361
+ return this.createPrimitive({ kind: 'cylinder', radius: r, height: h, sectors: s }, matId, t);
362
+ }
363
+
364
+ createCapsule(
365
+ r: number,
366
+ ch: number,
367
+ rings: number,
368
+ s: number,
369
+ matId: number,
370
+ t?: Transform3DProps
371
+ ) {
372
+ return this.createPrimitive(
373
+ { kind: 'capsule', radius: r, cylinderHeight: ch, rings, sectors: s },
374
+ matId,
375
+ t
376
+ );
377
+ }
378
+
379
+ createPyramid(w: number, d: number, h: number, matId: number, t?: Transform3DProps) {
380
+ return this.createPrimitive({ kind: 'pyramid', width: w, depth: d, height: h }, matId, t);
381
+ }
382
+
383
+ createTorus(
384
+ majR: number,
385
+ minR: number,
386
+ majS: number,
387
+ minS: number,
388
+ matId: number,
389
+ t?: Transform3DProps
390
+ ) {
391
+ return this.createPrimitive(
392
+ {
393
+ kind: 'torus',
394
+ majorRadius: majR,
395
+ minorRadius: minR,
396
+ majorSegments: majS,
397
+ minorSegments: minS
398
+ },
399
+ matId,
400
+ t
401
+ );
402
+ }
403
+
404
+ createCustomGeometry(
405
+ top: number,
406
+ vData: Uint8Array,
407
+ idx: Uint32Array,
408
+ matId: number,
409
+ t?: Transform3DProps
410
+ ) {
411
+ return this.createPrimitive(
412
+ { kind: 'custom', topology: top, vertexData: vData, indices: idx },
413
+ matId,
414
+ t
415
+ );
416
+ }
417
+
418
+ // Camera LookAt, Environment & Debug Gizmos
419
+ setCameraLookAt(
420
+ camera: number | { cameraId?: number; entityId?: EntityId },
421
+ lookAt: CameraLookAt3D
422
+ ): void {
423
+ const cameraId = resolveCameraId(this.world, camera);
424
+ if (this._engine) {
425
+ if (!this.syncSystem.isCameraSynced(cameraId)) this.flush();
426
+ this._engine.realm3dSetCameraLookAt(this.realmId, cameraId, lookAt);
427
+ storeCameraLookAt(this.world, cameraId, lookAt);
428
+ }
429
+ }
430
+
431
+ getCameraLookAt(cameraId: number): CameraLookAt3DStateDesc | undefined {
432
+ return this._engine?.realm3dGetCameraLookAt(this.realmId, cameraId);
433
+ }
434
+
435
+ clearCameraLookAt(cameraId: number): void {
436
+ if (!this._engine) return;
437
+ this._engine.realm3dClearCameraLookAt(this.realmId, cameraId);
438
+ storeCameraLookAt(this.world, cameraId);
439
+ }
440
+
441
+ setEnvironment(environment: EnvironmentRealm3DDesc): void {
442
+ this._engine?.realm3dSetEnvironment(this.realmId, environment);
443
+ }
444
+
445
+ drawLine(
446
+ x0: number,
447
+ y0: number,
448
+ z0: number,
449
+ x1: number,
450
+ y1: number,
451
+ z1: number,
452
+ r: number,
453
+ g: number,
454
+ b: number,
455
+ a: number
456
+ ): void {
457
+ this._engine?.realm3dDrawLine(this.realmId, x0, y0, z0, x1, y1, z1, r, g, b, a);
458
+ }
459
+
460
+ drawAabb(
461
+ minX: number,
462
+ minY: number,
463
+ minZ: number,
464
+ maxX: number,
465
+ maxY: number,
466
+ maxZ: number,
467
+ r: number,
468
+ g: number,
469
+ b: number,
470
+ a: number
471
+ ): void {
472
+ this._engine?.realm3dDrawAabb(this.realmId, minX, minY, minZ, maxX, maxY, maxZ, r, g, b, a);
473
+ }
474
+
475
+ clearGizmos(): void {
476
+ this._engine?.realm3dClearGizmos(this.realmId);
477
+ }
478
+
479
+ // Particles, Portals & Advanced Mesh Features
480
+ createParticleEmitter(
481
+ desc: ParticleEmitter3DDesc,
482
+ transform?: Transform3DProps
483
+ ): { entityId: EntityId; emitterId: number } {
484
+ const entityId = this.world.createEntity();
485
+ const emitterId = this.allocateResourceId();
486
+ this._attachModel(entityId, emitterId, transform);
487
+ this.world.attachComponent(entityId, createParticleEmitter3D(emitterId, desc));
488
+ return { entityId, emitterId };
489
+ }
490
+
491
+ particleEmitterBurst(
492
+ emitter: number | { emitterId?: number; entityId?: EntityId },
493
+ count: number
494
+ ): void {
495
+ const emitterId = resolveEmitterId(this.world, emitter);
496
+ if (this._engine) {
497
+ if (!this.syncSystem.isEmitterSynced(emitterId)) this.flush();
498
+ this._engine.realm3dParticleEmitterBurst(this.realmId, emitterId, count);
499
+ }
500
+ }
501
+
502
+ createPortal(desc: Portal3DDesc): EntityId {
503
+ const entityId = this.world.createEntity();
504
+ const portalId = this.allocateResourceId();
505
+ this.world.attachComponent(entityId, createPortal3D(portalId, desc));
506
+ return entityId;
507
+ }
508
+
509
+ setPortalActive(portalId: number, active: boolean): void {
510
+ this._engine?.realm3dSetPortalActive(this.realmId, portalId, active);
511
+ }
512
+
513
+ createInstancedModel(desc: InstancedModel3DDesc): EntityId {
514
+ const entityId = this.world.createEntity();
515
+ const instancedModelId = this.allocateResourceId();
516
+ this.world.attachComponent(entityId, createInstancedModel3D(instancedModelId, desc));
517
+ return entityId;
518
+ }
519
+
520
+ setInstancedModelData(instancedModelId: number, data: Float32Array): void {
521
+ this._engine?.realm3dSetInstancedModelData(this.realmId, instancedModelId, data, data.length);
522
+ }
523
+
524
+ setModelMorphWeights(modelId: number, weights: Float32Array): void {
525
+ this._pendingMorphWeights.set(modelId, new Float32Array(weights));
526
+ }
527
+
528
+ setGeometryMorphTargets(
529
+ geometryId: number,
530
+ targetData: Uint8Array | MorphTargetData[],
531
+ vertexCount?: number
532
+ ): void {
533
+ const geometry = this._geometries.get(geometryId);
534
+ if (!geometry) throw new Error(`Unknown geometry ${geometryId}`);
535
+ const vertexBytes =
536
+ geometry.primitive.kind === 'custom' ? geometry.primitive.vertexData.byteLength : undefined;
537
+ geometry.morphTargets = prepareMorphTargets(targetData, vertexCount, vertexBytes);
538
+ geometry.isDirty = true;
539
+ }
540
+
541
+ setInstancedModelMorphWeights(instancedModelId: number, weights: Float32Array): void {
542
+ this._engine?.realm3dSetInstancedModelMorphWeights(
543
+ this.realmId,
544
+ instancedModelId,
545
+ weights,
546
+ weights.length
547
+ );
548
+ }
549
+
550
+ updateBones(modelId: number, data: Float32Array): void {
551
+ this._engine?.realm3dUpdateBones(this.realmId, modelId, data, data.length);
552
+ }
553
+
554
+ /**
555
+ * Helper to animate a model transform directly through ECS.
556
+ */
557
+ animateModel(
558
+ entityId: EntityId,
559
+ targetValue: unknown,
560
+ config: TransitionConfig,
561
+ animation: AnimationCoordinator
562
+ ): TransitionId | undefined {
563
+ return animation.animateEcsProperty(this.world, entityId, 'transform3d', targetValue, config);
564
+ }
565
+
566
+ // Entity Name & Metadata Helpers
567
+ setEntityName(entityId: EntityId, name: string): void {
568
+ this.world.setEntityName(entityId, name);
569
+ }
570
+ getEntityName(entityId: EntityId): string | undefined {
571
+ return this.world.getEntityName(entityId);
572
+ }
573
+ findEntityByName(name: string): EntityId | undefined {
574
+ return this.world.findEntityByName(name);
575
+ }
576
+ findEntitiesByName(pattern: string | RegExp): EntityId[] {
577
+ return this.world.findEntitiesByName(pattern);
578
+ }
579
+ setMetadataKV(entityId: EntityId, key: string, value: unknown): void {
580
+ this.world.setMetadataKV(entityId, key, value);
581
+ }
582
+ getMetadataKV<T = unknown>(entityId: EntityId, key: string): T | undefined {
583
+ return this.world.getMetadataKV<T>(entityId, key);
584
+ }
585
+ findEntitiesByMetadata(key: string, value?: unknown): EntityId[] {
586
+ return this.world.findEntitiesByMetadata(key, value);
587
+ }
588
+ }
@@ -0,0 +1,62 @@
1
+ import type { EntityId } from '../ecs/entity';
2
+ import type { CameraLookAt3DDesc } from '@galfus/transport-types';
3
+ import type { EcsWorld } from '../ecs/world';
4
+ import type { Camera3DComponent, ParticleEmitter3DComponent } from './components';
5
+
6
+ /** Stores the renderer's camera orientation for host-side spatial queries. */
7
+ export function storeCameraLookAt(
8
+ world: EcsWorld,
9
+ cameraId: number,
10
+ lookAt?: CameraLookAt3DDesc
11
+ ): void {
12
+ for (const [, component] of world.components.entries<Camera3DComponent>('Camera3D')) {
13
+ if (component.cameraId === cameraId) {
14
+ component.desc.lookAt = lookAt
15
+ ? { target: [...lookAt.target], up: lookAt.up ? [...lookAt.up] : undefined }
16
+ : undefined;
17
+ break;
18
+ }
19
+ }
20
+ }
21
+
22
+ export function resolveCameraId(
23
+ world: EcsWorld,
24
+ camera: number | { cameraId?: number; entityId?: EntityId }
25
+ ): number {
26
+ if (typeof camera === 'number') {
27
+ return (
28
+ world.getComponent<Camera3DComponent>(camera as EntityId, 'Camera3D')?.cameraId ?? camera
29
+ );
30
+ }
31
+ const resolved =
32
+ camera.cameraId ??
33
+ (camera.entityId
34
+ ? world.getComponent<Camera3DComponent>(camera.entityId, 'Camera3D')?.cameraId
35
+ : undefined);
36
+ if (resolved === undefined)
37
+ throw new Error('Cannot resolve Camera3D ID: entity does not have a Camera3DComponent.');
38
+ return resolved;
39
+ }
40
+
41
+ export function resolveEmitterId(
42
+ world: EcsWorld,
43
+ emitter: number | { emitterId?: number; entityId?: EntityId }
44
+ ): number {
45
+ if (typeof emitter === 'number') {
46
+ return (
47
+ world.getComponent<ParticleEmitter3DComponent>(emitter as EntityId, 'ParticleEmitter3D')
48
+ ?.emitterId ?? emitter
49
+ );
50
+ }
51
+ const resolved =
52
+ emitter.emitterId ??
53
+ (emitter.entityId
54
+ ? world.getComponent<ParticleEmitter3DComponent>(emitter.entityId, 'ParticleEmitter3D')
55
+ ?.emitterId
56
+ : undefined);
57
+ if (resolved === undefined)
58
+ throw new Error(
59
+ 'Cannot resolve ParticleEmitter3D ID: entity does not have a ParticleEmitter3DComponent.'
60
+ );
61
+ return resolved;
62
+ }