@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.
- 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,522 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* High-level Realm2D manager combining an ECS World, Anchor solver, and core sync.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type {
|
|
6
|
+
Camera2DDesc,
|
|
7
|
+
GalfusEngine,
|
|
8
|
+
Light2DDesc,
|
|
9
|
+
Material2DDesc,
|
|
10
|
+
ParticleEmitter2DDesc,
|
|
11
|
+
Shadow2DConfigDesc,
|
|
12
|
+
Shape2DDesc,
|
|
13
|
+
Sprite2DDesc,
|
|
14
|
+
Svg2DDesc,
|
|
15
|
+
SvgElementRect,
|
|
16
|
+
SvgTextLayout,
|
|
17
|
+
TransitionConfig
|
|
18
|
+
} from '@galfus/transport-types';
|
|
19
|
+
import { assertValidLogicalId } from '@galfus/transport-types';
|
|
20
|
+
import type { Transform2DProps } from '../anchors/component';
|
|
21
|
+
import { HierarchyCoordinator } from '../anchors/hierarchy';
|
|
22
|
+
import { AnchorSolveSystem } from '../anchors/solver';
|
|
23
|
+
import type { AnimationCoordinator } from '../animation/coordinator';
|
|
24
|
+
import { Collider2DComponent, type ColliderOptions } from '../collision/components';
|
|
25
|
+
import { queryPoint2D, raycast2D } from '../collision/query2d';
|
|
26
|
+
import type { EntityId } from '../ecs/entity';
|
|
27
|
+
import type { SystemContext } from '../ecs/system';
|
|
28
|
+
import { EcsWorld } from '../ecs/world';
|
|
29
|
+
import type { RealmConnector, RealmId, RealmKind } from '../multiverse/realm';
|
|
30
|
+
import type { TransitionId } from '../types';
|
|
31
|
+
import {
|
|
32
|
+
createCamera2D,
|
|
33
|
+
createLight2D,
|
|
34
|
+
createModel2D,
|
|
35
|
+
createParticleEmitter2D,
|
|
36
|
+
createShape2D,
|
|
37
|
+
createSprite2D,
|
|
38
|
+
createSvg2D,
|
|
39
|
+
type ParticleEmitter2DComponent,
|
|
40
|
+
type Svg2DComponent
|
|
41
|
+
} from './components';
|
|
42
|
+
import { Realm2DSyncSystem } from './sync';
|
|
43
|
+
|
|
44
|
+
export class Realm2D implements RealmConnector {
|
|
45
|
+
readonly realmKind: RealmKind = '2d';
|
|
46
|
+
readonly realmId: RealmId;
|
|
47
|
+
readonly world = new EcsWorld();
|
|
48
|
+
readonly solver = new AnchorSolveSystem();
|
|
49
|
+
readonly syncSystem: Realm2DSyncSystem;
|
|
50
|
+
readonly hierarchy: HierarchyCoordinator<'2d'>;
|
|
51
|
+
|
|
52
|
+
private _engine: GalfusEngine | null = null;
|
|
53
|
+
private _nextResourceLogicalId = 1;
|
|
54
|
+
private _preRenderSteppedFrame = -1;
|
|
55
|
+
private _disposed = false;
|
|
56
|
+
private readonly _materials = new Map<number, Material2DDesc>();
|
|
57
|
+
private readonly _svgCache = new Map<
|
|
58
|
+
number,
|
|
59
|
+
{
|
|
60
|
+
rects: Map<string, Readonly<SvgElementRect> | null>;
|
|
61
|
+
layouts: Map<string, Readonly<SvgTextLayout> | null>;
|
|
62
|
+
}
|
|
63
|
+
>();
|
|
64
|
+
|
|
65
|
+
constructor(realmId: RealmId | number) {
|
|
66
|
+
this.realmId = realmId as RealmId;
|
|
67
|
+
assertValidLogicalId(this.realmId, 'RealmId');
|
|
68
|
+
this.hierarchy = new HierarchyCoordinator(this.world, '2d');
|
|
69
|
+
this.syncSystem = new Realm2DSyncSystem(this.realmId, this.solver.cache, (id) => {
|
|
70
|
+
this.invalidateSvgCache(id);
|
|
71
|
+
});
|
|
72
|
+
this._registerInternalSystems();
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
private _registerInternalSystems(): void {
|
|
76
|
+
this.world.addSystem('preRender', this.solver.createSystem(), { kind: 'anchor' });
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
get isDisposed(): boolean {
|
|
80
|
+
return this._disposed;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
allocateResourceId(): number {
|
|
84
|
+
const id = this._nextResourceLogicalId++;
|
|
85
|
+
assertValidLogicalId(id, 'ResourceId');
|
|
86
|
+
return id;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
bindEngine(engine: GalfusEngine): void {
|
|
90
|
+
if (this._disposed) return;
|
|
91
|
+
this._engine = engine;
|
|
92
|
+
engine.realm2dCreate(this.realmId);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
preRender(context: SystemContext): void {
|
|
96
|
+
if (this._disposed) return;
|
|
97
|
+
this.world.stepPreRender(context);
|
|
98
|
+
this._preRenderSteppedFrame = context.frameId;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
sync(context: SystemContext): void {
|
|
102
|
+
if (this._disposed) return;
|
|
103
|
+
if (this._preRenderSteppedFrame !== context.frameId) {
|
|
104
|
+
this.preRender(context);
|
|
105
|
+
}
|
|
106
|
+
if (this._engine) {
|
|
107
|
+
this.syncSystem.sync(this.world, this._engine, this._materials);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
postRender(context: SystemContext): void {
|
|
112
|
+
if (this._disposed) return;
|
|
113
|
+
this.world.stepPostRender(context);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
flush(): void {
|
|
117
|
+
if (this._disposed) return;
|
|
118
|
+
if (this._engine) {
|
|
119
|
+
this.solver.solve(this.world, { time: 0, dt: 0, frameId: 0, engine: this._engine });
|
|
120
|
+
this.syncSystem.sync(this.world, this._engine, this._materials);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
reset(): void {
|
|
125
|
+
if (this._disposed) throw new Error('Cannot reset a disposed Realm2D.');
|
|
126
|
+
this.world.reset();
|
|
127
|
+
this.solver.cache.clear();
|
|
128
|
+
this._materials.clear();
|
|
129
|
+
this._svgCache.clear();
|
|
130
|
+
this._preRenderSteppedFrame = -1;
|
|
131
|
+
this._registerInternalSystems();
|
|
132
|
+
if (this._engine) {
|
|
133
|
+
this.syncSystem.dispose(this._engine);
|
|
134
|
+
this._engine.realm2dDestroy(this.realmId);
|
|
135
|
+
this._engine.realm2dCreate(this.realmId);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
dispose(): void {
|
|
140
|
+
if (this._disposed) return;
|
|
141
|
+
this._disposed = true;
|
|
142
|
+
this.syncSystem.dispose(this._engine);
|
|
143
|
+
if (this._engine) {
|
|
144
|
+
this._engine.realm2dDestroy(this.realmId);
|
|
145
|
+
}
|
|
146
|
+
this._engine = null;
|
|
147
|
+
this.world.reset();
|
|
148
|
+
this.solver.cache.clear();
|
|
149
|
+
this._materials.clear();
|
|
150
|
+
this._svgCache.clear();
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/** Adds a collider to an entity with a matching Model component. */
|
|
154
|
+
addCollider(
|
|
155
|
+
entityId: EntityId,
|
|
156
|
+
shape: import('../collision/components').ColliderShape2D,
|
|
157
|
+
options?: ColliderOptions
|
|
158
|
+
): number {
|
|
159
|
+
if (!this.world.isAlive(entityId) || !this.world.hasComponent(entityId, 'Model2D'))
|
|
160
|
+
throw new Error('Collider requires a live entity with Model2D.');
|
|
161
|
+
const existing = this.world.getComponent<Collider2DComponent>(entityId, 'Collider2D');
|
|
162
|
+
const component = existing ?? new Collider2DComponent();
|
|
163
|
+
const id = this.allocateResourceId();
|
|
164
|
+
component.add(id, shape, options);
|
|
165
|
+
if (!existing) this.world.attachComponent(entityId, component);
|
|
166
|
+
return id;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
queryPoint(point: [number, number], mask = 0xffffffff) {
|
|
170
|
+
if (this._disposed) throw new Error('Cannot query a disposed Realm2D.');
|
|
171
|
+
this.solver.solve(this.world, { time: 0, dt: 0, frameId: 0 });
|
|
172
|
+
return queryPoint2D(this.world, point, mask);
|
|
173
|
+
}
|
|
174
|
+
raycast(
|
|
175
|
+
origin: [number, number],
|
|
176
|
+
direction: [number, number],
|
|
177
|
+
maxDistance: number,
|
|
178
|
+
mask = 0xffffffff
|
|
179
|
+
) {
|
|
180
|
+
if (this._disposed) throw new Error('Cannot query a disposed Realm2D.');
|
|
181
|
+
this.solver.solve(this.world, { time: 0, dt: 0, frameId: 0 });
|
|
182
|
+
return raycast2D(this.world, origin, direction, maxDistance, mask);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
destroyEntity(entityId: EntityId): boolean {
|
|
186
|
+
if (!this.world.isAlive(entityId)) return false;
|
|
187
|
+
this.hierarchy.detach(entityId);
|
|
188
|
+
this.hierarchy.handleTargetDestroyed(entityId);
|
|
189
|
+
this.solver.cache.remove(entityId);
|
|
190
|
+
return this.world.destroyEntity(entityId);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// ---------------------------------------------------------------------------
|
|
194
|
+
// Fluent Entity Spawning Helpers
|
|
195
|
+
// ---------------------------------------------------------------------------
|
|
196
|
+
/**
|
|
197
|
+
* Creates a pure 2D spatial node holding Model2DComponent without visual attachment.
|
|
198
|
+
*/
|
|
199
|
+
createSpatialNode(transform?: Transform2DProps): EntityId {
|
|
200
|
+
const entityId = this.world.createEntity();
|
|
201
|
+
const modelId = this.allocateResourceId();
|
|
202
|
+
const model = createModel2D(modelId);
|
|
203
|
+
if (transform) {
|
|
204
|
+
model.setTRS(transform.position, transform.rotation, transform.scale);
|
|
205
|
+
model.zLayer = transform.zLayer ?? 0;
|
|
206
|
+
model.drawOrder = transform.drawOrder ?? 0;
|
|
207
|
+
}
|
|
208
|
+
this.world.attachComponent(entityId, model);
|
|
209
|
+
return entityId;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
createSprite(desc: Sprite2DDesc, transform?: Transform2DProps): EntityId {
|
|
213
|
+
const entityId = this.world.createEntity();
|
|
214
|
+
const spriteId = this.allocateResourceId();
|
|
215
|
+
|
|
216
|
+
const model = createModel2D(spriteId);
|
|
217
|
+
if (transform) {
|
|
218
|
+
model.setTRS(transform.position, transform.rotation, transform.scale);
|
|
219
|
+
model.zLayer = transform.zLayer ?? 0;
|
|
220
|
+
model.drawOrder = transform.drawOrder ?? 0;
|
|
221
|
+
}
|
|
222
|
+
this.world.attachComponent(entityId, model);
|
|
223
|
+
this.world.attachComponent(entityId, createSprite2D(spriteId, desc));
|
|
224
|
+
return entityId;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
createShape(desc: Shape2DDesc, transform?: Transform2DProps): EntityId {
|
|
228
|
+
const entityId = this.world.createEntity();
|
|
229
|
+
const shapeId = this.allocateResourceId();
|
|
230
|
+
|
|
231
|
+
const model = createModel2D(shapeId);
|
|
232
|
+
if (transform) {
|
|
233
|
+
model.setTRS(transform.position, transform.rotation, transform.scale);
|
|
234
|
+
model.zLayer = transform.zLayer ?? 0;
|
|
235
|
+
model.drawOrder = transform.drawOrder ?? 0;
|
|
236
|
+
}
|
|
237
|
+
this.world.attachComponent(entityId, model);
|
|
238
|
+
this.world.attachComponent(entityId, createShape2D(shapeId, desc));
|
|
239
|
+
return entityId;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
createLight(desc: Light2DDesc, transform?: Transform2DProps): EntityId {
|
|
243
|
+
const entityId = this.world.createEntity();
|
|
244
|
+
const lightId = this.allocateResourceId();
|
|
245
|
+
const model = createModel2D(lightId);
|
|
246
|
+
if (transform) {
|
|
247
|
+
model.setTRS(transform.position, transform.rotation, transform.scale);
|
|
248
|
+
model.zLayer = transform.zLayer ?? 0;
|
|
249
|
+
model.drawOrder = transform.drawOrder ?? 0;
|
|
250
|
+
}
|
|
251
|
+
this.world.attachComponent(entityId, model);
|
|
252
|
+
this.world.attachComponent(entityId, createLight2D(lightId, desc));
|
|
253
|
+
return entityId;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
createCamera(
|
|
257
|
+
desc: Camera2DDesc,
|
|
258
|
+
transform?: Transform2DProps
|
|
259
|
+
): { entityId: EntityId; cameraId: number } {
|
|
260
|
+
const entityId = this.world.createEntity();
|
|
261
|
+
const cameraId = this.allocateResourceId();
|
|
262
|
+
const model = createModel2D(cameraId);
|
|
263
|
+
if (transform) {
|
|
264
|
+
model.setTRS(transform.position, transform.rotation, transform.scale);
|
|
265
|
+
}
|
|
266
|
+
this.world.attachComponent(entityId, model);
|
|
267
|
+
this.world.attachComponent(entityId, createCamera2D(cameraId, desc));
|
|
268
|
+
return { entityId, cameraId };
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
createSvg(desc: Svg2DDesc, transform?: Transform2DProps): { entityId: EntityId; svgId: number } {
|
|
272
|
+
const entityId = this.world.createEntity();
|
|
273
|
+
const svgId = this.allocateResourceId();
|
|
274
|
+
|
|
275
|
+
const model = createModel2D(svgId);
|
|
276
|
+
if (transform) {
|
|
277
|
+
model.setTRS(transform.position, transform.rotation, transform.scale);
|
|
278
|
+
model.zLayer = transform.zLayer ?? 0;
|
|
279
|
+
model.drawOrder = transform.drawOrder ?? 0;
|
|
280
|
+
}
|
|
281
|
+
this.world.attachComponent(entityId, model);
|
|
282
|
+
this.world.attachComponent(entityId, createSvg2D(svgId, desc));
|
|
283
|
+
return { entityId, svgId };
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// ---------------------------------------------------------------------------
|
|
287
|
+
// SVG Dynamic Manipulation
|
|
288
|
+
// ---------------------------------------------------------------------------
|
|
289
|
+
/** Returns element bounds in the SVG viewport coordinate system. */
|
|
290
|
+
getSvgElementRect(svgId: number, elementId: string): Readonly<SvgElementRect> | undefined {
|
|
291
|
+
if (this._disposed || !this._engine) return undefined;
|
|
292
|
+
if (!this.syncSystem.isSvgSynced(svgId)) this.flush();
|
|
293
|
+
if (!this.syncSystem.isSvgSynced(svgId)) return undefined;
|
|
294
|
+
const entries = this._svgCacheEntry(svgId).rects;
|
|
295
|
+
if (!entries.has(elementId)) {
|
|
296
|
+
const rect = this._engine.realm2dGetSvgElementRect(this.realmId, svgId, elementId).rect;
|
|
297
|
+
entries.set(elementId, rect ? Object.freeze(rect) : null);
|
|
298
|
+
}
|
|
299
|
+
return entries.get(elementId) ?? undefined;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/** Returns cached raw shaping metrics in SVG viewport coordinates. */
|
|
303
|
+
getSvgTextLayout(svgId: number, elementId: string): Readonly<SvgTextLayout> | undefined {
|
|
304
|
+
if (this._disposed || !this._engine) return undefined;
|
|
305
|
+
if (!this.syncSystem.isSvgSynced(svgId)) this.flush();
|
|
306
|
+
if (!this.syncSystem.isSvgSynced(svgId)) return undefined;
|
|
307
|
+
const entries = this._svgCacheEntry(svgId).layouts;
|
|
308
|
+
if (!entries.has(elementId)) {
|
|
309
|
+
const layout = this._engine.realm2dGetSvgTextLayout(this.realmId, svgId, elementId).layout;
|
|
310
|
+
if (layout) {
|
|
311
|
+
for (const cluster of layout.clusters) {
|
|
312
|
+
Object.freeze(cluster.transform);
|
|
313
|
+
Object.freeze(cluster);
|
|
314
|
+
}
|
|
315
|
+
Object.freeze(layout.clusters);
|
|
316
|
+
Object.freeze(layout.chunks);
|
|
317
|
+
Object.freeze(layout);
|
|
318
|
+
}
|
|
319
|
+
entries.set(elementId, layout ?? null);
|
|
320
|
+
}
|
|
321
|
+
return entries.get(elementId) ?? undefined;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/** @internal Invalidates cached SVG geometry after native animation updates. */
|
|
325
|
+
invalidateSvgCache(svgId: number): void {
|
|
326
|
+
this._svgCache.delete(svgId);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
private _svgCacheEntry(svgId: number) {
|
|
330
|
+
let entry = this._svgCache.get(svgId);
|
|
331
|
+
if (!entry) {
|
|
332
|
+
entry = { rects: new Map(), layouts: new Map() };
|
|
333
|
+
this._svgCache.set(svgId, entry);
|
|
334
|
+
}
|
|
335
|
+
return entry;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
private _resolveSvgId(svg: number | { svgId?: number; entityId?: EntityId }): number {
|
|
339
|
+
if (typeof svg === 'number') {
|
|
340
|
+
return this.world.getComponent<Svg2DComponent>(svg as EntityId, 'Svg2D')?.svgId ?? svg;
|
|
341
|
+
}
|
|
342
|
+
const resolved =
|
|
343
|
+
svg.svgId ??
|
|
344
|
+
(svg.entityId
|
|
345
|
+
? this.world.getComponent<Svg2DComponent>(svg.entityId, 'Svg2D')?.svgId
|
|
346
|
+
: undefined);
|
|
347
|
+
if (resolved === undefined) {
|
|
348
|
+
throw new Error('Cannot resolve Svg2D ID: entity does not have a Svg2DComponent.');
|
|
349
|
+
}
|
|
350
|
+
return resolved;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
setSvgSource(svg: number | { svgId?: number; entityId?: EntityId }, source: string): void {
|
|
354
|
+
const svgId = this._resolveSvgId(svg);
|
|
355
|
+
this.invalidateSvgCache(svgId);
|
|
356
|
+
if (this._engine) {
|
|
357
|
+
if (!this.syncSystem.isSvgSynced(svgId)) {
|
|
358
|
+
this.flush();
|
|
359
|
+
}
|
|
360
|
+
this._engine.realm2dSetSvgSource(this.realmId, svgId, source);
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
setSvgAttribute(
|
|
365
|
+
svg: number | { svgId?: number; entityId?: EntityId },
|
|
366
|
+
elementId: string,
|
|
367
|
+
name: string,
|
|
368
|
+
value: string
|
|
369
|
+
): void {
|
|
370
|
+
const svgId = this._resolveSvgId(svg);
|
|
371
|
+
this.invalidateSvgCache(svgId);
|
|
372
|
+
if (this._engine) {
|
|
373
|
+
if (!this.syncSystem.isSvgSynced(svgId)) {
|
|
374
|
+
this.flush();
|
|
375
|
+
}
|
|
376
|
+
this._engine.realm2dSetSvgAttribute(this.realmId, svgId, elementId, name, value);
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
removeSvgAttribute(
|
|
381
|
+
svg: number | { svgId?: number; entityId?: EntityId },
|
|
382
|
+
elementId: string,
|
|
383
|
+
name: string
|
|
384
|
+
): void {
|
|
385
|
+
const svgId = this._resolveSvgId(svg);
|
|
386
|
+
this.invalidateSvgCache(svgId);
|
|
387
|
+
if (this._engine) {
|
|
388
|
+
if (!this.syncSystem.isSvgSynced(svgId)) {
|
|
389
|
+
this.flush();
|
|
390
|
+
}
|
|
391
|
+
this._engine.realm2dRemoveSvgAttribute(this.realmId, svgId, elementId, name);
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
setSvgText(
|
|
396
|
+
svg: number | { svgId?: number; entityId?: EntityId },
|
|
397
|
+
elementId: string,
|
|
398
|
+
text: string
|
|
399
|
+
): void {
|
|
400
|
+
const svgId = this._resolveSvgId(svg);
|
|
401
|
+
this.invalidateSvgCache(svgId);
|
|
402
|
+
if (this._engine) {
|
|
403
|
+
if (!this.syncSystem.isSvgSynced(svgId)) {
|
|
404
|
+
this.flush();
|
|
405
|
+
}
|
|
406
|
+
this._engine.realm2dSetSvgText(this.realmId, svgId, elementId, text);
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
// ---------------------------------------------------------------------------
|
|
411
|
+
// 2D Polar Shadows, Fonts & Materials
|
|
412
|
+
// ---------------------------------------------------------------------------
|
|
413
|
+
setShadowConfig(config: Shadow2DConfigDesc): void {
|
|
414
|
+
if (this._engine) {
|
|
415
|
+
this._engine.realm2dSetShadowConfig(this.realmId, config);
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
loadFontData(data: Uint8Array): string {
|
|
420
|
+
if (!this._engine) {
|
|
421
|
+
throw new Error('Cannot load font: Realm2D is not bound to an engine instance.');
|
|
422
|
+
}
|
|
423
|
+
const family = this._engine.realm2dLoadFontData(this.realmId, data, data.byteLength);
|
|
424
|
+
this._svgCache.clear();
|
|
425
|
+
return family;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
/** Registers a material for upload during the next sync or flush. */
|
|
429
|
+
createMaterial(desc: Material2DDesc): number {
|
|
430
|
+
const materialId = this.allocateResourceId();
|
|
431
|
+
this._materials.set(materialId, desc);
|
|
432
|
+
return materialId;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
/** Removes a material on the next sync or flush; entities referencing it are not destroyed. */
|
|
436
|
+
destroyMaterial(materialId: number): void {
|
|
437
|
+
this._materials.delete(materialId);
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
// ---------------------------------------------------------------------------
|
|
441
|
+
// 2D Particle Emitters
|
|
442
|
+
// ---------------------------------------------------------------------------
|
|
443
|
+
createParticleEmitter(
|
|
444
|
+
desc: ParticleEmitter2DDesc,
|
|
445
|
+
transform?: Transform2DProps
|
|
446
|
+
): { entityId: EntityId; emitterId: number } {
|
|
447
|
+
const entityId = this.world.createEntity();
|
|
448
|
+
const emitterId = this.allocateResourceId();
|
|
449
|
+
const model = createModel2D(emitterId);
|
|
450
|
+
if (transform) {
|
|
451
|
+
model.setTRS(transform.position, transform.rotation, transform.scale);
|
|
452
|
+
model.zLayer = transform.zLayer ?? 0;
|
|
453
|
+
model.drawOrder = transform.drawOrder ?? 0;
|
|
454
|
+
}
|
|
455
|
+
this.world.attachComponent(entityId, model);
|
|
456
|
+
this.world.attachComponent(entityId, createParticleEmitter2D(emitterId, desc));
|
|
457
|
+
return { entityId, emitterId };
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
particleEmitterBurst(
|
|
461
|
+
emitter: number | { emitterId?: number; entityId?: EntityId },
|
|
462
|
+
count: number
|
|
463
|
+
): void {
|
|
464
|
+
const emitterId =
|
|
465
|
+
typeof emitter === 'number'
|
|
466
|
+
? (this.world.getComponent<ParticleEmitter2DComponent>(
|
|
467
|
+
emitter as EntityId,
|
|
468
|
+
'ParticleEmitter2D'
|
|
469
|
+
)?.emitterId ?? emitter)
|
|
470
|
+
: (emitter.emitterId ??
|
|
471
|
+
(emitter.entityId
|
|
472
|
+
? this.world.getComponent<ParticleEmitter2DComponent>(
|
|
473
|
+
emitter.entityId,
|
|
474
|
+
'ParticleEmitter2D'
|
|
475
|
+
)?.emitterId
|
|
476
|
+
: undefined));
|
|
477
|
+
if (emitterId === undefined) {
|
|
478
|
+
throw new Error(
|
|
479
|
+
'Cannot resolve ParticleEmitter2D ID: entity does not have a ParticleEmitter2DComponent.'
|
|
480
|
+
);
|
|
481
|
+
}
|
|
482
|
+
if (this._engine) {
|
|
483
|
+
if (!this.syncSystem.isEmitterSynced(emitterId)) {
|
|
484
|
+
this.flush();
|
|
485
|
+
}
|
|
486
|
+
this._engine.realm2dParticleEmitterBurst(this.realmId, emitterId, count);
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
/**
|
|
491
|
+
* Helper to animate a 2D model transform directly through ECS.
|
|
492
|
+
*/
|
|
493
|
+
animateModel(
|
|
494
|
+
entityId: EntityId,
|
|
495
|
+
targetValue: unknown,
|
|
496
|
+
config: TransitionConfig,
|
|
497
|
+
animation: AnimationCoordinator
|
|
498
|
+
): TransitionId | undefined {
|
|
499
|
+
return animation.animateEcsProperty(this.world, entityId, 'transform2d', targetValue, config);
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
/**
|
|
503
|
+
* Helper to animate SVG opacity directly without manual target ID construction.
|
|
504
|
+
*/
|
|
505
|
+
animateSvgOpacity(
|
|
506
|
+
entityId: EntityId,
|
|
507
|
+
initialOpacity: number,
|
|
508
|
+
targetOpacity: number,
|
|
509
|
+
duration: number,
|
|
510
|
+
animation: AnimationCoordinator
|
|
511
|
+
): TransitionId | undefined {
|
|
512
|
+
const svg = this.world.getComponent<Svg2DComponent>(entityId, 'Svg2D');
|
|
513
|
+
if (!svg) return undefined;
|
|
514
|
+
const transId = animation.createScalarTransition(initialOpacity, duration, 0, 0, {
|
|
515
|
+
kind: 'svgOpacity',
|
|
516
|
+
realmId: this.realmId,
|
|
517
|
+
svgId: svg.svgId
|
|
518
|
+
});
|
|
519
|
+
animation.transitionToScalar(transId, targetOpacity, duration);
|
|
520
|
+
return transId;
|
|
521
|
+
}
|
|
522
|
+
}
|