emeraldengine 3.0.0 → 3.1.0

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 (103) hide show
  1. package/README.md +1498 -1659
  2. package/dist/types/index.d.ts +4 -1
  3. package/dist/types/src/Animator.d.ts +1 -1
  4. package/dist/types/src/CollisionLayers.d.ts +2 -2
  5. package/dist/types/src/Color.d.ts +1 -0
  6. package/dist/types/src/Drawable.d.ts +1 -1
  7. package/dist/types/src/EmeraldDB.d.ts +2 -2
  8. package/dist/types/src/InstancedTexture.d.ts +17 -2
  9. package/dist/types/src/Material.d.ts +2 -2
  10. package/dist/types/src/MathUtils.d.ts +2 -1
  11. package/dist/types/src/ParticleEmitter.d.ts +1 -1
  12. package/dist/types/src/Physics.d.ts +148 -18
  13. package/dist/types/src/Scene.d.ts +1 -1
  14. package/dist/types/src/Shaders.d.ts +2 -2
  15. package/dist/types/src/Tilemap.d.ts +1 -1
  16. package/dist/types/src/UI.d.ts +1 -1
  17. package/dist/types/src/components/Behaviour.d.ts +2 -2
  18. package/dist/types/src/components/Collider.d.ts +7 -1
  19. package/dist/types/src/components/GameObject.d.ts +2 -2
  20. package/dist/types/src/components/PolygonCollider.d.ts +33 -0
  21. package/dist/types/src/components/RigidBody.d.ts +281 -8
  22. package/dist/types/src/importers/Aseprite.d.ts +2 -2
  23. package/dist/types/src/importers/ForgeLevel.d.ts +97 -0
  24. package/dist/types/src/importers/TiledMap.d.ts +1 -1
  25. package/dist/types/src/managers/EventManager.d.ts +1 -1
  26. package/dist/types/src/managers/Gamepad.d.ts +102 -0
  27. package/dist/types/src/managers/InputManager.d.ts +93 -2
  28. package/dist/types/src/managers/NetworkManager.d.ts +2 -2
  29. package/dist/types/src/managers/RenderStats.d.ts +1 -1
  30. package/dist/types/src/managers/TextureManager.d.ts +1 -1
  31. package/dist/types/src/physics/AABB.d.ts +92 -0
  32. package/dist/types/src/physics/Body.d.ts +435 -0
  33. package/dist/types/src/physics/BodyType.d.ts +6 -0
  34. package/dist/types/src/physics/BroadPhase.d.ts +210 -0
  35. package/dist/types/src/physics/Collision.d.ts +102 -0
  36. package/dist/types/src/physics/Contact.d.ts +206 -0
  37. package/dist/types/src/physics/ContactSolver.d.ts +108 -0
  38. package/dist/types/src/physics/Distance.d.ts +54 -0
  39. package/dist/types/src/physics/DistanceJoint.d.ts +90 -0
  40. package/dist/types/src/physics/Fixture.d.ts +221 -0
  41. package/dist/types/src/physics/Island.d.ts +52 -0
  42. package/dist/types/src/physics/Joint.d.ts +59 -0
  43. package/dist/types/src/physics/Math2D.d.ts +371 -0
  44. package/dist/types/src/physics/RevoluteJoint.d.ts +119 -0
  45. package/dist/types/src/physics/Settings.d.ts +22 -0
  46. package/dist/types/src/physics/Shapes.d.ts +207 -0
  47. package/dist/types/src/physics/TimeOfImpact.d.ts +22 -0
  48. package/dist/types/src/physics/World.d.ts +274 -0
  49. package/dist/types/src/physics/index.d.ts +34 -0
  50. package/index.js +6 -0
  51. package/package.json +2 -3
  52. package/src/Animator.js +1 -1
  53. package/src/CollisionLayers.js +3 -3
  54. package/src/Color.js +8 -0
  55. package/src/Drawable.js +1 -1
  56. package/src/Emerald.js +1 -1
  57. package/src/EmeraldDB.js +2 -2
  58. package/src/InstancedTexture.js +57 -9
  59. package/src/Material.js +2 -2
  60. package/src/MathUtils.js +2 -1
  61. package/src/ParticleEmitter.js +1 -1
  62. package/src/Physics.js +270 -60
  63. package/src/Scene.js +1 -1
  64. package/src/Shaders.js +20 -20
  65. package/src/Tilemap.js +1 -1
  66. package/src/UI.js +1 -1
  67. package/src/components/Behaviour.js +2 -2
  68. package/src/components/BoxCollider.js +7 -9
  69. package/src/components/BoxColliderDebug.js +3 -4
  70. package/src/components/CircleCollider.js +7 -9
  71. package/src/components/CircleColliderDebug.js +3 -2
  72. package/src/components/Collider.js +13 -3
  73. package/src/components/GameObject.js +2 -2
  74. package/src/components/PolygonCollider.js +55 -0
  75. package/src/components/RigidBody.js +441 -14
  76. package/src/importers/Aseprite.js +2 -2
  77. package/src/importers/ForgeLevel.js +581 -0
  78. package/src/importers/TiledMap.js +1 -1
  79. package/src/managers/EventManager.js +1 -1
  80. package/src/managers/Gamepad.js +126 -0
  81. package/src/managers/InputManager.js +129 -3
  82. package/src/managers/NetworkManager.js +2 -2
  83. package/src/managers/RenderStats.js +1 -1
  84. package/src/managers/TextureManager.js +1 -1
  85. package/src/physics/AABB.js +207 -0
  86. package/src/physics/Body.js +862 -0
  87. package/src/physics/BodyType.js +16 -0
  88. package/src/physics/BroadPhase.js +641 -0
  89. package/src/physics/Collision.js +534 -0
  90. package/src/physics/Contact.js +500 -0
  91. package/src/physics/ContactSolver.js +526 -0
  92. package/src/physics/Distance.js +403 -0
  93. package/src/physics/DistanceJoint.js +227 -0
  94. package/src/physics/Fixture.js +346 -0
  95. package/src/physics/Island.js +203 -0
  96. package/src/physics/Joint.js +78 -0
  97. package/src/physics/Math2D.js +573 -0
  98. package/src/physics/RevoluteJoint.js +278 -0
  99. package/src/physics/Settings.js +78 -0
  100. package/src/physics/Shapes.js +549 -0
  101. package/src/physics/TimeOfImpact.js +87 -0
  102. package/src/physics/World.js +731 -0
  103. package/src/physics/index.js +79 -0
