@zylem/game-lib 0.6.0 → 0.6.3
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 +9 -16
- package/dist/actions.d.ts +30 -21
- package/dist/actions.js +628 -145
- package/dist/actions.js.map +1 -1
- package/dist/behavior/platformer-3d.d.ts +296 -0
- package/dist/behavior/platformer-3d.js +518 -0
- package/dist/behavior/platformer-3d.js.map +1 -0
- package/dist/behavior/ricochet-2d.d.ts +274 -0
- package/dist/behavior/ricochet-2d.js +394 -0
- package/dist/behavior/ricochet-2d.js.map +1 -0
- package/dist/behavior/screen-wrap.d.ts +86 -0
- package/dist/behavior/screen-wrap.js +195 -0
- package/dist/behavior/screen-wrap.js.map +1 -0
- package/dist/behavior/thruster.d.ts +10 -0
- package/dist/behavior/thruster.js +234 -0
- package/dist/behavior/thruster.js.map +1 -0
- package/dist/behavior/world-boundary-2d.d.ts +141 -0
- package/dist/behavior/world-boundary-2d.js +181 -0
- package/dist/behavior/world-boundary-2d.js.map +1 -0
- package/dist/behavior-descriptor-BWNWmIjv.d.ts +142 -0
- package/dist/{blueprints-BOCc3Wve.d.ts → blueprints-BWGz8fII.d.ts} +2 -2
- package/dist/camera-B5e4c78l.d.ts +468 -0
- package/dist/camera.d.ts +3 -2
- package/dist/camera.js +962 -166
- package/dist/camera.js.map +1 -1
- package/dist/composition-DrzFrbqI.d.ts +218 -0
- package/dist/{core-CZhozNRH.d.ts → core-DAkskq6Y.d.ts} +97 -65
- package/dist/core.d.ts +12 -6
- package/dist/core.js +4449 -1052
- package/dist/core.js.map +1 -1
- package/dist/{entities-BAxfJOkk.d.ts → entities-DC9ce_vx.d.ts} +154 -45
- package/dist/entities.d.ts +5 -2
- package/dist/entities.js +2505 -722
- package/dist/entities.js.map +1 -1
- package/dist/entity-BpbZqg19.d.ts +1100 -0
- package/dist/entity-types-DAu8sGJH.d.ts +26 -0
- package/dist/global-change-Dc8uCKi2.d.ts +25 -0
- package/dist/main.d.ts +472 -29
- package/dist/main.js +11877 -6124
- package/dist/main.js.map +1 -1
- package/dist/{stage-types-CD21XoIU.d.ts → stage-types-BFsm3qsZ.d.ts} +255 -26
- package/dist/stage.d.ts +11 -6
- package/dist/stage.js +3462 -491
- package/dist/stage.js.map +1 -1
- package/dist/thruster-DhRaJnoL.d.ts +172 -0
- package/dist/world-Be5m1XC1.d.ts +31 -0
- package/package.json +21 -4
- package/dist/behaviors.d.ts +0 -106
- package/dist/behaviors.js +0 -398
- package/dist/behaviors.js.map +0 -1
- package/dist/camera-CpbDr4-V.d.ts +0 -116
- package/dist/entity-COvRtFNG.d.ts +0 -395
- package/dist/moveable-B_vyA6cw.d.ts +0 -67
- package/dist/transformable-CUhvyuYO.d.ts +0 -67
package/dist/stage.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// src/lib/stage/zylem-stage.ts
|
|
2
|
-
import {
|
|
3
|
-
import { Color as
|
|
2
|
+
import { addEntity, createWorld as createECS, removeEntity } from "bitecs";
|
|
3
|
+
import { Color as Color9, Vector3 as Vector318 } from "three";
|
|
4
4
|
|
|
5
5
|
// src/lib/collision/world.ts
|
|
6
6
|
import RAPIER from "@dimforge/rapier3d-compat";
|
|
@@ -61,11 +61,11 @@ function getGlobals() {
|
|
|
61
61
|
}
|
|
62
62
|
|
|
63
63
|
// src/lib/entities/actor.ts
|
|
64
|
-
import { ActiveCollisionTypes, ColliderDesc } from "@dimforge/rapier3d-compat";
|
|
65
|
-
import {
|
|
64
|
+
import { ActiveCollisionTypes as ActiveCollisionTypes2, ColliderDesc as ColliderDesc2 } from "@dimforge/rapier3d-compat";
|
|
65
|
+
import { MeshStandardMaterial as MeshStandardMaterial2, Group as Group2, Vector3 as Vector37 } from "three";
|
|
66
66
|
|
|
67
67
|
// src/lib/entities/entity.ts
|
|
68
|
-
import { ShaderMaterial } from "three";
|
|
68
|
+
import { Mesh, ShaderMaterial, Group } from "three";
|
|
69
69
|
|
|
70
70
|
// src/lib/systems/transformable.system.ts
|
|
71
71
|
import {
|
|
@@ -152,6 +152,9 @@ var BaseNode = class _BaseNode {
|
|
|
152
152
|
markedForRemoval = false;
|
|
153
153
|
/**
|
|
154
154
|
* Lifecycle callback arrays - use onSetup(), onUpdate(), etc. to add callbacks
|
|
155
|
+
* Uses `any` for the type parameter to avoid invariance issues when subclasses
|
|
156
|
+
* are assigned to BaseNode references. Type safety is enforced by the public
|
|
157
|
+
* onSetup/onUpdate/etc. methods which are typed with `this`.
|
|
155
158
|
*/
|
|
156
159
|
lifecycleCallbacks = {
|
|
157
160
|
setup: [],
|
|
@@ -265,12 +268,16 @@ var BaseNode = class _BaseNode {
|
|
|
265
268
|
if (typeof this._update === "function") {
|
|
266
269
|
this._update(params);
|
|
267
270
|
}
|
|
271
|
+
if (typeof this._tickActions === "function") {
|
|
272
|
+
this._tickActions(params.delta);
|
|
273
|
+
}
|
|
268
274
|
for (const callback of this.lifecycleCallbacks.update) {
|
|
269
275
|
callback(params);
|
|
270
276
|
}
|
|
271
277
|
this.children.forEach((child) => child.nodeUpdate(params));
|
|
272
278
|
}
|
|
273
279
|
nodeDestroy(params) {
|
|
280
|
+
if (this.markedForRemoval) return;
|
|
274
281
|
this.children.forEach((child) => child.nodeDestroy(params));
|
|
275
282
|
for (const callback of this.lifecycleCallbacks.destroy) {
|
|
276
283
|
callback(params);
|
|
@@ -279,6 +286,12 @@ var BaseNode = class _BaseNode {
|
|
|
279
286
|
this._destroy(params);
|
|
280
287
|
}
|
|
281
288
|
this.markedForRemoval = true;
|
|
289
|
+
for (const callback of this.lifecycleCallbacks.cleanup) {
|
|
290
|
+
callback(params);
|
|
291
|
+
}
|
|
292
|
+
if (typeof this._cleanup === "function") {
|
|
293
|
+
this._cleanup(params);
|
|
294
|
+
}
|
|
282
295
|
}
|
|
283
296
|
async nodeLoaded(params) {
|
|
284
297
|
if (typeof this._loaded === "function") {
|
|
@@ -288,12 +301,13 @@ var BaseNode = class _BaseNode {
|
|
|
288
301
|
callback(params);
|
|
289
302
|
}
|
|
290
303
|
}
|
|
291
|
-
|
|
304
|
+
nodeCleanup(params) {
|
|
305
|
+
this.children.forEach((child) => child.nodeCleanup?.(params));
|
|
292
306
|
for (const callback of this.lifecycleCallbacks.cleanup) {
|
|
293
307
|
callback(params);
|
|
294
308
|
}
|
|
295
309
|
if (typeof this._cleanup === "function") {
|
|
296
|
-
|
|
310
|
+
this._cleanup(params);
|
|
297
311
|
}
|
|
298
312
|
}
|
|
299
313
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
@@ -307,6 +321,422 @@ var BaseNode = class _BaseNode {
|
|
|
307
321
|
}
|
|
308
322
|
};
|
|
309
323
|
|
|
324
|
+
// ../../node_modules/.pnpm/mitt@3.0.1/node_modules/mitt/dist/mitt.mjs
|
|
325
|
+
function mitt_default(n) {
|
|
326
|
+
return { all: n = n || /* @__PURE__ */ new Map(), on: function(t, e) {
|
|
327
|
+
var i = n.get(t);
|
|
328
|
+
i ? i.push(e) : n.set(t, [e]);
|
|
329
|
+
}, off: function(t, e) {
|
|
330
|
+
var i = n.get(t);
|
|
331
|
+
i && (e ? i.splice(i.indexOf(e) >>> 0, 1) : n.set(t, []));
|
|
332
|
+
}, emit: function(t, e) {
|
|
333
|
+
var i = n.get(t);
|
|
334
|
+
i && i.slice().map(function(n2) {
|
|
335
|
+
n2(e);
|
|
336
|
+
}), (i = n.get("*")) && i.slice().map(function(n2) {
|
|
337
|
+
n2(t, e);
|
|
338
|
+
});
|
|
339
|
+
} };
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
// src/lib/events/event-emitter-delegate.ts
|
|
343
|
+
var EventEmitterDelegate = class {
|
|
344
|
+
emitter;
|
|
345
|
+
unsubscribes = [];
|
|
346
|
+
constructor() {
|
|
347
|
+
this.emitter = mitt_default();
|
|
348
|
+
}
|
|
349
|
+
/**
|
|
350
|
+
* Dispatch an event to all listeners.
|
|
351
|
+
*/
|
|
352
|
+
dispatch(event, payload) {
|
|
353
|
+
this.emitter.emit(event, payload);
|
|
354
|
+
}
|
|
355
|
+
/**
|
|
356
|
+
* Subscribe to an event. Returns an unsubscribe function.
|
|
357
|
+
*/
|
|
358
|
+
listen(event, handler) {
|
|
359
|
+
this.emitter.on(event, handler);
|
|
360
|
+
const unsub = () => this.emitter.off(event, handler);
|
|
361
|
+
this.unsubscribes.push(unsub);
|
|
362
|
+
return unsub;
|
|
363
|
+
}
|
|
364
|
+
/**
|
|
365
|
+
* Subscribe to all events.
|
|
366
|
+
*/
|
|
367
|
+
listenAll(handler) {
|
|
368
|
+
this.emitter.on("*", handler);
|
|
369
|
+
const unsub = () => this.emitter.off("*", handler);
|
|
370
|
+
this.unsubscribes.push(unsub);
|
|
371
|
+
return unsub;
|
|
372
|
+
}
|
|
373
|
+
/**
|
|
374
|
+
* Clean up all subscriptions.
|
|
375
|
+
*/
|
|
376
|
+
dispose() {
|
|
377
|
+
this.unsubscribes.forEach((fn) => fn());
|
|
378
|
+
this.unsubscribes = [];
|
|
379
|
+
this.emitter.all.clear();
|
|
380
|
+
}
|
|
381
|
+
};
|
|
382
|
+
|
|
383
|
+
// src/lib/events/zylem-events.ts
|
|
384
|
+
var zylemEventBus = mitt_default();
|
|
385
|
+
|
|
386
|
+
// src/lib/actions/capabilities/transform-store.ts
|
|
387
|
+
import { proxy as proxy2 } from "valtio";
|
|
388
|
+
function createTransformStore(initial) {
|
|
389
|
+
const defaultState = {
|
|
390
|
+
position: { x: 0, y: 0, z: 0 },
|
|
391
|
+
rotation: { x: 0, y: 0, z: 0, w: 1 },
|
|
392
|
+
velocity: { x: 0, y: 0, z: 0 },
|
|
393
|
+
angularVelocity: { x: 0, y: 0, z: 0 },
|
|
394
|
+
dirty: {
|
|
395
|
+
position: false,
|
|
396
|
+
rotation: false,
|
|
397
|
+
velocity: false,
|
|
398
|
+
angularVelocity: false
|
|
399
|
+
}
|
|
400
|
+
};
|
|
401
|
+
return proxy2({
|
|
402
|
+
...defaultState,
|
|
403
|
+
...initial,
|
|
404
|
+
// Ensure dirty flags are properly initialized even if partial initial state
|
|
405
|
+
dirty: {
|
|
406
|
+
...defaultState.dirty,
|
|
407
|
+
...initial?.dirty
|
|
408
|
+
}
|
|
409
|
+
});
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
// src/lib/actions/capabilities/moveable.ts
|
|
413
|
+
import { Vector3 } from "three";
|
|
414
|
+
function moveX(entity, delta) {
|
|
415
|
+
if (!entity.transformStore) return;
|
|
416
|
+
entity.transformStore.velocity.x = delta;
|
|
417
|
+
entity.transformStore.dirty.velocity = true;
|
|
418
|
+
}
|
|
419
|
+
function moveY(entity, delta) {
|
|
420
|
+
if (!entity.transformStore) return;
|
|
421
|
+
entity.transformStore.velocity.y = delta;
|
|
422
|
+
entity.transformStore.dirty.velocity = true;
|
|
423
|
+
}
|
|
424
|
+
function moveZ(entity, delta) {
|
|
425
|
+
if (!entity.transformStore) return;
|
|
426
|
+
entity.transformStore.velocity.z = delta;
|
|
427
|
+
entity.transformStore.dirty.velocity = true;
|
|
428
|
+
}
|
|
429
|
+
function moveXY(entity, deltaX, deltaY) {
|
|
430
|
+
if (!entity.transformStore) return;
|
|
431
|
+
entity.transformStore.velocity.x = deltaX;
|
|
432
|
+
entity.transformStore.velocity.y = deltaY;
|
|
433
|
+
entity.transformStore.dirty.velocity = true;
|
|
434
|
+
}
|
|
435
|
+
function moveXZ(entity, deltaX, deltaZ) {
|
|
436
|
+
if (!entity.transformStore) return;
|
|
437
|
+
entity.transformStore.velocity.x = deltaX;
|
|
438
|
+
entity.transformStore.velocity.z = deltaZ;
|
|
439
|
+
entity.transformStore.dirty.velocity = true;
|
|
440
|
+
}
|
|
441
|
+
function move(entity, vector) {
|
|
442
|
+
if (!entity.transformStore) return;
|
|
443
|
+
entity.transformStore.velocity.x += vector.x;
|
|
444
|
+
entity.transformStore.velocity.y += vector.y;
|
|
445
|
+
entity.transformStore.velocity.z += vector.z;
|
|
446
|
+
entity.transformStore.dirty.velocity = true;
|
|
447
|
+
}
|
|
448
|
+
function resetVelocity(entity) {
|
|
449
|
+
if (!entity.body) return;
|
|
450
|
+
entity.body.setLinvel(new Vector3(0, 0, 0), true);
|
|
451
|
+
entity.body.setLinearDamping(5);
|
|
452
|
+
}
|
|
453
|
+
function moveForwardXY(entity, delta, rotation2DAngle) {
|
|
454
|
+
const deltaX = Math.sin(-rotation2DAngle) * delta;
|
|
455
|
+
const deltaY = Math.cos(-rotation2DAngle) * delta;
|
|
456
|
+
moveXY(entity, deltaX, deltaY);
|
|
457
|
+
}
|
|
458
|
+
function getPosition(entity) {
|
|
459
|
+
if (!entity.body) return null;
|
|
460
|
+
return entity.body.translation();
|
|
461
|
+
}
|
|
462
|
+
function getVelocity(entity) {
|
|
463
|
+
if (!entity.body) return null;
|
|
464
|
+
return entity.body.linvel();
|
|
465
|
+
}
|
|
466
|
+
function setPosition(entity, x, y, z) {
|
|
467
|
+
if (!entity.body) return;
|
|
468
|
+
entity.body.setTranslation({ x, y, z }, true);
|
|
469
|
+
}
|
|
470
|
+
function setPositionX(entity, x) {
|
|
471
|
+
if (!entity.body) return;
|
|
472
|
+
const { y, z } = entity.body.translation();
|
|
473
|
+
entity.body.setTranslation({ x, y, z }, true);
|
|
474
|
+
}
|
|
475
|
+
function setPositionY(entity, y) {
|
|
476
|
+
if (!entity.body) return;
|
|
477
|
+
const { x, z } = entity.body.translation();
|
|
478
|
+
entity.body.setTranslation({ x, y, z }, true);
|
|
479
|
+
}
|
|
480
|
+
function setPositionZ(entity, z) {
|
|
481
|
+
if (!entity.body) return;
|
|
482
|
+
const { x, y } = entity.body.translation();
|
|
483
|
+
entity.body.setTranslation({ x, y, z }, true);
|
|
484
|
+
}
|
|
485
|
+
function wrapAroundXY(entity, boundsX, boundsY) {
|
|
486
|
+
const position2 = getPosition(entity);
|
|
487
|
+
if (!position2) return;
|
|
488
|
+
const { x, y } = position2;
|
|
489
|
+
const newX = x > boundsX ? -boundsX : x < -boundsX ? boundsX : x;
|
|
490
|
+
const newY = y > boundsY ? -boundsY : y < -boundsY ? boundsY : y;
|
|
491
|
+
if (newX !== x || newY !== y) {
|
|
492
|
+
setPosition(entity, newX, newY, 0);
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
function wrapAround3D(entity, boundsX, boundsY, boundsZ) {
|
|
496
|
+
const position2 = getPosition(entity);
|
|
497
|
+
if (!position2) return;
|
|
498
|
+
const { x, y, z } = position2;
|
|
499
|
+
const newX = x > boundsX ? -boundsX : x < -boundsX ? boundsX : x;
|
|
500
|
+
const newY = y > boundsY ? -boundsY : y < -boundsY ? boundsY : y;
|
|
501
|
+
const newZ = z > boundsZ ? -boundsZ : z < -boundsZ ? boundsZ : z;
|
|
502
|
+
if (newX !== x || newY !== y || newZ !== z) {
|
|
503
|
+
setPosition(entity, newX, newY, newZ);
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
function makeMoveable(entity) {
|
|
507
|
+
const moveable = entity;
|
|
508
|
+
if (!moveable.transformStore) {
|
|
509
|
+
moveable.transformStore = createTransformStore();
|
|
510
|
+
}
|
|
511
|
+
moveable.moveX = (delta) => moveX(entity, delta);
|
|
512
|
+
moveable.moveY = (delta) => moveY(entity, delta);
|
|
513
|
+
moveable.moveZ = (delta) => moveZ(entity, delta);
|
|
514
|
+
moveable.moveXY = (deltaX, deltaY) => moveXY(entity, deltaX, deltaY);
|
|
515
|
+
moveable.moveXZ = (deltaX, deltaZ) => moveXZ(entity, deltaX, deltaZ);
|
|
516
|
+
moveable.move = (vector) => move(entity, vector);
|
|
517
|
+
moveable.resetVelocity = () => resetVelocity(entity);
|
|
518
|
+
moveable.moveForwardXY = (delta, rotation2DAngle) => moveForwardXY(entity, delta, rotation2DAngle);
|
|
519
|
+
moveable.getPosition = () => getPosition(entity);
|
|
520
|
+
moveable.getVelocity = () => getVelocity(entity);
|
|
521
|
+
moveable.setPosition = (x, y, z) => setPosition(entity, x, y, z);
|
|
522
|
+
moveable.setPositionX = (x) => setPositionX(entity, x);
|
|
523
|
+
moveable.setPositionY = (y) => setPositionY(entity, y);
|
|
524
|
+
moveable.setPositionZ = (z) => setPositionZ(entity, z);
|
|
525
|
+
moveable.wrapAroundXY = (boundsX, boundsY) => wrapAroundXY(entity, boundsX, boundsY);
|
|
526
|
+
moveable.wrapAround3D = (boundsX, boundsY, boundsZ) => wrapAround3D(entity, boundsX, boundsY, boundsZ);
|
|
527
|
+
return moveable;
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
// src/lib/actions/capabilities/rotatable.ts
|
|
531
|
+
import { Euler, Vector3 as Vector32, MathUtils, Quaternion as Quaternion2 } from "three";
|
|
532
|
+
function rotateInDirection(entity, moveVector) {
|
|
533
|
+
if (!entity.body) return;
|
|
534
|
+
const rotate = Math.atan2(-moveVector.x, moveVector.z);
|
|
535
|
+
rotateYEuler(entity, rotate);
|
|
536
|
+
}
|
|
537
|
+
function rotateYEuler(entity, amount) {
|
|
538
|
+
rotateEuler(entity, new Vector32(0, -amount, 0));
|
|
539
|
+
}
|
|
540
|
+
function rotateEuler(entity, rotation2) {
|
|
541
|
+
if (!entity.group) return;
|
|
542
|
+
const euler = new Euler(rotation2.x, rotation2.y, rotation2.z);
|
|
543
|
+
entity.group.setRotationFromEuler(euler);
|
|
544
|
+
}
|
|
545
|
+
function rotateY(entity, delta) {
|
|
546
|
+
if (!entity.transformStore) return;
|
|
547
|
+
const halfAngle = delta / 2;
|
|
548
|
+
const deltaW = Math.cos(halfAngle);
|
|
549
|
+
const deltaY = Math.sin(halfAngle);
|
|
550
|
+
const q = entity.transformStore.rotation;
|
|
551
|
+
const newW = q.w * deltaW - q.y * deltaY;
|
|
552
|
+
const newX = q.x * deltaW + q.z * deltaY;
|
|
553
|
+
const newY = q.y * deltaW + q.w * deltaY;
|
|
554
|
+
const newZ = q.z * deltaW - q.x * deltaY;
|
|
555
|
+
entity.transformStore.rotation.w = newW;
|
|
556
|
+
entity.transformStore.rotation.x = newX;
|
|
557
|
+
entity.transformStore.rotation.y = newY;
|
|
558
|
+
entity.transformStore.rotation.z = newZ;
|
|
559
|
+
entity.transformStore.dirty.rotation = true;
|
|
560
|
+
}
|
|
561
|
+
function rotateX(entity, delta) {
|
|
562
|
+
if (!entity.transformStore) return;
|
|
563
|
+
const halfAngle = delta / 2;
|
|
564
|
+
const deltaW = Math.cos(halfAngle);
|
|
565
|
+
const deltaX = Math.sin(halfAngle);
|
|
566
|
+
const q = entity.transformStore.rotation;
|
|
567
|
+
const newW = q.w * deltaW - q.x * deltaX;
|
|
568
|
+
const newX = q.x * deltaW + q.w * deltaX;
|
|
569
|
+
const newY = q.y * deltaW + q.z * deltaX;
|
|
570
|
+
const newZ = q.z * deltaW - q.y * deltaX;
|
|
571
|
+
entity.transformStore.rotation.w = newW;
|
|
572
|
+
entity.transformStore.rotation.x = newX;
|
|
573
|
+
entity.transformStore.rotation.y = newY;
|
|
574
|
+
entity.transformStore.rotation.z = newZ;
|
|
575
|
+
entity.transformStore.dirty.rotation = true;
|
|
576
|
+
}
|
|
577
|
+
function rotateZ(entity, delta) {
|
|
578
|
+
if (!entity.transformStore) return;
|
|
579
|
+
const halfAngle = delta / 2;
|
|
580
|
+
const deltaW = Math.cos(halfAngle);
|
|
581
|
+
const deltaZ = Math.sin(halfAngle);
|
|
582
|
+
const q = entity.transformStore.rotation;
|
|
583
|
+
const newW = q.w * deltaW - q.z * deltaZ;
|
|
584
|
+
const newX = q.x * deltaW - q.y * deltaZ;
|
|
585
|
+
const newY = q.y * deltaW + q.x * deltaZ;
|
|
586
|
+
const newZ = q.z * deltaW + q.w * deltaZ;
|
|
587
|
+
entity.transformStore.rotation.w = newW;
|
|
588
|
+
entity.transformStore.rotation.x = newX;
|
|
589
|
+
entity.transformStore.rotation.y = newY;
|
|
590
|
+
entity.transformStore.rotation.z = newZ;
|
|
591
|
+
entity.transformStore.dirty.rotation = true;
|
|
592
|
+
}
|
|
593
|
+
function setRotationY(entity, y) {
|
|
594
|
+
if (!entity.transformStore) return;
|
|
595
|
+
const halfAngle = y / 2;
|
|
596
|
+
const w = Math.cos(halfAngle);
|
|
597
|
+
const yComponent = Math.sin(halfAngle);
|
|
598
|
+
entity.transformStore.rotation.w = w;
|
|
599
|
+
entity.transformStore.rotation.x = 0;
|
|
600
|
+
entity.transformStore.rotation.y = yComponent;
|
|
601
|
+
entity.transformStore.rotation.z = 0;
|
|
602
|
+
entity.transformStore.dirty.rotation = true;
|
|
603
|
+
}
|
|
604
|
+
function setRotationDegreesY(entity, y) {
|
|
605
|
+
if (!entity.body) return;
|
|
606
|
+
setRotationY(entity, MathUtils.degToRad(y));
|
|
607
|
+
}
|
|
608
|
+
function setRotationX(entity, x) {
|
|
609
|
+
if (!entity.transformStore) return;
|
|
610
|
+
const halfAngle = x / 2;
|
|
611
|
+
const w = Math.cos(halfAngle);
|
|
612
|
+
const xComponent = Math.sin(halfAngle);
|
|
613
|
+
entity.transformStore.rotation.w = w;
|
|
614
|
+
entity.transformStore.rotation.x = xComponent;
|
|
615
|
+
entity.transformStore.rotation.y = 0;
|
|
616
|
+
entity.transformStore.rotation.z = 0;
|
|
617
|
+
entity.transformStore.dirty.rotation = true;
|
|
618
|
+
}
|
|
619
|
+
function setRotationDegreesX(entity, x) {
|
|
620
|
+
if (!entity.body) return;
|
|
621
|
+
setRotationX(entity, MathUtils.degToRad(x));
|
|
622
|
+
}
|
|
623
|
+
function setRotationZ(entity, z) {
|
|
624
|
+
if (!entity.transformStore) return;
|
|
625
|
+
const halfAngle = z / 2;
|
|
626
|
+
const w = Math.cos(halfAngle);
|
|
627
|
+
const zComponent = Math.sin(halfAngle);
|
|
628
|
+
entity.transformStore.rotation.w = w;
|
|
629
|
+
entity.transformStore.rotation.x = 0;
|
|
630
|
+
entity.transformStore.rotation.y = 0;
|
|
631
|
+
entity.transformStore.rotation.z = zComponent;
|
|
632
|
+
entity.transformStore.dirty.rotation = true;
|
|
633
|
+
}
|
|
634
|
+
function setRotationDegreesZ(entity, z) {
|
|
635
|
+
if (!entity.body) return;
|
|
636
|
+
setRotationZ(entity, MathUtils.degToRad(z));
|
|
637
|
+
}
|
|
638
|
+
function setRotation(entity, x, y, z) {
|
|
639
|
+
if (!entity.transformStore) return;
|
|
640
|
+
const quat = new Quaternion2().setFromEuler(new Euler(x, y, z));
|
|
641
|
+
entity.transformStore.rotation.w = quat.w;
|
|
642
|
+
entity.transformStore.rotation.x = quat.x;
|
|
643
|
+
entity.transformStore.rotation.y = quat.y;
|
|
644
|
+
entity.transformStore.rotation.z = quat.z;
|
|
645
|
+
entity.transformStore.dirty.rotation = true;
|
|
646
|
+
}
|
|
647
|
+
function setRotationDegrees(entity, x, y, z) {
|
|
648
|
+
if (!entity.body) return;
|
|
649
|
+
setRotation(entity, MathUtils.degToRad(x), MathUtils.degToRad(y), MathUtils.degToRad(z));
|
|
650
|
+
}
|
|
651
|
+
function getRotation(entity) {
|
|
652
|
+
if (!entity.body) return null;
|
|
653
|
+
return entity.body.rotation();
|
|
654
|
+
}
|
|
655
|
+
function makeRotatable(entity) {
|
|
656
|
+
const rotatableEntity = entity;
|
|
657
|
+
if (!rotatableEntity.transformStore) {
|
|
658
|
+
rotatableEntity.transformStore = createTransformStore();
|
|
659
|
+
}
|
|
660
|
+
rotatableEntity.rotateInDirection = (moveVector) => rotateInDirection(entity, moveVector);
|
|
661
|
+
rotatableEntity.rotateYEuler = (amount) => rotateYEuler(entity, amount);
|
|
662
|
+
rotatableEntity.rotateEuler = (rotation2) => rotateEuler(entity, rotation2);
|
|
663
|
+
rotatableEntity.rotateX = (delta) => rotateX(entity, delta);
|
|
664
|
+
rotatableEntity.rotateY = (delta) => rotateY(entity, delta);
|
|
665
|
+
rotatableEntity.rotateZ = (delta) => rotateZ(entity, delta);
|
|
666
|
+
rotatableEntity.setRotationY = (y) => setRotationY(entity, y);
|
|
667
|
+
rotatableEntity.setRotationX = (x) => setRotationX(entity, x);
|
|
668
|
+
rotatableEntity.setRotationZ = (z) => setRotationZ(entity, z);
|
|
669
|
+
rotatableEntity.setRotationDegreesY = (y) => setRotationDegreesY(entity, y);
|
|
670
|
+
rotatableEntity.setRotationDegreesX = (x) => setRotationDegreesX(entity, x);
|
|
671
|
+
rotatableEntity.setRotationDegreesZ = (z) => setRotationDegreesZ(entity, z);
|
|
672
|
+
rotatableEntity.setRotationDegrees = (x, y, z) => setRotationDegrees(entity, x, y, z);
|
|
673
|
+
rotatableEntity.setRotation = (x, y, z) => setRotation(entity, x, y, z);
|
|
674
|
+
rotatableEntity.getRotation = () => getRotation(entity);
|
|
675
|
+
return rotatableEntity;
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
// src/lib/actions/capabilities/transformable.ts
|
|
679
|
+
function makeTransformable(entity) {
|
|
680
|
+
const withMovement = makeMoveable(entity);
|
|
681
|
+
const withRotation = makeRotatable(withMovement);
|
|
682
|
+
return withRotation;
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
// src/lib/entities/parts/collision-factories.ts
|
|
686
|
+
import {
|
|
687
|
+
ActiveCollisionTypes,
|
|
688
|
+
ColliderDesc,
|
|
689
|
+
RigidBodyDesc,
|
|
690
|
+
RigidBodyType
|
|
691
|
+
} from "@dimforge/rapier3d-compat";
|
|
692
|
+
import { Vector3 as Vector33 } from "three";
|
|
693
|
+
function isCollisionComponent(obj) {
|
|
694
|
+
return obj && obj.__kind === "collision";
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
// src/lib/entities/common.ts
|
|
698
|
+
import { Color, Vector3 as Vector34 } from "three";
|
|
699
|
+
|
|
700
|
+
// src/lib/graphics/shaders/vertex/object.shader.ts
|
|
701
|
+
var objectVertexShader = `
|
|
702
|
+
uniform vec2 uvScale;
|
|
703
|
+
varying vec2 vUv;
|
|
704
|
+
|
|
705
|
+
void main() {
|
|
706
|
+
vUv = uv;
|
|
707
|
+
vec4 mvPosition = modelViewMatrix * vec4( position, 1.0 );
|
|
708
|
+
gl_Position = projectionMatrix * mvPosition;
|
|
709
|
+
}
|
|
710
|
+
`;
|
|
711
|
+
|
|
712
|
+
// src/lib/graphics/shaders/standard.shader.ts
|
|
713
|
+
var fragment = `
|
|
714
|
+
uniform sampler2D tDiffuse;
|
|
715
|
+
varying vec2 vUv;
|
|
716
|
+
|
|
717
|
+
void main() {
|
|
718
|
+
vec4 texel = texture2D( tDiffuse, vUv );
|
|
719
|
+
|
|
720
|
+
gl_FragColor = texel;
|
|
721
|
+
}
|
|
722
|
+
`;
|
|
723
|
+
var standardShader = {
|
|
724
|
+
vertex: objectVertexShader,
|
|
725
|
+
fragment
|
|
726
|
+
};
|
|
727
|
+
|
|
728
|
+
// src/lib/entities/common.ts
|
|
729
|
+
var commonDefaults = {
|
|
730
|
+
position: new Vector34(0, 0, 0),
|
|
731
|
+
material: {
|
|
732
|
+
color: new Color("#ffffff"),
|
|
733
|
+
shader: standardShader
|
|
734
|
+
},
|
|
735
|
+
collision: {
|
|
736
|
+
static: false
|
|
737
|
+
}
|
|
738
|
+
};
|
|
739
|
+
|
|
310
740
|
// src/lib/entities/entity.ts
|
|
311
741
|
var GameEntity = class extends BaseNode {
|
|
312
742
|
behaviors = [];
|
|
@@ -318,20 +748,187 @@ var GameEntity = class extends BaseNode {
|
|
|
318
748
|
colliderDesc;
|
|
319
749
|
collider;
|
|
320
750
|
custom = {};
|
|
751
|
+
// Compound entity support: multiple colliders and meshes
|
|
752
|
+
/** All collider descriptions for this entity (including primary) */
|
|
753
|
+
colliderDescs = [];
|
|
754
|
+
/** All colliders attached to this entity's rigid body */
|
|
755
|
+
colliders = [];
|
|
756
|
+
/** Additional meshes for compound visual entities (added via add()) */
|
|
757
|
+
compoundMeshes = [];
|
|
321
758
|
debugInfo = {};
|
|
322
759
|
debugMaterial;
|
|
323
760
|
collisionDelegate = {
|
|
324
761
|
collision: []
|
|
325
762
|
};
|
|
326
763
|
collisionType;
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
764
|
+
// Instancing support
|
|
765
|
+
/** Batch key for instanced rendering (null if not instanced) */
|
|
766
|
+
batchKey = null;
|
|
767
|
+
/** Index within the instanced mesh batch */
|
|
768
|
+
instanceId = -1;
|
|
769
|
+
/** Whether this entity uses instanced rendering */
|
|
770
|
+
isInstanced = false;
|
|
771
|
+
// Event delegate for dispatch/listen API
|
|
772
|
+
eventDelegate = new EventEmitterDelegate();
|
|
773
|
+
// Behavior references (new ECS pattern)
|
|
774
|
+
behaviorRefs = [];
|
|
775
|
+
// Transform store for batched physics updates (auto-created in create())
|
|
776
|
+
transformStore;
|
|
777
|
+
// Movement & rotation methods are assigned at runtime by makeTransformable.
|
|
778
|
+
// The `implements` clause above ensures the type contract; declarations below
|
|
779
|
+
// satisfy the compiler while the constructor fills them in.
|
|
780
|
+
moveX;
|
|
781
|
+
moveY;
|
|
782
|
+
moveZ;
|
|
783
|
+
moveXY;
|
|
784
|
+
moveXZ;
|
|
785
|
+
move;
|
|
786
|
+
resetVelocity;
|
|
787
|
+
moveForwardXY;
|
|
788
|
+
getPosition;
|
|
789
|
+
getVelocity;
|
|
790
|
+
setPosition;
|
|
791
|
+
setPositionX;
|
|
792
|
+
setPositionY;
|
|
793
|
+
setPositionZ;
|
|
794
|
+
wrapAroundXY;
|
|
795
|
+
wrapAround3D;
|
|
796
|
+
rotateInDirection;
|
|
797
|
+
rotateYEuler;
|
|
798
|
+
rotateEuler;
|
|
799
|
+
rotateX;
|
|
800
|
+
rotateY;
|
|
801
|
+
rotateZ;
|
|
802
|
+
setRotationY;
|
|
803
|
+
setRotationX;
|
|
804
|
+
setRotationZ;
|
|
805
|
+
setRotationDegrees;
|
|
806
|
+
setRotationDegreesY;
|
|
807
|
+
setRotationDegreesX;
|
|
808
|
+
setRotationDegreesZ;
|
|
809
|
+
setRotation;
|
|
810
|
+
getRotation;
|
|
333
811
|
constructor() {
|
|
334
812
|
super();
|
|
813
|
+
this.transformStore = createTransformStore();
|
|
814
|
+
makeTransformable(this);
|
|
815
|
+
}
|
|
816
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
817
|
+
// Composable add() - accepts Mesh, CollisionComponent, or child entities
|
|
818
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
819
|
+
/**
|
|
820
|
+
* Add meshes, collision components, or child entities to this entity.
|
|
821
|
+
* Supports fluent chaining: `entity.add(boxMesh()).add(boxCollision())`.
|
|
822
|
+
*
|
|
823
|
+
* - `Mesh`: First mesh becomes the primary mesh; subsequent meshes are
|
|
824
|
+
* compound meshes grouped together.
|
|
825
|
+
* - `CollisionComponent`: First sets bodyDesc + colliderDesc; subsequent
|
|
826
|
+
* add extra colliders to the same rigid body.
|
|
827
|
+
* - `NodeInterface`: Added as a child entity (delegates to BaseNode.add).
|
|
828
|
+
*/
|
|
829
|
+
add(...components) {
|
|
830
|
+
for (const component of components) {
|
|
831
|
+
if (component instanceof Mesh) {
|
|
832
|
+
this.addMeshComponent(component);
|
|
833
|
+
} else if (isCollisionComponent(component)) {
|
|
834
|
+
this.addCollisionComponent(component);
|
|
835
|
+
} else {
|
|
836
|
+
super.add(component);
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
return this;
|
|
840
|
+
}
|
|
841
|
+
addMeshComponent(mesh) {
|
|
842
|
+
if (!this.mesh) {
|
|
843
|
+
this.mesh = mesh;
|
|
844
|
+
if (!this.materials) {
|
|
845
|
+
this.materials = [];
|
|
846
|
+
}
|
|
847
|
+
if (mesh.material) {
|
|
848
|
+
const mat = mesh.material;
|
|
849
|
+
if (Array.isArray(mat)) {
|
|
850
|
+
this.materials.push(...mat);
|
|
851
|
+
} else {
|
|
852
|
+
this.materials.push(mat);
|
|
853
|
+
}
|
|
854
|
+
}
|
|
855
|
+
} else {
|
|
856
|
+
this.compoundMeshes.push(mesh);
|
|
857
|
+
if (!this.group) {
|
|
858
|
+
this.group = new Group();
|
|
859
|
+
this.group.add(this.mesh);
|
|
860
|
+
}
|
|
861
|
+
this.group.add(mesh);
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
addCollisionComponent(collision) {
|
|
865
|
+
if (!this.bodyDesc) {
|
|
866
|
+
this.bodyDesc = collision.bodyDesc;
|
|
867
|
+
this.colliderDesc = collision.colliderDesc;
|
|
868
|
+
this.colliderDescs.push(collision.colliderDesc);
|
|
869
|
+
const pos = this.options?.position ?? { x: 0, y: 0, z: 0 };
|
|
870
|
+
this.bodyDesc.setTranslation(pos.x, pos.y, pos.z);
|
|
871
|
+
} else {
|
|
872
|
+
this.colliderDescs.push(collision.colliderDesc);
|
|
873
|
+
}
|
|
874
|
+
}
|
|
875
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
876
|
+
// Actions API -- entity-scoped, self-contained stateful actions
|
|
877
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
878
|
+
_actions = [];
|
|
879
|
+
/**
|
|
880
|
+
* Run a fire-and-forget action. Auto-removed when done.
|
|
881
|
+
* @example `me.runAction(moveBy({ x: 10, duration: 0.3 }))`
|
|
882
|
+
*/
|
|
883
|
+
runAction(action) {
|
|
884
|
+
this._actions.push(action);
|
|
885
|
+
return action;
|
|
886
|
+
}
|
|
887
|
+
/**
|
|
888
|
+
* Register a persistent action (throttle, onPress). Not removed when done.
|
|
889
|
+
* @example `const press = entity.action(onPress())`
|
|
890
|
+
*/
|
|
891
|
+
action(action) {
|
|
892
|
+
action.persistent = true;
|
|
893
|
+
this._actions.push(action);
|
|
894
|
+
return action;
|
|
895
|
+
}
|
|
896
|
+
/**
|
|
897
|
+
* Tick all registered actions. Called automatically before user onUpdate callbacks.
|
|
898
|
+
*
|
|
899
|
+
* Resets velocity/angularVelocity accumulation before ticking so that
|
|
900
|
+
* actions can compose via `+=` without cross-frame build-up.
|
|
901
|
+
* (The existing move helpers like `moveXY` use `=` which doesn't accumulate,
|
|
902
|
+
* but action composition needs additive writes on a clean slate each frame.)
|
|
903
|
+
*/
|
|
904
|
+
_tickActions(delta) {
|
|
905
|
+
if (this._actions.length === 0) return;
|
|
906
|
+
const store = this.transformStore;
|
|
907
|
+
store.velocity.x = 0;
|
|
908
|
+
store.velocity.y = 0;
|
|
909
|
+
store.velocity.z = 0;
|
|
910
|
+
store.dirty.velocity = false;
|
|
911
|
+
store.angularVelocity.x = 0;
|
|
912
|
+
store.angularVelocity.y = 0;
|
|
913
|
+
store.angularVelocity.z = 0;
|
|
914
|
+
store.dirty.angularVelocity = false;
|
|
915
|
+
for (let i = this._actions.length - 1; i >= 0; i--) {
|
|
916
|
+
const act = this._actions[i];
|
|
917
|
+
act.tick(this, delta);
|
|
918
|
+
if (act.done && !act.persistent) {
|
|
919
|
+
this._actions.splice(i, 1);
|
|
920
|
+
}
|
|
921
|
+
}
|
|
922
|
+
if (this._actions.length === 0) {
|
|
923
|
+
store.velocity.x = 0;
|
|
924
|
+
store.velocity.y = 0;
|
|
925
|
+
store.velocity.z = 0;
|
|
926
|
+
store.dirty.velocity = false;
|
|
927
|
+
store.angularVelocity.x = 0;
|
|
928
|
+
store.angularVelocity.y = 0;
|
|
929
|
+
store.angularVelocity.z = 0;
|
|
930
|
+
store.dirty.angularVelocity = false;
|
|
931
|
+
}
|
|
335
932
|
}
|
|
336
933
|
create() {
|
|
337
934
|
const { position: setupPosition } = this.options;
|
|
@@ -353,36 +950,88 @@ var GameEntity = class extends BaseNode {
|
|
|
353
950
|
return this;
|
|
354
951
|
}
|
|
355
952
|
/**
|
|
356
|
-
*
|
|
953
|
+
* Use a behavior on this entity via typed descriptor.
|
|
954
|
+
* Behaviors will be auto-registered as systems when the entity is spawned.
|
|
955
|
+
* @param descriptor The behavior descriptor (import from behaviors module)
|
|
956
|
+
* @param options Optional overrides for the behavior's default options
|
|
957
|
+
* @returns BehaviorHandle with behavior-specific methods for lazy FSM access
|
|
958
|
+
*/
|
|
959
|
+
use(descriptor, options) {
|
|
960
|
+
const behaviorRef = {
|
|
961
|
+
descriptor,
|
|
962
|
+
options: { ...descriptor.defaultOptions, ...options }
|
|
963
|
+
};
|
|
964
|
+
this.behaviorRefs.push(behaviorRef);
|
|
965
|
+
const baseHandle = {
|
|
966
|
+
getFSM: () => behaviorRef.fsm ?? null,
|
|
967
|
+
getOptions: () => behaviorRef.options,
|
|
968
|
+
ref: behaviorRef
|
|
969
|
+
};
|
|
970
|
+
const customMethods = descriptor.createHandle?.(behaviorRef) ?? {};
|
|
971
|
+
return {
|
|
972
|
+
...baseHandle,
|
|
973
|
+
...customMethods
|
|
974
|
+
};
|
|
975
|
+
}
|
|
976
|
+
/**
|
|
977
|
+
* Get all behavior references attached to this entity.
|
|
978
|
+
* Used by the stage to auto-register required systems.
|
|
979
|
+
*/
|
|
980
|
+
getBehaviorRefs() {
|
|
981
|
+
return this.behaviorRefs;
|
|
982
|
+
}
|
|
983
|
+
/**
|
|
984
|
+
* Entity-specific setup - resets actions for a fresh stage session.
|
|
357
985
|
* (User callbacks are handled by BaseNode's lifecycleCallbacks.setup)
|
|
358
986
|
*/
|
|
359
987
|
_setup(params) {
|
|
360
|
-
this.
|
|
361
|
-
|
|
362
|
-
|
|
988
|
+
for (let i = this._actions.length - 1; i >= 0; i--) {
|
|
989
|
+
const act = this._actions[i];
|
|
990
|
+
if (act.done && !act.persistent) {
|
|
991
|
+
this._actions.splice(i, 1);
|
|
992
|
+
} else {
|
|
993
|
+
act.reset();
|
|
994
|
+
}
|
|
995
|
+
}
|
|
363
996
|
}
|
|
364
997
|
async _loaded(_params) {
|
|
365
998
|
}
|
|
366
999
|
/**
|
|
367
|
-
* Entity-specific update - updates materials
|
|
1000
|
+
* Entity-specific update - updates materials.
|
|
1001
|
+
* Transform changes are applied by the stage after all update callbacks complete.
|
|
368
1002
|
* (User callbacks are handled by BaseNode's lifecycleCallbacks.update)
|
|
369
1003
|
*/
|
|
370
1004
|
_update(params) {
|
|
371
1005
|
this.updateMaterials(params);
|
|
372
|
-
this.behaviorCallbackMap.update.forEach((callback) => {
|
|
373
|
-
callback({ ...params, me: this });
|
|
374
|
-
});
|
|
375
1006
|
}
|
|
376
1007
|
/**
|
|
377
|
-
* Entity-specific destroy
|
|
378
|
-
*
|
|
1008
|
+
* Entity-specific destroy -- reserved for consumer game logic.
|
|
1009
|
+
* Engine-internal resource disposal runs in _cleanup() instead.
|
|
379
1010
|
*/
|
|
380
1011
|
_destroy(params) {
|
|
381
|
-
this.behaviorCallbackMap.destroy.forEach((callback) => {
|
|
382
|
-
callback({ ...params, me: this });
|
|
383
|
-
});
|
|
384
1012
|
}
|
|
385
|
-
|
|
1013
|
+
/**
|
|
1014
|
+
* Engine-internal resource cleanup -- runs automatically after destroy.
|
|
1015
|
+
* Disposes GPU/DOM resources (meshes, materials, debug material).
|
|
1016
|
+
*
|
|
1017
|
+
* Note: actions, collision callbacks, and behavior refs are intentionally
|
|
1018
|
+
* NOT cleared here -- they are registered by consumer code at module level
|
|
1019
|
+
* and must persist across stage reloads. Actions are reset in _setup().
|
|
1020
|
+
*/
|
|
1021
|
+
_cleanup(_params) {
|
|
1022
|
+
this.disposeEvents();
|
|
1023
|
+
for (const m of this.compoundMeshes) {
|
|
1024
|
+
m.geometry?.dispose();
|
|
1025
|
+
if (Array.isArray(m.material)) {
|
|
1026
|
+
m.material.forEach((mat) => mat.dispose());
|
|
1027
|
+
} else {
|
|
1028
|
+
m.material?.dispose();
|
|
1029
|
+
}
|
|
1030
|
+
}
|
|
1031
|
+
this.compoundMeshes.length = 0;
|
|
1032
|
+
this.debugMaterial?.dispose();
|
|
1033
|
+
this.debugMaterial = void 0;
|
|
1034
|
+
this.group?.removeFromParent();
|
|
386
1035
|
}
|
|
387
1036
|
_collision(other, globals) {
|
|
388
1037
|
if (this.collisionDelegate.collision?.length) {
|
|
@@ -391,25 +1040,6 @@ var GameEntity = class extends BaseNode {
|
|
|
391
1040
|
callback({ entity: this, other, globals });
|
|
392
1041
|
});
|
|
393
1042
|
}
|
|
394
|
-
this.behaviorCallbackMap.collision.forEach((callback) => {
|
|
395
|
-
callback({ entity: this, other, globals });
|
|
396
|
-
});
|
|
397
|
-
}
|
|
398
|
-
addBehavior(behaviorCallback) {
|
|
399
|
-
const handler = behaviorCallback.handler;
|
|
400
|
-
if (handler) {
|
|
401
|
-
this.behaviorCallbackMap[behaviorCallback.type].push(handler);
|
|
402
|
-
}
|
|
403
|
-
return this;
|
|
404
|
-
}
|
|
405
|
-
addBehaviors(behaviorCallbacks) {
|
|
406
|
-
behaviorCallbacks.forEach((callback) => {
|
|
407
|
-
const handler = callback.handler;
|
|
408
|
-
if (handler) {
|
|
409
|
-
this.behaviorCallbackMap[callback.type].push(handler);
|
|
410
|
-
}
|
|
411
|
-
});
|
|
412
|
-
return this;
|
|
413
1043
|
}
|
|
414
1044
|
updateMaterials(params) {
|
|
415
1045
|
if (!this.materials?.length) {
|
|
@@ -430,99 +1060,754 @@ var GameEntity = class extends BaseNode {
|
|
|
430
1060
|
info.eid = this.eid.toString();
|
|
431
1061
|
return info;
|
|
432
1062
|
}
|
|
1063
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
1064
|
+
// Events API
|
|
1065
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
1066
|
+
/**
|
|
1067
|
+
* Dispatch an event from this entity.
|
|
1068
|
+
* Events are emitted both locally and to the global event bus.
|
|
1069
|
+
*/
|
|
1070
|
+
dispatch(event, payload) {
|
|
1071
|
+
this.eventDelegate.dispatch(event, payload);
|
|
1072
|
+
zylemEventBus.emit(event, payload);
|
|
1073
|
+
}
|
|
1074
|
+
/**
|
|
1075
|
+
* Listen for events on this entity instance.
|
|
1076
|
+
* @returns Unsubscribe function
|
|
1077
|
+
*/
|
|
1078
|
+
listen(event, handler) {
|
|
1079
|
+
return this.eventDelegate.listen(event, handler);
|
|
1080
|
+
}
|
|
1081
|
+
/**
|
|
1082
|
+
* Clean up entity event subscriptions.
|
|
1083
|
+
*/
|
|
1084
|
+
disposeEvents() {
|
|
1085
|
+
this.eventDelegate.dispose();
|
|
1086
|
+
}
|
|
433
1087
|
};
|
|
434
1088
|
|
|
435
|
-
// src/lib/core/
|
|
436
|
-
import {
|
|
1089
|
+
// src/lib/core/asset-manager.ts
|
|
1090
|
+
import { LoadingManager, Cache } from "three";
|
|
1091
|
+
|
|
1092
|
+
// src/lib/core/loaders/texture-loader.ts
|
|
1093
|
+
import { TextureLoader, RepeatWrapping } from "three";
|
|
1094
|
+
var TextureLoaderAdapter = class {
|
|
1095
|
+
loader;
|
|
1096
|
+
constructor() {
|
|
1097
|
+
this.loader = new TextureLoader();
|
|
1098
|
+
}
|
|
1099
|
+
isSupported(url) {
|
|
1100
|
+
const ext = url.split(".").pop()?.toLowerCase();
|
|
1101
|
+
return ["png", "jpg", "jpeg", "gif", "webp", "bmp", "tga"].includes(ext || "");
|
|
1102
|
+
}
|
|
1103
|
+
async load(url, options) {
|
|
1104
|
+
const texture = await this.loader.loadAsync(url, (event) => {
|
|
1105
|
+
if (options?.onProgress && event.lengthComputable) {
|
|
1106
|
+
options.onProgress(event.loaded / event.total);
|
|
1107
|
+
}
|
|
1108
|
+
});
|
|
1109
|
+
if (options?.repeat) {
|
|
1110
|
+
texture.repeat.copy(options.repeat);
|
|
1111
|
+
}
|
|
1112
|
+
texture.wrapS = options?.wrapS ?? RepeatWrapping;
|
|
1113
|
+
texture.wrapT = options?.wrapT ?? RepeatWrapping;
|
|
1114
|
+
return texture;
|
|
1115
|
+
}
|
|
1116
|
+
/**
|
|
1117
|
+
* Clone a texture for independent usage
|
|
1118
|
+
*/
|
|
1119
|
+
clone(texture) {
|
|
1120
|
+
const cloned = texture.clone();
|
|
1121
|
+
cloned.needsUpdate = true;
|
|
1122
|
+
return cloned;
|
|
1123
|
+
}
|
|
1124
|
+
};
|
|
1125
|
+
|
|
1126
|
+
// src/lib/core/loaders/gltf-loader.ts
|
|
437
1127
|
import { GLTFLoader } from "three/addons/loaders/GLTFLoader.js";
|
|
438
|
-
var
|
|
439
|
-
loader
|
|
440
|
-
|
|
441
|
-
|
|
1128
|
+
var GLTFLoaderAdapter = class {
|
|
1129
|
+
loader;
|
|
1130
|
+
constructor() {
|
|
1131
|
+
this.loader = new GLTFLoader();
|
|
1132
|
+
}
|
|
1133
|
+
isSupported(url) {
|
|
1134
|
+
const ext = url.split(".").pop()?.toLowerCase();
|
|
1135
|
+
return ["gltf", "glb"].includes(ext || "");
|
|
1136
|
+
}
|
|
1137
|
+
async load(url, options) {
|
|
1138
|
+
if (options?.useAsyncFetch) {
|
|
1139
|
+
return this.loadWithAsyncFetch(url, options);
|
|
1140
|
+
}
|
|
1141
|
+
return this.loadMainThread(url, options);
|
|
1142
|
+
}
|
|
1143
|
+
/**
|
|
1144
|
+
* Load using native fetch + parseAsync
|
|
1145
|
+
* Both fetch and parsing are async, keeping the main thread responsive
|
|
1146
|
+
*/
|
|
1147
|
+
async loadWithAsyncFetch(url, options) {
|
|
1148
|
+
try {
|
|
1149
|
+
const response = await fetch(url);
|
|
1150
|
+
if (!response.ok) {
|
|
1151
|
+
throw new Error(`Failed to fetch ${url}: ${response.status} ${response.statusText}`);
|
|
1152
|
+
}
|
|
1153
|
+
const buffer = await response.arrayBuffer();
|
|
1154
|
+
const gltf = await this.loader.parseAsync(buffer, url);
|
|
1155
|
+
return {
|
|
1156
|
+
object: gltf.scene,
|
|
1157
|
+
animations: gltf.animations,
|
|
1158
|
+
gltf
|
|
1159
|
+
};
|
|
1160
|
+
} catch (error) {
|
|
1161
|
+
console.error(`Async fetch GLTF load failed for ${url}, falling back to loader.load():`, error);
|
|
1162
|
+
return this.loadMainThread(url, options);
|
|
1163
|
+
}
|
|
442
1164
|
}
|
|
443
|
-
async
|
|
1165
|
+
async loadMainThread(url, options) {
|
|
444
1166
|
return new Promise((resolve, reject) => {
|
|
445
1167
|
this.loader.load(
|
|
446
|
-
|
|
447
|
-
(
|
|
448
|
-
const animation = object.animations[0];
|
|
1168
|
+
url,
|
|
1169
|
+
(gltf) => {
|
|
449
1170
|
resolve({
|
|
450
|
-
object,
|
|
451
|
-
|
|
1171
|
+
object: gltf.scene,
|
|
1172
|
+
animations: gltf.animations,
|
|
1173
|
+
gltf
|
|
452
1174
|
});
|
|
453
1175
|
},
|
|
454
|
-
|
|
455
|
-
|
|
1176
|
+
(event) => {
|
|
1177
|
+
if (options?.onProgress && event.lengthComputable) {
|
|
1178
|
+
options.onProgress(event.loaded / event.total);
|
|
1179
|
+
}
|
|
1180
|
+
},
|
|
1181
|
+
(error) => reject(error)
|
|
456
1182
|
);
|
|
457
1183
|
});
|
|
458
1184
|
}
|
|
1185
|
+
/**
|
|
1186
|
+
* Clone a loaded GLTF scene for reuse
|
|
1187
|
+
*/
|
|
1188
|
+
clone(result) {
|
|
1189
|
+
return {
|
|
1190
|
+
object: result.object.clone(),
|
|
1191
|
+
animations: result.animations?.map((anim) => anim.clone()),
|
|
1192
|
+
gltf: result.gltf
|
|
1193
|
+
};
|
|
1194
|
+
}
|
|
459
1195
|
};
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
1196
|
+
|
|
1197
|
+
// src/lib/core/loaders/fbx-loader.ts
|
|
1198
|
+
import { FBXLoader } from "three/addons/loaders/FBXLoader.js";
|
|
1199
|
+
var FBXLoaderAdapter = class {
|
|
1200
|
+
loader;
|
|
1201
|
+
constructor() {
|
|
1202
|
+
this.loader = new FBXLoader();
|
|
1203
|
+
}
|
|
1204
|
+
isSupported(url) {
|
|
1205
|
+
const ext = url.split(".").pop()?.toLowerCase();
|
|
1206
|
+
return ext === "fbx";
|
|
1207
|
+
}
|
|
1208
|
+
async load(url, options) {
|
|
1209
|
+
if (options?.useAsyncFetch) {
|
|
1210
|
+
return this.loadWithAsyncFetch(url, options);
|
|
1211
|
+
}
|
|
1212
|
+
return this.loadMainThread(url, options);
|
|
1213
|
+
}
|
|
1214
|
+
/**
|
|
1215
|
+
* Load using native fetch + parse
|
|
1216
|
+
*/
|
|
1217
|
+
async loadWithAsyncFetch(url, _options) {
|
|
1218
|
+
try {
|
|
1219
|
+
const response = await fetch(url);
|
|
1220
|
+
if (!response.ok) {
|
|
1221
|
+
throw new Error(`Failed to fetch ${url}: ${response.status} ${response.statusText}`);
|
|
1222
|
+
}
|
|
1223
|
+
const buffer = await response.arrayBuffer();
|
|
1224
|
+
const object = this.loader.parse(buffer, url);
|
|
1225
|
+
return {
|
|
1226
|
+
object,
|
|
1227
|
+
animations: object.animations || []
|
|
1228
|
+
};
|
|
1229
|
+
} catch (error) {
|
|
1230
|
+
console.error(`Async fetch FBX load failed for ${url}, falling back to loader.load():`, error);
|
|
1231
|
+
return this.loadMainThread(url, _options);
|
|
1232
|
+
}
|
|
464
1233
|
}
|
|
465
|
-
async
|
|
1234
|
+
async loadMainThread(url, options) {
|
|
466
1235
|
return new Promise((resolve, reject) => {
|
|
467
1236
|
this.loader.load(
|
|
468
|
-
|
|
469
|
-
(
|
|
1237
|
+
url,
|
|
1238
|
+
(object) => {
|
|
470
1239
|
resolve({
|
|
471
|
-
object
|
|
472
|
-
|
|
1240
|
+
object,
|
|
1241
|
+
animations: object.animations || []
|
|
473
1242
|
});
|
|
474
1243
|
},
|
|
475
|
-
|
|
476
|
-
|
|
1244
|
+
(event) => {
|
|
1245
|
+
if (options?.onProgress && event.lengthComputable) {
|
|
1246
|
+
options.onProgress(event.loaded / event.total);
|
|
1247
|
+
}
|
|
1248
|
+
},
|
|
1249
|
+
(error) => reject(error)
|
|
477
1250
|
);
|
|
478
1251
|
});
|
|
479
1252
|
}
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
if (!loader) {
|
|
489
|
-
throw new Error(`Unsupported file type: ${file}`);
|
|
490
|
-
}
|
|
491
|
-
return loader.load(file);
|
|
1253
|
+
/**
|
|
1254
|
+
* Clone a loaded FBX object for reuse
|
|
1255
|
+
*/
|
|
1256
|
+
clone(result) {
|
|
1257
|
+
return {
|
|
1258
|
+
object: result.object.clone(),
|
|
1259
|
+
animations: result.animations?.map((anim) => anim.clone())
|
|
1260
|
+
};
|
|
492
1261
|
}
|
|
493
1262
|
};
|
|
494
1263
|
|
|
495
|
-
// src/lib/
|
|
496
|
-
import {
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
this.
|
|
1264
|
+
// src/lib/core/loaders/obj-loader.ts
|
|
1265
|
+
import { OBJLoader } from "three/addons/loaders/OBJLoader.js";
|
|
1266
|
+
import { MTLLoader } from "three/addons/loaders/MTLLoader.js";
|
|
1267
|
+
var OBJLoaderAdapter = class {
|
|
1268
|
+
loader;
|
|
1269
|
+
mtlLoader;
|
|
1270
|
+
constructor() {
|
|
1271
|
+
this.loader = new OBJLoader();
|
|
1272
|
+
this.mtlLoader = new MTLLoader();
|
|
504
1273
|
}
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
1274
|
+
isSupported(url) {
|
|
1275
|
+
const ext = url.split(".").pop()?.toLowerCase();
|
|
1276
|
+
return ext === "obj";
|
|
1277
|
+
}
|
|
1278
|
+
async load(url, options) {
|
|
1279
|
+
if (options?.mtlPath) {
|
|
1280
|
+
await this.loadMTL(options.mtlPath);
|
|
1281
|
+
}
|
|
1282
|
+
return new Promise((resolve, reject) => {
|
|
1283
|
+
this.loader.load(
|
|
1284
|
+
url,
|
|
1285
|
+
(object) => {
|
|
1286
|
+
resolve({
|
|
1287
|
+
object,
|
|
1288
|
+
animations: []
|
|
1289
|
+
});
|
|
1290
|
+
},
|
|
1291
|
+
(event) => {
|
|
1292
|
+
if (options?.onProgress && event.lengthComputable) {
|
|
1293
|
+
options.onProgress(event.loaded / event.total);
|
|
1294
|
+
}
|
|
1295
|
+
},
|
|
1296
|
+
(error) => reject(error)
|
|
1297
|
+
);
|
|
1298
|
+
});
|
|
1299
|
+
}
|
|
1300
|
+
async loadMTL(url) {
|
|
1301
|
+
return new Promise((resolve, reject) => {
|
|
1302
|
+
this.mtlLoader.load(
|
|
1303
|
+
url,
|
|
1304
|
+
(materials) => {
|
|
1305
|
+
materials.preload();
|
|
1306
|
+
this.loader.setMaterials(materials);
|
|
1307
|
+
resolve();
|
|
1308
|
+
},
|
|
1309
|
+
void 0,
|
|
1310
|
+
(error) => reject(error)
|
|
1311
|
+
);
|
|
1312
|
+
});
|
|
1313
|
+
}
|
|
1314
|
+
/**
|
|
1315
|
+
* Clone a loaded OBJ object for reuse
|
|
1316
|
+
*/
|
|
1317
|
+
clone(result) {
|
|
1318
|
+
return {
|
|
1319
|
+
object: result.object.clone(),
|
|
1320
|
+
animations: []
|
|
1321
|
+
};
|
|
1322
|
+
}
|
|
1323
|
+
};
|
|
1324
|
+
|
|
1325
|
+
// src/lib/core/loaders/audio-loader.ts
|
|
1326
|
+
import { AudioLoader } from "three";
|
|
1327
|
+
var AudioLoaderAdapter = class {
|
|
1328
|
+
loader;
|
|
1329
|
+
constructor() {
|
|
1330
|
+
this.loader = new AudioLoader();
|
|
1331
|
+
}
|
|
1332
|
+
isSupported(url) {
|
|
1333
|
+
const ext = url.split(".").pop()?.toLowerCase();
|
|
1334
|
+
return ["mp3", "ogg", "wav", "flac", "aac", "m4a"].includes(ext || "");
|
|
1335
|
+
}
|
|
1336
|
+
async load(url, options) {
|
|
1337
|
+
return new Promise((resolve, reject) => {
|
|
1338
|
+
this.loader.load(
|
|
1339
|
+
url,
|
|
1340
|
+
(buffer) => resolve(buffer),
|
|
1341
|
+
(event) => {
|
|
1342
|
+
if (options?.onProgress && event.lengthComputable) {
|
|
1343
|
+
options.onProgress(event.loaded / event.total);
|
|
1344
|
+
}
|
|
1345
|
+
},
|
|
1346
|
+
(error) => reject(error)
|
|
1347
|
+
);
|
|
1348
|
+
});
|
|
1349
|
+
}
|
|
1350
|
+
};
|
|
1351
|
+
|
|
1352
|
+
// src/lib/core/loaders/file-loader.ts
|
|
1353
|
+
import { FileLoader } from "three";
|
|
1354
|
+
var FileLoaderAdapter = class {
|
|
1355
|
+
loader;
|
|
1356
|
+
constructor() {
|
|
1357
|
+
this.loader = new FileLoader();
|
|
1358
|
+
}
|
|
1359
|
+
isSupported(_url) {
|
|
1360
|
+
return true;
|
|
1361
|
+
}
|
|
1362
|
+
async load(url, options) {
|
|
1363
|
+
const responseType = options?.responseType ?? "text";
|
|
1364
|
+
this.loader.setResponseType(responseType);
|
|
1365
|
+
return new Promise((resolve, reject) => {
|
|
1366
|
+
this.loader.load(
|
|
1367
|
+
url,
|
|
1368
|
+
(data) => resolve(data),
|
|
1369
|
+
(event) => {
|
|
1370
|
+
if (options?.onProgress && event.lengthComputable) {
|
|
1371
|
+
options.onProgress(event.loaded / event.total);
|
|
1372
|
+
}
|
|
1373
|
+
},
|
|
1374
|
+
(error) => reject(error)
|
|
1375
|
+
);
|
|
1376
|
+
});
|
|
1377
|
+
}
|
|
1378
|
+
};
|
|
1379
|
+
var JsonLoaderAdapter = class {
|
|
1380
|
+
fileLoader;
|
|
1381
|
+
constructor() {
|
|
1382
|
+
this.fileLoader = new FileLoaderAdapter();
|
|
1383
|
+
}
|
|
1384
|
+
isSupported(url) {
|
|
1385
|
+
const ext = url.split(".").pop()?.toLowerCase();
|
|
1386
|
+
return ext === "json";
|
|
1387
|
+
}
|
|
1388
|
+
async load(url, options) {
|
|
1389
|
+
const data = await this.fileLoader.load(url, { ...options, responseType: "json" });
|
|
1390
|
+
return data;
|
|
1391
|
+
}
|
|
1392
|
+
};
|
|
1393
|
+
|
|
1394
|
+
// src/lib/core/asset-manager.ts
|
|
1395
|
+
var AssetManager = class _AssetManager {
|
|
1396
|
+
static instance = null;
|
|
1397
|
+
// Caches for different asset types
|
|
1398
|
+
textureCache = /* @__PURE__ */ new Map();
|
|
1399
|
+
modelCache = /* @__PURE__ */ new Map();
|
|
1400
|
+
audioCache = /* @__PURE__ */ new Map();
|
|
1401
|
+
fileCache = /* @__PURE__ */ new Map();
|
|
1402
|
+
jsonCache = /* @__PURE__ */ new Map();
|
|
1403
|
+
// Loaders
|
|
1404
|
+
textureLoader;
|
|
1405
|
+
gltfLoader;
|
|
1406
|
+
fbxLoader;
|
|
1407
|
+
objLoader;
|
|
1408
|
+
audioLoader;
|
|
1409
|
+
fileLoader;
|
|
1410
|
+
jsonLoader;
|
|
1411
|
+
// Loading manager for progress tracking
|
|
1412
|
+
loadingManager;
|
|
1413
|
+
// Event emitter
|
|
1414
|
+
events;
|
|
1415
|
+
// Stats
|
|
1416
|
+
stats = {
|
|
1417
|
+
texturesLoaded: 0,
|
|
1418
|
+
modelsLoaded: 0,
|
|
1419
|
+
audioLoaded: 0,
|
|
1420
|
+
filesLoaded: 0,
|
|
1421
|
+
cacheHits: 0,
|
|
1422
|
+
cacheMisses: 0
|
|
1423
|
+
};
|
|
1424
|
+
constructor() {
|
|
1425
|
+
this.textureLoader = new TextureLoaderAdapter();
|
|
1426
|
+
this.gltfLoader = new GLTFLoaderAdapter();
|
|
1427
|
+
this.fbxLoader = new FBXLoaderAdapter();
|
|
1428
|
+
this.objLoader = new OBJLoaderAdapter();
|
|
1429
|
+
this.audioLoader = new AudioLoaderAdapter();
|
|
1430
|
+
this.fileLoader = new FileLoaderAdapter();
|
|
1431
|
+
this.jsonLoader = new JsonLoaderAdapter();
|
|
1432
|
+
this.loadingManager = new LoadingManager();
|
|
1433
|
+
this.loadingManager.onProgress = (url, loaded, total) => {
|
|
1434
|
+
this.events.emit("batch:progress", { loaded, total });
|
|
1435
|
+
};
|
|
1436
|
+
this.events = mitt_default();
|
|
1437
|
+
Cache.enabled = true;
|
|
1438
|
+
}
|
|
1439
|
+
/**
|
|
1440
|
+
* Get the singleton instance
|
|
1441
|
+
*/
|
|
1442
|
+
static getInstance() {
|
|
1443
|
+
if (!_AssetManager.instance) {
|
|
1444
|
+
_AssetManager.instance = new _AssetManager();
|
|
1445
|
+
}
|
|
1446
|
+
return _AssetManager.instance;
|
|
1447
|
+
}
|
|
1448
|
+
/**
|
|
1449
|
+
* Reset the singleton (useful for testing)
|
|
1450
|
+
*/
|
|
1451
|
+
static resetInstance() {
|
|
1452
|
+
if (_AssetManager.instance) {
|
|
1453
|
+
_AssetManager.instance.clearCache();
|
|
1454
|
+
_AssetManager.instance = null;
|
|
1455
|
+
}
|
|
1456
|
+
}
|
|
1457
|
+
// ==================== TEXTURE LOADING ====================
|
|
1458
|
+
/**
|
|
1459
|
+
* Load a texture with caching
|
|
1460
|
+
*/
|
|
1461
|
+
async loadTexture(url, options) {
|
|
1462
|
+
return this.loadWithCache(
|
|
1463
|
+
url,
|
|
1464
|
+
"texture" /* TEXTURE */,
|
|
1465
|
+
this.textureCache,
|
|
1466
|
+
() => this.textureLoader.load(url, options),
|
|
1467
|
+
options,
|
|
1468
|
+
(texture) => {
|
|
1469
|
+
if (!options?.clone) return texture;
|
|
1470
|
+
const cloned = this.textureLoader.clone(texture);
|
|
1471
|
+
if (options.repeat) {
|
|
1472
|
+
cloned.repeat.copy(options.repeat);
|
|
1473
|
+
}
|
|
1474
|
+
if (options.wrapS !== void 0) {
|
|
1475
|
+
cloned.wrapS = options.wrapS;
|
|
1476
|
+
}
|
|
1477
|
+
if (options.wrapT !== void 0) {
|
|
1478
|
+
cloned.wrapT = options.wrapT;
|
|
1479
|
+
}
|
|
1480
|
+
return cloned;
|
|
1481
|
+
}
|
|
1482
|
+
);
|
|
1483
|
+
}
|
|
1484
|
+
// ==================== MODEL LOADING ====================
|
|
1485
|
+
/**
|
|
1486
|
+
* Load a GLTF/GLB model with caching
|
|
1487
|
+
*/
|
|
1488
|
+
async loadGLTF(url, options) {
|
|
1489
|
+
return this.loadWithCache(
|
|
1490
|
+
url,
|
|
1491
|
+
"gltf" /* GLTF */,
|
|
1492
|
+
this.modelCache,
|
|
1493
|
+
() => this.gltfLoader.load(url, options),
|
|
1494
|
+
options,
|
|
1495
|
+
(result) => options?.clone ? this.gltfLoader.clone(result) : result
|
|
1496
|
+
);
|
|
1497
|
+
}
|
|
1498
|
+
/**
|
|
1499
|
+
* Load an FBX model with caching
|
|
1500
|
+
*/
|
|
1501
|
+
async loadFBX(url, options) {
|
|
1502
|
+
return this.loadWithCache(
|
|
1503
|
+
url,
|
|
1504
|
+
"fbx" /* FBX */,
|
|
1505
|
+
this.modelCache,
|
|
1506
|
+
() => this.fbxLoader.load(url, options),
|
|
1507
|
+
options,
|
|
1508
|
+
(result) => options?.clone ? this.fbxLoader.clone(result) : result
|
|
1509
|
+
);
|
|
1510
|
+
}
|
|
1511
|
+
/**
|
|
1512
|
+
* Load an OBJ model with caching
|
|
1513
|
+
*/
|
|
1514
|
+
async loadOBJ(url, options) {
|
|
1515
|
+
const cacheKey = options?.mtlPath ? `${url}:${options.mtlPath}` : url;
|
|
1516
|
+
return this.loadWithCache(
|
|
1517
|
+
cacheKey,
|
|
1518
|
+
"obj" /* OBJ */,
|
|
1519
|
+
this.modelCache,
|
|
1520
|
+
() => this.objLoader.load(url, options),
|
|
1521
|
+
options,
|
|
1522
|
+
(result) => options?.clone ? this.objLoader.clone(result) : result
|
|
1523
|
+
);
|
|
1524
|
+
}
|
|
1525
|
+
/**
|
|
1526
|
+
* Auto-detect model type and load
|
|
1527
|
+
*/
|
|
1528
|
+
async loadModel(url, options) {
|
|
1529
|
+
const ext = url.split(".").pop()?.toLowerCase();
|
|
1530
|
+
switch (ext) {
|
|
1531
|
+
case "gltf":
|
|
1532
|
+
case "glb":
|
|
1533
|
+
return this.loadGLTF(url, options);
|
|
1534
|
+
case "fbx":
|
|
1535
|
+
return this.loadFBX(url, options);
|
|
1536
|
+
case "obj":
|
|
1537
|
+
return this.loadOBJ(url, options);
|
|
1538
|
+
default:
|
|
1539
|
+
throw new Error(`Unsupported model format: ${ext}`);
|
|
1540
|
+
}
|
|
1541
|
+
}
|
|
1542
|
+
// ==================== AUDIO LOADING ====================
|
|
1543
|
+
/**
|
|
1544
|
+
* Load an audio buffer with caching
|
|
1545
|
+
*/
|
|
1546
|
+
async loadAudio(url, options) {
|
|
1547
|
+
return this.loadWithCache(
|
|
1548
|
+
url,
|
|
1549
|
+
"audio" /* AUDIO */,
|
|
1550
|
+
this.audioCache,
|
|
1551
|
+
() => this.audioLoader.load(url, options),
|
|
1552
|
+
options
|
|
1553
|
+
);
|
|
1554
|
+
}
|
|
1555
|
+
// ==================== FILE LOADING ====================
|
|
1556
|
+
/**
|
|
1557
|
+
* Load a raw file with caching
|
|
1558
|
+
*/
|
|
1559
|
+
async loadFile(url, options) {
|
|
1560
|
+
const cacheKey = options?.responseType ? `${url}:${options.responseType}` : url;
|
|
1561
|
+
return this.loadWithCache(
|
|
1562
|
+
cacheKey,
|
|
1563
|
+
"file" /* FILE */,
|
|
1564
|
+
this.fileCache,
|
|
1565
|
+
() => this.fileLoader.load(url, options),
|
|
1566
|
+
options
|
|
1567
|
+
);
|
|
1568
|
+
}
|
|
1569
|
+
/**
|
|
1570
|
+
* Load a JSON file with caching
|
|
1571
|
+
*/
|
|
1572
|
+
async loadJSON(url, options) {
|
|
1573
|
+
return this.loadWithCache(
|
|
1574
|
+
url,
|
|
1575
|
+
"json" /* JSON */,
|
|
1576
|
+
this.jsonCache,
|
|
1577
|
+
() => this.jsonLoader.load(url, options),
|
|
1578
|
+
options
|
|
1579
|
+
);
|
|
1580
|
+
}
|
|
1581
|
+
// ==================== BATCH LOADING ====================
|
|
1582
|
+
/**
|
|
1583
|
+
* Load multiple assets in parallel
|
|
1584
|
+
*/
|
|
1585
|
+
async loadBatch(items) {
|
|
1586
|
+
const results = /* @__PURE__ */ new Map();
|
|
1587
|
+
const promises = items.map(async (item) => {
|
|
1588
|
+
try {
|
|
1589
|
+
let result;
|
|
1590
|
+
switch (item.type) {
|
|
1591
|
+
case "texture" /* TEXTURE */:
|
|
1592
|
+
result = await this.loadTexture(item.url, item.options);
|
|
1593
|
+
break;
|
|
1594
|
+
case "gltf" /* GLTF */:
|
|
1595
|
+
result = await this.loadGLTF(item.url, item.options);
|
|
1596
|
+
break;
|
|
1597
|
+
case "fbx" /* FBX */:
|
|
1598
|
+
result = await this.loadFBX(item.url, item.options);
|
|
1599
|
+
break;
|
|
1600
|
+
case "obj" /* OBJ */:
|
|
1601
|
+
result = await this.loadOBJ(item.url, item.options);
|
|
1602
|
+
break;
|
|
1603
|
+
case "audio" /* AUDIO */:
|
|
1604
|
+
result = await this.loadAudio(item.url, item.options);
|
|
1605
|
+
break;
|
|
1606
|
+
case "file" /* FILE */:
|
|
1607
|
+
result = await this.loadFile(item.url, item.options);
|
|
1608
|
+
break;
|
|
1609
|
+
case "json" /* JSON */:
|
|
1610
|
+
result = await this.loadJSON(item.url, item.options);
|
|
1611
|
+
break;
|
|
1612
|
+
default:
|
|
1613
|
+
throw new Error(`Unknown asset type: ${item.type}`);
|
|
1614
|
+
}
|
|
1615
|
+
results.set(item.url, result);
|
|
1616
|
+
} catch (error) {
|
|
1617
|
+
this.events.emit("asset:error", {
|
|
1618
|
+
url: item.url,
|
|
1619
|
+
type: item.type,
|
|
1620
|
+
error
|
|
1621
|
+
});
|
|
1622
|
+
throw error;
|
|
1623
|
+
}
|
|
1624
|
+
});
|
|
1625
|
+
await Promise.all(promises);
|
|
1626
|
+
this.events.emit("batch:complete", { urls: items.map((i) => i.url) });
|
|
1627
|
+
return results;
|
|
1628
|
+
}
|
|
1629
|
+
/**
|
|
1630
|
+
* Preload assets without returning results
|
|
1631
|
+
*/
|
|
1632
|
+
async preload(items) {
|
|
1633
|
+
await this.loadBatch(items);
|
|
1634
|
+
}
|
|
1635
|
+
// ==================== CACHE MANAGEMENT ====================
|
|
1636
|
+
/**
|
|
1637
|
+
* Check if an asset is cached
|
|
1638
|
+
*/
|
|
1639
|
+
isCached(url) {
|
|
1640
|
+
return this.textureCache.has(url) || this.modelCache.has(url) || this.audioCache.has(url) || this.fileCache.has(url) || this.jsonCache.has(url);
|
|
1641
|
+
}
|
|
1642
|
+
/**
|
|
1643
|
+
* Clear all caches or a specific URL
|
|
1644
|
+
*/
|
|
1645
|
+
clearCache(url) {
|
|
1646
|
+
if (url) {
|
|
1647
|
+
this.textureCache.delete(url);
|
|
1648
|
+
this.modelCache.delete(url);
|
|
1649
|
+
this.audioCache.delete(url);
|
|
1650
|
+
this.fileCache.delete(url);
|
|
1651
|
+
this.jsonCache.delete(url);
|
|
1652
|
+
} else {
|
|
1653
|
+
this.textureCache.clear();
|
|
1654
|
+
this.modelCache.clear();
|
|
1655
|
+
this.audioCache.clear();
|
|
1656
|
+
this.fileCache.clear();
|
|
1657
|
+
this.jsonCache.clear();
|
|
1658
|
+
Cache.clear();
|
|
1659
|
+
}
|
|
1660
|
+
}
|
|
1661
|
+
/**
|
|
1662
|
+
* Get cache statistics
|
|
1663
|
+
*/
|
|
1664
|
+
getStats() {
|
|
1665
|
+
return { ...this.stats };
|
|
1666
|
+
}
|
|
1667
|
+
// ==================== EVENTS ====================
|
|
1668
|
+
/**
|
|
1669
|
+
* Subscribe to asset manager events
|
|
1670
|
+
*/
|
|
1671
|
+
on(event, handler) {
|
|
1672
|
+
this.events.on(event, handler);
|
|
1673
|
+
}
|
|
1674
|
+
/**
|
|
1675
|
+
* Unsubscribe from asset manager events
|
|
1676
|
+
*/
|
|
1677
|
+
off(event, handler) {
|
|
1678
|
+
this.events.off(event, handler);
|
|
1679
|
+
}
|
|
1680
|
+
// ==================== PRIVATE HELPERS ====================
|
|
1681
|
+
/**
|
|
1682
|
+
* Generic cache wrapper for loading assets
|
|
1683
|
+
*/
|
|
1684
|
+
async loadWithCache(url, type, cache, loader, options, cloner) {
|
|
1685
|
+
if (options?.forceReload) {
|
|
1686
|
+
cache.delete(url);
|
|
1687
|
+
}
|
|
1688
|
+
const cached = cache.get(url);
|
|
1689
|
+
if (cached) {
|
|
1690
|
+
this.stats.cacheHits++;
|
|
1691
|
+
this.events.emit("asset:loaded", { url, type, fromCache: true });
|
|
1692
|
+
const result = await cached.promise;
|
|
1693
|
+
return cloner ? cloner(result) : result;
|
|
1694
|
+
}
|
|
1695
|
+
this.stats.cacheMisses++;
|
|
1696
|
+
this.events.emit("asset:loading", { url, type });
|
|
1697
|
+
const promise = loader();
|
|
1698
|
+
const entry = {
|
|
1699
|
+
promise,
|
|
1700
|
+
loadedAt: Date.now()
|
|
1701
|
+
};
|
|
1702
|
+
cache.set(url, entry);
|
|
1703
|
+
try {
|
|
1704
|
+
const result = await promise;
|
|
1705
|
+
entry.result = result;
|
|
1706
|
+
this.updateStats(type);
|
|
1707
|
+
this.events.emit("asset:loaded", { url, type, fromCache: false });
|
|
1708
|
+
return cloner ? cloner(result) : result;
|
|
1709
|
+
} catch (error) {
|
|
1710
|
+
cache.delete(url);
|
|
1711
|
+
this.events.emit("asset:error", { url, type, error });
|
|
1712
|
+
throw error;
|
|
1713
|
+
}
|
|
1714
|
+
}
|
|
1715
|
+
updateStats(type) {
|
|
1716
|
+
switch (type) {
|
|
1717
|
+
case "texture" /* TEXTURE */:
|
|
1718
|
+
this.stats.texturesLoaded++;
|
|
1719
|
+
break;
|
|
1720
|
+
case "gltf" /* GLTF */:
|
|
1721
|
+
case "fbx" /* FBX */:
|
|
1722
|
+
case "obj" /* OBJ */:
|
|
1723
|
+
this.stats.modelsLoaded++;
|
|
1724
|
+
break;
|
|
1725
|
+
case "audio" /* AUDIO */:
|
|
1726
|
+
this.stats.audioLoaded++;
|
|
1727
|
+
break;
|
|
1728
|
+
case "file" /* FILE */:
|
|
1729
|
+
case "json" /* JSON */:
|
|
1730
|
+
this.stats.filesLoaded++;
|
|
1731
|
+
break;
|
|
1732
|
+
}
|
|
1733
|
+
}
|
|
1734
|
+
};
|
|
1735
|
+
var assetManager = AssetManager.getInstance();
|
|
1736
|
+
|
|
1737
|
+
// src/lib/core/entity-asset-loader.ts
|
|
1738
|
+
var EntityAssetLoader = class {
|
|
1739
|
+
/**
|
|
1740
|
+
* Load a model file (FBX, GLTF, GLB, OBJ) using the asset manager
|
|
1741
|
+
*/
|
|
1742
|
+
async loadFile(file) {
|
|
1743
|
+
const ext = file.split(".").pop()?.toLowerCase();
|
|
1744
|
+
switch (ext) {
|
|
1745
|
+
case "fbx": {
|
|
1746
|
+
const result = await assetManager.loadFBX(file);
|
|
1747
|
+
return {
|
|
1748
|
+
object: result.object,
|
|
1749
|
+
animation: result.animations?.[0]
|
|
1750
|
+
};
|
|
1751
|
+
}
|
|
1752
|
+
case "gltf":
|
|
1753
|
+
case "glb": {
|
|
1754
|
+
const result = await assetManager.loadGLTF(file);
|
|
1755
|
+
return {
|
|
1756
|
+
object: result.object,
|
|
1757
|
+
gltf: result.gltf
|
|
1758
|
+
};
|
|
1759
|
+
}
|
|
1760
|
+
case "obj": {
|
|
1761
|
+
const result = await assetManager.loadOBJ(file);
|
|
1762
|
+
return {
|
|
1763
|
+
object: result.object
|
|
1764
|
+
};
|
|
1765
|
+
}
|
|
1766
|
+
default:
|
|
1767
|
+
throw new Error(`Unsupported file type: ${file}`);
|
|
1768
|
+
}
|
|
1769
|
+
}
|
|
1770
|
+
};
|
|
1771
|
+
|
|
1772
|
+
// src/lib/entities/delegates/animation.ts
|
|
1773
|
+
import {
|
|
1774
|
+
AnimationMixer,
|
|
1775
|
+
LoopOnce,
|
|
1776
|
+
LoopRepeat
|
|
1777
|
+
} from "three";
|
|
1778
|
+
var AnimationDelegate = class {
|
|
1779
|
+
constructor(target) {
|
|
1780
|
+
this.target = target;
|
|
1781
|
+
}
|
|
1782
|
+
_mixer = null;
|
|
1783
|
+
_actions = {};
|
|
1784
|
+
_animations = [];
|
|
1785
|
+
_currentAction = null;
|
|
1786
|
+
_pauseAtPercentage = 0;
|
|
1787
|
+
_isPaused = false;
|
|
1788
|
+
_queuedKey = null;
|
|
1789
|
+
_fadeDuration = 0.5;
|
|
513
1790
|
_currentKey = "";
|
|
514
1791
|
_assetLoader = new EntityAssetLoader();
|
|
515
1792
|
async loadAnimations(animations) {
|
|
516
1793
|
if (!animations.length) return;
|
|
517
1794
|
const results = await Promise.all(animations.map((a) => this._assetLoader.loadFile(a.path)));
|
|
518
|
-
|
|
519
|
-
|
|
1795
|
+
const loadedAnimations = [];
|
|
1796
|
+
results.forEach((result, i) => {
|
|
1797
|
+
if (result.animation) {
|
|
1798
|
+
loadedAnimations.push({
|
|
1799
|
+
key: animations[i].key || i.toString(),
|
|
1800
|
+
clip: result.animation
|
|
1801
|
+
});
|
|
1802
|
+
}
|
|
1803
|
+
});
|
|
1804
|
+
if (!loadedAnimations.length) return;
|
|
1805
|
+
this._animations = loadedAnimations.map((a) => a.clip);
|
|
520
1806
|
this._mixer = new AnimationMixer(this.target);
|
|
521
|
-
|
|
522
|
-
const key = animations[i].key || i.toString();
|
|
1807
|
+
loadedAnimations.forEach(({ key, clip }) => {
|
|
523
1808
|
this._actions[key] = this._mixer.clipAction(clip);
|
|
524
1809
|
});
|
|
525
|
-
this.playAnimation({ key:
|
|
1810
|
+
this.playAnimation({ key: loadedAnimations[0].key });
|
|
526
1811
|
}
|
|
527
1812
|
update(delta) {
|
|
528
1813
|
if (!this._mixer || !this._currentAction) return;
|
|
@@ -546,14 +1831,13 @@ var AnimationDelegate = class {
|
|
|
546
1831
|
if (!this._mixer) return;
|
|
547
1832
|
const { key, pauseAtPercentage = 0, pauseAtEnd = false, fadeToKey, fadeDuration = 0.5 } = opts;
|
|
548
1833
|
if (key === this._currentKey) return;
|
|
1834
|
+
const action = this._actions[key];
|
|
1835
|
+
if (!action) return;
|
|
549
1836
|
this._queuedKey = fadeToKey || null;
|
|
550
1837
|
this._fadeDuration = fadeDuration;
|
|
551
1838
|
this._pauseAtPercentage = pauseAtEnd ? 100 : pauseAtPercentage;
|
|
552
1839
|
this._isPaused = false;
|
|
553
1840
|
const prev = this._currentAction;
|
|
554
|
-
if (prev) prev.stop();
|
|
555
|
-
const action = this._actions[key];
|
|
556
|
-
if (!action) return;
|
|
557
1841
|
if (this._pauseAtPercentage > 0) {
|
|
558
1842
|
action.setLoop(LoopOnce, Infinity);
|
|
559
1843
|
action.clampWhenFinished = true;
|
|
@@ -561,10 +1845,10 @@ var AnimationDelegate = class {
|
|
|
561
1845
|
action.setLoop(LoopRepeat, Infinity);
|
|
562
1846
|
action.clampWhenFinished = false;
|
|
563
1847
|
}
|
|
1848
|
+
action.reset().play();
|
|
564
1849
|
if (prev) {
|
|
565
1850
|
prev.crossFadeTo(action, fadeDuration, false);
|
|
566
1851
|
}
|
|
567
|
-
action.reset().play();
|
|
568
1852
|
this._currentAction = action;
|
|
569
1853
|
this._currentKey = key;
|
|
570
1854
|
}
|
|
@@ -593,22 +1877,66 @@ var AnimationDelegate = class {
|
|
|
593
1877
|
}
|
|
594
1878
|
};
|
|
595
1879
|
|
|
596
|
-
// src/lib/graphics/
|
|
597
|
-
|
|
1880
|
+
// src/lib/graphics/material.ts
|
|
1881
|
+
import { Color as Color3, Vector2 as Vector22, Vector3 as Vector36 } from "three";
|
|
1882
|
+
import {
|
|
1883
|
+
MeshPhongMaterial,
|
|
1884
|
+
MeshStandardMaterial,
|
|
1885
|
+
RepeatWrapping as RepeatWrapping2,
|
|
1886
|
+
ShaderMaterial as ShaderMaterial2
|
|
1887
|
+
} from "three";
|
|
1888
|
+
import {
|
|
1889
|
+
MeshBasicNodeMaterial,
|
|
1890
|
+
MeshStandardNodeMaterial
|
|
1891
|
+
} from "three/webgpu";
|
|
1892
|
+
|
|
1893
|
+
// src/lib/core/utility/strings.ts
|
|
1894
|
+
function sortedStringify(obj) {
|
|
1895
|
+
const sortedObj = Object.keys(obj).sort().reduce((acc, key) => {
|
|
1896
|
+
acc[key] = obj[key];
|
|
1897
|
+
return acc;
|
|
1898
|
+
}, {});
|
|
1899
|
+
return JSON.stringify(sortedObj);
|
|
1900
|
+
}
|
|
1901
|
+
function shortHash(objString) {
|
|
1902
|
+
let hash = 0;
|
|
1903
|
+
for (let i = 0; i < objString.length; i++) {
|
|
1904
|
+
hash = Math.imul(31, hash) + objString.charCodeAt(i) | 0;
|
|
1905
|
+
}
|
|
1906
|
+
return hash.toString(36);
|
|
1907
|
+
}
|
|
1908
|
+
|
|
1909
|
+
// src/lib/graphics/material.ts
|
|
1910
|
+
import {
|
|
1911
|
+
uniform,
|
|
1912
|
+
uv,
|
|
1913
|
+
time,
|
|
1914
|
+
vec3,
|
|
1915
|
+
vec4,
|
|
1916
|
+
float,
|
|
1917
|
+
Fn
|
|
1918
|
+
} from "three/tsl";
|
|
1919
|
+
function isTSLShader(shader) {
|
|
1920
|
+
return "colorNode" in shader;
|
|
1921
|
+
}
|
|
1922
|
+
function isGLSLShader(shader) {
|
|
1923
|
+
return "fragment" in shader && "vertex" in shader;
|
|
1924
|
+
}
|
|
598
1925
|
|
|
599
1926
|
// src/lib/entities/actor.ts
|
|
600
1927
|
var actorDefaults = {
|
|
601
|
-
|
|
1928
|
+
...commonDefaults,
|
|
602
1929
|
collision: {
|
|
603
1930
|
static: false,
|
|
604
|
-
size: new
|
|
605
|
-
position: new
|
|
1931
|
+
size: new Vector37(0.5, 0.5, 0.5),
|
|
1932
|
+
position: new Vector37(0, 0, 0)
|
|
606
1933
|
},
|
|
607
1934
|
material: {
|
|
608
|
-
shader:
|
|
1935
|
+
shader: standardShader
|
|
609
1936
|
},
|
|
610
1937
|
animations: [],
|
|
611
|
-
models: []
|
|
1938
|
+
models: [],
|
|
1939
|
+
collisionShape: "capsule"
|
|
612
1940
|
};
|
|
613
1941
|
var ACTOR_TYPE = /* @__PURE__ */ Symbol("Actor");
|
|
614
1942
|
var ZylemActor = class extends GameEntity {
|
|
@@ -624,21 +1952,26 @@ var ZylemActor = class extends GameEntity {
|
|
|
624
1952
|
this.prependUpdate(this.actorUpdate.bind(this));
|
|
625
1953
|
this.controlledRotation = true;
|
|
626
1954
|
}
|
|
627
|
-
|
|
1955
|
+
/**
|
|
1956
|
+
* Initiates model and animation loading in background (deferred).
|
|
1957
|
+
* Call returns immediately; assets will be ready on subsequent updates.
|
|
1958
|
+
*/
|
|
1959
|
+
load() {
|
|
628
1960
|
this._modelFileNames = this.options.models || [];
|
|
629
|
-
|
|
630
|
-
if (this._object) {
|
|
631
|
-
this._animationDelegate = new AnimationDelegate(this._object);
|
|
632
|
-
await this._animationDelegate.loadAnimations(this.options.animations || []);
|
|
633
|
-
}
|
|
1961
|
+
this.loadModelsDeferred();
|
|
634
1962
|
}
|
|
635
|
-
|
|
1963
|
+
/**
|
|
1964
|
+
* Returns current data synchronously.
|
|
1965
|
+
* May return null values if loading is still in progress.
|
|
1966
|
+
*/
|
|
1967
|
+
data() {
|
|
636
1968
|
return {
|
|
637
1969
|
animations: this._animationDelegate?.animations,
|
|
638
|
-
objectModel: this._object
|
|
1970
|
+
objectModel: this._object,
|
|
1971
|
+
collisionShape: this.options.collisionShape
|
|
639
1972
|
};
|
|
640
1973
|
}
|
|
641
|
-
|
|
1974
|
+
actorUpdate(params) {
|
|
642
1975
|
this._animationDelegate?.update(params.delta);
|
|
643
1976
|
}
|
|
644
1977
|
/**
|
|
@@ -669,26 +2002,79 @@ var ZylemActor = class extends GameEntity {
|
|
|
669
2002
|
}
|
|
670
2003
|
this._modelFileNames = [];
|
|
671
2004
|
}
|
|
672
|
-
|
|
2005
|
+
/**
|
|
2006
|
+
* Deferred loading - starts async load and updates entity when complete.
|
|
2007
|
+
* Called by synchronous load() method.
|
|
2008
|
+
*/
|
|
2009
|
+
loadModelsDeferred() {
|
|
673
2010
|
if (this._modelFileNames.length === 0) return;
|
|
2011
|
+
this.dispatch("entity:model:loading", {
|
|
2012
|
+
entityId: this.uuid,
|
|
2013
|
+
files: this._modelFileNames
|
|
2014
|
+
});
|
|
674
2015
|
const promises = this._modelFileNames.map((file) => this._assetLoader.loadFile(file));
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
this.
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
this.
|
|
685
|
-
this.
|
|
686
|
-
|
|
687
|
-
|
|
2016
|
+
Promise.all(promises).then((results) => {
|
|
2017
|
+
if (results[0]?.object) {
|
|
2018
|
+
this._object = results[0].object;
|
|
2019
|
+
}
|
|
2020
|
+
let meshCount = 0;
|
|
2021
|
+
if (this._object) {
|
|
2022
|
+
this._object.traverse((child) => {
|
|
2023
|
+
if (child.isMesh) meshCount++;
|
|
2024
|
+
});
|
|
2025
|
+
this.group = new Group2();
|
|
2026
|
+
this.group.attach(this._object);
|
|
2027
|
+
this.group.scale.set(
|
|
2028
|
+
this.options.scale?.x || 1,
|
|
2029
|
+
this.options.scale?.y || 1,
|
|
2030
|
+
this.options.scale?.z || 1
|
|
2031
|
+
);
|
|
2032
|
+
this.applyMaterialOverrides();
|
|
2033
|
+
this._animationDelegate = new AnimationDelegate(this._object);
|
|
2034
|
+
this._animationDelegate.loadAnimations(this.options.animations || []).then(() => {
|
|
2035
|
+
this.dispatch("entity:animation:loaded", {
|
|
2036
|
+
entityId: this.uuid,
|
|
2037
|
+
animationCount: this.options.animations?.length || 0
|
|
2038
|
+
});
|
|
2039
|
+
});
|
|
2040
|
+
}
|
|
2041
|
+
this.dispatch("entity:model:loaded", {
|
|
2042
|
+
entityId: this.uuid,
|
|
2043
|
+
success: !!this._object,
|
|
2044
|
+
meshCount
|
|
2045
|
+
});
|
|
2046
|
+
});
|
|
688
2047
|
}
|
|
689
2048
|
playAnimation(animationOptions) {
|
|
690
2049
|
this._animationDelegate?.playAnimation(animationOptions);
|
|
691
2050
|
}
|
|
2051
|
+
/**
|
|
2052
|
+
* Apply material overrides from options to all meshes in the loaded model.
|
|
2053
|
+
* Only applies if material options are explicitly specified (not just defaults).
|
|
2054
|
+
*/
|
|
2055
|
+
applyMaterialOverrides() {
|
|
2056
|
+
const materialOptions = this.options.material;
|
|
2057
|
+
if (!materialOptions || !materialOptions.color && !materialOptions.path) {
|
|
2058
|
+
return;
|
|
2059
|
+
}
|
|
2060
|
+
if (!this._object) return;
|
|
2061
|
+
this._object.traverse((child) => {
|
|
2062
|
+
if (child.isMesh) {
|
|
2063
|
+
const mesh = child;
|
|
2064
|
+
if (materialOptions.color) {
|
|
2065
|
+
const newMaterial = new MeshStandardMaterial2({
|
|
2066
|
+
color: materialOptions.color,
|
|
2067
|
+
emissiveIntensity: 0.5,
|
|
2068
|
+
lightMapIntensity: 0.5,
|
|
2069
|
+
fog: true
|
|
2070
|
+
});
|
|
2071
|
+
mesh.castShadow = true;
|
|
2072
|
+
mesh.receiveShadow = true;
|
|
2073
|
+
mesh.material = newMaterial;
|
|
2074
|
+
}
|
|
2075
|
+
}
|
|
2076
|
+
});
|
|
2077
|
+
}
|
|
692
2078
|
get object() {
|
|
693
2079
|
return this._object;
|
|
694
2080
|
}
|
|
@@ -754,6 +2140,13 @@ var ZylemWorld = class {
|
|
|
754
2140
|
}
|
|
755
2141
|
const collider = this.world.createCollider(entity.colliderDesc, entity.body);
|
|
756
2142
|
entity.collider = collider;
|
|
2143
|
+
entity.colliders = [collider];
|
|
2144
|
+
if (entity.colliderDescs?.length > 1) {
|
|
2145
|
+
for (let i = 1; i < entity.colliderDescs.length; i++) {
|
|
2146
|
+
const additionalCollider = this.world.createCollider(entity.colliderDescs[i], entity.body);
|
|
2147
|
+
entity.colliders.push(additionalCollider);
|
|
2148
|
+
}
|
|
2149
|
+
}
|
|
757
2150
|
if (entity.controlledRotation || entity instanceof ZylemActor) {
|
|
758
2151
|
entity.body.lockRotations(true, true);
|
|
759
2152
|
entity.characterController = this.world.createCharacterController(0.01);
|
|
@@ -772,7 +2165,18 @@ var ZylemWorld = class {
|
|
|
772
2165
|
}
|
|
773
2166
|
}
|
|
774
2167
|
destroyEntity(entity) {
|
|
775
|
-
if (entity.
|
|
2168
|
+
if (entity.characterController) {
|
|
2169
|
+
try {
|
|
2170
|
+
entity.characterController.free();
|
|
2171
|
+
} catch {
|
|
2172
|
+
}
|
|
2173
|
+
entity.characterController = null;
|
|
2174
|
+
}
|
|
2175
|
+
if (entity.colliders?.length) {
|
|
2176
|
+
for (const collider of entity.colliders) {
|
|
2177
|
+
this.world.removeCollider(collider, true);
|
|
2178
|
+
}
|
|
2179
|
+
} else if (entity.collider) {
|
|
776
2180
|
this.world.removeCollider(entity.collider, true);
|
|
777
2181
|
}
|
|
778
2182
|
if (entity.body) {
|
|
@@ -817,6 +2221,9 @@ var ZylemWorld = class {
|
|
|
817
2221
|
continue;
|
|
818
2222
|
}
|
|
819
2223
|
this.world.contactsWith(gameEntity.body.collider(0), (otherCollider) => {
|
|
2224
|
+
if (!otherCollider) {
|
|
2225
|
+
return;
|
|
2226
|
+
}
|
|
820
2227
|
const uuid = otherCollider._parent.userData.uuid;
|
|
821
2228
|
const entity = dictionaryRef.get(uuid);
|
|
822
2229
|
if (!entity) {
|
|
@@ -826,7 +2233,13 @@ var ZylemWorld = class {
|
|
|
826
2233
|
gameEntity._collision(entity, state.globals);
|
|
827
2234
|
}
|
|
828
2235
|
});
|
|
2236
|
+
if (!gameEntity.body || gameEntity.markedForRemoval) {
|
|
2237
|
+
continue;
|
|
2238
|
+
}
|
|
829
2239
|
this.world.intersectionsWith(gameEntity.body.collider(0), (otherCollider) => {
|
|
2240
|
+
if (!otherCollider) {
|
|
2241
|
+
return;
|
|
2242
|
+
}
|
|
830
2243
|
const uuid = otherCollider._parent.userData.uuid;
|
|
831
2244
|
const entity = dictionaryRef.get(uuid);
|
|
832
2245
|
if (!entity) {
|
|
@@ -853,6 +2266,10 @@ var ZylemWorld = class {
|
|
|
853
2266
|
this.collisionMap.clear();
|
|
854
2267
|
this.collisionBehaviorMap.clear();
|
|
855
2268
|
this._removalMap.clear();
|
|
2269
|
+
try {
|
|
2270
|
+
this.world?.free();
|
|
2271
|
+
} catch {
|
|
2272
|
+
}
|
|
856
2273
|
this.world = void 0;
|
|
857
2274
|
} catch {
|
|
858
2275
|
}
|
|
@@ -862,17 +2279,22 @@ var ZylemWorld = class {
|
|
|
862
2279
|
// src/lib/graphics/zylem-scene.ts
|
|
863
2280
|
import {
|
|
864
2281
|
Scene,
|
|
865
|
-
Color as
|
|
2282
|
+
Color as Color4,
|
|
866
2283
|
AmbientLight,
|
|
867
2284
|
DirectionalLight,
|
|
868
|
-
Vector3 as
|
|
869
|
-
|
|
870
|
-
|
|
2285
|
+
Vector3 as Vector38,
|
|
2286
|
+
GridHelper,
|
|
2287
|
+
BoxGeometry,
|
|
2288
|
+
ShaderMaterial as ShaderMaterial3,
|
|
2289
|
+
Mesh as Mesh3,
|
|
2290
|
+
BackSide,
|
|
2291
|
+
Texture as Texture3
|
|
871
2292
|
} from "three";
|
|
2293
|
+
import { MeshBasicNodeMaterial as MeshBasicNodeMaterial2 } from "three/webgpu";
|
|
872
2294
|
|
|
873
2295
|
// src/lib/debug/debug-state.ts
|
|
874
|
-
import { proxy as
|
|
875
|
-
var debugState =
|
|
2296
|
+
import { proxy as proxy3 } from "valtio";
|
|
2297
|
+
var debugState = proxy3({
|
|
876
2298
|
enabled: false,
|
|
877
2299
|
paused: false,
|
|
878
2300
|
tool: "none",
|
|
@@ -901,7 +2323,9 @@ var ZylemScene = class {
|
|
|
901
2323
|
type = "Scene";
|
|
902
2324
|
_setup;
|
|
903
2325
|
scene;
|
|
2326
|
+
/** @deprecated Use cameraManager instead */
|
|
904
2327
|
zylemCamera;
|
|
2328
|
+
cameraManager = null;
|
|
905
2329
|
containerElement = null;
|
|
906
2330
|
update = () => {
|
|
907
2331
|
};
|
|
@@ -909,61 +2333,159 @@ var ZylemScene = class {
|
|
|
909
2333
|
_destroy;
|
|
910
2334
|
name;
|
|
911
2335
|
tag;
|
|
2336
|
+
// Skybox for background shaders (supports both GLSL ShaderMaterial and TSL MeshBasicNodeMaterial)
|
|
2337
|
+
skyboxMaterial = null;
|
|
912
2338
|
constructor(id, camera, state2) {
|
|
913
2339
|
const scene = new Scene();
|
|
914
|
-
const isColor = state2.backgroundColor instanceof
|
|
915
|
-
const backgroundColor = isColor ? state2.backgroundColor : new
|
|
2340
|
+
const isColor = state2.backgroundColor instanceof Color4;
|
|
2341
|
+
const backgroundColor = isColor ? state2.backgroundColor : new Color4(state2.backgroundColor);
|
|
916
2342
|
scene.background = backgroundColor;
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
2343
|
+
console.log("ZylemScene state.backgroundShader:", state2.backgroundShader);
|
|
2344
|
+
if (state2.backgroundShader) {
|
|
2345
|
+
this.setupBackgroundShader(scene, state2.backgroundShader);
|
|
2346
|
+
} else if (state2.backgroundImage) {
|
|
2347
|
+
assetManager.loadTexture(state2.backgroundImage).then((texture) => {
|
|
2348
|
+
scene.background = texture;
|
|
2349
|
+
});
|
|
921
2350
|
}
|
|
922
2351
|
this.scene = scene;
|
|
923
2352
|
this.zylemCamera = camera;
|
|
924
2353
|
this.setupLighting(scene);
|
|
925
|
-
this.setupCamera(scene, camera);
|
|
926
2354
|
if (debugState.enabled) {
|
|
927
2355
|
this.debugScene();
|
|
928
2356
|
}
|
|
929
2357
|
}
|
|
2358
|
+
/**
|
|
2359
|
+
* Create a large inverted box with the shader for skybox effect
|
|
2360
|
+
* Supports both GLSL (ShaderMaterial) and TSL (MeshBasicNodeMaterial) shaders
|
|
2361
|
+
*/
|
|
2362
|
+
setupBackgroundShader(scene, shader) {
|
|
2363
|
+
scene.background = null;
|
|
2364
|
+
if (isTSLShader(shader)) {
|
|
2365
|
+
this.skyboxMaterial = new MeshBasicNodeMaterial2();
|
|
2366
|
+
this.skyboxMaterial.colorNode = shader.colorNode;
|
|
2367
|
+
if (shader.transparent) {
|
|
2368
|
+
this.skyboxMaterial.transparent = true;
|
|
2369
|
+
}
|
|
2370
|
+
this.skyboxMaterial.side = BackSide;
|
|
2371
|
+
this.skyboxMaterial.depthWrite = false;
|
|
2372
|
+
console.log("Skybox created with TSL shader");
|
|
2373
|
+
} else if (isGLSLShader(shader)) {
|
|
2374
|
+
const skyboxVertexShader = `
|
|
2375
|
+
varying vec2 vUv;
|
|
2376
|
+
varying vec3 vWorldPosition;
|
|
2377
|
+
|
|
2378
|
+
void main() {
|
|
2379
|
+
vUv = uv;
|
|
2380
|
+
vec4 worldPosition = modelMatrix * vec4(position, 1.0);
|
|
2381
|
+
vWorldPosition = worldPosition.xyz;
|
|
2382
|
+
vec4 pos = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
|
|
2383
|
+
gl_Position = pos.xyww; // Ensures depth is always 1.0 (farthest)
|
|
2384
|
+
}
|
|
2385
|
+
`;
|
|
2386
|
+
this.skyboxMaterial = new ShaderMaterial3({
|
|
2387
|
+
vertexShader: skyboxVertexShader,
|
|
2388
|
+
fragmentShader: shader.fragment,
|
|
2389
|
+
uniforms: {
|
|
2390
|
+
iTime: { value: 0 }
|
|
2391
|
+
},
|
|
2392
|
+
side: BackSide,
|
|
2393
|
+
// Render on inside of geometry
|
|
2394
|
+
depthWrite: false,
|
|
2395
|
+
// Don't write to depth buffer
|
|
2396
|
+
depthTest: true
|
|
2397
|
+
// But do test depth
|
|
2398
|
+
});
|
|
2399
|
+
console.log("Skybox created with GLSL shader");
|
|
2400
|
+
}
|
|
2401
|
+
const geometry = new BoxGeometry(1, 1, 1);
|
|
2402
|
+
const skybox = new Mesh3(geometry, this.skyboxMaterial);
|
|
2403
|
+
skybox.scale.setScalar(1e5);
|
|
2404
|
+
skybox.frustumCulled = false;
|
|
2405
|
+
scene.add(skybox);
|
|
2406
|
+
}
|
|
930
2407
|
setup() {
|
|
931
2408
|
if (this._setup) {
|
|
932
2409
|
this._setup({ me: this, camera: this.zylemCamera, globals: getGlobals() });
|
|
933
2410
|
}
|
|
934
2411
|
}
|
|
935
2412
|
destroy() {
|
|
936
|
-
if (this.
|
|
2413
|
+
if (this.cameraManager) {
|
|
2414
|
+
this.cameraManager.dispose();
|
|
2415
|
+
this.cameraManager = null;
|
|
2416
|
+
} else if (this.zylemCamera && this.zylemCamera.destroy) {
|
|
937
2417
|
this.zylemCamera.destroy();
|
|
938
2418
|
}
|
|
2419
|
+
if (this.skyboxMaterial) {
|
|
2420
|
+
this.skyboxMaterial.dispose();
|
|
2421
|
+
}
|
|
939
2422
|
if (this.scene) {
|
|
2423
|
+
if (this.scene.background instanceof Texture3) {
|
|
2424
|
+
this.scene.background.dispose();
|
|
2425
|
+
this.scene.background = null;
|
|
2426
|
+
}
|
|
940
2427
|
this.scene.traverse((obj) => {
|
|
941
2428
|
if (obj.geometry) {
|
|
942
2429
|
obj.geometry.dispose?.();
|
|
943
2430
|
}
|
|
944
2431
|
if (obj.material) {
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
2432
|
+
const materials = Array.isArray(obj.material) ? obj.material : [obj.material];
|
|
2433
|
+
for (const mat of materials) {
|
|
2434
|
+
if (mat.map) mat.map.dispose?.();
|
|
2435
|
+
if (mat.normalMap) mat.normalMap.dispose?.();
|
|
2436
|
+
if (mat.aoMap) mat.aoMap.dispose?.();
|
|
2437
|
+
if (mat.emissiveMap) mat.emissiveMap.dispose?.();
|
|
2438
|
+
if (mat.roughnessMap) mat.roughnessMap.dispose?.();
|
|
2439
|
+
if (mat.metalnessMap) mat.metalnessMap.dispose?.();
|
|
2440
|
+
if (mat.envMap) mat.envMap.dispose?.();
|
|
2441
|
+
if (mat.lightMap) mat.lightMap.dispose?.();
|
|
2442
|
+
if (mat.bumpMap) mat.bumpMap.dispose?.();
|
|
2443
|
+
if (mat.displacementMap) mat.displacementMap.dispose?.();
|
|
2444
|
+
if (mat.alphaMap) mat.alphaMap.dispose?.();
|
|
2445
|
+
mat.dispose?.();
|
|
949
2446
|
}
|
|
950
2447
|
}
|
|
2448
|
+
if (obj.isLight && obj.shadow?.map) {
|
|
2449
|
+
obj.shadow.map.dispose();
|
|
2450
|
+
}
|
|
951
2451
|
});
|
|
2452
|
+
this.scene.clear();
|
|
952
2453
|
}
|
|
953
2454
|
}
|
|
954
2455
|
/**
|
|
955
|
-
* Setup camera with the scene
|
|
2456
|
+
* Setup camera with the scene.
|
|
2457
|
+
* Supports both legacy single camera and CameraManager modes.
|
|
956
2458
|
*/
|
|
957
|
-
setupCamera(scene, camera) {
|
|
958
|
-
|
|
959
|
-
|
|
2459
|
+
setupCamera(scene, camera, rendererManager) {
|
|
2460
|
+
this.addCameraToScene(scene, camera);
|
|
2461
|
+
if (rendererManager) {
|
|
2462
|
+
camera.setup(scene, rendererManager);
|
|
960
2463
|
} else {
|
|
961
|
-
|
|
2464
|
+
camera.setupLegacy(scene);
|
|
962
2465
|
}
|
|
963
|
-
camera.setup(scene);
|
|
964
2466
|
}
|
|
965
2467
|
/**
|
|
966
|
-
* Setup
|
|
2468
|
+
* Setup with a CameraManager (multi-camera support).
|
|
2469
|
+
*/
|
|
2470
|
+
async setupCameraManager(scene, cameraManager, rendererManager) {
|
|
2471
|
+
this.cameraManager = cameraManager;
|
|
2472
|
+
for (const camera of cameraManager.activeCameras) {
|
|
2473
|
+
this.addCameraToScene(scene, camera);
|
|
2474
|
+
}
|
|
2475
|
+
await cameraManager.setup(scene, rendererManager);
|
|
2476
|
+
}
|
|
2477
|
+
/**
|
|
2478
|
+
* Add a camera (rig or direct) to the scene graph.
|
|
2479
|
+
*/
|
|
2480
|
+
addCameraToScene(scene, camera) {
|
|
2481
|
+
if (camera.cameraRig) {
|
|
2482
|
+
scene.add(camera.cameraRig);
|
|
2483
|
+
} else {
|
|
2484
|
+
scene.add(camera.camera);
|
|
2485
|
+
}
|
|
2486
|
+
}
|
|
2487
|
+
/**
|
|
2488
|
+
* Setup scene lighting
|
|
967
2489
|
*/
|
|
968
2490
|
setupLighting(scene) {
|
|
969
2491
|
const ambientLight = new AmbientLight(16777215, 2);
|
|
@@ -983,15 +2505,21 @@ var ZylemScene = class {
|
|
|
983
2505
|
scene.add(directionalLight);
|
|
984
2506
|
}
|
|
985
2507
|
/**
|
|
986
|
-
* Update renderer size - delegates to camera
|
|
2508
|
+
* Update renderer size - delegates to camera manager or camera
|
|
987
2509
|
*/
|
|
988
2510
|
updateRenderer(width, height) {
|
|
989
|
-
this.
|
|
2511
|
+
if (this.cameraManager) {
|
|
2512
|
+
for (const camera of this.cameraManager.allCameras) {
|
|
2513
|
+
camera.resize(width, height);
|
|
2514
|
+
}
|
|
2515
|
+
} else {
|
|
2516
|
+
this.zylemCamera.resize(width, height);
|
|
2517
|
+
}
|
|
990
2518
|
}
|
|
991
2519
|
/**
|
|
992
2520
|
* Add object to scene
|
|
993
2521
|
*/
|
|
994
|
-
add(object, position2 = new
|
|
2522
|
+
add(object, position2 = new Vector38(0, 0, 0)) {
|
|
995
2523
|
object.position.set(position2.x, position2.y, position2.z);
|
|
996
2524
|
this.scene.add(object);
|
|
997
2525
|
}
|
|
@@ -1005,6 +2533,22 @@ var ZylemScene = class {
|
|
|
1005
2533
|
this.add(entity.mesh, entity.options.position);
|
|
1006
2534
|
}
|
|
1007
2535
|
}
|
|
2536
|
+
/**
|
|
2537
|
+
* Add an entity's group or mesh to the scene (for late-loaded models).
|
|
2538
|
+
* Uses entity's current body position if physics is active.
|
|
2539
|
+
*/
|
|
2540
|
+
addEntityGroup(entity) {
|
|
2541
|
+
const position2 = entity.body ? new Vector38(
|
|
2542
|
+
entity.body.translation().x,
|
|
2543
|
+
entity.body.translation().y,
|
|
2544
|
+
entity.body.translation().z
|
|
2545
|
+
) : entity.options.position;
|
|
2546
|
+
if (entity.group) {
|
|
2547
|
+
this.add(entity.group, position2);
|
|
2548
|
+
} else if (entity.mesh) {
|
|
2549
|
+
this.add(entity.mesh, position2);
|
|
2550
|
+
}
|
|
2551
|
+
}
|
|
1008
2552
|
/**
|
|
1009
2553
|
* Add debug helpers to scene
|
|
1010
2554
|
*/
|
|
@@ -1014,20 +2558,299 @@ var ZylemScene = class {
|
|
|
1014
2558
|
const gridHelper = new GridHelper(size, divisions);
|
|
1015
2559
|
this.scene.add(gridHelper);
|
|
1016
2560
|
}
|
|
2561
|
+
/**
|
|
2562
|
+
* Update skybox shader uniforms (only applies to GLSL ShaderMaterial)
|
|
2563
|
+
* TSL shaders use the time node which auto-updates
|
|
2564
|
+
*/
|
|
2565
|
+
updateSkybox(delta) {
|
|
2566
|
+
if (this.skyboxMaterial && this.skyboxMaterial instanceof ShaderMaterial3) {
|
|
2567
|
+
if (this.skyboxMaterial.uniforms?.iTime) {
|
|
2568
|
+
this.skyboxMaterial.uniforms.iTime.value += delta;
|
|
2569
|
+
}
|
|
2570
|
+
}
|
|
2571
|
+
}
|
|
2572
|
+
};
|
|
2573
|
+
|
|
2574
|
+
// src/lib/graphics/instance-manager.ts
|
|
2575
|
+
import { InstancedMesh, Matrix4, Vector3 as Vector39, Quaternion as Quaternion3 } from "three";
|
|
2576
|
+
var InstanceManager = class _InstanceManager {
|
|
2577
|
+
batches = /* @__PURE__ */ new Map();
|
|
2578
|
+
entityToBatch = /* @__PURE__ */ new Map();
|
|
2579
|
+
// entity UUID -> batch key
|
|
2580
|
+
scene = null;
|
|
2581
|
+
/** Default initial capacity for new batches */
|
|
2582
|
+
static DEFAULT_CAPACITY = 128;
|
|
2583
|
+
/** Factor to grow batch when full */
|
|
2584
|
+
static GROWTH_FACTOR = 2;
|
|
2585
|
+
/**
|
|
2586
|
+
* Set the scene to add instanced meshes to
|
|
2587
|
+
*/
|
|
2588
|
+
setScene(scene) {
|
|
2589
|
+
this.scene = scene;
|
|
2590
|
+
}
|
|
2591
|
+
/**
|
|
2592
|
+
* Generate a batch key from configuration
|
|
2593
|
+
*/
|
|
2594
|
+
static generateBatchKey(config) {
|
|
2595
|
+
const keyData = {
|
|
2596
|
+
geo: `${config.geometryType}:${sortedStringify(config.dimensions)}`,
|
|
2597
|
+
mat: config.materialPath || "none",
|
|
2598
|
+
shader: config.shaderType || "standard",
|
|
2599
|
+
color: config.colorHex ?? 16777215
|
|
2600
|
+
};
|
|
2601
|
+
return shortHash(sortedStringify(keyData));
|
|
2602
|
+
}
|
|
2603
|
+
/**
|
|
2604
|
+
* Register an entity with the instance manager
|
|
2605
|
+
* @returns The instance index, or -1 if registration failed
|
|
2606
|
+
*/
|
|
2607
|
+
register(entity, geometry, material, batchKey) {
|
|
2608
|
+
let batch = this.batches.get(batchKey);
|
|
2609
|
+
if (!batch) {
|
|
2610
|
+
batch = this.createBatch(batchKey, geometry, material);
|
|
2611
|
+
}
|
|
2612
|
+
let index;
|
|
2613
|
+
if (batch.freeIndices.length > 0) {
|
|
2614
|
+
index = batch.freeIndices.pop();
|
|
2615
|
+
} else {
|
|
2616
|
+
index = batch.entities.length;
|
|
2617
|
+
if (index >= batch.capacity) {
|
|
2618
|
+
this.growBatch(batch);
|
|
2619
|
+
}
|
|
2620
|
+
batch.entities.push(null);
|
|
2621
|
+
}
|
|
2622
|
+
batch.entities[index] = entity;
|
|
2623
|
+
batch.entityMap.set(entity.uuid, index);
|
|
2624
|
+
this.entityToBatch.set(entity.uuid, batchKey);
|
|
2625
|
+
batch.instancedMesh.count = Math.max(batch.instancedMesh.count, index + 1);
|
|
2626
|
+
batch.activeIndices.push(index);
|
|
2627
|
+
batch.dirtyIndices.add(index);
|
|
2628
|
+
return index;
|
|
2629
|
+
}
|
|
2630
|
+
/**
|
|
2631
|
+
* Unregister an entity from the instance manager
|
|
2632
|
+
*/
|
|
2633
|
+
unregister(entity) {
|
|
2634
|
+
const batchKey = this.entityToBatch.get(entity.uuid);
|
|
2635
|
+
if (!batchKey) return;
|
|
2636
|
+
const batch = this.batches.get(batchKey);
|
|
2637
|
+
if (!batch) return;
|
|
2638
|
+
const index = batch.entityMap.get(entity.uuid);
|
|
2639
|
+
if (index === void 0) return;
|
|
2640
|
+
batch.entities[index] = null;
|
|
2641
|
+
batch.entityMap.delete(entity.uuid);
|
|
2642
|
+
this.entityToBatch.delete(entity.uuid);
|
|
2643
|
+
batch.freeIndices.push(index);
|
|
2644
|
+
const idxInActive = batch.activeIndices.indexOf(index);
|
|
2645
|
+
if (idxInActive !== -1) {
|
|
2646
|
+
const last = batch.activeIndices.pop();
|
|
2647
|
+
if (idxInActive < batch.activeIndices.length) {
|
|
2648
|
+
batch.activeIndices[idxInActive] = last;
|
|
2649
|
+
}
|
|
2650
|
+
}
|
|
2651
|
+
batch.dirtyIndices.delete(index);
|
|
2652
|
+
const matrix = new Matrix4();
|
|
2653
|
+
matrix.makeScale(0, 0, 0);
|
|
2654
|
+
batch.instancedMesh.setMatrixAt(index, matrix);
|
|
2655
|
+
batch.instancedMesh.instanceMatrix.needsUpdate = true;
|
|
2656
|
+
}
|
|
2657
|
+
/**
|
|
2658
|
+
* Mark an entity's transform as dirty (needs syncing)
|
|
2659
|
+
*/
|
|
2660
|
+
markDirty(entity) {
|
|
2661
|
+
const batchKey = this.entityToBatch.get(entity.uuid);
|
|
2662
|
+
if (!batchKey) return;
|
|
2663
|
+
const batch = this.batches.get(batchKey);
|
|
2664
|
+
if (!batch) return;
|
|
2665
|
+
const index = batch.entityMap.get(entity.uuid);
|
|
2666
|
+
if (index !== void 0) {
|
|
2667
|
+
batch.dirtyIndices.add(index);
|
|
2668
|
+
}
|
|
2669
|
+
}
|
|
2670
|
+
/**
|
|
2671
|
+
* Update all dirty instance transforms
|
|
2672
|
+
* Call this once per frame
|
|
2673
|
+
*/
|
|
2674
|
+
/**
|
|
2675
|
+
* Update all active instance transforms
|
|
2676
|
+
* Call this once per frame
|
|
2677
|
+
*/
|
|
2678
|
+
update() {
|
|
2679
|
+
const matrix = new Matrix4();
|
|
2680
|
+
const pos = new Vector39();
|
|
2681
|
+
const quat = new Quaternion3();
|
|
2682
|
+
const scale2 = new Vector39(1, 1, 1);
|
|
2683
|
+
for (const batch of this.batches.values()) {
|
|
2684
|
+
if (batch.activeIndices.length === 0) continue;
|
|
2685
|
+
let needsUpdate = false;
|
|
2686
|
+
if (batch.dirtyIndices.size > 0) {
|
|
2687
|
+
for (const index of batch.dirtyIndices) {
|
|
2688
|
+
this.updateInstanceMatrix(batch, index, matrix, pos, quat, scale2);
|
|
2689
|
+
}
|
|
2690
|
+
batch.dirtyIndices.clear();
|
|
2691
|
+
needsUpdate = true;
|
|
2692
|
+
}
|
|
2693
|
+
for (const index of batch.activeIndices) {
|
|
2694
|
+
const entity = batch.entities[index];
|
|
2695
|
+
if (entity && entity.body) {
|
|
2696
|
+
this.updateInstanceMatrix(batch, index, matrix, pos, quat, scale2);
|
|
2697
|
+
needsUpdate = true;
|
|
2698
|
+
}
|
|
2699
|
+
}
|
|
2700
|
+
if (needsUpdate) {
|
|
2701
|
+
batch.instancedMesh.instanceMatrix.needsUpdate = true;
|
|
2702
|
+
}
|
|
2703
|
+
}
|
|
2704
|
+
}
|
|
2705
|
+
updateInstanceMatrix(batch, index, matrix, pos, quat, scale2) {
|
|
2706
|
+
const entity = batch.entities[index];
|
|
2707
|
+
if (!entity) return;
|
|
2708
|
+
if (entity.body) {
|
|
2709
|
+
const translation = entity.body.translation();
|
|
2710
|
+
const rotation2 = entity.body.rotation();
|
|
2711
|
+
pos.set(translation.x, translation.y, translation.z);
|
|
2712
|
+
quat.set(rotation2.x, rotation2.y, rotation2.z, rotation2.w);
|
|
2713
|
+
matrix.compose(pos, quat, scale2);
|
|
2714
|
+
} else if (entity.mesh) {
|
|
2715
|
+
entity.mesh.updateMatrix();
|
|
2716
|
+
matrix.copy(entity.mesh.matrix);
|
|
2717
|
+
} else if (entity.group) {
|
|
2718
|
+
entity.group.updateMatrix();
|
|
2719
|
+
matrix.copy(entity.group.matrix);
|
|
2720
|
+
}
|
|
2721
|
+
batch.instancedMesh.setMatrixAt(index, matrix);
|
|
2722
|
+
}
|
|
2723
|
+
/**
|
|
2724
|
+
* Get batch info for an entity
|
|
2725
|
+
*/
|
|
2726
|
+
getBatchInfo(entity) {
|
|
2727
|
+
const batchKey = this.entityToBatch.get(entity.uuid);
|
|
2728
|
+
if (!batchKey) return null;
|
|
2729
|
+
const batch = this.batches.get(batchKey);
|
|
2730
|
+
if (!batch) return null;
|
|
2731
|
+
const instanceId = batch.entityMap.get(entity.uuid);
|
|
2732
|
+
if (instanceId === void 0) return null;
|
|
2733
|
+
return { batchKey, instanceId };
|
|
2734
|
+
}
|
|
2735
|
+
/**
|
|
2736
|
+
* Get statistics about current batching
|
|
2737
|
+
*/
|
|
2738
|
+
getStats() {
|
|
2739
|
+
let totalInstances = 0;
|
|
2740
|
+
const batches = [];
|
|
2741
|
+
for (const [key, batch] of this.batches) {
|
|
2742
|
+
const count = batch.entityMap.size;
|
|
2743
|
+
totalInstances += count;
|
|
2744
|
+
batches.push({ key, count, capacity: batch.capacity });
|
|
2745
|
+
}
|
|
2746
|
+
return { batchCount: this.batches.size, totalInstances, batches };
|
|
2747
|
+
}
|
|
2748
|
+
/**
|
|
2749
|
+
* Dispose all batches and release resources
|
|
2750
|
+
*/
|
|
2751
|
+
dispose() {
|
|
2752
|
+
for (const batch of this.batches.values()) {
|
|
2753
|
+
if (this.scene) {
|
|
2754
|
+
this.scene.remove(batch.instancedMesh);
|
|
2755
|
+
}
|
|
2756
|
+
batch.instancedMesh.dispose();
|
|
2757
|
+
batch.geometry.dispose();
|
|
2758
|
+
}
|
|
2759
|
+
this.batches.clear();
|
|
2760
|
+
this.entityToBatch.clear();
|
|
2761
|
+
}
|
|
2762
|
+
/**
|
|
2763
|
+
* Create a new batch group
|
|
2764
|
+
*/
|
|
2765
|
+
createBatch(key, geometry, material) {
|
|
2766
|
+
const capacity = _InstanceManager.DEFAULT_CAPACITY;
|
|
2767
|
+
const instancedMesh = new InstancedMesh(geometry, material, capacity);
|
|
2768
|
+
instancedMesh.count = 0;
|
|
2769
|
+
instancedMesh.frustumCulled = false;
|
|
2770
|
+
const hiddenMatrix = new Matrix4().makeScale(0, 0, 0);
|
|
2771
|
+
for (let i = 0; i < capacity; i++) {
|
|
2772
|
+
instancedMesh.setMatrixAt(i, hiddenMatrix);
|
|
2773
|
+
}
|
|
2774
|
+
instancedMesh.instanceMatrix.needsUpdate = true;
|
|
2775
|
+
const batch = {
|
|
2776
|
+
key,
|
|
2777
|
+
instancedMesh,
|
|
2778
|
+
geometry,
|
|
2779
|
+
material,
|
|
2780
|
+
entityMap: /* @__PURE__ */ new Map(),
|
|
2781
|
+
entities: [],
|
|
2782
|
+
freeIndices: [],
|
|
2783
|
+
activeIndices: [],
|
|
2784
|
+
dirtyIndices: /* @__PURE__ */ new Set(),
|
|
2785
|
+
capacity
|
|
2786
|
+
};
|
|
2787
|
+
this.batches.set(key, batch);
|
|
2788
|
+
if (this.scene) {
|
|
2789
|
+
this.scene.add(instancedMesh);
|
|
2790
|
+
}
|
|
2791
|
+
return batch;
|
|
2792
|
+
}
|
|
2793
|
+
/**
|
|
2794
|
+
* Grow a batch's capacity
|
|
2795
|
+
*/
|
|
2796
|
+
growBatch(batch) {
|
|
2797
|
+
const newCapacity = batch.capacity * _InstanceManager.GROWTH_FACTOR;
|
|
2798
|
+
const newInstancedMesh = new InstancedMesh(batch.geometry, batch.material, newCapacity);
|
|
2799
|
+
newInstancedMesh.count = batch.instancedMesh.count;
|
|
2800
|
+
newInstancedMesh.frustumCulled = false;
|
|
2801
|
+
const matrix = new Matrix4();
|
|
2802
|
+
for (let i = 0; i < batch.capacity; i++) {
|
|
2803
|
+
batch.instancedMesh.getMatrixAt(i, matrix);
|
|
2804
|
+
newInstancedMesh.setMatrixAt(i, matrix);
|
|
2805
|
+
}
|
|
2806
|
+
const hiddenMatrix = new Matrix4().makeScale(0, 0, 0);
|
|
2807
|
+
for (let i = batch.capacity; i < newCapacity; i++) {
|
|
2808
|
+
newInstancedMesh.setMatrixAt(i, hiddenMatrix);
|
|
2809
|
+
}
|
|
2810
|
+
newInstancedMesh.instanceMatrix.needsUpdate = true;
|
|
2811
|
+
if (this.scene) {
|
|
2812
|
+
this.scene.remove(batch.instancedMesh);
|
|
2813
|
+
this.scene.add(newInstancedMesh);
|
|
2814
|
+
}
|
|
2815
|
+
batch.instancedMesh.dispose();
|
|
2816
|
+
batch.instancedMesh = newInstancedMesh;
|
|
2817
|
+
batch.capacity = newCapacity;
|
|
2818
|
+
}
|
|
1017
2819
|
};
|
|
1018
2820
|
|
|
1019
2821
|
// src/lib/stage/stage-state.ts
|
|
1020
|
-
import { Color as
|
|
1021
|
-
import { proxy as
|
|
1022
|
-
|
|
1023
|
-
|
|
2822
|
+
import { Color as Color6, Vector3 as Vector311 } from "three";
|
|
2823
|
+
import { proxy as proxy4, subscribe as subscribe2 } from "valtio/vanilla";
|
|
2824
|
+
|
|
2825
|
+
// src/lib/core/utility/vector.ts
|
|
2826
|
+
import { Color as Color5 } from "three";
|
|
2827
|
+
import { Vector3 as Vector310 } from "@dimforge/rapier3d-compat";
|
|
2828
|
+
var ZylemBlueColor = new Color5("#0333EC");
|
|
2829
|
+
var Vec0 = new Vector310(0, 0, 0);
|
|
2830
|
+
var Vec1 = new Vector310(1, 1, 1);
|
|
2831
|
+
|
|
2832
|
+
// src/lib/stage/stage-state.ts
|
|
2833
|
+
var initialStageState = {
|
|
2834
|
+
backgroundColor: ZylemBlueColor,
|
|
2835
|
+
backgroundImage: null,
|
|
2836
|
+
backgroundShader: null,
|
|
2837
|
+
inputs: {
|
|
2838
|
+
p1: ["gamepad-1", "keyboard"],
|
|
2839
|
+
p2: ["gamepad-2", "keyboard"]
|
|
2840
|
+
},
|
|
2841
|
+
gravity: new Vector311(0, 0, 0),
|
|
2842
|
+
variables: {},
|
|
2843
|
+
entities: []
|
|
2844
|
+
};
|
|
2845
|
+
var stageState = proxy4({
|
|
2846
|
+
backgroundColor: new Color6(Color6.NAMES.cornflowerblue),
|
|
1024
2847
|
backgroundImage: null,
|
|
1025
2848
|
inputs: {
|
|
1026
2849
|
p1: ["gamepad-1", "keyboard-1"],
|
|
1027
2850
|
p2: ["gamepad-2", "keyboard-2"]
|
|
1028
2851
|
},
|
|
1029
2852
|
variables: {},
|
|
1030
|
-
gravity: new
|
|
2853
|
+
gravity: new Vector311(0, 0, 0),
|
|
1031
2854
|
entities: []
|
|
1032
2855
|
});
|
|
1033
2856
|
var setStageBackgroundColor = (value) => {
|
|
@@ -1047,13 +2870,6 @@ function clearVariables(target) {
|
|
|
1047
2870
|
variableProxyStore.delete(target);
|
|
1048
2871
|
}
|
|
1049
2872
|
|
|
1050
|
-
// src/lib/core/utility/vector.ts
|
|
1051
|
-
import { Color as Color4 } from "three";
|
|
1052
|
-
import { Vector3 as Vector34 } from "@dimforge/rapier3d-compat";
|
|
1053
|
-
var ZylemBlueColor = new Color4("#0333EC");
|
|
1054
|
-
var Vec0 = new Vector34(0, 0, 0);
|
|
1055
|
-
var Vec1 = new Vector34(1, 1, 1);
|
|
1056
|
-
|
|
1057
2873
|
// src/lib/stage/zylem-stage.ts
|
|
1058
2874
|
import { subscribe as subscribe4 } from "valtio/vanilla";
|
|
1059
2875
|
|
|
@@ -1096,34 +2912,34 @@ import { nanoid as nanoid2 } from "nanoid";
|
|
|
1096
2912
|
|
|
1097
2913
|
// src/lib/stage/stage-debug-delegate.ts
|
|
1098
2914
|
import { Ray } from "@dimforge/rapier3d-compat";
|
|
1099
|
-
import { BufferAttribute, BufferGeometry as
|
|
2915
|
+
import { BufferAttribute, BufferGeometry as BufferGeometry3, LineBasicMaterial as LineBasicMaterial2, LineSegments as LineSegments2, Raycaster, Vector2 as Vector23 } from "three";
|
|
1100
2916
|
|
|
1101
2917
|
// src/lib/stage/debug-entity-cursor.ts
|
|
1102
2918
|
import {
|
|
1103
2919
|
Box3,
|
|
1104
|
-
BoxGeometry,
|
|
1105
|
-
Color as
|
|
2920
|
+
BoxGeometry as BoxGeometry2,
|
|
2921
|
+
Color as Color7,
|
|
1106
2922
|
EdgesGeometry,
|
|
1107
2923
|
Group as Group3,
|
|
1108
2924
|
LineBasicMaterial,
|
|
1109
2925
|
LineSegments,
|
|
1110
|
-
Mesh as
|
|
2926
|
+
Mesh as Mesh4,
|
|
1111
2927
|
MeshBasicMaterial,
|
|
1112
|
-
Vector3 as
|
|
2928
|
+
Vector3 as Vector312
|
|
1113
2929
|
} from "three";
|
|
1114
2930
|
var DebugEntityCursor = class {
|
|
1115
2931
|
scene;
|
|
1116
2932
|
container;
|
|
1117
2933
|
fillMesh;
|
|
1118
2934
|
edgeLines;
|
|
1119
|
-
currentColor = new
|
|
2935
|
+
currentColor = new Color7(65280);
|
|
1120
2936
|
bbox = new Box3();
|
|
1121
|
-
size = new
|
|
1122
|
-
center = new
|
|
2937
|
+
size = new Vector312();
|
|
2938
|
+
center = new Vector312();
|
|
1123
2939
|
constructor(scene) {
|
|
1124
2940
|
this.scene = scene;
|
|
1125
|
-
const initialGeometry = new
|
|
1126
|
-
this.fillMesh = new
|
|
2941
|
+
const initialGeometry = new BoxGeometry2(1, 1, 1);
|
|
2942
|
+
this.fillMesh = new Mesh4(
|
|
1127
2943
|
initialGeometry,
|
|
1128
2944
|
new MeshBasicMaterial({
|
|
1129
2945
|
color: this.currentColor,
|
|
@@ -1164,7 +2980,7 @@ var DebugEntityCursor = class {
|
|
|
1164
2980
|
}
|
|
1165
2981
|
this.bbox.getSize(this.size);
|
|
1166
2982
|
this.bbox.getCenter(this.center);
|
|
1167
|
-
const newGeom = new
|
|
2983
|
+
const newGeom = new BoxGeometry2(
|
|
1168
2984
|
Math.max(this.size.x, 1e-6),
|
|
1169
2985
|
Math.max(this.size.y, 1e-6),
|
|
1170
2986
|
Math.max(this.size.z, 1e-6)
|
|
@@ -1195,7 +3011,7 @@ var DELETE_TOOL_COLOR = 16724787;
|
|
|
1195
3011
|
var StageDebugDelegate = class {
|
|
1196
3012
|
stage;
|
|
1197
3013
|
options;
|
|
1198
|
-
mouseNdc = new
|
|
3014
|
+
mouseNdc = new Vector23(-2, -2);
|
|
1199
3015
|
raycaster = new Raycaster();
|
|
1200
3016
|
isMouseDown = false;
|
|
1201
3017
|
disposeFns = [];
|
|
@@ -1207,20 +3023,39 @@ var StageDebugDelegate = class {
|
|
|
1207
3023
|
maxRayDistance: options?.maxRayDistance ?? 5e3,
|
|
1208
3024
|
addEntityFactory: options?.addEntityFactory ?? null
|
|
1209
3025
|
};
|
|
1210
|
-
if (this.stage.scene) {
|
|
1211
|
-
this.debugLines = new LineSegments2(
|
|
1212
|
-
new BufferGeometry2(),
|
|
1213
|
-
new LineBasicMaterial2({ vertexColors: true })
|
|
1214
|
-
);
|
|
1215
|
-
this.stage.scene.scene.add(this.debugLines);
|
|
1216
|
-
this.debugLines.visible = true;
|
|
1217
|
-
this.debugCursor = new DebugEntityCursor(this.stage.scene.scene);
|
|
1218
|
-
}
|
|
1219
3026
|
this.attachDomListeners();
|
|
1220
3027
|
}
|
|
3028
|
+
initDebugVisuals() {
|
|
3029
|
+
if (this.debugLines || !this.stage.scene) return;
|
|
3030
|
+
this.debugLines = new LineSegments2(
|
|
3031
|
+
new BufferGeometry3(),
|
|
3032
|
+
new LineBasicMaterial2({ vertexColors: true })
|
|
3033
|
+
);
|
|
3034
|
+
this.stage.scene.scene.add(this.debugLines);
|
|
3035
|
+
this.debugLines.visible = true;
|
|
3036
|
+
this.debugCursor = new DebugEntityCursor(this.stage.scene.scene);
|
|
3037
|
+
}
|
|
3038
|
+
disposeDebugVisuals() {
|
|
3039
|
+
if (this.debugLines && this.stage.scene) {
|
|
3040
|
+
this.stage.scene.scene.remove(this.debugLines);
|
|
3041
|
+
this.debugLines.geometry.dispose();
|
|
3042
|
+
this.debugLines.material.dispose();
|
|
3043
|
+
this.debugLines = null;
|
|
3044
|
+
}
|
|
3045
|
+
this.debugCursor?.dispose();
|
|
3046
|
+
this.debugCursor = null;
|
|
3047
|
+
}
|
|
1221
3048
|
update() {
|
|
1222
|
-
if (!debugState.enabled)
|
|
3049
|
+
if (!debugState.enabled) {
|
|
3050
|
+
if (this.debugLines) {
|
|
3051
|
+
this.disposeDebugVisuals();
|
|
3052
|
+
}
|
|
3053
|
+
return;
|
|
3054
|
+
}
|
|
1223
3055
|
if (!this.stage.scene || !this.stage.world || !this.stage.cameraRef) return;
|
|
3056
|
+
if (!this.debugLines) {
|
|
3057
|
+
this.initDebugVisuals();
|
|
3058
|
+
}
|
|
1224
3059
|
const { world, cameraRef } = this.stage;
|
|
1225
3060
|
if (this.debugLines) {
|
|
1226
3061
|
const { vertices, colors } = world.world.debugRender();
|
|
@@ -1278,13 +3113,7 @@ var StageDebugDelegate = class {
|
|
|
1278
3113
|
dispose() {
|
|
1279
3114
|
this.disposeFns.forEach((fn) => fn());
|
|
1280
3115
|
this.disposeFns = [];
|
|
1281
|
-
this.
|
|
1282
|
-
if (this.debugLines && this.stage.scene) {
|
|
1283
|
-
this.stage.scene.scene.remove(this.debugLines);
|
|
1284
|
-
this.debugLines.geometry.dispose();
|
|
1285
|
-
this.debugLines.material.dispose();
|
|
1286
|
-
this.debugLines = null;
|
|
1287
|
-
}
|
|
3116
|
+
this.disposeDebugVisuals();
|
|
1288
3117
|
}
|
|
1289
3118
|
handleActionOnHit(hoveredUuid, origin, direction, toi) {
|
|
1290
3119
|
const tool = getDebugTool();
|
|
@@ -1363,10 +3192,10 @@ var StageCameraDebugDelegate = class {
|
|
|
1363
3192
|
};
|
|
1364
3193
|
|
|
1365
3194
|
// src/lib/stage/stage-camera-delegate.ts
|
|
1366
|
-
import { Vector2 as
|
|
3195
|
+
import { Vector2 as Vector28 } from "three";
|
|
1367
3196
|
|
|
1368
3197
|
// src/lib/camera/zylem-camera.ts
|
|
1369
|
-
import { PerspectiveCamera, Vector3 as
|
|
3198
|
+
import { PerspectiveCamera as PerspectiveCamera2, Vector3 as Vector316, Object3D as Object3D8, OrthographicCamera } from "three";
|
|
1370
3199
|
|
|
1371
3200
|
// src/lib/camera/perspective.ts
|
|
1372
3201
|
var Perspectives = {
|
|
@@ -1378,15 +3207,21 @@ var Perspectives = {
|
|
|
1378
3207
|
};
|
|
1379
3208
|
|
|
1380
3209
|
// src/lib/camera/third-person.ts
|
|
1381
|
-
import { Vector3 as
|
|
3210
|
+
import { Vector3 as Vector314 } from "three";
|
|
1382
3211
|
var ThirdPersonCamera = class {
|
|
1383
3212
|
distance;
|
|
1384
3213
|
screenResolution = null;
|
|
1385
3214
|
renderer = null;
|
|
1386
3215
|
scene = null;
|
|
1387
3216
|
cameraRef = null;
|
|
3217
|
+
/** Padding multiplier when framing multiple targets. Higher = more zoom out. */
|
|
3218
|
+
paddingFactor = 1.5;
|
|
3219
|
+
/** Minimum camera distance when multi-framing (prevents extreme zoom-in). */
|
|
3220
|
+
minDistance = 5;
|
|
3221
|
+
/** Lerp factor for camera position smoothing. */
|
|
3222
|
+
lerpFactor = 0.1;
|
|
1388
3223
|
constructor() {
|
|
1389
|
-
this.distance = new
|
|
3224
|
+
this.distance = new Vector314(0, 5, 8);
|
|
1390
3225
|
}
|
|
1391
3226
|
/**
|
|
1392
3227
|
* Setup the third person camera controller
|
|
@@ -1399,15 +3234,59 @@ var ThirdPersonCamera = class {
|
|
|
1399
3234
|
this.cameraRef = camera;
|
|
1400
3235
|
}
|
|
1401
3236
|
/**
|
|
1402
|
-
* Update the third person camera
|
|
3237
|
+
* Update the third person camera.
|
|
3238
|
+
* Handles 0, 1, and multi-target scenarios.
|
|
1403
3239
|
*/
|
|
1404
3240
|
update(delta) {
|
|
1405
|
-
if (!this.cameraRef
|
|
3241
|
+
if (!this.cameraRef) return;
|
|
3242
|
+
const targets = this.cameraRef.targets;
|
|
3243
|
+
if (targets.length === 0) {
|
|
3244
|
+
this.cameraRef.camera.lookAt(new Vector314(0, 0, 0));
|
|
1406
3245
|
return;
|
|
1407
3246
|
}
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
3247
|
+
if (targets.length === 1) {
|
|
3248
|
+
this.updateSingleTarget(targets[0]);
|
|
3249
|
+
return;
|
|
3250
|
+
}
|
|
3251
|
+
this.updateMultiTarget(targets);
|
|
3252
|
+
}
|
|
3253
|
+
/**
|
|
3254
|
+
* Classic single-target follow: lerp to target position + offset, lookAt target.
|
|
3255
|
+
*/
|
|
3256
|
+
updateSingleTarget(target) {
|
|
3257
|
+
const useTarget = target.group?.position || new Vector314(0, 0, 0);
|
|
3258
|
+
const desiredCameraPosition = useTarget.clone().add(this.distance);
|
|
3259
|
+
this.cameraRef.camera.position.lerp(desiredCameraPosition, this.lerpFactor);
|
|
3260
|
+
this.cameraRef.camera.lookAt(useTarget);
|
|
3261
|
+
}
|
|
3262
|
+
/**
|
|
3263
|
+
* Multi-target framing: compute centroid, measure spread, zoom out to fit all.
|
|
3264
|
+
*/
|
|
3265
|
+
updateMultiTarget(targets) {
|
|
3266
|
+
const centroid = new Vector314();
|
|
3267
|
+
for (const t of targets) {
|
|
3268
|
+
centroid.add(t.group.position);
|
|
3269
|
+
}
|
|
3270
|
+
centroid.divideScalar(targets.length);
|
|
3271
|
+
let maxDistFromCentroid = 0;
|
|
3272
|
+
for (const t of targets) {
|
|
3273
|
+
const dist = centroid.distanceTo(t.group.position);
|
|
3274
|
+
if (dist > maxDistFromCentroid) {
|
|
3275
|
+
maxDistFromCentroid = dist;
|
|
3276
|
+
}
|
|
3277
|
+
}
|
|
3278
|
+
const dynamicDistance = Math.max(maxDistFromCentroid * this.paddingFactor, this.minDistance);
|
|
3279
|
+
const offsetDirection = this.distance.clone().normalize();
|
|
3280
|
+
const desiredCameraPosition = centroid.clone().add(
|
|
3281
|
+
offsetDirection.multiplyScalar(dynamicDistance)
|
|
3282
|
+
);
|
|
3283
|
+
const baseLen = this.distance.length();
|
|
3284
|
+
if (baseLen > 0) {
|
|
3285
|
+
const heightRatio = this.distance.y / baseLen;
|
|
3286
|
+
desiredCameraPosition.y = centroid.y + dynamicDistance * heightRatio;
|
|
3287
|
+
}
|
|
3288
|
+
this.cameraRef.camera.position.lerp(desiredCameraPosition, this.lerpFactor);
|
|
3289
|
+
this.cameraRef.camera.lookAt(centroid);
|
|
1411
3290
|
}
|
|
1412
3291
|
/**
|
|
1413
3292
|
* Handle resize events
|
|
@@ -1418,7 +3297,7 @@ var ThirdPersonCamera = class {
|
|
|
1418
3297
|
}
|
|
1419
3298
|
}
|
|
1420
3299
|
/**
|
|
1421
|
-
* Set the distance from the target
|
|
3300
|
+
* Set the distance offset from the target
|
|
1422
3301
|
*/
|
|
1423
3302
|
setDistance(distance) {
|
|
1424
3303
|
this.distance = distance;
|
|
@@ -1461,102 +3340,17 @@ var Fixed2DCamera = class {
|
|
|
1461
3340
|
}
|
|
1462
3341
|
};
|
|
1463
3342
|
|
|
1464
|
-
// src/lib/camera/zylem-camera.ts
|
|
1465
|
-
import { EffectComposer } from "three/examples/jsm/postprocessing/EffectComposer.js";
|
|
1466
|
-
|
|
1467
|
-
// src/lib/graphics/render-pass.ts
|
|
1468
|
-
import * as THREE from "three";
|
|
1469
|
-
|
|
1470
|
-
// src/lib/graphics/shaders/vertex/standard.glsl
|
|
1471
|
-
var standard_default2 = "varying vec2 vUv;\n\nvoid main() {\n vUv = uv;\n gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );\n}";
|
|
1472
|
-
|
|
1473
|
-
// src/lib/graphics/render-pass.ts
|
|
1474
|
-
import { WebGLRenderTarget } from "three";
|
|
1475
|
-
import { Pass, FullScreenQuad } from "three/addons/postprocessing/Pass.js";
|
|
1476
|
-
var RenderPass = class extends Pass {
|
|
1477
|
-
fsQuad;
|
|
1478
|
-
resolution;
|
|
1479
|
-
scene;
|
|
1480
|
-
camera;
|
|
1481
|
-
rgbRenderTarget;
|
|
1482
|
-
normalRenderTarget;
|
|
1483
|
-
normalMaterial;
|
|
1484
|
-
constructor(resolution, scene, camera) {
|
|
1485
|
-
super();
|
|
1486
|
-
this.resolution = resolution;
|
|
1487
|
-
this.fsQuad = new FullScreenQuad(this.material());
|
|
1488
|
-
this.scene = scene;
|
|
1489
|
-
this.camera = camera;
|
|
1490
|
-
this.rgbRenderTarget = new WebGLRenderTarget(resolution.x * 4, resolution.y * 4);
|
|
1491
|
-
this.normalRenderTarget = new WebGLRenderTarget(resolution.x * 4, resolution.y * 4);
|
|
1492
|
-
this.normalMaterial = new THREE.MeshNormalMaterial();
|
|
1493
|
-
}
|
|
1494
|
-
render(renderer, writeBuffer) {
|
|
1495
|
-
renderer.setRenderTarget(this.rgbRenderTarget);
|
|
1496
|
-
renderer.render(this.scene, this.camera);
|
|
1497
|
-
const overrideMaterial_old = this.scene.overrideMaterial;
|
|
1498
|
-
renderer.setRenderTarget(this.normalRenderTarget);
|
|
1499
|
-
this.scene.overrideMaterial = this.normalMaterial;
|
|
1500
|
-
renderer.render(this.scene, this.camera);
|
|
1501
|
-
this.scene.overrideMaterial = overrideMaterial_old;
|
|
1502
|
-
const uniforms = this.fsQuad.material.uniforms;
|
|
1503
|
-
uniforms.tDiffuse.value = this.rgbRenderTarget.texture;
|
|
1504
|
-
uniforms.tDepth.value = this.rgbRenderTarget.depthTexture;
|
|
1505
|
-
uniforms.tNormal.value = this.normalRenderTarget.texture;
|
|
1506
|
-
uniforms.iTime.value += 0.01;
|
|
1507
|
-
if (this.renderToScreen) {
|
|
1508
|
-
renderer.setRenderTarget(null);
|
|
1509
|
-
} else {
|
|
1510
|
-
renderer.setRenderTarget(writeBuffer);
|
|
1511
|
-
}
|
|
1512
|
-
this.fsQuad.render(renderer);
|
|
1513
|
-
}
|
|
1514
|
-
material() {
|
|
1515
|
-
return new THREE.ShaderMaterial({
|
|
1516
|
-
uniforms: {
|
|
1517
|
-
iTime: { value: 0 },
|
|
1518
|
-
tDiffuse: { value: null },
|
|
1519
|
-
tDepth: { value: null },
|
|
1520
|
-
tNormal: { value: null },
|
|
1521
|
-
resolution: {
|
|
1522
|
-
value: new THREE.Vector4(
|
|
1523
|
-
this.resolution.x,
|
|
1524
|
-
this.resolution.y,
|
|
1525
|
-
1 / this.resolution.x,
|
|
1526
|
-
1 / this.resolution.y
|
|
1527
|
-
)
|
|
1528
|
-
}
|
|
1529
|
-
},
|
|
1530
|
-
vertexShader: standard_default2,
|
|
1531
|
-
fragmentShader: standard_default
|
|
1532
|
-
});
|
|
1533
|
-
}
|
|
1534
|
-
dispose() {
|
|
1535
|
-
try {
|
|
1536
|
-
this.fsQuad?.dispose?.();
|
|
1537
|
-
} catch {
|
|
1538
|
-
}
|
|
1539
|
-
try {
|
|
1540
|
-
this.rgbRenderTarget?.dispose?.();
|
|
1541
|
-
this.normalRenderTarget?.dispose?.();
|
|
1542
|
-
} catch {
|
|
1543
|
-
}
|
|
1544
|
-
try {
|
|
1545
|
-
this.normalMaterial?.dispose?.();
|
|
1546
|
-
} catch {
|
|
1547
|
-
}
|
|
1548
|
-
}
|
|
1549
|
-
};
|
|
1550
|
-
|
|
1551
3343
|
// src/lib/camera/camera-debug-delegate.ts
|
|
1552
|
-
import { Vector3 as
|
|
3344
|
+
import { Vector3 as Vector315 } from "three";
|
|
1553
3345
|
import { OrbitControls } from "three/addons/controls/OrbitControls.js";
|
|
1554
3346
|
var CameraOrbitController = class {
|
|
1555
3347
|
camera;
|
|
1556
3348
|
domElement;
|
|
3349
|
+
cameraRig = null;
|
|
3350
|
+
sceneRef = null;
|
|
1557
3351
|
orbitControls = null;
|
|
1558
3352
|
orbitTarget = null;
|
|
1559
|
-
orbitTargetWorldPos = new
|
|
3353
|
+
orbitTargetWorldPos = new Vector315();
|
|
1560
3354
|
debugDelegate = null;
|
|
1561
3355
|
debugUnsubscribe = null;
|
|
1562
3356
|
debugStateSnapshot = { enabled: false, selected: [] };
|
|
@@ -1564,16 +3358,37 @@ var CameraOrbitController = class {
|
|
|
1564
3358
|
savedCameraPosition = null;
|
|
1565
3359
|
savedCameraQuaternion = null;
|
|
1566
3360
|
savedCameraZoom = null;
|
|
1567
|
-
|
|
3361
|
+
savedCameraLocalPosition = null;
|
|
3362
|
+
// Saved debug camera state for restoration when re-entering debug mode
|
|
3363
|
+
savedDebugCameraPosition = null;
|
|
3364
|
+
savedDebugCameraQuaternion = null;
|
|
3365
|
+
savedDebugCameraZoom = null;
|
|
3366
|
+
savedDebugOrbitTarget = null;
|
|
3367
|
+
/** Whether user-configured orbital controls are enabled (independent of debug) */
|
|
3368
|
+
_userOrbitEnabled = false;
|
|
3369
|
+
constructor(camera, domElement, cameraRig) {
|
|
1568
3370
|
this.camera = camera;
|
|
1569
3371
|
this.domElement = domElement;
|
|
3372
|
+
this.cameraRig = cameraRig ?? null;
|
|
3373
|
+
}
|
|
3374
|
+
/**
|
|
3375
|
+
* Set the scene reference for adding/removing camera when detaching from rig.
|
|
3376
|
+
*/
|
|
3377
|
+
setScene(scene) {
|
|
3378
|
+
this.sceneRef = scene;
|
|
1570
3379
|
}
|
|
1571
3380
|
/**
|
|
1572
|
-
* Check if debug mode is currently active (orbit controls enabled).
|
|
3381
|
+
* Check if debug mode is currently active (orbit controls enabled via debug system).
|
|
1573
3382
|
*/
|
|
1574
3383
|
get isActive() {
|
|
1575
3384
|
return this.debugStateSnapshot.enabled;
|
|
1576
3385
|
}
|
|
3386
|
+
/**
|
|
3387
|
+
* Check if any orbit controls are currently active (debug or user).
|
|
3388
|
+
*/
|
|
3389
|
+
get isOrbitActive() {
|
|
3390
|
+
return this.debugStateSnapshot.enabled || this._userOrbitEnabled;
|
|
3391
|
+
}
|
|
1577
3392
|
/**
|
|
1578
3393
|
* Update orbit controls each frame.
|
|
1579
3394
|
* Should be called from the camera's update loop.
|
|
@@ -1585,6 +3400,26 @@ var CameraOrbitController = class {
|
|
|
1585
3400
|
}
|
|
1586
3401
|
this.orbitControls?.update();
|
|
1587
3402
|
}
|
|
3403
|
+
/**
|
|
3404
|
+
* Enable user-configured orbital controls (not debug mode).
|
|
3405
|
+
* These orbit controls persist until explicitly disabled.
|
|
3406
|
+
*/
|
|
3407
|
+
enableUserOrbitControls() {
|
|
3408
|
+
this._userOrbitEnabled = true;
|
|
3409
|
+
if (!this.orbitControls) {
|
|
3410
|
+
this.enableOrbitControls();
|
|
3411
|
+
}
|
|
3412
|
+
}
|
|
3413
|
+
/**
|
|
3414
|
+
* Disable user-configured orbital controls.
|
|
3415
|
+
* Will not disable orbit controls if debug mode is active.
|
|
3416
|
+
*/
|
|
3417
|
+
disableUserOrbitControls() {
|
|
3418
|
+
this._userOrbitEnabled = false;
|
|
3419
|
+
if (!this.debugStateSnapshot.enabled) {
|
|
3420
|
+
this.disableOrbitControls();
|
|
3421
|
+
}
|
|
3422
|
+
}
|
|
1588
3423
|
/**
|
|
1589
3424
|
* Attach a delegate to react to debug state changes.
|
|
1590
3425
|
*/
|
|
@@ -1626,11 +3461,17 @@ var CameraOrbitController = class {
|
|
|
1626
3461
|
};
|
|
1627
3462
|
if (state2.enabled && !wasEnabled) {
|
|
1628
3463
|
this.saveCameraState();
|
|
3464
|
+
this.detachCameraFromRig();
|
|
1629
3465
|
this.enableOrbitControls();
|
|
3466
|
+
this.restoreDebugCameraState();
|
|
1630
3467
|
this.updateOrbitTargetFromSelection(state2.selected);
|
|
1631
3468
|
} else if (!state2.enabled && wasEnabled) {
|
|
3469
|
+
this.saveDebugCameraState();
|
|
1632
3470
|
this.orbitTarget = null;
|
|
1633
|
-
this.
|
|
3471
|
+
if (!this._userOrbitEnabled) {
|
|
3472
|
+
this.disableOrbitControls();
|
|
3473
|
+
}
|
|
3474
|
+
this.reattachCameraToRig();
|
|
1634
3475
|
this.restoreCameraState();
|
|
1635
3476
|
} else if (state2.enabled) {
|
|
1636
3477
|
this.updateOrbitTargetFromSelection(state2.selected);
|
|
@@ -1716,76 +3557,488 @@ var CameraOrbitController = class {
|
|
|
1716
3557
|
this.savedCameraZoom = null;
|
|
1717
3558
|
}
|
|
1718
3559
|
}
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
3560
|
+
/**
|
|
3561
|
+
* Save debug camera state when exiting debug mode.
|
|
3562
|
+
*/
|
|
3563
|
+
saveDebugCameraState() {
|
|
3564
|
+
this.savedDebugCameraPosition = this.camera.position.clone();
|
|
3565
|
+
this.savedDebugCameraQuaternion = this.camera.quaternion.clone();
|
|
3566
|
+
if ("zoom" in this.camera) {
|
|
3567
|
+
this.savedDebugCameraZoom = this.camera.zoom;
|
|
3568
|
+
}
|
|
3569
|
+
if (this.orbitControls) {
|
|
3570
|
+
this.savedDebugOrbitTarget = this.orbitControls.target.clone();
|
|
3571
|
+
}
|
|
3572
|
+
}
|
|
3573
|
+
/**
|
|
3574
|
+
* Restore debug camera state when re-entering debug mode.
|
|
3575
|
+
*/
|
|
3576
|
+
restoreDebugCameraState() {
|
|
3577
|
+
if (this.savedDebugCameraPosition) {
|
|
3578
|
+
this.camera.position.copy(this.savedDebugCameraPosition);
|
|
3579
|
+
}
|
|
3580
|
+
if (this.savedDebugCameraQuaternion) {
|
|
3581
|
+
this.camera.quaternion.copy(this.savedDebugCameraQuaternion);
|
|
3582
|
+
}
|
|
3583
|
+
if (this.savedDebugCameraZoom !== null && "zoom" in this.camera) {
|
|
3584
|
+
this.camera.zoom = this.savedDebugCameraZoom;
|
|
3585
|
+
this.camera.updateProjectionMatrix?.();
|
|
3586
|
+
}
|
|
3587
|
+
if (this.savedDebugOrbitTarget && this.orbitControls) {
|
|
3588
|
+
this.orbitControls.target.copy(this.savedDebugOrbitTarget);
|
|
3589
|
+
}
|
|
3590
|
+
}
|
|
3591
|
+
/**
|
|
3592
|
+
* Detach camera from its rig to allow free orbit movement in debug mode.
|
|
3593
|
+
* Preserves the camera's world position.
|
|
3594
|
+
*/
|
|
3595
|
+
detachCameraFromRig() {
|
|
3596
|
+
if (!this.cameraRig || this.camera.parent !== this.cameraRig) {
|
|
3597
|
+
return;
|
|
3598
|
+
}
|
|
3599
|
+
this.savedCameraLocalPosition = this.camera.position.clone();
|
|
3600
|
+
const worldPos = new Vector315();
|
|
3601
|
+
this.camera.getWorldPosition(worldPos);
|
|
3602
|
+
this.cameraRig.remove(this.camera);
|
|
3603
|
+
if (this.sceneRef) {
|
|
3604
|
+
this.sceneRef.add(this.camera);
|
|
3605
|
+
}
|
|
3606
|
+
this.camera.position.copy(worldPos);
|
|
3607
|
+
}
|
|
3608
|
+
/**
|
|
3609
|
+
* Reattach camera to its rig when exiting debug mode.
|
|
3610
|
+
* Restores the camera's local position relative to the rig.
|
|
3611
|
+
*/
|
|
3612
|
+
reattachCameraToRig() {
|
|
3613
|
+
if (!this.cameraRig || this.camera.parent === this.cameraRig) {
|
|
3614
|
+
return;
|
|
3615
|
+
}
|
|
3616
|
+
if (this.sceneRef && this.camera.parent === this.sceneRef) {
|
|
3617
|
+
this.sceneRef.remove(this.camera);
|
|
3618
|
+
}
|
|
3619
|
+
this.cameraRig.add(this.camera);
|
|
3620
|
+
if (this.savedCameraLocalPosition) {
|
|
3621
|
+
this.camera.position.copy(this.savedCameraLocalPosition);
|
|
3622
|
+
this.savedCameraLocalPosition = null;
|
|
3623
|
+
}
|
|
3624
|
+
}
|
|
3625
|
+
};
|
|
3626
|
+
|
|
3627
|
+
// src/lib/camera/renderer-manager.ts
|
|
3628
|
+
import { Vector2 as Vector25, WebGLRenderer as WebGLRenderer2 } from "three";
|
|
3629
|
+
import { WebGPURenderer } from "three/webgpu";
|
|
3630
|
+
import { EffectComposer } from "three/examples/jsm/postprocessing/EffectComposer.js";
|
|
3631
|
+
|
|
3632
|
+
// src/lib/graphics/render-pass.ts
|
|
3633
|
+
import * as THREE from "three";
|
|
3634
|
+
import { WebGLRenderTarget } from "three";
|
|
3635
|
+
import { Pass, FullScreenQuad } from "three/addons/postprocessing/Pass.js";
|
|
3636
|
+
var RenderPass = class extends Pass {
|
|
3637
|
+
fsQuad;
|
|
3638
|
+
resolution;
|
|
3639
|
+
scene;
|
|
3640
|
+
camera;
|
|
3641
|
+
rgbRenderTarget;
|
|
3642
|
+
normalRenderTarget;
|
|
3643
|
+
normalMaterial;
|
|
3644
|
+
constructor(resolution, scene, camera) {
|
|
3645
|
+
super();
|
|
3646
|
+
this.resolution = resolution;
|
|
3647
|
+
this.fsQuad = new FullScreenQuad(this.material());
|
|
3648
|
+
this.scene = scene;
|
|
3649
|
+
this.camera = camera;
|
|
3650
|
+
this.rgbRenderTarget = new WebGLRenderTarget(resolution.x * 4, resolution.y * 4);
|
|
3651
|
+
this.normalRenderTarget = new WebGLRenderTarget(resolution.x * 4, resolution.y * 4);
|
|
3652
|
+
this.normalMaterial = new THREE.MeshNormalMaterial();
|
|
3653
|
+
}
|
|
3654
|
+
render(renderer, writeBuffer) {
|
|
3655
|
+
renderer.setRenderTarget(this.rgbRenderTarget);
|
|
3656
|
+
renderer.render(this.scene, this.camera);
|
|
3657
|
+
const overrideMaterial_old = this.scene.overrideMaterial;
|
|
3658
|
+
renderer.setRenderTarget(this.normalRenderTarget);
|
|
3659
|
+
this.scene.overrideMaterial = this.normalMaterial;
|
|
3660
|
+
renderer.render(this.scene, this.camera);
|
|
3661
|
+
this.scene.overrideMaterial = overrideMaterial_old;
|
|
3662
|
+
const uniforms = this.fsQuad.material.uniforms;
|
|
3663
|
+
uniforms.tDiffuse.value = this.rgbRenderTarget.texture;
|
|
3664
|
+
uniforms.tDepth.value = this.rgbRenderTarget.depthTexture;
|
|
3665
|
+
uniforms.tNormal.value = this.normalRenderTarget.texture;
|
|
3666
|
+
uniforms.iTime.value += 0.01;
|
|
3667
|
+
if (this.renderToScreen) {
|
|
3668
|
+
renderer.setRenderTarget(null);
|
|
3669
|
+
} else {
|
|
3670
|
+
renderer.setRenderTarget(writeBuffer);
|
|
3671
|
+
}
|
|
3672
|
+
this.fsQuad.render(renderer);
|
|
3673
|
+
}
|
|
3674
|
+
material() {
|
|
3675
|
+
return new THREE.ShaderMaterial({
|
|
3676
|
+
uniforms: {
|
|
3677
|
+
iTime: { value: 0 },
|
|
3678
|
+
tDiffuse: { value: null },
|
|
3679
|
+
tDepth: { value: null },
|
|
3680
|
+
tNormal: { value: null },
|
|
3681
|
+
resolution: {
|
|
3682
|
+
value: new THREE.Vector4(
|
|
3683
|
+
this.resolution.x,
|
|
3684
|
+
this.resolution.y,
|
|
3685
|
+
1 / this.resolution.x,
|
|
3686
|
+
1 / this.resolution.y
|
|
3687
|
+
)
|
|
3688
|
+
}
|
|
3689
|
+
},
|
|
3690
|
+
vertexShader: standardShader.vertex,
|
|
3691
|
+
fragmentShader: standardShader.fragment
|
|
3692
|
+
});
|
|
3693
|
+
}
|
|
3694
|
+
dispose() {
|
|
3695
|
+
try {
|
|
3696
|
+
this.fsQuad?.dispose?.();
|
|
3697
|
+
} catch {
|
|
3698
|
+
}
|
|
3699
|
+
try {
|
|
3700
|
+
this.rgbRenderTarget?.dispose?.();
|
|
3701
|
+
this.normalRenderTarget?.dispose?.();
|
|
3702
|
+
} catch {
|
|
3703
|
+
}
|
|
3704
|
+
try {
|
|
3705
|
+
this.normalMaterial?.dispose?.();
|
|
3706
|
+
} catch {
|
|
3707
|
+
}
|
|
3708
|
+
}
|
|
3709
|
+
};
|
|
3710
|
+
|
|
3711
|
+
// src/lib/camera/renderer-manager.ts
|
|
3712
|
+
var DEFAULT_VIEWPORT = { x: 0, y: 0, width: 1, height: 1 };
|
|
3713
|
+
async function isWebGPUSupported() {
|
|
3714
|
+
if (!("gpu" in navigator)) return false;
|
|
3715
|
+
try {
|
|
3716
|
+
const adapter = await navigator.gpu.requestAdapter();
|
|
3717
|
+
return adapter !== null;
|
|
3718
|
+
} catch {
|
|
3719
|
+
return false;
|
|
3720
|
+
}
|
|
3721
|
+
}
|
|
3722
|
+
var RendererManager = class {
|
|
1726
3723
|
renderer;
|
|
1727
3724
|
composer;
|
|
3725
|
+
screenResolution;
|
|
3726
|
+
rendererType;
|
|
3727
|
+
_isWebGPU = false;
|
|
3728
|
+
_initialized = false;
|
|
3729
|
+
_sceneRef = null;
|
|
3730
|
+
constructor(screenResolution, rendererType = "webgl") {
|
|
3731
|
+
this.screenResolution = screenResolution || new Vector25(window.innerWidth, window.innerHeight);
|
|
3732
|
+
this.rendererType = rendererType;
|
|
3733
|
+
}
|
|
3734
|
+
/**
|
|
3735
|
+
* Check if the renderer has been initialized
|
|
3736
|
+
*/
|
|
3737
|
+
get initialized() {
|
|
3738
|
+
return this._initialized;
|
|
3739
|
+
}
|
|
3740
|
+
/**
|
|
3741
|
+
* Check if using WebGPU renderer
|
|
3742
|
+
*/
|
|
3743
|
+
get isWebGPU() {
|
|
3744
|
+
return this._isWebGPU;
|
|
3745
|
+
}
|
|
3746
|
+
/**
|
|
3747
|
+
* Initialize the renderer (must be called before rendering).
|
|
3748
|
+
* Async because WebGPU requires async initialization.
|
|
3749
|
+
*/
|
|
3750
|
+
async initRenderer() {
|
|
3751
|
+
if (this._initialized) return;
|
|
3752
|
+
let useWebGPU = false;
|
|
3753
|
+
if (this.rendererType === "webgpu") {
|
|
3754
|
+
useWebGPU = true;
|
|
3755
|
+
} else if (this.rendererType === "auto") {
|
|
3756
|
+
useWebGPU = await isWebGPUSupported();
|
|
3757
|
+
}
|
|
3758
|
+
if (useWebGPU) {
|
|
3759
|
+
try {
|
|
3760
|
+
this.renderer = new WebGPURenderer({ antialias: true });
|
|
3761
|
+
await this.renderer.init();
|
|
3762
|
+
this._isWebGPU = true;
|
|
3763
|
+
console.log("RendererManager: Using WebGPU renderer");
|
|
3764
|
+
} catch (e) {
|
|
3765
|
+
console.warn("RendererManager: WebGPU init failed, falling back to WebGL", e);
|
|
3766
|
+
this.renderer = new WebGLRenderer2({ antialias: false, alpha: true });
|
|
3767
|
+
this._isWebGPU = false;
|
|
3768
|
+
}
|
|
3769
|
+
} else {
|
|
3770
|
+
this.renderer = new WebGLRenderer2({ antialias: false, alpha: true });
|
|
3771
|
+
this._isWebGPU = false;
|
|
3772
|
+
console.log("RendererManager: Using WebGL renderer");
|
|
3773
|
+
}
|
|
3774
|
+
this.renderer.setSize(this.screenResolution.x, this.screenResolution.y);
|
|
3775
|
+
if (this.renderer instanceof WebGLRenderer2) {
|
|
3776
|
+
this.renderer.shadowMap.enabled = true;
|
|
3777
|
+
}
|
|
3778
|
+
if (!this._isWebGPU) {
|
|
3779
|
+
this.composer = new EffectComposer(this.renderer);
|
|
3780
|
+
}
|
|
3781
|
+
this._initialized = true;
|
|
3782
|
+
}
|
|
3783
|
+
/**
|
|
3784
|
+
* Set the current scene reference for rendering.
|
|
3785
|
+
*/
|
|
3786
|
+
setScene(scene) {
|
|
3787
|
+
this._sceneRef = scene;
|
|
3788
|
+
}
|
|
3789
|
+
/**
|
|
3790
|
+
* Setup post-processing render pass for a camera (WebGL only).
|
|
3791
|
+
*/
|
|
3792
|
+
setupRenderPass(scene, camera) {
|
|
3793
|
+
if (this._isWebGPU || !this.composer) return;
|
|
3794
|
+
if (this.composer.passes.length > 0) {
|
|
3795
|
+
this.composer.passes.forEach((p) => {
|
|
3796
|
+
try {
|
|
3797
|
+
p.dispose?.();
|
|
3798
|
+
} catch {
|
|
3799
|
+
}
|
|
3800
|
+
});
|
|
3801
|
+
this.composer.passes.length = 0;
|
|
3802
|
+
}
|
|
3803
|
+
const renderResolution = this.screenResolution.clone().divideScalar(2);
|
|
3804
|
+
renderResolution.x |= 0;
|
|
3805
|
+
renderResolution.y |= 0;
|
|
3806
|
+
const pass = new RenderPass(renderResolution, scene, camera);
|
|
3807
|
+
this.composer.addPass(pass);
|
|
3808
|
+
}
|
|
3809
|
+
/**
|
|
3810
|
+
* Start the render loop. Calls the provided callback each frame.
|
|
3811
|
+
*/
|
|
3812
|
+
startRenderLoop(onFrame) {
|
|
3813
|
+
this.renderer.setAnimationLoop((delta) => {
|
|
3814
|
+
onFrame(delta || 0);
|
|
3815
|
+
});
|
|
3816
|
+
}
|
|
3817
|
+
/**
|
|
3818
|
+
* Stop the render loop.
|
|
3819
|
+
*/
|
|
3820
|
+
stopRenderLoop() {
|
|
3821
|
+
try {
|
|
3822
|
+
this.renderer.setAnimationLoop(null);
|
|
3823
|
+
} catch {
|
|
3824
|
+
}
|
|
3825
|
+
}
|
|
3826
|
+
/**
|
|
3827
|
+
* Render a scene from a single camera's perspective.
|
|
3828
|
+
* Sets the viewport based on the camera's viewport config.
|
|
3829
|
+
*/
|
|
3830
|
+
renderCamera(scene, camera) {
|
|
3831
|
+
const vp = camera.viewport;
|
|
3832
|
+
const w = this.screenResolution.x;
|
|
3833
|
+
const h = this.screenResolution.y;
|
|
3834
|
+
const pixelX = Math.floor(vp.x * w);
|
|
3835
|
+
const pixelY = Math.floor(vp.y * h);
|
|
3836
|
+
const pixelW = Math.floor(vp.width * w);
|
|
3837
|
+
const pixelH = Math.floor(vp.height * h);
|
|
3838
|
+
if (this.renderer instanceof WebGLRenderer2) {
|
|
3839
|
+
this.renderer.setViewport(pixelX, pixelY, pixelW, pixelH);
|
|
3840
|
+
this.renderer.setScissor(pixelX, pixelY, pixelW, pixelH);
|
|
3841
|
+
this.renderer.setScissorTest(true);
|
|
3842
|
+
}
|
|
3843
|
+
if (this._isWebGPU) {
|
|
3844
|
+
this.renderer.render(scene, camera.camera);
|
|
3845
|
+
} else if (this.composer) {
|
|
3846
|
+
this.composer.render(0);
|
|
3847
|
+
}
|
|
3848
|
+
}
|
|
3849
|
+
/**
|
|
3850
|
+
* Render a scene from multiple cameras, each with their own viewport.
|
|
3851
|
+
* Cameras are rendered in order (first = bottom layer, last = top layer).
|
|
3852
|
+
*/
|
|
3853
|
+
renderCameras(scene, cameras) {
|
|
3854
|
+
if (!scene || cameras.length === 0) return;
|
|
3855
|
+
if (this.renderer instanceof WebGLRenderer2) {
|
|
3856
|
+
this.renderer.setScissorTest(false);
|
|
3857
|
+
this.renderer.clear();
|
|
3858
|
+
}
|
|
3859
|
+
for (const cam of cameras) {
|
|
3860
|
+
this.renderCamera(scene, cam);
|
|
3861
|
+
}
|
|
3862
|
+
if (this.renderer instanceof WebGLRenderer2) {
|
|
3863
|
+
this.renderer.setScissorTest(false);
|
|
3864
|
+
}
|
|
3865
|
+
}
|
|
3866
|
+
/**
|
|
3867
|
+
* Simple single-camera render (backwards compatible).
|
|
3868
|
+
* Uses the full viewport for a single camera.
|
|
3869
|
+
*/
|
|
3870
|
+
render(scene, camera) {
|
|
3871
|
+
if (this._isWebGPU) {
|
|
3872
|
+
this.renderer.render(scene, camera);
|
|
3873
|
+
} else if (this.composer) {
|
|
3874
|
+
this.composer.render(0);
|
|
3875
|
+
}
|
|
3876
|
+
}
|
|
3877
|
+
/**
|
|
3878
|
+
* Resize the renderer and update resolution.
|
|
3879
|
+
*/
|
|
3880
|
+
resize(width, height) {
|
|
3881
|
+
this.screenResolution.set(width, height);
|
|
3882
|
+
this.renderer.setSize(width, height, false);
|
|
3883
|
+
if (this.composer) {
|
|
3884
|
+
this.composer.setSize(width, height);
|
|
3885
|
+
}
|
|
3886
|
+
}
|
|
3887
|
+
/**
|
|
3888
|
+
* Update renderer pixel ratio (DPR).
|
|
3889
|
+
*/
|
|
3890
|
+
setPixelRatio(dpr) {
|
|
3891
|
+
const safe = Math.max(1, Number.isFinite(dpr) ? dpr : 1);
|
|
3892
|
+
this.renderer.setPixelRatio(safe);
|
|
3893
|
+
}
|
|
3894
|
+
/**
|
|
3895
|
+
* Get the DOM element for the renderer.
|
|
3896
|
+
*/
|
|
3897
|
+
getDomElement() {
|
|
3898
|
+
return this.renderer.domElement;
|
|
3899
|
+
}
|
|
3900
|
+
/**
|
|
3901
|
+
* Dispose renderer, composer, and related resources.
|
|
3902
|
+
*/
|
|
3903
|
+
dispose() {
|
|
3904
|
+
this.stopRenderLoop();
|
|
3905
|
+
try {
|
|
3906
|
+
this.composer?.passes?.forEach((p) => p.dispose?.());
|
|
3907
|
+
this.composer?.dispose?.();
|
|
3908
|
+
} catch {
|
|
3909
|
+
}
|
|
3910
|
+
try {
|
|
3911
|
+
this.renderer.dispose();
|
|
3912
|
+
} catch {
|
|
3913
|
+
}
|
|
3914
|
+
this._sceneRef = null;
|
|
3915
|
+
this._initialized = false;
|
|
3916
|
+
}
|
|
3917
|
+
};
|
|
3918
|
+
|
|
3919
|
+
// src/lib/camera/zylem-camera.ts
|
|
3920
|
+
var ZylemCamera = class {
|
|
3921
|
+
cameraRig = null;
|
|
3922
|
+
camera;
|
|
3923
|
+
screenResolution;
|
|
1728
3924
|
_perspective;
|
|
1729
|
-
target = null;
|
|
1730
|
-
sceneRef = null;
|
|
1731
3925
|
frustumSize = 10;
|
|
3926
|
+
rendererType;
|
|
3927
|
+
sceneRef = null;
|
|
3928
|
+
/** Name for camera manager lookup */
|
|
3929
|
+
name = "";
|
|
3930
|
+
/**
|
|
3931
|
+
* Viewport in normalized coordinates (0-1).
|
|
3932
|
+
* Default is fullscreen: { x: 0, y: 0, width: 1, height: 1 }
|
|
3933
|
+
*/
|
|
3934
|
+
viewport = { ...DEFAULT_VIEWPORT };
|
|
3935
|
+
/**
|
|
3936
|
+
* Multiple targets for the camera to follow/frame.
|
|
3937
|
+
* Replaces the old single `target` property.
|
|
3938
|
+
*/
|
|
3939
|
+
targets = [];
|
|
3940
|
+
/**
|
|
3941
|
+
* @deprecated Use `targets` array instead. This getter/setter is kept for backward compatibility.
|
|
3942
|
+
*/
|
|
3943
|
+
get target() {
|
|
3944
|
+
return this.targets.length > 0 ? this.targets[0] : null;
|
|
3945
|
+
}
|
|
3946
|
+
set target(entity) {
|
|
3947
|
+
if (entity) {
|
|
3948
|
+
if (this.targets.length === 0) {
|
|
3949
|
+
this.targets.push(entity);
|
|
3950
|
+
} else {
|
|
3951
|
+
this.targets[0] = entity;
|
|
3952
|
+
}
|
|
3953
|
+
} else {
|
|
3954
|
+
this.targets = [];
|
|
3955
|
+
}
|
|
3956
|
+
}
|
|
1732
3957
|
// Perspective controller delegation
|
|
1733
3958
|
perspectiveController = null;
|
|
1734
|
-
//
|
|
3959
|
+
// Orbit controls
|
|
1735
3960
|
orbitController = null;
|
|
1736
|
-
|
|
3961
|
+
_useOrbitalControls = false;
|
|
3962
|
+
/** Reference to the shared renderer manager (set during setup) */
|
|
3963
|
+
_rendererManager = null;
|
|
3964
|
+
constructor(perspective, screenResolution, frustumSize = 10, rendererType = "webgl") {
|
|
1737
3965
|
this._perspective = perspective;
|
|
1738
3966
|
this.screenResolution = screenResolution;
|
|
1739
3967
|
this.frustumSize = frustumSize;
|
|
1740
|
-
this.
|
|
1741
|
-
this.renderer.setSize(screenResolution.x, screenResolution.y);
|
|
1742
|
-
this.renderer.shadowMap.enabled = true;
|
|
1743
|
-
this.composer = new EffectComposer(this.renderer);
|
|
3968
|
+
this.rendererType = rendererType;
|
|
1744
3969
|
const aspectRatio = screenResolution.x / screenResolution.y;
|
|
1745
3970
|
this.camera = this.createCameraForPerspective(aspectRatio);
|
|
1746
3971
|
if (this.needsRig()) {
|
|
1747
|
-
this.cameraRig = new
|
|
3972
|
+
this.cameraRig = new Object3D8();
|
|
1748
3973
|
this.cameraRig.position.set(0, 3, 10);
|
|
1749
3974
|
this.cameraRig.add(this.camera);
|
|
1750
|
-
this.camera.lookAt(new
|
|
3975
|
+
this.camera.lookAt(new Vector316(0, 2, 0));
|
|
1751
3976
|
} else {
|
|
1752
3977
|
this.camera.position.set(0, 0, 10);
|
|
1753
|
-
this.camera.lookAt(new
|
|
3978
|
+
this.camera.lookAt(new Vector316(0, 0, 0));
|
|
1754
3979
|
}
|
|
1755
3980
|
this.initializePerspectiveController();
|
|
1756
|
-
this.orbitController = new CameraOrbitController(this.camera, this.renderer.domElement);
|
|
1757
3981
|
}
|
|
1758
3982
|
/**
|
|
1759
|
-
* Setup the camera with a scene
|
|
3983
|
+
* Setup the camera with a scene and renderer manager.
|
|
3984
|
+
* The renderer manager provides shared rendering infrastructure.
|
|
1760
3985
|
*/
|
|
1761
|
-
async setup(scene) {
|
|
3986
|
+
async setup(scene, rendererManager) {
|
|
1762
3987
|
this.sceneRef = scene;
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
3988
|
+
if (rendererManager) {
|
|
3989
|
+
this._rendererManager = rendererManager;
|
|
3990
|
+
}
|
|
3991
|
+
if (this._rendererManager && !this._rendererManager.initialized) {
|
|
3992
|
+
await this._rendererManager.initRenderer();
|
|
3993
|
+
}
|
|
3994
|
+
if (this.perspectiveController && this._rendererManager) {
|
|
1769
3995
|
this.perspectiveController.setup({
|
|
1770
3996
|
screenResolution: this.screenResolution,
|
|
1771
|
-
renderer: this.renderer,
|
|
3997
|
+
renderer: this._rendererManager.renderer,
|
|
1772
3998
|
scene,
|
|
1773
3999
|
camera: this
|
|
1774
4000
|
});
|
|
1775
4001
|
}
|
|
1776
|
-
this.
|
|
1777
|
-
this.
|
|
1778
|
-
|
|
4002
|
+
if (this._rendererManager) {
|
|
4003
|
+
this.orbitController = new CameraOrbitController(
|
|
4004
|
+
this.camera,
|
|
4005
|
+
this._rendererManager.renderer.domElement,
|
|
4006
|
+
this.cameraRig
|
|
4007
|
+
);
|
|
4008
|
+
this.orbitController.setScene(scene);
|
|
4009
|
+
if (this._useOrbitalControls) {
|
|
4010
|
+
this.orbitController.enableUserOrbitControls();
|
|
4011
|
+
}
|
|
4012
|
+
}
|
|
1779
4013
|
}
|
|
1780
4014
|
/**
|
|
1781
|
-
*
|
|
4015
|
+
* Legacy setup method for backward compatibility.
|
|
4016
|
+
* Creates a temporary RendererManager internally.
|
|
4017
|
+
* @deprecated Use setup(scene, rendererManager) instead.
|
|
4018
|
+
*/
|
|
4019
|
+
async setupLegacy(scene) {
|
|
4020
|
+
if (!this._rendererManager) {
|
|
4021
|
+
this._rendererManager = new RendererManager(this.screenResolution, this.rendererType);
|
|
4022
|
+
await this._rendererManager.initRenderer();
|
|
4023
|
+
this._rendererManager.setupRenderPass(scene, this.camera);
|
|
4024
|
+
this._rendererManager.startRenderLoop((delta) => {
|
|
4025
|
+
this.update(delta);
|
|
4026
|
+
if (this._rendererManager && this.sceneRef) {
|
|
4027
|
+
this._rendererManager.render(this.sceneRef, this.camera);
|
|
4028
|
+
}
|
|
4029
|
+
});
|
|
4030
|
+
}
|
|
4031
|
+
await this.setup(scene, this._rendererManager);
|
|
4032
|
+
}
|
|
4033
|
+
/**
|
|
4034
|
+
* Update camera controllers (called each frame).
|
|
4035
|
+
* Does NOT render -- rendering is handled by RendererManager.
|
|
1782
4036
|
*/
|
|
1783
4037
|
update(delta) {
|
|
1784
4038
|
this.orbitController?.update();
|
|
1785
|
-
if (this.perspectiveController && !this.isDebugModeActive()) {
|
|
4039
|
+
if (this.perspectiveController && !this.isDebugModeActive() && !this._useOrbitalControls) {
|
|
1786
4040
|
this.perspectiveController.update(delta);
|
|
1787
4041
|
}
|
|
1788
|
-
this.composer.render(delta);
|
|
1789
4042
|
}
|
|
1790
4043
|
/**
|
|
1791
4044
|
* Check if debug mode is active (orbit controls taking over camera)
|
|
@@ -1794,27 +4047,59 @@ var ZylemCamera = class {
|
|
|
1794
4047
|
return this.orbitController?.isActive ?? false;
|
|
1795
4048
|
}
|
|
1796
4049
|
/**
|
|
1797
|
-
*
|
|
4050
|
+
* Enable user-configured orbital controls (not debug mode).
|
|
1798
4051
|
*/
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
|
|
4052
|
+
enableOrbitalControls() {
|
|
4053
|
+
this._useOrbitalControls = true;
|
|
4054
|
+
this.orbitController?.enableUserOrbitControls();
|
|
4055
|
+
}
|
|
4056
|
+
/**
|
|
4057
|
+
* Disable user-configured orbital controls.
|
|
4058
|
+
*/
|
|
4059
|
+
disableOrbitalControls() {
|
|
4060
|
+
this._useOrbitalControls = false;
|
|
4061
|
+
this.orbitController?.disableUserOrbitControls();
|
|
4062
|
+
}
|
|
4063
|
+
/**
|
|
4064
|
+
* Whether user orbital controls are enabled.
|
|
4065
|
+
*/
|
|
4066
|
+
get useOrbitalControls() {
|
|
4067
|
+
return this._useOrbitalControls;
|
|
4068
|
+
}
|
|
4069
|
+
/**
|
|
4070
|
+
* Add a target entity for the camera to follow/frame.
|
|
4071
|
+
*/
|
|
4072
|
+
addTarget(entity) {
|
|
4073
|
+
if (!this.targets.includes(entity)) {
|
|
4074
|
+
this.targets.push(entity);
|
|
1807
4075
|
}
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
|
|
4076
|
+
}
|
|
4077
|
+
/**
|
|
4078
|
+
* Remove a target entity.
|
|
4079
|
+
*/
|
|
4080
|
+
removeTarget(entity) {
|
|
4081
|
+
const index = this.targets.indexOf(entity);
|
|
4082
|
+
if (index !== -1) {
|
|
4083
|
+
this.targets.splice(index, 1);
|
|
1812
4084
|
}
|
|
4085
|
+
}
|
|
4086
|
+
/**
|
|
4087
|
+
* Clear all targets.
|
|
4088
|
+
*/
|
|
4089
|
+
clearTargets() {
|
|
4090
|
+
this.targets = [];
|
|
4091
|
+
}
|
|
4092
|
+
/**
|
|
4093
|
+
* Dispose camera resources (not the renderer -- that's managed by RendererManager).
|
|
4094
|
+
*/
|
|
4095
|
+
destroy() {
|
|
1813
4096
|
try {
|
|
1814
|
-
this.
|
|
4097
|
+
this.orbitController?.dispose();
|
|
1815
4098
|
} catch {
|
|
1816
4099
|
}
|
|
1817
4100
|
this.sceneRef = null;
|
|
4101
|
+
this.targets = [];
|
|
4102
|
+
this._rendererManager = null;
|
|
1818
4103
|
}
|
|
1819
4104
|
/**
|
|
1820
4105
|
* Attach a delegate to react to debug state changes.
|
|
@@ -1823,13 +4108,11 @@ var ZylemCamera = class {
|
|
|
1823
4108
|
this.orbitController?.setDebugDelegate(delegate);
|
|
1824
4109
|
}
|
|
1825
4110
|
/**
|
|
1826
|
-
* Resize camera
|
|
4111
|
+
* Resize camera projection.
|
|
1827
4112
|
*/
|
|
1828
4113
|
resize(width, height) {
|
|
1829
4114
|
this.screenResolution.set(width, height);
|
|
1830
|
-
this.
|
|
1831
|
-
this.composer.setSize(width, height);
|
|
1832
|
-
if (this.camera instanceof PerspectiveCamera) {
|
|
4115
|
+
if (this.camera instanceof PerspectiveCamera2) {
|
|
1833
4116
|
this.camera.aspect = width / height;
|
|
1834
4117
|
this.camera.updateProjectionMatrix();
|
|
1835
4118
|
}
|
|
@@ -1838,111 +4121,362 @@ var ZylemCamera = class {
|
|
|
1838
4121
|
}
|
|
1839
4122
|
}
|
|
1840
4123
|
/**
|
|
1841
|
-
*
|
|
4124
|
+
* Set the viewport for this camera (normalized 0-1 coordinates).
|
|
4125
|
+
*/
|
|
4126
|
+
setViewport(x, y, width, height) {
|
|
4127
|
+
this.viewport = { x, y, width, height };
|
|
4128
|
+
}
|
|
4129
|
+
/**
|
|
4130
|
+
* Create camera based on perspective type
|
|
4131
|
+
*/
|
|
4132
|
+
createCameraForPerspective(aspectRatio) {
|
|
4133
|
+
switch (this._perspective) {
|
|
4134
|
+
case Perspectives.ThirdPerson:
|
|
4135
|
+
return this.createThirdPersonCamera(aspectRatio);
|
|
4136
|
+
case Perspectives.FirstPerson:
|
|
4137
|
+
return this.createFirstPersonCamera(aspectRatio);
|
|
4138
|
+
case Perspectives.Isometric:
|
|
4139
|
+
return this.createIsometricCamera(aspectRatio);
|
|
4140
|
+
case Perspectives.Flat2D:
|
|
4141
|
+
return this.createFlat2DCamera(aspectRatio);
|
|
4142
|
+
case Perspectives.Fixed2D:
|
|
4143
|
+
return this.createFixed2DCamera(aspectRatio);
|
|
4144
|
+
default:
|
|
4145
|
+
return this.createThirdPersonCamera(aspectRatio);
|
|
4146
|
+
}
|
|
4147
|
+
}
|
|
4148
|
+
/**
|
|
4149
|
+
* Initialize perspective-specific controller
|
|
4150
|
+
*/
|
|
4151
|
+
initializePerspectiveController() {
|
|
4152
|
+
switch (this._perspective) {
|
|
4153
|
+
case Perspectives.ThirdPerson:
|
|
4154
|
+
this.perspectiveController = new ThirdPersonCamera();
|
|
4155
|
+
break;
|
|
4156
|
+
case Perspectives.Fixed2D:
|
|
4157
|
+
this.perspectiveController = new Fixed2DCamera();
|
|
4158
|
+
break;
|
|
4159
|
+
default:
|
|
4160
|
+
this.perspectiveController = new ThirdPersonCamera();
|
|
4161
|
+
}
|
|
4162
|
+
}
|
|
4163
|
+
createThirdPersonCamera(aspectRatio) {
|
|
4164
|
+
return new PerspectiveCamera2(75, aspectRatio, 0.1, 1e3);
|
|
4165
|
+
}
|
|
4166
|
+
createFirstPersonCamera(aspectRatio) {
|
|
4167
|
+
return new PerspectiveCamera2(75, aspectRatio, 0.1, 1e3);
|
|
4168
|
+
}
|
|
4169
|
+
createIsometricCamera(aspectRatio) {
|
|
4170
|
+
return new OrthographicCamera(
|
|
4171
|
+
this.frustumSize * aspectRatio / -2,
|
|
4172
|
+
this.frustumSize * aspectRatio / 2,
|
|
4173
|
+
this.frustumSize / 2,
|
|
4174
|
+
this.frustumSize / -2,
|
|
4175
|
+
1,
|
|
4176
|
+
1e3
|
|
4177
|
+
);
|
|
4178
|
+
}
|
|
4179
|
+
createFlat2DCamera(aspectRatio) {
|
|
4180
|
+
return new OrthographicCamera(
|
|
4181
|
+
this.frustumSize * aspectRatio / -2,
|
|
4182
|
+
this.frustumSize * aspectRatio / 2,
|
|
4183
|
+
this.frustumSize / 2,
|
|
4184
|
+
this.frustumSize / -2,
|
|
4185
|
+
1,
|
|
4186
|
+
1e3
|
|
4187
|
+
);
|
|
4188
|
+
}
|
|
4189
|
+
createFixed2DCamera(aspectRatio) {
|
|
4190
|
+
return this.createFlat2DCamera(aspectRatio);
|
|
4191
|
+
}
|
|
4192
|
+
// Movement methods
|
|
4193
|
+
moveCamera(position2) {
|
|
4194
|
+
if (this._perspective === Perspectives.Flat2D || this._perspective === Perspectives.Fixed2D) {
|
|
4195
|
+
this.frustumSize = position2.z;
|
|
4196
|
+
}
|
|
4197
|
+
if (this.cameraRig) {
|
|
4198
|
+
this.cameraRig.position.set(position2.x, position2.y, position2.z);
|
|
4199
|
+
} else {
|
|
4200
|
+
this.camera.position.set(position2.x, position2.y, position2.z);
|
|
4201
|
+
}
|
|
4202
|
+
}
|
|
4203
|
+
move(position2) {
|
|
4204
|
+
this.moveCamera(position2);
|
|
4205
|
+
}
|
|
4206
|
+
rotate(pitch, yaw, roll) {
|
|
4207
|
+
if (this.cameraRig) {
|
|
4208
|
+
this.cameraRig.rotateX(pitch);
|
|
4209
|
+
this.cameraRig.rotateY(yaw);
|
|
4210
|
+
this.cameraRig.rotateZ(roll);
|
|
4211
|
+
} else {
|
|
4212
|
+
this.camera.rotateX(pitch);
|
|
4213
|
+
this.camera.rotateY(yaw);
|
|
4214
|
+
this.camera.rotateZ(roll);
|
|
4215
|
+
}
|
|
4216
|
+
}
|
|
4217
|
+
/**
|
|
4218
|
+
* Check if this perspective type needs a camera rig
|
|
4219
|
+
*/
|
|
4220
|
+
needsRig() {
|
|
4221
|
+
return this._perspective === Perspectives.ThirdPerson;
|
|
4222
|
+
}
|
|
4223
|
+
/**
|
|
4224
|
+
* Get the DOM element for the renderer.
|
|
4225
|
+
* @deprecated Access via RendererManager instead.
|
|
4226
|
+
*/
|
|
4227
|
+
getDomElement() {
|
|
4228
|
+
if (this._rendererManager) {
|
|
4229
|
+
return this._rendererManager.getDomElement();
|
|
4230
|
+
}
|
|
4231
|
+
throw new Error("ZylemCamera: No renderer manager available. Call setup() first.");
|
|
4232
|
+
}
|
|
4233
|
+
/**
|
|
4234
|
+
* Get the renderer manager reference.
|
|
4235
|
+
*/
|
|
4236
|
+
getRendererManager() {
|
|
4237
|
+
return this._rendererManager;
|
|
4238
|
+
}
|
|
4239
|
+
/**
|
|
4240
|
+
* Set the renderer manager reference (used by CameraManager during setup).
|
|
4241
|
+
*/
|
|
4242
|
+
setRendererManager(manager) {
|
|
4243
|
+
this._rendererManager = manager;
|
|
4244
|
+
}
|
|
4245
|
+
// ─── Legacy compatibility methods ────────────────────────────────────────
|
|
4246
|
+
/**
|
|
4247
|
+
* @deprecated Renderer is now owned by RendererManager
|
|
4248
|
+
*/
|
|
4249
|
+
get renderer() {
|
|
4250
|
+
return this._rendererManager?.renderer;
|
|
4251
|
+
}
|
|
4252
|
+
/**
|
|
4253
|
+
* @deprecated Composer is now owned by RendererManager
|
|
4254
|
+
*/
|
|
4255
|
+
get composer() {
|
|
4256
|
+
return this._rendererManager?.composer;
|
|
4257
|
+
}
|
|
4258
|
+
/**
|
|
4259
|
+
* @deprecated Use RendererManager.setPixelRatio() instead
|
|
4260
|
+
*/
|
|
4261
|
+
setPixelRatio(dpr) {
|
|
4262
|
+
this._rendererManager?.setPixelRatio(dpr);
|
|
4263
|
+
}
|
|
4264
|
+
};
|
|
4265
|
+
|
|
4266
|
+
// src/lib/camera/camera-manager.ts
|
|
4267
|
+
import { Vector2 as Vector27 } from "three";
|
|
4268
|
+
var CameraManager = class {
|
|
4269
|
+
/** Named camera registry */
|
|
4270
|
+
cameras = /* @__PURE__ */ new Map();
|
|
4271
|
+
/** Currently active cameras, ordered by render layer (first = bottom) */
|
|
4272
|
+
_activeCameras = [];
|
|
4273
|
+
/** Auto-created debug camera with orbit controls */
|
|
4274
|
+
_debugCamera = null;
|
|
4275
|
+
/** Reference to the shared renderer manager */
|
|
4276
|
+
_rendererManager = null;
|
|
4277
|
+
/** Scene reference */
|
|
4278
|
+
_sceneRef = null;
|
|
4279
|
+
/** Counter for auto-generated camera names */
|
|
4280
|
+
_autoNameCounter = 0;
|
|
4281
|
+
constructor() {
|
|
4282
|
+
}
|
|
4283
|
+
/**
|
|
4284
|
+
* Get the list of currently active cameras.
|
|
4285
|
+
*/
|
|
4286
|
+
get activeCameras() {
|
|
4287
|
+
return this._activeCameras;
|
|
4288
|
+
}
|
|
4289
|
+
/**
|
|
4290
|
+
* Get the primary active camera (first in the active list).
|
|
4291
|
+
*/
|
|
4292
|
+
get primaryCamera() {
|
|
4293
|
+
return this._activeCameras.length > 0 ? this._activeCameras[0] : null;
|
|
4294
|
+
}
|
|
4295
|
+
/**
|
|
4296
|
+
* Get the debug camera.
|
|
4297
|
+
*/
|
|
4298
|
+
get debugCamera() {
|
|
4299
|
+
return this._debugCamera;
|
|
4300
|
+
}
|
|
4301
|
+
/**
|
|
4302
|
+
* Get all registered cameras.
|
|
4303
|
+
*/
|
|
4304
|
+
get allCameras() {
|
|
4305
|
+
return Array.from(this.cameras.values());
|
|
4306
|
+
}
|
|
4307
|
+
/**
|
|
4308
|
+
* Add a camera to the manager.
|
|
4309
|
+
* If no name is provided, one is auto-generated.
|
|
4310
|
+
* The first camera added becomes the active camera.
|
|
4311
|
+
*
|
|
4312
|
+
* @param camera The ZylemCamera instance to add
|
|
4313
|
+
* @param name Optional name for lookup
|
|
4314
|
+
* @returns The assigned name
|
|
4315
|
+
*/
|
|
4316
|
+
addCamera(camera, name) {
|
|
4317
|
+
const resolvedName = name || camera.name || `camera_${this._autoNameCounter++}`;
|
|
4318
|
+
camera.name = resolvedName;
|
|
4319
|
+
this.cameras.set(resolvedName, camera);
|
|
4320
|
+
if (this._activeCameras.length === 0) {
|
|
4321
|
+
this._activeCameras.push(camera);
|
|
4322
|
+
}
|
|
4323
|
+
return resolvedName;
|
|
4324
|
+
}
|
|
4325
|
+
/**
|
|
4326
|
+
* Remove a camera by name or reference.
|
|
4327
|
+
* Cannot remove the debug camera via this method.
|
|
4328
|
+
*/
|
|
4329
|
+
removeCamera(nameOrRef) {
|
|
4330
|
+
let name;
|
|
4331
|
+
if (typeof nameOrRef === "string") {
|
|
4332
|
+
name = nameOrRef;
|
|
4333
|
+
} else {
|
|
4334
|
+
for (const [key, cam] of this.cameras.entries()) {
|
|
4335
|
+
if (cam === nameOrRef) {
|
|
4336
|
+
name = key;
|
|
4337
|
+
break;
|
|
4338
|
+
}
|
|
4339
|
+
}
|
|
4340
|
+
}
|
|
4341
|
+
if (!name) return false;
|
|
4342
|
+
const camera = this.cameras.get(name);
|
|
4343
|
+
if (!camera) return false;
|
|
4344
|
+
if (camera === this._debugCamera) {
|
|
4345
|
+
console.warn("CameraManager: Cannot remove the debug camera");
|
|
4346
|
+
return false;
|
|
4347
|
+
}
|
|
4348
|
+
this.cameras.delete(name);
|
|
4349
|
+
const activeIndex = this._activeCameras.indexOf(camera);
|
|
4350
|
+
if (activeIndex !== -1) {
|
|
4351
|
+
this._activeCameras.splice(activeIndex, 1);
|
|
4352
|
+
}
|
|
4353
|
+
return true;
|
|
4354
|
+
}
|
|
4355
|
+
/**
|
|
4356
|
+
* Set a camera as the primary active camera (replaces all active cameras
|
|
4357
|
+
* except additional viewport cameras).
|
|
4358
|
+
*
|
|
4359
|
+
* @param nameOrRef Camera name or reference to activate
|
|
4360
|
+
*/
|
|
4361
|
+
setActiveCamera(nameOrRef) {
|
|
4362
|
+
const camera = this.resolveCamera(nameOrRef);
|
|
4363
|
+
if (!camera) {
|
|
4364
|
+
console.warn(`CameraManager: Camera not found: ${nameOrRef}`);
|
|
4365
|
+
return false;
|
|
4366
|
+
}
|
|
4367
|
+
const pipCameras = this._activeCameras.filter((c) => {
|
|
4368
|
+
return c !== this._activeCameras[0] && c.viewport.width < 1;
|
|
4369
|
+
});
|
|
4370
|
+
this._activeCameras = [camera, ...pipCameras];
|
|
4371
|
+
return true;
|
|
4372
|
+
}
|
|
4373
|
+
/**
|
|
4374
|
+
* Add a camera as an additional active camera (for split-screen or PiP).
|
|
4375
|
+
*/
|
|
4376
|
+
addActiveCamera(nameOrRef) {
|
|
4377
|
+
const camera = this.resolveCamera(nameOrRef);
|
|
4378
|
+
if (!camera) return false;
|
|
4379
|
+
if (!this._activeCameras.includes(camera)) {
|
|
4380
|
+
this._activeCameras.push(camera);
|
|
4381
|
+
}
|
|
4382
|
+
return true;
|
|
4383
|
+
}
|
|
4384
|
+
/**
|
|
4385
|
+
* Remove a camera from the active render list (does not remove from registry).
|
|
4386
|
+
*/
|
|
4387
|
+
deactivateCamera(nameOrRef) {
|
|
4388
|
+
const camera = this.resolveCamera(nameOrRef);
|
|
4389
|
+
if (!camera) return false;
|
|
4390
|
+
const index = this._activeCameras.indexOf(camera);
|
|
4391
|
+
if (index !== -1) {
|
|
4392
|
+
this._activeCameras.splice(index, 1);
|
|
4393
|
+
return true;
|
|
4394
|
+
}
|
|
4395
|
+
return false;
|
|
4396
|
+
}
|
|
4397
|
+
/**
|
|
4398
|
+
* Get a camera by name.
|
|
1842
4399
|
*/
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
this.renderer.setPixelRatio(safe);
|
|
4400
|
+
getCamera(name) {
|
|
4401
|
+
return this.cameras.get(name) ?? null;
|
|
1846
4402
|
}
|
|
1847
4403
|
/**
|
|
1848
|
-
*
|
|
4404
|
+
* Setup all cameras with the given scene and renderer manager.
|
|
4405
|
+
* Also creates the debug camera.
|
|
1849
4406
|
*/
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
return this.createFixed2DCamera(aspectRatio);
|
|
1862
|
-
default:
|
|
1863
|
-
return this.createThirdPersonCamera(aspectRatio);
|
|
4407
|
+
async setup(scene, rendererManager) {
|
|
4408
|
+
this._sceneRef = scene;
|
|
4409
|
+
this._rendererManager = rendererManager;
|
|
4410
|
+
this.createDebugCamera(rendererManager.screenResolution);
|
|
4411
|
+
for (const camera of this.cameras.values()) {
|
|
4412
|
+
camera.setRendererManager(rendererManager);
|
|
4413
|
+
await camera.setup(scene, rendererManager);
|
|
4414
|
+
}
|
|
4415
|
+
if (this._debugCamera) {
|
|
4416
|
+
this._debugCamera.setRendererManager(rendererManager);
|
|
4417
|
+
await this._debugCamera.setup(scene, rendererManager);
|
|
1864
4418
|
}
|
|
1865
4419
|
}
|
|
1866
4420
|
/**
|
|
1867
|
-
*
|
|
4421
|
+
* Update all active cameras' controllers.
|
|
1868
4422
|
*/
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
this.perspectiveController = new ThirdPersonCamera();
|
|
1873
|
-
break;
|
|
1874
|
-
case Perspectives.Fixed2D:
|
|
1875
|
-
this.perspectiveController = new Fixed2DCamera();
|
|
1876
|
-
break;
|
|
1877
|
-
default:
|
|
1878
|
-
this.perspectiveController = new ThirdPersonCamera();
|
|
4423
|
+
update(delta) {
|
|
4424
|
+
for (const camera of this._activeCameras) {
|
|
4425
|
+
camera.update(delta);
|
|
1879
4426
|
}
|
|
1880
4427
|
}
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
|
|
1886
|
-
|
|
1887
|
-
createIsometricCamera(aspectRatio) {
|
|
1888
|
-
return new OrthographicCamera(
|
|
1889
|
-
this.frustumSize * aspectRatio / -2,
|
|
1890
|
-
this.frustumSize * aspectRatio / 2,
|
|
1891
|
-
this.frustumSize / 2,
|
|
1892
|
-
this.frustumSize / -2,
|
|
1893
|
-
1,
|
|
1894
|
-
1e3
|
|
1895
|
-
);
|
|
1896
|
-
}
|
|
1897
|
-
createFlat2DCamera(aspectRatio) {
|
|
1898
|
-
return new OrthographicCamera(
|
|
1899
|
-
this.frustumSize * aspectRatio / -2,
|
|
1900
|
-
this.frustumSize * aspectRatio / 2,
|
|
1901
|
-
this.frustumSize / 2,
|
|
1902
|
-
this.frustumSize / -2,
|
|
1903
|
-
1,
|
|
1904
|
-
1e3
|
|
1905
|
-
);
|
|
1906
|
-
}
|
|
1907
|
-
createFixed2DCamera(aspectRatio) {
|
|
1908
|
-
return this.createFlat2DCamera(aspectRatio);
|
|
4428
|
+
/**
|
|
4429
|
+
* Render all active cameras through the renderer manager.
|
|
4430
|
+
*/
|
|
4431
|
+
render(scene) {
|
|
4432
|
+
if (!this._rendererManager || this._activeCameras.length === 0) return;
|
|
4433
|
+
this._rendererManager.renderCameras(scene, this._activeCameras);
|
|
1909
4434
|
}
|
|
1910
|
-
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
|
|
4435
|
+
/**
|
|
4436
|
+
* Create a default third-person camera if no cameras have been added.
|
|
4437
|
+
*/
|
|
4438
|
+
ensureDefaultCamera() {
|
|
4439
|
+
if (this.cameras.size === 0 || this._activeCameras.length === 0) {
|
|
4440
|
+
const screenRes = this._rendererManager?.screenResolution || new Vector27(window.innerWidth, window.innerHeight);
|
|
4441
|
+
const defaultCam = new ZylemCamera(Perspectives.ThirdPerson, screenRes);
|
|
4442
|
+
this.addCamera(defaultCam, "default");
|
|
4443
|
+
return defaultCam;
|
|
1919
4444
|
}
|
|
4445
|
+
return this._activeCameras[0];
|
|
1920
4446
|
}
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
this.cameraRig.rotateY(yaw);
|
|
1928
|
-
this.cameraRig.rotateZ(roll);
|
|
1929
|
-
} else {
|
|
1930
|
-
this.camera.rotateX(pitch);
|
|
1931
|
-
this.camera.rotateY(yaw);
|
|
1932
|
-
this.camera.rotateZ(roll);
|
|
4447
|
+
/**
|
|
4448
|
+
* Dispose all cameras and cleanup.
|
|
4449
|
+
*/
|
|
4450
|
+
dispose() {
|
|
4451
|
+
for (const camera of this.cameras.values()) {
|
|
4452
|
+
camera.destroy();
|
|
1933
4453
|
}
|
|
4454
|
+
this._debugCamera?.destroy();
|
|
4455
|
+
this.cameras.clear();
|
|
4456
|
+
this._activeCameras = [];
|
|
4457
|
+
this._debugCamera = null;
|
|
4458
|
+
this._rendererManager = null;
|
|
4459
|
+
this._sceneRef = null;
|
|
1934
4460
|
}
|
|
1935
4461
|
/**
|
|
1936
|
-
*
|
|
4462
|
+
* Create the always-available debug camera with orbit controls.
|
|
1937
4463
|
*/
|
|
1938
|
-
|
|
1939
|
-
|
|
4464
|
+
createDebugCamera(screenResolution) {
|
|
4465
|
+
this._debugCamera = new ZylemCamera(Perspectives.ThirdPerson, screenResolution);
|
|
4466
|
+
this._debugCamera.name = "__debug__";
|
|
4467
|
+
this._debugCamera.enableOrbitalControls();
|
|
1940
4468
|
}
|
|
1941
4469
|
/**
|
|
1942
|
-
*
|
|
4470
|
+
* Resolve a camera from a name or reference.
|
|
1943
4471
|
*/
|
|
1944
|
-
|
|
1945
|
-
|
|
4472
|
+
resolveCamera(nameOrRef) {
|
|
4473
|
+
if (typeof nameOrRef === "string") {
|
|
4474
|
+
return this.cameras.get(nameOrRef) ?? null;
|
|
4475
|
+
}
|
|
4476
|
+
for (const cam of this.cameras.values()) {
|
|
4477
|
+
if (cam === nameOrRef) return cam;
|
|
4478
|
+
}
|
|
4479
|
+
return null;
|
|
1946
4480
|
}
|
|
1947
4481
|
};
|
|
1948
4482
|
|
|
@@ -1958,7 +4492,7 @@ var StageCameraDelegate = class {
|
|
|
1958
4492
|
createDefaultCamera() {
|
|
1959
4493
|
const width = window.innerWidth;
|
|
1960
4494
|
const height = window.innerHeight;
|
|
1961
|
-
const screenResolution = new
|
|
4495
|
+
const screenResolution = new Vector28(width, height);
|
|
1962
4496
|
return new ZylemCamera(Perspectives.ThirdPerson, screenResolution);
|
|
1963
4497
|
}
|
|
1964
4498
|
/**
|
|
@@ -1978,6 +4512,32 @@ var StageCameraDelegate = class {
|
|
|
1978
4512
|
}
|
|
1979
4513
|
return this.createDefaultCamera();
|
|
1980
4514
|
}
|
|
4515
|
+
/**
|
|
4516
|
+
* Build a CameraManager from stage options.
|
|
4517
|
+
* Supports single camera (backward compatible) and multiple cameras.
|
|
4518
|
+
*
|
|
4519
|
+
* @param cameraOverride Optional camera override from game-level config
|
|
4520
|
+
* @param cameraWrappers Camera wrappers from stage options (can be single or array)
|
|
4521
|
+
* @returns A CameraManager populated with the resolved cameras
|
|
4522
|
+
*/
|
|
4523
|
+
buildCameraManager(cameraOverride, ...cameraWrappers) {
|
|
4524
|
+
const manager = new CameraManager();
|
|
4525
|
+
if (cameraOverride) {
|
|
4526
|
+
manager.addCamera(cameraOverride, cameraOverride.name || "main");
|
|
4527
|
+
return manager;
|
|
4528
|
+
}
|
|
4529
|
+
const validWrappers = cameraWrappers.filter((w) => w !== void 0);
|
|
4530
|
+
if (validWrappers.length > 0) {
|
|
4531
|
+
for (const wrapper of validWrappers) {
|
|
4532
|
+
const cam = wrapper.cameraRef;
|
|
4533
|
+
manager.addCamera(cam, cam.name || void 0);
|
|
4534
|
+
}
|
|
4535
|
+
} else {
|
|
4536
|
+
const defaultCam = this.createDefaultCamera();
|
|
4537
|
+
manager.addCamera(defaultCam, "default");
|
|
4538
|
+
}
|
|
4539
|
+
return manager;
|
|
4540
|
+
}
|
|
1981
4541
|
};
|
|
1982
4542
|
|
|
1983
4543
|
// src/lib/stage/stage-loading-delegate.ts
|
|
@@ -2057,8 +4617,61 @@ var StageLoadingDelegate = class {
|
|
|
2057
4617
|
}
|
|
2058
4618
|
};
|
|
2059
4619
|
|
|
4620
|
+
// src/lib/stage/stage-entity-model-delegate.ts
|
|
4621
|
+
var StageEntityModelDelegate = class {
|
|
4622
|
+
scene = null;
|
|
4623
|
+
pendingEntities = /* @__PURE__ */ new Map();
|
|
4624
|
+
modelLoadedHandler = null;
|
|
4625
|
+
/**
|
|
4626
|
+
* Initialize the delegate with the scene reference and start listening.
|
|
4627
|
+
*/
|
|
4628
|
+
attach(scene) {
|
|
4629
|
+
this.scene = scene;
|
|
4630
|
+
this.modelLoadedHandler = (payload) => {
|
|
4631
|
+
this.handleModelLoaded(payload.entityId, payload.success);
|
|
4632
|
+
};
|
|
4633
|
+
zylemEventBus.on("entity:model:loaded", this.modelLoadedHandler);
|
|
4634
|
+
}
|
|
4635
|
+
/**
|
|
4636
|
+
* Register an entity for observation.
|
|
4637
|
+
* When its model loads, the group will be added to the scene.
|
|
4638
|
+
*/
|
|
4639
|
+
observe(entity) {
|
|
4640
|
+
this.pendingEntities.set(entity.uuid, entity);
|
|
4641
|
+
}
|
|
4642
|
+
/**
|
|
4643
|
+
* Unregister an entity (e.g., when removed before model loads).
|
|
4644
|
+
*/
|
|
4645
|
+
unobserve(entityId) {
|
|
4646
|
+
this.pendingEntities.delete(entityId);
|
|
4647
|
+
}
|
|
4648
|
+
/**
|
|
4649
|
+
* Handle model loaded event - add group to scene if entity is pending.
|
|
4650
|
+
*/
|
|
4651
|
+
handleModelLoaded(entityId, success) {
|
|
4652
|
+
const entity = this.pendingEntities.get(entityId);
|
|
4653
|
+
if (!entity || !success) {
|
|
4654
|
+
this.pendingEntities.delete(entityId);
|
|
4655
|
+
return;
|
|
4656
|
+
}
|
|
4657
|
+
this.scene?.addEntityGroup(entity);
|
|
4658
|
+
this.pendingEntities.delete(entityId);
|
|
4659
|
+
}
|
|
4660
|
+
/**
|
|
4661
|
+
* Cleanup all subscriptions and pending entities.
|
|
4662
|
+
*/
|
|
4663
|
+
dispose() {
|
|
4664
|
+
if (this.modelLoadedHandler) {
|
|
4665
|
+
zylemEventBus.off("entity:model:loaded", this.modelLoadedHandler);
|
|
4666
|
+
this.modelLoadedHandler = null;
|
|
4667
|
+
}
|
|
4668
|
+
this.pendingEntities.clear();
|
|
4669
|
+
this.scene = null;
|
|
4670
|
+
}
|
|
4671
|
+
};
|
|
4672
|
+
|
|
2060
4673
|
// src/lib/stage/stage-config.ts
|
|
2061
|
-
import { Vector3 as
|
|
4674
|
+
import { Vector3 as Vector317 } from "three";
|
|
2062
4675
|
|
|
2063
4676
|
// src/lib/core/utility/options-parser.ts
|
|
2064
4677
|
function isBaseNode(item) {
|
|
@@ -2088,10 +4701,11 @@ function isEntityInput(item) {
|
|
|
2088
4701
|
|
|
2089
4702
|
// src/lib/stage/stage-config.ts
|
|
2090
4703
|
var StageConfig = class {
|
|
2091
|
-
constructor(inputs, backgroundColor, backgroundImage, gravity, variables) {
|
|
4704
|
+
constructor(inputs, backgroundColor, backgroundImage, backgroundShader, gravity, variables) {
|
|
2092
4705
|
this.inputs = inputs;
|
|
2093
4706
|
this.backgroundColor = backgroundColor;
|
|
2094
4707
|
this.backgroundImage = backgroundImage;
|
|
4708
|
+
this.backgroundShader = backgroundShader;
|
|
2095
4709
|
this.gravity = gravity;
|
|
2096
4710
|
this.variables = variables;
|
|
2097
4711
|
}
|
|
@@ -2104,7 +4718,8 @@ function createDefaultStageConfig() {
|
|
|
2104
4718
|
},
|
|
2105
4719
|
ZylemBlueColor,
|
|
2106
4720
|
null,
|
|
2107
|
-
|
|
4721
|
+
null,
|
|
4722
|
+
new Vector317(0, 0, 0),
|
|
2108
4723
|
{}
|
|
2109
4724
|
);
|
|
2110
4725
|
}
|
|
@@ -2113,10 +4728,10 @@ function parseStageOptions(options = []) {
|
|
|
2113
4728
|
let config = {};
|
|
2114
4729
|
const entities = [];
|
|
2115
4730
|
const asyncEntities = [];
|
|
2116
|
-
|
|
4731
|
+
const cameras = [];
|
|
2117
4732
|
for (const item of options) {
|
|
2118
4733
|
if (isCameraWrapper(item)) {
|
|
2119
|
-
|
|
4734
|
+
cameras.push(item);
|
|
2120
4735
|
} else if (isBaseNode(item)) {
|
|
2121
4736
|
entities.push(item);
|
|
2122
4737
|
} else if (isEntityInput(item) && !isBaseNode(item)) {
|
|
@@ -2129,30 +4744,78 @@ function parseStageOptions(options = []) {
|
|
|
2129
4744
|
config.inputs ?? defaults.inputs,
|
|
2130
4745
|
config.backgroundColor ?? defaults.backgroundColor,
|
|
2131
4746
|
config.backgroundImage ?? defaults.backgroundImage,
|
|
4747
|
+
config.backgroundShader ?? defaults.backgroundShader,
|
|
2132
4748
|
config.gravity ?? defaults.gravity,
|
|
2133
4749
|
config.variables ?? defaults.variables
|
|
2134
4750
|
);
|
|
2135
|
-
|
|
4751
|
+
const camera = cameras.length > 0 ? cameras[0] : void 0;
|
|
4752
|
+
return { config: resolvedConfig, entities, asyncEntities, camera, cameras };
|
|
2136
4753
|
}
|
|
2137
4754
|
|
|
4755
|
+
// src/lib/actions/capabilities/apply-transform.ts
|
|
4756
|
+
function applyTransformChanges(entity, store) {
|
|
4757
|
+
if (!entity.body) return;
|
|
4758
|
+
if (store.dirty.velocity) {
|
|
4759
|
+
entity.body.setLinvel(store.velocity, true);
|
|
4760
|
+
}
|
|
4761
|
+
if (store.dirty.rotation) {
|
|
4762
|
+
entity.body.setRotation(store.rotation, true);
|
|
4763
|
+
}
|
|
4764
|
+
if (store.dirty.angularVelocity) {
|
|
4765
|
+
entity.body.setAngvel(store.angularVelocity, true);
|
|
4766
|
+
}
|
|
4767
|
+
if (store.dirty.position) {
|
|
4768
|
+
const current = entity.body.translation();
|
|
4769
|
+
entity.body.setTranslation(
|
|
4770
|
+
{
|
|
4771
|
+
x: current.x + store.position.x,
|
|
4772
|
+
y: current.y + store.position.y,
|
|
4773
|
+
z: current.z + store.position.z
|
|
4774
|
+
},
|
|
4775
|
+
true
|
|
4776
|
+
);
|
|
4777
|
+
}
|
|
4778
|
+
}
|
|
4779
|
+
|
|
4780
|
+
// src/lib/core/vessel.ts
|
|
4781
|
+
var VESSEL_TYPE = /* @__PURE__ */ Symbol("vessel");
|
|
4782
|
+
var Vessel = class extends BaseNode {
|
|
4783
|
+
static type = VESSEL_TYPE;
|
|
4784
|
+
_setup(_params) {
|
|
4785
|
+
}
|
|
4786
|
+
async _loaded(_params) {
|
|
4787
|
+
}
|
|
4788
|
+
_update(_params) {
|
|
4789
|
+
}
|
|
4790
|
+
_destroy(_params) {
|
|
4791
|
+
}
|
|
4792
|
+
_cleanup(_params) {
|
|
4793
|
+
}
|
|
4794
|
+
create() {
|
|
4795
|
+
return this;
|
|
4796
|
+
}
|
|
4797
|
+
/**
|
|
4798
|
+
* Add one or more child entities to this vessel.
|
|
4799
|
+
* Overrides parent to support multiple arguments.
|
|
4800
|
+
* @returns this for chaining
|
|
4801
|
+
*/
|
|
4802
|
+
add(...nodes) {
|
|
4803
|
+
for (const node of nodes) {
|
|
4804
|
+
super.add(node);
|
|
4805
|
+
}
|
|
4806
|
+
return this;
|
|
4807
|
+
}
|
|
4808
|
+
};
|
|
4809
|
+
|
|
2138
4810
|
// src/lib/stage/zylem-stage.ts
|
|
2139
4811
|
var STAGE_TYPE = "Stage";
|
|
2140
4812
|
var ZylemStage = class extends LifeCycleBase {
|
|
2141
4813
|
type = STAGE_TYPE;
|
|
2142
|
-
state = {
|
|
2143
|
-
backgroundColor: ZylemBlueColor,
|
|
2144
|
-
backgroundImage: null,
|
|
2145
|
-
inputs: {
|
|
2146
|
-
p1: ["gamepad-1", "keyboard"],
|
|
2147
|
-
p2: ["gamepad-2", "keyboard"]
|
|
2148
|
-
},
|
|
2149
|
-
gravity: new Vector311(0, 0, 0),
|
|
2150
|
-
variables: {},
|
|
2151
|
-
entities: []
|
|
2152
|
-
};
|
|
4814
|
+
state = { ...initialStageState };
|
|
2153
4815
|
gravity;
|
|
2154
4816
|
world;
|
|
2155
4817
|
scene;
|
|
4818
|
+
instanceManager = null;
|
|
2156
4819
|
children = [];
|
|
2157
4820
|
_childrenMap = /* @__PURE__ */ new Map();
|
|
2158
4821
|
_removalMap = /* @__PURE__ */ new Map();
|
|
@@ -2164,16 +4827,24 @@ var ZylemStage = class extends LifeCycleBase {
|
|
|
2164
4827
|
ecs = createECS();
|
|
2165
4828
|
testSystem = null;
|
|
2166
4829
|
transformSystem = null;
|
|
4830
|
+
behaviorSystems = [];
|
|
4831
|
+
registeredSystemKeys = /* @__PURE__ */ new Set();
|
|
2167
4832
|
debugDelegate = null;
|
|
2168
4833
|
cameraDebugDelegate = null;
|
|
2169
4834
|
debugStateUnsubscribe = null;
|
|
2170
4835
|
uuid;
|
|
2171
4836
|
wrapperRef = null;
|
|
2172
4837
|
camera;
|
|
4838
|
+
cameras = [];
|
|
2173
4839
|
cameraRef = null;
|
|
4840
|
+
/** Camera manager for multi-camera support */
|
|
4841
|
+
cameraManagerRef = null;
|
|
4842
|
+
/** Shared renderer manager (injected by the game) */
|
|
4843
|
+
rendererManager = null;
|
|
2174
4844
|
// Delegates
|
|
2175
4845
|
cameraDelegate;
|
|
2176
4846
|
loadingDelegate;
|
|
4847
|
+
entityModelDelegate;
|
|
2177
4848
|
/**
|
|
2178
4849
|
* Create a new stage.
|
|
2179
4850
|
* @param options Stage options: partial config, camera, and initial entities or factories
|
|
@@ -2185,8 +4856,10 @@ var ZylemStage = class extends LifeCycleBase {
|
|
|
2185
4856
|
this.uuid = nanoid2();
|
|
2186
4857
|
this.cameraDelegate = new StageCameraDelegate(this);
|
|
2187
4858
|
this.loadingDelegate = new StageLoadingDelegate();
|
|
4859
|
+
this.entityModelDelegate = new StageEntityModelDelegate();
|
|
2188
4860
|
const parsed = parseStageOptions(options);
|
|
2189
4861
|
this.camera = parsed.camera;
|
|
4862
|
+
this.cameras = parsed.cameras;
|
|
2190
4863
|
this.children = parsed.entities;
|
|
2191
4864
|
this.pendingEntities = parsed.asyncEntities;
|
|
2192
4865
|
this.saveState({
|
|
@@ -2194,11 +4867,12 @@ var ZylemStage = class extends LifeCycleBase {
|
|
|
2194
4867
|
inputs: parsed.config.inputs,
|
|
2195
4868
|
backgroundColor: parsed.config.backgroundColor,
|
|
2196
4869
|
backgroundImage: parsed.config.backgroundImage,
|
|
4870
|
+
backgroundShader: parsed.config.backgroundShader,
|
|
2197
4871
|
gravity: parsed.config.gravity,
|
|
2198
4872
|
variables: parsed.config.variables,
|
|
2199
4873
|
entities: []
|
|
2200
4874
|
});
|
|
2201
|
-
this.gravity = parsed.config.gravity ?? new
|
|
4875
|
+
this.gravity = parsed.config.gravity ?? new Vector318(0, 0, 0);
|
|
2202
4876
|
}
|
|
2203
4877
|
handleEntityImmediatelyOrQueue(entity) {
|
|
2204
4878
|
if (this.isLoaded) {
|
|
@@ -2219,7 +4893,7 @@ var ZylemStage = class extends LifeCycleBase {
|
|
|
2219
4893
|
}
|
|
2220
4894
|
setState() {
|
|
2221
4895
|
const { backgroundColor, backgroundImage } = this.state;
|
|
2222
|
-
const color = backgroundColor instanceof
|
|
4896
|
+
const color = backgroundColor instanceof Color9 ? backgroundColor : new Color9(backgroundColor);
|
|
2223
4897
|
setStageBackgroundColor(color);
|
|
2224
4898
|
setStageBackgroundImage(backgroundImage);
|
|
2225
4899
|
setStageVariables(this.state.variables ?? {});
|
|
@@ -2230,14 +4904,39 @@ var ZylemStage = class extends LifeCycleBase {
|
|
|
2230
4904
|
* @param id DOM element id for the renderer container
|
|
2231
4905
|
* @param camera Optional camera override
|
|
2232
4906
|
*/
|
|
2233
|
-
async load(id, camera) {
|
|
4907
|
+
async load(id, camera, rendererManager) {
|
|
2234
4908
|
this.setState();
|
|
2235
|
-
|
|
4909
|
+
if (rendererManager) {
|
|
4910
|
+
this.rendererManager = rendererManager;
|
|
4911
|
+
}
|
|
4912
|
+
if (this.rendererManager) {
|
|
4913
|
+
this.cameraManagerRef = this.cameraDelegate.buildCameraManager(
|
|
4914
|
+
camera,
|
|
4915
|
+
...this.cameras
|
|
4916
|
+
);
|
|
4917
|
+
}
|
|
4918
|
+
const zylemCamera = this.cameraManagerRef?.primaryCamera ?? this.cameraDelegate.resolveCamera(camera, this.camera);
|
|
2236
4919
|
this.cameraRef = zylemCamera;
|
|
2237
4920
|
this.scene = new ZylemScene(id, zylemCamera, this.state);
|
|
2238
|
-
const physicsWorld = await ZylemWorld.loadPhysics(this.gravity ?? new
|
|
4921
|
+
const physicsWorld = await ZylemWorld.loadPhysics(this.gravity ?? new Vector318(0, 0, 0));
|
|
2239
4922
|
this.world = new ZylemWorld(physicsWorld);
|
|
2240
4923
|
this.scene.setup();
|
|
4924
|
+
if (this.cameraManagerRef && this.rendererManager) {
|
|
4925
|
+
await this.scene.setupCameraManager(this.scene.scene, this.cameraManagerRef, this.rendererManager);
|
|
4926
|
+
const primaryCam = this.cameraManagerRef.primaryCamera;
|
|
4927
|
+
if (primaryCam) {
|
|
4928
|
+
this.rendererManager.setupRenderPass(this.scene.scene, primaryCam.camera);
|
|
4929
|
+
}
|
|
4930
|
+
this.rendererManager.startRenderLoop((delta) => {
|
|
4931
|
+
this.cameraManagerRef?.update(delta);
|
|
4932
|
+
this.cameraManagerRef?.render(this.scene.scene);
|
|
4933
|
+
});
|
|
4934
|
+
} else {
|
|
4935
|
+
this.scene.setupCamera(this.scene.scene, zylemCamera);
|
|
4936
|
+
}
|
|
4937
|
+
this.entityModelDelegate.attach(this.scene);
|
|
4938
|
+
this.instanceManager = new InstanceManager();
|
|
4939
|
+
this.instanceManager.setScene(this.scene.scene);
|
|
2241
4940
|
this.loadingDelegate.emitStart();
|
|
2242
4941
|
await this.runEntityLoadGenerator();
|
|
2243
4942
|
this.transformSystem = createTransformSystem(this);
|
|
@@ -2276,11 +4975,11 @@ var ZylemStage = class extends LifeCycleBase {
|
|
|
2276
4975
|
* Runs the entity load generator, yielding to the event loop between loads.
|
|
2277
4976
|
* This allows the browser to process events and update the UI in real-time.
|
|
2278
4977
|
*/
|
|
2279
|
-
|
|
4978
|
+
runEntityLoadGenerator() {
|
|
2280
4979
|
const gen = this.entityLoadGenerator();
|
|
2281
4980
|
for (const progress of gen) {
|
|
2282
4981
|
this.loadingDelegate.emitProgress(`Loaded ${progress.name}`, progress.current, progress.total);
|
|
2283
|
-
|
|
4982
|
+
new Promise((resolve) => setTimeout(resolve, 0));
|
|
2284
4983
|
}
|
|
2285
4984
|
}
|
|
2286
4985
|
_setup(params) {
|
|
@@ -2316,17 +5015,25 @@ var ZylemStage = class extends LifeCycleBase {
|
|
|
2316
5015
|
return;
|
|
2317
5016
|
}
|
|
2318
5017
|
this.world.update(params);
|
|
2319
|
-
|
|
5018
|
+
for (const system of this.behaviorSystems) {
|
|
5019
|
+
system.update(this.ecs, delta);
|
|
5020
|
+
}
|
|
2320
5021
|
this._childrenMap.forEach((child, eid) => {
|
|
2321
5022
|
child.nodeUpdate({
|
|
2322
5023
|
...params,
|
|
2323
5024
|
me: child
|
|
2324
5025
|
});
|
|
5026
|
+
if (child.transformStore) {
|
|
5027
|
+
applyTransformChanges(child, child.transformStore);
|
|
5028
|
+
}
|
|
2325
5029
|
if (child.markedForRemoval) {
|
|
2326
5030
|
this.removeEntityByUuid(child.uuid);
|
|
2327
5031
|
}
|
|
2328
5032
|
});
|
|
5033
|
+
this.transformSystem?.system(this.ecs);
|
|
5034
|
+
this.instanceManager?.update();
|
|
2329
5035
|
this.scene.update({ delta });
|
|
5036
|
+
this.scene.updateSkybox(delta);
|
|
2330
5037
|
}
|
|
2331
5038
|
outOfLoop() {
|
|
2332
5039
|
this.debugUpdate();
|
|
@@ -2339,11 +5046,17 @@ var ZylemStage = class extends LifeCycleBase {
|
|
|
2339
5046
|
}
|
|
2340
5047
|
/** Cleanup owned resources when the stage is destroyed. */
|
|
2341
5048
|
_destroy(params) {
|
|
5049
|
+
for (const system of this.behaviorSystems) {
|
|
5050
|
+
system.destroy?.(this.ecs);
|
|
5051
|
+
}
|
|
5052
|
+
this.behaviorSystems = [];
|
|
5053
|
+
this.registeredSystemKeys.clear();
|
|
2342
5054
|
this._childrenMap.forEach((child) => {
|
|
2343
5055
|
try {
|
|
2344
5056
|
child.nodeDestroy({ me: child, globals: getGlobals() });
|
|
2345
5057
|
} catch {
|
|
2346
5058
|
}
|
|
5059
|
+
clearVariables(child);
|
|
2347
5060
|
});
|
|
2348
5061
|
this._childrenMap.clear();
|
|
2349
5062
|
this._removalMap.clear();
|
|
@@ -2358,10 +5071,17 @@ var ZylemStage = class extends LifeCycleBase {
|
|
|
2358
5071
|
this.debugDelegate = null;
|
|
2359
5072
|
this.cameraRef?.setDebugDelegate(null);
|
|
2360
5073
|
this.cameraDebugDelegate = null;
|
|
5074
|
+
this.entityModelDelegate.dispose();
|
|
5075
|
+
this.instanceManager?.dispose();
|
|
5076
|
+
this.instanceManager = null;
|
|
5077
|
+
if (this.rendererManager) {
|
|
5078
|
+
this.rendererManager.stopRenderLoop();
|
|
5079
|
+
}
|
|
2361
5080
|
this.isLoaded = false;
|
|
2362
5081
|
this.world = null;
|
|
2363
5082
|
this.scene = null;
|
|
2364
5083
|
this.cameraRef = null;
|
|
5084
|
+
this.cameraManagerRef = null;
|
|
2365
5085
|
this.transformSystem?.destroy(this.ecs);
|
|
2366
5086
|
this.transformSystem = null;
|
|
2367
5087
|
resetStageVariables();
|
|
@@ -2375,28 +5095,84 @@ var ZylemStage = class extends LifeCycleBase {
|
|
|
2375
5095
|
if (!this.scene || !this.world) {
|
|
2376
5096
|
return;
|
|
2377
5097
|
}
|
|
5098
|
+
if (child instanceof Vessel) {
|
|
5099
|
+
child.create();
|
|
5100
|
+
for (const childEntity of child.getChildren()) {
|
|
5101
|
+
if (childEntity instanceof BaseNode) {
|
|
5102
|
+
await this.spawnEntity(childEntity);
|
|
5103
|
+
}
|
|
5104
|
+
}
|
|
5105
|
+
const vesselEid = addEntity(this.ecs);
|
|
5106
|
+
child.eid = vesselEid;
|
|
5107
|
+
child.nodeSetup({
|
|
5108
|
+
me: child,
|
|
5109
|
+
globals: getGlobals(),
|
|
5110
|
+
camera: this.scene.zylemCamera
|
|
5111
|
+
});
|
|
5112
|
+
this.addEntityToStage(child);
|
|
5113
|
+
return;
|
|
5114
|
+
}
|
|
2378
5115
|
const entity = child.create();
|
|
2379
5116
|
const eid = addEntity(this.ecs);
|
|
2380
5117
|
entity.eid = eid;
|
|
2381
5118
|
this.scene.addEntity(entity);
|
|
2382
|
-
if (
|
|
2383
|
-
for (
|
|
2384
|
-
|
|
2385
|
-
|
|
2386
|
-
|
|
2387
|
-
|
|
5119
|
+
if (typeof entity.getBehaviorRefs === "function") {
|
|
5120
|
+
for (const ref of entity.getBehaviorRefs()) {
|
|
5121
|
+
const key = ref.descriptor.key;
|
|
5122
|
+
if (!this.registeredSystemKeys.has(key)) {
|
|
5123
|
+
const system = ref.descriptor.systemFactory({ world: this.world, ecs: this.ecs, scene: this.scene });
|
|
5124
|
+
this.behaviorSystems.push(system);
|
|
5125
|
+
this.registeredSystemKeys.add(key);
|
|
2388
5126
|
}
|
|
2389
5127
|
}
|
|
2390
5128
|
}
|
|
2391
5129
|
if (entity.colliderDesc) {
|
|
2392
5130
|
this.world.addEntity(entity);
|
|
2393
5131
|
}
|
|
5132
|
+
for (const childNode of child.getChildren()) {
|
|
5133
|
+
if (childNode instanceof BaseNode) {
|
|
5134
|
+
await this.spawnEntity(childNode);
|
|
5135
|
+
}
|
|
5136
|
+
}
|
|
2394
5137
|
child.nodeSetup({
|
|
2395
5138
|
me: child,
|
|
2396
5139
|
globals: getGlobals(),
|
|
2397
5140
|
camera: this.scene.zylemCamera
|
|
2398
5141
|
});
|
|
5142
|
+
this.tryRegisterInstance(entity);
|
|
2399
5143
|
this.addEntityToStage(entity);
|
|
5144
|
+
this.entityModelDelegate.observe(entity);
|
|
5145
|
+
}
|
|
5146
|
+
/**
|
|
5147
|
+
* Try to register an entity for instanced rendering.
|
|
5148
|
+
* Batching is enabled by default unless explicitly disabled with batched: false.
|
|
5149
|
+
*/
|
|
5150
|
+
tryRegisterInstance(entity) {
|
|
5151
|
+
if (!this.instanceManager) return;
|
|
5152
|
+
const options = entity.options;
|
|
5153
|
+
if (options?.batched !== true) return;
|
|
5154
|
+
if (!entity.mesh?.geometry || !entity.materials?.length) return;
|
|
5155
|
+
const geometry = entity.mesh.geometry;
|
|
5156
|
+
const material = entity.materials[0];
|
|
5157
|
+
const entityType = entity.constructor.type?.description || "unknown";
|
|
5158
|
+
const size = options.size || { x: 1, y: 1, z: 1 };
|
|
5159
|
+
const matOptions = options.material || {};
|
|
5160
|
+
const batchKey = InstanceManager.generateBatchKey({
|
|
5161
|
+
geometryType: entityType,
|
|
5162
|
+
dimensions: { x: size.x, y: size.y, z: size.z },
|
|
5163
|
+
materialPath: matOptions.path || null,
|
|
5164
|
+
shaderType: matOptions.shader ? "custom" : "standard",
|
|
5165
|
+
colorHex: matOptions.color?.getHex?.() || 16777215
|
|
5166
|
+
});
|
|
5167
|
+
const instanceId = this.instanceManager.register(entity, geometry, material, batchKey);
|
|
5168
|
+
if (instanceId >= 0) {
|
|
5169
|
+
entity.batchKey = batchKey;
|
|
5170
|
+
entity.instanceId = instanceId;
|
|
5171
|
+
entity.isInstanced = true;
|
|
5172
|
+
if (entity.mesh) {
|
|
5173
|
+
entity.mesh.visible = false;
|
|
5174
|
+
}
|
|
5175
|
+
}
|
|
2400
5176
|
}
|
|
2401
5177
|
buildEntityState(child) {
|
|
2402
5178
|
if (child instanceof GameEntity) {
|
|
@@ -2446,6 +5222,21 @@ var ZylemStage = class extends LifeCycleBase {
|
|
|
2446
5222
|
onLoading(callback) {
|
|
2447
5223
|
return this.loadingDelegate.onLoading(callback);
|
|
2448
5224
|
}
|
|
5225
|
+
/**
|
|
5226
|
+
* Register an ECS behavior system to run each frame.
|
|
5227
|
+
* @param systemOrFactory A BehaviorSystem instance or factory function
|
|
5228
|
+
* @returns this for chaining
|
|
5229
|
+
*/
|
|
5230
|
+
registerSystem(systemOrFactory) {
|
|
5231
|
+
let system;
|
|
5232
|
+
if (typeof systemOrFactory === "function") {
|
|
5233
|
+
system = systemOrFactory({ world: this.world, ecs: this.ecs, scene: this.scene });
|
|
5234
|
+
} else {
|
|
5235
|
+
system = systemOrFactory;
|
|
5236
|
+
}
|
|
5237
|
+
this.behaviorSystems.push(system);
|
|
5238
|
+
return this;
|
|
5239
|
+
}
|
|
2449
5240
|
/**
|
|
2450
5241
|
* Remove an entity and its resources by its UUID.
|
|
2451
5242
|
* @returns true if removed, false if not found or stage not ready
|
|
@@ -2455,6 +5246,10 @@ var ZylemStage = class extends LifeCycleBase {
|
|
|
2455
5246
|
const mapEntity = this.world.collisionMap.get(uuid);
|
|
2456
5247
|
const entity = mapEntity ?? this._debugMap.get(uuid);
|
|
2457
5248
|
if (!entity) return false;
|
|
5249
|
+
this.entityModelDelegate.unobserve(uuid);
|
|
5250
|
+
if (entity.isInstanced && this.instanceManager) {
|
|
5251
|
+
this.instanceManager.unregister(entity);
|
|
5252
|
+
}
|
|
2458
5253
|
this.world.destroyEntity(entity);
|
|
2459
5254
|
if (entity.group) {
|
|
2460
5255
|
this.scene.scene.remove(entity.group);
|
|
@@ -2486,6 +5281,35 @@ var ZylemStage = class extends LifeCycleBase {
|
|
|
2486
5281
|
logMissingEntities() {
|
|
2487
5282
|
console.warn("Zylem world or scene is null");
|
|
2488
5283
|
}
|
|
5284
|
+
// ─── Camera management forwarding ─────────────────────────────────────
|
|
5285
|
+
/**
|
|
5286
|
+
* Add a camera to this stage's camera manager.
|
|
5287
|
+
*/
|
|
5288
|
+
addCamera(camera, name) {
|
|
5289
|
+
if (!this.cameraManagerRef) {
|
|
5290
|
+
console.warn("ZylemStage: CameraManager not available. Ensure stage is loaded with a RendererManager.");
|
|
5291
|
+
return null;
|
|
5292
|
+
}
|
|
5293
|
+
return this.cameraManagerRef.addCamera(camera, name);
|
|
5294
|
+
}
|
|
5295
|
+
/**
|
|
5296
|
+
* Remove a camera from this stage's camera manager.
|
|
5297
|
+
*/
|
|
5298
|
+
removeCamera(nameOrRef) {
|
|
5299
|
+
return this.cameraManagerRef?.removeCamera(nameOrRef) ?? false;
|
|
5300
|
+
}
|
|
5301
|
+
/**
|
|
5302
|
+
* Set the active camera by name or reference.
|
|
5303
|
+
*/
|
|
5304
|
+
setActiveCamera(nameOrRef) {
|
|
5305
|
+
return this.cameraManagerRef?.setActiveCamera(nameOrRef) ?? false;
|
|
5306
|
+
}
|
|
5307
|
+
/**
|
|
5308
|
+
* Get a camera by name from the camera manager.
|
|
5309
|
+
*/
|
|
5310
|
+
getCamera(name) {
|
|
5311
|
+
return this.cameraManagerRef?.getCamera(name) ?? null;
|
|
5312
|
+
}
|
|
2489
5313
|
/** Resize renderer viewport. */
|
|
2490
5314
|
resize(width, height) {
|
|
2491
5315
|
if (this.scene) {
|
|
@@ -2526,17 +5350,62 @@ var ZylemStage = class extends LifeCycleBase {
|
|
|
2526
5350
|
};
|
|
2527
5351
|
|
|
2528
5352
|
// src/lib/camera/camera.ts
|
|
2529
|
-
import { Vector2 as
|
|
5353
|
+
import { Vector2 as Vector210, Vector3 as Vector319 } from "three";
|
|
2530
5354
|
var CameraWrapper = class {
|
|
2531
5355
|
cameraRef;
|
|
2532
5356
|
constructor(camera) {
|
|
2533
5357
|
this.cameraRef = camera;
|
|
2534
5358
|
}
|
|
5359
|
+
// ─── Target management ──────────────────────────────────────────────────
|
|
5360
|
+
/**
|
|
5361
|
+
* Add a target entity for the camera to follow/frame.
|
|
5362
|
+
* With multiple targets, the camera auto-frames to include all of them.
|
|
5363
|
+
*/
|
|
5364
|
+
addTarget(entity) {
|
|
5365
|
+
this.cameraRef.addTarget(entity);
|
|
5366
|
+
}
|
|
5367
|
+
/**
|
|
5368
|
+
* Remove a target entity from the camera.
|
|
5369
|
+
*/
|
|
5370
|
+
removeTarget(entity) {
|
|
5371
|
+
this.cameraRef.removeTarget(entity);
|
|
5372
|
+
}
|
|
5373
|
+
/**
|
|
5374
|
+
* Clear all targets. Camera will look at world origin.
|
|
5375
|
+
*/
|
|
5376
|
+
clearTargets() {
|
|
5377
|
+
this.cameraRef.clearTargets();
|
|
5378
|
+
}
|
|
5379
|
+
// ─── Orbital controls ───────────────────────────────────────────────────
|
|
5380
|
+
/**
|
|
5381
|
+
* Enable orbital controls for this camera.
|
|
5382
|
+
* Allows the user to orbit, pan, and zoom the camera.
|
|
5383
|
+
*/
|
|
5384
|
+
enableOrbitalControls() {
|
|
5385
|
+
this.cameraRef.enableOrbitalControls();
|
|
5386
|
+
}
|
|
5387
|
+
/**
|
|
5388
|
+
* Disable orbital controls for this camera.
|
|
5389
|
+
*/
|
|
5390
|
+
disableOrbitalControls() {
|
|
5391
|
+
this.cameraRef.disableOrbitalControls();
|
|
5392
|
+
}
|
|
5393
|
+
// ─── Viewport ───────────────────────────────────────────────────────────
|
|
5394
|
+
/**
|
|
5395
|
+
* Set the viewport for this camera (normalized 0-1 coordinates).
|
|
5396
|
+
* @param x Left edge (0 = left of canvas)
|
|
5397
|
+
* @param y Bottom edge (0 = bottom of canvas)
|
|
5398
|
+
* @param width Width as fraction of canvas
|
|
5399
|
+
* @param height Height as fraction of canvas
|
|
5400
|
+
*/
|
|
5401
|
+
setViewport(x, y, width, height) {
|
|
5402
|
+
this.cameraRef.setViewport(x, y, width, height);
|
|
5403
|
+
}
|
|
2535
5404
|
};
|
|
2536
5405
|
|
|
2537
5406
|
// src/lib/stage/stage-default.ts
|
|
2538
|
-
import { proxy as
|
|
2539
|
-
import { Vector3 as
|
|
5407
|
+
import { proxy as proxy5 } from "valtio/vanilla";
|
|
5408
|
+
import { Vector3 as Vector320 } from "three";
|
|
2540
5409
|
var initialDefaults = {
|
|
2541
5410
|
backgroundColor: ZylemBlueColor,
|
|
2542
5411
|
backgroundImage: null,
|
|
@@ -2544,10 +5413,10 @@ var initialDefaults = {
|
|
|
2544
5413
|
p1: ["gamepad-1", "keyboard"],
|
|
2545
5414
|
p2: ["gamepad-2", "keyboard"]
|
|
2546
5415
|
},
|
|
2547
|
-
gravity: new
|
|
5416
|
+
gravity: new Vector320(0, 0, 0),
|
|
2548
5417
|
variables: {}
|
|
2549
5418
|
};
|
|
2550
|
-
var stageDefaultsState =
|
|
5419
|
+
var stageDefaultsState = proxy5({
|
|
2551
5420
|
...initialDefaults
|
|
2552
5421
|
});
|
|
2553
5422
|
function getStageOptions(options) {
|
|
@@ -2569,6 +5438,25 @@ function getStageDefaultConfig() {
|
|
|
2569
5438
|
};
|
|
2570
5439
|
}
|
|
2571
5440
|
|
|
5441
|
+
// src/lib/input/input-presets.ts
|
|
5442
|
+
var INPUT_PLAYERS = ["p1", "p2", "p3", "p4", "p5", "p6", "p7", "p8"];
|
|
5443
|
+
function mergeInputConfigs(...configs) {
|
|
5444
|
+
const result = {};
|
|
5445
|
+
for (const config of configs) {
|
|
5446
|
+
for (const player of INPUT_PLAYERS) {
|
|
5447
|
+
const source = config[player];
|
|
5448
|
+
if (!source) continue;
|
|
5449
|
+
const target = result[player] ?? {};
|
|
5450
|
+
result[player] = {
|
|
5451
|
+
key: { ...target.key, ...source.key },
|
|
5452
|
+
mouse: { ...target.mouse, ...source.mouse },
|
|
5453
|
+
includeDefaults: source.includeDefaults ?? target.includeDefaults
|
|
5454
|
+
};
|
|
5455
|
+
}
|
|
5456
|
+
}
|
|
5457
|
+
return result;
|
|
5458
|
+
}
|
|
5459
|
+
|
|
2572
5460
|
// src/lib/stage/stage.ts
|
|
2573
5461
|
var Stage = class {
|
|
2574
5462
|
wrappedStage;
|
|
@@ -2580,11 +5468,34 @@ var Stage = class {
|
|
|
2580
5468
|
updateCallbacks = [];
|
|
2581
5469
|
destroyCallbacks = [];
|
|
2582
5470
|
pendingLoadingCallbacks = [];
|
|
5471
|
+
// Event delegate for dispatch/listen API
|
|
5472
|
+
eventDelegate = new EventEmitterDelegate();
|
|
5473
|
+
/** Per-stage input configuration overrides. Merged with game-level defaults on stage load. */
|
|
5474
|
+
inputConfig = null;
|
|
5475
|
+
/**
|
|
5476
|
+
* Callback set by the game to trigger input reconfiguration
|
|
5477
|
+
* when this stage's input config changes at runtime.
|
|
5478
|
+
* @internal
|
|
5479
|
+
*/
|
|
5480
|
+
onInputConfigChanged = null;
|
|
2583
5481
|
constructor(options) {
|
|
2584
5482
|
this.options = options;
|
|
2585
5483
|
this.wrappedStage = null;
|
|
2586
5484
|
}
|
|
2587
|
-
|
|
5485
|
+
/**
|
|
5486
|
+
* Set composable input configuration for this stage.
|
|
5487
|
+
* Multiple configs are deep-merged (later configs win on key conflicts).
|
|
5488
|
+
* If this stage is currently active, the change takes effect immediately.
|
|
5489
|
+
* @example stage.setInputConfiguration(useArrowsForAxes('p1'), useWASDForDirections('p2'));
|
|
5490
|
+
*/
|
|
5491
|
+
setInputConfiguration(...configs) {
|
|
5492
|
+
this.inputConfig = mergeInputConfigs(...configs);
|
|
5493
|
+
if (this.onInputConfigChanged) {
|
|
5494
|
+
this.onInputConfigChanged();
|
|
5495
|
+
}
|
|
5496
|
+
return this;
|
|
5497
|
+
}
|
|
5498
|
+
async load(id, camera, rendererManager) {
|
|
2588
5499
|
stageState.entities = [];
|
|
2589
5500
|
const loadOptions = [...this.options, ...this._pendingEntities];
|
|
2590
5501
|
this._pendingEntities = [];
|
|
@@ -2595,7 +5506,7 @@ var Stage = class {
|
|
|
2595
5506
|
});
|
|
2596
5507
|
this.pendingLoadingCallbacks = [];
|
|
2597
5508
|
const zylemCamera = camera instanceof CameraWrapper ? camera.cameraRef : camera;
|
|
2598
|
-
await this.wrappedStage.load(id, zylemCamera);
|
|
5509
|
+
await this.wrappedStage.load(id, zylemCamera, rendererManager);
|
|
2599
5510
|
this.wrappedStage.onEntityAdded((child) => {
|
|
2600
5511
|
const next = this.wrappedStage.buildEntityState(child);
|
|
2601
5512
|
stageState.entities = [...stageState.entities, next];
|
|
@@ -2700,6 +5611,66 @@ var Stage = class {
|
|
|
2700
5611
|
const entity = this.wrappedStage?.children.find((c) => c.name === name);
|
|
2701
5612
|
return entity;
|
|
2702
5613
|
}
|
|
5614
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
5615
|
+
// Camera management
|
|
5616
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
5617
|
+
/**
|
|
5618
|
+
* Add a camera to this stage.
|
|
5619
|
+
* @param camera The ZylemCamera or CameraWrapper to add
|
|
5620
|
+
* @param name Optional name for lookup
|
|
5621
|
+
*/
|
|
5622
|
+
addCamera(camera, name) {
|
|
5623
|
+
const zylemCam = camera instanceof CameraWrapper ? camera.cameraRef : camera;
|
|
5624
|
+
return this.wrappedStage?.addCamera(zylemCam, name) ?? null;
|
|
5625
|
+
}
|
|
5626
|
+
/**
|
|
5627
|
+
* Remove a camera from this stage by name or reference.
|
|
5628
|
+
*/
|
|
5629
|
+
removeCamera(nameOrRef) {
|
|
5630
|
+
if (nameOrRef instanceof CameraWrapper) {
|
|
5631
|
+
return this.wrappedStage?.removeCamera(nameOrRef.cameraRef) ?? false;
|
|
5632
|
+
}
|
|
5633
|
+
return this.wrappedStage?.removeCamera(nameOrRef) ?? false;
|
|
5634
|
+
}
|
|
5635
|
+
/**
|
|
5636
|
+
* Set the active camera by name or reference.
|
|
5637
|
+
*/
|
|
5638
|
+
setActiveCamera(nameOrRef) {
|
|
5639
|
+
if (nameOrRef instanceof CameraWrapper) {
|
|
5640
|
+
return this.wrappedStage?.setActiveCamera(nameOrRef.cameraRef) ?? false;
|
|
5641
|
+
}
|
|
5642
|
+
return this.wrappedStage?.setActiveCamera(nameOrRef) ?? false;
|
|
5643
|
+
}
|
|
5644
|
+
/**
|
|
5645
|
+
* Get a camera by name from the camera manager.
|
|
5646
|
+
*/
|
|
5647
|
+
getCamera(name) {
|
|
5648
|
+
return this.wrappedStage?.getCamera(name) ?? null;
|
|
5649
|
+
}
|
|
5650
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
5651
|
+
// Events API
|
|
5652
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
5653
|
+
/**
|
|
5654
|
+
* Dispatch an event from the stage.
|
|
5655
|
+
* Events are emitted both locally and to the global event bus.
|
|
5656
|
+
*/
|
|
5657
|
+
dispatch(event, payload) {
|
|
5658
|
+
this.eventDelegate.dispatch(event, payload);
|
|
5659
|
+
zylemEventBus.emit(event, payload);
|
|
5660
|
+
}
|
|
5661
|
+
/**
|
|
5662
|
+
* Listen for events on this stage instance.
|
|
5663
|
+
* @returns Unsubscribe function
|
|
5664
|
+
*/
|
|
5665
|
+
listen(event, handler) {
|
|
5666
|
+
return this.eventDelegate.listen(event, handler);
|
|
5667
|
+
}
|
|
5668
|
+
/**
|
|
5669
|
+
* Clean up stage resources including event subscriptions.
|
|
5670
|
+
*/
|
|
5671
|
+
dispose() {
|
|
5672
|
+
this.eventDelegate.dispose();
|
|
5673
|
+
}
|
|
2703
5674
|
};
|
|
2704
5675
|
function createStage(...options) {
|
|
2705
5676
|
const _options = getStageOptions(options);
|
|
@@ -2707,7 +5678,7 @@ function createStage(...options) {
|
|
|
2707
5678
|
}
|
|
2708
5679
|
|
|
2709
5680
|
// src/lib/stage/entity-spawner.ts
|
|
2710
|
-
import { Euler, Quaternion as
|
|
5681
|
+
import { Euler as Euler2, Quaternion as Quaternion5, Vector2 as Vector211 } from "three";
|
|
2711
5682
|
function entitySpawner(factory) {
|
|
2712
5683
|
return {
|
|
2713
5684
|
spawn: async (stage, x, y) => {
|
|
@@ -2715,7 +5686,7 @@ function entitySpawner(factory) {
|
|
|
2715
5686
|
stage.add(instance);
|
|
2716
5687
|
return instance;
|
|
2717
5688
|
},
|
|
2718
|
-
spawnRelative: async (source, stage, offset = new
|
|
5689
|
+
spawnRelative: async (source, stage, offset = new Vector211(0, 1)) => {
|
|
2719
5690
|
if (!source.body) {
|
|
2720
5691
|
console.warn("body missing for entity during spawnRelative");
|
|
2721
5692
|
return void 0;
|
|
@@ -2724,8 +5695,8 @@ function entitySpawner(factory) {
|
|
|
2724
5695
|
let rz = source._rotation2DAngle ?? 0;
|
|
2725
5696
|
try {
|
|
2726
5697
|
const r = source.body.rotation();
|
|
2727
|
-
const q = new
|
|
2728
|
-
const e = new
|
|
5698
|
+
const q = new Quaternion5(r.x, r.y, r.z, r.w);
|
|
5699
|
+
const e = new Euler2().setFromQuaternion(q, "XYZ");
|
|
2729
5700
|
rz = e.z;
|
|
2730
5701
|
} catch {
|
|
2731
5702
|
}
|