@flighthq/geometry 0.3.0 → 0.3.1-next.1041.35b950c

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 (125) hide show
  1. package/dist/aabb.d.ts +1 -0
  2. package/dist/aabb.d.ts.map +1 -1
  3. package/dist/aabb.js +58 -38
  4. package/dist/aabb.js.map +1 -1
  5. package/dist/boundingSphere.d.ts.map +1 -1
  6. package/dist/boundingSphere.js +3 -2
  7. package/dist/boundingSphere.js.map +1 -1
  8. package/dist/capsule.d.ts +6 -5
  9. package/dist/capsule.d.ts.map +1 -1
  10. package/dist/capsule.js +38 -15
  11. package/dist/capsule.js.map +1 -1
  12. package/dist/contract.d.ts +1 -0
  13. package/dist/contract.d.ts.map +1 -1
  14. package/dist/contract.js +1 -0
  15. package/dist/contract.js.map +1 -1
  16. package/dist/enableGeometryPoolGuards.d.ts +4 -0
  17. package/dist/enableGeometryPoolGuards.d.ts.map +1 -0
  18. package/dist/enableGeometryPoolGuards.js +32 -0
  19. package/dist/enableGeometryPoolGuards.js.map +1 -0
  20. package/dist/frustum.d.ts +2 -1
  21. package/dist/frustum.d.ts.map +1 -1
  22. package/dist/frustum.js +4 -1
  23. package/dist/frustum.js.map +1 -1
  24. package/dist/geometryPoolGuards.d.ts +6 -0
  25. package/dist/geometryPoolGuards.d.ts.map +1 -0
  26. package/dist/geometryPoolGuards.js +7 -0
  27. package/dist/geometryPoolGuards.js.map +1 -0
  28. package/dist/index.d.ts +1 -1
  29. package/dist/index.d.ts.map +1 -1
  30. package/dist/index.js +1 -1
  31. package/dist/index.js.map +1 -1
  32. package/dist/matrix.d.ts +12 -9
  33. package/dist/matrix.d.ts.map +1 -1
  34. package/dist/matrix.js +103 -106
  35. package/dist/matrix.js.map +1 -1
  36. package/dist/matrix3.d.ts +17 -2
  37. package/dist/matrix3.d.ts.map +1 -1
  38. package/dist/matrix3.js +88 -44
  39. package/dist/matrix3.js.map +1 -1
  40. package/dist/matrix3Pool.d.ts.map +1 -1
  41. package/dist/matrix3Pool.js +6 -0
  42. package/dist/matrix3Pool.js.map +1 -1
  43. package/dist/matrix4.d.ts +23 -10
  44. package/dist/matrix4.d.ts.map +1 -1
  45. package/dist/matrix4.js +74 -53
  46. package/dist/matrix4.js.map +1 -1
  47. package/dist/matrix4Pool.d.ts.map +1 -1
  48. package/dist/matrix4Pool.js +6 -0
  49. package/dist/matrix4Pool.js.map +1 -1
  50. package/dist/matrixPool.d.ts.map +1 -1
  51. package/dist/matrixPool.js +6 -0
  52. package/dist/matrixPool.js.map +1 -1
  53. package/dist/obb.d.ts +4 -2
  54. package/dist/obb.d.ts.map +1 -1
  55. package/dist/obb.js +164 -88
  56. package/dist/obb.js.map +1 -1
  57. package/dist/quaternion.d.ts +7 -4
  58. package/dist/quaternion.d.ts.map +1 -1
  59. package/dist/quaternion.js +40 -25
  60. package/dist/quaternion.js.map +1 -1
  61. package/dist/quaternionPool.d.ts.map +1 -1
  62. package/dist/quaternionPool.js +6 -0
  63. package/dist/quaternionPool.js.map +1 -1
  64. package/dist/ray3d.d.ts +5 -4
  65. package/dist/ray3d.d.ts.map +1 -1
  66. package/dist/ray3d.js +23 -8
  67. package/dist/ray3d.js.map +1 -1
  68. package/dist/rectangle.d.ts.map +1 -1
  69. package/dist/rectangle.js +66 -43
  70. package/dist/rectangle.js.map +1 -1
  71. package/dist/rectanglePool.d.ts.map +1 -1
  72. package/dist/rectanglePool.js +6 -0
  73. package/dist/rectanglePool.js.map +1 -1
  74. package/dist/transform2d.d.ts.map +1 -1
  75. package/dist/transform2d.js +5 -2
  76. package/dist/transform2d.js.map +1 -1
  77. package/dist/typedarray.js +3 -6
  78. package/dist/typedarray.js.map +1 -1
  79. package/dist/vector2.d.ts.map +1 -1
  80. package/dist/vector2.js +44 -26
  81. package/dist/vector2.js.map +1 -1
  82. package/dist/vector2Pool.d.ts.map +1 -1
  83. package/dist/vector2Pool.js +6 -0
  84. package/dist/vector2Pool.js.map +1 -1
  85. package/dist/vector3.d.ts +2 -2
  86. package/dist/vector3.d.ts.map +1 -1
  87. package/dist/vector3.js +55 -37
  88. package/dist/vector3.js.map +1 -1
  89. package/dist/vector3Pool.d.ts.map +1 -1
  90. package/dist/vector3Pool.js +6 -0
  91. package/dist/vector3Pool.js.map +1 -1
  92. package/dist/vector4.d.ts +2 -2
  93. package/dist/vector4.d.ts.map +1 -1
  94. package/dist/vector4.js +66 -48
  95. package/dist/vector4.js.map +1 -1
  96. package/dist/vector4Pool.d.ts.map +1 -1
  97. package/dist/vector4Pool.js +6 -0
  98. package/dist/vector4Pool.js.map +1 -1
  99. package/package.json +4 -3
  100. package/src/aabb.test.ts +18 -0
  101. package/src/boundingSphere.test.ts +39 -0
  102. package/src/capsule.test.ts +158 -0
  103. package/src/enableGeometryPoolGuards.test.ts +121 -0
  104. package/src/frustum.test.ts +38 -0
  105. package/src/geometryPoolGuards.test.ts +16 -0
  106. package/src/matrix.test.ts +52 -6
  107. package/src/matrix3.test.ts +46 -6
  108. package/src/matrix3Pool.test.ts +11 -0
  109. package/src/matrix4.test.ts +553 -135
  110. package/src/matrix4Pool.test.ts +11 -0
  111. package/src/matrixPool.test.ts +11 -0
  112. package/src/obb.test.ts +277 -0
  113. package/src/plane.test.ts +20 -0
  114. package/src/quaternion.test.ts +222 -4
  115. package/src/quaternionPool.test.ts +12 -0
  116. package/src/ray3d.test.ts +204 -0
  117. package/src/rectangle.test.ts +58 -14
  118. package/src/rectanglePool.test.ts +11 -0
  119. package/src/transform2d.test.ts +38 -0
  120. package/src/vector2.test.ts +170 -0
  121. package/src/vector2Pool.test.ts +11 -0
  122. package/src/vector3.test.ts +210 -0
  123. package/src/vector3Pool.test.ts +11 -0
  124. package/src/vector4.test.ts +231 -0
  125. package/src/vector4Pool.test.ts +11 -0
