@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.
- package/README.md +190 -2
- package/package.json +59 -4
- package/src/anchors/cache.ts +101 -0
- package/src/anchors/component.ts +123 -0
- package/src/anchors/hierarchy.ts +192 -0
- package/src/anchors/index.ts +8 -0
- package/src/anchors/solver.ts +247 -0
- package/src/anchors/strategy.ts +142 -0
- package/src/animation/blending.ts +163 -0
- package/src/animation/clip.ts +132 -0
- package/src/animation/coordinator.ts +568 -0
- package/src/animation/index.ts +7 -0
- package/src/animation/native-targets.ts +37 -0
- package/src/animation/player.ts +287 -0
- package/src/audio/index.ts +4 -0
- package/src/audio/manager.ts +371 -0
- package/src/collision/components.ts +211 -0
- package/src/collision/index.ts +16 -0
- package/src/collision/query2d.ts +167 -0
- package/src/collision/query3d.ts +170 -0
- package/src/ecs/component.ts +130 -0
- package/src/ecs/entity.ts +75 -0
- package/src/ecs/index.ts +9 -0
- package/src/ecs/intent.ts +162 -0
- package/src/ecs/metadata.ts +44 -0
- package/src/ecs/system.ts +224 -0
- package/src/ecs/world.ts +291 -0
- package/src/index.ts +19 -0
- package/src/interaction/focus.ts +327 -0
- package/src/interaction/index.ts +9 -0
- package/src/interaction/listener.ts +58 -0
- package/src/interaction/snapshot2d.ts +210 -0
- package/src/interaction/snapshot3d.ts +214 -0
- package/src/interaction/system.ts +544 -0
- package/src/interaction/types.ts +27 -0
- package/src/math/common.ts +67 -0
- package/src/math/index.ts +17 -0
- package/src/math/mat2d.ts +320 -0
- package/src/math/mat4.ts +576 -0
- package/src/math/quat.ts +438 -0
- package/src/math/vec2.ts +161 -0
- package/src/math/vec3.ts +242 -0
- package/src/math/vec4.ts +171 -0
- package/src/multiverse/coordinator.ts +540 -0
- package/src/multiverse/index.ts +13 -0
- package/src/multiverse/presentation-graph.ts +306 -0
- package/src/multiverse/presentation-links.ts +134 -0
- package/src/multiverse/realm.ts +49 -0
- package/src/platform/index.ts +2 -0
- package/src/platform/platform.ts +149 -0
- package/src/platform/window.ts +181 -0
- package/src/realm2d/components.ts +254 -0
- package/src/realm2d/index.ts +12 -0
- package/src/realm2d/presets/index.ts +36 -0
- package/src/realm2d/realm.ts +522 -0
- package/src/realm2d/sync.ts +284 -0
- package/src/realm2d/utils/index.ts +32 -0
- package/src/realm3d/components.ts +341 -0
- package/src/realm3d/geometry.ts +224 -0
- package/src/realm3d/index.ts +12 -0
- package/src/realm3d/presets/index.ts +67 -0
- package/src/realm3d/realm.ts +588 -0
- package/src/realm3d/resource-ids.ts +62 -0
- package/src/realm3d/skinning.ts +128 -0
- package/src/realm3d/sync.ts +442 -0
- package/src/realm3d/utils/index.ts +26 -0
- package/src/resources/index.ts +4 -0
- package/src/resources/manager.ts +92 -0
- package/src/runtime.ts +588 -0
- package/src/spatial/dirty_registry.ts +217 -0
- package/src/spatial/flags.ts +14 -0
- package/src/spatial/index.ts +6 -0
- package/src/spatial/metrics.ts +124 -0
- package/src/text/editor.ts +224 -0
- package/src/text/encoding.ts +38 -0
- package/src/text/index.ts +13 -0
- package/src/text/input.ts +160 -0
- package/src/text/keyboard.ts +50 -0
- package/src/text/svg-decorations.ts +147 -0
- 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
|
+
}
|