glmaths 0.0.1

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.
@@ -0,0 +1,418 @@
1
+ import { describe, it } from 'node:test'
2
+ import assert from 'node:assert/strict'
3
+ import { Quat, Vec3, vec3, Vec4, Mat3 } from '../dist/esm/glmaths'
4
+
5
+ function closeTo(actual: number, expected: number, numDigits = 5) {
6
+ const pass = Math.abs(actual - expected) < Math.pow(10, -numDigits) / 2
7
+ assert.ok(pass, `expected ${actual} to be close to ${expected}`)
8
+ }
9
+
10
+ describe('Quat', () => {
11
+ describe('constructor', () => {
12
+ it('defaults to identity (0,0,0,1)', () => {
13
+ const q = new Quat()
14
+ assert.strictEqual(q[0], 0)
15
+ assert.strictEqual(q[1], 0)
16
+ assert.strictEqual(q[2], 0)
17
+ assert.strictEqual(q[3], 1)
18
+ })
19
+ it('creates with given values', () => {
20
+ const q = new Quat(1, 2, 3, 4)
21
+ assert.strictEqual(q[0], 1)
22
+ assert.strictEqual(q[1], 2)
23
+ assert.strictEqual(q[2], 3)
24
+ assert.strictEqual(q[3], 4)
25
+ })
26
+ })
27
+
28
+ describe('identity', () => {
29
+ it('creates identity quaternion', () => {
30
+ const q = Quat.identity
31
+ assert.strictEqual(q[0], 0)
32
+ assert.strictEqual(q[1], 0)
33
+ assert.strictEqual(q[2], 0)
34
+ assert.strictEqual(q[3], 1)
35
+ })
36
+ })
37
+
38
+ describe('multiply (Hamilton product)', () => {
39
+ it('identity * identity = identity', () => {
40
+ const a = Quat.identity
41
+ const b = Quat.identity
42
+ const out = new Quat()
43
+ a.multiply(b, out)
44
+ closeTo(out[0], 0)
45
+ closeTo(out[1], 0)
46
+ closeTo(out[2], 0)
47
+ closeTo(out[3], 1)
48
+ })
49
+ it('identity * q = q', () => {
50
+ const q = new Quat(1, 2, 3, 4)
51
+ const out = new Quat()
52
+ Quat.identity.multiply(q, out)
53
+ closeTo(out[0], 1)
54
+ closeTo(out[1], 2)
55
+ closeTo(out[2], 3)
56
+ closeTo(out[3], 4)
57
+ })
58
+ it('q * conjugate(q) = unit quaternion', () => {
59
+ const q = Quat.fromAxisAngle(new Vec3(0, 1, 0), Math.PI / 4)
60
+ const conj = new Quat()
61
+ Quat.conjugate(q, conj)
62
+ const out = new Quat()
63
+ q.multiply(conj, out)
64
+ closeTo(out[0], 0)
65
+ closeTo(out[1], 0)
66
+ closeTo(out[2], 0)
67
+ closeTo(out[3], 1, 4)
68
+ })
69
+ it('scalar multiply scales all components', () => {
70
+ const q = new Quat(1, 2, 3, 4)
71
+ const out = new Quat()
72
+ q.multiply(2, out)
73
+ closeTo(out[0], 2)
74
+ closeTo(out[1], 4)
75
+ closeTo(out[2], 6)
76
+ closeTo(out[3], 8)
77
+ })
78
+ it('mult/mul/times are aliases for multiply', () => {
79
+ const q = Quat.identity
80
+ assert.strictEqual(q.mult, q.multiply)
81
+ assert.strictEqual(q.mul, q.multiply)
82
+ assert.strictEqual(q.times, q.multiply)
83
+ })
84
+ it('composing two 90-degree rotations around Y gives 180-degree', () => {
85
+ const q90 = Quat.fromAxisAngle(new Vec3(0, 1, 0), Math.PI / 2)
86
+ let q = Quat.identity
87
+ q = q.rotateY(Math.PI / 2)
88
+ q = q.rotateY(Math.PI / 2)
89
+ const q180 = Quat.fromAxisAngle(new Vec3(0, 1, 0), Math.PI)
90
+ const dot = q[0] * q180[0] + q[1] * q180[1] + q[2] * q180[2] + q[3] * q180[3]
91
+ closeTo(Math.abs(dot), 1, 4)
92
+ })
93
+ it('composing X then Y rotation produces correct result', () => {
94
+ const q = Quat.identity
95
+ q.rotateX(Math.PI / 2)
96
+ q.rotateY(Math.PI / 2)
97
+ const len = Math.sqrt(q[0] ** 2 + q[1] ** 2 + q[2] ** 2 + q[3] ** 2)
98
+ closeTo(len, 1, 4)
99
+ })
100
+ })
101
+
102
+ describe('fromAxisAngle', () => {
103
+ it('creates quaternion from axis and angle', () => {
104
+ const q = Quat.fromAxisAngle(new Vec3(0, 1, 0), Math.PI / 2)
105
+ closeTo(q[0], 0)
106
+ closeTo(q[1], Math.sin(Math.PI / 4))
107
+ closeTo(q[2], 0)
108
+ closeTo(q[3], Math.cos(Math.PI / 4))
109
+ })
110
+ it('zero angle gives identity', () => {
111
+ const q = Quat.fromAxisAngle(new Vec3(1, 0, 0), 0)
112
+ closeTo(q[0], 0)
113
+ closeTo(q[3], 1)
114
+ })
115
+ })
116
+
117
+ describe('setAxisAngle', () => {
118
+ it('sets quaternion to axis/angle', () => {
119
+ const q = new Quat()
120
+ const r = q.setAxisAngle(new Vec3(0, 0, 1), Math.PI / 2)
121
+ closeTo(r[2], Math.sin(Math.PI / 4))
122
+ closeTo(r[3], Math.cos(Math.PI / 4))
123
+ })
124
+ })
125
+
126
+ describe('getAxisAngle', () => {
127
+ it('recovers axis from fromAxisAngle', () => {
128
+ const axis = new Vec3(0, 1, 0)
129
+ const q = Quat.fromAxisAngle(axis, Math.PI / 3)
130
+ const recovered = vec3()
131
+ q.getAxisAngle(recovered)
132
+ closeTo(recovered[0], 0)
133
+ closeTo(recovered[1], 1)
134
+ closeTo(recovered[2], 0)
135
+ })
136
+ })
137
+
138
+ describe('angle', () => {
139
+ it('angle between identical quaternions is 0', () => {
140
+ const q = Quat.fromAxisAngle(new Vec3(0, 1, 0), Math.PI / 4)
141
+ closeTo(Quat.angle(q, q), 0, 2)
142
+ })
143
+ })
144
+
145
+ describe('rotateX / rotateY / rotateZ', () => {
146
+ it('rotateX from identity', () => {
147
+ const q = Quat.identity
148
+ const r = q.rotateX(Math.PI / 2)
149
+ closeTo(r[0], Math.sin(Math.PI / 4))
150
+ closeTo(r[3], Math.cos(Math.PI / 4))
151
+ })
152
+ it('rotateY from identity', () => {
153
+ const q = Quat.identity
154
+ const r = q.rotateY(Math.PI / 2)
155
+ closeTo(r[1], Math.sin(Math.PI / 4))
156
+ closeTo(r[3], Math.cos(Math.PI / 4))
157
+ })
158
+ it('rotateZ from identity', () => {
159
+ const q = Quat.identity
160
+ const r = q.rotateZ(Math.PI / 2)
161
+ closeTo(r[2], Math.sin(Math.PI / 4))
162
+ closeTo(r[3], Math.cos(Math.PI / 4))
163
+ })
164
+ })
165
+
166
+ describe('calculateW', () => {
167
+ it('recovers W for unit quaternion', () => {
168
+ const q = Quat.fromAxisAngle(new Vec3(0, 1, 0), Math.PI / 4)
169
+ const w = q.calculateW()
170
+ closeTo(w, q[3], 4)
171
+ })
172
+ })
173
+
174
+ describe('exp / ln', () => {
175
+ it('ln(exp(q)) ~= q for small q', () => {
176
+ const q = new Quat(0.1, 0.2, 0.3, 0)
177
+ const out = new Quat()
178
+ Quat.exp(q, out)
179
+ const back = new Quat()
180
+ Quat.ln(out, back)
181
+ closeTo(back[0], q[0], 4)
182
+ closeTo(back[1], q[1], 4)
183
+ closeTo(back[2], q[2], 4)
184
+ })
185
+ it('instance exp/ln roundtrip', () => {
186
+ const q = new Quat(0.1, 0.2, 0.3, 0)
187
+ const e = new Quat()
188
+ q.exp(e)
189
+ const back = new Quat()
190
+ e.ln(back)
191
+ closeTo(back[0], q[0], 4)
192
+ })
193
+ })
194
+
195
+ describe('pow', () => {
196
+ it('q^1 ~= q for unit quaternion', () => {
197
+ const q = Quat.fromAxisAngle(new Vec3(0, 1, 0), Math.PI / 4)
198
+ const original = new Quat(q[0], q[1], q[2], q[3])
199
+ q.pow(1)
200
+ closeTo(q[0], original[0], 3)
201
+ closeTo(q[1], original[1], 3)
202
+ closeTo(q[2], original[2], 3)
203
+ closeTo(q[3], original[3], 3)
204
+ })
205
+ })
206
+
207
+ describe('slerp', () => {
208
+ it('slerp(a, b, 0) = a', () => {
209
+ const a = Quat.fromAxisAngle(new Vec3(0, 1, 0), 0)
210
+ const b = Quat.fromAxisAngle(new Vec3(0, 1, 0), Math.PI / 2)
211
+ const out = Quat.slerp(a, b, 0)
212
+ closeTo(out[0], a[0])
213
+ closeTo(out[1], a[1])
214
+ closeTo(out[2], a[2])
215
+ closeTo(out[3], a[3])
216
+ })
217
+ it('slerp(a, b, 1) = b', () => {
218
+ const a = Quat.fromAxisAngle(new Vec3(0, 1, 0), 0)
219
+ const b = Quat.fromAxisAngle(new Vec3(0, 1, 0), Math.PI / 2)
220
+ const out = Quat.slerp(a, b, 1)
221
+ closeTo(out[0], b[0])
222
+ closeTo(out[1], b[1])
223
+ closeTo(out[2], b[2])
224
+ closeTo(out[3], b[3])
225
+ })
226
+ it('slerp(a, a, t) = a', () => {
227
+ const a = Quat.fromAxisAngle(new Vec3(0, 1, 0), Math.PI / 4)
228
+ const out = Quat.slerp(a, a, 0.5)
229
+ closeTo(out[0], a[0])
230
+ closeTo(out[1], a[1])
231
+ closeTo(out[2], a[2])
232
+ closeTo(out[3], a[3])
233
+ })
234
+ it('instance slerp', () => {
235
+ const a = Quat.fromAxisAngle(new Vec3(0, 1, 0), 0)
236
+ const b = Quat.fromAxisAngle(new Vec3(0, 1, 0), Math.PI / 2)
237
+ const out = new Quat()
238
+ a.slerp(b, 0.5, out)
239
+ const halfQ = Quat.fromAxisAngle(new Vec3(0, 1, 0), Math.PI / 4)
240
+ closeTo(out[1], halfQ[1], 4)
241
+ closeTo(out[3], halfQ[3], 4)
242
+ })
243
+ })
244
+
245
+ describe('invert', () => {
246
+ it('static invert gives conjugate for unit quaternion', () => {
247
+ const q = Quat.fromAxisAngle(new Vec3(0, 1, 0), Math.PI / 4)
248
+ const inv = Quat.invert(q)
249
+ closeTo(inv[0], -q[0])
250
+ closeTo(inv[1], -q[1])
251
+ closeTo(inv[2], -q[2])
252
+ closeTo(inv[3], q[3])
253
+ })
254
+ it('instance invert', () => {
255
+ const q = Quat.fromAxisAngle(new Vec3(0, 1, 0), Math.PI / 4)
256
+ const out = new Quat()
257
+ q.invert(out)
258
+ closeTo(out[1], -q[1])
259
+ })
260
+ })
261
+
262
+ describe('conjugate', () => {
263
+ it('static conjugate negates xyz', () => {
264
+ const q = new Quat(1, 2, 3, 4)
265
+ const out = Quat.conjugate(q)
266
+ assert.strictEqual(out[0], -1)
267
+ assert.strictEqual(out[1], -2)
268
+ assert.strictEqual(out[2], -3)
269
+ assert.strictEqual(out[3], 4)
270
+ })
271
+ it('instance conjugate', () => {
272
+ const q = new Quat(1, 2, 3, 4)
273
+ const r = q.conjugate()
274
+ assert.strictEqual(r[0], -1)
275
+ assert.strictEqual(r[3], 4)
276
+ })
277
+ })
278
+
279
+ describe('fromMat3', () => {
280
+ it('identity matrix gives identity quaternion', () => {
281
+ const q = Quat.fromMat3(Mat3.identity)
282
+ closeTo(q[0], 0)
283
+ closeTo(q[1], 0)
284
+ closeTo(q[2], 0)
285
+ closeTo(q[3], 1)
286
+ })
287
+ it('roundtrips with Mat3.fromQuat', () => {
288
+ const original = Quat.fromAxisAngle(new Vec3(0, 1, 0), Math.PI / 3)
289
+ const mat = Mat3.fromQuat(original)
290
+ const recovered = Quat.fromMat3(mat)
291
+ const dot = original[0] * recovered[0] + original[1] * recovered[1] +
292
+ original[2] * recovered[2] + original[3] * recovered[3]
293
+ closeTo(Math.abs(dot), 1, 4)
294
+ })
295
+ })
296
+
297
+ describe('fromEuler', () => {
298
+ it('zero angles give identity', () => {
299
+ const q = Quat.fromEuler(0, 0, 0)
300
+ closeTo(q[0], 0)
301
+ closeTo(q[1], 0)
302
+ closeTo(q[2], 0)
303
+ closeTo(q[3], 1)
304
+ })
305
+ it('90 degree Y rotation', () => {
306
+ const q = Quat.fromEuler(0, 90, 0, 'zyx')
307
+ const expected = Quat.fromAxisAngle(new Vec3(0, 1, 0), Math.PI / 2)
308
+ const dot = q[0] * expected[0] + q[1] * expected[1] + q[2] * expected[2] + q[3] * expected[3]
309
+ closeTo(Math.abs(dot), 1, 4)
310
+ })
311
+ it('throws for unknown order', () => {
312
+ assert.throws(() => Quat.fromEuler(0, 0, 0, 'abc'), { message: /Unknown angle order/ })
313
+ })
314
+ it('supports all valid orders', () => {
315
+ for (const order of ['xyz', 'xzy', 'yxz', 'yzx', 'zxy', 'zyx']) {
316
+ const q = Quat.fromEuler(30, 45, 60, order)
317
+ assert.ok(q instanceof Quat)
318
+ const len = Math.sqrt(q[0] ** 2 + q[1] ** 2 + q[2] ** 2 + q[3] ** 2)
319
+ closeTo(len, 1, 4)
320
+ }
321
+ })
322
+ })
323
+
324
+ describe('toString', () => {
325
+ it('returns string representation', () => {
326
+ const q = new Quat(1, 2, 3, 4)
327
+ assert.strictEqual(q.toString(), 'quat(1, 2, 3, 4)')
328
+ })
329
+ })
330
+
331
+ describe('rotationTo', () => {
332
+ it('rotation from X to Y axis', () => {
333
+ const q = Quat.rotationTo(new Vec3(1, 0, 0), new Vec3(0, 1, 0))
334
+ const m = Mat3.fromQuat(q)
335
+ const result = new Vec3(
336
+ m[0] * 1 + m[3] * 0 + m[6] * 0,
337
+ m[1] * 1 + m[4] * 0 + m[7] * 0,
338
+ m[2] * 1 + m[5] * 0 + m[8] * 0
339
+ )
340
+ closeTo(result[0], 0)
341
+ closeTo(result[1], 1)
342
+ closeTo(result[2], 0)
343
+ })
344
+ it('rotation from same vector returns identity', () => {
345
+ const q = Quat.rotationTo(new Vec3(1, 0, 0), new Vec3(1, 0, 0))
346
+ closeTo(q[0], 0)
347
+ closeTo(q[1], 0)
348
+ closeTo(q[2], 0)
349
+ closeTo(q[3], 1)
350
+ })
351
+ it('rotation from opposite vectors (180 degrees)', () => {
352
+ const q = Quat.rotationTo(new Vec3(1, 0, 0), new Vec3(-1, 0, 0))
353
+ const len = Math.sqrt(q[0] ** 2 + q[1] ** 2 + q[2] ** 2 + q[3] ** 2)
354
+ closeTo(len, 1, 4)
355
+ })
356
+ })
357
+
358
+ describe('random', () => {
359
+ it('produces unit quaternion', () => {
360
+ const q = Quat.random()
361
+ const len = Math.sqrt(q[0] ** 2 + q[1] ** 2 + q[2] ** 2 + q[3] ** 2)
362
+ closeTo(len, 1, 4)
363
+ })
364
+ })
365
+
366
+ describe('sqlerp', () => {
367
+ it('returns valid quaternion', () => {
368
+ const a = Quat.identity
369
+ const b = Quat.fromAxisAngle(new Vec3(0, 1, 0), Math.PI / 4)
370
+ const c = Quat.fromAxisAngle(new Vec3(0, 1, 0), Math.PI / 2)
371
+ const d = Quat.fromAxisAngle(new Vec3(0, 1, 0), Math.PI * 3 / 4)
372
+ const out = Quat.sqlerp(a, b, c, d, 0.5)
373
+ assert.ok(out instanceof Quat)
374
+ })
375
+ })
376
+
377
+ describe('euler angles', () => {
378
+ it('pitch/yaw/roll from identity is zero', () => {
379
+ const q = Quat.identity
380
+ closeTo(q.pitch(), 0)
381
+ closeTo(q.yaw(), 0)
382
+ closeTo(q.roll(), 0)
383
+ })
384
+ it('eulerAngles returns Vec3', () => {
385
+ const q = Quat.fromAxisAngle(new Vec3(1, 0, 0), Math.PI / 4)
386
+ const e = q.eulerAngles()
387
+ assert.ok(e instanceof Vec3)
388
+ closeTo(e[0], Math.PI / 4)
389
+ })
390
+ })
391
+
392
+ describe('toMat3 / toMat4', () => {
393
+ it('identity quat gives identity mat3', () => {
394
+ const m = Quat.identity.toMat3()
395
+ closeTo(m[0], 1)
396
+ closeTo(m[4], 1)
397
+ closeTo(m[8], 1)
398
+ closeTo(m[1], 0)
399
+ })
400
+ it('identity quat gives identity mat4', () => {
401
+ const m = Quat.identity.toMat4()
402
+ closeTo(m[0], 1)
403
+ closeTo(m[5], 1)
404
+ closeTo(m[10], 1)
405
+ closeTo(m[15], 1)
406
+ closeTo(m[1], 0)
407
+ })
408
+ })
409
+
410
+ describe('quatLookAt', () => {
411
+ it('looking down -Z gives identity-like quat', () => {
412
+ const q = Quat.quatLookAt(new Vec3(0, 0, -1), new Vec3(0, 1, 0))
413
+ const v = new Vec3(0, 0, -1)
414
+ v.transformQuat(q)
415
+ closeTo(v[2], -1, 1)
416
+ })
417
+ })
418
+ })
@@ -0,0 +1,222 @@
1
+ import { describe, it } from 'node:test'
2
+ import assert from 'node:assert/strict'
3
+ import { Quat2, Quat, Vec3, Mat4 } from '../dist/esm/glmaths'
4
+
5
+ function closeTo(actual: number, expected: number, numDigits = 5) {
6
+ const pass = Math.abs(actual - expected) < Math.pow(10, -numDigits) / 2
7
+ assert.ok(pass, `expected ${actual} to be close to ${expected}`)
8
+ }
9
+
10
+ describe('Quat2', () => {
11
+ describe('constructor', () => {
12
+ it('defaults to identity (0,0,0,1, 0,0,0,0)', () => {
13
+ const dq = new Quat2()
14
+ assert.strictEqual(dq[0], 0)
15
+ assert.strictEqual(dq[1], 0)
16
+ assert.strictEqual(dq[2], 0)
17
+ assert.strictEqual(dq[3], 1)
18
+ assert.strictEqual(dq[4], 0)
19
+ assert.strictEqual(dq[5], 0)
20
+ assert.strictEqual(dq[6], 0)
21
+ assert.strictEqual(dq[7], 0)
22
+ })
23
+ it('extends Float32Array with length 8', () => {
24
+ const dq = new Quat2()
25
+ assert.ok(dq instanceof Float32Array)
26
+ assert.strictEqual(dq.length, 8)
27
+ })
28
+ })
29
+
30
+ describe('identity', () => {
31
+ it('creates identity dual quaternion', () => {
32
+ const dq = Quat2.identity
33
+ assert.strictEqual(dq[3], 1)
34
+ assert.strictEqual(dq[0], 0)
35
+ assert.strictEqual(dq[7], 0)
36
+ })
37
+ })
38
+
39
+ describe('fromRotationTranslation', () => {
40
+ it('creates from identity rotation and translation', () => {
41
+ const q = Quat.identity
42
+ const t = new Vec3(2, 4, 6)
43
+ const dq = Quat2.fromRotationTranslation(q, t)
44
+ const out = dq.getTranslation()
45
+ closeTo(out[0], 2)
46
+ closeTo(out[1], 4)
47
+ closeTo(out[2], 6)
48
+ })
49
+ })
50
+
51
+ describe('fromTranslation', () => {
52
+ it('creates from translation only', () => {
53
+ const dq = Quat2.fromTranslation(new Vec3(1, 2, 3))
54
+ const t = dq.getTranslation()
55
+ closeTo(t[0], 1)
56
+ closeTo(t[1], 2)
57
+ closeTo(t[2], 3)
58
+ })
59
+ })
60
+
61
+ describe('fromRotation', () => {
62
+ it('creates from rotation only', () => {
63
+ const q = Quat.fromAxisAngle(new Vec3(0, 1, 0), Math.PI / 2)
64
+ const dq = Quat2.fromRotation(q)
65
+ closeTo(dq[0], q[0])
66
+ closeTo(dq[1], q[1])
67
+ closeTo(dq[2], q[2])
68
+ closeTo(dq[3], q[3])
69
+ assert.strictEqual(dq[4], 0)
70
+ assert.strictEqual(dq[5], 0)
71
+ assert.strictEqual(dq[6], 0)
72
+ assert.strictEqual(dq[7], 0)
73
+ })
74
+ })
75
+
76
+ describe('fromMat4', () => {
77
+ it('round-trips through Mat4', () => {
78
+ const q = Quat.fromAxisAngle(new Vec3(0, 1, 0), Math.PI / 4)
79
+ const t = new Vec3(1, 2, 3)
80
+ const m = Mat4.fromRotationTranslation(q, t)
81
+ const dq = Quat2.fromMat4(m)
82
+ const tOut = dq.getTranslation()
83
+ closeTo(tOut[0], 1)
84
+ closeTo(tOut[1], 2)
85
+ closeTo(tOut[2], 3)
86
+ })
87
+ })
88
+
89
+ describe('getReal / getDual', () => {
90
+ it('gets real part', () => {
91
+ const dq = new Quat2(1, 2, 3, 4, 5, 6, 7, 8)
92
+ const real = dq.getReal()
93
+ assert.strictEqual(real[0], 1)
94
+ assert.strictEqual(real[1], 2)
95
+ assert.strictEqual(real[2], 3)
96
+ assert.strictEqual(real[3], 4)
97
+ })
98
+ it('gets dual part', () => {
99
+ const dq = new Quat2(1, 2, 3, 4, 5, 6, 7, 8)
100
+ const dual = dq.getDual()
101
+ assert.strictEqual(dual[0], 5)
102
+ assert.strictEqual(dual[1], 6)
103
+ assert.strictEqual(dual[2], 7)
104
+ assert.strictEqual(dual[3], 8)
105
+ })
106
+ })
107
+
108
+ describe('multiply', () => {
109
+ it('identity * identity = identity', () => {
110
+ const a = Quat2.identity
111
+ const b = Quat2.identity
112
+ const out = new Quat2()
113
+ a.multiply(b, out)
114
+ closeTo(out[0], 0)
115
+ closeTo(out[1], 0)
116
+ closeTo(out[2], 0)
117
+ closeTo(out[3], 1)
118
+ closeTo(out[4], 0)
119
+ closeTo(out[5], 0)
120
+ closeTo(out[6], 0)
121
+ closeTo(out[7], 0)
122
+ })
123
+ it('combines rotation and translation', () => {
124
+ const rot = Quat2.fromRotation(Quat.fromAxisAngle(new Vec3(0, 1, 0), Math.PI / 2))
125
+ const trans = Quat2.fromTranslation(new Vec3(1, 0, 0))
126
+ const out = new Quat2()
127
+ rot.multiply(trans, out)
128
+ const t = out.getTranslation()
129
+ closeTo(t[0], 0, 1)
130
+ closeTo(t[2], -1, 1)
131
+ })
132
+ })
133
+
134
+ describe('translate', () => {
135
+ it('adds translation', () => {
136
+ const dq = Quat2.fromTranslation(new Vec3(1, 0, 0))
137
+ const r = dq.translate(new Vec3(0, 2, 0))
138
+ const t = r.getTranslation()
139
+ closeTo(t[0], 1)
140
+ closeTo(t[1], 2)
141
+ closeTo(t[2], 0)
142
+ })
143
+ })
144
+
145
+ describe('conjugate', () => {
146
+ it('negates xyz of both parts, keeps w', () => {
147
+ const dq = new Quat2(1, 2, 3, 4, 5, 6, 7, 8)
148
+ const out = new Quat2()
149
+ dq.conjugate(out)
150
+ assert.strictEqual(out[0], -1)
151
+ assert.strictEqual(out[1], -2)
152
+ assert.strictEqual(out[2], -3)
153
+ assert.strictEqual(out[3], 4)
154
+ assert.strictEqual(out[4], -5)
155
+ assert.strictEqual(out[5], -6)
156
+ assert.strictEqual(out[6], -7)
157
+ assert.strictEqual(out[7], 8)
158
+ })
159
+ })
160
+
161
+ describe('normalize', () => {
162
+ it('normalizes the dual quaternion', () => {
163
+ const dq = Quat2.fromRotationTranslation(Quat.identity, new Vec3(1, 2, 3))
164
+ dq.normalize()
165
+ const len = Math.sqrt(dq[0]*dq[0] + dq[1]*dq[1] + dq[2]*dq[2] + dq[3]*dq[3])
166
+ closeTo(len, 1)
167
+ })
168
+ })
169
+
170
+ describe('clone', () => {
171
+ it('creates independent copy', () => {
172
+ const a = new Quat2(1, 2, 3, 4, 5, 6, 7, 8)
173
+ const b = a.clone()
174
+ a[0] = 99
175
+ assert.strictEqual(b[0], 1)
176
+ })
177
+ })
178
+
179
+ describe('equals / exactEquals', () => {
180
+ it('equals returns true for same values', () => {
181
+ const a = new Quat2(1, 2, 3, 4, 5, 6, 7, 8)
182
+ const b = new Quat2(1, 2, 3, 4, 5, 6, 7, 8)
183
+ assert.strictEqual(a.equals(b), true)
184
+ })
185
+ it('exactEquals returns false for different values', () => {
186
+ const a = new Quat2(1, 2, 3, 4, 5, 6, 7, 8)
187
+ const b = new Quat2(1, 2, 3, 4, 5, 6, 7, 9)
188
+ assert.strictEqual(a.exactEquals(b), false)
189
+ })
190
+ })
191
+
192
+ describe('toString', () => {
193
+ it('uses dual number notation', () => {
194
+ const dq = Quat2.identity
195
+ const s = dq.toString()
196
+ assert.ok(s.includes('ε'))
197
+ })
198
+ })
199
+
200
+ describe('lerp', () => {
201
+ it('interpolates at t=0 returns a', () => {
202
+ const a = Quat2.fromTranslation(new Vec3(0, 0, 0))
203
+ const b = Quat2.fromTranslation(new Vec3(10, 0, 0))
204
+ const out = Quat2.lerp(a, b, 0)
205
+ closeTo(out.getTranslation()[0], 0)
206
+ })
207
+ it('interpolates at t=1 returns b', () => {
208
+ const a = Quat2.fromTranslation(new Vec3(0, 0, 0))
209
+ const b = Quat2.fromTranslation(new Vec3(10, 0, 0))
210
+ const out = Quat2.lerp(a, b, 1)
211
+ closeTo(out.getTranslation()[0], 10)
212
+ })
213
+ })
214
+
215
+ describe('dot', () => {
216
+ it('computes dot product of real parts', () => {
217
+ const a = Quat2.identity
218
+ const b = Quat2.identity
219
+ closeTo(Quat2.dot(a, b), 1)
220
+ })
221
+ })
222
+ })