@@ -96,6 +96,27 @@ describe('addVector2', () => {
96
96
  });
97
97
 
98
98
  describe('clampVector2', () => {
99
+ it('clamps each component to its own bounds, below, above, and not at all', () => {
100
+ // Every component gets a DIFFERENT min and max, so a component reading a neighbour's bound
101
+ // comes out as a wrong number instead of the right one by luck. Each component is then run
102
+ // through all three outcomes — under its floor, over its ceiling, and already in range.
103
+ const min = createVector2(-1, -2);
104
+ const max = createVector2(10, 20);
105
+ const out = createVector2();
106
+
107
+ clampVector2(out, createVector2(-5, -6), min, max);
108
+ expect(out.x).toBe(-1);
109
+ expect(out.y).toBe(-2);
110
+
111
+ clampVector2(out, createVector2(100, 200), min, max);
112
+ expect(out.x).toBe(10);
113
+ expect(out.y).toBe(20);
114
+
115
+ clampVector2(out, createVector2(0, 1), min, max);
116
+ expect(out.x).toBe(0);
117
+ expect(out.y).toBe(1);
118
+ });
119
+
99
120
  it('clamps each component independently', () => {
100
121
  const out = createVector2();
101
122
  const value = createVector2(5, -2);
@@ -116,12 +137,28 @@ describe('clampVector2', () => {
116
137
  expect(out.y).toBe(2);
117
138
  });
118
139
 
140
+ it('clamps x low and y high', () => {
141
+ const out = createVector2();
142
+ clampVector2(out, createVector2(-2, 5), createVector2(0, 0), createVector2(3, 3));
143
+ expect(out).toMatchObject({ x: 0, y: 3 });
144
+ });
145
+
119
146
  it('supports out === value', () => {
120
147
  const v = createVector2(5, -1);
121
148
  clampVector2(v, v, createVector2(0, 0), createVector2(3, 3));
122
149
  expect(v.x).toBe(3);
123
150
  expect(v.y).toBe(0);
124
151
  });
152
+
153
+ it('supports out === min or max', () => {
154
+ const min = createVector2(0, 0);
155
+ clampVector2(min, createVector2(5, -2), min, createVector2(3, 3));
156
+ expect(min).toMatchObject({ x: 3, y: 0 });
157
+
158
+ const max = createVector2(3, 3);
159
+ clampVector2(max, createVector2(5, -2), createVector2(0, 0), max);
160
+ expect(max).toMatchObject({ x: 3, y: 0 });
161
+ });
125
162
  });
126
163
 
127
164
  describe('cloneVector2', () => {
@@ -195,6 +232,21 @@ describe('createVector2FromPolar', () => {
195
232
  });
196
233
 
197
234
  describe('divideVector2', () => {
235
+ it('gives 0 for a zero divisor on each component in turn', () => {
236
+ // A zero divisor is answered per component, not for the whole vector, so each component is
237
+ // zeroed on its own while the others still divide normally.
238
+ const source = createVector2(6, 8);
239
+ const out = createVector2();
240
+
241
+ divideVector2(out, source, createVector2(0, 4));
242
+ expect(out.x).toBe(0);
243
+ expect(out.y).toBe(2);
244
+
245
+ divideVector2(out, source, createVector2(2, 0));
246
+ expect(out.x).toBe(3);
247
+ expect(out.y).toBe(0);
248
+ });
249
+
198
250
  it('divides component-wise', () => {
199
251
  const out = createVector2();
200
252
  divideVector2(out, createVector2(6, 8), createVector2(2, 4));
@@ -209,15 +261,32 @@ describe('divideVector2', () => {
209
261
  expect(out.y).toBe(2);
210
262
  });
211
263
 
264
+ it('produces 0 for a zero y divisor', () => {
265
+ const out = createVector2();
266
+ divideVector2(out, createVector2(6, 8), createVector2(2, 0));
267
+ expect(out).toMatchObject({ x: 3, y: 0 });
268
+ });
269
+
212
270
  it('supports out === source', () => {
213
271
  const v = createVector2(6, 8);
214
272
  divideVector2(v, v, createVector2(2, 4));
215
273
  expect(v.x).toBe(3);
216
274
  expect(v.y).toBe(2);
217
275
  });
276
+
277
+ it('supports out === divisor', () => {
278
+ const divisor = createVector2(2, 4);
279
+ divideVector2(divisor, createVector2(6, 8), divisor);
280
+ expect(divisor).toMatchObject({ x: 3, y: 2 });
281
+ });
218
282
  });
219
283
 
220
284
  describe('equalsVector2', () => {
285
+ it('returns false when either vector is absent', () => {
286
+ expect(equalsVector2(pt, null)).toBe(false);
287
+ expect(equalsVector2(undefined, pt)).toBe(false);
288
+ });
289
+
221
290
  it('returns true if vectors are identical, false otherwise', () => {
222
291
  expect(equalsVector2(pt, pt2)).toBe(true);
223
292
  pt.x = 1;
@@ -234,6 +303,34 @@ describe('equalsVector2', () => {
234
303
  pt2.x = 1;
235
304
  expect(equalsVector2(pt, pt2)).toBe(true);
236
305
  });
306
+
307
+ it('compares every component, not just the first', () => {
308
+ // Two hand-written comparisons in a row: one left reading the wrong field still agrees with
309
+ // the right answer on every pair that differs in more than one place, so each differs alone.
310
+ const a = createVector2(1, 2);
311
+ expect(equalsVector2(a, createVector2(9, 2))).toBe(false);
312
+ expect(equalsVector2(a, createVector2(1, 9))).toBe(false);
313
+ });
314
+
315
+ it('returns false when either side is missing', () => {
316
+ // The signature accepts null and undefined, so absence is an answerable input rather than
317
+ // misuse: nothing is equal to a vector, including another absence.
318
+ const v = createVector2(1, 2);
319
+ expect(equalsVector2(v, null)).toBe(false);
320
+ expect(equalsVector2(null, v)).toBe(false);
321
+ expect(equalsVector2(v, undefined)).toBe(false);
322
+ expect(equalsVector2(undefined, v)).toBe(false);
323
+ expect(equalsVector2(null, null)).toBe(false);
324
+ expect(equalsVector2(undefined, undefined)).toBe(false);
325
+ });
326
+
327
+ it('holds a vector equal to itself even with a NaN component', () => {
328
+ // Comparing field by field would say a vector carrying NaN differs from itself, and a caller
329
+ // watching for changes would then see one every time it looked.
330
+ const notANumber = createVector2(NaN, 2);
331
+ expect(equalsVector2(notANumber, notANumber)).toBe(true);
332
+ expect(equalsVector2(notANumber, createVector2(NaN, 2))).toBe(false);
333
+ });
237
334
  });
238
335
 
239
336
  describe('getVector2AngleBetween', () => {
@@ -446,6 +543,23 @@ describe('interpolateVector2', () => {
446
543
  });
447
544
 
448
545
  describe('maxVector2', () => {
546
+ it('takes each component from whichever side wins, and gives the same answer either way round', () => {
547
+ // The winner alternates between the two arguments component by component, and all 4 numbers
548
+ // are distinct, so a line reading the wrong component or the wrong side cannot land on the
549
+ // right answer anyway. Running it again with the arguments swapped must not change it.
550
+ const a = createVector2(1, 5);
551
+ const b = createVector2(3, 0);
552
+ const out = createVector2();
553
+
554
+ maxVector2(out, a, b);
555
+ expect(out.x).toBe(3);
556
+ expect(out.y).toBe(5);
557
+
558
+ maxVector2(out, b, a);
559
+ expect(out.x).toBe(3);
560
+ expect(out.y).toBe(5);
561
+ });
562
+
449
563
  it('returns the component-wise maximum', () => {
450
564
  const out = createVector2();
451
565
  maxVector2(out, createVector2(1, 5), createVector2(3, 2));
@@ -460,9 +574,39 @@ describe('maxVector2', () => {
460
574
  expect(a.x).toBe(3);
461
575
  expect(a.y).toBe(5);
462
576
  });
577
+
578
+ it('supports out === b', () => {
579
+ const a = createVector2(1, 5);
580
+ const b = createVector2(3, 2);
581
+ maxVector2(b, a, b);
582
+ expect(b).toMatchObject({ x: 3, y: 5 });
583
+ });
584
+
585
+ it('selects a for x and b for y', () => {
586
+ const out = createVector2();
587
+ maxVector2(out, createVector2(5, 1), createVector2(2, 3));
588
+ expect(out).toMatchObject({ x: 5, y: 3 });
589
+ });
463
590
  });
464
591
 
465
592
  describe('minVector2', () => {
593
+ it('takes each component from whichever side wins, and gives the same answer either way round', () => {
594
+ // The winner alternates between the two arguments component by component, and all 4 numbers
595
+ // are distinct, so a line reading the wrong component or the wrong side cannot land on the
596
+ // right answer anyway. Running it again with the arguments swapped must not change it.
597
+ const a = createVector2(1, 5);
598
+ const b = createVector2(3, 0);
599
+ const out = createVector2();
600
+
601
+ minVector2(out, a, b);
602
+ expect(out.x).toBe(1);
603
+ expect(out.y).toBe(0);
604
+
605
+ minVector2(out, b, a);
606
+ expect(out.x).toBe(1);
607
+ expect(out.y).toBe(0);
608
+ });
609
+
466
610
  it('returns the component-wise minimum', () => {
467
611
  const out = createVector2();
468
612
  minVector2(out, createVector2(1, 5), createVector2(3, 2));
@@ -477,6 +621,19 @@ describe('minVector2', () => {
477
621
  expect(a.x).toBe(1);
478
622
  expect(a.y).toBe(2);
479
623
  });
624
+
625
+ it('supports out === b', () => {
626
+ const a = createVector2(1, 5);
627
+ const b = createVector2(3, 2);
628
+ minVector2(b, a, b);
629
+ expect(b).toMatchObject({ x: 1, y: 2 });
630
+ });
631
+
632
+ it('selects b for x and a for y', () => {
633
+ const out = createVector2();
634
+ minVector2(out, createVector2(5, 1), createVector2(2, 3));
635
+ expect(out).toMatchObject({ x: 2, y: 1 });
636
+ });
480
637
  });
481
638
 
482
639
  describe('multiplyVector2', () => {
@@ -493,6 +650,12 @@ describe('multiplyVector2', () => {
493
650
  expect(a.x).toBe(8);
494
651
  expect(a.y).toBe(15);
495
652
  });
653
+
654
+ it('supports out === b', () => {
655
+ const b = createVector2(4, 5);
656
+ multiplyVector2(b, createVector2(2, 3), b);
657
+ expect(b).toMatchObject({ x: 8, y: 15 });
658
+ });
496
659
  });
497
660
 
498
661
  describe('nearEqualsVector2', () => {
@@ -646,6 +809,13 @@ describe('reflectVector2', () => {
646
809
  expect(v.x).toBeCloseTo(-1, 6);
647
810
  expect(v.y).toBeCloseTo(-1, 6);
648
811
  });
812
+
813
+ it('supports out === normal', () => {
814
+ const normal = createVector2(1, 0);
815
+ reflectVector2(normal, createVector2(1, -1), normal);
816
+ expect(normal.x).toBeCloseTo(-1, 6);
817
+ expect(normal.y).toBeCloseTo(-1, 6);
818
+ });
649
819
  });
650
820
 
651
821
  describe('scaleVector2', () => {
@@ -1,3 +1,4 @@
1
+ import { attachEntityBinding, getEntityBinding } from '@flighthq/entity/contract';
1
2
  import type { Vector2 } from '@flighthq/types/contract';
2
3
 
3
4
  import { acquireEmptyVector2, acquireVector2, clearVector2Pool, releaseVector2 } from './vector2Pool';
@@ -59,6 +60,16 @@ describe('clearVector2Pool', () => {
59
60
  });
60
61
 
61
62
  describe('releaseVector2', () => {
63
+ it('detaches the previous owner binding before the vector is reused', () => {
64
+ const v = acquireVector2();
65
+ attachEntityBinding(v, { owner: 'first' });
66
+ releaseVector2(v);
67
+
68
+ const reused = acquireVector2();
69
+ expect(reused).toBe(v);
70
+ expect(getEntityBinding(reused)).toBeNull();
71
+ });
72
+
62
73
  it('handles null safely', () => {
63
74
  expect(() => releaseVector2(null as unknown as Vector2)).not.toThrow();
64
75
  });
@@ -88,6 +88,30 @@ describe('addVector3', () => {
88
88
  });
89
89
 
90
90
  describe('clampVector3', () => {
91
+ it('clamps each component to its own bounds, below, above, and not at all', () => {
92
+ // Every component gets a DIFFERENT min and max, so a component reading a neighbour's bound
93
+ // comes out as a wrong number instead of the right one by luck. Each component is then run
94
+ // through all three outcomes — under its floor, over its ceiling, and already in range.
95
+ const min = createVector3(-1, -2, -3);
96
+ const max = createVector3(10, 20, 30);
97
+ const out = createVector3();
98
+
99
+ clampVector3(out, createVector3(-5, -6, -7), min, max);
100
+ expect(out.x).toBe(-1);
101
+ expect(out.y).toBe(-2);
102
+ expect(out.z).toBe(-3);
103
+
104
+ clampVector3(out, createVector3(100, 200, 300), min, max);
105
+ expect(out.x).toBe(10);
106
+ expect(out.y).toBe(20);
107
+ expect(out.z).toBe(30);
108
+
109
+ clampVector3(out, createVector3(0, 1, 2), min, max);
110
+ expect(out.x).toBe(0);
111
+ expect(out.y).toBe(1);
112
+ expect(out.z).toBe(2);
113
+ });
114
+
91
115
  it('clamps each component independently', () => {
92
116
  const out = createVector3();
93
117
  clampVector3(out, createVector3(5, -2, 1), createVector3(0, 0, 0), createVector3(3, 3, 3));
@@ -103,6 +127,25 @@ describe('clampVector3', () => {
103
127
  expect(v.y).toBe(0);
104
128
  expect(v.z).toBe(3);
105
129
  });
130
+
131
+ it('supports out === min or max', () => {
132
+ const min = createVector3(0, 0, 0);
133
+ clampVector3(min, createVector3(5, -2, 1), min, createVector3(3, 3, 3));
134
+ expect(min).toMatchObject({ x: 3, y: 0, z: 1 });
135
+
136
+ const max = createVector3(3, 3, 3);
137
+ clampVector3(max, createVector3(5, -2, 1), createVector3(0, 0, 0), max);
138
+ expect(max).toMatchObject({ x: 3, y: 0, z: 1 });
139
+ });
140
+
141
+ it('covers every clamp outcome on every component', () => {
142
+ const out = createVector3();
143
+ clampVector3(out, createVector3(-1, 2, 5), createVector3(0, 0, 0), createVector3(3, 3, 3));
144
+ expect(out).toMatchObject({ x: 0, y: 2, z: 3 });
145
+
146
+ clampVector3(out, createVector3(2, 5, -1), createVector3(0, 0, 0), createVector3(3, 3, 3));
147
+ expect(out).toMatchObject({ x: 2, y: 3, z: 0 });
148
+ });
106
149
  });
107
150
 
108
151
  describe('cloneVector3', () => {
@@ -211,6 +254,28 @@ describe('crossVector3', () => {
211
254
  });
212
255
 
213
256
  describe('divideVector3', () => {
257
+ it('gives 0 for a zero divisor on each component in turn', () => {
258
+ // A zero divisor is answered per component, not for the whole vector, so each component is
259
+ // zeroed on its own while the others still divide normally.
260
+ const source = createVector3(6, 8, 9);
261
+ const out = createVector3();
262
+
263
+ divideVector3(out, source, createVector3(0, 4, 3));
264
+ expect(out.x).toBe(0);
265
+ expect(out.y).toBe(2);
266
+ expect(out.z).toBe(3);
267
+
268
+ divideVector3(out, source, createVector3(2, 0, 3));
269
+ expect(out.x).toBe(3);
270
+ expect(out.y).toBe(0);
271
+ expect(out.z).toBe(3);
272
+
273
+ divideVector3(out, source, createVector3(2, 4, 0));
274
+ expect(out.x).toBe(3);
275
+ expect(out.y).toBe(2);
276
+ expect(out.z).toBe(0);
277
+ });
278
+
214
279
  it('divides component-wise', () => {
215
280
  const out = createVector3();
216
281
  divideVector3(out, createVector3(6, 8, 9), createVector3(2, 4, 3));
@@ -234,9 +299,26 @@ describe('divideVector3', () => {
234
299
  expect(v.y).toBe(2);
235
300
  expect(v.z).toBe(3);
236
301
  });
302
+
303
+ it('supports out === divisor', () => {
304
+ const divisor = createVector3(2, 4, 3);
305
+ divideVector3(divisor, createVector3(6, 8, 9), divisor);
306
+ expect(divisor).toMatchObject({ x: 3, y: 2, z: 3 });
307
+ });
308
+
309
+ it('produces 0 for a zero y divisor', () => {
310
+ const out = createVector3();
311
+ divideVector3(out, createVector3(6, 8, 9), createVector3(2, 0, 3));
312
+ expect(out).toMatchObject({ x: 3, y: 0, z: 3 });
313
+ });
237
314
  });
238
315
 
239
316
  describe('equalsVector3', () => {
317
+ it('returns false when either vector is absent', () => {
318
+ expect(equalsVector3(createVector3(), null)).toBe(false);
319
+ expect(equalsVector3(undefined, createVector3())).toBe(false);
320
+ });
321
+
240
322
  it('returns true if vectors are equal', () => {
241
323
  const a = createVector3(1, 2, 3);
242
324
  const b = createVector3(1, 2, 3);
@@ -248,6 +330,35 @@ describe('equalsVector3', () => {
248
330
  const b = createVector3(4, 5, 6);
249
331
  expect(equalsVector3(a, b)).toBe(false);
250
332
  });
333
+
334
+ it('compares every component, not just the first', () => {
335
+ // Three hand-written comparisons in a row: one left reading the wrong field agrees with the
336
+ // right answer on every pair that differs in more than one place, so each differs alone here.
337
+ const a = createVector3(1, 2, 3);
338
+ expect(equalsVector3(a, createVector3(9, 2, 3))).toBe(false);
339
+ expect(equalsVector3(a, createVector3(1, 9, 3))).toBe(false);
340
+ expect(equalsVector3(a, createVector3(1, 2, 9))).toBe(false);
341
+ });
342
+
343
+ it('returns false when either side is missing', () => {
344
+ // The signature accepts null and undefined, so absence is an answerable input rather than
345
+ // misuse: nothing is equal to a vector, including another absence.
346
+ const v = createVector3(1, 2, 3);
347
+ expect(equalsVector3(v, null)).toBe(false);
348
+ expect(equalsVector3(null, v)).toBe(false);
349
+ expect(equalsVector3(v, undefined)).toBe(false);
350
+ expect(equalsVector3(undefined, v)).toBe(false);
351
+ expect(equalsVector3(null, null)).toBe(false);
352
+ expect(equalsVector3(undefined, undefined)).toBe(false);
353
+ });
354
+
355
+ it('holds a vector equal to itself even with a NaN component', () => {
356
+ // Comparing field by field would say a vector carrying NaN differs from itself, and a caller
357
+ // watching for changes would then see one every time it looked.
358
+ const notANumber = createVector3(NaN, 2, 3);
359
+ expect(equalsVector3(notANumber, notANumber)).toBe(true);
360
+ expect(equalsVector3(notANumber, createVector3(NaN, 2, 3))).toBe(false);
361
+ });
251
362
  });
252
363
 
253
364
  describe('getVector3AngleBetween', () => {
@@ -340,6 +451,14 @@ describe('getVector3Spherical', () => {
340
451
  expect(sph.y).toBe(0);
341
452
  expect(sph.z).toBe(0);
342
453
  });
454
+
455
+ it('supports out === source', () => {
456
+ const vector = createVector3(1, 0, 0);
457
+ getVector3Spherical(vector, vector);
458
+ expect(vector.x).toBeCloseTo(1, 6);
459
+ expect(vector.y).toBeCloseTo(Math.PI / 2, 6);
460
+ expect(vector.z).toBeCloseTo(0, 6);
461
+ });
343
462
  });
344
463
 
345
464
  describe('interpolateVector3', () => {
@@ -373,9 +492,35 @@ describe('interpolateVector3', () => {
373
492
  expect(a.y).toBe(2);
374
493
  expect(a.z).toBe(3);
375
494
  });
495
+
496
+ it('supports out === b', () => {
497
+ const a = createVector3(0, 0, 0);
498
+ const b = createVector3(2, 4, 6);
499
+ interpolateVector3(b, a, b, 0.5);
500
+ expect(b).toMatchObject({ x: 1, y: 2, z: 3 });
501
+ });
376
502
  });
377
503
 
378
504
  describe('maxVector3', () => {
505
+ it('takes each component from whichever side wins, and gives the same answer either way round', () => {
506
+ // The winner alternates between the two arguments component by component, and all 6 numbers
507
+ // are distinct, so a line reading the wrong component or the wrong side cannot land on the
508
+ // right answer anyway. Running it again with the arguments swapped must not change it.
509
+ const a = createVector3(1, 5, 2);
510
+ const b = createVector3(3, 0, 7);
511
+ const out = createVector3();
512
+
513
+ maxVector3(out, a, b);
514
+ expect(out.x).toBe(3);
515
+ expect(out.y).toBe(5);
516
+ expect(out.z).toBe(7);
517
+
518
+ maxVector3(out, b, a);
519
+ expect(out.x).toBe(3);
520
+ expect(out.y).toBe(5);
521
+ expect(out.z).toBe(7);
522
+ });
523
+
379
524
  it('returns the component-wise maximum', () => {
380
525
  const out = createVector3();
381
526
  maxVector3(out, createVector3(1, 5, 2), createVector3(3, 2, 7));
@@ -383,9 +528,44 @@ describe('maxVector3', () => {
383
528
  expect(out.y).toBe(5);
384
529
  expect(out.z).toBe(7);
385
530
  });
531
+
532
+ it('supports out === a or b', () => {
533
+ const a = createVector3(1, 5, 2);
534
+ maxVector3(a, a, createVector3(3, 2, 7));
535
+ expect(a).toMatchObject({ x: 3, y: 5, z: 7 });
536
+
537
+ const b = createVector3(3, 2, 7);
538
+ maxVector3(b, createVector3(1, 5, 2), b);
539
+ expect(b).toMatchObject({ x: 3, y: 5, z: 7 });
540
+ });
541
+
542
+ it('selects the complementary component outcomes', () => {
543
+ const out = createVector3();
544
+ maxVector3(out, createVector3(5, 1, 7), createVector3(2, 3, 4));
545
+ expect(out).toMatchObject({ x: 5, y: 3, z: 7 });
546
+ });
386
547
  });
387
548
 
388
549
  describe('minVector3', () => {
550
+ it('takes each component from whichever side wins, and gives the same answer either way round', () => {
551
+ // The winner alternates between the two arguments component by component, and all 6 numbers
552
+ // are distinct, so a line reading the wrong component or the wrong side cannot land on the
553
+ // right answer anyway. Running it again with the arguments swapped must not change it.
554
+ const a = createVector3(1, 5, 2);
555
+ const b = createVector3(3, 0, 7);
556
+ const out = createVector3();
557
+
558
+ minVector3(out, a, b);
559
+ expect(out.x).toBe(1);
560
+ expect(out.y).toBe(0);
561
+ expect(out.z).toBe(2);
562
+
563
+ minVector3(out, b, a);
564
+ expect(out.x).toBe(1);
565
+ expect(out.y).toBe(0);
566
+ expect(out.z).toBe(2);
567
+ });
568
+
389
569
  it('returns the component-wise minimum', () => {
390
570
  const out = createVector3();
391
571
  minVector3(out, createVector3(1, 5, 2), createVector3(3, 2, 7));
@@ -393,6 +573,22 @@ describe('minVector3', () => {
393
573
  expect(out.y).toBe(2);
394
574
  expect(out.z).toBe(2);
395
575
  });
576
+
577
+ it('supports out === a or b', () => {
578
+ const a = createVector3(1, 5, 2);
579
+ minVector3(a, a, createVector3(3, 2, 7));
580
+ expect(a).toMatchObject({ x: 1, y: 2, z: 2 });
581
+
582
+ const b = createVector3(3, 2, 7);
583
+ minVector3(b, createVector3(1, 5, 2), b);
584
+ expect(b).toMatchObject({ x: 1, y: 2, z: 2 });
585
+ });
586
+
587
+ it('selects the complementary component outcomes', () => {
588
+ const out = createVector3();
589
+ minVector3(out, createVector3(5, 1, 7), createVector3(2, 3, 4));
590
+ expect(out).toMatchObject({ x: 2, y: 1, z: 4 });
591
+ });
396
592
  });
397
593
 
398
594
  describe('multiplyVector3', () => {
@@ -411,6 +607,12 @@ describe('multiplyVector3', () => {
411
607
  expect(a.y).toBe(18);
412
608
  expect(a.z).toBe(28);
413
609
  });
610
+
611
+ it('supports out === b', () => {
612
+ const b = createVector3(5, 6, 7);
613
+ multiplyVector3(b, createVector3(2, 3, 4), b);
614
+ expect(b).toMatchObject({ x: 10, y: 18, z: 28 });
615
+ });
414
616
  });
415
617
 
416
618
  describe('nearEqualsVector3', () => {
@@ -567,6 +769,14 @@ describe('reflectVector3', () => {
567
769
  expect(v.x).toBeCloseTo(-1, 6);
568
770
  expect(v.y).toBeCloseTo(-1, 6);
569
771
  });
772
+
773
+ it('supports out === normal', () => {
774
+ const normal = createVector3(1, 0, 0);
775
+ reflectVector3(normal, createVector3(1, -1, 0), normal);
776
+ expect(normal.x).toBeCloseTo(-1, 6);
777
+ expect(normal.y).toBeCloseTo(-1, 6);
778
+ expect(normal.z).toBeCloseTo(0, 6);
779
+ });
570
780
  });
571
781
 
572
782
  describe('scaleVector3', () => {
@@ -1,3 +1,4 @@
1
+ import { attachEntityBinding, getEntityBinding } from '@flighthq/entity/contract';
1
2
  import type { Vector3 } from '@flighthq/types/contract';
2
3
 
3
4
  import { acquireEmptyVector3, acquireVector3, clearVector3Pool, releaseVector3 } from './vector3Pool';
@@ -62,6 +63,16 @@ describe('clearVector3Pool', () => {
62
63
  });
63
64
 
64
65
  describe('releaseVector3', () => {
66
+ it('detaches the previous owner binding before the vector is reused', () => {
67
+ const v = acquireVector3();
68
+ attachEntityBinding(v, { owner: 'first' });
69
+ releaseVector3(v);
70
+
71
+ const reused = acquireVector3();
72
+ expect(reused).toBe(v);
73
+ expect(getEntityBinding(reused)).toBeNull();
74
+ });
75
+
65
76
  it('handles null safely', () => {
66
77
  expect(() => releaseVector3(null as unknown as Vector3)).not.toThrow();
67
78
  });