@@ -0,0 +1,126 @@
1
+ /**
2
+ * @class Gamepad
3
+ * @description An autocomplete-friendly way to build the raw gamepad token
4
+ * strings InputManager already understands ("pad:0:south", etc.). It does
5
+ * not read input itself, and it is unrelated to the browser's own native
6
+ * `Gamepad` interface (the objects `navigator.getGamepads()` returns). It
7
+ * exists purely so you don't have to remember button-name spelling: every
8
+ * button/stick-direction name InputManager's standard mapping recognizes is
9
+ * a static constant here, so typing `Gamepad.` in an editor lists them the
10
+ * way an enum's members would.
11
+ *
12
+ * `Gamepad.get(index).key(name)` produces exactly the same string as writing
13
+ * the token by hand, so the two are fully interchangeable anywhere
14
+ * InputManager takes one: `mapAction`, `isDown`, `justPressed`,
15
+ * `justReleased`.
16
+ *
17
+ * @example
18
+ * import { Gamepad, InputManager } from "./index.js";
19
+ *
20
+ * const input = new InputManager();
21
+ * const pad = Gamepad.get(0); // the first controller
22
+ *
23
+ * input.mapAction("jump", [pad.key(Gamepad.SOUTH)]);
24
+ * input.mapAction("left", [pad.key(Gamepad.DPAD_LEFT), pad.key(Gamepad.LEFT_STICK_LEFT)]);
25
+ *
26
+ * // in the loop:
27
+ * if (input.justPressed("jump")) player.jump();
28
+ */
29
+ class Gamepad {
30
+ /**
31
+ * @method get
32
+ * @description Returns the (cached) handle for a pad index: 0 for the
33
+ * first controller, 1 for the second, and so on.
34
+ * @param {number} [index=0]
35
+ * @returns {Gamepad}
36
+ */
37
+ static get(index = 0) {
38
+ let pad = Gamepad._instances.get(index);
39
+ if (!pad) {
40
+ pad = new Gamepad(index);
41
+ Gamepad._instances.set(index, pad);
42
+ }
43
+ return pad;
44
+ }
45
+
46
+ /**
47
+ * @private Use `Gamepad.get(index)` rather than constructing directly, so
48
+ * every caller asking for the same pad index shares one instance.
49
+ */
50
+ constructor(index) {
51
+ this.index = index;
52
+ }
53
+
54
+ /**
55
+ * @method key
56
+ * @description A face/shoulder/centre/d-pad/stick-as-button token for this
57
+ * pad. Pass one of the `Gamepad.*` constants (`Gamepad.SOUTH`,
58
+ * `Gamepad.DPAD_UP`, …). InputManager's standard mapping also accepts a
59
+ * few vendor aliases these don't cover (e.g. "a", "cross" for `SOUTH`) if
60
+ * you'd rather think in Xbox/PlayStation terms; those still work as plain
61
+ * strings, `key()` just doesn't need to name them since `SOUTH` already
62
+ * reads the same on every pad.
63
+ * @param {string} name - One of the `Gamepad.*` button constants
64
+ * @returns {string}
65
+ */
66
+ key(name) {
67
+ return `pad:${this.index}:${name}`;
68
+ }
69
+
70
+ /**
71
+ * @method button
72
+ * @description A raw button index, mapping-independent: an escape hatch
73
+ * for a pad whose layout the standard/custom mapping tables don't cover.
74
+ * @param {number} n
75
+ * @returns {string}
76
+ */
77
+ button(n) {
78
+ return `pad:${this.index}:${n}`;
79
+ }
80
+
81
+ /**
82
+ * @method axis
83
+ * @description A raw analog axis past the deadzone, in one direction.
84
+ * @param {number} n - Axis index (0/1 = left stick X/Y, 2/3 = right stick X/Y on a standard pad)
85
+ * @param {"+"|"-"} [sign="+"]
86
+ * @returns {string}
87
+ */
88
+ axis(n, sign = "+") {
89
+ return `pad:${this.index}:axis${n}${sign}`;
90
+ }
91
+ }
92
+
93
+ Gamepad._instances = new Map();
94
+
95
+ Gamepad.SOUTH = "south";
96
+ Gamepad.EAST = "east";
97
+ Gamepad.WEST = "west";
98
+ Gamepad.NORTH = "north";
99
+
100
+ Gamepad.L1 = "l1";
101
+ Gamepad.R1 = "r1";
102
+ Gamepad.L2 = "l2";
103
+ Gamepad.R2 = "r2";
104
+
105
+ Gamepad.SELECT = "select";
106
+ Gamepad.START = "start";
107
+ Gamepad.HOME = "home";
108
+
109
+ Gamepad.L3 = "l3";
110
+ Gamepad.R3 = "r3";
111
+
112
+ Gamepad.DPAD_UP = "dpadUp";
113
+ Gamepad.DPAD_DOWN = "dpadDown";
114
+ Gamepad.DPAD_LEFT = "dpadLeft";
115
+ Gamepad.DPAD_RIGHT = "dpadRight";
116
+
117
+ Gamepad.LEFT_STICK_UP = "leftStickUp";
118
+ Gamepad.LEFT_STICK_DOWN = "leftStickDown";
119
+ Gamepad.LEFT_STICK_LEFT = "leftStickLeft";
120
+ Gamepad.LEFT_STICK_RIGHT = "leftStickRight";
121
+ Gamepad.RIGHT_STICK_UP = "rightStickUp";
122
+ Gamepad.RIGHT_STICK_DOWN = "rightStickDown";
123
+ Gamepad.RIGHT_STICK_LEFT = "rightStickLeft";
124
+ Gamepad.RIGHT_STICK_RIGHT = "rightStickRight";
125
+
126
+ export default Gamepad;
@@ -173,10 +173,10 @@ for (const b of BUILTIN_MAPPINGS) {
173
173
  * gamepad support (analog sticks/triggers, semantic button names, rumble,
174
174
  * connect events, and per-controller mapping), with rebindable named actions.
175
175
  * Call `update()` once per frame so `justPressed`/`justReleased` edge queries
176
- * work for every device including the gamepad.
176
+ * work for every device, including the gamepad.
177
177
  *
178
178
  * Gamepad tokens (usable anywhere a key token is, including in mapAction and
179
- * justPressed) `<i>` is the pad index:
179
+ * justPressed); `<i>` is the pad index:
180
180
  * "pad:<i>:south" / "pad:<i>:a" - face buttons (also east/b, west/x, north/y)
