@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,284 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Realm 2D synchronization system syncing ECS state with core GalfusEngine.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { GalfusEngine, Material2DDesc, Transform2DDesc } 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 { mat2d, vec2 } from '../math';
|
|
10
|
+
import { SpatialDirtyFlags } from '../spatial/flags';
|
|
11
|
+
import type {
|
|
12
|
+
Camera2DComponent,
|
|
13
|
+
Light2DComponent,
|
|
14
|
+
Model2DComponent,
|
|
15
|
+
ParticleEmitter2DComponent,
|
|
16
|
+
Shape2DComponent,
|
|
17
|
+
Sprite2DComponent,
|
|
18
|
+
Svg2DComponent
|
|
19
|
+
} from './components';
|
|
20
|
+
|
|
21
|
+
const SCALE = vec2.create();
|
|
22
|
+
|
|
23
|
+
export class Realm2DSyncSystem {
|
|
24
|
+
private readonly _syncedMaterials = new Set<number>();
|
|
25
|
+
private readonly _syncedSprites = new Set<number>();
|
|
26
|
+
private readonly _syncedShapes = new Set<number>();
|
|
27
|
+
private readonly _syncedLights = new Set<number>();
|
|
28
|
+
private readonly _syncedCameras = new Set<number>();
|
|
29
|
+
private readonly _syncedSvgs = new Set<number>();
|
|
30
|
+
private readonly _syncedEmitters = new Set<number>();
|
|
31
|
+
|
|
32
|
+
constructor(
|
|
33
|
+
readonly realmId: number,
|
|
34
|
+
readonly cache: AnchorMatrixCache,
|
|
35
|
+
private readonly onSvgChanged?: (svgId: number) => void
|
|
36
|
+
) {}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Extracts a Transform2DDesc from the persistent cache and component metadata.
|
|
40
|
+
*/
|
|
41
|
+
getResolvedTransform(world: EcsWorld, id: EntityId): Transform2DDesc {
|
|
42
|
+
const model = world.getComponent<Model2DComponent>(id, 'Model2D');
|
|
43
|
+
const mat = model ? model.worldMatrix : this.cache.getWorld2D(id);
|
|
44
|
+
|
|
45
|
+
if (mat) {
|
|
46
|
+
const tx = mat[4];
|
|
47
|
+
const ty = mat[5];
|
|
48
|
+
mat2d.getScaling(SCALE, mat);
|
|
49
|
+
|
|
50
|
+
return {
|
|
51
|
+
position: [tx, ty],
|
|
52
|
+
rotation: mat2d.getRotation(mat),
|
|
53
|
+
scale: [SCALE[0], SCALE[1]],
|
|
54
|
+
zLayer: model?.zLayer ?? 0,
|
|
55
|
+
drawOrder: model?.drawOrder ?? 0
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return { position: [0, 0] };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Synchronizes all active 2D components to the engine driver.
|
|
64
|
+
*/
|
|
65
|
+
sync(
|
|
66
|
+
world: EcsWorld,
|
|
67
|
+
engine: GalfusEngine,
|
|
68
|
+
materials: ReadonlyMap<number, Material2DDesc> = new Map()
|
|
69
|
+
): void {
|
|
70
|
+
for (const [id, desc] of materials) {
|
|
71
|
+
if (!this._syncedMaterials.has(id)) {
|
|
72
|
+
engine.realm2dCreateMaterial(this.realmId, id, desc);
|
|
73
|
+
this._syncedMaterials.add(id);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
for (const id of this._syncedMaterials) {
|
|
77
|
+
if (!materials.has(id)) {
|
|
78
|
+
engine.realm2dDestroyMaterial(this.realmId, id);
|
|
79
|
+
this._syncedMaterials.delete(id);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
// 1. Sync Sprites
|
|
83
|
+
const spriteEntities = world.query('Sprite2D');
|
|
84
|
+
const currentSprites = new Set<number>();
|
|
85
|
+
for (const id of spriteEntities) {
|
|
86
|
+
const sprite = world.getComponent<Sprite2DComponent>(id, 'Sprite2D');
|
|
87
|
+
if (!sprite) continue;
|
|
88
|
+
currentSprites.add(sprite.spriteId);
|
|
89
|
+
|
|
90
|
+
const model = world.getComponent<Model2DComponent>(id, 'Model2D');
|
|
91
|
+
const tf = this.getResolvedTransform(world, id);
|
|
92
|
+
const isRegistered = this._syncedSprites.has(sprite.spriteId);
|
|
93
|
+
const isSpatialDirty = model ? (model.flags & SpatialDirtyFlags.WorldDirty) !== 0 : false;
|
|
94
|
+
|
|
95
|
+
if (!isRegistered || sprite.isDirty) {
|
|
96
|
+
engine.realm2dCreateSprite(this.realmId, sprite.spriteId, sprite.desc, tf);
|
|
97
|
+
this._syncedSprites.add(sprite.spriteId);
|
|
98
|
+
sprite.isDirty = false;
|
|
99
|
+
} else if (isSpatialDirty) {
|
|
100
|
+
engine.realm2dSetSpriteTransform(this.realmId, sprite.spriteId, tf);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
for (const spriteId of this._syncedSprites) {
|
|
104
|
+
if (!currentSprites.has(spriteId)) {
|
|
105
|
+
engine.realm2dDestroySprite(this.realmId, spriteId);
|
|
106
|
+
this._syncedSprites.delete(spriteId);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// 2. Sync Shapes
|
|
111
|
+
const shapeEntities = world.query('Shape2D');
|
|
112
|
+
const currentShapes = new Set<number>();
|
|
113
|
+
for (const id of shapeEntities) {
|
|
114
|
+
const shape = world.getComponent<Shape2DComponent>(id, 'Shape2D');
|
|
115
|
+
if (!shape) continue;
|
|
116
|
+
currentShapes.add(shape.shapeId);
|
|
117
|
+
|
|
118
|
+
const model = world.getComponent<Model2DComponent>(id, 'Model2D');
|
|
119
|
+
const tf = this.getResolvedTransform(world, id);
|
|
120
|
+
const isRegistered = this._syncedShapes.has(shape.shapeId);
|
|
121
|
+
const isSpatialDirty = model ? (model.flags & SpatialDirtyFlags.WorldDirty) !== 0 : false;
|
|
122
|
+
|
|
123
|
+
if (!isRegistered || shape.isDirty) {
|
|
124
|
+
engine.realm2dCreateShape(this.realmId, shape.shapeId, shape.desc, tf);
|
|
125
|
+
this._syncedShapes.add(shape.shapeId);
|
|
126
|
+
shape.isDirty = false;
|
|
127
|
+
} else if (isSpatialDirty) {
|
|
128
|
+
engine.realm2dSetShapeTransform(this.realmId, shape.shapeId, tf);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
for (const shapeId of this._syncedShapes) {
|
|
132
|
+
if (!currentShapes.has(shapeId)) {
|
|
133
|
+
engine.realm2dDestroyShape(this.realmId, shapeId);
|
|
134
|
+
this._syncedShapes.delete(shapeId);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// 3. Sync Lights
|
|
139
|
+
const lightEntities = world.query('Light2D');
|
|
140
|
+
const currentLights = new Set<number>();
|
|
141
|
+
for (const id of lightEntities) {
|
|
142
|
+
const light = world.getComponent<Light2DComponent>(id, 'Light2D');
|
|
143
|
+
if (!light) continue;
|
|
144
|
+
currentLights.add(light.lightId);
|
|
145
|
+
|
|
146
|
+
const model = world.getComponent<Model2DComponent>(id, 'Model2D');
|
|
147
|
+
const isSpatialDirty = model ? (model.flags & SpatialDirtyFlags.WorldDirty) !== 0 : false;
|
|
148
|
+
const tf = this.getResolvedTransform(world, id);
|
|
149
|
+
if (!this._syncedLights.has(light.lightId) || light.isDirty) {
|
|
150
|
+
engine.realm2dCreateLight(this.realmId, light.lightId, light.desc, tf);
|
|
151
|
+
this._syncedLights.add(light.lightId);
|
|
152
|
+
light.isDirty = false;
|
|
153
|
+
} else if (isSpatialDirty) {
|
|
154
|
+
engine.realm2dSetLightTransform(this.realmId, light.lightId, tf);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
for (const lightId of this._syncedLights) {
|
|
158
|
+
if (!currentLights.has(lightId)) {
|
|
159
|
+
engine.realm2dDestroyLight(this.realmId, lightId);
|
|
160
|
+
this._syncedLights.delete(lightId);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// 4. Sync Cameras
|
|
165
|
+
const cameraEntities = world.query('Camera2D');
|
|
166
|
+
const currentCameras = new Set<number>();
|
|
167
|
+
for (const id of cameraEntities) {
|
|
168
|
+
const camera = world.getComponent<Camera2DComponent>(id, 'Camera2D');
|
|
169
|
+
if (!camera) continue;
|
|
170
|
+
currentCameras.add(camera.cameraId);
|
|
171
|
+
|
|
172
|
+
const model = world.getComponent<Model2DComponent>(id, 'Model2D');
|
|
173
|
+
const isSpatialDirty = model ? (model.flags & SpatialDirtyFlags.WorldDirty) !== 0 : false;
|
|
174
|
+
const tf = this.getResolvedTransform(world, id);
|
|
175
|
+
if (!this._syncedCameras.has(camera.cameraId) || camera.isDirty) {
|
|
176
|
+
engine.realm2dCreateCamera(this.realmId, camera.cameraId, camera.desc, tf);
|
|
177
|
+
this._syncedCameras.add(camera.cameraId);
|
|
178
|
+
camera.isDirty = false;
|
|
179
|
+
} else if (isSpatialDirty) {
|
|
180
|
+
engine.realm2dSetCameraTransform(this.realmId, camera.cameraId, tf);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
for (const camId of this._syncedCameras) {
|
|
184
|
+
if (!currentCameras.has(camId)) {
|
|
185
|
+
engine.realm2dDestroyCamera(this.realmId, camId);
|
|
186
|
+
this._syncedCameras.delete(camId);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// 5. Sync SVGs
|
|
191
|
+
const svgEntities = world.query('Svg2D');
|
|
192
|
+
const currentSvgs = new Set<number>();
|
|
193
|
+
for (const id of svgEntities) {
|
|
194
|
+
const svg = world.getComponent<Svg2DComponent>(id, 'Svg2D');
|
|
195
|
+
if (!svg) continue;
|
|
196
|
+
currentSvgs.add(svg.svgId);
|
|
197
|
+
|
|
198
|
+
const model = world.getComponent<Model2DComponent>(id, 'Model2D');
|
|
199
|
+
const tf = this.getResolvedTransform(world, id);
|
|
200
|
+
const isRegistered = this._syncedSvgs.has(svg.svgId);
|
|
201
|
+
const isSpatialDirty = model ? (model.flags & SpatialDirtyFlags.WorldDirty) !== 0 : false;
|
|
202
|
+
|
|
203
|
+
if (!isRegistered || svg.isDirty) {
|
|
204
|
+
engine.realm2dCreateSvg(this.realmId, svg.svgId, svg.desc, tf);
|
|
205
|
+
this.onSvgChanged?.(svg.svgId);
|
|
206
|
+
this._syncedSvgs.add(svg.svgId);
|
|
207
|
+
svg.isDirty = false;
|
|
208
|
+
} else if (isSpatialDirty) {
|
|
209
|
+
engine.realm2dSetSvgTransform(this.realmId, svg.svgId, tf);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
for (const svgId of this._syncedSvgs) {
|
|
213
|
+
if (!currentSvgs.has(svgId)) {
|
|
214
|
+
engine.realm2dDestroySvg(this.realmId, svgId);
|
|
215
|
+
this.onSvgChanged?.(svgId);
|
|
216
|
+
this._syncedSvgs.delete(svgId);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// 6. Sync Particle Emitters
|
|
221
|
+
const emitterEntities = world.query('ParticleEmitter2D');
|
|
222
|
+
const currentEmitters = new Set<number>();
|
|
223
|
+
for (const id of emitterEntities) {
|
|
224
|
+
const emitter = world.getComponent<ParticleEmitter2DComponent>(id, 'ParticleEmitter2D');
|
|
225
|
+
if (!emitter) continue;
|
|
226
|
+
currentEmitters.add(emitter.emitterId);
|
|
227
|
+
|
|
228
|
+
const model = world.getComponent<Model2DComponent>(id, 'Model2D');
|
|
229
|
+
const isSpatialDirty = model ? (model.flags & SpatialDirtyFlags.WorldDirty) !== 0 : false;
|
|
230
|
+
const tf = this.getResolvedTransform(world, id);
|
|
231
|
+
if (!this._syncedEmitters.has(emitter.emitterId) || emitter.isDirty) {
|
|
232
|
+
engine.realm2dCreateParticleEmitter(this.realmId, emitter.emitterId, emitter.desc, tf);
|
|
233
|
+
this._syncedEmitters.add(emitter.emitterId);
|
|
234
|
+
emitter.isDirty = false;
|
|
235
|
+
} else if (isSpatialDirty) {
|
|
236
|
+
engine.realm2dSetParticleEmitterTransform(this.realmId, emitter.emitterId, tf);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
for (const emitterId of this._syncedEmitters) {
|
|
240
|
+
if (!currentEmitters.has(emitterId)) {
|
|
241
|
+
engine.realm2dDestroyParticleEmitter(this.realmId, emitterId);
|
|
242
|
+
this._syncedEmitters.delete(emitterId);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// 7. Clear WorldDirty for all Model2D entities after all Core sync steps are complete
|
|
247
|
+
for (const id of world.query('Model2D')) {
|
|
248
|
+
const model = world.getComponent<Model2DComponent>(id, 'Model2D');
|
|
249
|
+
if (model) {
|
|
250
|
+
model.flags &= ~SpatialDirtyFlags.WorldDirty;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
isSvgSynced(svgId: number): boolean {
|
|
256
|
+
return this._syncedSvgs.has(svgId);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
isEmitterSynced(emitterId: number): boolean {
|
|
260
|
+
return this._syncedEmitters.has(emitterId);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* Disposes all tracked IDs on the engine when the realm is destroyed.
|
|
265
|
+
*/
|
|
266
|
+
dispose(engine: GalfusEngine | null): void {
|
|
267
|
+
if (engine) {
|
|
268
|
+
for (const id of this._syncedSprites) engine.realm2dDestroySprite(this.realmId, id);
|
|
269
|
+
for (const id of this._syncedShapes) engine.realm2dDestroyShape(this.realmId, id);
|
|
270
|
+
for (const id of this._syncedLights) engine.realm2dDestroyLight(this.realmId, id);
|
|
271
|
+
for (const id of this._syncedCameras) engine.realm2dDestroyCamera(this.realmId, id);
|
|
272
|
+
for (const id of this._syncedSvgs) engine.realm2dDestroySvg(this.realmId, id);
|
|
273
|
+
for (const id of this._syncedEmitters) engine.realm2dDestroyParticleEmitter(this.realmId, id);
|
|
274
|
+
for (const id of this._syncedMaterials) engine.realm2dDestroyMaterial(this.realmId, id);
|
|
275
|
+
}
|
|
276
|
+
this._syncedSprites.clear();
|
|
277
|
+
this._syncedShapes.clear();
|
|
278
|
+
this._syncedLights.clear();
|
|
279
|
+
this._syncedCameras.clear();
|
|
280
|
+
this._syncedSvgs.clear();
|
|
281
|
+
this._syncedEmitters.clear();
|
|
282
|
+
this._syncedMaterials.clear();
|
|
283
|
+
}
|
|
284
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Realm 2D math, geometry and spatial utility functions.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { Vec2 } from '../../math/vec2';
|
|
6
|
+
|
|
7
|
+
export interface AABB2D {
|
|
8
|
+
min: [number, number];
|
|
9
|
+
max: [number, number];
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function createAABB2D(minX: number, minY: number, maxX: number, maxY: number): AABB2D {
|
|
13
|
+
return {
|
|
14
|
+
min: [minX, minY],
|
|
15
|
+
max: [maxX, maxY]
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function containsPoint2D(aabb: AABB2D, point: [number, number] | Vec2): boolean {
|
|
20
|
+
return (
|
|
21
|
+
point[0] >= aabb.min[0] &&
|
|
22
|
+
point[0] <= aabb.max[0] &&
|
|
23
|
+
point[1] >= aabb.min[1] &&
|
|
24
|
+
point[1] <= aabb.max[1]
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function intersectsAABB2D(a: AABB2D, b: AABB2D): boolean {
|
|
29
|
+
return (
|
|
30
|
+
a.min[0] <= b.max[0] && a.max[0] >= b.min[0] && a.min[1] <= b.max[1] && a.max[1] >= b.min[1]
|
|
31
|
+
);
|
|
32
|
+
}
|
|
@@ -0,0 +1,341 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Realm 3D component declarations.
|
|
3
|
+
* Incorporates spatial Model3D with contiguous local/world matrices and SpatialDirtyFlags.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type {
|
|
7
|
+
Camera3DDesc,
|
|
8
|
+
InstancedModel3DDesc,
|
|
9
|
+
Light3DDesc,
|
|
10
|
+
Material3DDesc,
|
|
11
|
+
ParticleEmitter3DDesc,
|
|
12
|
+
Portal3DDesc
|
|
13
|
+
} from '@galfus/transport-types';
|
|
14
|
+
import type { Component } from '../ecs/component';
|
|
15
|
+
import type { EntityId } from '../ecs/entity';
|
|
16
|
+
import { mat4, type Mat4, quat, type Quat, vec3, type Vec3 } from '../math/index';
|
|
17
|
+
import { SpatialDirtyFlags } from '../spatial/flags';
|
|
18
|
+
|
|
19
|
+
const SCRATCH_P = vec3.create();
|
|
20
|
+
const SCRATCH_S = vec3.create();
|
|
21
|
+
const SCRATCH_Q = quat.create();
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* 3D Spatial node component holding dual 4x4 matrices (local and resolved world)
|
|
25
|
+
* and a two-stage dirty flag state machine (LocalDirty -> WorldDirty -> Clean).
|
|
26
|
+
*/
|
|
27
|
+
export class Model3DComponent implements Component {
|
|
28
|
+
readonly type = 'Model3D' as const;
|
|
29
|
+
readonly modelId: number;
|
|
30
|
+
readonly localMatrix: Mat4;
|
|
31
|
+
readonly worldMatrix: Mat4;
|
|
32
|
+
flags: SpatialDirtyFlags;
|
|
33
|
+
|
|
34
|
+
constructor(modelId: number, initialLocal?: Float32Array | number[]) {
|
|
35
|
+
this.modelId = modelId;
|
|
36
|
+
this.localMatrix = mat4.create();
|
|
37
|
+
this.worldMatrix = mat4.create();
|
|
38
|
+
if (initialLocal) {
|
|
39
|
+
this.localMatrix.set(initialLocal);
|
|
40
|
+
this.worldMatrix.set(initialLocal);
|
|
41
|
+
}
|
|
42
|
+
this.flags = SpatialDirtyFlags.LocalDirty | SpatialDirtyFlags.WorldDirty;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
setPosition(x: number, y: number, z: number): this;
|
|
46
|
+
setPosition(position: [number, number, number] | Vec3): this;
|
|
47
|
+
setPosition(xOrPos: number | [number, number, number] | Vec3, y?: number, z?: number): this {
|
|
48
|
+
if (typeof xOrPos === 'number') {
|
|
49
|
+
this.localMatrix[12] = xOrPos;
|
|
50
|
+
this.localMatrix[13] = y ?? 0;
|
|
51
|
+
this.localMatrix[14] = z ?? 0;
|
|
52
|
+
} else {
|
|
53
|
+
this.localMatrix[12] = xOrPos[0];
|
|
54
|
+
this.localMatrix[13] = xOrPos[1];
|
|
55
|
+
this.localMatrix[14] = xOrPos[2];
|
|
56
|
+
}
|
|
57
|
+
this.flags |= SpatialDirtyFlags.LocalDirty;
|
|
58
|
+
return this;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
setRotation(x: number, y: number, z: number, w: number): this;
|
|
62
|
+
setRotation(rotation: [number, number, number, number] | Quat): this;
|
|
63
|
+
setRotation(
|
|
64
|
+
xOrRot: number | [number, number, number, number] | Quat,
|
|
65
|
+
y?: number,
|
|
66
|
+
z?: number,
|
|
67
|
+
w?: number
|
|
68
|
+
): this {
|
|
69
|
+
mat4.getTranslation(SCRATCH_P, this.localMatrix);
|
|
70
|
+
mat4.getScaling(SCRATCH_S, this.localMatrix);
|
|
71
|
+
let q: Quat;
|
|
72
|
+
if (typeof xOrRot === 'number') {
|
|
73
|
+
quat.set(SCRATCH_Q, xOrRot, y ?? 0, z ?? 0, w ?? 1);
|
|
74
|
+
q = SCRATCH_Q;
|
|
75
|
+
} else {
|
|
76
|
+
q = xOrRot as Quat;
|
|
77
|
+
}
|
|
78
|
+
mat4.fromRotationTranslationScale(this.localMatrix, q, SCRATCH_P, SCRATCH_S);
|
|
79
|
+
this.flags |= SpatialDirtyFlags.LocalDirty;
|
|
80
|
+
return this;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
setScale(x: number, y: number, z: number): this;
|
|
84
|
+
setScale(scale: [number, number, number] | Vec3): this;
|
|
85
|
+
setScale(xOrScale: number | [number, number, number] | Vec3, y?: number, z?: number): this {
|
|
86
|
+
mat4.getTranslation(SCRATCH_P, this.localMatrix);
|
|
87
|
+
mat4.getRotation(SCRATCH_Q, this.localMatrix);
|
|
88
|
+
if (typeof xOrScale === 'number') {
|
|
89
|
+
vec3.set(SCRATCH_S, xOrScale, y ?? 1, z ?? 1);
|
|
90
|
+
} else {
|
|
91
|
+
vec3.set(SCRATCH_S, xOrScale[0], xOrScale[1], xOrScale[2]);
|
|
92
|
+
}
|
|
93
|
+
mat4.fromRotationTranslationScale(this.localMatrix, SCRATCH_Q, SCRATCH_P, SCRATCH_S);
|
|
94
|
+
this.flags |= SpatialDirtyFlags.LocalDirty;
|
|
95
|
+
return this;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
setTRS(
|
|
99
|
+
position?: [number, number, number] | Vec3,
|
|
100
|
+
rotation?: [number, number, number, number] | Quat,
|
|
101
|
+
scale?: [number, number, number] | Vec3
|
|
102
|
+
): this {
|
|
103
|
+
const p = (position ?? [0, 0, 0]) as unknown as Vec3;
|
|
104
|
+
const r = (rotation ?? [0, 0, 0, 1]) as unknown as Quat;
|
|
105
|
+
const s = (scale ?? [1, 1, 1]) as unknown as Vec3;
|
|
106
|
+
mat4.fromRotationTranslationScale(this.localMatrix, r, p, s);
|
|
107
|
+
this.flags |= SpatialDirtyFlags.LocalDirty;
|
|
108
|
+
return this;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
setLocalMatrix(mat: Float32Array | number[]): this {
|
|
112
|
+
this.localMatrix.set(mat);
|
|
113
|
+
this.flags |= SpatialDirtyFlags.LocalDirty;
|
|
114
|
+
return this;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
setWorldMatrix(mat: Float32Array | number[]): this {
|
|
118
|
+
this.worldMatrix.set(mat);
|
|
119
|
+
this.flags |= SpatialDirtyFlags.WorldDirty;
|
|
120
|
+
return this;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
markLocalDirty(): void {
|
|
124
|
+
this.flags |= SpatialDirtyFlags.LocalDirty;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
markWorldDirty(): void {
|
|
128
|
+
this.flags |= SpatialDirtyFlags.WorldDirty;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
get isDirty(): boolean {
|
|
132
|
+
return this.flags !== SpatialDirtyFlags.Clean;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
set isDirty(val: boolean) {
|
|
136
|
+
if (val) {
|
|
137
|
+
this.flags |= SpatialDirtyFlags.WorldDirty;
|
|
138
|
+
} else {
|
|
139
|
+
this.flags = SpatialDirtyFlags.Clean;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export function createModel3D(
|
|
145
|
+
modelId: number,
|
|
146
|
+
initialLocal?: Float32Array | number[]
|
|
147
|
+
): Model3DComponent {
|
|
148
|
+
return new Model3DComponent(modelId, initialLocal);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* 3D Mesh rendering component linked to an existing spatial Model3D on the same entity.
|
|
153
|
+
*/
|
|
154
|
+
export interface Mesh3DDesc {
|
|
155
|
+
geometryId: number;
|
|
156
|
+
materialId: number;
|
|
157
|
+
castShadow?: boolean;
|
|
158
|
+
receiveShadow?: boolean;
|
|
159
|
+
castOutline?: boolean;
|
|
160
|
+
outlineColor?: [number, number, number, number];
|
|
161
|
+
active?: boolean;
|
|
162
|
+
boneOffset?: number;
|
|
163
|
+
boneCount?: number;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
export interface Mesh3DComponent extends Component {
|
|
167
|
+
readonly type: 'Mesh3D';
|
|
168
|
+
readonly meshId: number;
|
|
169
|
+
readonly modelId: number;
|
|
170
|
+
geometryId: number;
|
|
171
|
+
materialId: number;
|
|
172
|
+
castShadow: boolean;
|
|
173
|
+
receiveShadow: boolean;
|
|
174
|
+
castOutline: boolean;
|
|
175
|
+
outlineColor: [number, number, number, number];
|
|
176
|
+
active: boolean;
|
|
177
|
+
boneOffset: number;
|
|
178
|
+
boneCount: number;
|
|
179
|
+
isDirty: boolean;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
export function createMesh3D(meshId: number, modelId: number, desc: Mesh3DDesc): Mesh3DComponent {
|
|
183
|
+
return {
|
|
184
|
+
type: 'Mesh3D',
|
|
185
|
+
meshId,
|
|
186
|
+
modelId,
|
|
187
|
+
geometryId: desc.geometryId,
|
|
188
|
+
materialId: desc.materialId,
|
|
189
|
+
castShadow: desc.castShadow ?? true,
|
|
190
|
+
receiveShadow: desc.receiveShadow ?? true,
|
|
191
|
+
castOutline: desc.castOutline ?? false,
|
|
192
|
+
outlineColor: desc.outlineColor ?? [1, 1, 1, 1],
|
|
193
|
+
active: desc.active ?? true,
|
|
194
|
+
boneOffset: desc.boneOffset ?? 0,
|
|
195
|
+
boneCount: desc.boneCount ?? 0,
|
|
196
|
+
isDirty: true
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
export interface Camera3DComponent extends Component {
|
|
201
|
+
readonly type: 'Camera3D';
|
|
202
|
+
readonly cameraId: number;
|
|
203
|
+
desc: Camera3DDesc;
|
|
204
|
+
isDirty: boolean;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
export function createCamera3D(cameraId: number, desc: Camera3DDesc): Camera3DComponent {
|
|
208
|
+
return {
|
|
209
|
+
type: 'Camera3D',
|
|
210
|
+
cameraId,
|
|
211
|
+
desc,
|
|
212
|
+
isDirty: true
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
export interface Light3DComponent extends Component {
|
|
217
|
+
readonly type: 'Light3D';
|
|
218
|
+
readonly lightId: number;
|
|
219
|
+
desc: Light3DDesc;
|
|
220
|
+
isDirty: boolean;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
export function createLight3D(lightId: number, desc: Light3DDesc): Light3DComponent {
|
|
224
|
+
return {
|
|
225
|
+
type: 'Light3D',
|
|
226
|
+
lightId,
|
|
227
|
+
desc,
|
|
228
|
+
isDirty: true
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
export type Primitive3DKind =
|
|
233
|
+
| { kind: 'cube'; size: number }
|
|
234
|
+
| { kind: 'plane'; width: number; depth: number }
|
|
235
|
+
| { kind: 'quad'; width: number; height: number }
|
|
236
|
+
| { kind: 'sphere'; radius: number; rings: number; sectors: number }
|
|
237
|
+
| { kind: 'cylinder'; radius: number; height: number; sectors: number }
|
|
238
|
+
| { kind: 'capsule'; radius: number; cylinderHeight: number; rings: number; sectors: number }
|
|
239
|
+
| { kind: 'pyramid'; width: number; depth: number; height: number }
|
|
240
|
+
| {
|
|
241
|
+
kind: 'torus';
|
|
242
|
+
majorRadius: number;
|
|
243
|
+
minorRadius: number;
|
|
244
|
+
majorSegments: number;
|
|
245
|
+
minorSegments: number;
|
|
246
|
+
}
|
|
247
|
+
| { kind: 'custom'; topology: number; vertexData: Uint8Array; indices: Uint32Array };
|
|
248
|
+
|
|
249
|
+
export interface GeometryResourceEntry {
|
|
250
|
+
geometryId: number;
|
|
251
|
+
primitive: Primitive3DKind;
|
|
252
|
+
isDirty: boolean;
|
|
253
|
+
morphTargets?: { data: Uint8Array; count: number };
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
export interface MaterialResourceEntry {
|
|
257
|
+
materialId: number;
|
|
258
|
+
desc: Material3DDesc;
|
|
259
|
+
isDirty: boolean;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
export interface ParticleEmitter3DComponent extends Component {
|
|
263
|
+
readonly type: 'ParticleEmitter3D';
|
|
264
|
+
readonly emitterId: number;
|
|
265
|
+
desc: ParticleEmitter3DDesc;
|
|
266
|
+
isDirty: boolean;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
export function createParticleEmitter3D(
|
|
270
|
+
emitterId: number,
|
|
271
|
+
desc: ParticleEmitter3DDesc
|
|
272
|
+
): ParticleEmitter3DComponent {
|
|
273
|
+
return {
|
|
274
|
+
type: 'ParticleEmitter3D',
|
|
275
|
+
emitterId,
|
|
276
|
+
desc,
|
|
277
|
+
isDirty: true
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
export interface Portal3DComponent extends Component {
|
|
282
|
+
readonly type: 'Portal3D';
|
|
283
|
+
readonly portalId: number;
|
|
284
|
+
desc: Portal3DDesc;
|
|
285
|
+
isDirty: boolean;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
export function createPortal3D(portalId: number, desc: Portal3DDesc): Portal3DComponent {
|
|
289
|
+
return {
|
|
290
|
+
type: 'Portal3D',
|
|
291
|
+
portalId,
|
|
292
|
+
desc,
|
|
293
|
+
isDirty: true
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
export interface InstancedModel3DComponent extends Component {
|
|
298
|
+
readonly type: 'InstancedModel3D';
|
|
299
|
+
readonly instancedModelId: number;
|
|
300
|
+
desc: InstancedModel3DDesc;
|
|
301
|
+
isDirty: boolean;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
export function createInstancedModel3D(
|
|
305
|
+
instancedModelId: number,
|
|
306
|
+
desc: InstancedModel3DDesc
|
|
307
|
+
): InstancedModel3DComponent {
|
|
308
|
+
return {
|
|
309
|
+
type: 'InstancedModel3D',
|
|
310
|
+
instancedModelId,
|
|
311
|
+
desc,
|
|
312
|
+
isDirty: true
|
|
313
|
+
};
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
export interface Skin3DComponent extends Component {
|
|
317
|
+
readonly type: 'Skin3D';
|
|
318
|
+
readonly skinId: number;
|
|
319
|
+
readonly modelEntityId: EntityId;
|
|
320
|
+
readonly joints: EntityId[];
|
|
321
|
+
readonly inverseBindMatrices: Float32Array;
|
|
322
|
+
readonly jointMatrices: Float32Array;
|
|
323
|
+
isDirty?: boolean;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
export function createSkin3D(
|
|
327
|
+
skinId: number,
|
|
328
|
+
modelEntityId: EntityId,
|
|
329
|
+
joints: EntityId[],
|
|
330
|
+
inverseBindMatrices: Float32Array
|
|
331
|
+
): Skin3DComponent {
|
|
332
|
+
return {
|
|
333
|
+
type: 'Skin3D',
|
|
334
|
+
skinId,
|
|
335
|
+
modelEntityId,
|
|
336
|
+
joints,
|
|
337
|
+
inverseBindMatrices,
|
|
338
|
+
jointMatrices: new Float32Array(joints.length * 16),
|
|
339
|
+
isDirty: true
|
|
340
|
+
};
|
|
341
|
+
}
|