@galfus/engine 0.0.0-stage → 0.31.0-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,128 @@
1
+ /**
2
+ * Skeletal skinning resolution system for Galfus Engine.
3
+ *
4
+ * Runs during the `preRender` stage after AnchorSolveSystem, reading world matrices
5
+ * directly from AnchorMatrixCache and uploading deformation matrices via realm3dUpdateBones.
6
+ */
7
+
8
+ import type { GalfusEngine } from '@galfus/transport-types';
9
+ import type { AnchorMatrixCache } from '../anchors/cache';
10
+ import type { System } from '../ecs/system';
11
+ import type { EcsWorld } from '../ecs/world';
12
+ import { mat4, type Mat4 } from '../math/index';
13
+ import { SpatialDirtyFlags } from '../spatial/flags';
14
+ import type { Model3DComponent, Skin3DComponent } from './components';
15
+
16
+ export class SkinningSolveSystem {
17
+ // Pre-allocated scratch matrices for zero-GC execution
18
+ private readonly _invModelMat: Mat4 = mat4.create();
19
+ private readonly _tempMat: Mat4 = mat4.create();
20
+ private readonly _skinMat: Mat4 = mat4.create();
21
+ private readonly _invBindMat: Mat4 = mat4.create();
22
+ private readonly _identityMat: Mat4 = mat4.create();
23
+
24
+ constructor(
25
+ readonly realmId: number,
26
+ readonly cache: AnchorMatrixCache
27
+ ) {}
28
+
29
+ /**
30
+ * Evaluates all active skins and submits bone matrices to the engine.
31
+ */
32
+ solve(world: EcsWorld, engine: GalfusEngine | null): void {
33
+ const skinEntities = world.query('Skin3D');
34
+ if (skinEntities.length === 0) return;
35
+
36
+ for (const skinEntityId of skinEntities) {
37
+ const skin = world.getComponent<Skin3DComponent>(skinEntityId, 'Skin3D');
38
+ if (!skin || skin.joints.length === 0) continue;
39
+
40
+ if (
41
+ !world.isAlive(skin.modelEntityId) ||
42
+ !world.getComponent<Model3DComponent>(skin.modelEntityId, 'Model3D')
43
+ ) {
44
+ world.destroyEntity(skinEntityId);
45
+ continue;
46
+ }
47
+
48
+ const model = world.getComponent<Model3DComponent>(skin.modelEntityId, 'Model3D');
49
+ if (!model) continue;
50
+
51
+ // Incremental optimization: skip re-computation and upload if model, joints, and skin are clean
52
+ const isModelDirty = (model.flags & SpatialDirtyFlags.WorldDirty) !== 0;
53
+ let isAnyJointDirty = false;
54
+ for (let j = 0; j < skin.joints.length; j++) {
55
+ const jointEntityId = skin.joints[j];
56
+ if (jointEntityId === undefined) continue;
57
+ const jointModel = world.getComponent<Model3DComponent>(jointEntityId, 'Model3D');
58
+ if (jointModel && (jointModel.flags & SpatialDirtyFlags.WorldDirty) !== 0) {
59
+ isAnyJointDirty = true;
60
+ break;
61
+ }
62
+ }
63
+
64
+ if (!skin.isDirty && !isModelDirty && !isAnyJointDirty) {
65
+ continue;
66
+ }
67
+
68
+ // 1. Get model world matrix (from model component or cache)
69
+ const modelWorld =
70
+ model.worldMatrix ?? this.cache.getWorld3D(skin.modelEntityId) ?? this._identityMat;
71
+
72
+ // 2. Invert model world matrix
73
+ if (!mat4.invert(this._invModelMat, modelWorld)) {
74
+ mat4.identity(this._invModelMat);
75
+ }
76
+
77
+ // 3. For each joint bone, compute: SkinMatrix = inv(ModelWorld) * JointWorld * InvBind
78
+ const jointCount = skin.joints.length;
79
+ for (let j = 0; j < jointCount; j++) {
80
+ const jointEntityId = skin.joints[j];
81
+ if (jointEntityId === undefined) continue;
82
+
83
+ const jointModel = world.getComponent<Model3DComponent>(jointEntityId, 'Model3D');
84
+ const jointWorld =
85
+ jointModel?.worldMatrix ?? this.cache.getWorld3D(jointEntityId) ?? this._identityMat;
86
+ const offset = j * 16;
87
+
88
+ // Copy 16 floats of the inverse bind matrix into scratch
89
+ for (let k = 0; k < 16; k++) {
90
+ this._invBindMat[k] = skin.inverseBindMatrices[offset + k] ?? (k % 5 === 0 ? 1 : 0);
91
+ }
92
+
93
+ // temp = JointWorld * InvBind
94
+ mat4.multiply(this._tempMat, jointWorld, this._invBindMat);
95
+
96
+ // skinMat = inv(ModelWorld) * temp
97
+ mat4.multiply(this._skinMat, this._invModelMat, this._tempMat);
98
+
99
+ // Store into pre-allocated jointMatrices buffer
100
+ for (let k = 0; k < 16; k++) {
101
+ skin.jointMatrices[offset + k] = this._skinMat[k] ?? 0;
102
+ }
103
+ }
104
+
105
+ // 4. Upload bone matrices to native core
106
+ if (engine) {
107
+ engine.realm3dUpdateBones(
108
+ this.realmId,
109
+ model.modelId,
110
+ skin.jointMatrices,
111
+ skin.jointMatrices.length
112
+ );
113
+ }
114
+
115
+ skin.isDirty = false;
116
+ }
117
+ }
118
+
119
+ /**
120
+ * Helper function creating an ECS System instance running this solver in `preRender`.
121
+ */
122
+ createSystem(getEngine: () => GalfusEngine | null): System<EcsWorld> {
123
+ return (world, context) => {
124
+ const engine = context.engine ?? getEngine();
125
+ this.solve(world, engine);
126
+ };
127
+ }
128
+ }
@@ -0,0 +1,442 @@
1
+ /**
2
+ * Realm 3D synchronization system syncing ECS state and resource registries with core GalfusEngine.
3
+ */
4
+
5
+ import type { GalfusEngine, Model3DDesc, Transform3D } from '@galfus/transport-types';
6
+ import type { AnchorMatrixCache } from '../anchors/cache';
7
+ import type { EntityId } from '../ecs/entity';
8
+ import type { EcsWorld } from '../ecs/world';
9
+ import { mat4, quat, vec3 } from '../math/index';
10
+ import { SpatialDirtyFlags } from '../spatial/flags';
11
+ import type {
12
+ Camera3DComponent,
13
+ GeometryResourceEntry,
14
+ InstancedModel3DComponent,
15
+ Light3DComponent,
16
+ MaterialResourceEntry,
17
+ Mesh3DComponent,
18
+ Model3DComponent,
19
+ ParticleEmitter3DComponent,
20
+ Portal3DComponent,
21
+ Primitive3DKind
22
+ } from './components';
23
+
24
+ export interface Realm3DResourceProvider {
25
+ getGeometries(): Iterable<GeometryResourceEntry>;
26
+ getMaterials(): Iterable<MaterialResourceEntry>;
27
+ }
28
+
29
+ const scratchTrans = vec3.create();
30
+ const scratchRot = quat.create();
31
+ const scratchScale = vec3.create();
32
+
33
+ export class Realm3DSyncSystem {
34
+ private readonly _syncedGeometries = new Set<number>();
35
+ private readonly _syncedMaterials = new Set<number>();
36
+ private readonly _syncedModels = new Set<number>();
37
+ private readonly _syncedCameras = new Set<number>();
38
+ private readonly _syncedLights = new Set<number>();
39
+ private readonly _syncedEmitters = new Set<number>();
40
+ private readonly _syncedPortals = new Set<number>();
41
+ private readonly _syncedInstancedModels = new Set<number>();
42
+ private readonly _transformCache = new Map<EntityId, Transform3D>();
43
+
44
+ constructor(
45
+ readonly realmId: number,
46
+ readonly cache: AnchorMatrixCache
47
+ ) {}
48
+
49
+ /**
50
+ * Extracts a Transform3D from the resolved model world matrix or cache.
51
+ */
52
+ getResolvedTransform(world: EcsWorld, id: EntityId): Transform3D {
53
+ let tf = this._transformCache.get(id);
54
+ if (!tf) {
55
+ tf = {
56
+ position: [0, 0, 0],
57
+ rotation: [0, 0, 0, 1],
58
+ scale: [1, 1, 1]
59
+ };
60
+ this._transformCache.set(id, tf);
61
+ }
62
+
63
+ const model = world.getComponent<Model3DComponent>(id, 'Model3D');
64
+ const mat = model ? model.worldMatrix : this.cache.getWorld3D(id);
65
+
66
+ if (mat) {
67
+ mat4.getTranslation(scratchTrans, mat);
68
+ mat4.getRotation(scratchRot, mat);
69
+ mat4.getScaling(scratchScale, mat);
70
+
71
+ const p = tf.position;
72
+ p[0] = scratchTrans[0]!;
73
+ p[1] = scratchTrans[1]!;
74
+ p[2] = scratchTrans[2]!;
75
+
76
+ const r = tf.rotation!;
77
+ r[0] = scratchRot[0]!;
78
+ r[1] = scratchRot[1]!;
79
+ r[2] = scratchRot[2]!;
80
+ r[3] = scratchRot[3]!;
81
+
82
+ const s = tf.scale!;
83
+ s[0] = scratchScale[0]!;
84
+ s[1] = scratchScale[1]!;
85
+ s[2] = scratchScale[2]!;
86
+ } else {
87
+ tf.position[0] = 0;
88
+ tf.position[1] = 0;
89
+ tf.position[2] = 0;
90
+ }
91
+
92
+ return tf;
93
+ }
94
+
95
+ private _uploadPrimitive(
96
+ engine: GalfusEngine,
97
+ geometryId: number,
98
+ primitive: Primitive3DKind
99
+ ): void {
100
+ switch (primitive.kind) {
101
+ case 'cube':
102
+ engine.realm3dCreateCube(this.realmId, geometryId, primitive.size);
103
+ break;
104
+ case 'plane':
105
+ engine.realm3dCreatePlane(this.realmId, geometryId, primitive.width, primitive.depth);
106
+ break;
107
+ case 'quad':
108
+ engine.realm3dCreateQuad(this.realmId, geometryId, primitive.width, primitive.height);
109
+ break;
110
+ case 'sphere':
111
+ engine.realm3dCreateSphere(
112
+ this.realmId,
113
+ geometryId,
114
+ primitive.radius,
115
+ primitive.rings,
116
+ primitive.sectors
117
+ );
118
+ break;
119
+ case 'cylinder':
120
+ engine.realm3dCreateCylinder(
121
+ this.realmId,
122
+ geometryId,
123
+ primitive.radius,
124
+ primitive.height,
125
+ primitive.sectors
126
+ );
127
+ break;
128
+ case 'capsule':
129
+ engine.realm3dCreateCapsule(
130
+ this.realmId,
131
+ geometryId,
132
+ primitive.radius,
133
+ primitive.cylinderHeight,
134
+ primitive.rings,
135
+ primitive.sectors
136
+ );
137
+ break;
138
+ case 'pyramid':
139
+ engine.realm3dCreatePyramid(
140
+ this.realmId,
141
+ geometryId,
142
+ primitive.width,
143
+ primitive.depth,
144
+ primitive.height
145
+ );
146
+ break;
147
+ case 'torus':
148
+ engine.realm3dCreateTorus(
149
+ this.realmId,
150
+ geometryId,
151
+ primitive.majorRadius,
152
+ primitive.minorRadius,
153
+ primitive.majorSegments,
154
+ primitive.minorSegments
155
+ );
156
+ break;
157
+ case 'custom':
158
+ engine.realm3dCreateGeometry(
159
+ this.realmId,
160
+ geometryId,
161
+ primitive.topology,
162
+ primitive.vertexData,
163
+ primitive.vertexData.byteLength,
164
+ primitive.indices,
165
+ primitive.indices.length
166
+ );
167
+ break;
168
+ }
169
+ }
170
+
171
+ /**
172
+ * Synchronizes all active 3D resources and ECS components to the engine driver.
173
+ */
174
+ sync(world: EcsWorld, engine: GalfusEngine, resourceProvider?: Realm3DResourceProvider): void {
175
+ // 1. Sync geometries from the resource registry
176
+ const currentGeometries = new Set<number>();
177
+ if (resourceProvider) {
178
+ for (const geo of resourceProvider.getGeometries()) {
179
+ currentGeometries.add(geo.geometryId);
180
+ if (!this._syncedGeometries.has(geo.geometryId) || geo.isDirty) {
181
+ this._uploadPrimitive(engine, geo.geometryId, geo.primitive);
182
+ if (geo.morphTargets) {
183
+ const { data, count } = geo.morphTargets;
184
+ engine.realm3dSetGeometryMorphTargets(
185
+ this.realmId,
186
+ geo.geometryId,
187
+ data,
188
+ data.byteLength,
189
+ count
190
+ );
191
+ }
192
+ this._syncedGeometries.add(geo.geometryId);
193
+ geo.isDirty = false;
194
+ }
195
+ }
196
+ }
197
+ for (const geoId of this._syncedGeometries) {
198
+ if (!currentGeometries.has(geoId)) {
199
+ engine.realm3dDestroyGeometry(this.realmId, geoId);
200
+ this._syncedGeometries.delete(geoId);
201
+ }
202
+ }
203
+
204
+ // 2. Sync materials from the resource registry
205
+ const currentMaterials = new Set<number>();
206
+ if (resourceProvider) {
207
+ for (const mat of resourceProvider.getMaterials()) {
208
+ currentMaterials.add(mat.materialId);
209
+ if (!this._syncedMaterials.has(mat.materialId) || mat.isDirty) {
210
+ engine.realm3dCreateMaterial(this.realmId, mat.materialId, mat.desc);
211
+ this._syncedMaterials.add(mat.materialId);
212
+ mat.isDirty = false;
213
+ }
214
+ }
215
+ }
216
+ for (const matId of this._syncedMaterials) {
217
+ if (!currentMaterials.has(matId)) {
218
+ engine.realm3dDestroyMaterial(this.realmId, matId);
219
+ this._syncedMaterials.delete(matId);
220
+ }
221
+ }
222
+
223
+ // 3. Sync Models & Meshes
224
+ const modelEntities = world.query('Model3D');
225
+ const currentModels = new Set<number>();
226
+ for (const id of modelEntities) {
227
+ const mesh = world.getComponent<Mesh3DComponent>(id, 'Mesh3D');
228
+ // If the entity has no mesh AND has a dedicated spatial component (Camera, Light, Emitter, Portal, InstancedModel),
229
+ // we do not register an empty Rust model for it.
230
+ if (
231
+ !mesh &&
232
+ (world.hasComponent(id, 'Camera3D') ||
233
+ world.hasComponent(id, 'Light3D') ||
234
+ world.hasComponent(id, 'ParticleEmitter3D') ||
235
+ world.hasComponent(id, 'Portal3D') ||
236
+ world.hasComponent(id, 'InstancedModel3D'))
237
+ ) {
238
+ continue;
239
+ }
240
+ const model = world.getComponent<Model3DComponent>(id, 'Model3D');
241
+ if (!model) continue;
242
+ currentModels.add(model.modelId);
243
+
244
+ const tf = this.getResolvedTransform(world, id);
245
+ const isRegistered = this._syncedModels.has(model.modelId);
246
+ const isSpatialDirty = (model.flags & SpatialDirtyFlags.WorldDirty) !== 0;
247
+ const isMeshDirty = mesh ? mesh.isDirty : false;
248
+
249
+ if (!isRegistered || isMeshDirty) {
250
+ const desc: Model3DDesc = {
251
+ geometryId: mesh ? mesh.geometryId : 0,
252
+ materialId: mesh ? mesh.materialId : 0,
253
+ castShadow: mesh ? mesh.castShadow : true,
254
+ receiveShadow: mesh ? mesh.receiveShadow : true,
255
+ castOutline: mesh ? mesh.castOutline : false,
256
+ outlineColor: mesh ? mesh.outlineColor : [1, 1, 1, 1],
257
+ active: mesh ? mesh.active : true,
258
+ boneOffset: mesh ? mesh.boneOffset : 0,
259
+ boneCount: mesh ? mesh.boneCount : 0
260
+ };
261
+ engine.realm3dCreateModel(this.realmId, model.modelId, desc, tf);
262
+ this._syncedModels.add(model.modelId);
263
+ if (mesh) mesh.isDirty = false;
264
+ } else if (isSpatialDirty) {
265
+ engine.realm3dSetModelTransform(this.realmId, model.modelId, tf);
266
+ }
267
+ }
268
+ for (const modelId of this._syncedModels) {
269
+ if (!currentModels.has(modelId)) {
270
+ engine.realm3dDestroyModel(this.realmId, modelId);
271
+ this._syncedModels.delete(modelId);
272
+ this._transformCache.delete(modelId);
273
+ }
274
+ }
275
+
276
+ // 4. Sync Cameras
277
+ const cameraEntities = world.query('Camera3D');
278
+ const currentCameras = new Set<number>();
279
+ for (const id of cameraEntities) {
280
+ const camera = world.getComponent<Camera3DComponent>(id, 'Camera3D');
281
+ if (!camera) continue;
282
+ currentCameras.add(camera.cameraId);
283
+
284
+ const model = world.getComponent<Model3DComponent>(id, 'Model3D');
285
+ const isSpatialDirty = model ? (model.flags & SpatialDirtyFlags.WorldDirty) !== 0 : false;
286
+ const tf = this.getResolvedTransform(world, id);
287
+ if (!this._syncedCameras.has(camera.cameraId) || camera.isDirty) {
288
+ engine.realm3dCreateCamera(this.realmId, camera.cameraId, camera.desc, tf);
289
+ this._syncedCameras.add(camera.cameraId);
290
+ camera.isDirty = false;
291
+ } else if (isSpatialDirty) {
292
+ engine.realm3dSetCameraTransform(this.realmId, camera.cameraId, tf);
293
+ }
294
+ }
295
+ for (const camId of this._syncedCameras) {
296
+ if (!currentCameras.has(camId)) {
297
+ engine.realm3dDestroyCamera(this.realmId, camId);
298
+ this._syncedCameras.delete(camId);
299
+ }
300
+ }
301
+
302
+ // 5. Sync Lights
303
+ const lightEntities = world.query('Light3D');
304
+ const currentLights = new Set<number>();
305
+ for (const id of lightEntities) {
306
+ const light = world.getComponent<Light3DComponent>(id, 'Light3D');
307
+ if (!light) continue;
308
+ currentLights.add(light.lightId);
309
+
310
+ const model = world.getComponent<Model3DComponent>(id, 'Model3D');
311
+ const isSpatialDirty = model ? (model.flags & SpatialDirtyFlags.WorldDirty) !== 0 : false;
312
+ const tf = this.getResolvedTransform(world, id);
313
+ if (!this._syncedLights.has(light.lightId) || light.isDirty) {
314
+ engine.realm3dCreateLight(this.realmId, light.lightId, light.desc, tf);
315
+ this._syncedLights.add(light.lightId);
316
+ light.isDirty = false;
317
+ } else if (isSpatialDirty) {
318
+ engine.realm3dSetLightTransform(this.realmId, light.lightId, tf);
319
+ }
320
+ }
321
+ for (const lightId of this._syncedLights) {
322
+ if (!currentLights.has(lightId)) {
323
+ engine.realm3dDestroyLight(this.realmId, lightId);
324
+ this._syncedLights.delete(lightId);
325
+ }
326
+ }
327
+
328
+ // 6. Sync Particle Emitters
329
+ const emitterEntities = world.query('ParticleEmitter3D');
330
+ const currentEmitters = new Set<number>();
331
+ for (const id of emitterEntities) {
332
+ const emitter = world.getComponent<ParticleEmitter3DComponent>(id, 'ParticleEmitter3D');
333
+ if (!emitter) continue;
334
+ currentEmitters.add(emitter.emitterId);
335
+
336
+ const model = world.getComponent<Model3DComponent>(id, 'Model3D');
337
+ const isSpatialDirty = model ? (model.flags & SpatialDirtyFlags.WorldDirty) !== 0 : false;
338
+ const tf = this.getResolvedTransform(world, id);
339
+ if (!this._syncedEmitters.has(emitter.emitterId) || emitter.isDirty) {
340
+ engine.realm3dCreateParticleEmitter(this.realmId, emitter.emitterId, emitter.desc, tf);
341
+ this._syncedEmitters.add(emitter.emitterId);
342
+ emitter.isDirty = false;
343
+ } else if (isSpatialDirty) {
344
+ engine.realm3dSetParticleEmitterTransform(this.realmId, emitter.emitterId, tf);
345
+ }
346
+ }
347
+ for (const emitterId of this._syncedEmitters) {
348
+ if (!currentEmitters.has(emitterId)) {
349
+ engine.realm3dDestroyParticleEmitter(this.realmId, emitterId);
350
+ this._syncedEmitters.delete(emitterId);
351
+ }
352
+ }
353
+
354
+ // 7. Sync Portals
355
+ const portalEntities = world.query('Portal3D');
356
+ const currentPortals = new Set<number>();
357
+ for (const id of portalEntities) {
358
+ const portal = world.getComponent<Portal3DComponent>(id, 'Portal3D');
359
+ if (!portal) continue;
360
+ currentPortals.add(portal.portalId);
361
+
362
+ if (!this._syncedPortals.has(portal.portalId) || portal.isDirty) {
363
+ engine.realm3dCreatePortal(this.realmId, portal.portalId, portal.desc);
364
+ this._syncedPortals.add(portal.portalId);
365
+ portal.isDirty = false;
366
+ }
367
+ }
368
+ for (const portalId of this._syncedPortals) {
369
+ if (!currentPortals.has(portalId)) {
370
+ engine.realm3dDestroyPortal(this.realmId, portalId);
371
+ this._syncedPortals.delete(portalId);
372
+ }
373
+ }
374
+
375
+ // 8. Sync Instanced Models
376
+ const instancedEntities = world.query('InstancedModel3D');
377
+ const currentInstanced = new Set<number>();
378
+ for (const id of instancedEntities) {
379
+ const instanced = world.getComponent<InstancedModel3DComponent>(id, 'InstancedModel3D');
380
+ if (!instanced) continue;
381
+ currentInstanced.add(instanced.instancedModelId);
382
+
383
+ if (!this._syncedInstancedModels.has(instanced.instancedModelId) || instanced.isDirty) {
384
+ engine.realm3dCreateInstancedModel(
385
+ this.realmId,
386
+ instanced.instancedModelId,
387
+ instanced.desc
388
+ );
389
+ this._syncedInstancedModels.add(instanced.instancedModelId);
390
+ instanced.isDirty = false;
391
+ }
392
+ }
393
+ for (const instancedId of this._syncedInstancedModels) {
394
+ if (!currentInstanced.has(instancedId)) {
395
+ engine.realm3dDestroyInstancedModel(this.realmId, instancedId);
396
+ this._syncedInstancedModels.delete(instancedId);
397
+ }
398
+ }
399
+
400
+ // 9. Clear WorldDirty for all Model3D entities after all Core sync steps are complete
401
+ for (const id of modelEntities) {
402
+ const model = world.getComponent<Model3DComponent>(id, 'Model3D');
403
+ if (model) {
404
+ model.flags &= ~SpatialDirtyFlags.WorldDirty;
405
+ }
406
+ }
407
+ }
408
+
409
+ isCameraSynced(cameraId: number): boolean {
410
+ return this._syncedCameras.has(cameraId);
411
+ }
412
+
413
+ isEmitterSynced(emitterId: number): boolean {
414
+ return this._syncedEmitters.has(emitterId);
415
+ }
416
+
417
+ /**
418
+ * Disposes all tracked IDs on the engine when the realm is destroyed.
419
+ */
420
+ dispose(engine: GalfusEngine | null): void {
421
+ if (engine) {
422
+ for (const id of this._syncedModels) engine.realm3dDestroyModel(this.realmId, id);
423
+ for (const id of this._syncedLights) engine.realm3dDestroyLight(this.realmId, id);
424
+ for (const id of this._syncedCameras) engine.realm3dDestroyCamera(this.realmId, id);
425
+ for (const id of this._syncedMaterials) engine.realm3dDestroyMaterial(this.realmId, id);
426
+ for (const id of this._syncedGeometries) engine.realm3dDestroyGeometry(this.realmId, id);
427
+ for (const id of this._syncedEmitters) engine.realm3dDestroyParticleEmitter(this.realmId, id);
428
+ for (const id of this._syncedPortals) engine.realm3dDestroyPortal(this.realmId, id);
429
+ for (const id of this._syncedInstancedModels)
430
+ engine.realm3dDestroyInstancedModel(this.realmId, id);
431
+ }
432
+ this._syncedModels.clear();
433
+ this._syncedLights.clear();
434
+ this._syncedCameras.clear();
435
+ this._syncedMaterials.clear();
436
+ this._syncedGeometries.clear();
437
+ this._syncedEmitters.clear();
438
+ this._syncedPortals.clear();
439
+ this._syncedInstancedModels.clear();
440
+ this._transformCache.clear();
441
+ }
442
+ }
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Realm 3D math and spatial utilities.
3
+ */
4
+
5
+ import { vec3 } from '../../math/index';
6
+ import type { Vec3 } from '../../math/vec3';
7
+
8
+ export interface BoundingSphere3D {
9
+ center: [number, number, number];
10
+ radius: number;
11
+ }
12
+
13
+ export function createBoundingSphere3D(
14
+ center: [number, number, number] | Vec3,
15
+ radius: number
16
+ ): BoundingSphere3D {
17
+ return {
18
+ center: [center[0], center[1], center[2]],
19
+ radius
20
+ };
21
+ }
22
+
23
+ export function intersectsSphere3D(a: BoundingSphere3D, b: BoundingSphere3D): boolean {
24
+ const d = vec3.distance(a.center, b.center);
25
+ return d <= a.radius + b.radius;
26
+ }
@@ -0,0 +1,4 @@
1
+ /**
2
+ * Subpath export for GPU shared resources subsystem.
3
+ */
4
+ export * from './manager';
@@ -0,0 +1,92 @@
1
+ /**
2
+ * ResourceManager manages allocation and GPU binding for shared textures.
3
+ */
4
+
5
+ import type { GalfusEngine, GpuTextureDesc, TextureUpdateDesc } from '@galfus/transport-types';
6
+ import { IdAllocator, type TextureId } from '../types';
7
+
8
+ export interface LoadTextureOptions {
9
+ /** Debug label for the GPU texture resource */
10
+ label?: string;
11
+ /** Whether pixels represent sRGB color (defaults to true; set false for normal/data maps) */
12
+ srgb?: boolean;
13
+ }
14
+
15
+ export class ResourceManager {
16
+ private _engine: GalfusEngine | null = null;
17
+ private readonly _idAllocator = new IdAllocator<TextureId>(1);
18
+ private readonly _textures = new Set<TextureId>();
19
+
20
+ bindEngine(engine: GalfusEngine): void {
21
+ this._engine = engine;
22
+ }
23
+
24
+ /**
25
+ * Loads raw image bytes (PNG, JPEG, WebP, ICO) directly into a tracked GPU texture.
26
+ * Auto-detection and decoding are handled natively inside the Galfus core.
27
+ * Follows payload-first convention: (data, options?).
28
+ *
29
+ * @param data Encoded image file bytes (PNG, JPEG, WebP, ICO).
30
+ * @param options Configuration options (sRGB color space, debug label).
31
+ * @returns The allocated logical TextureId.
32
+ */
33
+ loadTexture(data: Uint8Array, options?: LoadTextureOptions): TextureId {
34
+ const srgb = options?.srgb ?? true;
35
+ return this.createTexture(data, {
36
+ label: options?.label ?? 'image-texture',
37
+ width: 0,
38
+ height: 0,
39
+ format: srgb ? 'rgba8UnormSrgb' : 'rgba8Unorm'
40
+ });
41
+ }
42
+
43
+ /**
44
+ * Creates a shared GPU texture from raw byte data under a tracked logical TextureId.
45
+ * If `data` contains encoded image container bytes (PNG, JPEG, WebP, ICO), the Galfus core
46
+ * automatically decodes it into RGBA8 and derives width/height.
47
+ *
48
+ * Follows payload-first convention: (data, desc).
49
+ */
50
+ createTexture(data: Uint8Array, desc: GpuTextureDesc): TextureId {
51
+ const textureId = this._idAllocator.allocate('TextureId');
52
+ if (this._engine) {
53
+ this._engine.resourceCreateTexture(textureId, desc, data, data.byteLength);
54
+ }
55
+ this._textures.add(textureId);
56
+ return textureId;
57
+ }
58
+
59
+ /**
60
+ * Updates a sub-rectangle of an existing GPU texture.
61
+ * Follows payload-first convention: (textureId, data, rect).
62
+ */
63
+ updateTexture(textureId: TextureId, data: Uint8Array, rect: TextureUpdateDesc): void {
64
+ if (this._engine) {
65
+ this._engine.resourceUpdateTexture(textureId, rect, data, data.byteLength);
66
+ }
67
+ }
68
+
69
+ /**
70
+ * Disposes a GPU texture and frees its logical ID.
71
+ */
72
+ disposeTexture(textureId: TextureId): void {
73
+ if (!this._textures.has(textureId)) return;
74
+ if (this._engine) {
75
+ this._engine.resourceDisposeTexture(textureId);
76
+ }
77
+ this._textures.delete(textureId);
78
+ }
79
+
80
+ /**
81
+ * Disposes all active textures.
82
+ */
83
+ dispose(): void {
84
+ if (this._engine) {
85
+ for (const textureId of this._textures) {
86
+ this._engine.resourceDisposeTexture(textureId);
87
+ }
88
+ }
89
+ this._textures.clear();
90
+ this._engine = null;
91
+ }
92
+ }