181
181
  * "pad:<i>:l1" / "pad:<i>:r2" ... - shoulders / triggers
182
182
  * "pad:<i>:start" / "pad:<i>:select" - center buttons
@@ -189,6 +189,11 @@ for (const b of BUILTIN_MAPPINGS) {
189
189
  * Names resolve through the active mapping, so "pad:0:south" is the bottom face
190
190
  * button regardless of whether the pad reports Xbox or PlayStation ordering.
191
191
  *
192
+ * Typing these by hand means remembering the exact spelling of every button
193
+ * name above; {@link Gamepad} builds the same strings from named constants
194
+ * instead (`Gamepad.get(0).key(Gamepad.SOUTH)` === "pad:0:south"), which most
195
+ * editors will autocomplete the way an enum's members would.
196
+ *
192
197
  * @example
193
198
  * const input = new InputManager();
194
199
  * input.mapAction("jump", ["Space", " ", "pad:0:south"]);
@@ -245,15 +250,28 @@ class InputManager {
245
250
  this._disconnectHandlers = [];
246
251
  /** @private */
247
252
  this._connected = new Map();
253
+ /**
254
+ * Which device last produced real input: "keyboard", "mouse",
255
+ * "gamepad", "touch", or null before anything has happened yet. See
256
+ * {@link getLastActiveDevice}.
257
+ * @private
258
+ */
259
+ this._lastActiveDevice = null;
260
+ /** Pending {@link identifyButton} calls, resolved from `update()`. @private */
261
+ this._buttonWaiters = [];
248
262
 
249
263
  /** @private */
250
- this._onKeyDown = (e) => this.down.add(this._normKey(e.key));
264
+ this._onKeyDown = (e) => {
265
+ this.down.add(this._normKey(e.key));
266
+ this._lastActiveDevice = "keyboard";
267
+ };
251
268
  /** @private */
252
269
  this._onKeyUp = (e) => this.down.delete(this._normKey(e.key));
253
270
  /** @private */
254
271
  this._onMouseDown = (e) => {
255
272
  this.down.add(`mouse:${e.button}`);
256
273
  this.mouse.buttons.add(e.button);
274
+ this._lastActiveDevice = "mouse";
257
275
  };
258
276
  /** @private */
259
277
  this._onMouseUp = (e) => {
@@ -272,6 +290,7 @@ class InputManager {
272
290
  y: t.clientY,
273
291
  id: t.identifier,
274
292
  }));
293
+ this._lastActiveDevice = "touch";
275
294
  };
