@flighthq/geometry 0.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 (125) hide show
  1. package/dist/aabb.d.ts +88 -0
  2. package/dist/aabb.d.ts.map +1 -0
  3. package/dist/aabb.js +230 -0
  4. package/dist/aabb.js.map +1 -0
  5. package/dist/boundingSphere.d.ts +62 -0
  6. package/dist/boundingSphere.d.ts.map +1 -0
  7. package/dist/boundingSphere.js +188 -0
  8. package/dist/boundingSphere.js.map +1 -0
  9. package/dist/capsule.d.ts +38 -0
  10. package/dist/capsule.d.ts.map +1 -0
  11. package/dist/capsule.js +215 -0
  12. package/dist/capsule.js.map +1 -0
  13. package/dist/frustum.d.ts +49 -0
  14. package/dist/frustum.d.ts.map +1 -0
  15. package/dist/frustum.js +149 -0
  16. package/dist/frustum.js.map +1 -0
  17. package/dist/index.d.ts +25 -0
  18. package/dist/index.d.ts.map +1 -0
  19. package/dist/index.js +25 -0
  20. package/dist/index.js.map +1 -0
  21. package/dist/matrix.d.ts +152 -0
  22. package/dist/matrix.d.ts.map +1 -0
  23. package/dist/matrix.js +488 -0
  24. package/dist/matrix.js.map +1 -0
  25. package/dist/matrix3.d.ts +63 -0
  26. package/dist/matrix3.d.ts.map +1 -0
  27. package/dist/matrix3.js +408 -0
  28. package/dist/matrix3.js.map +1 -0
  29. package/dist/matrix3Pool.d.ts +6 -0
  30. package/dist/matrix3Pool.d.ts.map +1 -0
  31. package/dist/matrix3Pool.js +26 -0
  32. package/dist/matrix3Pool.js.map +1 -0
  33. package/dist/matrix4.d.ts +238 -0
  34. package/dist/matrix4.d.ts.map +1 -0
  35. package/dist/matrix4.js +931 -0
  36. package/dist/matrix4.js.map +1 -0
  37. package/dist/matrix4Pool.d.ts +6 -0
  38. package/dist/matrix4Pool.d.ts.map +1 -0
  39. package/dist/matrix4Pool.js +26 -0
  40. package/dist/matrix4Pool.js.map +1 -0
  41. package/dist/matrixPool.d.ts +6 -0
  42. package/dist/matrixPool.d.ts.map +1 -0
  43. package/dist/matrixPool.js +26 -0
  44. package/dist/matrixPool.js.map +1 -0
  45. package/dist/obb.d.ts +44 -0
  46. package/dist/obb.d.ts.map +1 -0
  47. package/dist/obb.js +248 -0
  48. package/dist/obb.js.map +1 -0
  49. package/dist/plane.d.ts +66 -0
  50. package/dist/plane.d.ts.map +1 -0
  51. package/dist/plane.js +140 -0
  52. package/dist/plane.js.map +1 -0
  53. package/dist/quaternion.d.ts +128 -0
  54. package/dist/quaternion.d.ts.map +1 -0
  55. package/dist/quaternion.js +508 -0
  56. package/dist/quaternion.js.map +1 -0
  57. package/dist/quaternionPool.d.ts +6 -0
  58. package/dist/quaternionPool.d.ts.map +1 -0
  59. package/dist/quaternionPool.js +29 -0
  60. package/dist/quaternionPool.js.map +1 -0
  61. package/dist/ray3d.d.ts +69 -0
  62. package/dist/ray3d.d.ts.map +1 -0
  63. package/dist/ray3d.js +264 -0
  64. package/dist/ray3d.js.map +1 -0
  65. package/dist/rectangle.d.ts +56 -0
  66. package/dist/rectangle.d.ts.map +1 -0
  67. package/dist/rectangle.js +235 -0
  68. package/dist/rectangle.js.map +1 -0
  69. package/dist/rectanglePool.d.ts +6 -0
  70. package/dist/rectanglePool.d.ts.map +1 -0
  71. package/dist/rectanglePool.js +29 -0
  72. package/dist/rectanglePool.js.map +1 -0
  73. package/dist/typedarray.d.ts +4 -0
  74. package/dist/typedarray.d.ts.map +1 -0
  75. package/dist/typedarray.js +25 -0
  76. package/dist/typedarray.js.map +1 -0
  77. package/dist/vector2.d.ts +130 -0
  78. package/dist/vector2.d.ts.map +1 -0
  79. package/dist/vector2.js +250 -0
  80. package/dist/vector2.js.map +1 -0
  81. package/dist/vector2Pool.d.ts +6 -0
  82. package/dist/vector2Pool.d.ts.map +1 -0
  83. package/dist/vector2Pool.js +20 -0
  84. package/dist/vector2Pool.js.map +1 -0
  85. package/dist/vector3.d.ts +220 -0
  86. package/dist/vector3.d.ts.map +1 -0
  87. package/dist/vector3.js +380 -0
  88. package/dist/vector3.js.map +1 -0
  89. package/dist/vector3Pool.d.ts +6 -0
  90. package/dist/vector3Pool.d.ts.map +1 -0
  91. package/dist/vector3Pool.js +28 -0
  92. package/dist/vector3Pool.js.map +1 -0
  93. package/dist/vector4.d.ts +180 -0
  94. package/dist/vector4.d.ts.map +1 -0
  95. package/dist/vector4.js +333 -0
  96. package/dist/vector4.js.map +1 -0
  97. package/dist/vector4Pool.d.ts +6 -0
  98. package/dist/vector4Pool.d.ts.map +1 -0
  99. package/dist/vector4Pool.js +29 -0
  100. package/dist/vector4Pool.js.map +1 -0
  101. package/package.json +38 -0
  102. package/src/aabb.test.ts +341 -0
  103. package/src/boundingSphere.test.ts +247 -0
  104. package/src/capsule.test.ts +157 -0
  105. package/src/frustum.test.ts +160 -0
  106. package/src/matrix.test.ts +1111 -0
  107. package/src/matrix3.test.ts +752 -0
  108. package/src/matrix3Pool.test.ts +73 -0
  109. package/src/matrix4.test.ts +1627 -0
  110. package/src/matrix4Pool.test.ts +71 -0
  111. package/src/matrixPool.test.ts +69 -0
  112. package/src/obb.test.ts +183 -0
  113. package/src/plane.test.ts +225 -0
  114. package/src/quaternion.test.ts +531 -0
  115. package/src/quaternionPool.test.ts +64 -0
  116. package/src/ray3d.test.ts +297 -0
  117. package/src/rectangle.test.ts +982 -0
  118. package/src/rectanglePool.test.ts +71 -0
  119. package/src/typedarray.test.ts +118 -0
  120. package/src/vector2.test.ts +958 -0
  121. package/src/vector2Pool.test.ts +65 -0
  122. package/src/vector3.test.ts +749 -0
  123. package/src/vector3Pool.test.ts +68 -0
  124. package/src/vector4.test.ts +660 -0
  125. package/src/vector4Pool.test.ts +71 -0
