@zylem/game-lib 0.6.2 → 0.6.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (82) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +9 -16
  3. package/dist/actions.d.ts +30 -21
  4. package/dist/actions.js +793 -146
  5. package/dist/actions.js.map +1 -1
  6. package/dist/behavior/jumper-2d.d.ts +114 -0
  7. package/dist/behavior/jumper-2d.js +711 -0
  8. package/dist/behavior/jumper-2d.js.map +1 -0
  9. package/dist/behavior/platformer-3d.d.ts +296 -0
  10. package/dist/behavior/platformer-3d.js +761 -0
  11. package/dist/behavior/platformer-3d.js.map +1 -0
  12. package/dist/behavior/ricochet-2d.d.ts +275 -0
  13. package/dist/behavior/ricochet-2d.js +425 -0
  14. package/dist/behavior/ricochet-2d.js.map +1 -0
  15. package/dist/behavior/ricochet-3d.d.ts +117 -0
  16. package/dist/behavior/ricochet-3d.js +443 -0
  17. package/dist/behavior/ricochet-3d.js.map +1 -0
  18. package/dist/behavior/screen-visibility.d.ts +79 -0
  19. package/dist/behavior/screen-visibility.js +358 -0
  20. package/dist/behavior/screen-visibility.js.map +1 -0
  21. package/dist/behavior/screen-wrap.d.ts +87 -0
  22. package/dist/behavior/screen-wrap.js +246 -0
  23. package/dist/behavior/screen-wrap.js.map +1 -0
  24. package/dist/behavior/shooter-2d.d.ts +79 -0
  25. package/dist/behavior/shooter-2d.js +180 -0
  26. package/dist/behavior/shooter-2d.js.map +1 -0
  27. package/dist/behavior/thruster.d.ts +11 -0
  28. package/dist/behavior/thruster.js +292 -0
  29. package/dist/behavior/thruster.js.map +1 -0
  30. package/dist/behavior/top-down-movement.d.ts +56 -0
  31. package/dist/behavior/top-down-movement.js +125 -0
  32. package/dist/behavior/top-down-movement.js.map +1 -0
  33. package/dist/behavior/world-boundary-2d.d.ts +142 -0
  34. package/dist/behavior/world-boundary-2d.js +235 -0
  35. package/dist/behavior/world-boundary-2d.js.map +1 -0
  36. package/dist/behavior/world-boundary-3d.d.ts +76 -0
  37. package/dist/behavior/world-boundary-3d.js +274 -0
  38. package/dist/behavior/world-boundary-3d.js.map +1 -0
  39. package/dist/behavior-descriptor-BXnVR8Ki.d.ts +159 -0
  40. package/dist/{blueprints-Cq3Ko6_G.d.ts → blueprints-DmbK2dki.d.ts} +2 -2
  41. package/dist/camera-4XO5gbQH.d.ts +905 -0
  42. package/dist/camera.d.ts +3 -2
  43. package/dist/camera.js +1653 -377
  44. package/dist/camera.js.map +1 -1
  45. package/dist/composition-BASvMKrW.d.ts +218 -0
  46. package/dist/{core-bO8TzV7u.d.ts → core-CARRaS55.d.ts} +110 -69
  47. package/dist/core.d.ts +11 -6
  48. package/dist/core.js +10766 -5626
  49. package/dist/core.js.map +1 -1
  50. package/dist/{entities-DvByhMGU.d.ts → entities-ChFirVL9.d.ts} +133 -29
  51. package/dist/entities.d.ts +5 -3
  52. package/dist/entities.js +4679 -3202
  53. package/dist/entities.js.map +1 -1
  54. package/dist/entity-vj-HTjzU.d.ts +1169 -0
  55. package/dist/global-change-2JvMaz44.d.ts +25 -0
  56. package/dist/main.d.ts +1118 -16
  57. package/dist/main.js +17538 -8499
  58. package/dist/main.js.map +1 -1
  59. package/dist/physics-pose-DCc4oE44.d.ts +25 -0
  60. package/dist/physics-protocol-BDD3P5W2.d.ts +200 -0
  61. package/dist/physics-worker.d.ts +21 -0
  62. package/dist/physics-worker.js +306 -0
  63. package/dist/physics-worker.js.map +1 -0
  64. package/dist/physics.d.ts +205 -0
  65. package/dist/physics.js +577 -0
  66. package/dist/physics.js.map +1 -0
  67. package/dist/stage-types-C19IhuzA.d.ts +731 -0
  68. package/dist/stage.d.ts +11 -7
  69. package/dist/stage.js +8024 -3852
  70. package/dist/stage.js.map +1 -1
  71. package/dist/sync-state-machine-CZyspBpj.d.ts +16 -0
  72. package/dist/thruster-23lzoPZd.d.ts +180 -0
  73. package/dist/world-DfgxoNMt.d.ts +105 -0
  74. package/package.json +53 -13
  75. package/dist/behaviors.d.ts +0 -854
  76. package/dist/behaviors.js +0 -1209
  77. package/dist/behaviors.js.map +0 -1
  78. package/dist/camera-CeJPAgGg.d.ts +0 -116
  79. package/dist/moveable-B_vyA6cw.d.ts +0 -67
  80. package/dist/stage-types-Bd-KtcYT.d.ts +0 -375
  81. package/dist/transformable-CUhvyuYO.d.ts +0 -67
  82. package/dist/world-C8tQ7Plj.d.ts +0 -774
package/dist/camera.js CHANGED
@@ -1,8 +1,8 @@
1
1
  // src/lib/camera/camera.ts
2
- import { Vector2 as Vector23, Vector3 as Vector34 } from "three";
2
+ import { Vector2 as Vector23, Vector3 as Vector37 } from "three";
3
3
 
4
4
  // src/lib/camera/zylem-camera.ts
5
- import { PerspectiveCamera, Vector3 as Vector33, Object3D as Object3D2, OrthographicCamera, WebGLRenderer as WebGLRenderer3 } from "three";
5
+ import { PerspectiveCamera as PerspectiveCamera2, Vector3 as Vector36, OrthographicCamera, WebGLRenderTarget as WebGLRenderTarget2, LinearFilter } from "three";
6
6
 
7
7
  // src/lib/camera/perspective.ts