276
295
  /** @private */
277
296
  this._onGamepadConnected = (e) => {
@@ -636,6 +655,30 @@ class InputManager {
636
655
  return !!this.gamepads[padIndex];
637
656
  }
638
657
 
658
+ /**
659
+ * @method getLastActiveDevice
660
+ * @description Which device the player most recently *actually used*,
661
+ * not just "is a gamepad plugged in" (see `isGamepadConnected`, which stays
662
+ * true all game long once one is), but "did they just press a key, click,
663
+ * touch, or move a gamepad button/stick". A pad being connected doesn't
664
+ * mean it's what's driving the game right now; this is the signal for
665
+ * swapping on-screen prompts between keyboard and gamepad button icons as
666
+ * the player actually switches between them mid-session.
667
+ *
668
+ * Only counts deliberate input: keydown, mousedown, and touch, not
669
+ * incidental mouse movement, so idly nudging the mouse while playing on a
670
+ * pad won't flip prompts back to keyboard/mouse.
671
+ *
672
+ * @example
673
+ * // once per frame, after input.update():
674
+ * hud.setPromptStyle(input.getLastActiveDevice() === "gamepad" ? "pad" : "keyboard");
675
+ *
676
+ * @returns {"keyboard"|"mouse"|"gamepad"|"touch"|null} - null before any input at all
677
+ */
678
+ getLastActiveDevice() {
679
+ return this._lastActiveDevice;
680
+ }
681
+
639
682
  /**
640
683
  * @method getPressedButtons
641
684
  * @description Diagnostic: raw indices of all currently pressed buttons on a
@@ -655,6 +698,71 @@ class InputManager {
655
698
  return out;
656
699
  }
657
700
 
701
+ /**
702
+ * @method identifyButton
703
+ * @description Resolves with the raw index of the next *new* button press
704
+ * on a pad: the reliable way to support a controller whose layout isn't
705
+ * already covered by the standard/registered mapping tables (a Steam
706
+ * Controller running outside Steam Input, an old flight stick, anything
707
+ * with a scrambled button order), instead of guessing indices: ask for one
708
+ * physical press and record wherever it actually lands on that specific
709
+ * device. Keep calling `update()` as normal while waiting; the promise
710
+ * resolves on the frame a button transitions from up to down. A button
711
+ * already held when this is called doesn't count; only a fresh press does.
712
+ *
713
+ * @example
714
+ * console.log("Press the button you want for Jump…");
715
+ * const index = await input.identifyButton(0);
716
+ * InputManager.registerGamepadMapping(input.getGamepadInfo(0).id, {
717
+ * buttons: { south: index },
718
+ * });
719
+ *
720
+ * @param {number} [padIndex=0]
721
+ * @returns {Promise<number>} - Never resolves if no new button is ever pressed
722
+ */
723
+ identifyButton(padIndex = 0) {
724
+ return new Promise((resolve) => {
725
+ this._buttonWaiters.push({
726
+ padIndex,
727
+ before: new Set(this.getPressedButtons(padIndex)),
728
+ resolve,
729
+ });
730
+ });
731
+ }
732
+
733
+ /**
734
+ * @method calibrateGamepad
735
+ * @description Walks a list of semantic button names one at a time, asking
736
+ * for a physical press for each (see {@link identifyButton}), and resolves
737
+ * with a `{name: rawIndex}` map ready to hand straight to
738
+ * `InputManager.registerGamepadMapping`, a complete fix for an oddball
739
+ * controller in a few seconds, without knowing anything about its layout
740
+ * in advance.
741
+ *
742
+ * @example
743
+ * const mapping = await input.calibrateGamepad(
744
+ * 0,
745
+ * ["south", "east", "west", "north", "l1", "r1", "start"],
746
+ * (name, i, total) => showPrompt(`(${i + 1}/${total}) Press the button for "${name}"`)
747
+ * );
748
+ * InputManager.registerGamepadMapping(input.getGamepadInfo(0).id, { buttons: mapping });
749
+ *
750
+ * @param {number} padIndex
751
+ * @param {string[]} names - Semantic names to calibrate, e.g. ["south", "east", "start"]
752
+ * @param {(name: string, index: number, total: number) => void} [onPrompt] -
753
+ * Called right before waiting for each name, so you can show "press ___" UI
754
+ * @returns {Promise<Object.<string, number>>} - { [name]: rawButtonIndex }
755
+ */
756
+ async calibrateGamepad(padIndex, names, onPrompt) {
757
+ const mapping = {};
758
+ for (let i = 0; i < names.length; i++) {
759
+ const name = names[i];
760
+ if (onPrompt) onPrompt(name, i, names.length);
761
+ mapping[name] = await this.identifyButton(padIndex);
762
+ }
763
+ return mapping;
764
+ }
765
+
658
766
  /**
659
767
  * @method _decodeHat
660
768
  * @description Some non-standard pads report the d-pad as an 8-way "hat" on a
@@ -749,6 +857,24 @@ class InputManager {
749
857
  for (const t of this._gamepadTokens) this.down.delete(t);
750
858
  this._collectGamepadTokens();
751
859
  for (const t of this._gamepadTokens) this.down.add(t);
860
+ if (this._gamepadTokens.size > 0) this._lastActiveDevice = "gamepad";
861
+ if (this._buttonWaiters.length) this._resolveButtonWaiters();
862
+ }
863
+
864
+ /**
865
+ * @method _resolveButtonWaiters
866
+ * @description Resolves any {@link identifyButton} calls whose pad just
867
+ * saw a button go from up to down.
868
+ * @private
869
+ */
870
+ _resolveButtonWaiters() {
871
+ this._buttonWaiters = this._buttonWaiters.filter((waiter) => {
872
+ const now = this.getPressedButtons(waiter.padIndex);
873
+ const fresh = now.find((b) => !waiter.before.has(b));
874
+ if (fresh == null) return true;
875
+ waiter.resolve(fresh);
876
+ return false;
877
+ });
752
878
  }
753
879
 
754
880
  /**
@@ -4,7 +4,7 @@ import Interpolator from "../Interpolator.js";
4
4
  * @class NetworkManager
5
5
  * @description A thin, optional multiplayer layer over Colyseus. `colyseus.js`
6
6
  * is an optional peer dependency and is imported dynamically, so the engine has
7
- * no hard dependency on it games that don't use networking never load it.
7
+ * no hard dependency on it, so games that don't use networking never load it.
8
8
  *
9
9
  * It wraps connection/room lifecycle, exposes a small event API
10
10
  * (onStateChange / onMessage / onAdd / onRemove / onLeave), and bundles an
@@ -140,7 +140,7 @@ class NetworkManager {
140
140
 
141
141
  /**
142
142
  * @method now
143
- * @description Seconds since this manager was created a convenient clock for
143
+ * @description Seconds since this manager was created, a convenient clock for
144
144
  * feeding the interpolator.
145
145
  * @returns {number}
146
146
  */
@@ -3,7 +3,7 @@
3
3
  * @description Frame-level render counters, incremented by every draw site in
4
4
  * the engine (Drawable, InstancedTexture, SpriteBatch, post-processing) and
5
5
  * reset at the start of each drawScene. Read the previous completed frame via
6
- * `RenderStats.frame` or `emerald.getRenderStats()` DebugOverlay shows it
6
+ * `RenderStats.frame` or `emerald.getRenderStats()`. DebugOverlay shows it
7
7
  * automatically.
8
8
  *
9
9
  * - drawCalls: GPU draw commands issued (the batching win shows up here)
@@ -102,7 +102,7 @@ class TextureManager {
102
102
  * @method restoreAll
103
103
  * @description Re-uploads every cached texture after a WebGL context loss.
104
104
  * The image cache survives the loss, so this is upload-only (no network).
105
- * Reference counts are untouched they track logical ownership by
105
+ * Reference counts are untouched: they track logical ownership by
106
106
  * drawables, which still exist.
107
107
  */
108
108
  static restoreAll() {
@@ -0,0 +1,207 @@
1
+ import { Vec2 } from "./Math2D.js";
2
+
3
+ /**
4
+ * @class AABB
5
+ * @description An axis-aligned bounding box in physics space. Used by the
6
+ * broadphase to reject pairs long before any real collision maths runs.
7
+ */
8
+ class AABB {
9
+ constructor(lowerX = 0, lowerY = 0, upperX = 0, upperY = 0) {
10
+ this.lowerBound = new Vec2(lowerX, lowerY);
11
+ this.upperBound = new Vec2(upperX, upperY);
12
+ }
13
+
14
+ /**
15
+ * @method set
16
+ * @description Sets all four bounds in place.
17
+ * @param {number} lx
18
+ * @param {number} ly
19
+ * @param {number} ux
20
+ * @param {number} uy
21
+ * @returns {AABB} - this
22
+ */
23
+ set(lx, ly, ux, uy) {
24
+ this.lowerBound.set(lx, ly);
25
+ this.upperBound.set(ux, uy);
26
+ return this;
27
+ }
28
+
29
+ /**
30
+ * @method copy
31
+ * @description Copies another AABB.
32
+ * @param {AABB} other
33
+ * @returns {AABB} - this
34
+ */
35
+ copy(other) {
36
+ this.lowerBound.copy(other.lowerBound);
37
+ this.upperBound.copy(other.upperBound);
38
+ return this;
39
+ }
40
+
41
+ /**
42
+ * @method getCenter
43
+ * @description Returns the box center.
44
+ * @returns {Vec2}
45
+ */
46
+ getCenter() {
47
+ return new Vec2(
48
+ 0.5 * (this.lowerBound.x + this.upperBound.x),
49
+ 0.5 * (this.lowerBound.y + this.upperBound.y)
50
+ );
51
+ }
52
+
53
+ /**
54
+ * @method getExtents
55
+ * @description Returns the box half-extents.
56
+ * @returns {Vec2}
57
+ */
58
+ getExtents() {
59
+ return new Vec2(
60
+ 0.5 * (this.upperBound.x - this.lowerBound.x),
61
+ 0.5 * (this.upperBound.y - this.lowerBound.y)
62
+ );
63
+ }
64
+
65
+ /**
66
+ * @method getPerimeter
67
+ * @description Perimeter of the box, the cost function the AABB tree
68
+ * minimizes when choosing where to insert a leaf.
69
+ * @returns {number}
70
+ */
71
+ getPerimeter() {
72
+ return (
73
+ 2 *
74
+ (this.upperBound.x -
75
+ this.lowerBound.x +
76
+ (this.upperBound.y - this.lowerBound.y))
77
+ );
78
+ }
79
+
80
+ /**
81
+ * @method combine
82
+ * @description Sets this box to the union of two others.
83
+ * @param {AABB} a
84
+ * @param {AABB} b
85
+ * @returns {AABB} - this
86
+ */
87
+ combine(a, b) {
88
+ this.lowerBound.x = Math.min(a.lowerBound.x, b.lowerBound.x);
89
+ this.lowerBound.y = Math.min(a.lowerBound.y, b.lowerBound.y);
90
+ this.upperBound.x = Math.max(a.upperBound.x, b.upperBound.x);
91
+ this.upperBound.y = Math.max(a.upperBound.y, b.upperBound.y);
92
+ return this;
93
+ }
94
+
95
+ /**
96
+ * @method extend
97
+ * @description Grows the box by `amount` on every side.
98
+ * @param {number} amount
99
+ * @returns {AABB} - this
100
+ */
101
+ extend(amount) {
102
+ this.lowerBound.x -= amount;
103
+ this.lowerBound.y -= amount;
104
+ this.upperBound.x += amount;
105
+ this.upperBound.y += amount;
106
+ return this;
107
+ }
108
+
109
+ /**
110
+ * @method contains
111
+ * @description True when `other` lies entirely inside this box.
112
+ * @param {AABB} other
113
+ * @returns {boolean}
114
+ */
115
+ contains(other) {
116
+ return (
117
+ this.lowerBound.x <= other.lowerBound.x &&
118
+ this.lowerBound.y <= other.lowerBound.y &&
119
+ other.upperBound.x <= this.upperBound.x &&
120
+ other.upperBound.y <= this.upperBound.y
121
+ );
122
+ }
123
+
124
+ /**
125
+ * @method containsPoint
126
+ * @description True when a point lies inside this box.
127
+ * @param {Object} p - `{ x, y }`
128
+ * @returns {boolean}
129
+ */
130
+ containsPoint(p) {
131
+ return (
132
+ this.lowerBound.x <= p.x &&
133
+ p.x <= this.upperBound.x &&
134
+ this.lowerBound.y <= p.y &&
135
+ p.y <= this.upperBound.y
136
+ );
137
+ }
138
+
139
+ /**
140
+ * @method testOverlap
141
+ * @description True when two boxes overlap.
142
+ * @param {AABB} a
143
+ * @param {AABB} b
144
+ * @returns {boolean}
145
+ */
146
+ static testOverlap(a, b) {
147
+ return !(
148
+ b.lowerBound.x - a.upperBound.x > 0 ||
149
+ b.lowerBound.y - a.upperBound.y > 0 ||
150
+ a.lowerBound.x - b.upperBound.x > 0 ||
151
+ a.lowerBound.y - b.upperBound.y > 0
152
+ );
153
+ }
154
+
155
+ /**
156
+ * @method rayCast
157
+ * @description Slab test of a ray against this box.
158
+ * @param {Object} input - `{ p1, p2, maxFraction }`
159
+ * @returns {boolean} - True if the ray hits the box within maxFraction
160
+ */
161
+ rayCast(input) {
162
+ let tmin = -Number.MAX_VALUE;
163
+ let tmax = Number.MAX_VALUE;
164
+
165
+ const p = input.p1;
166
+ const dx = input.p2.x - input.p1.x;
167
+ const dy = input.p2.y - input.p1.y;
168
+ const absDx = Math.abs(dx);
169
+ const absDy = Math.abs(dy);
170
+
171
+ if (absDx < 1e-12) {
172
+ if (p.x < this.lowerBound.x || this.upperBound.x < p.x) return false;
173
+ } else {
174
+ const invD = 1 / dx;
175
+ let t1 = (this.lowerBound.x - p.x) * invD;
176
+ let t2 = (this.upperBound.x - p.x) * invD;
177
+ if (t1 > t2) {
178
+ const t = t1;
179
+ t1 = t2;
180
+ t2 = t;
181
+ }
182
+ tmin = Math.max(tmin, t1);
183
+ tmax = Math.min(tmax, t2);
184
+ if (tmin > tmax) return false;
185
+ }
186
+
187
+ if (absDy < 1e-12) {
188
+ if (p.y < this.lowerBound.y || this.upperBound.y < p.y) return false;
189
+ } else {
190
+ const invD = 1 / dy;
191
+ let t1 = (this.lowerBound.y - p.y) * invD;
192
+ let t2 = (this.upperBound.y - p.y) * invD;
193
+ if (t1 > t2) {
194
+ const t = t1;
195
+ t1 = t2;
196
+ t2 = t;
197
+ }
198
+ tmin = Math.max(tmin, t1);
199
+ tmax = Math.min(tmax, t2);
200
+ if (tmin > tmax) return false;
201
+ }
202
+
203
+ return tmax >= 0 && tmin <= (input.maxFraction ?? 1);
204
+ }
205
+ }
206
+
207
+ export default AABB;