@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.
- package/dist/aabb.d.ts +1 -0
- package/dist/aabb.d.ts.map +1 -1
- package/dist/aabb.js +58 -38
- package/dist/aabb.js.map +1 -1
- package/dist/boundingSphere.d.ts.map +1 -1
- package/dist/boundingSphere.js +3 -2
- package/dist/boundingSphere.js.map +1 -1
- package/dist/capsule.d.ts +6 -5
- package/dist/capsule.d.ts.map +1 -1
- package/dist/capsule.js +38 -15
- package/dist/capsule.js.map +1 -1
- package/dist/contract.d.ts +1 -0
- package/dist/contract.d.ts.map +1 -1
- package/dist/contract.js +1 -0
- package/dist/contract.js.map +1 -1
- package/dist/enableGeometryPoolGuards.d.ts +4 -0
- package/dist/enableGeometryPoolGuards.d.ts.map +1 -0
- package/dist/enableGeometryPoolGuards.js +32 -0
- package/dist/enableGeometryPoolGuards.js.map +1 -0
- package/dist/frustum.d.ts +2 -1
- package/dist/frustum.d.ts.map +1 -1
- package/dist/frustum.js +4 -1
- package/dist/frustum.js.map +1 -1
- package/dist/geometryPoolGuards.d.ts +6 -0
- package/dist/geometryPoolGuards.d.ts.map +1 -0
- package/dist/geometryPoolGuards.js +7 -0
- package/dist/geometryPoolGuards.js.map +1 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/matrix.d.ts +12 -9
- package/dist/matrix.d.ts.map +1 -1
- package/dist/matrix.js +103 -106
- package/dist/matrix.js.map +1 -1
- package/dist/matrix3.d.ts +17 -2
- package/dist/matrix3.d.ts.map +1 -1
- package/dist/matrix3.js +88 -44
- package/dist/matrix3.js.map +1 -1
- package/dist/matrix3Pool.d.ts.map +1 -1
- package/dist/matrix3Pool.js +6 -0
- package/dist/matrix3Pool.js.map +1 -1
- package/dist/matrix4.d.ts +23 -10
- package/dist/matrix4.d.ts.map +1 -1
- package/dist/matrix4.js +74 -53
- package/dist/matrix4.js.map +1 -1
- package/dist/matrix4Pool.d.ts.map +1 -1
- package/dist/matrix4Pool.js +6 -0
- package/dist/matrix4Pool.js.map +1 -1
- package/dist/matrixPool.d.ts.map +1 -1
- package/dist/matrixPool.js +6 -0
- package/dist/matrixPool.js.map +1 -1
- package/dist/obb.d.ts +4 -2
- package/dist/obb.d.ts.map +1 -1
- package/dist/obb.js +164 -88
- package/dist/obb.js.map +1 -1
- package/dist/quaternion.d.ts +7 -4
- package/dist/quaternion.d.ts.map +1 -1
- package/dist/quaternion.js +40 -25
- package/dist/quaternion.js.map +1 -1
- package/dist/quaternionPool.d.ts.map +1 -1
- package/dist/quaternionPool.js +6 -0
- package/dist/quaternionPool.js.map +1 -1
- package/dist/ray3d.d.ts +5 -4
- package/dist/ray3d.d.ts.map +1 -1
- package/dist/ray3d.js +23 -8
- package/dist/ray3d.js.map +1 -1
- package/dist/rectangle.d.ts.map +1 -1
- package/dist/rectangle.js +66 -43
- package/dist/rectangle.js.map +1 -1
- package/dist/rectanglePool.d.ts.map +1 -1
- package/dist/rectanglePool.js +6 -0
- package/dist/rectanglePool.js.map +1 -1
- package/dist/transform2d.d.ts.map +1 -1
- package/dist/transform2d.js +5 -2
- package/dist/transform2d.js.map +1 -1
- package/dist/typedarray.js +3 -6
- package/dist/typedarray.js.map +1 -1
- package/dist/vector2.d.ts.map +1 -1
- package/dist/vector2.js +44 -26
- package/dist/vector2.js.map +1 -1
- package/dist/vector2Pool.d.ts.map +1 -1
- package/dist/vector2Pool.js +6 -0
- package/dist/vector2Pool.js.map +1 -1
- package/dist/vector3.d.ts +2 -2
- package/dist/vector3.d.ts.map +1 -1
- package/dist/vector3.js +55 -37
- package/dist/vector3.js.map +1 -1
- package/dist/vector3Pool.d.ts.map +1 -1
- package/dist/vector3Pool.js +6 -0
- package/dist/vector3Pool.js.map +1 -1
- package/dist/vector4.d.ts +2 -2
- package/dist/vector4.d.ts.map +1 -1
- package/dist/vector4.js +66 -48
- package/dist/vector4.js.map +1 -1
- package/dist/vector4Pool.d.ts.map +1 -1
- package/dist/vector4Pool.js +6 -0
- package/dist/vector4Pool.js.map +1 -1
- package/package.json +4 -3
- package/src/aabb.test.ts +18 -0
- package/src/boundingSphere.test.ts +39 -0
- package/src/capsule.test.ts +158 -0
- package/src/enableGeometryPoolGuards.test.ts +121 -0
- package/src/frustum.test.ts +38 -0
- package/src/geometryPoolGuards.test.ts +16 -0
- package/src/matrix.test.ts +52 -6
- package/src/matrix3.test.ts +46 -6
- package/src/matrix3Pool.test.ts +11 -0
- package/src/matrix4.test.ts +553 -135
- package/src/matrix4Pool.test.ts +11 -0
- package/src/matrixPool.test.ts +11 -0
- package/src/obb.test.ts +277 -0
- package/src/plane.test.ts +20 -0
- package/src/quaternion.test.ts +222 -4
- package/src/quaternionPool.test.ts +12 -0
- package/src/ray3d.test.ts +204 -0
- package/src/rectangle.test.ts +58 -14
- package/src/rectanglePool.test.ts +11 -0
- package/src/transform2d.test.ts +38 -0
- package/src/vector2.test.ts +170 -0
- package/src/vector2Pool.test.ts +11 -0
- package/src/vector3.test.ts +210 -0
- package/src/vector3Pool.test.ts +11 -0
- package/src/vector4.test.ts +231 -0
- package/src/vector4Pool.test.ts +11 -0
package/src/matrix4Pool.test.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { attachEntityBinding, getEntityBinding } from '@flighthq/entity/contract';
|
|
1
2
|
import { getMatrix4Element } from '@flighthq/geometry/contract';
|
|
2
3
|
import type { Matrix4 } from '@flighthq/types/contract';
|
|
3
4
|
|
|
@@ -65,6 +66,16 @@ describe('clearMatrix4Pool', () => {
|
|
|
65
66
|
});
|
|
66
67
|
|
|
67
68
|
describe('releaseMatrix4', () => {
|
|
69
|
+
it('detaches the previous owner binding before the matrix is reused', () => {
|
|
70
|
+
const m = acquireMatrix4();
|
|
71
|
+
attachEntityBinding(m, { owner: 'first' });
|
|
72
|
+
releaseMatrix4(m);
|
|
73
|
+
|
|
74
|
+
const reused = acquireMatrix4();
|
|
75
|
+
expect(reused).toBe(m);
|
|
76
|
+
expect(getEntityBinding(reused)).toBeNull();
|
|
77
|
+
});
|
|
78
|
+
|
|
68
79
|
it('handles null safely', () => {
|
|
69
80
|
expect(() => releaseMatrix4(null as unknown as Matrix4)).not.toThrow();
|
|
70
81
|
});
|
package/src/matrixPool.test.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { attachEntityBinding, getEntityBinding } from '@flighthq/entity/contract';
|
|
1
2
|
import type { Matrix } from '@flighthq/types/contract';
|
|
2
3
|
|
|
3
4
|
import { acquireIdentityMatrix, acquireMatrix, clearMatrixPool, releaseMatrix } from './matrixPool';
|
|
@@ -63,6 +64,16 @@ describe('clearMatrixPool', () => {
|
|
|
63
64
|
});
|
|
64
65
|
|
|
65
66
|
describe('releaseMatrix', () => {
|
|
67
|
+
it('detaches the previous owner binding before the matrix is reused', () => {
|
|
68
|
+
const m = acquireMatrix();
|
|
69
|
+
attachEntityBinding(m, { owner: 'first' });
|
|
70
|
+
releaseMatrix(m);
|
|
71
|
+
|
|
72
|
+
const reused = acquireMatrix();
|
|
73
|
+
expect(reused).toBe(m);
|
|
74
|
+
expect(getEntityBinding(reused)).toBeNull();
|
|
75
|
+
});
|
|
76
|
+
|
|
66
77
|
it('handles null safely', () => {
|
|
67
78
|
expect(() => releaseMatrix(null as unknown as Matrix)).not.toThrow();
|
|
68
79
|
});
|
package/src/obb.test.ts
CHANGED
|
@@ -4,14 +4,19 @@ import {
|
|
|
4
4
|
createObb,
|
|
5
5
|
createRay3D,
|
|
6
6
|
createVector3,
|
|
7
|
+
crossVector3,
|
|
7
8
|
getClosestPointOnObb,
|
|
8
9
|
intersectRay3DObb,
|
|
9
10
|
isObbIntersectingAabb,
|
|
10
11
|
isObbIntersectingObb,
|
|
12
|
+
matrix4TransformPoint,
|
|
13
|
+
normalizeVector3,
|
|
14
|
+
rotateMatrix4,
|
|
11
15
|
setMatrix4Position,
|
|
12
16
|
setObb,
|
|
13
17
|
transformObbByMatrix4,
|
|
14
18
|
} from '@flighthq/geometry/contract';
|
|
19
|
+
import type { Matrix4, Obb, Vector3 } from '@flighthq/types/contract';
|
|
15
20
|
|
|
16
21
|
describe('createObb', () => {
|
|
17
22
|
it('stores center, half-extents, and orientation', () => {
|
|
@@ -91,6 +96,21 @@ describe('intersectRay3DObb', () => {
|
|
|
91
96
|
const ray = createRay3D(0, 0, 0, 1, 0, 0);
|
|
92
97
|
expect(intersectRay3DObb(ray, o)).toBe(-1);
|
|
93
98
|
});
|
|
99
|
+
|
|
100
|
+
it('returns -1 for a ray with no direction, inside the box or out', () => {
|
|
101
|
+
// A zero-length direction is not a ray, so it never hits anything — the same answer the
|
|
102
|
+
// sphere, capsule, plane and triangle tests give, and not a containment test in disguise.
|
|
103
|
+
const o = createObb(0, 0, 0, 1, 1, 1, 0, 0, 0, 1);
|
|
104
|
+
expect(intersectRay3DObb(createRay3D(0.5, 0.5, 0.5, 0, 0, 0), o)).toBe(-1);
|
|
105
|
+
expect(intersectRay3DObb(createRay3D(5, 0, 0, 0, 0, 0), o)).toBe(-1);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it('hits a flat OBB whose half-extent along one axis is zero', () => {
|
|
109
|
+
// A collapsed axis is a plane, not an empty volume: the slab still has a finite entry point.
|
|
110
|
+
const o = createObb(0, 0, 0, 0, 1, 1, 0, 0, 0, 1);
|
|
111
|
+
expect(intersectRay3DObb(createRay3D(5, 0, 0, -1, 0, 0), o)).toBeCloseTo(5, 6);
|
|
112
|
+
expect(intersectRay3DObb(createRay3D(5, 3, 0, -1, 0, 0), o)).toBe(-1);
|
|
113
|
+
});
|
|
94
114
|
});
|
|
95
115
|
|
|
96
116
|
describe('isObbIntersectingAabb', () => {
|
|
@@ -111,6 +131,15 @@ describe('isObbIntersectingAabb', () => {
|
|
|
111
131
|
const a = createAabb(-1, -1, -1, 1, 1, 1);
|
|
112
132
|
expect(isObbIntersectingAabb(o, a)).toBe(false);
|
|
113
133
|
});
|
|
134
|
+
|
|
135
|
+
it.each([
|
|
136
|
+
['x', createAabb(1, 0, 0, 0, 1, 1)],
|
|
137
|
+
['y', createAabb(0, 1, 0, 1, 0, 1)],
|
|
138
|
+
['z', createAabb(0, 0, 1, 1, 1, 0)],
|
|
139
|
+
])('returns false for an AABB empty on %s', (_axis, empty) => {
|
|
140
|
+
const o = createObb(0, 0, 0, 10, 10, 10, 0, 0, 0, 1);
|
|
141
|
+
expect(isObbIntersectingAabb(o, empty)).toBe(false);
|
|
142
|
+
});
|
|
114
143
|
});
|
|
115
144
|
|
|
116
145
|
describe('isObbIntersectingObb', () => {
|
|
@@ -131,6 +160,94 @@ describe('isObbIntersectingObb', () => {
|
|
|
131
160
|
const b = createObb(10, 0, 0, 1, 1, 1, 0, 0, 0, 1);
|
|
132
161
|
expect(isObbIntersectingObb(a, b)).toBe(false);
|
|
133
162
|
});
|
|
163
|
+
|
|
164
|
+
it('remains correct when reading an input reenters another OBB query', () => {
|
|
165
|
+
const quarterTurn = orientationFromAxisAngle(0, 0, 1, Math.PI * 0.25);
|
|
166
|
+
const turned = createObb(0, 0, 0, 1, 1, 1, ...quarterTurn);
|
|
167
|
+
const other = createObb(2.1, 0, 0, 1, 1, 1, 0, 0, 0, 1);
|
|
168
|
+
const nestedA = createObb(0, 0, 0, 1, 1, 1, 0, 0, 0, 1);
|
|
169
|
+
const nestedB = createObb(10, 0, 0, 1, 1, 1, 0, 0, 0, 1);
|
|
170
|
+
let reentered = false;
|
|
171
|
+
Object.defineProperty(other, 'orientationY', {
|
|
172
|
+
get() {
|
|
173
|
+
if (!reentered) {
|
|
174
|
+
reentered = true;
|
|
175
|
+
expect(isObbIntersectingObb(nestedA, nestedB)).toBe(false);
|
|
176
|
+
}
|
|
177
|
+
return 0;
|
|
178
|
+
},
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
// The turned box overlaps at center x=2.1 because its projected radius is sqrt(2). Shared
|
|
182
|
+
// module scratch lets the nested separated query overwrite its axes and falsely returns false.
|
|
183
|
+
expect(isObbIntersectingObb(turned, other)).toBe(true);
|
|
184
|
+
expect(reentered).toBe(true);
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
it('separates on each of the six face axes on its own', () => {
|
|
188
|
+
// Six of the fifteen candidate axes are the two boxes' own faces. Each case below is tuned so
|
|
189
|
+
// that ONE named axis is the only one of the fifteen that separates the pair — pushed just
|
|
190
|
+
// past where that face clears and no further. That is what makes the case pin its axis: drop
|
|
191
|
+
// that single check and the pair reads as a hit, where a pair separated on several axes at
|
|
192
|
+
// once would still be rejected by one of the others and hide the loss.
|
|
193
|
+
const tilt = orientationFromAxisAngle(1, 2, 3, 0.9);
|
|
194
|
+
const tiltAsMatrix = rotationMatrix4(1, 2, 3, 0.9);
|
|
195
|
+
const first = createObb(0, 0, 0, 1, 2, 3, 0, 0, 0, 1);
|
|
196
|
+
|
|
197
|
+
for (const { axis, ofTurnedBox, extents, clear } of FACE_AXIS_SEPARATIONS) {
|
|
198
|
+
const direction = createUnitVector3(axis);
|
|
199
|
+
if (ofTurnedBox) matrix4TransformPoint(direction, tiltAsMatrix, direction);
|
|
200
|
+
|
|
201
|
+
const away = createObb(
|
|
202
|
+
direction.x * clear,
|
|
203
|
+
direction.y * clear,
|
|
204
|
+
direction.z * clear,
|
|
205
|
+
extents[0],
|
|
206
|
+
extents[1],
|
|
207
|
+
extents[2],
|
|
208
|
+
...tilt,
|
|
209
|
+
);
|
|
210
|
+
expect(isObbIntersectingObb(first, away)).toBe(false);
|
|
211
|
+
|
|
212
|
+
const concentric = createObb(0, 0, 0, extents[0], extents[1], extents[2], ...tilt);
|
|
213
|
+
expect(isObbIntersectingObb(first, concentric)).toBe(true);
|
|
214
|
+
}
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
it('separates on every one of the nine edge-cross axes', () => {
|
|
218
|
+
// The nine edge axes are nine hand-written index permutations of one cross-product, so a
|
|
219
|
+
// transposed subscript in any single one is invisible until that exact edge pair decides a
|
|
220
|
+
// case. Each pair below is a rod along one local axis of each box, thin enough and offset
|
|
221
|
+
// along their common perpendicular by little enough that its own cross axis is the only
|
|
222
|
+
// separating axis in the whole set: a wrong expression there reads as a hit.
|
|
223
|
+
const tilt = orientationFromAxisAngle(1, 2, 3, 0.9);
|
|
224
|
+
const tiltAsMatrix = rotationMatrix4(1, 2, 3, 0.9);
|
|
225
|
+
const rodLongDirection = createVector3();
|
|
226
|
+
|
|
227
|
+
for (let firstLongAxis = 0; firstLongAxis < 3; firstLongAxis++) {
|
|
228
|
+
for (let secondLongAxis = 0; secondLongAxis < 3; secondLongAxis++) {
|
|
229
|
+
const first = createRodObb(0, 0, 0, firstLongAxis, 0, 0, 0, 1);
|
|
230
|
+
matrix4TransformPoint(rodLongDirection, tiltAsMatrix, createUnitVector3(secondLongAxis));
|
|
231
|
+
|
|
232
|
+
const perpendicular = createVector3();
|
|
233
|
+
crossVector3(perpendicular, createUnitVector3(firstLongAxis), rodLongDirection);
|
|
234
|
+
normalizeVector3(perpendicular, perpendicular);
|
|
235
|
+
|
|
236
|
+
const clear = 0.3;
|
|
237
|
+
const separated = createRodObb(
|
|
238
|
+
perpendicular.x * clear,
|
|
239
|
+
perpendicular.y * clear,
|
|
240
|
+
perpendicular.z * clear,
|
|
241
|
+
secondLongAxis,
|
|
242
|
+
...tilt,
|
|
243
|
+
);
|
|
244
|
+
expect(isObbIntersectingObb(first, separated)).toBe(false);
|
|
245
|
+
|
|
246
|
+
const concentric = createRodObb(0, 0, 0, secondLongAxis, ...tilt);
|
|
247
|
+
expect(isObbIntersectingObb(first, concentric)).toBe(true);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
});
|
|
134
251
|
});
|
|
135
252
|
|
|
136
253
|
describe('setObb', () => {
|
|
@@ -180,4 +297,164 @@ describe('transformObbByMatrix4', () => {
|
|
|
180
297
|
expect(o.centerY).toBeCloseTo(3, 6);
|
|
181
298
|
expect(o.halfExtentX).toBeCloseTo(1, 6);
|
|
182
299
|
});
|
|
300
|
+
|
|
301
|
+
it('rotates the box the same way the matrix rotates points', () => {
|
|
302
|
+
// The orientation extracted from the matrix must turn the box in the matrix's own direction.
|
|
303
|
+
// A conjugated extraction turns it by the same angle about the opposite axis, which is
|
|
304
|
+
// invisible for axis-aligned boxes and half turns but wrong for every general rotation.
|
|
305
|
+
expectTransformedObbMatchesRotatedPoints(rotationMatrix4(1, 0, 0, 0.7), createTiltedObb());
|
|
306
|
+
expectTransformedObbMatchesRotatedPoints(rotationMatrix4(1, 2, 3, 0.9), createTiltedObb());
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
it('rotates the box correctly near a half turn about each axis', () => {
|
|
310
|
+
// A half turn drives the three `trace <= 0` branches of the matrix-to-quaternion extraction,
|
|
311
|
+
// one per dominant diagonal term. Just short of pi so the conjugate is a different rotation:
|
|
312
|
+
// at exactly pi the quaternion equals its own conjugate up to sign and hides an error.
|
|
313
|
+
const nearHalfTurn = Math.PI - 0.01;
|
|
314
|
+
expectTransformedObbMatchesRotatedPoints(rotationMatrix4(1, 0, 0, nearHalfTurn), createTiltedObb());
|
|
315
|
+
expectTransformedObbMatchesRotatedPoints(rotationMatrix4(0, 1, 0, nearHalfTurn), createTiltedObb());
|
|
316
|
+
expectTransformedObbMatchesRotatedPoints(rotationMatrix4(0, 0, 1, nearHalfTurn), createTiltedObb());
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
it('flattens the box along a collapsed matrix column and keeps the other extents', () => {
|
|
320
|
+
// A zero-length column has no direction to read, so that axis falls back to its identity
|
|
321
|
+
// direction rather than dividing by zero, and the half-extent along it goes to zero.
|
|
322
|
+
const o = createObb(0, 0, 0, 2, 3, 4, 0, 0, 0, 1);
|
|
323
|
+
const m = createMatrix4(0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
|
|
324
|
+
const out = createObb(0, 0, 0, 0, 0, 0, 0, 0, 0, 1);
|
|
325
|
+
transformObbByMatrix4(out, o, m);
|
|
326
|
+
expect(out.halfExtentX).toBe(0);
|
|
327
|
+
expect(out.halfExtentY).toBeCloseTo(3, 6);
|
|
328
|
+
expect(out.halfExtentZ).toBeCloseTo(4, 6);
|
|
329
|
+
expect(out.orientationW).toBeCloseTo(1, 6);
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
it('collapses the box to a point at the matrix position when every column is zero', () => {
|
|
333
|
+
// All three columns collapse, so the fallback rotation is the identity and the box keeps the
|
|
334
|
+
// orientation it came in with — the transform contributes nothing but the translation.
|
|
335
|
+
const tilt = orientationFromAxisAngle(1, 1, 1, 0.6);
|
|
336
|
+
const o = createObb(1, 2, 3, 2, 3, 4, ...tilt);
|
|
337
|
+
const m = createMatrix4(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 5, 6, 7, 1);
|
|
338
|
+
const out = createObb(0, 0, 0, 0, 0, 0, 0, 0, 0, 1);
|
|
339
|
+
transformObbByMatrix4(out, o, m);
|
|
340
|
+
expect(out.centerX).toBeCloseTo(5, 6);
|
|
341
|
+
expect(out.centerY).toBeCloseTo(6, 6);
|
|
342
|
+
expect(out.centerZ).toBeCloseTo(7, 6);
|
|
343
|
+
expect(out.halfExtentX).toBe(0);
|
|
344
|
+
expect(out.halfExtentY).toBe(0);
|
|
345
|
+
expect(out.halfExtentZ).toBe(0);
|
|
346
|
+
expect(out.orientationX).toBeCloseTo(tilt[0], 6);
|
|
347
|
+
expect(out.orientationY).toBeCloseTo(tilt[1], 6);
|
|
348
|
+
expect(out.orientationZ).toBeCloseTo(tilt[2], 6);
|
|
349
|
+
expect(out.orientationW).toBeCloseTo(tilt[3], 6);
|
|
350
|
+
});
|
|
183
351
|
});
|
|
352
|
+
|
|
353
|
+
// A thin rod: long along one of its own local axes, barely thick along the other two, so the
|
|
354
|
+
// dominant feature of the box is a single edge direction.
|
|
355
|
+
function createRodObb(
|
|
356
|
+
centerX: number,
|
|
357
|
+
centerY: number,
|
|
358
|
+
centerZ: number,
|
|
359
|
+
longAxis: number,
|
|
360
|
+
orientationX: number,
|
|
361
|
+
orientationY: number,
|
|
362
|
+
orientationZ: number,
|
|
363
|
+
orientationW: number,
|
|
364
|
+
): Obb {
|
|
365
|
+
const thickness = 0.1;
|
|
366
|
+
return createObb(
|
|
367
|
+
centerX,
|
|
368
|
+
centerY,
|
|
369
|
+
centerZ,
|
|
370
|
+
longAxis === 0 ? 3 : thickness,
|
|
371
|
+
longAxis === 1 ? 3 : thickness,
|
|
372
|
+
longAxis === 2 ? 3 : thickness,
|
|
373
|
+
orientationX,
|
|
374
|
+
orientationY,
|
|
375
|
+
orientationZ,
|
|
376
|
+
orientationW,
|
|
377
|
+
);
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
// An OBB with three distinct half-extents at an orientation that shares no axis with the world,
|
|
381
|
+
// so no box symmetry can mask a wrong rotation. Axis (1, 2, 3) normalized, turned by 0.8 radians.
|
|
382
|
+
function createTiltedObb(): Obb {
|
|
383
|
+
return createObb(0, 0, 0, 3, 2, 1, ...orientationFromAxisAngle(1, 2, 3, 0.8));
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
function createUnitVector3(axis: number): Vector3 {
|
|
387
|
+
return createVector3(axis === 0 ? 1 : 0, axis === 1 ? 1 : 0, axis === 2 ? 1 : 0);
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
// The `orientationX, orientationY, orientationZ, orientationW` arguments `createObb` and `setObb`
|
|
391
|
+
// take, spelled as a turn about an axis so the test states the rotation rather than four decimals.
|
|
392
|
+
function orientationFromAxisAngle(
|
|
393
|
+
axisX: number,
|
|
394
|
+
axisY: number,
|
|
395
|
+
axisZ: number,
|
|
396
|
+
radians: number,
|
|
397
|
+
): [number, number, number, number] {
|
|
398
|
+
const length = Math.sqrt(axisX * axisX + axisY * axisY + axisZ * axisZ);
|
|
399
|
+
const half = Math.sin(radians / 2);
|
|
400
|
+
return [(axisX / length) * half, (axisY / length) * half, (axisZ / length) * half, Math.cos(radians / 2)];
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
// Asserts the transformed OBB sits where the matrix puts the original: for probe points all around
|
|
404
|
+
// it, the closest point on the transformed box equals the transformed closest point on the original.
|
|
405
|
+
// This reads the orientation through the same axes every OBB query uses, so it fails on any
|
|
406
|
+
// orientation error rather than on a chosen quaternion convention.
|
|
407
|
+
function expectTransformedObbMatchesRotatedPoints(m: Readonly<Matrix4>, obb: Readonly<Obb>): void {
|
|
408
|
+
const transformed = createObb(0, 0, 0, 0, 0, 0, 0, 0, 0, 1);
|
|
409
|
+
transformObbByMatrix4(transformed, obb, m);
|
|
410
|
+
|
|
411
|
+
const expected = createVector3();
|
|
412
|
+
const actual = createVector3();
|
|
413
|
+
const probe = createVector3();
|
|
414
|
+
|
|
415
|
+
for (const [px, py, pz] of [
|
|
416
|
+
[10, 0, 0],
|
|
417
|
+
[0, 10, 0],
|
|
418
|
+
[0, 0, 10],
|
|
419
|
+
[4, -6, 2],
|
|
420
|
+
[-5, 3, 8],
|
|
421
|
+
[7, 7, -7],
|
|
422
|
+
]) {
|
|
423
|
+
getClosestPointOnObb(expected, obb, createVector3(px, py, pz));
|
|
424
|
+
matrix4TransformPoint(expected, m, expected);
|
|
425
|
+
|
|
426
|
+
matrix4TransformPoint(probe, m, createVector3(px, py, pz));
|
|
427
|
+
getClosestPointOnObb(actual, transformed, probe);
|
|
428
|
+
|
|
429
|
+
expect(actual.x).toBeCloseTo(expected.x, 4);
|
|
430
|
+
expect(actual.y).toBeCloseTo(expected.y, 4);
|
|
431
|
+
expect(actual.z).toBeCloseTo(expected.z, 4);
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
// `rotateMatrix4` expects a unit axis, so normalize here rather than at every call site.
|
|
436
|
+
function rotationMatrix4(axisX: number, axisY: number, axisZ: number, radians: number): Matrix4 {
|
|
437
|
+
const length = Math.sqrt(axisX * axisX + axisY * axisY + axisZ * axisZ);
|
|
438
|
+
const m = createMatrix4();
|
|
439
|
+
rotateMatrix4(m, m, createVector3(axisX / length, axisY / length, axisZ / length), radians);
|
|
440
|
+
return m;
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
// One case per face axis of the two boxes in `separates on each of the six face axes on its own`.
|
|
444
|
+
// The first box is fixed at half-extents (1, 2, 3); the second carries the listed half-extents and
|
|
445
|
+
// a turn about (1, 2, 3), pushed `clear` along the named axis — `axis` indexes x, y, z, and
|
|
446
|
+
// `ofTurnedBox` says whether that is the second box's own axis rather than a world one. The
|
|
447
|
+
// shapes and distances are the ones for which that axis, and only it, separates the pair.
|
|
448
|
+
const FACE_AXIS_SEPARATIONS: ReadonlyArray<{
|
|
449
|
+
readonly axis: number;
|
|
450
|
+
readonly ofTurnedBox: boolean;
|
|
451
|
+
readonly extents: readonly [number, number, number];
|
|
452
|
+
readonly clear: number;
|
|
453
|
+
}> = [
|
|
454
|
+
{ axis: 0, clear: 3.8, extents: [2.5, 1.5, 0.5], ofTurnedBox: false },
|
|
455
|
+
{ axis: 1, clear: 4.9, extents: [2.5, 1.5, 0.5], ofTurnedBox: false },
|
|
456
|
+
{ axis: 2, clear: 5.9, extents: [0.5, 1.5, 2.5], ofTurnedBox: false },
|
|
457
|
+
{ axis: 0, clear: 3.6, extents: [0.5, 1.5, 2.5], ofTurnedBox: true },
|
|
458
|
+
{ axis: 1, clear: 4.7, extents: [0.5, 1.5, 2.5], ofTurnedBox: true },
|
|
459
|
+
{ axis: 2, clear: 3.7, extents: [2.5, 1.5, 0.5], ofTurnedBox: true },
|
|
460
|
+
];
|
package/src/plane.test.ts
CHANGED
|
@@ -222,4 +222,24 @@ describe('setPlaneFromPoints', () => {
|
|
|
222
222
|
expect(getPlaneSignedDistanceToPoint(p, b)).toBeCloseTo(0, 5);
|
|
223
223
|
expect(getPlaneSignedDistanceToPoint(p, c)).toBeCloseTo(0, 5);
|
|
224
224
|
});
|
|
225
|
+
|
|
226
|
+
it('writes a zero plane for three points that do not span one', () => {
|
|
227
|
+
// Points in a line — or on top of each other — describe no single plane, and there is no
|
|
228
|
+
// normal to scale to unit length. The coefficients come out zero rather than dividing by the
|
|
229
|
+
// zero length, which would put NaN in a plane the caller then culls against.
|
|
230
|
+
const p = createPlane(1, 1, 1, 1);
|
|
231
|
+
setPlaneFromPoints(p, createVector3(0, 0, 0), createVector3(1, 1, 1), createVector3(2, 2, 2));
|
|
232
|
+
expect(p.a).toBe(0);
|
|
233
|
+
expect(p.b).toBe(0);
|
|
234
|
+
expect(p.c).toBe(0);
|
|
235
|
+
expect(p.d).toBe(0);
|
|
236
|
+
|
|
237
|
+
const q = createPlane(1, 1, 1, 1);
|
|
238
|
+
const same = createVector3(4, 5, 6);
|
|
239
|
+
setPlaneFromPoints(q, same, same, same);
|
|
240
|
+
expect(q.a).toBe(0);
|
|
241
|
+
expect(q.b).toBe(0);
|
|
242
|
+
expect(q.c).toBe(0);
|
|
243
|
+
expect(q.d).toBe(0);
|
|
244
|
+
});
|
|
225
245
|
});
|
package/src/quaternion.test.ts
CHANGED
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
matrix4TransformPoint,
|
|
14
14
|
multiplyQuaternion,
|
|
15
15
|
normalizeQuaternion,
|
|
16
|
+
normalizeVector3,
|
|
16
17
|
rotateVector3ByQuaternion,
|
|
17
18
|
setMatrix4FromQuaternion,
|
|
18
19
|
setQuaternion,
|
|
@@ -221,6 +222,21 @@ describe('getQuaternionEuler', () => {
|
|
|
221
222
|
expect(Math.abs(getQuaternionDot(q, back))).toBeCloseTo(1, 6);
|
|
222
223
|
},
|
|
223
224
|
);
|
|
225
|
+
// A Quaternion structurally satisfies Vector3Like, so a caller can pass one object as both the
|
|
226
|
+
// euler output and the rotation source. All four components must be read before the first write.
|
|
227
|
+
it('writes the same values whether out aliases source or not', () => {
|
|
228
|
+
const source = createQuaternion();
|
|
229
|
+
setQuaternionFromEuler(source, 0.3, 0.4, 0.5);
|
|
230
|
+
const distinct = createVector3();
|
|
231
|
+
const aliased = cloneQuaternion(source);
|
|
232
|
+
|
|
233
|
+
getQuaternionEuler(distinct, source);
|
|
234
|
+
getQuaternionEuler(aliased, aliased);
|
|
235
|
+
|
|
236
|
+
expect(aliased.x).toBeCloseTo(distinct.x, 12);
|
|
237
|
+
expect(aliased.y).toBeCloseTo(distinct.y, 12);
|
|
238
|
+
expect(aliased.z).toBeCloseTo(distinct.z, 12);
|
|
239
|
+
});
|
|
224
240
|
});
|
|
225
241
|
|
|
226
242
|
describe('inverseQuaternion', () => {
|
|
@@ -242,6 +258,25 @@ describe('inverseQuaternion', () => {
|
|
|
242
258
|
inverseQuaternion(q, q);
|
|
243
259
|
expectQuaternionClose(q, expected.x, expected.y, expected.z, expected.w);
|
|
244
260
|
});
|
|
261
|
+
|
|
262
|
+
// Inverting divides by the squared length, so an all-zero quaternion is the one input that can
|
|
263
|
+
// reach the caller as NaN. The guard trades it for the identity rotation.
|
|
264
|
+
it('writes the identity rotation for a zero-length quaternion', () => {
|
|
265
|
+
const out = createQuaternion(9, 9, 9, 9);
|
|
266
|
+
|
|
267
|
+
inverseQuaternion(out, createQuaternion(0, 0, 0, 0));
|
|
268
|
+
|
|
269
|
+
expectQuaternionClose(out, 0, 0, 0, 1);
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
it('divides a non-unit quaternion by its squared length', () => {
|
|
273
|
+
const out = createQuaternion();
|
|
274
|
+
|
|
275
|
+
// |q|² = 4, so the inverse is the conjugate scaled by 1/4.
|
|
276
|
+
inverseQuaternion(out, createQuaternion(0, 2, 0, 0));
|
|
277
|
+
|
|
278
|
+
expectQuaternionClose(out, 0, -0.5, 0, 0);
|
|
279
|
+
});
|
|
245
280
|
});
|
|
246
281
|
|
|
247
282
|
describe('multiplyQuaternion', () => {
|
|
@@ -416,6 +451,42 @@ describe('setQuaternionFromMatrix4', () => {
|
|
|
416
451
|
setQuaternionFromMatrix4(back, createMatrix4());
|
|
417
452
|
expectQuaternionClose(back, 0, 0, 0, 1);
|
|
418
453
|
});
|
|
454
|
+
|
|
455
|
+
// Extraction picks one of four formulas by which diagonal term dominates; only the trace case is
|
|
456
|
+
// reached by small rotations. A half turn about each axis selects one of the other three, and the
|
|
457
|
+
// round trip is what proves they agree with setMatrix4FromQuaternion rather than merely running.
|
|
458
|
+
it.each([
|
|
459
|
+
['x', createVector3(1, 0, 0)],
|
|
460
|
+
['y', createVector3(0, 1, 0)],
|
|
461
|
+
['z', createVector3(0, 0, 1)],
|
|
462
|
+
])('round-trips a half turn about %s', (_axis, axis) => {
|
|
463
|
+
const q = createQuaternion();
|
|
464
|
+
setQuaternionFromAxisAngle(q, axis, Math.PI);
|
|
465
|
+
const m = createMatrix4();
|
|
466
|
+
setMatrix4FromQuaternion(m, q);
|
|
467
|
+
|
|
468
|
+
const back = createQuaternion();
|
|
469
|
+
setQuaternionFromMatrix4(back, m);
|
|
470
|
+
|
|
471
|
+
const roundTripped = createMatrix4();
|
|
472
|
+
setMatrix4FromQuaternion(roundTripped, back);
|
|
473
|
+
for (let i = 0; i < 16; i++) {
|
|
474
|
+
expect(roundTripped.m[i]).toBeCloseTo(m.m[i], 6);
|
|
475
|
+
}
|
|
476
|
+
});
|
|
477
|
+
it('writes the same values whether out is fresh or already holds a rotation', () => {
|
|
478
|
+
const m = createMatrix4();
|
|
479
|
+
const q = createQuaternion();
|
|
480
|
+
setQuaternionFromAxisAngle(q, createVector3(0, 1, 0), Math.PI / 3);
|
|
481
|
+
setMatrix4FromQuaternion(m, q);
|
|
482
|
+
|
|
483
|
+
const fresh = createQuaternion();
|
|
484
|
+
const reused = createQuaternion(9, 9, 9, 9);
|
|
485
|
+
setQuaternionFromMatrix4(fresh, m);
|
|
486
|
+
setQuaternionFromMatrix4(reused, m);
|
|
487
|
+
|
|
488
|
+
expectQuaternionClose(reused, fresh.x, fresh.y, fresh.z, fresh.w);
|
|
489
|
+
});
|
|
419
490
|
});
|
|
420
491
|
|
|
421
492
|
describe('setQuaternionFromUnitVectors', () => {
|
|
@@ -459,6 +530,20 @@ describe('setQuaternionFromUnitVectors', () => {
|
|
|
459
530
|
expect(out.y).toBeCloseTo(-1, 5);
|
|
460
531
|
expect(out.z).toBeCloseTo(0, 5);
|
|
461
532
|
});
|
|
533
|
+
it('writes the same values whether out aliases an input or not', () => {
|
|
534
|
+
const from = createVector3(1, 0, 0);
|
|
535
|
+
const to = createVector3(0, 1, 0);
|
|
536
|
+
const distinct = createQuaternion();
|
|
537
|
+
setQuaternionFromUnitVectors(distinct, from, to);
|
|
538
|
+
|
|
539
|
+
const aliasedFrom = createQuaternion(from.x, from.y, from.z, 0);
|
|
540
|
+
setQuaternionFromUnitVectors(aliasedFrom, aliasedFrom, to);
|
|
541
|
+
expectQuaternionClose(aliasedFrom, distinct.x, distinct.y, distinct.z, distinct.w);
|
|
542
|
+
|
|
543
|
+
const aliasedTo = createQuaternion(to.x, to.y, to.z, 0);
|
|
544
|
+
setQuaternionFromUnitVectors(aliasedTo, from, aliasedTo);
|
|
545
|
+
expectQuaternionClose(aliasedTo, distinct.x, distinct.y, distinct.z, distinct.w);
|
|
546
|
+
});
|
|
462
547
|
});
|
|
463
548
|
|
|
464
549
|
describe('setQuaternionIdentity', () => {
|
|
@@ -470,10 +555,21 @@ describe('setQuaternionIdentity', () => {
|
|
|
470
555
|
});
|
|
471
556
|
|
|
472
557
|
describe('setQuaternionLookRotation', () => {
|
|
473
|
-
it(
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
558
|
+
it.each([
|
|
559
|
+
['non-z-aligned', createVector3(2, 3, 6)],
|
|
560
|
+
['z-aligned', createVector3(1, 2, 20)],
|
|
561
|
+
])('preserves an asymmetric %s forward direction when up is parallel', (_case, rawForward) => {
|
|
562
|
+
const length = Math.sqrt(rawForward.x * rawForward.x + rawForward.y * rawForward.y + rawForward.z * rawForward.z);
|
|
563
|
+
const forward = createVector3(rawForward.x / length, rawForward.y / length, rawForward.z / length);
|
|
564
|
+
const q = createQuaternion();
|
|
565
|
+
|
|
566
|
+
setQuaternionLookRotation(q, forward, forward);
|
|
567
|
+
|
|
568
|
+
const rotated = createVector3();
|
|
569
|
+
rotateVector3ByQuaternion(rotated, createVector3(0, 0, 1), q);
|
|
570
|
+
expect(rotated.x).toBeCloseTo(forward.x, 6);
|
|
571
|
+
expect(rotated.y).toBeCloseTo(forward.y, 6);
|
|
572
|
+
expect(rotated.z).toBeCloseTo(forward.z, 6);
|
|
477
573
|
});
|
|
478
574
|
|
|
479
575
|
it('zero forward yields identity (degenerate case)', () => {
|
|
@@ -495,6 +591,55 @@ describe('setQuaternionLookRotation', () => {
|
|
|
495
591
|
setQuaternionLookRotation(q, createVector3(0, 0, 1), createVector3(0, 1, 0));
|
|
496
592
|
expectQuaternionClose(q, 0, 0, 0, 1);
|
|
497
593
|
});
|
|
594
|
+
|
|
595
|
+
it('rotates +Z onto an asymmetric forward direction', () => {
|
|
596
|
+
const forward = createVector3(2 / 7, 3 / 7, 6 / 7);
|
|
597
|
+
const q = createQuaternion();
|
|
598
|
+
|
|
599
|
+
setQuaternionLookRotation(q, forward, createVector3(0, 1, 0));
|
|
600
|
+
|
|
601
|
+
const rotated = createVector3();
|
|
602
|
+
rotateVector3ByQuaternion(rotated, createVector3(0, 0, 1), q);
|
|
603
|
+
expect(rotated.x).toBeCloseTo(forward.x, 6);
|
|
604
|
+
expect(rotated.y).toBeCloseTo(forward.y, 6);
|
|
605
|
+
expect(rotated.z).toBeCloseTo(forward.z, 6);
|
|
606
|
+
});
|
|
607
|
+
|
|
608
|
+
// Looking backwards drives the basis trace negative, which selects one of the three
|
|
609
|
+
// largest-diagonal formulas instead of the trace one. The contract is the same in every case:
|
|
610
|
+
// the quaternion must rotate +Z onto `forward`. A wrong branch produces a valid-looking unit
|
|
611
|
+
// quaternion that aims somewhere else entirely, which no NaN check would catch.
|
|
612
|
+
it.each([
|
|
613
|
+
['-Z with +Y up', createVector3(0, 0, -1), createVector3(0, 1, 0)],
|
|
614
|
+
['-Z with -Y up', createVector3(0, 0, -1), createVector3(0, -1, 0)],
|
|
615
|
+
['+Z with -Y up', createVector3(0, 0, 1), createVector3(0, -1, 0)],
|
|
616
|
+
['-X with -Y up', createVector3(-1, 0, 0), createVector3(0, -1, 0)],
|
|
617
|
+
])('rotates +Z onto forward for %s', (_case, forward, up) => {
|
|
618
|
+
const q = createQuaternion();
|
|
619
|
+
setQuaternionLookRotation(q, forward, up);
|
|
620
|
+
|
|
621
|
+
const rotated = createVector3();
|
|
622
|
+
rotateVector3ByQuaternion(rotated, createVector3(0, 0, 1), q);
|
|
623
|
+
|
|
624
|
+
expect(rotated.x).toBeCloseTo(forward.x, 6);
|
|
625
|
+
expect(rotated.y).toBeCloseTo(forward.y, 6);
|
|
626
|
+
expect(rotated.z).toBeCloseTo(forward.z, 6);
|
|
627
|
+
});
|
|
628
|
+
it('writes the same values whether out aliases an input or not', () => {
|
|
629
|
+
const forward = createVector3(0, 0, -1);
|
|
630
|
+
const up = createVector3(0, 1, 0);
|
|
631
|
+
const distinct = createQuaternion();
|
|
632
|
+
setQuaternionLookRotation(distinct, forward, up);
|
|
633
|
+
|
|
634
|
+
const aliased = createQuaternion(forward.x, forward.y, forward.z, 0);
|
|
635
|
+
setQuaternionLookRotation(aliased, aliased, up);
|
|
636
|
+
|
|
637
|
+
expectQuaternionClose(aliased, distinct.x, distinct.y, distinct.z, distinct.w);
|
|
638
|
+
|
|
639
|
+
const aliasedUp = createQuaternion(up.x, up.y, up.z, 0);
|
|
640
|
+
setQuaternionLookRotation(aliasedUp, forward, aliasedUp);
|
|
641
|
+
expectQuaternionClose(aliasedUp, distinct.x, distinct.y, distinct.z, distinct.w);
|
|
642
|
+
});
|
|
498
643
|
});
|
|
499
644
|
|
|
500
645
|
describe('slerpQuaternion', () => {
|
|
@@ -510,6 +655,46 @@ describe('slerpQuaternion', () => {
|
|
|
510
655
|
expectQuaternionClose(out, b.x, b.y, b.z, b.w);
|
|
511
656
|
});
|
|
512
657
|
|
|
658
|
+
it('halfway interpolates about a tilted axis, exercising every component', () => {
|
|
659
|
+
// Every other slerp case rotates about (0,1,0), which leaves x and z zero in BOTH inputs — so the
|
|
660
|
+
// x and z arms of the shortest-path negation and of the final blend cannot be observed at all, and
|
|
661
|
+
// a sign error in either would pass. A tilted, normalized axis gives all four components distinct
|
|
662
|
+
// non-zero magnitudes. The oracle is independent of slerp: halfway between identity and R(axis, θ)
|
|
663
|
+
// is R(axis, θ/2) by definition of the axis-angle form.
|
|
664
|
+
const axis = createVector3(1, 2, 3);
|
|
665
|
+
normalizeVector3(axis, axis);
|
|
666
|
+
const a = createQuaternion();
|
|
667
|
+
const b = createQuaternion();
|
|
668
|
+
setQuaternionFromAxisAngle(b, axis, Math.PI / 2);
|
|
669
|
+
|
|
670
|
+
const out = createQuaternion();
|
|
671
|
+
slerpQuaternion(out, a, b, 0.5);
|
|
672
|
+
|
|
673
|
+
const expected = createQuaternion();
|
|
674
|
+
setQuaternionFromAxisAngle(expected, axis, Math.PI / 4);
|
|
675
|
+
expectQuaternionClose(out, expected.x, expected.y, expected.z, expected.w);
|
|
676
|
+
});
|
|
677
|
+
|
|
678
|
+
it('takes the shortest path when the inputs face opposite hemispheres', () => {
|
|
679
|
+
// A quaternion and its negation are the SAME rotation, so slerping toward either must give the same
|
|
680
|
+
// result — that is what the cosHalfTheta < 0 branch exists to guarantee. Without an input pair whose
|
|
681
|
+
// dot product is negative, that branch never runs and its per-component sign flips are unobservable;
|
|
682
|
+
// the tilted axis then keeps bx and bz non-zero so a dropped flip on either actually moves the answer.
|
|
683
|
+
const axis = createVector3(1, 2, 3);
|
|
684
|
+
normalizeVector3(axis, axis);
|
|
685
|
+
const a = createQuaternion();
|
|
686
|
+
const far = createQuaternion();
|
|
687
|
+
setQuaternionFromAxisAngle(far, axis, Math.PI / 2);
|
|
688
|
+
const negated = createQuaternion(-far.x, -far.y, -far.z, -far.w);
|
|
689
|
+
|
|
690
|
+
const out = createQuaternion();
|
|
691
|
+
slerpQuaternion(out, a, negated, 0.5);
|
|
692
|
+
|
|
693
|
+
const expected = createQuaternion();
|
|
694
|
+
setQuaternionFromAxisAngle(expected, axis, Math.PI / 4);
|
|
695
|
+
expectQuaternionClose(out, expected.x, expected.y, expected.z, expected.w);
|
|
696
|
+
});
|
|
697
|
+
|
|
513
698
|
it('halfway interpolates the angle', () => {
|
|
514
699
|
const a = createQuaternion();
|
|
515
700
|
const b = createQuaternion();
|
|
@@ -537,4 +722,37 @@ describe('slerpQuaternion', () => {
|
|
|
537
722
|
slerpQuaternion(b, a, b, 0);
|
|
538
723
|
expectQuaternionClose(b, a.x, a.y, a.z, a.w);
|
|
539
724
|
});
|
|
725
|
+
|
|
726
|
+
// q and -q name the same rotation, so an interpolator that does not flip the sign travels the
|
|
727
|
+
// long way round: it would pass through a 135-degree pose here instead of a 45-degree one.
|
|
728
|
+
it('takes the shorter arc when the inputs have a negative dot product', () => {
|
|
729
|
+
const a = createQuaternion();
|
|
730
|
+
const quarterTurn = createQuaternion();
|
|
731
|
+
setQuaternionFromAxisAngle(quarterTurn, createVector3(0, 1, 0), Math.PI / 2);
|
|
732
|
+
const negated = createQuaternion(-quarterTurn.x, -quarterTurn.y, -quarterTurn.z, -quarterTurn.w);
|
|
733
|
+
|
|
734
|
+
const out = createQuaternion();
|
|
735
|
+
slerpQuaternion(out, a, negated, 0.5);
|
|
736
|
+
|
|
737
|
+
const eighthTurn = createQuaternion();
|
|
738
|
+
setQuaternionFromAxisAngle(eighthTurn, createVector3(0, 1, 0), Math.PI / 4);
|
|
739
|
+
expect(Math.abs(getQuaternionDot(out, eighthTurn))).toBeCloseTo(1, 6);
|
|
740
|
+
});
|
|
741
|
+
|
|
742
|
+
// Nearly-collinear inputs make sin(halfTheta) approach zero; without the linear fallback both
|
|
743
|
+
// scale factors divide by it and the caller receives NaN.
|
|
744
|
+
it('interpolates nearly identical rotations without dividing by zero', () => {
|
|
745
|
+
const a = createQuaternion();
|
|
746
|
+
const b = createQuaternion();
|
|
747
|
+
setQuaternionFromAxisAngle(b, createVector3(0, 1, 0), 1e-7);
|
|
748
|
+
|
|
749
|
+
const out = createQuaternion();
|
|
750
|
+
slerpQuaternion(out, a, b, 0.5);
|
|
751
|
+
|
|
752
|
+
expect(Number.isFinite(out.x)).toBe(true);
|
|
753
|
+
expect(Number.isFinite(out.y)).toBe(true);
|
|
754
|
+
expect(Number.isFinite(out.z)).toBe(true);
|
|
755
|
+
expect(Number.isFinite(out.w)).toBe(true);
|
|
756
|
+
expect(getQuaternionDot(out, out)).toBeCloseTo(1, 6);
|
|
757
|
+
});
|
|
540
758
|
});
|