@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/matrix4.test.ts
CHANGED
|
@@ -26,6 +26,8 @@ import {
|
|
|
26
26
|
interpolateMatrix4,
|
|
27
27
|
inverseMatrix4,
|
|
28
28
|
isAffineMatrix4,
|
|
29
|
+
getVector3Dot,
|
|
30
|
+
getVector3Length,
|
|
29
31
|
matrix4TransformPoint,
|
|
30
32
|
matrix4TransformVector,
|
|
31
33
|
matrix4TransformVectors,
|
|
@@ -49,6 +51,7 @@ import {
|
|
|
49
51
|
setOrthographicMatrix4,
|
|
50
52
|
setPerspectiveMatrix4,
|
|
51
53
|
setQuaternionFromAxisAngle,
|
|
54
|
+
setVector3,
|
|
52
55
|
translateMatrix4,
|
|
53
56
|
transposeMatrix4,
|
|
54
57
|
writeMatrix4ToFloat32Array,
|
|
@@ -59,32 +62,55 @@ const X_AXIS = { x: 1, y: 0, z: 0, w: 0 };
|
|
|
59
62
|
const Y_AXIS = { x: 0, y: 1, z: 0, w: 0 };
|
|
60
63
|
const Z_AXIS = { x: 0, y: 0, z: 1, w: 0 };
|
|
61
64
|
|
|
65
|
+
// Rotation AND non-uniform scale AND translation together. A diagonal matrix, a uniform scale, or a
|
|
66
|
+
// zero translation each make two different compositions agree — that coincidence is what hid the
|
|
67
|
+
// inverted operands from the previous suite, so every composition test below starts from this.
|
|
68
|
+
function discriminatingMatrix4(): Matrix4 {
|
|
69
|
+
const m = createMatrix4();
|
|
70
|
+
setMatrix4(m, 0, 2, 0, 0, -3, 0, 0, 0, 0, 0, 4, 0, 7, 8, 9, 1);
|
|
71
|
+
return m;
|
|
72
|
+
}
|
|
73
|
+
|
|
62
74
|
describe('appendMatrix4', () => {
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
75
|
+
// OpenFL's Matrix3D.append composes `other` so it applies AFTER everything already in `source`,
|
|
76
|
+
// which under Flight's column-vector convention is the LEFT operand: out = other · source. Asserting
|
|
77
|
+
// against a hand-built product rather than a captured result is what keeps this readable as algebra.
|
|
78
|
+
it('composes other after source: out = other · source', () => {
|
|
79
|
+
const source = discriminatingMatrix4();
|
|
80
|
+
const other = createMatrix4();
|
|
81
|
+
setMatrix4(other, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 5, 0, 0, 1);
|
|
82
|
+
const expected = createMatrix4();
|
|
83
|
+
multiplyMatrix4(expected, other, source);
|
|
84
|
+
|
|
85
|
+
const out = createMatrix4();
|
|
86
|
+
appendMatrix4(out, source, other);
|
|
68
87
|
|
|
69
|
-
|
|
70
|
-
|
|
88
|
+
expect(Array.from(out.m)).toEqual(Array.from(expected.m));
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it('is not the other operand order', () => {
|
|
92
|
+
const source = discriminatingMatrix4();
|
|
93
|
+
const other = createMatrix4();
|
|
94
|
+
setMatrix4(other, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 5, 0, 0, 1);
|
|
95
|
+
const transposedOrder = createMatrix4();
|
|
96
|
+
multiplyMatrix4(transposedOrder, source, other);
|
|
71
97
|
|
|
72
|
-
const
|
|
73
|
-
|
|
98
|
+
const out = createMatrix4();
|
|
99
|
+
appendMatrix4(out, source, other);
|
|
74
100
|
|
|
75
|
-
expect(
|
|
101
|
+
expect(Array.from(out.m)).not.toEqual(Array.from(transposedOrder.m));
|
|
76
102
|
});
|
|
77
103
|
|
|
78
104
|
it('supports out === source', () => {
|
|
79
|
-
const
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
scaleMatrix4(b, b, 2, 3, 4);
|
|
105
|
+
const source = discriminatingMatrix4();
|
|
106
|
+
const other = createMatrix4();
|
|
107
|
+
scaleMatrix4(other, other, 2, 3, 4);
|
|
83
108
|
const expected = createMatrix4();
|
|
84
|
-
appendMatrix4(expected,
|
|
109
|
+
appendMatrix4(expected, source, other);
|
|
110
|
+
|
|
111
|
+
appendMatrix4(source, source, other);
|
|
85
112
|
|
|
86
|
-
|
|
87
|
-
expect(equalsMatrix4(a, expected)).toBe(true);
|
|
113
|
+
expect(Array.from(source.m)).toEqual(Array.from(expected.m));
|
|
88
114
|
});
|
|
89
115
|
});
|
|
90
116
|
|
|
@@ -100,95 +126,173 @@ describe('appendRotationMatrix4', () => {
|
|
|
100
126
|
expect(m.m[5]).toBeCloseTo(0);
|
|
101
127
|
});
|
|
102
128
|
|
|
103
|
-
|
|
129
|
+
// A world-frame rotation turns the space the matrix sits in, so the existing translation sweeps
|
|
130
|
+
// around the origin with it. Before the operand fix this function left the translation in place,
|
|
131
|
+
// which is the local behavior and belonged to prependRotationMatrix4.
|
|
132
|
+
it('sweeps the existing translation around the origin', () => {
|
|
104
133
|
const m = createMatrix4();
|
|
105
134
|
translateMatrix4(m, m, 10, 0, 0);
|
|
106
135
|
|
|
107
136
|
appendRotationMatrix4(m, m, Math.PI / 2, Z_AXIS);
|
|
108
137
|
|
|
109
|
-
expect(m.m[12]).
|
|
110
|
-
expect(m.m[13]).
|
|
138
|
+
expect(m.m[12]).toBeCloseTo(0, 6);
|
|
139
|
+
expect(m.m[13]).toBeCloseTo(10, 6);
|
|
111
140
|
});
|
|
112
141
|
|
|
113
|
-
it('
|
|
114
|
-
const
|
|
115
|
-
|
|
142
|
+
it('applies the rotation after source: out = R · source', () => {
|
|
143
|
+
const source = discriminatingMatrix4();
|
|
144
|
+
const rotation = createMatrix4();
|
|
145
|
+
appendRotationMatrix4(rotation, rotation, Math.PI / 3, Z_AXIS);
|
|
146
|
+
const expected = createMatrix4();
|
|
147
|
+
multiplyMatrix4(expected, rotation, source);
|
|
116
148
|
|
|
117
|
-
|
|
149
|
+
const out = createMatrix4();
|
|
150
|
+
appendRotationMatrix4(out, source, Math.PI / 3, Z_AXIS);
|
|
118
151
|
|
|
119
|
-
expect(
|
|
120
|
-
expect(m.m[13]).toBeCloseTo(5);
|
|
152
|
+
for (let i = 0; i < 16; i++) expect(out.m[i]).toBeCloseTo(expected.m[i], 5);
|
|
121
153
|
});
|
|
122
154
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
155
|
+
// The pivot is the point the rotation holds still. Asserting that property rather than a captured
|
|
156
|
+
// translation is what distinguishes a correct composite from the transposed T(-p)·R·T(p) this
|
|
157
|
+
// function used to build — the old test asserted the inverted numbers and so could never fail.
|
|
158
|
+
it('holds the pivot point fixed', () => {
|
|
159
|
+
const source = createMatrix4();
|
|
160
|
+
translateMatrix4(source, source, 10, 0, 0);
|
|
161
|
+
const pivot = { x: 5, y: 0, z: 0, w: 1 };
|
|
126
162
|
|
|
127
|
-
|
|
128
|
-
|
|
163
|
+
const out = createMatrix4();
|
|
164
|
+
appendRotationMatrix4(out, source, Math.PI / 2, Z_AXIS, pivot);
|
|
129
165
|
|
|
130
|
-
|
|
166
|
+
// A world-frame rotation about `pivot` fixes the world point `pivot`, so the source point that
|
|
167
|
+
// already lands there must still land there.
|
|
168
|
+
const landsOnPivot = createVector3(-5, 0, 0);
|
|
169
|
+
const before = createVector3();
|
|
170
|
+
matrix4TransformPoint(before, source, landsOnPivot);
|
|
171
|
+
expect(before.x).toBeCloseTo(pivot.x, 6);
|
|
172
|
+
|
|
173
|
+
const after = createVector3();
|
|
174
|
+
matrix4TransformPoint(after, out, landsOnPivot);
|
|
175
|
+
expect(after.x).toBeCloseTo(pivot.x, 6);
|
|
176
|
+
expect(after.y).toBeCloseTo(pivot.y, 6);
|
|
177
|
+
expect(after.z).toBeCloseTo(pivot.z, 6);
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
it('differs from the local prependRotationMatrix4 on a translated matrix', () => {
|
|
181
|
+
const source = discriminatingMatrix4();
|
|
182
|
+
const world = createMatrix4();
|
|
183
|
+
const local = createMatrix4();
|
|
184
|
+
|
|
185
|
+
appendRotationMatrix4(world, source, Math.PI / 3, Z_AXIS);
|
|
186
|
+
prependRotationMatrix4(local, source, Math.PI / 3, Z_AXIS);
|
|
187
|
+
|
|
188
|
+
expect(Array.from(world.m)).not.toEqual(Array.from(local.m));
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
it('writes the same values whether out aliases source or not', () => {
|
|
192
|
+
const source = discriminatingMatrix4();
|
|
193
|
+
const distinct = createMatrix4();
|
|
194
|
+
const aliased = cloneMatrix4(source);
|
|
195
|
+
|
|
196
|
+
appendRotationMatrix4(distinct, source, Math.PI / 3, Z_AXIS, { x: 1, y: 2, z: 3, w: 1 });
|
|
197
|
+
appendRotationMatrix4(aliased, aliased, Math.PI / 3, Z_AXIS, { x: 1, y: 2, z: 3, w: 1 });
|
|
198
|
+
|
|
199
|
+
expect(Array.from(aliased.m)).toEqual(Array.from(distinct.m));
|
|
131
200
|
});
|
|
132
201
|
});
|
|
133
202
|
|
|
134
203
|
describe('appendScaleMatrix4', () => {
|
|
135
|
-
it
|
|
136
|
-
|
|
204
|
+
// Inherits its composition from appendMatrix4 — it is not implemented separately, so this test also
|
|
205
|
+
// pins that the wrapper was corrected by inheritance rather than by hand.
|
|
206
|
+
it('applies the scale after source: out = S · source', () => {
|
|
207
|
+
const source = discriminatingMatrix4();
|
|
208
|
+
const scale = createMatrix4();
|
|
209
|
+
setMatrix4(scale, 2, 0, 0, 0, 0, 3, 0, 0, 0, 0, 4, 0, 0, 0, 0, 1);
|
|
210
|
+
const expected = createMatrix4();
|
|
211
|
+
multiplyMatrix4(expected, scale, source);
|
|
137
212
|
|
|
138
|
-
|
|
213
|
+
const out = createMatrix4();
|
|
214
|
+
appendScaleMatrix4(out, source, 2, 3, 4);
|
|
139
215
|
|
|
140
|
-
expect(
|
|
141
|
-
expect(m.m[5]).toBe(3);
|
|
142
|
-
expect(m.m[10]).toBe(4);
|
|
216
|
+
expect(Array.from(out.m)).toEqual(Array.from(expected.m));
|
|
143
217
|
});
|
|
144
218
|
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
219
|
+
// A world-frame scale reaches the translation; the local one does not. This is the pair that was
|
|
220
|
+
// indistinguishable before the operand fix, because both computed source · S.
|
|
221
|
+
it('scales the translation, unlike the local scaleMatrix4', () => {
|
|
222
|
+
const source = discriminatingMatrix4();
|
|
223
|
+
const world = createMatrix4();
|
|
224
|
+
const local = createMatrix4();
|
|
148
225
|
|
|
149
|
-
appendScaleMatrix4(
|
|
226
|
+
appendScaleMatrix4(world, source, 2, 3, 4);
|
|
227
|
+
scaleMatrix4(local, source, 2, 3, 4);
|
|
150
228
|
|
|
151
|
-
expect(
|
|
152
|
-
expect(
|
|
153
|
-
expect(
|
|
229
|
+
expect(world.m[12]).toBe(source.m[12] * 2);
|
|
230
|
+
expect(local.m[12]).toBe(source.m[12]);
|
|
231
|
+
expect(Array.from(world.m)).not.toEqual(Array.from(local.m));
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
it('supports out === source', () => {
|
|
235
|
+
const source = discriminatingMatrix4();
|
|
236
|
+
const expected = createMatrix4();
|
|
237
|
+
appendScaleMatrix4(expected, source, 2, 3, 4);
|
|
238
|
+
|
|
239
|
+
appendScaleMatrix4(source, source, 2, 3, 4);
|
|
240
|
+
|
|
241
|
+
expect(Array.from(source.m)).toEqual(Array.from(expected.m));
|
|
154
242
|
});
|
|
155
243
|
});
|
|
156
244
|
|
|
157
245
|
describe('appendTranslationMatrix4', () => {
|
|
158
|
-
|
|
159
|
-
|
|
246
|
+
// The one member of the family that was hand-ported rather than routed through appendMatrix4, which
|
|
247
|
+
// is why it alone stayed correct while the wrapper was inverted. out = T · source, i.e. the offset is
|
|
248
|
+
// a bare sum on the translation and the basis is untouched.
|
|
249
|
+
it('adds the offset to the translation and leaves the basis alone', () => {
|
|
250
|
+
const source = discriminatingMatrix4();
|
|
251
|
+
const out = createMatrix4();
|
|
160
252
|
|
|
161
|
-
appendTranslationMatrix4(
|
|
253
|
+
appendTranslationMatrix4(out, source, 1, 2, 3);
|
|
162
254
|
|
|
163
|
-
expect(
|
|
164
|
-
expect(
|
|
165
|
-
expect(
|
|
255
|
+
expect(out.m[12]).toBe(source.m[12] + 1);
|
|
256
|
+
expect(out.m[13]).toBe(source.m[13] + 2);
|
|
257
|
+
expect(out.m[14]).toBe(source.m[14] + 3);
|
|
258
|
+
expect(Array.from(out.m).slice(0, 12)).toEqual(Array.from(source.m).slice(0, 12));
|
|
166
259
|
});
|
|
167
260
|
|
|
168
|
-
it('
|
|
169
|
-
const
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
261
|
+
it('equals composing a translation matrix after source', () => {
|
|
262
|
+
const source = discriminatingMatrix4();
|
|
263
|
+
const translation = createMatrix4();
|
|
264
|
+
setMatrix4(translation, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 1, 2, 3, 1);
|
|
265
|
+
const expected = createMatrix4();
|
|
266
|
+
multiplyMatrix4(expected, translation, source);
|
|
173
267
|
|
|
174
|
-
|
|
268
|
+
const out = createMatrix4();
|
|
269
|
+
appendTranslationMatrix4(out, source, 1, 2, 3);
|
|
175
270
|
|
|
176
|
-
expect(
|
|
177
|
-
expect(m.m[13]).toBe(22);
|
|
178
|
-
expect(m.m[14]).toBe(33);
|
|
271
|
+
expect(Array.from(out.m)).toEqual(Array.from(expected.m));
|
|
179
272
|
});
|
|
180
273
|
|
|
181
|
-
it('
|
|
182
|
-
const
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
m.m[10] = 4; // scale z
|
|
274
|
+
it('differs from the local prependTranslationMatrix4 on a rotated matrix', () => {
|
|
275
|
+
const source = discriminatingMatrix4();
|
|
276
|
+
const world = createMatrix4();
|
|
277
|
+
const local = createMatrix4();
|
|
186
278
|
|
|
187
|
-
appendTranslationMatrix4(
|
|
279
|
+
appendTranslationMatrix4(world, source, 1, 2, 3);
|
|
280
|
+
prependTranslationMatrix4(local, source, 1, 2, 3);
|
|
188
281
|
|
|
189
|
-
expect(
|
|
190
|
-
|
|
191
|
-
|
|
282
|
+
expect(Array.from(world.m)).not.toEqual(Array.from(local.m));
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
it('copies the whole source matrix when out is a distinct object', () => {
|
|
286
|
+
const source = discriminatingMatrix4();
|
|
287
|
+
const before = Array.from(source.m);
|
|
288
|
+
const out = createMatrix4(9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9);
|
|
289
|
+
const aliased = cloneMatrix4(source);
|
|
290
|
+
|
|
291
|
+
appendTranslationMatrix4(out, source, 1, 2, 3);
|
|
292
|
+
appendTranslationMatrix4(aliased, aliased, 1, 2, 3);
|
|
293
|
+
|
|
294
|
+
expect(Array.from(out.m)).toEqual(Array.from(aliased.m));
|
|
295
|
+
expect(Array.from(source.m)).toEqual(before);
|
|
192
296
|
});
|
|
193
297
|
});
|
|
194
298
|
|
|
@@ -281,6 +385,14 @@ describe('copyMatrix4', () => {
|
|
|
281
385
|
expect(source.m[0]).toBe(1);
|
|
282
386
|
expect(out.m[0]).toBe(99);
|
|
283
387
|
});
|
|
388
|
+
it('is a no-op when out aliases source', () => {
|
|
389
|
+
const m = createMatrix4();
|
|
390
|
+
setMatrix4(m, 0, 1, 0, 0, -1, 0, 0, 0, 0, 0, 1, 0, 7, 8, 9, 1);
|
|
391
|
+
|
|
392
|
+
copyMatrix4(m, m);
|
|
393
|
+
|
|
394
|
+
expect(Array.from(m.m)).toEqual([0, 1, 0, 0, -1, 0, 0, 0, 0, 0, 1, 0, 7, 8, 9, 1]);
|
|
395
|
+
});
|
|
284
396
|
});
|
|
285
397
|
|
|
286
398
|
describe('copyMatrix4ColumnFromVector4', () => {
|
|
@@ -315,6 +427,23 @@ describe('copyMatrix4ColumnFromVector4', () => {
|
|
|
315
427
|
expect(() => copyMatrix4ColumnFromVector4(m, -1, v)).toThrow(RangeError);
|
|
316
428
|
expect(() => copyMatrix4ColumnFromVector4(m, 4, v)).toThrow(RangeError);
|
|
317
429
|
});
|
|
430
|
+
|
|
431
|
+
// Column c occupies elements 4c..4c+3 and nothing else moves. Asserting the whole matrix per
|
|
432
|
+
// column catches a row/column transposition or a stray write that a single-index check misses.
|
|
433
|
+
it('writes column c into elements 4c..4c+3 for every column', () => {
|
|
434
|
+
for (let column = 0; column < 4; column++) {
|
|
435
|
+
const m = createMatrix4(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0);
|
|
436
|
+
|
|
437
|
+
copyMatrix4ColumnFromVector4(m, column, { x: 1, y: 2, z: 3, w: 4 });
|
|
438
|
+
|
|
439
|
+
const expected = new Array<number>(16).fill(0);
|
|
440
|
+
expected[column * 4] = 1;
|
|
441
|
+
expected[column * 4 + 1] = 2;
|
|
442
|
+
expected[column * 4 + 2] = 3;
|
|
443
|
+
expected[column * 4 + 3] = 4;
|
|
444
|
+
expect(Array.from(m.m)).toEqual(expected);
|
|
445
|
+
}
|
|
446
|
+
});
|
|
318
447
|
});
|
|
319
448
|
|
|
320
449
|
describe('copyMatrix4ColumnToVector4', () => {
|
|
@@ -338,6 +467,24 @@ describe('copyMatrix4ColumnToVector4', () => {
|
|
|
338
467
|
const out = { x: 0, y: 0, z: 0, w: 0 };
|
|
339
468
|
|
|
340
469
|
expect(() => copyMatrix4ColumnToVector4(out, 99, m)).toThrow(RangeError);
|
|
470
|
+
expect(() => copyMatrix4ColumnToVector4(out, -1, m)).toThrow(RangeError);
|
|
471
|
+
});
|
|
472
|
+
|
|
473
|
+
it('reads column c from elements 4c..4c+3 for every column', () => {
|
|
474
|
+
const m = createMatrix4(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16);
|
|
475
|
+
|
|
476
|
+
for (let column = 0; column < 4; column++) {
|
|
477
|
+
const out = { x: 0, y: 0, z: 0, w: 0 };
|
|
478
|
+
|
|
479
|
+
copyMatrix4ColumnToVector4(out, column, m);
|
|
480
|
+
|
|
481
|
+
expect(out).toEqual({
|
|
482
|
+
x: column * 4 + 1,
|
|
483
|
+
y: column * 4 + 2,
|
|
484
|
+
z: column * 4 + 3,
|
|
485
|
+
w: column * 4 + 4,
|
|
486
|
+
});
|
|
487
|
+
}
|
|
341
488
|
});
|
|
342
489
|
});
|
|
343
490
|
|
|
@@ -373,6 +520,23 @@ describe('copyMatrix4RowFromVector4', () => {
|
|
|
373
520
|
expect(() => copyMatrix4RowFromVector4(m, -1, v)).toThrow(RangeError);
|
|
374
521
|
expect(() => copyMatrix4RowFromVector4(m, 4, v)).toThrow(RangeError);
|
|
375
522
|
});
|
|
523
|
+
|
|
524
|
+
// Row r occupies elements r, r+4, r+8, r+12 — the stride that distinguishes a row write from a
|
|
525
|
+
// column write in column-major storage.
|
|
526
|
+
it('writes row r with a stride of four for every row', () => {
|
|
527
|
+
for (let row = 0; row < 4; row++) {
|
|
528
|
+
const m = createMatrix4(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0);
|
|
529
|
+
|
|
530
|
+
copyMatrix4RowFromVector4(m, row, { x: 1, y: 2, z: 3, w: 4 });
|
|
531
|
+
|
|
532
|
+
const expected = new Array<number>(16).fill(0);
|
|
533
|
+
expected[row] = 1;
|
|
534
|
+
expected[row + 4] = 2;
|
|
535
|
+
expected[row + 8] = 3;
|
|
536
|
+
expected[row + 12] = 4;
|
|
537
|
+
expect(Array.from(m.m)).toEqual(expected);
|
|
538
|
+
}
|
|
539
|
+
});
|
|
376
540
|
});
|
|
377
541
|
|
|
378
542
|
describe('copyMatrix4RowToVector4', () => {
|
|
@@ -396,6 +560,19 @@ describe('copyMatrix4RowToVector4', () => {
|
|
|
396
560
|
const out = { x: 0, y: 0, z: 0, w: 0 };
|
|
397
561
|
|
|
398
562
|
expect(() => copyMatrix4RowToVector4(out, 42, m)).toThrow(RangeError);
|
|
563
|
+
expect(() => copyMatrix4RowToVector4(out, -1, m)).toThrow(RangeError);
|
|
564
|
+
});
|
|
565
|
+
|
|
566
|
+
it('reads row r with a stride of four for every row', () => {
|
|
567
|
+
const m = createMatrix4(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16);
|
|
568
|
+
|
|
569
|
+
for (let row = 0; row < 4; row++) {
|
|
570
|
+
const out = { x: 0, y: 0, z: 0, w: 0 };
|
|
571
|
+
|
|
572
|
+
copyMatrix4RowToVector4(out, row, m);
|
|
573
|
+
|
|
574
|
+
expect(out).toEqual({ x: row + 1, y: row + 5, z: row + 9, w: row + 13 });
|
|
575
|
+
}
|
|
399
576
|
});
|
|
400
577
|
});
|
|
401
578
|
|
|
@@ -519,6 +696,71 @@ describe('decomposeMatrix4', () => {
|
|
|
519
696
|
expect(s.z).toBeCloseTo(1, 6);
|
|
520
697
|
expect(r.w).toBeCloseTo(1, 6);
|
|
521
698
|
});
|
|
699
|
+
|
|
700
|
+
// Quaternion extraction picks one of four formulas by which diagonal term dominates. A half turn
|
|
701
|
+
// about each axis drives the trace negative and selects a different one, so recomposing from the
|
|
702
|
+
// result is the check that all four agree with composeMatrix4 rather than only the trace case.
|
|
703
|
+
it.each([
|
|
704
|
+
['x', createVector3(1, 0, 0)],
|
|
705
|
+
['y', createVector3(0, 1, 0)],
|
|
706
|
+
['z', createVector3(0, 0, 1)],
|
|
707
|
+
])('round-trips a half turn about %s', (_axis, axis) => {
|
|
708
|
+
const rotation = createQuaternion();
|
|
709
|
+
setQuaternionFromAxisAngle(rotation, axis, Math.PI);
|
|
710
|
+
const original = createMatrix4();
|
|
711
|
+
composeMatrix4(original, createVector3(5, 6, 7), rotation, createVector3(2, 3, 4));
|
|
712
|
+
|
|
713
|
+
const p = createVector3();
|
|
714
|
+
const r = createQuaternion();
|
|
715
|
+
const s = createVector3();
|
|
716
|
+
decomposeMatrix4(p, r, s, original);
|
|
717
|
+
const recomposed = createMatrix4();
|
|
718
|
+
composeMatrix4(recomposed, p, r, s);
|
|
719
|
+
|
|
720
|
+
for (let i = 0; i < 16; i++) {
|
|
721
|
+
expect(recomposed.m[i]).toBeCloseTo(original.m[i], 5);
|
|
722
|
+
}
|
|
723
|
+
});
|
|
724
|
+
|
|
725
|
+
it('folds a mirrored basis into a negative x scale', () => {
|
|
726
|
+
const original = createMatrix4();
|
|
727
|
+
composeMatrix4(original, createVector3(0, 0, 0), createQuaternion(), createVector3(-2, 3, 4));
|
|
728
|
+
|
|
729
|
+
const p = createVector3();
|
|
730
|
+
const r = createQuaternion();
|
|
731
|
+
const s = createVector3();
|
|
732
|
+
decomposeMatrix4(p, r, s, original);
|
|
733
|
+
|
|
734
|
+
expect(s.x).toBeCloseTo(-2, 5);
|
|
735
|
+
expect(s.y).toBeCloseTo(3, 5);
|
|
736
|
+
expect(s.z).toBeCloseTo(4, 5);
|
|
737
|
+
const recomposed = createMatrix4();
|
|
738
|
+
composeMatrix4(recomposed, p, r, s);
|
|
739
|
+
for (let i = 0; i < 16; i++) {
|
|
740
|
+
expect(recomposed.m[i]).toBeCloseTo(original.m[i], 5);
|
|
741
|
+
}
|
|
742
|
+
});
|
|
743
|
+
|
|
744
|
+
// A collapsed axis has no recoverable direction, so the guard substitutes zero rather than
|
|
745
|
+
// dividing by it. The contract is that the caller gets finite numbers and an honest zero scale.
|
|
746
|
+
it('reports a collapsed axis as zero scale without producing NaN', () => {
|
|
747
|
+
const original = createMatrix4();
|
|
748
|
+
composeMatrix4(original, createVector3(1, 2, 3), createQuaternion(), createVector3(0, 0, 0));
|
|
749
|
+
|
|
750
|
+
const p = createVector3();
|
|
751
|
+
const r = createQuaternion();
|
|
752
|
+
const s = createVector3();
|
|
753
|
+
decomposeMatrix4(p, r, s, original);
|
|
754
|
+
|
|
755
|
+
expect(s.x).toBe(0);
|
|
756
|
+
expect(s.y).toBe(0);
|
|
757
|
+
expect(s.z).toBe(0);
|
|
758
|
+
expect(Number.isFinite(r.x)).toBe(true);
|
|
759
|
+
expect(Number.isFinite(r.y)).toBe(true);
|
|
760
|
+
expect(Number.isFinite(r.z)).toBe(true);
|
|
761
|
+
expect(Number.isFinite(r.w)).toBe(true);
|
|
762
|
+
expect(p.x).toBe(1);
|
|
763
|
+
});
|
|
522
764
|
});
|
|
523
765
|
|
|
524
766
|
describe('equalsMatrix4', () => {
|
|
@@ -978,7 +1220,7 @@ describe('multiplyMatrix4', () => {
|
|
|
978
1220
|
expect(equalsMatrix4(m1, m2)).toBe(true);
|
|
979
1221
|
});
|
|
980
1222
|
|
|
981
|
-
it('prependScale equals
|
|
1223
|
+
it('prependScale equals multiplying the scale on the right', () => {
|
|
982
1224
|
const m1 = createMatrix4();
|
|
983
1225
|
translateMatrix4(m1, m1, 10, 0, 0);
|
|
984
1226
|
const m2 = createMatrix4();
|
|
@@ -988,7 +1230,7 @@ describe('multiplyMatrix4', () => {
|
|
|
988
1230
|
scaleMatrix4(s, s, 2, 2, 2);
|
|
989
1231
|
|
|
990
1232
|
prependScaleMatrix4(m1, m1, 2, 2, 2);
|
|
991
|
-
multiplyMatrix4(m2,
|
|
1233
|
+
multiplyMatrix4(m2, m2, s);
|
|
992
1234
|
|
|
993
1235
|
expect(equalsMatrix4(m1, m2)).toBe(true);
|
|
994
1236
|
});
|
|
@@ -996,31 +1238,44 @@ describe('multiplyMatrix4', () => {
|
|
|
996
1238
|
});
|
|
997
1239
|
|
|
998
1240
|
describe('prependMatrix4', () => {
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
const
|
|
1003
|
-
|
|
1241
|
+
// The mirror of append: `other` applies BEFORE everything already in `source`, so it is the RIGHT
|
|
1242
|
+
// operand — out = source · other.
|
|
1243
|
+
it('composes other before source: out = source · other', () => {
|
|
1244
|
+
const source = discriminatingMatrix4();
|
|
1245
|
+
const other = createMatrix4();
|
|
1246
|
+
setMatrix4(other, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 5, 0, 0, 1);
|
|
1247
|
+
const expected = createMatrix4();
|
|
1248
|
+
multiplyMatrix4(expected, source, other);
|
|
1004
1249
|
|
|
1005
|
-
const
|
|
1006
|
-
prependMatrix4(
|
|
1250
|
+
const out = createMatrix4();
|
|
1251
|
+
prependMatrix4(out, source, other);
|
|
1252
|
+
|
|
1253
|
+
expect(Array.from(out.m)).toEqual(Array.from(expected.m));
|
|
1254
|
+
});
|
|
1007
1255
|
|
|
1008
|
-
|
|
1009
|
-
|
|
1256
|
+
it('is the opposite composition from appendMatrix4', () => {
|
|
1257
|
+
const source = discriminatingMatrix4();
|
|
1258
|
+
const other = createMatrix4();
|
|
1259
|
+
setMatrix4(other, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 5, 0, 0, 1);
|
|
1010
1260
|
|
|
1011
|
-
|
|
1261
|
+
const appended = createMatrix4();
|
|
1262
|
+
appendMatrix4(appended, source, other);
|
|
1263
|
+
const prepended = createMatrix4();
|
|
1264
|
+
prependMatrix4(prepended, source, other);
|
|
1265
|
+
|
|
1266
|
+
expect(Array.from(appended.m)).not.toEqual(Array.from(prepended.m));
|
|
1012
1267
|
});
|
|
1013
1268
|
|
|
1014
1269
|
it('supports out === source', () => {
|
|
1015
|
-
const
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
scaleMatrix4(b, b, 2, 3, 4);
|
|
1270
|
+
const source = discriminatingMatrix4();
|
|
1271
|
+
const other = createMatrix4();
|
|
1272
|
+
scaleMatrix4(other, other, 2, 3, 4);
|
|
1019
1273
|
const expected = createMatrix4();
|
|
1020
|
-
prependMatrix4(expected,
|
|
1274
|
+
prependMatrix4(expected, source, other);
|
|
1021
1275
|
|
|
1022
|
-
prependMatrix4(
|
|
1023
|
-
|
|
1276
|
+
prependMatrix4(source, source, other);
|
|
1277
|
+
|
|
1278
|
+
expect(Array.from(source.m)).toEqual(Array.from(expected.m));
|
|
1024
1279
|
});
|
|
1025
1280
|
});
|
|
1026
1281
|
|
|
@@ -1034,86 +1289,130 @@ describe('prependRotationMatrix4', () => {
|
|
|
1034
1289
|
expect(m.m[1]).toBeCloseTo(1);
|
|
1035
1290
|
});
|
|
1036
1291
|
|
|
1037
|
-
|
|
1292
|
+
// A local rotation turns the object about its own axes, so its position in the outer space is
|
|
1293
|
+
// unchanged. Before the operand fix this function swept the translation, which is append's job.
|
|
1294
|
+
it('leaves the existing translation in place', () => {
|
|
1038
1295
|
const m = createMatrix4();
|
|
1039
1296
|
translateMatrix4(m, m, 10, 0, 0);
|
|
1040
1297
|
|
|
1041
1298
|
prependRotationMatrix4(m, m, Math.PI / 2, Z_AXIS);
|
|
1042
1299
|
|
|
1043
|
-
|
|
1044
|
-
expect(m.m[
|
|
1045
|
-
|
|
1300
|
+
expect(m.m[12]).toBeCloseTo(10, 6);
|
|
1301
|
+
expect(m.m[13]).toBeCloseTo(0, 6);
|
|
1302
|
+
});
|
|
1303
|
+
|
|
1304
|
+
it('applies the rotation before source: out = source · R', () => {
|
|
1305
|
+
const source = discriminatingMatrix4();
|
|
1306
|
+
const rotation = createMatrix4();
|
|
1307
|
+
prependRotationMatrix4(rotation, rotation, Math.PI / 3, Z_AXIS);
|
|
1308
|
+
const expected = createMatrix4();
|
|
1309
|
+
multiplyMatrix4(expected, source, rotation);
|
|
1310
|
+
|
|
1311
|
+
const out = createMatrix4();
|
|
1312
|
+
prependRotationMatrix4(out, source, Math.PI / 3, Z_AXIS);
|
|
1313
|
+
|
|
1314
|
+
for (let i = 0; i < 16; i++) expect(out.m[i]).toBeCloseTo(expected.m[i], 5);
|
|
1315
|
+
});
|
|
1316
|
+
|
|
1317
|
+
it('holds the pivot point fixed', () => {
|
|
1318
|
+
const source = discriminatingMatrix4();
|
|
1319
|
+
const pivot = { x: 5, y: 0, z: 0, w: 1 };
|
|
1320
|
+
const before = createVector3();
|
|
1321
|
+
matrix4TransformPoint(before, source, createVector3(pivot.x, pivot.y, pivot.z));
|
|
1322
|
+
|
|
1323
|
+
const out = createMatrix4();
|
|
1324
|
+
prependRotationMatrix4(out, source, Math.PI / 2, Z_AXIS, pivot);
|
|
1325
|
+
|
|
1326
|
+
const after = createVector3();
|
|
1327
|
+
matrix4TransformPoint(after, out, createVector3(pivot.x, pivot.y, pivot.z));
|
|
1328
|
+
expect(after.x).toBeCloseTo(before.x, 5);
|
|
1329
|
+
expect(after.y).toBeCloseTo(before.y, 5);
|
|
1330
|
+
expect(after.z).toBeCloseTo(before.z, 5);
|
|
1331
|
+
});
|
|
1332
|
+
|
|
1333
|
+
it('writes the same values whether out aliases source or not', () => {
|
|
1334
|
+
const source = discriminatingMatrix4();
|
|
1335
|
+
const distinct = createMatrix4();
|
|
1336
|
+
const aliased = cloneMatrix4(source);
|
|
1337
|
+
|
|
1338
|
+
prependRotationMatrix4(distinct, source, Math.PI / 3, Z_AXIS, { x: 1, y: 2, z: 3, w: 1 });
|
|
1339
|
+
prependRotationMatrix4(aliased, aliased, Math.PI / 3, Z_AXIS, { x: 1, y: 2, z: 3, w: 1 });
|
|
1340
|
+
|
|
1341
|
+
expect(Array.from(aliased.m)).toEqual(Array.from(distinct.m));
|
|
1046
1342
|
});
|
|
1047
1343
|
});
|
|
1048
1344
|
|
|
1049
1345
|
describe('prependScaleMatrix4', () => {
|
|
1050
|
-
it('
|
|
1051
|
-
const
|
|
1346
|
+
it('applies the scale before source: out = source · S', () => {
|
|
1347
|
+
const source = discriminatingMatrix4();
|
|
1348
|
+
const scale = createMatrix4();
|
|
1349
|
+
setMatrix4(scale, 2, 0, 0, 0, 0, 3, 0, 0, 0, 0, 4, 0, 0, 0, 0, 1);
|
|
1350
|
+
const expected = createMatrix4();
|
|
1351
|
+
multiplyMatrix4(expected, source, scale);
|
|
1052
1352
|
|
|
1053
|
-
|
|
1353
|
+
const out = createMatrix4();
|
|
1354
|
+
prependScaleMatrix4(out, source, 2, 3, 4);
|
|
1054
1355
|
|
|
1055
|
-
expect(
|
|
1056
|
-
expect(m.m[5]).toBe(3);
|
|
1057
|
-
expect(m.m[10]).toBe(4);
|
|
1356
|
+
expect(Array.from(out.m)).toEqual(Array.from(expected.m));
|
|
1058
1357
|
});
|
|
1059
1358
|
|
|
1060
|
-
it('
|
|
1061
|
-
const
|
|
1062
|
-
|
|
1359
|
+
it('leaves the translation alone, unlike the world-frame appendScaleMatrix4', () => {
|
|
1360
|
+
const source = discriminatingMatrix4();
|
|
1361
|
+
const out = createMatrix4();
|
|
1063
1362
|
|
|
1064
|
-
prependScaleMatrix4(
|
|
1363
|
+
prependScaleMatrix4(out, source, 2, 3, 4);
|
|
1065
1364
|
|
|
1066
|
-
expect(
|
|
1067
|
-
expect(
|
|
1068
|
-
expect(
|
|
1365
|
+
expect(out.m[12]).toBe(source.m[12]);
|
|
1366
|
+
expect(out.m[13]).toBe(source.m[13]);
|
|
1367
|
+
expect(out.m[14]).toBe(source.m[14]);
|
|
1069
1368
|
});
|
|
1070
1369
|
|
|
1071
|
-
it('
|
|
1072
|
-
const
|
|
1073
|
-
const
|
|
1074
|
-
|
|
1370
|
+
it('supports out === source', () => {
|
|
1371
|
+
const source = discriminatingMatrix4();
|
|
1372
|
+
const expected = createMatrix4();
|
|
1373
|
+
prependScaleMatrix4(expected, source, 2, 3, 4);
|
|
1075
1374
|
|
|
1076
|
-
|
|
1077
|
-
appendScaleMatrix4(b, b, 2, 3, 4);
|
|
1078
|
-
prependScaleMatrix4(c, c, 2, 3, 4);
|
|
1375
|
+
prependScaleMatrix4(source, source, 2, 3, 4);
|
|
1079
1376
|
|
|
1080
|
-
expect(
|
|
1081
|
-
expect(equalsMatrix4(b, c)).toBe(true);
|
|
1377
|
+
expect(Array.from(source.m)).toEqual(Array.from(expected.m));
|
|
1082
1378
|
});
|
|
1083
1379
|
});
|
|
1084
1380
|
|
|
1085
1381
|
describe('prependTranslationMatrix4', () => {
|
|
1086
|
-
it('
|
|
1087
|
-
const
|
|
1382
|
+
it('applies the offset before source: out = source · T', () => {
|
|
1383
|
+
const source = discriminatingMatrix4();
|
|
1384
|
+
const translation = createMatrix4();
|
|
1385
|
+
setMatrix4(translation, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 1, 2, 3, 1);
|
|
1386
|
+
const expected = createMatrix4();
|
|
1387
|
+
multiplyMatrix4(expected, source, translation);
|
|
1088
1388
|
|
|
1089
|
-
|
|
1389
|
+
const out = createMatrix4();
|
|
1390
|
+
prependTranslationMatrix4(out, source, 1, 2, 3);
|
|
1090
1391
|
|
|
1091
|
-
expect(
|
|
1092
|
-
expect(m.m[13]).toBe(2);
|
|
1093
|
-
expect(m.m[14]).toBe(3);
|
|
1392
|
+
expect(Array.from(out.m)).toEqual(Array.from(expected.m));
|
|
1094
1393
|
});
|
|
1095
1394
|
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1395
|
+
// The local frame carries the offset through source's own basis, so a rotated, scaled source turns
|
|
1396
|
+
// and stretches it — a bare sum would be the world-frame answer.
|
|
1397
|
+
it('carries the offset through the source basis', () => {
|
|
1398
|
+
const source = discriminatingMatrix4();
|
|
1399
|
+
const out = createMatrix4();
|
|
1099
1400
|
|
|
1100
|
-
prependTranslationMatrix4(
|
|
1401
|
+
prependTranslationMatrix4(out, source, 1, 2, 3);
|
|
1101
1402
|
|
|
1102
|
-
|
|
1103
|
-
expect(
|
|
1403
|
+
expect(out.m[12]).toBe(source.m[0] * 1 + source.m[4] * 2 + source.m[8] * 3 + source.m[12]);
|
|
1404
|
+
expect(out.m[13]).toBe(source.m[1] * 1 + source.m[5] * 2 + source.m[9] * 3 + source.m[13]);
|
|
1405
|
+
expect(out.m[14]).toBe(source.m[2] * 1 + source.m[6] * 2 + source.m[10] * 3 + source.m[14]);
|
|
1104
1406
|
});
|
|
1105
1407
|
|
|
1106
|
-
it('
|
|
1107
|
-
const
|
|
1108
|
-
const
|
|
1109
|
-
|
|
1408
|
+
it('supports out === source', () => {
|
|
1409
|
+
const source = discriminatingMatrix4();
|
|
1410
|
+
const expected = createMatrix4();
|
|
1411
|
+
prependTranslationMatrix4(expected, source, 1, 2, 3);
|
|
1110
1412
|
|
|
1111
|
-
|
|
1112
|
-
appendTranslationMatrix4(b, b, 1, 2, 3);
|
|
1113
|
-
prependTranslationMatrix4(c, c, 1, 2, 3);
|
|
1413
|
+
prependTranslationMatrix4(source, source, 1, 2, 3);
|
|
1114
1414
|
|
|
1115
|
-
expect(
|
|
1116
|
-
expect(equalsMatrix4(b, c)).toBe(true);
|
|
1415
|
+
expect(Array.from(source.m)).toEqual(Array.from(expected.m));
|
|
1117
1416
|
});
|
|
1118
1417
|
});
|
|
1119
1418
|
|
|
@@ -1128,6 +1427,23 @@ describe('rotateMatrix4', () => {
|
|
|
1128
1427
|
expect(equalsMatrix4(a, b)).toBe(true);
|
|
1129
1428
|
});
|
|
1130
1429
|
|
|
1430
|
+
it('matches the quaternion rotation path for an asymmetric unit axis', () => {
|
|
1431
|
+
const inverseLength = 1 / Math.sqrt(14);
|
|
1432
|
+
const axis = createVector3(2 * inverseLength, inverseLength, 3 * inverseLength);
|
|
1433
|
+
const radians = Math.PI / 3;
|
|
1434
|
+
const quaternion = createQuaternion();
|
|
1435
|
+
setQuaternionFromAxisAngle(quaternion, axis, radians);
|
|
1436
|
+
const expected = createMatrix4();
|
|
1437
|
+
setMatrix4FromQuaternion(expected, quaternion);
|
|
1438
|
+
|
|
1439
|
+
const actual = createMatrix4();
|
|
1440
|
+
rotateMatrix4(actual, actual, axis, radians);
|
|
1441
|
+
|
|
1442
|
+
for (let i = 0; i < 16; i++) {
|
|
1443
|
+
expect(actual.m[i]).toBeCloseTo(expected.m[i], 6);
|
|
1444
|
+
}
|
|
1445
|
+
});
|
|
1446
|
+
|
|
1131
1447
|
it('preserves translation when rotating locally', () => {
|
|
1132
1448
|
const m = createMatrix4();
|
|
1133
1449
|
translateMatrix4(m, m, 5, 0, 0);
|
|
@@ -1157,6 +1473,17 @@ describe('rotateMatrix4', () => {
|
|
|
1157
1473
|
expect(m.m[5]).toBeCloseTo(1);
|
|
1158
1474
|
expect(m.m[6]).toBeCloseTo(0);
|
|
1159
1475
|
});
|
|
1476
|
+
it('writes the same values whether out aliases source or not', () => {
|
|
1477
|
+
const source = createMatrix4();
|
|
1478
|
+
setMatrix4(source, 0, 1, 0, 0, -1, 0, 0, 0, 0, 0, 1, 0, 7, 8, 9, 1);
|
|
1479
|
+
const distinct = createMatrix4();
|
|
1480
|
+
const aliased = cloneMatrix4(source);
|
|
1481
|
+
|
|
1482
|
+
rotateMatrix4(distinct, source, createVector3(0, 0, 1), Math.PI / 3);
|
|
1483
|
+
rotateMatrix4(aliased, aliased, createVector3(0, 0, 1), Math.PI / 3);
|
|
1484
|
+
|
|
1485
|
+
expect(Array.from(aliased.m)).toEqual(Array.from(distinct.m));
|
|
1486
|
+
});
|
|
1160
1487
|
});
|
|
1161
1488
|
|
|
1162
1489
|
describe('scaleMatrix4', () => {
|
|
@@ -1191,6 +1518,49 @@ describe('scaleMatrix4', () => {
|
|
|
1191
1518
|
expect(m.m[13]).toBe(20);
|
|
1192
1519
|
expect(m.m[14]).toBe(30);
|
|
1193
1520
|
});
|
|
1521
|
+
|
|
1522
|
+
// A diagonal matrix cannot tell a column scale from a row scale, so a rotated basis with a
|
|
1523
|
+
// non-uniform scale is the only input that pins the multiplication side down.
|
|
1524
|
+
it('post-multiplies by diag(sx, sy, sz, 1) on a rotated basis', () => {
|
|
1525
|
+
const rotated = createMatrix4();
|
|
1526
|
+
setMatrix4(rotated, 0, 1, 0, 0, -1, 0, 0, 0, 0, 0, 1, 0, 7, 8, 9, 1);
|
|
1527
|
+
const scale = createMatrix4();
|
|
1528
|
+
setMatrix4(scale, 2, 0, 0, 0, 0, 3, 0, 0, 0, 0, 4, 0, 0, 0, 0, 1);
|
|
1529
|
+
const expected = createMatrix4();
|
|
1530
|
+
multiplyMatrix4(expected, rotated, scale);
|
|
1531
|
+
|
|
1532
|
+
const result = createMatrix4();
|
|
1533
|
+
scaleMatrix4(result, rotated, 2, 3, 4);
|
|
1534
|
+
|
|
1535
|
+
expect(Array.from(result.m)).toEqual(Array.from(expected.m));
|
|
1536
|
+
// The discriminating elements: column 0 carries sx, not row 0.
|
|
1537
|
+
expect(result.m[1]).toBe(2);
|
|
1538
|
+
expect(result.m[4]).toBe(-3);
|
|
1539
|
+
});
|
|
1540
|
+
|
|
1541
|
+
it('scales the fourth row of each basis column for a non-affine matrix', () => {
|
|
1542
|
+
const projective = createMatrix4();
|
|
1543
|
+
setMatrix4(projective, 1, 0, 0, 5, 0, 1, 0, 6, 0, 0, 1, 7, 0, 0, 0, 1);
|
|
1544
|
+
|
|
1545
|
+
scaleMatrix4(projective, projective, 2, 3, 4);
|
|
1546
|
+
|
|
1547
|
+
expect(projective.m[3]).toBe(10);
|
|
1548
|
+
expect(projective.m[7]).toBe(18);
|
|
1549
|
+
expect(projective.m[11]).toBe(28);
|
|
1550
|
+
expect(projective.m[15]).toBe(1);
|
|
1551
|
+
});
|
|
1552
|
+
|
|
1553
|
+
it('writes the same values whether out aliases source or not', () => {
|
|
1554
|
+
const source = createMatrix4();
|
|
1555
|
+
setMatrix4(source, 0, 1, 0, 0, -1, 0, 0, 0, 0, 0, 1, 0, 7, 8, 9, 1);
|
|
1556
|
+
const distinct = createMatrix4();
|
|
1557
|
+
const aliased = cloneMatrix4(source);
|
|
1558
|
+
|
|
1559
|
+
scaleMatrix4(distinct, source, 2, 3, 4);
|
|
1560
|
+
scaleMatrix4(aliased, aliased, 2, 3, 4);
|
|
1561
|
+
|
|
1562
|
+
expect(Array.from(aliased.m)).toEqual(Array.from(distinct.m));
|
|
1563
|
+
});
|
|
1194
1564
|
});
|
|
1195
1565
|
|
|
1196
1566
|
describe('setMatrix4', () => {
|
|
@@ -1439,6 +1809,38 @@ describe('setMatrix4LookAt', () => {
|
|
|
1439
1809
|
// Target is 5 units in front of the eye => view-space z = -5 (RH).
|
|
1440
1810
|
expect(out.z).toBeCloseTo(-5, 6);
|
|
1441
1811
|
});
|
|
1812
|
+
|
|
1813
|
+
// An overhead camera (up parallel to the view direction) and a camera sitting on its own target
|
|
1814
|
+
// are both reachable from ordinary application code. Roll is arbitrary in those cases, but the
|
|
1815
|
+
// basis must stay orthonormal — a singular view matrix cannot be inverted for picking or for
|
|
1816
|
+
// deriving a camera world transform, and it collapses every rendered position onto a plane.
|
|
1817
|
+
it.each([
|
|
1818
|
+
['up parallel to the view direction', createVector3(0, 5, 0), createVector3(0, 0, 0), createVector3(0, 1, 0)],
|
|
1819
|
+
['eye at the target', createVector3(2, 2, 2), createVector3(2, 2, 2), createVector3(0, 1, 0)],
|
|
1820
|
+
['up parallel to a z-aligned view', createVector3(0, 0, 5), createVector3(0, 0, 0), createVector3(0, 0, 1)],
|
|
1821
|
+
])('keeps the basis orthonormal when %s', (_case, eye, target, up) => {
|
|
1822
|
+
const m = createMatrix4();
|
|
1823
|
+
|
|
1824
|
+
setMatrix4LookAt(m, eye, target, up);
|
|
1825
|
+
|
|
1826
|
+
for (let i = 0; i < 16; i++) {
|
|
1827
|
+
expect(Number.isFinite(m.m[i])).toBe(true);
|
|
1828
|
+
}
|
|
1829
|
+
const xAxis = createVector3();
|
|
1830
|
+
const yAxis = createVector3();
|
|
1831
|
+
const zAxis = createVector3();
|
|
1832
|
+
setVector3(xAxis, m.m[0], m.m[4], m.m[8]);
|
|
1833
|
+
setVector3(yAxis, m.m[1], m.m[5], m.m[9]);
|
|
1834
|
+
setVector3(zAxis, m.m[2], m.m[6], m.m[10]);
|
|
1835
|
+
|
|
1836
|
+
expect(getVector3Length(xAxis)).toBeCloseTo(1, 6);
|
|
1837
|
+
expect(getVector3Length(yAxis)).toBeCloseTo(1, 6);
|
|
1838
|
+
expect(getVector3Length(zAxis)).toBeCloseTo(1, 6);
|
|
1839
|
+
expect(getVector3Dot(xAxis, yAxis)).toBeCloseTo(0, 6);
|
|
1840
|
+
expect(getVector3Dot(xAxis, zAxis)).toBeCloseTo(0, 6);
|
|
1841
|
+
expect(getVector3Dot(yAxis, zAxis)).toBeCloseTo(0, 6);
|
|
1842
|
+
expect(Math.abs(getMatrix4Determinant(m))).toBeCloseTo(1, 6);
|
|
1843
|
+
});
|
|
1442
1844
|
});
|
|
1443
1845
|
|
|
1444
1846
|
describe('setMatrix4Position', () => {
|
|
@@ -1548,6 +1950,22 @@ describe('translateMatrix4', () => {
|
|
|
1548
1950
|
expect(m.m[13]).toBe(7);
|
|
1549
1951
|
expect(m.m[14]).toBe(9);
|
|
1550
1952
|
});
|
|
1953
|
+
|
|
1954
|
+
// The distinct-out path copies the basis across before writing the translation. Only the aliased
|
|
1955
|
+
// path was exercised before, so a broken copy would have left the basis at whatever `out` held.
|
|
1956
|
+
it('copies the whole source matrix when out is a distinct object', () => {
|
|
1957
|
+
const source = createMatrix4();
|
|
1958
|
+
setMatrix4(source, 0, 1, 0, 0, -1, 0, 0, 0, 0, 0, 1, 0, 7, 8, 9, 1);
|
|
1959
|
+
const out = createMatrix4(9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9);
|
|
1960
|
+
const aliased = cloneMatrix4(source);
|
|
1961
|
+
|
|
1962
|
+
translateMatrix4(out, source, 1, 2, 3);
|
|
1963
|
+
translateMatrix4(aliased, aliased, 1, 2, 3);
|
|
1964
|
+
|
|
1965
|
+
expect(Array.from(out.m)).toEqual(Array.from(aliased.m));
|
|
1966
|
+
// The source is left untouched by the distinct-out call.
|
|
1967
|
+
expect(Array.from(source.m)).toEqual([0, 1, 0, 0, -1, 0, 0, 0, 0, 0, 1, 0, 7, 8, 9, 1]);
|
|
1968
|
+
});
|
|
1551
1969
|
});
|
|
1552
1970
|
|
|
1553
1971
|
describe('transposeMatrix4', () => {
|