8
8
  var Perspectives = {
@@ -10,207 +10,12 @@ var Perspectives = {
10
10
  ThirdPerson: "third-person",
11
11
  Isometric: "isometric",
12
12
  Flat2D: "flat-2d",
13
- Fixed2D: "fixed-2d"
14
- };
15
-
16
- // src/lib/camera/third-person.ts
17
- import { Vector3 } from "three";
18
- var ThirdPersonCamera = class {
19
- distance;
20
- screenResolution = null;
21
- renderer = null;
22
- scene = null;
23
- cameraRef = null;
24
- constructor() {
25
- this.distance = new Vector3(0, 5, 8);
26
- }
27
- /**
28
- * Setup the third person camera controller
29
- */
30
- setup(params) {
31
- const { screenResolution, renderer, scene, camera } = params;
32
- this.screenResolution = screenResolution;
33
- this.renderer = renderer;
34
- this.scene = scene;
35
- this.cameraRef = camera;
36
- }
37
- /**
38
- * Update the third person camera
39
- */
40
- update(delta) {
41
- if (!this.cameraRef.target) {
42
- return;
43
- }
44
- const desiredCameraPosition = this.cameraRef.target.group.position.clone().add(this.distance);
45
- this.cameraRef.camera.position.lerp(desiredCameraPosition, 0.1);
46
- this.cameraRef.camera.lookAt(this.cameraRef.target.group.position);
47
- }
48
- /**
49
- * Handle resize events
50
- */
51
- resize(width, height) {
52
- if (this.screenResolution) {
53
- this.screenResolution.set(width, height);
54
- }
55
- }
56
- /**
57
- * Set the distance from the target
58
- */
59
- setDistance(distance) {
60
- this.distance = distance;
61
- }
62
- };
63
-
64
- // src/lib/camera/fixed-2d.ts
65
- var Fixed2DCamera = class {
66
- screenResolution = null;
67
- renderer = null;
68
- scene = null;
69
- cameraRef = null;
70
- constructor() {
71
- }
72
- /**
73
- * Setup the fixed 2D camera controller
74
- */
75
- setup(params) {
76
- const { screenResolution, renderer, scene, camera } = params;
77
- this.screenResolution = screenResolution;
78
- this.renderer = renderer;
79
- this.scene = scene;
80
- this.cameraRef = camera;
81
- this.cameraRef.camera.position.set(0, 0, 10);
82
- this.cameraRef.camera.lookAt(0, 0, 0);
83
- }
84
- /**
85
- * Update the fixed 2D camera
86
- * Fixed cameras don't need to update position/rotation automatically
87
- */
88
- update(delta) {
89
- }
90
- /**
91
- * Handle resize events for 2D camera
92
- */
93
- resize(width, height) {
94
- if (this.screenResolution) {
95
- this.screenResolution.set(width, height);
96
- }
97
- }
98
- };
99
-
100
- // src/lib/camera/zylem-camera.ts
101
- import { EffectComposer } from "three/examples/jsm/postprocessing/EffectComposer.js";
102
-
103
- // src/lib/graphics/render-pass.ts
104
- import * as THREE from "three";
105
-
106
- // src/lib/graphics/shaders/vertex/object.shader.ts
107
- var objectVertexShader = `
108
- uniform vec2 uvScale;
109
- varying vec2 vUv;
110
-
111
- void main() {
112
- vUv = uv;
113
- vec4 mvPosition = modelViewMatrix * vec4( position, 1.0 );
114
- gl_Position = projectionMatrix * mvPosition;
115
- }
116
- `;
117
-
118
- // src/lib/graphics/shaders/standard.shader.ts
119
- var fragment = `
120
- uniform sampler2D tDiffuse;
121
- varying vec2 vUv;
122
-
123
- void main() {
124
- vec4 texel = texture2D( tDiffuse, vUv );
125
-
126
- gl_FragColor = texel;
127
- }
128
- `;
129
- var standardShader = {
130
- vertex: objectVertexShader,
131
- fragment
132
- };
133
-
134
- // src/lib/graphics/render-pass.ts
135
- import { WebGLRenderTarget } from "three";
136
- import { Pass, FullScreenQuad } from "three/addons/postprocessing/Pass.js";
137
- var RenderPass = class extends Pass {
138
- fsQuad;
139
- resolution;
140
- scene;
141
- camera;
142
- rgbRenderTarget;
143
- normalRenderTarget;
144
- normalMaterial;
145
- constructor(resolution, scene, camera) {
146
- super();
147
- this.resolution = resolution;
148
- this.fsQuad = new FullScreenQuad(this.material());
149
- this.scene = scene;
150
- this.camera = camera;
151
- this.rgbRenderTarget = new WebGLRenderTarget(resolution.x * 4, resolution.y * 4);
152
- this.normalRenderTarget = new WebGLRenderTarget(resolution.x * 4, resolution.y * 4);
153
- this.normalMaterial = new THREE.MeshNormalMaterial();
154
- }
155
- render(renderer, writeBuffer) {
156
- renderer.setRenderTarget(this.rgbRenderTarget);
157
- renderer.render(this.scene, this.camera);
158
- const overrideMaterial_old = this.scene.overrideMaterial;
159
- renderer.setRenderTarget(this.normalRenderTarget);
160
- this.scene.overrideMaterial = this.normalMaterial;
161
- renderer.render(this.scene, this.camera);
162
- this.scene.overrideMaterial = overrideMaterial_old;
163
- const uniforms = this.fsQuad.material.uniforms;
164
- uniforms.tDiffuse.value = this.rgbRenderTarget.texture;
165
- uniforms.tDepth.value = this.rgbRenderTarget.depthTexture;
166
- uniforms.tNormal.value = this.normalRenderTarget.texture;
167
- uniforms.iTime.value += 0.01;
168
- if (this.renderToScreen) {
169
- renderer.setRenderTarget(null);
170
- } else {
171
- renderer.setRenderTarget(writeBuffer);
172
- }
173
- this.fsQuad.render(renderer);
174
- }
175
- material() {
176
- return new THREE.ShaderMaterial({
177
- uniforms: {
178
- iTime: { value: 0 },
179
- tDiffuse: { value: null },
180
- tDepth: { value: null },
181
- tNormal: { value: null },
182
- resolution: {
183
- value: new THREE.Vector4(
184
- this.resolution.x,
185
- this.resolution.y,
186
- 1 / this.resolution.x,
187
- 1 / this.resolution.y
188
- )
189
- }
190
- },
191
- vertexShader: standardShader.vertex,
192
- fragmentShader: standardShader.fragment
193
- });
194
- }
195
- dispose() {
196
- try {
197
- this.fsQuad?.dispose?.();
198
- } catch {
199
- }
200
- try {
201
- this.rgbRenderTarget?.dispose?.();
202
- this.normalRenderTarget?.dispose?.();
203
- } catch {
204
- }
205
- try {
206
- this.normalMaterial?.dispose?.();
207
- } catch {
208
- }
209
- }
13
+ Fixed2D: "fixed-2d",
14
+ TopDown: "top-down"
210
15
  };
211
16
 
212
17
  // src/lib/camera/camera-debug-delegate.ts
213
- import { Vector3 as Vector32 } from "three";
18
+ import { Vector3 } from "three";
214
19
  import { OrbitControls } from "three/addons/controls/OrbitControls.js";
215
20
  var CameraOrbitController = class {
216
21
  camera;
@@ -219,7 +24,7 @@ var CameraOrbitController = class {
219
24
  sceneRef = null;
220
25
  orbitControls = null;
221
26
  orbitTarget = null;
222
- orbitTargetWorldPos = new Vector32();
27
+ orbitTargetWorldPos = new Vector3();
223
28
  debugDelegate = null;
224
29
  debugUnsubscribe = null;
225
30
  debugStateSnapshot = { enabled: false, selected: [] };
@@ -233,6 +38,8 @@ var CameraOrbitController = class {
233
38
  savedDebugCameraQuaternion = null;
234
39
  savedDebugCameraZoom = null;
235
40
  savedDebugOrbitTarget = null;
41
+ /** Whether user-configured orbital controls are enabled (independent of debug) */
42
+ _userOrbitEnabled = false;
236
43
  constructor(camera, domElement, cameraRig) {
237
44
  this.camera = camera;
238
45
  this.domElement = domElement;
@@ -245,11 +52,17 @@ var CameraOrbitController = class {
245
52
  this.sceneRef = scene;
246
53
  }
247
54
  /**
248
- * Check if debug mode is currently active (orbit controls enabled).
55
+ * Check if debug mode is currently active (orbit controls enabled via debug system).
249
56
  */
250
57
  get isActive() {
251
58
  return this.debugStateSnapshot.enabled;
252
59
  }
60
+ /**
61
+ * Check if any orbit controls are currently active (debug or user).
62
+ */
63
+ get isOrbitActive() {
64
+ return this.debugStateSnapshot.enabled || this._userOrbitEnabled;
65
+ }
253
66
  /**
254
67
  * Update orbit controls each frame.
255
68
  * Should be called from the camera's update loop.
@@ -261,6 +74,26 @@ var CameraOrbitController = class {
261
74
  }
262
75
  this.orbitControls?.update();
263
76
  }
77
+ /**
78
+ * Enable user-configured orbital controls (not debug mode).
79
+ * These orbit controls persist until explicitly disabled.
80
+ */
81
+ enableUserOrbitControls() {
82
+ this._userOrbitEnabled = true;
83
+ if (!this.orbitControls) {
84
+ this.enableOrbitControls();
85
+ }
86
+ }
87
+ /**
88
+ * Disable user-configured orbital controls.
89
+ * Will not disable orbit controls if debug mode is active.
90
+ */
91
+ disableUserOrbitControls() {
92
+ this._userOrbitEnabled = false;
93
+ if (!this.debugStateSnapshot.enabled) {
94
+ this.disableOrbitControls();
95
+ }
96
+ }
264
97
  /**
265
98
  * Attach a delegate to react to debug state changes.
266
99
  */
@@ -309,7 +142,9 @@ var CameraOrbitController = class {
309
142
  } else if (!state.enabled && wasEnabled) {
310
143
  this.saveDebugCameraState();
311
144
  this.orbitTarget = null;
312
- this.disableOrbitControls();
145
+ if (!this._userOrbitEnabled) {
146
+ this.disableOrbitControls();
147
+ }
313
148
  this.reattachCameraToRig();
314
149
  this.restoreCameraState();
315
150
  } else if (state.enabled) {
@@ -436,7 +271,7 @@ var CameraOrbitController = class {
436
271
  return;
437
272
  }
438
273
  this.savedCameraLocalPosition = this.camera.position.clone();
439
- const worldPos = new Vector32();
274
+ const worldPos = new Vector3();
440
275
  this.camera.getWorldPosition(worldPos);
441
276
  this.cameraRig.remove(this.camera);
442
277
  if (this.sceneRef) {
@@ -463,240 +298,1401 @@ var CameraOrbitController = class {
463
298
  }
464
299
  };
465
300
 
466
- // src/lib/camera/zylem-camera.ts
467
- var ZylemCamera = class {
468
- cameraRig = null;
301
+ // src/lib/camera/renderer-manager.ts
302
+ import { Vector2, WebGLRenderer as WebGLRenderer2 } from "three";
303
+ import { WebGPURenderer } from "three/webgpu";
304
+ import { EffectComposer } from "three/examples/jsm/postprocessing/EffectComposer.js";
305
+
306
+ // src/lib/graphics/render-pass.ts
307
+ import * as THREE from "three";
308
+
309
+ // src/lib/graphics/shaders/vertex/object.shader.ts
310
+ var objectVertexShader = `
311
+ uniform vec2 uvScale;
312
+ varying vec2 vUv;
313
+
314
+ void main() {
315
+ vUv = uv;
316
+ vec4 mvPosition = modelViewMatrix * vec4( position, 1.0 );
317
+ gl_Position = projectionMatrix * mvPosition;
318
+ }
319
+ `;
320
+
321
+ // src/lib/graphics/shaders/standard.shader.ts
322
+ var fragment = `
323
+ uniform sampler2D tDiffuse;
324
+ varying vec2 vUv;
325
+
326
+ void main() {
327
+ vec4 texel = texture2D( tDiffuse, vUv );
328
+
329
+ gl_FragColor = texel;
330
+ }
331
+ `;
332
+ var standardShader = {
333
+ vertex: objectVertexShader,
334
+ fragment
335
+ };
336
+
337
+ // src/lib/graphics/render-pass.ts
338
+ import { WebGLRenderTarget } from "three";
339
+ import { Pass, FullScreenQuad } from "three/addons/postprocessing/Pass.js";
340
+ var RenderPass = class extends Pass {
341
+ fsQuad;
342
+ resolution;
343
+ scene;
469
344
  camera;
470
- screenResolution;
471
- renderer;
472
- composer;
473
- _perspective;
474
- target = null;
475
- sceneRef = null;
476
- frustumSize = 10;
477
- // Perspective controller delegation
478
- perspectiveController = null;
479
- // Debug/orbit controls delegation
480
- orbitController = null;
481
- constructor(perspective, screenResolution, frustumSize = 10) {
482
- this._perspective = perspective;
483
- this.screenResolution = screenResolution;
484
- this.frustumSize = frustumSize;
485
- this.renderer = new WebGLRenderer3({ antialias: false, alpha: true });
486
- this.renderer.setSize(screenResolution.x, screenResolution.y);
487
- this.renderer.shadowMap.enabled = true;
488
- this.composer = new EffectComposer(this.renderer);
489
- const aspectRatio = screenResolution.x / screenResolution.y;
490
- this.camera = this.createCameraForPerspective(aspectRatio);
491
- if (this.needsRig()) {
492
- this.cameraRig = new Object3D2();
493
- this.cameraRig.position.set(0, 3, 10);
494
- this.cameraRig.add(this.camera);
495
- this.camera.lookAt(new Vector33(0, 2, 0));
496
- } else {
497
- this.camera.position.set(0, 0, 10);
498
- this.camera.lookAt(new Vector33(0, 0, 0));
499
- }
500
- this.initializePerspectiveController();
501
- this.orbitController = new CameraOrbitController(this.camera, this.renderer.domElement, this.cameraRig);
502
- }
503
- /**
504
- * Setup the camera with a scene
505
- */
506
- async setup(scene) {
507
- this.sceneRef = scene;
508
- let renderResolution = this.screenResolution.clone().divideScalar(2);
509
- renderResolution.x |= 0;
510
- renderResolution.y |= 0;
511
- const pass = new RenderPass(renderResolution, scene, this.camera);
512
- this.composer.addPass(pass);
513
- if (this.perspectiveController) {
514
- this.perspectiveController.setup({
515
- screenResolution: this.screenResolution,
516
- renderer: this.renderer,
517
- scene,
518
- camera: this
519
- });
520
- }
521
- this.orbitController?.setScene(scene);
522
- this.renderer.setAnimationLoop((delta) => {
523
- this.update(delta || 0);
524
- });
345
+ rgbRenderTarget;
346
+ normalRenderTarget;
347
+ normalMaterial;
348
+ constructor(resolution, scene, camera) {
349
+ super();
350
+ this.resolution = resolution;
351
+ this.fsQuad = new FullScreenQuad(this.material());
352
+ this.scene = scene;
353
+ this.camera = camera;
354
+ this.rgbRenderTarget = new WebGLRenderTarget(resolution.x * 4, resolution.y * 4);
355
+ this.normalRenderTarget = new WebGLRenderTarget(resolution.x * 4, resolution.y * 4);
356
+ this.normalMaterial = new THREE.MeshNormalMaterial();
525
357
  }
526
- /**
527
- * Update camera and render
528
- */
529
- update(delta) {
530
- this.orbitController?.update();
531
- if (this.perspectiveController && !this.isDebugModeActive()) {
532
- this.perspectiveController.update(delta);
358
+ render(renderer, writeBuffer) {
359
+ renderer.setRenderTarget(this.rgbRenderTarget);
360
+ renderer.render(this.scene, this.camera);
361
+ const overrideMaterial_old = this.scene.overrideMaterial;
362
+ renderer.setRenderTarget(this.normalRenderTarget);
363
+ this.scene.overrideMaterial = this.normalMaterial;
364
+ renderer.render(this.scene, this.camera);
365
+ this.scene.overrideMaterial = overrideMaterial_old;
366
+ const uniforms = this.fsQuad.material.uniforms;
367
+ uniforms.tDiffuse.value = this.rgbRenderTarget.texture;
368
+ uniforms.tDepth.value = this.rgbRenderTarget.depthTexture;
369
+ uniforms.tNormal.value = this.normalRenderTarget.texture;
370
+ uniforms.iTime.value += 0.01;
371
+ if (this.renderToScreen) {
372
+ renderer.setRenderTarget(null);
373
+ } else {
374
+ renderer.setRenderTarget(writeBuffer);
375
+ }
376
+ this.fsQuad.render(renderer);
377
+ }
378
+ material() {
379
+ return new THREE.ShaderMaterial({
380
+ uniforms: {
381
+ iTime: { value: 0 },
382
+ tDiffuse: { value: null },
383
+ tDepth: { value: null },
384
+ tNormal: { value: null },
385
+ resolution: {
386
+ value: new THREE.Vector4(
387
+ this.resolution.x,
388
+ this.resolution.y,
389
+ 1 / this.resolution.x,
390
+ 1 / this.resolution.y
391
+ )
392
+ }
393
+ },
394
+ vertexShader: standardShader.vertex,
395
+ fragmentShader: standardShader.fragment
396
+ });
397
+ }
398
+ dispose() {
399
+ try {
400
+ this.fsQuad?.dispose?.();
401
+ } catch {
402
+ }
403
+ try {
404
+ this.rgbRenderTarget?.dispose?.();
405
+ this.normalRenderTarget?.dispose?.();
406
+ } catch {
407
+ }
408
+ try {
409
+ this.normalMaterial?.dispose?.();
410
+ } catch {
411
+ }
412
+ }
413
+ };
414
+
415
+ // src/lib/camera/renderer-manager.ts
416
+ var DEFAULT_VIEWPORT = { x: 0, y: 0, width: 1, height: 1 };
417
+ async function isWebGPUSupported() {
418
+ if (!("gpu" in navigator)) return false;
419
+ try {
420
+ const adapter = await navigator.gpu.requestAdapter();
421
+ return adapter !== null;
422
+ } catch {
423
+ return false;
424
+ }
425
+ }
426
+ var RendererManager = class {
427
+ renderer;
428
+ composer;
429
+ screenResolution;
430
+ rendererType;
431
+ _isWebGPU = false;
432
+ _initialized = false;
433
+ _sceneRef = null;
434
+ _lastAnimationTimestamp = null;
435
+ constructor(screenResolution, rendererType = "webgl") {
436
+ this.screenResolution = screenResolution || new Vector2(window.innerWidth, window.innerHeight);
437
+ this.rendererType = rendererType;
438
+ }
439
+ /**
440
+ * Check if the renderer has been initialized
441
+ */
442
+ get initialized() {
443
+ return this._initialized;
444
+ }
445
+ /**
446
+ * Check if using WebGPU renderer
447
+ */
448
+ get isWebGPU() {
449
+ return this._isWebGPU;
450
+ }
451
+ /**
452
+ * Initialize the renderer (must be called before rendering).
453
+ * Async because WebGPU requires async initialization.
454
+ */
455
+ async initRenderer() {
456
+ if (this._initialized) return;
457
+ let useWebGPU = false;
458
+ if (this.rendererType === "webgpu") {
459
+ useWebGPU = true;
460
+ } else if (this.rendererType === "auto") {
461
+ useWebGPU = await isWebGPUSupported();
462
+ }
463
+ if (useWebGPU) {
464
+ try {
465
+ this.renderer = new WebGPURenderer({ antialias: true });
466
+ await this.renderer.init();
467
+ this._isWebGPU = true;
468
+ console.log("RendererManager: Using WebGPU renderer");
469
+ } catch (e) {
470
+ console.warn("RendererManager: WebGPU init failed, falling back to WebGL", e);
471
+ this.renderer = new WebGLRenderer2({ antialias: false, alpha: true });
472
+ this._isWebGPU = false;
473
+ }
474
+ } else {
475
+ this.renderer = new WebGLRenderer2({ antialias: false, alpha: true });
476
+ this._isWebGPU = false;
477
+ console.log("RendererManager: Using WebGL renderer");
478
+ }
479
+ this.renderer.setSize(this.screenResolution.x, this.screenResolution.y);
480
+ if (this.renderer instanceof WebGLRenderer2) {
481
+ this.renderer.shadowMap.enabled = true;
482
+ }
483
+ if (!this._isWebGPU) {
484
+ this.composer = new EffectComposer(this.renderer);
485
+ }
486
+ this._initialized = true;
487
+ }
488
+ /**
489
+ * Set the current scene reference for rendering.
490
+ */
491
+ setScene(scene) {
492
+ this._sceneRef = scene;
493
+ }
494
+ /**
495
+ * Setup post-processing render pass for a camera (WebGL only).
496
+ */
497
+ setupRenderPass(scene, camera) {
498
+ if (this._isWebGPU || !this.composer) return;
499
+ if (this.composer.passes.length > 0) {
500
+ this.composer.passes.forEach((p) => {
501
+ try {
502
+ p.dispose?.();
503
+ } catch {
504
+ }
505
+ });
506
+ this.composer.passes.length = 0;
507
+ }
508
+ const renderResolution = this.screenResolution.clone().divideScalar(2);
509
+ renderResolution.x |= 0;
510
+ renderResolution.y |= 0;
511
+ const pass = new RenderPass(renderResolution, scene, camera);
512
+ this.composer.addPass(pass);
513
+ }
514
+ /**
515
+ * Start the render loop. Calls the provided callback each frame.
516
+ */
517
+ startRenderLoop(onFrame) {
518
+ this._lastAnimationTimestamp = null;
519
+ this.renderer.setAnimationLoop((timestamp) => {
520
+ const deltaSeconds = this._lastAnimationTimestamp === null ? 0 : Math.max(0, (timestamp - this._lastAnimationTimestamp) / 1e3);
521
+ this._lastAnimationTimestamp = timestamp;
522
+ onFrame(deltaSeconds);
523
+ });
524
+ }
525
+ /**
526
+ * Stop the render loop.
527
+ */
528
+ stopRenderLoop() {
529
+ this._lastAnimationTimestamp = null;
530
+ try {
531
+ this.renderer.setAnimationLoop(null);
532
+ } catch {
533
+ }
534
+ }
535
+ /**
536
+ * Render a scene from a single camera's perspective.
537
+ * Sets the viewport based on the camera's viewport config.
538
+ */
539
+ renderCamera(scene, camera) {
540
+ const vp = camera.viewport;
541
+ const w = this.screenResolution.x;
542
+ const h = this.screenResolution.y;
543
+ const pixelX = Math.floor(vp.x * w);
544
+ const pixelY = Math.floor(vp.y * h);
545
+ const pixelW = Math.floor(vp.width * w);
546
+ const pixelH = Math.floor(vp.height * h);
547
+ if (this.renderer instanceof WebGLRenderer2) {
548
+ this.renderer.setViewport(pixelX, pixelY, pixelW, pixelH);
549
+ this.renderer.setScissor(pixelX, pixelY, pixelW, pixelH);
550
+ this.renderer.setScissorTest(true);
551
+ }
552
+ if (this._isWebGPU) {
553
+ this.renderer.render(scene, camera.camera);
554
+ } else if (this.composer) {
555
+ this.composer.render(0);
556
+ }
557
+ }
558
+ /**
559
+ * Render a camera to its offscreen render target (WebGL only).
560
+ * Bypasses the EffectComposer since post-processing is not needed
561
+ * for render-to-texture output.
562
+ *
563
+ * The camera must have a non-null renderTarget.
564
+ */
565
+ renderCameraToTarget(scene, camera) {
566
+ if (!camera.renderTarget) return;
567
+ if (this.renderer instanceof WebGLRenderer2) {
568
+ const prevTarget = this.renderer.getRenderTarget();
569
+ this.renderer.setRenderTarget(camera.renderTarget);
570
+ this.renderer.clear();
571
+ this.renderer.render(scene, camera.camera);
572
+ this.renderer.setRenderTarget(prevTarget);
573
+ } else {
574
+ console.warn("RendererManager: Render-to-texture is not yet supported for WebGPU");
575
+ }
576
+ }
577
+ /**
578
+ * Render a scene from multiple cameras, each with their own viewport.
579
+ * Cameras are rendered in order (first = bottom layer, last = top layer).
580
+ */
581
+ renderCameras(scene, cameras) {
582
+ if (!scene || cameras.length === 0) return;
583
+ if (this.renderer instanceof WebGLRenderer2) {
584
+ this.renderer.setScissorTest(false);
585
+ this.renderer.clear();
586
+ }
587
+ for (const cam of cameras) {
588
+ this.renderCamera(scene, cam);
589
+ }
590
+ if (this.renderer instanceof WebGLRenderer2) {
591
+ this.renderer.setScissorTest(false);
592
+ }
593
+ }
594
+ /**
595
+ * Simple single-camera render (backwards compatible).
596
+ * Uses the full viewport for a single camera.
597
+ */
598
+ render(scene, camera) {
599
+ if (this._isWebGPU) {
600
+ this.renderer.render(scene, camera);
601
+ } else if (this.composer) {
602
+ this.composer.render(0);
603
+ }
604
+ }
605
+ /**
606
+ * Resize the renderer and update resolution.
607
+ */
608
+ resize(width, height) {
609
+ this.screenResolution.set(width, height);
610
+ this.renderer.setSize(width, height, false);
611
+ if (this.composer) {
612
+ this.composer.setSize(width, height);
613
+ }
614
+ }
615
+ /**
616
+ * Update renderer pixel ratio (DPR).
617
+ */
618
+ setPixelRatio(dpr) {
619
+ const safe = Math.max(1, Number.isFinite(dpr) ? dpr : 1);
620
+ this.renderer.setPixelRatio(safe);
621
+ }
622
+ /**
623
+ * Get the DOM element for the renderer.
624
+ */
625
+ getDomElement() {
626
+ return this.renderer.domElement;
627
+ }
628
+ /**
629
+ * Dispose renderer, composer, and related resources.
630
+ */
631
+ dispose() {
632
+ this.stopRenderLoop();
633
+ try {
634
+ this.composer?.passes?.forEach((p) => p.dispose?.());
635
+ this.composer?.dispose?.();
636
+ } catch {
637
+ }
638
+ try {
639
+ this.renderer.dispose();
640
+ } catch {
641
+ }
642
+ this._sceneRef = null;
643
+ this._initialized = false;
644
+ }
645
+ };
646
+
647
+ // src/lib/camera/smoothing.ts
648
+ import { Vector3 as Vector32, Quaternion as Quaternion2, MathUtils } from "three";
649
+ function defaultPose() {
650
+ return {
651
+ position: new Vector32(0, 0, 10),
652
+ rotation: new Quaternion2(),
653
+ fov: 75,
654
+ zoom: 1,
655
+ near: 0.1,
656
+ far: 1e3
657
+ };
658
+ }
659
+ function clonePose(pose) {
660
+ return {
661
+ position: pose.position.clone(),
662
+ rotation: pose.rotation.clone(),
663
+ fov: pose.fov,
664
+ zoom: pose.zoom,
665
+ near: pose.near,
666
+ far: pose.far,
667
+ lookAt: pose.lookAt?.clone()
668
+ };
669
+ }
670
+ function applyDelta(pose, delta) {
671
+ const result = clonePose(pose);
672
+ if (delta.position) {
673
+ result.position.add(delta.position);
674
+ }
675
+ if (delta.rotation) {
676
+ result.rotation.multiply(delta.rotation);
677
+ }
678
+ if (delta.fov != null && result.fov != null) {
679
+ result.fov += delta.fov;
680
+ }
681
+ if (delta.zoom != null && result.zoom != null) {
682
+ result.zoom += delta.zoom;
683
+ }
684
+ return result;
685
+ }
686
+ function smoothPose(current, target, damping, dt) {
687
+ const t = 1 - Math.pow(1 - MathUtils.clamp(damping, 0, 1), dt * 60);
688
+ const result = clonePose(current);
689
+ result.position.lerp(target.position, t);
690
+ result.rotation.slerp(target.rotation, t);
691
+ if (target.fov != null && result.fov != null) {
692
+ result.fov = MathUtils.lerp(result.fov, target.fov, t);
693
+ }
694
+ if (target.zoom != null && result.zoom != null) {
695
+ result.zoom = MathUtils.lerp(result.zoom, target.zoom, t);
696
+ }
697
+ if (target.near != null) {
698
+ result.near = target.near;
699
+ }
700
+ if (target.far != null) {
701
+ result.far = target.far;
702
+ }
703
+ result.lookAt = target.lookAt?.clone();
704
+ return result;
705
+ }
706
+
707
+ // src/lib/camera/camera-pipeline.ts
708
+ var CameraPipeline = class {
709
+ /** Active perspective (exactly one at a time). */
710
+ perspective = null;
711
+ /** Keyed behaviors, sorted by priority each frame. */
712
+ behaviors = /* @__PURE__ */ new Map();
713
+ /** Active transient actions. Expired actions are auto-removed. */
714
+ actions = [];
715
+ /** The desired pose after perspective + behaviors + actions. */
716
+ _desiredPose = defaultPose();
717
+ /** The smoothed final pose committed to the Three.js camera. */
718
+ _finalPose = defaultPose();
719
+ /** Whether the pipeline has run at least once (prevents lerp from origin on first frame). */
720
+ _initialized = false;
721
+ /** Smoothing factor: 0 = no movement, 1 = instant snap. */
722
+ damping = 0.15;
723
+ constructor(perspective) {
724
+ if (perspective) {
725
+ this.perspective = perspective;
726
+ if (perspective.defaults?.damping != null) {
727
+ this.damping = perspective.defaults.damping;
728
+ }
729
+ }
730
+ }
731
+ /**
732
+ * Run the full pipeline for one frame.
733
+ * Returns the final pose that should be committed to the Three.js camera.
734
+ */
735
+ run(ctx) {
736
+ let pose = this.perspective ? this.perspective.getBasePose(ctx) : defaultPose();
737
+ const sorted = this.getSortedBehaviors();
738
+ for (const behavior of sorted) {
739
+ if (behavior.enabled === false) continue;
740
+ pose = behavior.update(ctx, pose);
741
+ }
742
+ this._desiredPose = clonePose(pose);
743
+ for (let i = this.actions.length - 1; i >= 0; i--) {
744
+ const action = this.actions[i];
745
+ const delta = action.update(ctx);
746
+ pose = applyDelta(pose, delta);
747
+ if (action.isDone(ctx)) {
748
+ this.actions.splice(i, 1);
749
+ }
750
+ }
751
+ if (!this._initialized) {
752
+ this._finalPose = clonePose(pose);
753
+ this._initialized = true;
754
+ } else {
755
+ this._finalPose = smoothPose(this._finalPose, pose, this.damping, ctx.dt);
756
+ }
757
+ return this._finalPose;
758
+ }
759
+ // ─── Behavior management ───────────────────────────────────────────────
760
+ /**
761
+ * Add or replace a behavior by key (idempotent).
762
+ * Calls onDetach on the old behavior and onAttach on the new one.
763
+ */
764
+ addBehavior(key, behavior, ctx) {
765
+ const existing = this.behaviors.get(key);
766
+ if (existing?.onDetach && ctx) {
767
+ existing.onDetach(ctx);
768
+ }
769
+ this.behaviors.set(key, behavior);
770
+ if (behavior.onAttach && ctx) {
771
+ behavior.onAttach(ctx);
772
+ }
773
+ }
774
+ /**
775
+ * Remove a behavior by key. Calls onDetach if a context is provided.
776
+ */
777
+ removeBehavior(key, ctx) {
778
+ const existing = this.behaviors.get(key);
779
+ if (!existing) return false;
780
+ if (existing.onDetach && ctx) {
781
+ existing.onDetach(ctx);
782
+ }
783
+ return this.behaviors.delete(key);
784
+ }
785
+ /**
786
+ * Check whether a behavior with the given key exists.
787
+ */
788
+ hasBehavior(key) {
789
+ return this.behaviors.has(key);
790
+ }
791
+ // ─── Action management ─────────────────────────────────────────────────
792
+ /**
793
+ * Add a transient action. Actions self-expire via isDone().
794
+ */
795
+ addAction(action) {
796
+ this.actions.push(action);
797
+ }
798
+ // ─── State inspection ──────────────────────────────────────────────────
799
+ /**
800
+ * Return a debug snapshot of the pipeline state.
801
+ */
802
+ getState() {
803
+ return {
804
+ perspectiveId: this.perspective?.id ?? null,
805
+ desiredPose: this._desiredPose ? clonePose(this._desiredPose) : null,
806
+ finalPose: this._finalPose ? clonePose(this._finalPose) : null,
807
+ activeBehaviors: Array.from(this.behaviors.keys()),
808
+ activeActionCount: this.actions.length
809
+ };
810
+ }
811
+ /**
812
+ * Set a new perspective. Resets the pipeline initialization flag so the
813
+ * first frame with the new perspective snaps instead of lerping from the old pose.
814
+ */
815
+ setPerspective(perspective) {
816
+ this.perspective = perspective;
817
+ this._initialized = false;
818
+ if (perspective.defaults?.damping != null) {
819
+ this.damping = perspective.defaults.damping;
820
+ }
821
+ }
822
+ // ─── Internal helpers ──────────────────────────────────────────────────
823
+ getSortedBehaviors() {
824
+ return Array.from(this.behaviors.values()).sort(
825
+ (a, b) => (a.priority ?? 0) - (b.priority ?? 0)
826
+ );
827
+ }
828
+ };
829
+
830
+ // src/lib/camera/perspectives/third-person-perspective.ts
831
+ import { Vector3 as Vector33, Quaternion as Quaternion3 } from "three";
832
+ var DEFAULTS = {
833
+ distance: 8,
834
+ height: 5,
835
+ shoulderOffset: 0,
836
+ targetKey: "primary",
837
+ fov: 75,
838
+ paddingFactor: 1.5,
839
+ minDistance: 5
840
+ };
841
+ var ThirdPersonPerspective = class {
842
+ id = "third-person";
843
+ defaults = { damping: 0.15 };
844
+ opts;
845
+ initialPosition;
846
+ initialLookAt;
847
+ constructor(options) {
848
+ const { initialPosition, initialLookAt, ...rest } = options ?? {};
849
+ this.opts = { ...DEFAULTS, ...rest };
850
+ this.initialPosition = initialPosition;
851
+ this.initialLookAt = initialLookAt;
852
+ }
853
+ getBasePose(ctx) {
854
+ const targetKeys = Object.keys(ctx.targets);
855
+ const primary = ctx.targets[this.opts.targetKey];
856
+ if (targetKeys.length === 0 || !primary) {
857
+ return this.staticPose();
858
+ }
859
+ if (targetKeys.length === 1) {
860
+ return this.singleTargetPose(primary.position);
861
+ }
862
+ return this.multiTargetPose(ctx);
863
+ }
864
+ /**
865
+ * No targets: use the user-specified initial position if available,
866
+ * otherwise fall back to a default pose behind the origin.
867
+ */
868
+ staticPose() {
869
+ if (this.initialPosition) {
870
+ return {
871
+ position: this.initialPosition.clone(),
872
+ rotation: new Quaternion3(),
873
+ fov: this.opts.fov,
874
+ zoom: 1,
875
+ near: 0.1,
876
+ far: 1e3,
877
+ lookAt: this.initialLookAt?.clone() ?? new Vector33(0, 0, 0)
878
+ };
879
+ }
880
+ const position = new Vector33(
881
+ this.opts.shoulderOffset,
882
+ this.opts.height,
883
+ this.opts.distance
884
+ );
885
+ return {
886
+ position,
887
+ rotation: new Quaternion3(),
888
+ fov: this.opts.fov,
889
+ zoom: 1,
890
+ near: 0.1,
891
+ far: 1e3,
892
+ lookAt: new Vector33(0, 0, 0)
893
+ };
894
+ }
895
+ /**
896
+ * Single target: position = target + offset, lookAt = target.
897
+ */
898
+ singleTargetPose(targetPos) {
899
+ const position = new Vector33(
900
+ targetPos.x + this.opts.shoulderOffset,
901
+ targetPos.y + this.opts.height,
902
+ targetPos.z + this.opts.distance
903
+ );
904
+ return {
905
+ position,
906
+ rotation: new Quaternion3(),
907
+ fov: this.opts.fov,
908
+ zoom: 1,
909
+ near: 0.1,
910
+ far: 1e3,
911
+ lookAt: targetPos.clone()
912
+ };
913
+ }
914
+ /**
915
+ * Multi-target: compute centroid and adjust distance to frame all targets.
916
+ */
917
+ multiTargetPose(ctx) {
918
+ const targets = Object.values(ctx.targets);
919
+ const centroid = new Vector33();
920
+ for (const t of targets) {
921
+ centroid.add(t.position);
922
+ }
923
+ centroid.divideScalar(targets.length);
924
+ let maxDist = 0;
925
+ for (const t of targets) {
926
+ const d = centroid.distanceTo(t.position);
927
+ if (d > maxDist) maxDist = d;
928
+ }
929
+ const dynamicDistance = Math.max(
930
+ maxDist * this.opts.paddingFactor,
931
+ this.opts.minDistance
932
+ );
933
+ const baseOffset = new Vector33(
934
+ this.opts.shoulderOffset,
935
+ this.opts.height,
936
+ this.opts.distance
937
+ );
938
+ const baseLen = baseOffset.length();
939
+ const dir = baseLen > 0 ? baseOffset.clone().normalize() : new Vector33(0, 0.5, 1).normalize();
940
+ const position = centroid.clone().add(dir.multiplyScalar(dynamicDistance));
941
+ if (baseLen > 0) {
942
+ const heightRatio = this.opts.height / baseLen;
943
+ position.y = centroid.y + dynamicDistance * heightRatio;
944
+ }
945
+ return {
946
+ position,
947
+ rotation: new Quaternion3(),
948
+ fov: this.opts.fov,
949
+ zoom: 1,
950
+ near: 0.1,
951
+ far: 1e3,
952
+ lookAt: centroid.clone()
953
+ };
954
+ }
955
+ };
956
+
957
+ // src/lib/camera/perspectives/fixed-2d-perspective.ts
958
+ import { Vector3 as Vector34, Quaternion as Quaternion4 } from "three";
959
+ var DEFAULTS2 = {
960
+ position: { x: 0, y: 0, z: 10 },
961
+ zoom: 10
962
+ };
963
+ var Fixed2DPerspective = class {
964
+ id = "fixed-2d";
965
+ defaults = { damping: 1 };
966
+ opts;
967
+ constructor(options) {
968
+ this.opts = { ...DEFAULTS2, ...options };
969
+ }
970
+ getBasePose(_ctx) {
971
+ return {
972
+ position: new Vector34(
973
+ this.opts.position.x,
974
+ this.opts.position.y,
975
+ this.opts.position.z
976
+ ),
977
+ rotation: new Quaternion4(),
978
+ zoom: this.opts.zoom,
979
+ near: 1,
980
+ far: 1e3,
981
+ lookAt: new Vector34(this.opts.position.x, this.opts.position.y, 0)
982
+ };
983
+ }
984
+ };
985
+
986
+ // src/lib/camera/perspectives/first-person-perspective.ts
987
+ import { Vector3 as Vector35, Quaternion as Quaternion5, Euler, MathUtils as MathUtils2 } from "three";
988
+ var DEFAULTS3 = {
989
+ eyeHeight: 1.7,
990
+ defaultFov: 75,
991
+ pitchLimit: Math.PI / 2 - 0.01,
992
+ lookAtLerpSpeed: 5,
993
+ fovLerpSpeed: 8,
994
+ targetKey: "primary"
995
+ };
996
+ var FirstPersonPerspective = class {
997
+ id = "first-person";
998
+ defaults = { damping: 1 };
999
+ opts;
1000
+ /** Fallback position when no target entity is attached. Mutate directly for manual movement. */
1001
+ initialPosition;
1002
+ initialLookAt;
1003
+ // --- Look state ---
1004
+ _yaw = 0;
1005
+ _pitch = 0;
1006
+ // --- FOV zoom state ---
1007
+ _currentFov;
1008
+ _targetFov;
1009
+ // --- Look-at target state ---
1010
+ _lookAtTarget = null;
1011
+ _lookAtLerpSpeed;
1012
+ _currentRotation = new Quaternion5();
1013
+ _rotationInitialized = false;
1014
+ constructor(options) {
1015
+ const { initialPosition, initialLookAt, ...rest } = options ?? {};
1016
+ this.opts = { ...DEFAULTS3, ...rest };
1017
+ this.initialPosition = initialPosition;
1018
+ this.initialLookAt = initialLookAt;
1019
+ this._currentFov = this.opts.defaultFov;
1020
+ this._targetFov = this.opts.defaultFov;
1021
+ this._lookAtLerpSpeed = this.opts.lookAtLerpSpeed;
1022
+ if (initialPosition && initialLookAt) {
1023
+ this.deriveYawPitchFromLookAt(initialPosition, initialLookAt);
1024
+ }
1025
+ }
1026
+ // --- Public API (called by game code / FPS behavior) ---
1027
+ /** Accumulate yaw and pitch deltas. Pitch is clamped to the configured limit. */
1028
+ look(deltaYaw, deltaPitch) {
1029
+ this._yaw += deltaYaw;
1030
+ this._pitch = MathUtils2.clamp(
1031
+ this._pitch + deltaPitch,
1032
+ -this.opts.pitchLimit,
1033
+ this.opts.pitchLimit
1034
+ );
1035
+ }
1036
+ /** Set absolute yaw and pitch. Pitch is clamped to the configured limit. */
1037
+ setLook(yaw, pitch) {
1038
+ this._yaw = yaw;
1039
+ this._pitch = MathUtils2.clamp(pitch, -this.opts.pitchLimit, this.opts.pitchLimit);
1040
+ }
1041
+ /** Current yaw in radians. */
1042
+ get yaw() {
1043
+ return this._yaw;
1044
+ }
1045
+ /** Current pitch in radians. */
1046
+ get pitch() {
1047
+ return this._pitch;
1048
+ }
1049
+ /** Set the target FOV for a smooth zoom transition (e.g. sniper scope). */
1050
+ zoom(fov) {
1051
+ this._targetFov = fov;
1052
+ }
1053
+ /** Return to the default FOV. */
1054
+ resetZoom() {
1055
+ this._targetFov = this.opts.defaultFov;
1056
+ }
1057
+ /** Current field of view. */
1058
+ get currentFov() {
1059
+ return this._currentFov;
1060
+ }
1061
+ /**
1062
+ * Enable smooth look-at toward a world position.
1063
+ * The camera will slerp from the current rotation toward the look-at direction.
1064
+ */
1065
+ lookAt(target, lerpSpeed) {
1066
+ this._lookAtTarget = target;
1067
+ if (lerpSpeed != null) {
1068
+ this._lookAtLerpSpeed = lerpSpeed;
1069
+ }
1070
+ }
1071
+ /** Disable look-at and return to manual yaw/pitch control. */
1072
+ clearLookAt() {
1073
+ if (this._lookAtTarget) {
1074
+ this.deriveYawPitchFromQuaternion(this._currentRotation);
1075
+ }
1076
+ this._lookAtTarget = null;
1077
+ }
1078
+ // --- CameraPerspective interface ---
1079
+ getBasePose(ctx) {
1080
+ const position = this.computePosition(ctx);
1081
+ const rotation = this.computeRotation(position, ctx.dt);
1082
+ this._currentFov = this.lerpFov(ctx.dt);
1083
+ return {
1084
+ position,
1085
+ rotation,
1086
+ fov: this._currentFov,
1087
+ near: 0.1,
1088
+ far: 1e3
1089
+ };
1090
+ }
1091
+ // --- Private helpers ---
1092
+ computePosition(ctx) {
1093
+ const target = ctx.targets[this.opts.targetKey];
1094
+ if (target) {
1095
+ return new Vector35(
1096
+ target.position.x,
1097
+ target.position.y + this.opts.eyeHeight,
1098
+ target.position.z
1099
+ );
1100
+ }
1101
+ if (this.initialPosition) {
1102
+ return this.initialPosition.clone();
1103
+ }
1104
+ return new Vector35(0, this.opts.eyeHeight, 0);
1105
+ }
1106
+ computeRotation(eyePosition, dt) {
1107
+ const yawPitchQuat = new Quaternion5().setFromEuler(
1108
+ new Euler(this._pitch, this._yaw, 0, "YXZ")
1109
+ );
1110
+ if (!this._rotationInitialized) {
1111
+ this._currentRotation.copy(yawPitchQuat);
1112
+ this._rotationInitialized = true;
1113
+ }
1114
+ if (this._lookAtTarget) {
1115
+ const dir = _vec3.copy(this._lookAtTarget).sub(eyePosition);
1116
+ if (dir.lengthSq() > 1e-4) {
1117
+ dir.normalize();
1118
+ const desiredYaw = Math.atan2(-dir.x, -dir.z);
1119
+ const desiredPitch = Math.asin(MathUtils2.clamp(dir.y, -1, 1));
1120
+ const desiredQuat = _quat.setFromEuler(
1121
+ _euler.set(desiredPitch, desiredYaw, 0, "YXZ")
1122
+ );
1123
+ const t = 1 - Math.pow(1 - Math.min(this._lookAtLerpSpeed * dt, 1), 1);
1124
+ this._currentRotation.slerp(desiredQuat, MathUtils2.clamp(t, 0, 1));
1125
+ return this._currentRotation.clone();
1126
+ }
1127
+ }
1128
+ this._currentRotation.copy(yawPitchQuat);
1129
+ return yawPitchQuat;
1130
+ }
1131
+ lerpFov(dt) {
1132
+ if (Math.abs(this._currentFov - this._targetFov) < 0.01) {
1133
+ return this._targetFov;
1134
+ }
1135
+ const t = 1 - Math.pow(1 - Math.min(this.opts.fovLerpSpeed * dt, 1), 1);
1136
+ return MathUtils2.lerp(this._currentFov, this._targetFov, MathUtils2.clamp(t, 0, 1));
1137
+ }
1138
+ deriveYawPitchFromLookAt(from, to) {
1139
+ const dir = _vec3.copy(to).sub(from).normalize();
1140
+ this._yaw = Math.atan2(-dir.x, -dir.z);
1141
+ this._pitch = Math.asin(MathUtils2.clamp(dir.y, -1, 1));
1142
+ }
1143
+ deriveYawPitchFromQuaternion(q) {
1144
+ const euler = _euler.setFromQuaternion(q, "YXZ");
1145
+ this._yaw = euler.y;
1146
+ this._pitch = MathUtils2.clamp(euler.x, -this.opts.pitchLimit, this.opts.pitchLimit);
1147
+ }
1148
+ };
1149
+ var _vec3 = new Vector35();
1150
+ var _quat = new Quaternion5();
1151
+ var _euler = new Euler();
1152
+
1153
+ // src/lib/camera/perspectives/index.ts
1154
+ function createPerspective(type, options) {
1155
+ switch (type) {
1156
+ case "third-person":
1157
+ return new ThirdPersonPerspective(options);
1158
+ case "first-person":
1159
+ return new FirstPersonPerspective(options);
1160
+ case "isometric":
1161
+ return new ThirdPersonPerspective({
1162
+ distance: 10,
1163
+ height: 10,
1164
+ ...options
1165
+ });
1166
+ case "flat-2d":
1167
+ return new Fixed2DPerspective(options);
1168
+ case "fixed-2d":
1169
+ return new Fixed2DPerspective(options);
1170
+ default:
1171
+ return new ThirdPersonPerspective(options);
1172
+ }
1173
+ }
1174
+
1175
+ // src/lib/camera/zylem-camera.ts
1176
+ var ZylemCamera = class {
1177
+ /**
1178
+ * @deprecated No longer used. Kept as null for backward compatibility
1179
+ * with code that checks `camera.cameraRig` (e.g. scene graph insertion).
1180
+ */
1181
+ cameraRig = null;
1182
+ camera;
1183
+ screenResolution;
1184
+ _perspective;
1185
+ frustumSize = 10;
1186
+ rendererType;
1187
+ sceneRef = null;
1188
+ /** Name for camera manager lookup. */
1189
+ name = "";
1190
+ /**
1191
+ * Viewport in normalized coordinates (0-1).
1192
+ * Default is fullscreen: { x: 0, y: 0, width: 1, height: 1 }
1193
+ */
1194
+ viewport = { ...DEFAULT_VIEWPORT };
1195
+ /**
1196
+ * Multiple targets for the camera to follow/frame.
1197
+ */
1198
+ targets = [];
1199
+ /**
1200
+ * The camera pose pipeline.
1201
+ * Exposed so CameraWrapper can delegate addBehavior/addAction/getState.
1202
+ */
1203
+ pipeline;
1204
+ /**
1205
+ * Offscreen render target for render-to-texture (RTT) cameras.
1206
+ * When set, the camera renders to this target instead of the screen viewport.
1207
+ * Created via createRenderTarget() or automatically by setCameraFeed().
1208
+ */
1209
+ renderTarget = null;
1210
+ /**
1211
+ * @deprecated Use `targets` array instead. Kept for backward compatibility.
1212
+ */
1213
+ get target() {
1214
+ return this.targets.length > 0 ? this.targets[0] : null;
1215
+ }
1216
+ set target(entity) {
1217
+ if (entity) {
1218
+ if (this.targets.length === 0) {
1219
+ this.targets.push(entity);
1220
+ } else {
1221
+ this.targets[0] = entity;
1222
+ }
1223
+ } else {
1224
+ this.targets = [];
1225
+ }
1226
+ }
1227
+ // Orbit controls
1228
+ orbitController = null;
1229
+ _useOrbitalControls = false;
1230
+ /** When true, debug-mode orbital controls are not attached to this camera. */
1231
+ _skipDebugOrbit = false;
1232
+ /** Reference to the shared renderer manager (set during setup). */
1233
+ _rendererManager = null;
1234
+ /** Elapsed time tracker for CameraContext. */
1235
+ _elapsedTime = 0;
1236
+ constructor(perspective, screenResolution, frustumSize = 10, rendererType = "webgl") {
1237
+ this._perspective = perspective;
1238
+ this.screenResolution = screenResolution;
1239
+ this.frustumSize = frustumSize;
1240
+ this.rendererType = rendererType;
1241
+ const aspectRatio = screenResolution.x / screenResolution.y;
1242
+ this.camera = this.createCameraForPerspective(aspectRatio);
1243
+ this.camera.position.set(0, 0, 10);
1244
+ this.camera.lookAt(new Vector36(0, 0, 0));
1245
+ const perspectiveImpl = createPerspective(perspective);
1246
+ this.pipeline = new CameraPipeline(perspectiveImpl);
1247
+ }
1248
+ /**
1249
+ * Setup the camera with a scene and renderer manager.
1250
+ */
1251
+ async setup(scene, rendererManager) {
1252
+ this.sceneRef = scene;
1253
+ if (rendererManager) {
1254
+ this._rendererManager = rendererManager;
1255
+ }
1256
+ if (this._rendererManager && !this._rendererManager.initialized) {
1257
+ await this._rendererManager.initRenderer();
1258
+ }
1259
+ if (this._rendererManager) {
1260
+ this.orbitController = new CameraOrbitController(
1261
+ this.camera,
1262
+ this._rendererManager.renderer.domElement,
1263
+ null
1264
+ // no camera rig
1265
+ );
1266
+ this.orbitController.setScene(scene);
1267
+ if (this._useOrbitalControls) {
1268
+ this.orbitController.enableUserOrbitControls();
1269
+ }
1270
+ }
1271
+ }
1272
+ /**
1273
+ * Legacy setup method for backward compatibility.
1274
+ * Creates a temporary RendererManager internally.
1275
+ * @deprecated Use setup(scene, rendererManager) instead.
1276
+ */
1277
+ async setupLegacy(scene) {
1278
+ if (!this._rendererManager) {
1279
+ this._rendererManager = new RendererManager(this.screenResolution, this.rendererType);
1280
+ await this._rendererManager.initRenderer();
1281
+ this._rendererManager.setupRenderPass(scene, this.camera);
1282
+ this._rendererManager.startRenderLoop((delta) => {
1283
+ this.update(delta);
1284
+ if (this._rendererManager && this.sceneRef) {
1285
+ this._rendererManager.render(this.sceneRef, this.camera);
1286
+ }
1287
+ });
1288
+ }
1289
+ await this.setup(scene, this._rendererManager);
1290
+ }
1291
+ /**
1292
+ * Update the camera each frame.
1293
+ *
1294
+ * When orbit/debug controls are active, the pipeline is skipped and
1295
+ * orbit controls manage the camera directly. Otherwise, the pipeline
1296
+ * runs: Perspective -> Behaviors -> Actions -> Smoothing -> Commit.
1297
+ */
1298
+ update(delta) {
1299
+ this.orbitController?.update();
1300
+ if (this.isDebugModeActive() || this._useOrbitalControls) {
1301
+ return;
533
1302
  }
534
- this.composer.render(delta);
1303
+ this._elapsedTime += delta;
1304
+ const ctx = this.buildContext(delta);
1305
+ const finalPose = this.pipeline.run(ctx);
1306
+ this.commitPose(finalPose);
535
1307
  }
536
1308
  /**
537
- * Check if debug mode is active (orbit controls taking over camera)
1309
+ * Check if debug mode is active (orbit controls taking over camera).
538
1310
  */
539
1311
  isDebugModeActive() {
540
1312
  return this.orbitController?.isActive ?? false;
541
1313
  }
542
1314
  /**
543
- * Dispose renderer, composer, controls, and detach from scene
1315
+ * Enable user-configured orbital controls (not debug mode).
544
1316
  */
545
- destroy() {
546
- try {
547
- this.renderer.setAnimationLoop(null);
548
- } catch {
1317
+ enableOrbitalControls() {
1318
+ this._useOrbitalControls = true;
1319
+ this.orbitController?.enableUserOrbitControls();
1320
+ }
1321
+ /**
1322
+ * Disable user-configured orbital controls.
1323
+ */
1324
+ disableOrbitalControls() {
1325
+ this._useOrbitalControls = false;
1326
+ this.orbitController?.disableUserOrbitControls();
1327
+ }
1328
+ /**
1329
+ * Whether user orbital controls are enabled.
1330
+ */
1331
+ get useOrbitalControls() {
1332
+ return this._useOrbitalControls;
1333
+ }
1334
+ // ─── Target management ──────────────────────────────────────────────────
1335
+ /**
1336
+ * Add a target entity for the camera to follow/frame.
1337
+ */
1338
+ addTarget(entity) {
1339
+ if (!this.targets.includes(entity)) {
1340
+ this.targets.push(entity);
549
1341
  }
550
- try {
551
- this.orbitController?.dispose();
552
- } catch {
1342
+ }
1343
+ /**
1344
+ * Remove a target entity.
1345
+ */
1346
+ removeTarget(entity) {
1347
+ const index = this.targets.indexOf(entity);
1348
+ if (index !== -1) {
1349
+ this.targets.splice(index, 1);
1350
+ }
1351
+ }
1352
+ /**
1353
+ * Clear all targets.
1354
+ */
1355
+ clearTargets() {
1356
+ this.targets = [];
1357
+ }
1358
+ // ─── Viewport & resize ──────────────────────────────────────────────────
1359
+ /**
1360
+ * Set the viewport for this camera (normalized 0-1 coordinates).
1361
+ */
1362
+ setViewport(x, y, width, height) {
1363
+ this.viewport = { x, y, width, height };
1364
+ }
1365
+ /**
1366
+ * Resize camera projection.
1367
+ */
1368
+ resize(width, height) {
1369
+ this.screenResolution.set(width, height);
1370
+ if (this.camera instanceof PerspectiveCamera2) {
1371
+ this.camera.aspect = width / height;
1372
+ this.camera.updateProjectionMatrix();
1373
+ }
1374
+ if (this.camera instanceof OrthographicCamera) {
1375
+ const aspect = width / height;
1376
+ this.camera.left = this.frustumSize * aspect / -2;
1377
+ this.camera.right = this.frustumSize * aspect / 2;
1378
+ this.camera.top = this.frustumSize / 2;
1379
+ this.camera.bottom = this.frustumSize / -2;
1380
+ this.camera.updateProjectionMatrix();
1381
+ }
1382
+ }
1383
+ // ─── Render-to-texture ─────────────────────────────────────────────────
1384
+ /**
1385
+ * Create an offscreen render target for this camera.
1386
+ * When a render target is present, CameraManager renders this camera
1387
+ * to the target instead of the screen viewport.
1388
+ *
1389
+ * @param width Texture width in pixels (default 512)
1390
+ * @param height Texture height in pixels (default 512)
1391
+ */
1392
+ createRenderTarget(width = 512, height = 512) {
1393
+ if (this.renderTarget) {
1394
+ this.renderTarget.dispose();
553
1395
  }
1396
+ this.renderTarget = new WebGLRenderTarget2(width, height, {
1397
+ minFilter: LinearFilter,
1398
+ magFilter: LinearFilter
1399
+ });
1400
+ return this.renderTarget;
1401
+ }
1402
+ /**
1403
+ * Get the texture from the render target (for applying to a mesh material).
1404
+ * Returns null if no render target has been created.
1405
+ */
1406
+ getRenderTexture() {
1407
+ return this.renderTarget?.texture ?? null;
1408
+ }
1409
+ // ─── Lifecycle ──────────────────────────────────────────────────────────
1410
+ /**
1411
+ * Dispose camera resources.
1412
+ */
1413
+ destroy() {
554
1414
  try {
555
- this.composer?.passes?.forEach((p) => p.dispose?.());
556
- this.composer?.dispose?.();
1415
+ this.orbitController?.dispose();
557
1416
  } catch {
558
1417
  }
559
1418
  try {
560
- this.renderer.dispose();
1419
+ this.renderTarget?.dispose();
561
1420
  } catch {
562
1421
  }
1422
+ this.renderTarget = null;
563
1423
  this.sceneRef = null;
1424
+ this.targets = [];
1425
+ this._rendererManager = null;
564
1426
  }
1427
+ // ─── Debug delegate ─────────────────────────────────────────────────────
565
1428
  /**
566
1429
  * Attach a delegate to react to debug state changes.
1430
+ * Skipped when _skipDebugOrbit is true so the pipeline always runs.
567
1431
  */
568
1432
  setDebugDelegate(delegate) {
1433
+ if (this._skipDebugOrbit) return;
569
1434
  this.orbitController?.setDebugDelegate(delegate);
570
1435
  }
1436
+ // ─── Movement helpers (backward compat) ─────────────────────────────────
571
1437
  /**
572
- * Resize camera and renderer
1438
+ * Directly set the camera position.
573
1439
  */
574
- resize(width, height) {
575
- this.screenResolution.set(width, height);
576
- this.renderer.setSize(width, height, false);
577
- this.composer.setSize(width, height);
578
- if (this.camera instanceof PerspectiveCamera) {
579
- this.camera.aspect = width / height;
580
- this.camera.updateProjectionMatrix();
1440
+ move(position) {
1441
+ if (this._perspective === Perspectives.Flat2D || this._perspective === Perspectives.Fixed2D) {
1442
+ this.frustumSize = position.z;
581
1443
  }
582
- if (this.perspectiveController) {
583
- this.perspectiveController.resize(width, height);
1444
+ this.camera.position.set(position.x, position.y, position.z);
1445
+ }
1446
+ /**
1447
+ * Apply incremental rotation to the camera.
1448
+ */
1449
+ rotate(pitch, yaw, roll) {
1450
+ this.camera.rotateX(pitch);
1451
+ this.camera.rotateY(yaw);
1452
+ this.camera.rotateZ(roll);
1453
+ }
1454
+ // ─── Renderer manager access ────────────────────────────────────────────
1455
+ /**
1456
+ * Get the DOM element for the renderer.
1457
+ * @deprecated Access via RendererManager instead.
1458
+ */
1459
+ getDomElement() {
1460
+ if (this._rendererManager) {
1461
+ return this._rendererManager.getDomElement();
584
1462
  }
1463
+ throw new Error("ZylemCamera: No renderer manager available. Call setup() first.");
1464
+ }
1465
+ /**
1466
+ * Get the renderer manager reference.
1467
+ */
1468
+ getRendererManager() {
1469
+ return this._rendererManager;
585
1470
  }
586
1471
  /**
587
- * Update renderer pixel ratio (DPR)
1472
+ * Set the renderer manager reference (used by CameraManager during setup).
588
1473
  */
1474
+ setRendererManager(manager) {
1475
+ this._rendererManager = manager;
1476
+ }
1477
+ // ─── Legacy compatibility methods ────────────────────────────────────────
1478
+ /** @deprecated Renderer is now owned by RendererManager */
1479
+ get renderer() {
1480
+ return this._rendererManager?.renderer;
1481
+ }
1482
+ /** @deprecated Composer is now owned by RendererManager */
1483
+ get composer() {
1484
+ return this._rendererManager?.composer;
1485
+ }
1486
+ /** @deprecated Use RendererManager.setPixelRatio() instead */
589
1487
  setPixelRatio(dpr) {
590
- const safe = Math.max(1, Number.isFinite(dpr) ? dpr : 1);
591
- this.renderer.setPixelRatio(safe);
1488
+ this._rendererManager?.setPixelRatio(dpr);
1489
+ }
1490
+ // ─── Private helpers ────────────────────────────────────────────────────
1491
+ /**
1492
+ * Build a CameraContext from current ZylemCamera state.
1493
+ * Converts StageEntity[] targets into Record<string, TransformLike>.
1494
+ */
1495
+ buildContext(delta) {
1496
+ const targets = {};
1497
+ for (let i = 0; i < this.targets.length; i++) {
1498
+ const entity = this.targets[i];
1499
+ const key = i === 0 ? "primary" : `target_${i}`;
1500
+ if (entity.group) {
1501
+ targets[key] = {
1502
+ position: entity.group.position,
1503
+ rotation: entity.group.quaternion
1504
+ };
1505
+ }
1506
+ }
1507
+ return {
1508
+ dt: delta,
1509
+ time: this._elapsedTime,
1510
+ viewport: {
1511
+ width: this.screenResolution.x,
1512
+ height: this.screenResolution.y,
1513
+ aspect: this.screenResolution.x / this.screenResolution.y
1514
+ },
1515
+ targets
1516
+ };
1517
+ }
1518
+ /**
1519
+ * Apply the final pipeline pose to the Three.js camera.
1520
+ */
1521
+ commitPose(pose) {
1522
+ this.camera.position.copy(pose.position);
1523
+ if (pose.lookAt) {
1524
+ this.camera.lookAt(pose.lookAt);
1525
+ } else {
1526
+ this.camera.quaternion.copy(pose.rotation);
1527
+ }
1528
+ if (this.camera instanceof PerspectiveCamera2) {
1529
+ if (pose.fov != null) this.camera.fov = pose.fov;
1530
+ if (pose.near != null) this.camera.near = pose.near;
1531
+ if (pose.far != null) this.camera.far = pose.far;
1532
+ this.camera.updateProjectionMatrix();
1533
+ }
1534
+ if (this.camera instanceof OrthographicCamera) {
1535
+ if (pose.zoom != null) {
1536
+ const aspect = this.screenResolution.x / this.screenResolution.y;
1537
+ const size = pose.zoom;
1538
+ this.camera.left = -size * aspect / 2;
1539
+ this.camera.right = size * aspect / 2;
1540
+ this.camera.top = size / 2;
1541
+ this.camera.bottom = -size / 2;
1542
+ }
1543
+ if (pose.near != null) this.camera.near = pose.near;
1544
+ if (pose.far != null) this.camera.far = pose.far;
1545
+ this.camera.updateProjectionMatrix();
1546
+ }
592
1547
  }
593
1548
  /**
594
- * Create camera based on perspective type
1549
+ * Create a Three.js camera based on perspective type.
595
1550
  */
596
1551
  createCameraForPerspective(aspectRatio) {
597
1552
  switch (this._perspective) {
598
1553
  case Perspectives.ThirdPerson:
599
- return this.createThirdPersonCamera(aspectRatio);
1554
+ return new PerspectiveCamera2(75, aspectRatio, 0.1, 1e3);
600
1555
  case Perspectives.FirstPerson:
601
- return this.createFirstPersonCamera(aspectRatio);
1556
+ return new PerspectiveCamera2(75, aspectRatio, 0.1, 1e3);
602
1557
  case Perspectives.Isometric:
603
- return this.createIsometricCamera(aspectRatio);
1558
+ return new OrthographicCamera(
1559
+ this.frustumSize * aspectRatio / -2,
1560
+ this.frustumSize * aspectRatio / 2,
1561
+ this.frustumSize / 2,
1562
+ this.frustumSize / -2,
1563
+ 1,
1564
+ 1e3
1565
+ );
604
1566
  case Perspectives.Flat2D:
605
- return this.createFlat2DCamera(aspectRatio);
606
1567
  case Perspectives.Fixed2D:
607
- return this.createFixed2DCamera(aspectRatio);
1568
+ return new OrthographicCamera(
1569
+ this.frustumSize * aspectRatio / -2,
1570
+ this.frustumSize * aspectRatio / 2,
1571
+ this.frustumSize / 2,
1572
+ this.frustumSize / -2,
1573
+ 1,
1574
+ 1e3
1575
+ );
608
1576
  default:
609
- return this.createThirdPersonCamera(aspectRatio);
1577
+ return new PerspectiveCamera2(75, aspectRatio, 0.1, 1e3);
610
1578
  }
611
1579
  }
1580
+ };
1581
+
1582
+ // src/lib/camera/camera.ts
1583
+ var CameraWrapper = class {
1584
+ cameraRef;
1585
+ constructor(camera) {
1586
+ this.cameraRef = camera;
1587
+ }
1588
+ // ─── Target management ──────────────────────────────────────────────────
612
1589
  /**
613
- * Initialize perspective-specific controller
1590
+ * Add a target entity for the camera to follow/frame.
1591
+ * With multiple targets, the camera auto-frames to include all of them.
614
1592
  */
615
- initializePerspectiveController() {
616
- switch (this._perspective) {
617
- case Perspectives.ThirdPerson:
618
- this.perspectiveController = new ThirdPersonCamera();
619
- break;
620
- case Perspectives.Fixed2D:
621
- this.perspectiveController = new Fixed2DCamera();
622
- break;
623
- default:
624
- this.perspectiveController = new ThirdPersonCamera();
625
- }
1593
+ addTarget(entity) {
1594
+ this.cameraRef.addTarget(entity);
1595
+ }
1596
+ /**
1597
+ * Remove a target entity from the camera.
1598
+ */
1599
+ removeTarget(entity) {
1600
+ this.cameraRef.removeTarget(entity);
626
1601
  }
627
- createThirdPersonCamera(aspectRatio) {
628
- return new PerspectiveCamera(75, aspectRatio, 0.1, 1e3);
1602
+ /**
1603
+ * Clear all targets. Camera will look at world origin.
1604
+ */
1605
+ clearTargets() {
1606
+ this.cameraRef.clearTargets();
629
1607
  }
630
- createFirstPersonCamera(aspectRatio) {
631
- return new PerspectiveCamera(75, aspectRatio, 0.1, 1e3);
1608
+ // ─── Orbital controls ───────────────────────────────────────────────────
1609
+ /**
1610
+ * Enable orbital controls for this camera.
1611
+ * Allows the user to orbit, pan, and zoom the camera.
1612
+ */
1613
+ enableOrbitalControls() {
1614
+ this.cameraRef.enableOrbitalControls();
632
1615
  }
633
- createIsometricCamera(aspectRatio) {
634
- return new OrthographicCamera(
635
- this.frustumSize * aspectRatio / -2,
636
- this.frustumSize * aspectRatio / 2,
637
- this.frustumSize / 2,
638
- this.frustumSize / -2,
639
- 1,
640
- 1e3
641
- );
1616
+ /**
1617
+ * Disable orbital controls for this camera.
1618
+ */
1619
+ disableOrbitalControls() {
1620
+ this.cameraRef.disableOrbitalControls();
642
1621
  }
643
- createFlat2DCamera(aspectRatio) {
644
- return new OrthographicCamera(
645
- this.frustumSize * aspectRatio / -2,
646
- this.frustumSize * aspectRatio / 2,
647
- this.frustumSize / 2,
648
- this.frustumSize / -2,
649
- 1,
650
- 1e3
651
- );
1622
+ // ─── Viewport ───────────────────────────────────────────────────────────
1623
+ /**
1624
+ * Set the viewport for this camera (normalized 0-1 coordinates).
1625
+ * @param x Left edge (0 = left of canvas)
1626
+ * @param y Bottom edge (0 = bottom of canvas)
1627
+ * @param width Width as fraction of canvas
1628
+ * @param height Height as fraction of canvas
1629
+ */
1630
+ setViewport(x, y, width, height) {
1631
+ this.cameraRef.setViewport(x, y, width, height);
652
1632
  }
653
- createFixed2DCamera(aspectRatio) {
654
- return this.createFlat2DCamera(aspectRatio);
1633
+ // ─── Pipeline: Behaviors ────────────────────────────────────────────────
1634
+ /**
1635
+ * Add or replace a behavior by key (idempotent).
1636
+ * Behaviors modify the desired camera pose each frame.
1637
+ *
1638
+ * @param key Unique key for this behavior (used for replacement/removal).
1639
+ * @param behavior The CameraBehavior implementation.
1640
+ */
1641
+ addBehavior(key, behavior) {
1642
+ this.cameraRef.pipeline.addBehavior(key, behavior);
655
1643
  }
656
- // Movement methods
657
- moveCamera(position) {
658
- if (this._perspective === Perspectives.Flat2D || this._perspective === Perspectives.Fixed2D) {
659
- this.frustumSize = position.z;
660
- }
661
- if (this.cameraRig) {
662
- this.cameraRig.position.set(position.x, position.y, position.z);
663
- } else {
664
- this.camera.position.set(position.x, position.y, position.z);
665
- }
1644
+ /**
1645
+ * Remove a behavior by key.
1646
+ */
1647
+ removeBehavior(key) {
1648
+ return this.cameraRef.pipeline.removeBehavior(key);
666
1649
  }
667
- move(position) {
668
- this.moveCamera(position);
1650
+ // ─── Pipeline: Actions ──────────────────────────────────────────────────
1651
+ /**
1652
+ * Add a transient action (screenshake, recoil, etc.).
1653
+ * Actions apply additive deltas and self-expire when isDone() returns true.
1654
+ */
1655
+ addAction(action) {
1656
+ this.cameraRef.pipeline.addAction(action);
669
1657
  }
670
- rotate(pitch, yaw, roll) {
671
- if (this.cameraRig) {
672
- this.cameraRig.rotateX(pitch);
673
- this.cameraRig.rotateY(yaw);
674
- this.cameraRig.rotateZ(roll);
675
- } else {
676
- this.camera.rotateX(pitch);
677
- this.camera.rotateY(yaw);
678
- this.camera.rotateZ(roll);
679
- }
1658
+ // ─── Pipeline: Perspective ──────────────────────────────────────────────
1659
+ /**
1660
+ * Switch the camera's active perspective at runtime.
1661
+ * The first frame after switching snaps to the new pose (no lerp).
1662
+ *
1663
+ * @param type Perspective type string (e.g. Perspectives.ThirdPerson).
1664
+ * @param options Perspective-specific options (distance, height, zoom, etc.).
1665
+ */
1666
+ setPerspective(type, options) {
1667
+ this.cameraRef.pipeline.setPerspective(createPerspective(type, options));
680
1668
  }
681
1669
  /**
682
- * Check if this perspective type needs a camera rig
1670
+ * Retrieve the active perspective instance, cast to the desired type.
1671
+ * Useful for calling perspective-specific methods (e.g. FirstPersonPerspective.look()).
1672
+ *
1673
+ * @example
1674
+ * const fps = camera.getPerspective<FirstPersonPerspective>();
1675
+ * fps.look(dx, dy);
683
1676
  */
684
- needsRig() {
685
- return this._perspective === Perspectives.ThirdPerson;
1677
+ getPerspective() {
1678
+ return this.cameraRef.pipeline.perspective;
686
1679
  }
1680
+ // ─── Pipeline: Debug state ──────────────────────────────────────────────
687
1681
  /**
688
- * Get the DOM element for the renderer
1682
+ * Return a debug snapshot of the camera pipeline state.
1683
+ * Includes: active perspective, desired/final pose, behavior keys, action count.
689
1684
  */
690
- getDomElement() {
691
- return this.renderer.domElement;
1685
+ getState() {
1686
+ return this.cameraRef.pipeline.getState();
692
1687
  }
693
- };
694
-
695
- // src/lib/camera/camera.ts
696
- var CameraWrapper = class {
697
- cameraRef;
698
- constructor(camera) {
699
- this.cameraRef = camera;
1688
+ // ─── Render-to-texture ─────────────────────────────────────────────────
1689
+ /**
1690
+ * Get the offscreen render texture for this camera.
1691
+ * Returns null if the camera was not created with renderToTexture.
1692
+ * Use with setCameraFeed() or apply directly to a mesh material.
1693
+ */
1694
+ getRenderTexture() {
1695
+ return this.cameraRef.getRenderTexture();
700
1696
  }
701
1697
  };
702
1698
  function createCamera(options) {
@@ -705,13 +1701,293 @@ function createCamera(options) {
705
1701
  if (options.perspective === "fixed-2d") {
706
1702
  frustumSize = options.zoom || 10;
707
1703
  }
708
- const zylemCamera = new ZylemCamera(options.perspective || "third-person", screenResolution, frustumSize);
709
- zylemCamera.move(options.position || new Vector34(0, 0, 0));
710
- zylemCamera.camera.lookAt(options.target || new Vector34(0, 0, 0));
1704
+ const zylemCamera = new ZylemCamera(
1705
+ options.perspective || "third-person",
1706
+ screenResolution,
1707
+ frustumSize,
1708
+ options.rendererType || "webgl"
1709
+ );
1710
+ if (options.name) {
1711
+ zylemCamera.name = options.name;
1712
+ }
1713
+ const position = options.position ? options.position instanceof Vector37 ? options.position : new Vector37(options.position.x, options.position.y, options.position.z) : new Vector37(0, 0, 0);
1714
+ const target = options.target ? options.target instanceof Vector37 ? options.target : new Vector37(options.target.x, options.target.y, options.target.z) : new Vector37(0, 0, 0);
1715
+ zylemCamera.move(position);
1716
+ zylemCamera.camera.lookAt(target);
1717
+ const perspType = options.perspective || "third-person";
1718
+ if (perspType === "fixed-2d" || perspType === "flat-2d") {
1719
+ zylemCamera.pipeline.setPerspective(
1720
+ createPerspective(perspType, { zoom: frustumSize })
1721
+ );
1722
+ } else {
1723
+ zylemCamera.pipeline.setPerspective(
1724
+ createPerspective(perspType, {
1725
+ initialPosition: position.clone(),
1726
+ initialLookAt: target.clone()
1727
+ })
1728
+ );
1729
+ }
1730
+ if (options.viewport) {
1731
+ zylemCamera.viewport = { ...options.viewport };
1732
+ }
1733
+ if (options.useOrbitalControls) {
1734
+ zylemCamera._useOrbitalControls = true;
1735
+ }
1736
+ if (options.skipDebugOrbit) {
1737
+ zylemCamera._skipDebugOrbit = true;
1738
+ }
1739
+ if (options.damping != null) {
1740
+ zylemCamera.pipeline.damping = options.damping;
1741
+ }
1742
+ if (options.behaviors) {
1743
+ for (const [key, behavior] of Object.entries(options.behaviors)) {
1744
+ zylemCamera.pipeline.addBehavior(key, behavior);
1745
+ }
1746
+ }
1747
+ if (options.renderToTexture) {
1748
+ const { width = 512, height = 512 } = options.renderToTexture;
1749
+ zylemCamera.createRenderTarget(width, height);
1750
+ }
711
1751
  return new CameraWrapper(zylemCamera);
712
1752
  }
1753
+
1754
+ // src/lib/camera/camera-manager.ts
1755
+ import { Vector2 as Vector24 } from "three";
1756
+ var CameraManager = class {
1757
+ /** Named camera registry */
1758
+ cameras = /* @__PURE__ */ new Map();
1759
+ /** Currently active cameras, ordered by render layer (first = bottom) */
1760
+ _activeCameras = [];
1761
+ /** Auto-created debug camera with orbit controls */
1762
+ _debugCamera = null;
1763
+ /** Reference to the shared renderer manager */
1764
+ _rendererManager = null;
1765
+ /** Scene reference */
1766
+ _sceneRef = null;
1767
+ /** Counter for auto-generated camera names */
1768
+ _autoNameCounter = 0;
1769
+ constructor() {
1770
+ }
1771
+ /**
1772
+ * Get the list of currently active cameras.
1773
+ */
1774
+ get activeCameras() {
1775
+ return this._activeCameras;
1776
+ }
1777
+ /**
1778
+ * Get the primary active camera (first in the active list).
1779
+ */
1780
+ get primaryCamera() {
1781
+ return this._activeCameras.length > 0 ? this._activeCameras[0] : null;
1782
+ }
1783
+ /**
1784
+ * Get the debug camera.
1785
+ */
1786
+ get debugCamera() {
1787
+ return this._debugCamera;
1788
+ }
1789
+ /**
1790
+ * Get all registered cameras.
1791
+ */
1792
+ get allCameras() {
1793
+ return Array.from(this.cameras.values());
1794
+ }
1795
+ /**
1796
+ * Add a camera to the manager.
1797
+ * If no name is provided, one is auto-generated.
1798
+ * The first camera added becomes the active camera.
1799
+ *
1800
+ * @param camera The ZylemCamera instance to add
1801
+ * @param name Optional name for lookup
1802
+ * @returns The assigned name
1803
+ */
1804
+ addCamera(camera, name) {
1805
+ const resolvedName = name || camera.name || `camera_${this._autoNameCounter++}`;
1806
+ camera.name = resolvedName;
1807
+ this.cameras.set(resolvedName, camera);
1808
+ if (this._activeCameras.length === 0) {
1809
+ this._activeCameras.push(camera);
1810
+ }
1811
+ return resolvedName;
1812
+ }
1813
+ /**
1814
+ * Remove a camera by name or reference.
1815
+ * Cannot remove the debug camera via this method.
1816
+ */
1817
+ removeCamera(nameOrRef) {
1818
+ let name;
1819
+ if (typeof nameOrRef === "string") {
1820
+ name = nameOrRef;
1821
+ } else {
1822
+ for (const [key, cam] of this.cameras.entries()) {
1823
+ if (cam === nameOrRef) {
1824
+ name = key;
1825
+ break;
1826
+ }
1827
+ }
1828
+ }
1829
+ if (!name) return false;
1830
+ const camera = this.cameras.get(name);
1831
+ if (!camera) return false;
1832
+ if (camera === this._debugCamera) {
1833
+ console.warn("CameraManager: Cannot remove the debug camera");
1834
+ return false;
1835
+ }
1836
+ this.cameras.delete(name);
1837
+ const activeIndex = this._activeCameras.indexOf(camera);
1838
+ if (activeIndex !== -1) {
1839
+ this._activeCameras.splice(activeIndex, 1);
1840
+ }
1841
+ return true;
1842
+ }
1843
+ /**
1844
+ * Set a camera as the primary active camera (replaces all active cameras
1845
+ * except additional viewport cameras).
1846
+ *
1847
+ * @param nameOrRef Camera name or reference to activate
1848
+ */
1849
+ setActiveCamera(nameOrRef) {
1850
+ const camera = this.resolveCamera(nameOrRef);
1851
+ if (!camera) {
1852
+ console.warn(`CameraManager: Camera not found: ${nameOrRef}`);
1853
+ return false;
1854
+ }
1855
+ const pipCameras = this._activeCameras.filter((c) => {
1856
+ return c !== this._activeCameras[0] && c.viewport.width < 1;
1857
+ });
1858
+ this._activeCameras = [camera, ...pipCameras];
1859
+ return true;
1860
+ }
1861
+ /**
1862
+ * Add a camera as an additional active camera (for split-screen or PiP).
1863
+ */
1864
+ addActiveCamera(nameOrRef) {
1865
+ const camera = this.resolveCamera(nameOrRef);
1866
+ if (!camera) return false;
1867
+ if (!this._activeCameras.includes(camera)) {
1868
+ this._activeCameras.push(camera);
1869
+ }
1870
+ return true;
1871
+ }
1872
+ /**
1873
+ * Remove a camera from the active render list (does not remove from registry).
1874
+ */
1875
+ deactivateCamera(nameOrRef) {
1876
+ const camera = this.resolveCamera(nameOrRef);
1877
+ if (!camera) return false;
1878
+ const index = this._activeCameras.indexOf(camera);
1879
+ if (index !== -1) {
1880
+ this._activeCameras.splice(index, 1);
1881
+ return true;
1882
+ }
1883
+ return false;
1884
+ }
1885
+ /**
1886
+ * Get a camera by name.
1887
+ */
1888
+ getCamera(name) {
1889
+ return this.cameras.get(name) ?? null;
1890
+ }
1891
+ /**
1892
+ * Setup all cameras with the given scene and renderer manager.
1893
+ * Also creates the debug camera.
1894
+ */
1895
+ async setup(scene, rendererManager) {
1896
+ this._sceneRef = scene;
1897
+ this._rendererManager = rendererManager;
1898
+ this.createDebugCamera(rendererManager.screenResolution);
1899
+ for (const camera of this.cameras.values()) {
1900
+ camera.setRendererManager(rendererManager);
1901
+ await camera.setup(scene, rendererManager);
1902
+ }
1903
+ if (this._debugCamera) {
1904
+ this._debugCamera.setRendererManager(rendererManager);
1905
+ await this._debugCamera.setup(scene, rendererManager);
1906
+ }
1907
+ }
1908
+ /**
1909
+ * Update all active cameras' controllers.
1910
+ */
1911
+ update(delta) {
1912
+ for (const camera of this._activeCameras) {
1913
+ camera.update(delta);
1914
+ }
1915
+ }
1916
+ /**
1917
+ * Render all active cameras through the renderer manager.
1918
+ * RTT cameras (those with a renderTarget) are rendered first to their
1919
+ * offscreen textures, then viewport cameras are rendered to the screen.
1920
+ */
1921
+ render(scene) {
1922
+ if (!this._rendererManager || this._activeCameras.length === 0) return;
1923
+ const rttCameras = [];
1924
+ const viewportCameras = [];
1925
+ for (const cam of this._activeCameras) {
1926
+ if (cam.renderTarget) {
1927
+ rttCameras.push(cam);
1928
+ } else {
1929
+ viewportCameras.push(cam);
1930
+ }
1931
+ }
1932
+ for (const cam of rttCameras) {
1933
+ this._rendererManager.renderCameraToTarget(scene, cam);
1934
+ }
1935
+ if (viewportCameras.length > 0) {
1936
+ this._rendererManager.renderCameras(scene, viewportCameras);
1937
+ }
1938
+ }
1939
+ /**
1940
+ * Create a default third-person camera if no cameras have been added.
1941
+ */
1942
+ ensureDefaultCamera() {
1943
+ if (this.cameras.size === 0 || this._activeCameras.length === 0) {
1944
+ const screenRes = this._rendererManager?.screenResolution || new Vector24(window.innerWidth, window.innerHeight);
1945
+ const defaultCam = new ZylemCamera(Perspectives.ThirdPerson, screenRes);
1946
+ this.addCamera(defaultCam, "default");
1947
+ return defaultCam;
1948
+ }
1949
+ return this._activeCameras[0];
1950
+ }
1951
+ /**
1952
+ * Dispose all cameras and cleanup.
1953
+ */
1954
+ dispose() {
1955
+ for (const camera of this.cameras.values()) {
1956
+ camera.destroy();
1957
+ }
1958
+ this._debugCamera?.destroy();
1959
+ this.cameras.clear();
1960
+ this._activeCameras = [];
1961
+ this._debugCamera = null;
1962
+ this._rendererManager = null;
1963
+ this._sceneRef = null;
1964
+ }
1965
+ /**
1966
+ * Create the always-available debug camera with orbit controls.
1967
+ */
1968
+ createDebugCamera(screenResolution) {
1969
+ this._debugCamera = new ZylemCamera(Perspectives.ThirdPerson, screenResolution);
1970
+ this._debugCamera.name = "__debug__";
1971
+ this._debugCamera.enableOrbitalControls();
1972
+ }
1973
+ /**
1974
+ * Resolve a camera from a name or reference.
1975
+ */
1976
+ resolveCamera(nameOrRef) {
1977
+ if (typeof nameOrRef === "string") {
1978
+ return this.cameras.get(nameOrRef) ?? null;
1979
+ }
1980
+ for (const cam of this.cameras.values()) {
1981
+ if (cam === nameOrRef) return cam;
1982
+ }
1983
+ return null;
1984
+ }
1985
+ };
713
1986
  export {
1987
+ CameraManager,
1988
+ CameraWrapper,
714
1989
  Perspectives,
715
- createCamera
1990
+ createCamera,
1991
+ isWebGPUSupported
716
1992
  };
717
1993
  //# sourceMappingURL=camera.js.map