@@ -0,0 +1,531 @@
1
+ import {
2
+ cloneQuaternion,
3
+ conjugateQuaternion,
4
+ copyQuaternion,
5
+ createMatrix4,
6
+ createQuaternion,
7
+ createVector3,
8
+ equalsQuaternion,
9
+ getQuaternionAngleBetween,
10
+ getQuaternionDot,
11
+ getQuaternionEuler,
12
+ inverseQuaternion,
13
+ matrix4TransformPoint,
14
+ multiplyQuaternion,
15
+ normalizeQuaternion,
16
+ rotateVector3ByQuaternion,
17
+ setMatrix4FromQuaternion,
18
+ setQuaternionFromAxisAngle,
19
+ setQuaternionFromEuler,
20
+ setQuaternionFromMatrix4,
21
+ setQuaternionFromUnitVectors,
22
+ setQuaternionIdentity,
23
+ setQuaternionLookRotation,
24
+ slerpQuaternion,
25
+ } from '@flighthq/geometry';
26
+ import type { Quaternion } from '@flighthq/types';
27
+
28
+ function expectQuaternionClose(q: Readonly<Quaternion>, x: number, y: number, z: number, w: number): void {
29
+ expect(q.x).toBeCloseTo(x, 6);
30
+ expect(q.y).toBeCloseTo(y, 6);
31
+ expect(q.z).toBeCloseTo(z, 6);
32
+ expect(q.w).toBeCloseTo(w, 6);
33
+ }
34
+
35
+ describe('cloneQuaternion', () => {
36
+ it('creates an independent copy', () => {
37
+ const q = createQuaternion(1, 2, 3, 4);
38
+ const c = cloneQuaternion(q);
39
+ expect(c).not.toBe(q);
40
+ expect(c.x).toBe(1);
41
+ expect(c.y).toBe(2);
42
+ expect(c.z).toBe(3);
43
+ expect(c.w).toBe(4);
44
+ });
45
+ });
46
+
47
+ describe('conjugateQuaternion', () => {
48
+ it('negates the vector part', () => {
49
+ const q = createQuaternion(1, 2, 3, 4);
50
+ const out = createQuaternion();
51
+ conjugateQuaternion(out, q);
52
+ expectQuaternionClose(out, -1, -2, -3, 4);
53
+ });
54
+
55
+ it('supports out === source', () => {
56
+ const q = createQuaternion(1, 2, 3, 4);
57
+ conjugateQuaternion(q, q);
58
+ expectQuaternionClose(q, -1, -2, -3, 4);
59
+ });
60
+ });
61
+
62
+ describe('copyQuaternion', () => {
63
+ it('copies all components', () => {
64
+ const src = createQuaternion(5, 6, 7, 8);
65
+ const out = createQuaternion();
66
+ copyQuaternion(out, src);
67
+ expectQuaternionClose(out, 5, 6, 7, 8);
68
+ });
69
+
70
+ it('supports out === source', () => {
71
+ const q = createQuaternion(5, 6, 7, 8);
72
+ copyQuaternion(q, q);
73
+ expectQuaternionClose(q, 5, 6, 7, 8);
74
+ });
75
+ });
76
+
77
+ describe('createQuaternion', () => {
78
+ it('defaults to identity', () => {
79
+ expectQuaternionClose(createQuaternion(), 0, 0, 0, 1);
80
+ });
81
+
82
+ it('uses provided components', () => {
83
+ expectQuaternionClose(createQuaternion(1, 2, 3, 4), 1, 2, 3, 4);
84
+ });
85
+ });
86
+
87
+ describe('equalsQuaternion', () => {
88
+ it('returns true for identical components', () => {
89
+ expect(equalsQuaternion(createQuaternion(1, 2, 3, 4), createQuaternion(1, 2, 3, 4))).toBe(true);
90
+ });
91
+
92
+ it('returns true for the same reference', () => {
93
+ const q = createQuaternion(1, 2, 3, 4);
94
+ expect(equalsQuaternion(q, q)).toBe(true);
95
+ });
96
+
97
+ it('returns false for differing components', () => {
98
+ expect(equalsQuaternion(createQuaternion(1, 2, 3, 4), createQuaternion(1, 2, 3, 5))).toBe(false);
99
+ });
100
+
101
+ it('returns false for null inputs', () => {
102
+ expect(equalsQuaternion(null, createQuaternion())).toBe(false);
103
+ expect(equalsQuaternion(createQuaternion(), undefined)).toBe(false);
104
+ });
105
+ });
106
+
107
+ describe('getQuaternionAngleBetween', () => {
108
+ it('returns 0 for two identical quaternions', () => {
109
+ const a = createQuaternion();
110
+ expect(getQuaternionAngleBetween(a, a)).toBeCloseTo(0, 6);
111
+ });
112
+
113
+ it('returns the angle between two axis-angle quaternions', () => {
114
+ const a = createQuaternion();
115
+ const b = createQuaternion();
116
+ setQuaternionFromAxisAngle(a, createVector3(0, 1, 0), 0);
117
+ setQuaternionFromAxisAngle(b, createVector3(0, 1, 0), Math.PI / 2);
118
+ expect(getQuaternionAngleBetween(a, b)).toBeCloseTo(Math.PI / 2, 5);
119
+ });
120
+ });
121
+
122
+ describe('getQuaternionDot', () => {
123
+ it('returns 1 for identical unit quaternions', () => {
124
+ const q = createQuaternion();
125
+ expect(getQuaternionDot(q, q)).toBeCloseTo(1, 6);
126
+ });
127
+
128
+ it('computes the four-component dot product', () => {
129
+ const a = createQuaternion(1, 2, 3, 4);
130
+ const b = createQuaternion(5, 6, 7, 8);
131
+ expect(getQuaternionDot(a, b)).toBe(1 * 5 + 2 * 6 + 3 * 7 + 4 * 8);
132
+ });
133
+ });
134
+
135
+ describe('getQuaternionEuler', () => {
136
+ it('identity quaternion decodes to (0, 0, 0) for XYZ order', () => {
137
+ const q = createQuaternion(); // identity
138
+ const out = createVector3();
139
+ getQuaternionEuler(out, q, 'XYZ');
140
+ expect(out.x).toBeCloseTo(0, 5);
141
+ expect(out.y).toBeCloseTo(0, 5);
142
+ expect(out.z).toBeCloseTo(0, 5);
143
+ });
144
+
145
+ it('pure X rotation round-trips (XYZ order)', () => {
146
+ // Single-axis rotations are unambiguous and always round-trip.
147
+ const q = createQuaternion();
148
+ setQuaternionFromEuler(q, 0.8, 0, 0, 'XYZ');
149
+ const out = createVector3();
150
+ getQuaternionEuler(out, q, 'XYZ');
151
+ expect(out.x).toBeCloseTo(0.8, 5);
152
+ expect(out.y).toBeCloseTo(0, 5);
153
+ expect(out.z).toBeCloseTo(0, 5);
154
+ });
155
+
156
+ it('pure Y rotation round-trips (XYZ order)', () => {
157
+ const q = createQuaternion();
158
+ setQuaternionFromEuler(q, 0, 0.6, 0, 'XYZ');
159
+ const out = createVector3();
160
+ getQuaternionEuler(out, q, 'XYZ');
161
+ expect(out.x).toBeCloseTo(0, 5);
162
+ expect(out.y).toBeCloseTo(0.6, 5);
163
+ expect(out.z).toBeCloseTo(0, 5);
164
+ });
165
+
166
+ // set → get is a true inverse for every order: a combined rotation must round-trip back to a
167
+ // quaternion equal (up to sign) to the original. |dot| ≈ 1 confirms the two represent the same
168
+ // rotation. This is the regression guard for the get-side extraction fix.
169
+ it.each(['XYZ', 'XZY', 'YXZ', 'YZX', 'ZXY', 'ZYX'] as const)(
170
+ 'combined (0.3, 0.5, 0.7) rotation round-trips for %s order',
171
+ (order) => {
172
+ const q = createQuaternion();
173
+ setQuaternionFromEuler(q, 0.3, 0.5, 0.7, order);
174
+ const euler = createVector3();
175
+ getQuaternionEuler(euler, q, order);
176
+ const back = createQuaternion();
177
+ setQuaternionFromEuler(back, euler.x, euler.y, euler.z, order);
178
+ const dot = getQuaternionDot(q, back);
179
+ expect(Math.abs(dot)).toBeCloseTo(1, 6);
180
+ },
181
+ );
182
+
183
+ it.each(['XYZ', 'XZY', 'YXZ', 'YZX', 'ZXY', 'ZYX'] as const)(
184
+ 'round-trips a fuzzed set of unit quaternions for %s order',
185
+ (order) => {
186
+ // Property: decompose then recompose must land on the same rotation (up to sign) for any
187
+ // unit quaternion, for every Euler order.
188
+ let seed = 0x9e3779b9 ^ order.charCodeAt(0);
189
+ const next = () => {
190
+ seed = (seed * 1664525 + 1013904223) | 0;
191
+ return ((seed >>> 0) / 0xffffffff) * 2 - 1;
192
+ };
193
+ const q = createQuaternion();
194
+ const back = createQuaternion();
195
+ const euler = createVector3();
196
+ for (let i = 0; i < 200; i++) {
197
+ q.x = next();
198
+ q.y = next();
199
+ q.z = next();
200
+ q.w = next();
201
+ normalizeQuaternion(q, q);
202
+ getQuaternionEuler(euler, q, order);
203
+ setQuaternionFromEuler(back, euler.x, euler.y, euler.z, order);
204
+ expect(Math.abs(getQuaternionDot(q, back))).toBeCloseTo(1, 5);
205
+ }
206
+ },
207
+ );
208
+
209
+ it.each(['XYZ', 'XZY', 'YXZ', 'YZX', 'ZXY', 'ZYX'] as const)(
210
+ 'round-trips at the gimbal singularity (middle axis near ±90°) for %s order',
211
+ (order) => {
212
+ // Drive the middle axis of each order to ≈ +90° to hit the singular branch, then confirm
213
+ // the decomposition still represents the same rotation.
214
+ const q = createQuaternion();
215
+ setQuaternionFromEuler(q, Math.PI / 2, Math.PI / 2, Math.PI / 2, order);
216
+ const euler = createVector3();
217
+ getQuaternionEuler(euler, q, order);
218
+ const back = createQuaternion();
219
+ setQuaternionFromEuler(back, euler.x, euler.y, euler.z, order);
220
+ expect(Math.abs(getQuaternionDot(q, back))).toBeCloseTo(1, 6);
221
+ },
222
+ );
223
+ });
224
+
225
+ describe('inverseQuaternion', () => {
226
+ it('q * inverse(q) ≈ identity for a unit quaternion', () => {
227
+ const q = createQuaternion();
228
+ setQuaternionFromAxisAngle(q, createVector3(0, 1, 0), Math.PI / 3);
229
+ const inv = createQuaternion();
230
+ inverseQuaternion(inv, q);
231
+ const out = createQuaternion();
232
+ multiplyQuaternion(out, q, inv);
233
+ expectQuaternionClose(out, 0, 0, 0, 1);
234
+ });
235
+
236
+ it('supports out === source', () => {
237
+ const q = createQuaternion();
238
+ setQuaternionFromAxisAngle(q, createVector3(1, 0, 0), Math.PI / 4);
239
+ const expected = createQuaternion();
240
+ inverseQuaternion(expected, q);
241
+ inverseQuaternion(q, q);
242
+ expectQuaternionClose(q, expected.x, expected.y, expected.z, expected.w);
243
+ });
244
+ });
245
+
246
+ describe('multiplyQuaternion', () => {
247
+ it('multiplying by identity returns the original', () => {
248
+ const a = createQuaternion(0.5, 0.5, 0.5, 0.5);
249
+ const id = createQuaternion();
250
+ const out = createQuaternion();
251
+ multiplyQuaternion(out, a, id);
252
+ expectQuaternionClose(out, 0.5, 0.5, 0.5, 0.5);
253
+ });
254
+
255
+ it('composes two axis rotations into a 180-degree turn about z', () => {
256
+ const z = createVector3(0, 0, 1);
257
+ const a = createQuaternion();
258
+ const b = createQuaternion();
259
+ setQuaternionFromAxisAngle(a, z, Math.PI / 2);
260
+ setQuaternionFromAxisAngle(b, z, Math.PI / 2);
261
+ const out = createQuaternion();
262
+ multiplyQuaternion(out, a, b);
263
+ // 90 + 90 = 180 about z => (0,0,1,0)
264
+ expectQuaternionClose(out, 0, 0, 1, 0);
265
+ });
266
+
267
+ it('supports out === a', () => {
268
+ const a = createQuaternion(0.5, 0.5, 0.5, 0.5);
269
+ const id = createQuaternion();
270
+ multiplyQuaternion(a, a, id);
271
+ expectQuaternionClose(a, 0.5, 0.5, 0.5, 0.5);
272
+ });
273
+
274
+ it('supports out === b', () => {
275
+ const id = createQuaternion();
276
+ const b = createQuaternion(0.5, 0.5, 0.5, 0.5);
277
+ multiplyQuaternion(b, id, b);
278
+ expectQuaternionClose(b, 0.5, 0.5, 0.5, 0.5);
279
+ });
280
+ });
281
+
282
+ describe('normalizeQuaternion', () => {
283
+ it('produces a unit quaternion and returns the original length', () => {
284
+ const q = createQuaternion(0, 0, 0, 2);
285
+ const out = createQuaternion();
286
+ const l = normalizeQuaternion(out, q);
287
+ expect(l).toBeCloseTo(2, 6);
288
+ expectQuaternionClose(out, 0, 0, 0, 1);
289
+ });
290
+
291
+ it('maps a zero quaternion to identity', () => {
292
+ const q = createQuaternion(0, 0, 0, 0);
293
+ const out = createQuaternion();
294
+ const l = normalizeQuaternion(out, q);
295
+ expect(l).toBe(0);
296
+ expectQuaternionClose(out, 0, 0, 0, 1);
297
+ });
298
+
299
+ it('supports out === source', () => {
300
+ const q = createQuaternion(0, 3, 0, 0);
301
+ normalizeQuaternion(q, q);
302
+ expectQuaternionClose(q, 0, 1, 0, 0);
303
+ });
304
+ });
305
+
306
+ describe('rotateVector3ByQuaternion', () => {
307
+ it('90-degree rotation about z maps +x to +y', () => {
308
+ const q = createQuaternion();
309
+ setQuaternionFromAxisAngle(q, createVector3(0, 0, 1), Math.PI / 2);
310
+ const v = createVector3(1, 0, 0);
311
+ const out = createVector3();
312
+ rotateVector3ByQuaternion(out, v, q);
313
+ expect(out.x).toBeCloseTo(0, 5);
314
+ expect(out.y).toBeCloseTo(1, 5);
315
+ expect(out.z).toBeCloseTo(0, 5);
316
+ });
317
+
318
+ it('identity quaternion leaves vector unchanged', () => {
319
+ const q = createQuaternion();
320
+ const v = createVector3(3, 4, 5);
321
+ const out = createVector3();
322
+ rotateVector3ByQuaternion(out, v, q);
323
+ expect(out.x).toBeCloseTo(3, 5);
324
+ expect(out.y).toBeCloseTo(4, 5);
325
+ expect(out.z).toBeCloseTo(5, 5);
326
+ });
327
+
328
+ it('supports out === vector', () => {
329
+ const q = createQuaternion();
330
+ setQuaternionFromAxisAngle(q, createVector3(0, 0, 1), Math.PI / 2);
331
+ const v = createVector3(1, 0, 0);
332
+ rotateVector3ByQuaternion(v, v, q);
333
+ expect(v.x).toBeCloseTo(0, 5);
334
+ expect(v.y).toBeCloseTo(1, 5);
335
+ });
336
+ });
337
+
338
+ describe('setMatrix4FromQuaternion', () => {
339
+ it('a 90-degree rotation about z maps +x to +y', () => {
340
+ const q = createQuaternion();
341
+ setQuaternionFromAxisAngle(q, createVector3(0, 0, 1), Math.PI / 2);
342
+ const m = createMatrix4();
343
+ setMatrix4FromQuaternion(m, q);
344
+ const out = createVector3();
345
+ matrix4TransformPoint(out, m, createVector3(1, 0, 0));
346
+ expect(out.x).toBeCloseTo(0, 6);
347
+ expect(out.y).toBeCloseTo(1, 6);
348
+ expect(out.z).toBeCloseTo(0, 6);
349
+ });
350
+
351
+ it('identity quaternion yields the identity matrix', () => {
352
+ const m = createMatrix4();
353
+ setMatrix4FromQuaternion(m, createQuaternion());
354
+ const out = createVector3();
355
+ matrix4TransformPoint(out, m, createVector3(3, 5, 7));
356
+ expect(out.x).toBeCloseTo(3, 6);
357
+ expect(out.y).toBeCloseTo(5, 6);
358
+ expect(out.z).toBeCloseTo(7, 6);
359
+ });
360
+ });
361
+
362
+ describe('setQuaternionFromAxisAngle', () => {
363
+ it('builds a half-angle quaternion', () => {
364
+ const q = createQuaternion();
365
+ setQuaternionFromAxisAngle(q, createVector3(0, 1, 0), Math.PI / 2);
366
+ expectQuaternionClose(q, 0, Math.sin(Math.PI / 4), 0, Math.cos(Math.PI / 4));
367
+ });
368
+
369
+ it('zero angle yields identity', () => {
370
+ const q = createQuaternion();
371
+ setQuaternionFromAxisAngle(q, createVector3(1, 0, 0), 0);
372
+ expectQuaternionClose(q, 0, 0, 0, 1);
373
+ });
374
+ });
375
+
376
+ describe('setQuaternionFromEuler', () => {
377
+ it('zero angles yield identity', () => {
378
+ const q = createQuaternion(1, 2, 3, 4);
379
+ setQuaternionFromEuler(q, 0, 0, 0, 'XYZ');
380
+ expectQuaternionClose(q, 0, 0, 0, 1);
381
+ });
382
+
383
+ it('90 degrees about X only', () => {
384
+ const q = createQuaternion();
385
+ setQuaternionFromEuler(q, Math.PI / 2, 0, 0, 'XYZ');
386
+ expectQuaternionClose(q, Math.sin(Math.PI / 4), 0, 0, Math.cos(Math.PI / 4));
387
+ });
388
+ });
389
+
390
+ describe('setQuaternionFromMatrix4', () => {
391
+ it('round-trips through setMatrix4FromQuaternion', () => {
392
+ const axis = createVector3(0, 0, 1);
393
+ const q = createQuaternion();
394
+ setQuaternionFromAxisAngle(q, axis, Math.PI / 3);
395
+ const m = createMatrix4();
396
+ setMatrix4FromQuaternion(m, q);
397
+
398
+ const back = createQuaternion();
399
+ setQuaternionFromMatrix4(back, m);
400
+ // Quaternion and its negation represent the same rotation; compare via |dot|.
401
+ const dot = back.x * q.x + back.y * q.y + back.z * q.z + back.w * q.w;
402
+ expect(Math.abs(dot)).toBeCloseTo(1, 6);
403
+ });
404
+
405
+ it('identity matrix yields identity quaternion', () => {
406
+ const back = createQuaternion();
407
+ setQuaternionFromMatrix4(back, createMatrix4());
408
+ expectQuaternionClose(back, 0, 0, 0, 1);
409
+ });
410
+ });
411
+
412
+ describe('setQuaternionFromUnitVectors', () => {
413
+ it('+x to +y yields 90-degree rotation about z', () => {
414
+ const q = createQuaternion();
415
+ const from = createVector3(1, 0, 0);
416
+ const to = createVector3(0, 1, 0);
417
+ setQuaternionFromUnitVectors(q, from, to);
418
+ const out = createVector3();
419
+ rotateVector3ByQuaternion(out, from, q);
420
+ expect(out.x).toBeCloseTo(0, 5);
421
+ expect(out.y).toBeCloseTo(1, 5);
422
+ expect(out.z).toBeCloseTo(0, 5);
423
+ });
424
+
425
+ it('same direction yields identity', () => {
426
+ const q = createQuaternion();
427
+ const v = createVector3(0, 1, 0);
428
+ setQuaternionFromUnitVectors(q, v, v);
429
+ expectQuaternionClose(q, 0, 0, 0, 1);
430
+ });
431
+
432
+ it('antiparallel vectors produce a 180-degree rotation', () => {
433
+ const q = createQuaternion();
434
+ setQuaternionFromUnitVectors(q, createVector3(1, 0, 0), createVector3(-1, 0, 0));
435
+ const out = createVector3();
436
+ rotateVector3ByQuaternion(out, createVector3(1, 0, 0), q);
437
+ expect(out.x).toBeCloseTo(-1, 5);
438
+ });
439
+
440
+ // The antiparallel branch picks a perpendicular axis differently depending on whether `from`
441
+ // is near the X axis; cover the non-X branch (`from` along +Y) so both perpendicular-axis
442
+ // selections are exercised and the result stays a valid unit quaternion that flips the vector.
443
+ it('antiparallel along +Y produces a valid 180-degree rotation', () => {
444
+ const q = createQuaternion();
445
+ setQuaternionFromUnitVectors(q, createVector3(0, 1, 0), createVector3(0, -1, 0));
446
+ expect(Math.hypot(q.x, q.y, q.z, q.w)).toBeCloseTo(1, 6);
447
+ const out = createVector3();
448
+ rotateVector3ByQuaternion(out, createVector3(0, 1, 0), q);
449
+ expect(out.x).toBeCloseTo(0, 5);
450
+ expect(out.y).toBeCloseTo(-1, 5);
451
+ expect(out.z).toBeCloseTo(0, 5);
452
+ });
453
+ });
454
+
455
+ describe('setQuaternionIdentity', () => {
456
+ it('resets to identity', () => {
457
+ const q = createQuaternion(1, 2, 3, 4);
458
+ setQuaternionIdentity(q);
459
+ expectQuaternionClose(q, 0, 0, 0, 1);
460
+ });
461
+ });
462
+
463
+ describe('setQuaternionLookRotation', () => {
464
+ it('forward parallel to up yields identity (degenerate case)', () => {
465
+ const q = createQuaternion(1, 2, 3, 4);
466
+ setQuaternionLookRotation(q, createVector3(0, 1, 0), createVector3(0, 1, 0));
467
+ expectQuaternionClose(q, 0, 0, 0, 1);
468
+ });
469
+
470
+ it('zero forward yields identity (degenerate case)', () => {
471
+ const q = createQuaternion(1, 2, 3, 4);
472
+ setQuaternionLookRotation(q, createVector3(0, 0, 0), createVector3(0, 1, 0));
473
+ expectQuaternionClose(q, 0, 0, 0, 1);
474
+ });
475
+
476
+ it('consistent result: calling twice with same inputs produces same output', () => {
477
+ const q1 = createQuaternion();
478
+ const q2 = createQuaternion();
479
+ setQuaternionLookRotation(q1, createVector3(1, 0, 0), createVector3(0, 1, 0));
480
+ setQuaternionLookRotation(q2, createVector3(1, 0, 0), createVector3(0, 1, 0));
481
+ expectQuaternionClose(q1, q2.x, q2.y, q2.z, q2.w);
482
+ });
483
+
484
+ it('+Z forward with +Y up yields identity', () => {
485
+ const q = createQuaternion();
486
+ setQuaternionLookRotation(q, createVector3(0, 0, 1), createVector3(0, 1, 0));
487
+ expectQuaternionClose(q, 0, 0, 0, 1);
488
+ });
489
+ });
490
+
491
+ describe('slerpQuaternion', () => {
492
+ it('returns a at t=0 and b at t=1', () => {
493
+ const a = createQuaternion();
494
+ const b = createQuaternion();
495
+ setQuaternionFromAxisAngle(b, createVector3(0, 1, 0), Math.PI / 2);
496
+
497
+ const out = createQuaternion();
498
+ slerpQuaternion(out, a, b, 0);
499
+ expectQuaternionClose(out, a.x, a.y, a.z, a.w);
500
+ slerpQuaternion(out, a, b, 1);
501
+ expectQuaternionClose(out, b.x, b.y, b.z, b.w);
502
+ });
503
+
504
+ it('halfway interpolates the angle', () => {
505
+ const a = createQuaternion();
506
+ const b = createQuaternion();
507
+ setQuaternionFromAxisAngle(b, createVector3(0, 1, 0), Math.PI / 2);
508
+
509
+ const out = createQuaternion();
510
+ slerpQuaternion(out, a, b, 0.5);
511
+
512
+ const expected = createQuaternion();
513
+ setQuaternionFromAxisAngle(expected, createVector3(0, 1, 0), Math.PI / 4);
514
+ expectQuaternionClose(out, expected.x, expected.y, expected.z, expected.w);
515
+ });
516
+
517
+ it('supports out === a', () => {
518
+ const a = createQuaternion();
519
+ const b = createQuaternion();
520
+ setQuaternionFromAxisAngle(b, createVector3(0, 1, 0), Math.PI / 2);
521
+ slerpQuaternion(a, a, b, 1);
522
+ expectQuaternionClose(a, b.x, b.y, b.z, b.w);
523
+ });
524
+
525
+ it('supports out === b', () => {
526
+ const a = createQuaternion(1, 0, 0, 0);
527
+ const b = createQuaternion();
528
+ slerpQuaternion(b, a, b, 0);
529
+ expectQuaternionClose(b, a.x, a.y, a.z, a.w);
530
+ });
531
+ });
@@ -0,0 +1,64 @@
1
+ import {
2
+ acquireIdentityQuaternion,
3
+ acquireQuaternion,
4
+ clearQuaternionPool,
5
+ releaseQuaternion,
6
+ } from '@flighthq/geometry';
7
+
8
+ describe('acquireIdentityQuaternion', () => {
9
+ it('returns an identity quaternion', () => {
10
+ clearQuaternionPool();
11
+ const q = acquireIdentityQuaternion();
12
+ expect(q.x).toBe(0);
13
+ expect(q.y).toBe(0);
14
+ expect(q.z).toBe(0);
15
+ expect(q.w).toBe(1);
16
+ releaseQuaternion(q);
17
+ });
18
+
19
+ it('resets a recycled quaternion to identity', () => {
20
+ clearQuaternionPool();
21
+ const q = acquireQuaternion();
22
+ q.x = 9;
23
+ q.w = 0;
24
+ releaseQuaternion(q);
25
+ const reused = acquireIdentityQuaternion();
26
+ expect(reused).toBe(q);
27
+ expect(reused.x).toBe(0);
28
+ expect(reused.w).toBe(1);
29
+ });
30
+ });
31
+
32
+ describe('acquireQuaternion', () => {
33
+ it('reuses a released quaternion', () => {
34
+ clearQuaternionPool();
35
+ const q = acquireQuaternion();
36
+ releaseQuaternion(q);
37
+ expect(acquireQuaternion()).toBe(q);
38
+ });
39
+
40
+ it('allocates when the pool is empty', () => {
41
+ clearQuaternionPool();
42
+ const a = acquireQuaternion();
43
+ const b = acquireQuaternion();
44
+ expect(a).not.toBe(b);
45
+ });
46
+ });
47
+
48
+ describe('clearQuaternionPool', () => {
49
+ it('empties the pool so acquire allocates fresh', () => {
50
+ const q = acquireQuaternion();
51
+ releaseQuaternion(q);
52
+ clearQuaternionPool();
53
+ expect(acquireQuaternion()).not.toBe(q);
54
+ });
55
+ });
56
+
57
+ describe('releaseQuaternion', () => {
58
+ it('ignores a falsy value', () => {
59
+ clearQuaternionPool();
60
+
61
+ expect(() => releaseQuaternion(null as any)).not.toThrow();
62
+ expect(acquireQuaternion()).toBeDefined();
63
+ });
64
+ });