@js-draw/math 1.0.0 → 1.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (65) hide show
  1. package/LICENSE +21 -0
  2. package/dist/cjs/Color4.d.ts +40 -0
  3. package/dist/cjs/Color4.js +102 -0
  4. package/dist/cjs/Color4.test.d.ts +1 -0
  5. package/dist/cjs/Mat33.test.d.ts +1 -0
  6. package/dist/cjs/Vec2.test.d.ts +1 -0
  7. package/dist/cjs/Vec3.test.d.ts +1 -0
  8. package/dist/cjs/polynomial/solveQuadratic.test.d.ts +1 -0
  9. package/dist/cjs/rounding.test.d.ts +1 -0
  10. package/dist/cjs/shapes/LineSegment2.test.d.ts +1 -0
  11. package/dist/cjs/shapes/Path.fromString.test.d.ts +1 -0
  12. package/dist/cjs/shapes/Path.test.d.ts +1 -0
  13. package/dist/cjs/shapes/Path.toString.test.d.ts +1 -0
  14. package/dist/cjs/shapes/QuadraticBezier.test.d.ts +1 -0
  15. package/dist/cjs/shapes/Rect2.test.d.ts +1 -0
  16. package/dist/cjs/shapes/Triangle.test.d.ts +1 -0
  17. package/dist/mjs/Color4.d.ts +40 -0
  18. package/dist/mjs/Color4.mjs +102 -0
  19. package/dist/mjs/Color4.test.d.ts +1 -0
  20. package/dist/mjs/Mat33.test.d.ts +1 -0
  21. package/dist/mjs/Vec2.test.d.ts +1 -0
  22. package/dist/mjs/Vec3.test.d.ts +1 -0
  23. package/dist/mjs/polynomial/solveQuadratic.test.d.ts +1 -0
  24. package/dist/mjs/rounding.test.d.ts +1 -0
  25. package/dist/mjs/shapes/LineSegment2.test.d.ts +1 -0
  26. package/dist/mjs/shapes/Path.fromString.test.d.ts +1 -0
  27. package/dist/mjs/shapes/Path.test.d.ts +1 -0
  28. package/dist/mjs/shapes/Path.toString.test.d.ts +1 -0
  29. package/dist/mjs/shapes/QuadraticBezier.test.d.ts +1 -0
  30. package/dist/mjs/shapes/Rect2.test.d.ts +1 -0
  31. package/dist/mjs/shapes/Triangle.test.d.ts +1 -0
  32. package/dist-test/test_imports/package-lock.json +13 -0
  33. package/dist-test/test_imports/package.json +12 -0
  34. package/dist-test/test_imports/test-imports.js +15 -0
  35. package/dist-test/test_imports/test-require.cjs +15 -0
  36. package/package.json +4 -3
  37. package/src/Color4.test.ts +94 -0
  38. package/src/Color4.ts +430 -0
  39. package/src/Mat33.test.ts +244 -0
  40. package/src/Mat33.ts +450 -0
  41. package/src/Vec2.test.ts +30 -0
  42. package/src/Vec2.ts +49 -0
  43. package/src/Vec3.test.ts +51 -0
  44. package/src/Vec3.ts +245 -0
  45. package/src/lib.ts +42 -0
  46. package/src/polynomial/solveQuadratic.test.ts +39 -0
  47. package/src/polynomial/solveQuadratic.ts +43 -0
  48. package/src/rounding.test.ts +65 -0
  49. package/src/rounding.ts +167 -0
  50. package/src/shapes/Abstract2DShape.ts +63 -0
  51. package/src/shapes/BezierJSWrapper.ts +93 -0
  52. package/src/shapes/CubicBezier.ts +35 -0
  53. package/src/shapes/LineSegment2.test.ts +99 -0
  54. package/src/shapes/LineSegment2.ts +232 -0
  55. package/src/shapes/Path.fromString.test.ts +223 -0
  56. package/src/shapes/Path.test.ts +309 -0
  57. package/src/shapes/Path.toString.test.ts +77 -0
  58. package/src/shapes/Path.ts +963 -0
  59. package/src/shapes/PointShape2D.ts +33 -0
  60. package/src/shapes/QuadraticBezier.test.ts +31 -0
  61. package/src/shapes/QuadraticBezier.ts +142 -0
  62. package/src/shapes/Rect2.test.ts +209 -0
  63. package/src/shapes/Rect2.ts +346 -0
  64. package/src/shapes/Triangle.test.ts +61 -0
  65. package/src/shapes/Triangle.ts +139 -0
package/src/Mat33.ts ADDED
@@ -0,0 +1,450 @@
1
+ import { Point2, Vec2 } from './Vec2';
2
+ import Vec3 from './Vec3';
3
+
4
+ export type Mat33Array = [
5
+ number, number, number,
6
+ number, number, number,
7
+ number, number, number,
8
+ ];
9
+
10
+ /**
11
+ * Represents a three dimensional linear transformation or
12
+ * a two-dimensional affine transformation. (An affine transformation scales/rotates/shears
13
+ * **and** translates while a linear transformation just scales/rotates/shears).
14
+ */
15
+ export class Mat33 {
16
+ private readonly rows: Vec3[];
17
+
18
+ /**
19
+ * Creates a matrix from inputs in the form,
20
+ * $$
21
+ * \begin{bmatrix}
22
+ * a1 & a2 & a3 \\
23
+ * b1 & b2 & b3 \\
24
+ * c1 & c2 & c3
25
+ * \end{bmatrix}
26
+ * $$
27
+ */
28
+ public constructor(
29
+ public readonly a1: number,
30
+ public readonly a2: number,
31
+ public readonly a3: number,
32
+
33
+ public readonly b1: number,
34
+ public readonly b2: number,
35
+ public readonly b3: number,
36
+
37
+ public readonly c1: number,
38
+ public readonly c2: number,
39
+ public readonly c3: number
40
+ ) {
41
+ this.rows = [
42
+ Vec3.of(a1, a2, a3),
43
+ Vec3.of(b1, b2, b3),
44
+ Vec3.of(c1, c2, c3),
45
+ ];
46
+ }
47
+
48
+ /**
49
+ * Creates a matrix from the given rows:
50
+ * $$
51
+ * \begin{bmatrix}
52
+ * \texttt{r1.x} & \texttt{r1.y} & \texttt{r1.z}\\
53
+ * \texttt{r2.x} & \texttt{r2.y} & \texttt{r2.z}\\
54
+ * \texttt{r3.x} & \texttt{r3.y} & \texttt{r3.z}\\
55
+ * \end{bmatrix}
56
+ * $$
57
+ */
58
+ public static ofRows(r1: Vec3, r2: Vec3, r3: Vec3): Mat33 {
59
+ return new Mat33(
60
+ r1.x, r1.y, r1.z,
61
+ r2.x, r2.y, r2.z,
62
+ r3.x, r3.y, r3.z
63
+ );
64
+ }
65
+
66
+ public static identity = new Mat33(
67
+ 1, 0, 0,
68
+ 0, 1, 0,
69
+ 0, 0, 1
70
+ );
71
+
72
+ /**
73
+ * Either returns the inverse of this, or, if this matrix is singular/uninvertable,
74
+ * returns Mat33.identity.
75
+ *
76
+ * This may cache the computed inverse and return the cached version instead of recomputing
77
+ * it.
78
+ */
79
+ public inverse(): Mat33 {
80
+ return this.computeInverse() ?? Mat33.identity;
81
+ }
82
+
83
+ public invertable(): boolean {
84
+ return this.computeInverse() !== null;
85
+ }
86
+
87
+ private cachedInverse: Mat33|undefined|null = undefined;
88
+ private computeInverse(): Mat33|null {
89
+ if (this.cachedInverse !== undefined) {
90
+ return this.cachedInverse;
91
+ }
92
+
93
+ const toIdentity = [
94
+ this.rows[0],
95
+ this.rows[1],
96
+ this.rows[2],
97
+ ];
98
+
99
+ const toResult = [
100
+ Vec3.unitX,
101
+ Vec3.unitY,
102
+ Vec3.unitZ,
103
+ ];
104
+
105
+ // Convert toIdentity to the identity matrix and
106
+ // toResult to the inverse through elementary row operations
107
+ for (let cursor = 0; cursor < 3; cursor++) {
108
+ // Select the [cursor]th diagonal entry
109
+ let pivot = toIdentity[cursor].at(cursor);
110
+
111
+ // Don't divide by zero (treat very small numbers as zero).
112
+ const minDivideBy = 1e-10;
113
+ if (Math.abs(pivot) < minDivideBy) {
114
+ let swapIndex = -1;
115
+ // For all other rows,
116
+ for (let i = 1; i <= 2; i++) {
117
+ const otherRowIdx = (cursor + i) % 3;
118
+
119
+ if (Math.abs(toIdentity[otherRowIdx].at(cursor)) >= minDivideBy) {
120
+ swapIndex = otherRowIdx;
121
+ break;
122
+ }
123
+ }
124
+
125
+ // Can't swap with another row?
126
+ if (swapIndex === -1) {
127
+ this.cachedInverse = null;
128
+ return null;
129
+ }
130
+
131
+ const tmpIdentityRow = toIdentity[cursor];
132
+ const tmpResultRow = toResult[cursor];
133
+
134
+ // Swap!
135
+ toIdentity[cursor] = toIdentity[swapIndex];
136
+ toResult[cursor] = toResult[swapIndex];
137
+ toIdentity[swapIndex] = tmpIdentityRow;
138
+ toResult[swapIndex] = tmpResultRow;
139
+
140
+ pivot = toIdentity[cursor].at(cursor);
141
+ }
142
+
143
+ // Make toIdentity[k = cursor] = 1
144
+ let scale = 1.0 / pivot;
145
+ toIdentity[cursor] = toIdentity[cursor].times(scale);
146
+ toResult[cursor] = toResult[cursor].times(scale);
147
+
148
+ const cursorToIdentityRow = toIdentity[cursor];
149
+ const cursorToResultRow = toResult[cursor];
150
+
151
+ // Make toIdentity[k ≠ cursor] = 0
152
+ for (let i = 1; i <= 2; i++) {
153
+ const otherRowIdx = (cursor + i) % 3;
154
+ scale = -toIdentity[otherRowIdx].at(cursor);
155
+ toIdentity[otherRowIdx] = toIdentity[otherRowIdx].plus(
156
+ cursorToIdentityRow.times(scale)
157
+ );
158
+ toResult[otherRowIdx] = toResult[otherRowIdx].plus(
159
+ cursorToResultRow.times(scale)
160
+ );
161
+ }
162
+ }
163
+
164
+ const inverse = Mat33.ofRows(
165
+ toResult[0],
166
+ toResult[1],
167
+ toResult[2]
168
+ );
169
+ this.cachedInverse = inverse;
170
+ return inverse;
171
+ }
172
+
173
+ public transposed(): Mat33 {
174
+ return new Mat33(
175
+ this.a1, this.b1, this.c1,
176
+ this.a2, this.b2, this.c2,
177
+ this.a3, this.b3, this.c3
178
+ );
179
+ }
180
+
181
+ public rightMul(other: Mat33): Mat33 {
182
+ other = other.transposed();
183
+
184
+ const at = (row: number, col: number): number => {
185
+ return this.rows[row].dot(other.rows[col]);
186
+ };
187
+
188
+ return new Mat33(
189
+ at(0, 0), at(0, 1), at(0, 2),
190
+ at(1, 0), at(1, 1), at(1, 2),
191
+ at(2, 0), at(2, 1), at(2, 2)
192
+ );
193
+ }
194
+
195
+ /**
196
+ * Applies this as an **affine** transformation to the given vector.
197
+ * Returns a transformed version of `other`.
198
+ *
199
+ * Unlike {@link transformVec3}, this **does** translate the given vector.
200
+ */
201
+ public transformVec2(other: Vec2): Vec2 {
202
+ // When transforming a Vec2, we want to use the z transformation
203
+ // components of this for translation:
204
+ // ⎡ . . tX ⎤
205
+ // ⎢ . . tY ⎥
206
+ // ⎣ 0 0 1 ⎦
207
+ // For this, we need other's z component to be 1 (so that tX and tY
208
+ // are scaled by 1):
209
+ let intermediate = Vec3.of(other.x, other.y, 1);
210
+ intermediate = this.transformVec3(intermediate);
211
+
212
+ // Drop the z=1 to allow magnitude to work as expected
213
+ return Vec2.of(intermediate.x, intermediate.y);
214
+ }
215
+
216
+ /**
217
+ * Applies this as a linear transformation to the given vector (doesn't translate).
218
+ * This is the standard way of transforming vectors in ℝ³.
219
+ */
220
+ public transformVec3(other: Vec3): Vec3 {
221
+ return Vec3.of(
222
+ this.rows[0].dot(other),
223
+ this.rows[1].dot(other),
224
+ this.rows[2].dot(other)
225
+ );
226
+ }
227
+
228
+ /** @returns true iff this is the identity matrix. */
229
+ public isIdentity(): boolean {
230
+ if (this === Mat33.identity) {
231
+ return true;
232
+ }
233
+
234
+ return this.eq(Mat33.identity);
235
+ }
236
+
237
+ /** Returns true iff this = other ± fuzz */
238
+ public eq(other: Mat33, fuzz: number = 0): boolean {
239
+ for (let i = 0; i < 3; i++) {
240
+ if (!this.rows[i].eq(other.rows[i], fuzz)) {
241
+ return false;
242
+ }
243
+ }
244
+
245
+ return true;
246
+ }
247
+
248
+ public toString(): string {
249
+ let result = '';
250
+ const maxColumnLens = [ 0, 0, 0 ];
251
+
252
+ // Determine the longest item in each column so we can pad the others to that
253
+ // length.
254
+ for (const row of this.rows) {
255
+ for (let i = 0; i < 3; i++) {
256
+ maxColumnLens[i] = Math.max(maxColumnLens[0], `${row.at(i)}`.length);
257
+ }
258
+ }
259
+
260
+ for (let i = 0; i < 3; i++) {
261
+ if (i === 0) {
262
+ result += '⎡ ';
263
+ } else if (i === 1) {
264
+ result += '⎢ ';
265
+ } else {
266
+ result += '⎣ ';
267
+ }
268
+
269
+ // Add each component of the ith row (after padding it)
270
+ for (let j = 0; j < 3; j++) {
271
+ const val = this.rows[i].at(j).toString();
272
+
273
+ let padding = '';
274
+ for (let i = val.length; i < maxColumnLens[j]; i++) {
275
+ padding += ' ';
276
+ }
277
+
278
+ result += val + ', ' + padding;
279
+ }
280
+
281
+ if (i === 0) {
282
+ result += ' ⎤';
283
+ } else if (i === 1) {
284
+ result += ' ⎥';
285
+ } else {
286
+ result += ' ⎦';
287
+ }
288
+ result += '\n';
289
+ }
290
+
291
+ return result.trimEnd();
292
+ }
293
+
294
+ /**
295
+ * ```
296
+ * result[0] = top left element
297
+ * result[1] = element at row zero, column 1
298
+ * ...
299
+ * ```
300
+ */
301
+ public toArray(): Mat33Array {
302
+ return [
303
+ this.a1, this.a2, this.a3,
304
+ this.b1, this.b2, this.b3,
305
+ this.c1, this.c2, this.c3,
306
+ ];
307
+ }
308
+
309
+ /**
310
+ * Returns a new `Mat33` where each entry is the output of the function
311
+ * `mapping`.
312
+ *
313
+ * @example
314
+ * ```
315
+ * new Mat33(
316
+ * 1, 2, 3,
317
+ * 4, 5, 6,
318
+ * 7, 8, 9,
319
+ * ).mapEntries(component => component - 1);
320
+ * // → ⎡ 0, 1, 2 ⎤
321
+ * // ⎢ 3, 4, 5 ⎥
322
+ * // ⎣ 6, 7, 8 ⎦
323
+ * ```
324
+ */
325
+ public mapEntries(mapping: (component: number, rowcol: [number, number])=>number): Mat33 {
326
+ return new Mat33(
327
+ mapping(this.a1, [0, 0]), mapping(this.a2, [0, 1]), mapping(this.a3, [0, 2]),
328
+ mapping(this.b1, [1, 0]), mapping(this.b2, [1, 1]), mapping(this.b3, [1, 2]),
329
+ mapping(this.c1, [2, 0]), mapping(this.c2, [2, 1]), mapping(this.c3, [2, 2]),
330
+ );
331
+ }
332
+
333
+ /** Estimate the scale factor of this matrix (based on the first row). */
334
+ public getScaleFactor() {
335
+ return Math.hypot(this.a1, this.a2);
336
+ }
337
+
338
+ /**
339
+ * Constructs a 3x3 translation matrix (for translating `Vec2`s) using
340
+ * **transformVec2**.
341
+ */
342
+ public static translation(amount: Vec2): Mat33 {
343
+ // When transforming Vec2s by a 3x3 matrix, we give the input
344
+ // Vec2s z = 1. As such,
345
+ // outVec2.x = inVec2.x * 1 + inVec2.y * 0 + 1 * amount.x
346
+ // ...
347
+ return new Mat33(
348
+ 1, 0, amount.x,
349
+ 0, 1, amount.y,
350
+ 0, 0, 1
351
+ );
352
+ }
353
+
354
+ public static zRotation(radians: number, center: Point2 = Vec2.zero): Mat33 {
355
+ if (radians === 0) {
356
+ return Mat33.identity;
357
+ }
358
+
359
+ const cos = Math.cos(radians);
360
+ const sin = Math.sin(radians);
361
+
362
+ // Translate everything so that rotation is about the origin
363
+ let result = Mat33.translation(center);
364
+
365
+ result = result.rightMul(new Mat33(
366
+ cos, -sin, 0,
367
+ sin, cos, 0,
368
+ 0, 0, 1
369
+ ));
370
+ return result.rightMul(Mat33.translation(center.times(-1)));
371
+ }
372
+
373
+ public static scaling2D(amount: number|Vec2, center: Point2 = Vec2.zero): Mat33 {
374
+ let result = Mat33.translation(center);
375
+ let xAmount, yAmount;
376
+
377
+ if (typeof amount === 'number') {
378
+ xAmount = amount;
379
+ yAmount = amount;
380
+ } else {
381
+ xAmount = amount.x;
382
+ yAmount = amount.y;
383
+ }
384
+
385
+ result = result.rightMul(new Mat33(
386
+ xAmount, 0, 0,
387
+ 0, yAmount, 0,
388
+ 0, 0, 1
389
+ ));
390
+
391
+ // Translate such that [center] goes to (0, 0)
392
+ return result.rightMul(Mat33.translation(center.times(-1)));
393
+ }
394
+
395
+ /** @see {@link fromCSSMatrix} */
396
+ public toCSSMatrix(): string {
397
+ return `matrix(${this.a1},${this.b1},${this.a2},${this.b2},${this.a3},${this.b3})`;
398
+ }
399
+
400
+ /**
401
+ * Converts a CSS-form `matrix(a, b, c, d, e, f)` to a Mat33.
402
+ *
403
+ * Note that such a matrix has the form,
404
+ * ```
405
+ * ⎡ a c e ⎤
406
+ * ⎢ b d f ⎥
407
+ * ⎣ 0 0 1 ⎦
408
+ * ```
409
+ */
410
+ public static fromCSSMatrix(cssString: string): Mat33 {
411
+ if (cssString === '' || cssString === 'none') {
412
+ return Mat33.identity;
413
+ }
414
+
415
+ const numberExp = '([-]?\\d*(?:\\.\\d*)?(?:[eE][-]?\\d+)?)';
416
+ const numberSepExp = '[, \\t\\n]';
417
+ const regExpSource = `^\\s*matrix\\s*\\(${
418
+ [
419
+ // According to MDN, matrix(a,b,c,d,e,f) has form:
420
+ // ⎡ a c e ⎤
421
+ // ⎢ b d f ⎥
422
+ // ⎣ 0 0 1 ⎦
423
+ numberExp, numberExp, numberExp, // a, c, e
424
+ numberExp, numberExp, numberExp, // b, d, f
425
+ ].join(`${numberSepExp}+`)
426
+ }${numberSepExp}*\\)\\s*$`;
427
+ const matrixExp = new RegExp(regExpSource, 'i');
428
+ const match = matrixExp.exec(cssString);
429
+
430
+ if (!match) {
431
+ throw new Error(`Unsupported transformation: ${cssString}`);
432
+ }
433
+
434
+ const matrixData = match.slice(1).map(entry => parseFloat(entry));
435
+ const a = matrixData[0];
436
+ const b = matrixData[1];
437
+ const c = matrixData[2];
438
+ const d = matrixData[3];
439
+ const e = matrixData[4];
440
+ const f = matrixData[5];
441
+
442
+ const transform = new Mat33(
443
+ a, c, e,
444
+ b, d, f,
445
+ 0, 0, 1
446
+ );
447
+ return transform;
448
+ }
449
+ }
450
+ export default Mat33;
@@ -0,0 +1,30 @@
1
+ import { Vec2 } from './Vec2';
2
+ import Vec3 from './Vec3';
3
+
4
+ describe('Vec2', () => {
5
+ it('Magnitude', () => {
6
+ expect(Vec2.of(3, 4).magnitude()).toBe(5);
7
+ });
8
+
9
+ it('Addition', () => {
10
+ expect(Vec2.of(1, 2).plus(Vec2.of(3, 4))).objEq(Vec2.of(4, 6));
11
+ });
12
+
13
+ it('Multiplication', () => {
14
+ expect(Vec2.of(1, -1).times(22)).objEq(Vec2.of(22, -22));
15
+ });
16
+
17
+ it('More complicated expressions', () => {
18
+ expect((Vec2.of(1, 2).plus(Vec2.of(3, 4))).times(2)).objEq(Vec2.of(8, 12));
19
+ });
20
+
21
+ it('Angle', () => {
22
+ expect(Vec2.of(-1, 1).angle()).toBeCloseTo(3 * Math.PI / 4);
23
+ });
24
+
25
+ it('Perpindicular', () => {
26
+ const fuzz = 0.001;
27
+ expect(Vec2.unitX.cross(Vec3.unitZ)).objEq(Vec2.unitY.times(-1), fuzz);
28
+ expect(Vec2.unitX.orthog()).objEq(Vec2.unitY, fuzz);
29
+ });
30
+ });
package/src/Vec2.ts ADDED
@@ -0,0 +1,49 @@
1
+ import Vec3 from './Vec3';
2
+
3
+ /**
4
+ * Utility functions that facilitate treating `Vec3`s as 2D vectors.
5
+ *
6
+ * @example
7
+ * ```ts,runnable,console
8
+ * import { Vec2 } from '@js-draw/math';
9
+ * console.log(Vec2.of(1, 2));
10
+ * ```
11
+ */
12
+ export namespace Vec2 {
13
+ /**
14
+ * Creates a `Vec2` from an x and y coordinate.
15
+ *
16
+ * For example,
17
+ * ```ts
18
+ * const v = Vec2.of(3, 4); // x=3, y=4.
19
+ * ```
20
+ */
21
+ export const of = (x: number, y: number): Vec2 => {
22
+ return Vec3.of(x, y, 0);
23
+ };
24
+
25
+ /**
26
+ * Creates a `Vec2` from an object containing x and y coordinates.
27
+ *
28
+ * For example,
29
+ * ```ts
30
+ * const v1 = Vec2.ofXY({ x: 3, y: 4.5 });
31
+ * const v2 = Vec2.ofXY({ x: -123.4, y: 1 });
32
+ * ```
33
+ */
34
+ export const ofXY = ({x, y}: { x: number, y: number }): Vec2 => {
35
+ return Vec3.of(x, y, 0);
36
+ };
37
+
38
+ /** A vector of length 1 in the X direction (→). */
39
+ export const unitX = Vec2.of(1, 0);
40
+
41
+ /** A vector of length 1 in the Y direction (↑). */
42
+ export const unitY = Vec2.of(0, 1);
43
+
44
+ /** The zero vector: A vector with x=0, y=0. */
45
+ export const zero = Vec2.of(0, 0);
46
+ }
47
+
48
+ export type Point2 = Vec3;
49
+ export type Vec2 = Vec3; // eslint-disable-line
@@ -0,0 +1,51 @@
1
+
2
+ import Vec3 from './Vec3';
3
+
4
+ describe('Vec3', () => {
5
+ it('.xy should contain the x and y components', () => {
6
+ const vec = Vec3.of(1, 2, 3);
7
+ expect(vec.xy).toMatchObject({
8
+ x: 1,
9
+ y: 2,
10
+ });
11
+ });
12
+
13
+ it('should be combinable with other vectors via .zip', () => {
14
+ const vec1 = Vec3.unitX;
15
+ const vec2 = Vec3.unitY;
16
+ expect(vec1.zip(vec2, Math.min)).objEq(Vec3.zero);
17
+ expect(vec1.zip(vec2, Math.max)).objEq(Vec3.of(1, 1, 0));
18
+ });
19
+
20
+ it('.cross should obey the right hand rule', () => {
21
+ const vec1 = Vec3.unitX;
22
+ const vec2 = Vec3.unitY;
23
+ expect(vec1.cross(vec2)).objEq(Vec3.unitZ);
24
+ expect(vec2.cross(vec1)).objEq(Vec3.unitZ.times(-1));
25
+ });
26
+
27
+ it('.orthog should return an orthogonal vector', () => {
28
+ expect(Vec3.unitZ.orthog().dot(Vec3.unitZ)).toBe(0);
29
+
30
+ // Should return some orthogonal vector, even if given the zero vector
31
+ expect(Vec3.zero.orthog().dot(Vec3.zero)).toBe(0);
32
+ });
33
+
34
+ it('.minus should return the difference between two vectors', () => {
35
+ expect(Vec3.of(1, 2, 3).minus(Vec3.of(4, 5, 6))).objEq(Vec3.of(1 - 4, 2 - 5, 3 - 6));
36
+ });
37
+
38
+ it('.orthog should return a unit vector', () => {
39
+ expect(Vec3.zero.orthog().magnitude()).toBe(1);
40
+ expect(Vec3.unitZ.orthog().magnitude()).toBe(1);
41
+ expect(Vec3.unitX.orthog().magnitude()).toBe(1);
42
+ expect(Vec3.unitY.orthog().magnitude()).toBe(1);
43
+ });
44
+
45
+ it('.normalizedOrZero should normalize the given vector or return zero', () => {
46
+ expect(Vec3.zero.normalizedOrZero()).objEq(Vec3.zero);
47
+ expect(Vec3.unitX.normalizedOrZero()).objEq(Vec3.unitX);
48
+ expect(Vec3.unitX.times(22).normalizedOrZero()).objEq(Vec3.unitX);
49
+ expect(Vec3.of(1, 1, 1).times(22).normalizedOrZero().length()).toBeCloseTo(1);
50
+ });
51
+ });