@emasoft/svg-matrix 1.0.1 → 1.0.3

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/src/matrix.js CHANGED
@@ -1,11 +1,33 @@
1
1
  import Decimal from 'decimal.js';
2
2
  import { Vector } from './vector.js';
3
+
4
+ /**
5
+ * Helper to convert any numeric input to Decimal.
6
+ * Accepts numbers, strings, or Decimal instances.
7
+ * @param {number|string|Decimal} x - The value to convert
8
+ * @returns {Decimal} The Decimal representation
9
+ */
3
10
  const D = x => (x instanceof Decimal ? x : new Decimal(x));
4
11
 
5
12
  /**
6
- * Matrix - Decimal-backed matrix class
13
+ * Matrix - Decimal-backed matrix class for arbitrary-precision matrix operations.
14
+ *
15
+ * All numeric inputs are automatically converted to Decimal for high precision.
16
+ * Supports basic operations (add, sub, mul, transpose), linear algebra
17
+ * (LU, QR, determinant, inverse, solve), and matrix exponential.
18
+ *
19
+ * @example
20
+ * const M = Matrix.from([[1, 2], [3, 4]]);
21
+ * const I = Matrix.identity(2);
22
+ * const product = M.mul(I);
23
+ * console.log(product.toArrayOfStrings());
7
24
  */
8
25
  export class Matrix {
26
+ /**
27
+ * Create a new Matrix from a 2D array.
28
+ * @param {Array<Array<number|string|Decimal>>} data - 2D array of matrix elements
29
+ * @throws {Error} If data is not a non-empty 2D array with consistent row lengths
30
+ */
9
31
  constructor(data) {
10
32
  if (!Array.isArray(data) || data.length === 0) throw new Error('Matrix requires non-empty 2D array');
11
33
  const cols = data[0].length;
@@ -17,35 +39,82 @@ export class Matrix {
17
39
  this.cols = cols;
18
40
  }
19
41
 
42
+ /**
43
+ * Factory method to create a Matrix from a 2D array.
44
+ * @param {Array<Array<number|string|Decimal>>} arr - 2D array of matrix elements
45
+ * @returns {Matrix} New Matrix instance
46
+ */
20
47
  static from(arr) {
21
48
  return new Matrix(arr);
22
49
  }
23
50
 
51
+ /**
52
+ * Create a matrix of zeros.
53
+ * @param {number} r - Number of rows
54
+ * @param {number} c - Number of columns
55
+ * @returns {Matrix} New r×c zero matrix
56
+ */
24
57
  static zeros(r, c) {
25
58
  const out = Array.from({ length: r }, () => Array.from({ length: c }, () => new Decimal(0)));
26
59
  return new Matrix(out);
27
60
  }
28
61
 
62
+ /**
63
+ * Create an identity matrix.
64
+ * @param {number} n - Size of the square identity matrix
65
+ * @returns {Matrix} New n×n identity matrix
66
+ */
29
67
  static identity(n) {
30
68
  const out = Array.from({ length: n }, (_, i) => Array.from({ length: n }, (_, j) => (i === j ? new Decimal(1) : new Decimal(0))));
31
69
  return new Matrix(out);
32
70
  }
33
71
 
72
+ /**
73
+ * Create a deep copy of this matrix.
74
+ * @returns {Matrix} New Matrix with copied values
75
+ */
34
76
  clone() {
35
77
  return new Matrix(this.data.map(r => r.map(v => new Decimal(v))));
36
78
  }
37
79
 
80
+ /**
81
+ * Convert matrix to 2D array of strings.
82
+ * Preserves full precision of Decimal values.
83
+ * @returns {string[][]} 2D array of string values
84
+ */
38
85
  toArrayOfStrings() {
39
86
  return this.data.map(r => r.map(v => v.toString()));
40
87
  }
41
88
 
42
- // apply matrix to Vector or array: returns Vector
89
+ /**
90
+ * Convert matrix to 2D array of JavaScript numbers.
91
+ * Note: May lose precision for very large or precise values.
92
+ * @returns {number[][]} 2D array of number values
93
+ */
94
+ toNumberArray() {
95
+ return this.data.map(r => r.map(v => v.toNumber()));
96
+ }
97
+
98
+ /**
99
+ * Check if this is a square matrix.
100
+ * @returns {boolean} True if rows equals cols
101
+ */
102
+ isSquare() {
103
+ return this.rows === this.cols;
104
+ }
105
+
106
+ /**
107
+ * Apply this matrix to a Vector (matrix-vector multiplication).
108
+ * @param {Vector|Array} vec - Vector or array to multiply
109
+ * @returns {Vector} Result of M × v
110
+ * @throws {Error} If dimensions don't match
111
+ */
43
112
  applyToVector(vec) {
44
113
  let v;
45
114
  if (vec instanceof Vector) v = vec;
46
115
  else if (Array.isArray(vec)) v = Vector.from(vec);
47
116
  else throw new Error('applyToVector expects Vector or array');
48
- if (this.cols !== v.length) throw new Error('shape mismatch');
117
+ if (this.cols !== v.length) throw new Error('shape mismatch: matrix cols must equal vector length');
49
118
  const out = [];
50
119
  for (let i = 0; i < this.rows; i++) {
51
120
  let sum = new Decimal(0);
@@ -55,10 +124,15 @@ export class Matrix {
55
124
  return new Vector(out);
56
125
  }
57
126
 
58
- // elementwise add/sub or scalar
127
+ /**
128
+ * Element-wise addition, or scalar addition if other is a number.
129
+ * @param {Matrix|number|string|Decimal} other - Matrix or scalar to add
130
+ * @returns {Matrix} New Matrix with sum
131
+ * @throws {Error} If dimensions mismatch (for Matrix addition)
132
+ */
59
133
  add(other) {
60
134
  if (other instanceof Matrix) {
61
- if (this.rows !== other.rows || this.cols !== other.cols) throw new Error('shape mismatch');
135
+ if (this.rows !== other.rows || this.cols !== other.cols) throw new Error('shape mismatch: matrices must have same dimensions');
62
136
  return new Matrix(this.data.map((r, i) => r.map((v, j) => v.plus(other.data[i][j]))));
63
137
  } else {
64
138
  const s = D(other);
@@ -66,9 +140,15 @@ export class Matrix {
66
140
  }
67
141
  }
68
142
 
143
+ /**
144
+ * Element-wise subtraction, or scalar subtraction if other is a number.
145
+ * @param {Matrix|number|string|Decimal} other - Matrix or scalar to subtract
146
+ * @returns {Matrix} New Matrix with difference
147
+ * @throws {Error} If dimensions mismatch (for Matrix subtraction)
148
+ */
69
149
  sub(other) {
70
150
  if (other instanceof Matrix) {
71
- if (this.rows !== other.rows || this.cols !== other.cols) throw new Error('shape mismatch');
151
+ if (this.rows !== other.rows || this.cols !== other.cols) throw new Error('shape mismatch: matrices must have same dimensions');
72
152
  return new Matrix(this.data.map((r, i) => r.map((v, j) => v.minus(other.data[i][j]))));
73
153
  } else {
74
154
  const s = D(other);
@@ -76,9 +156,15 @@ export class Matrix {
76
156
  }
77
157
  }
78
158
 
159
+ /**
160
+ * Matrix multiplication, or scalar multiplication if other is a number.
161
+ * @param {Matrix|number|string|Decimal} other - Matrix or scalar to multiply
162
+ * @returns {Matrix} New Matrix with product
163
+ * @throws {Error} If dimensions don't allow multiplication
164
+ */
79
165
  mul(other) {
80
166
  if (other instanceof Matrix) {
81
- if (this.cols !== other.rows) throw new Error('shape mismatch');
167
+ if (this.cols !== other.rows) throw new Error('shape mismatch: A.cols must equal B.rows for matrix multiplication');
82
168
  const out = Array.from({ length: this.rows }, () => Array.from({ length: other.cols }, () => new Decimal(0)));
83
169
  for (let i = 0; i < this.rows; i++) {
84
170
  for (let k = 0; k < this.cols; k++) {
@@ -94,28 +180,94 @@ export class Matrix {
94
180
  }
95
181
  }
96
182
 
183
+ /**
184
+ * Scalar division (divide all elements by scalar).
185
+ * @param {number|string|Decimal} scalar - Scalar to divide by
186
+ * @returns {Matrix} New Matrix with each element divided
187
+ * @throws {Error} If scalar is zero
188
+ */
189
+ div(scalar) {
190
+ const s = D(scalar);
191
+ if (s.isZero()) throw new Error('Cannot divide by zero');
192
+ return new Matrix(this.data.map(r => r.map(v => v.div(s))));
193
+ }
194
+
195
+ /**
196
+ * Negate all elements (multiply by -1).
197
+ * @returns {Matrix} New Matrix with negated elements
198
+ */
199
+ negate() {
200
+ return new Matrix(this.data.map(r => r.map(v => v.negated())));
201
+ }
202
+
203
+ /**
204
+ * Transpose the matrix (swap rows and columns).
205
+ * @returns {Matrix} New transposed Matrix
206
+ */
97
207
  transpose() {
98
208
  const out = Array.from({ length: this.cols }, (_, i) => Array.from({ length: this.rows }, (_, j) => new Decimal(this.data[j][i])));
99
209
  return new Matrix(out);
100
210
  }
101
211
 
102
- // LU decomposition (returns {L, U, P})
212
+ /**
213
+ * Compute the trace (sum of diagonal elements).
214
+ * Only defined for square matrices.
215
+ * @returns {Decimal} Sum of diagonal elements
216
+ * @throws {Error} If matrix is not square
217
+ */
218
+ trace() {
219
+ if (!this.isSquare()) throw new Error('Trace only defined for square matrices');
220
+ let sum = new Decimal(0);
221
+ for (let i = 0; i < this.rows; i++) {
222
+ sum = sum.plus(this.data[i][i]);
223
+ }
224
+ return sum;
225
+ }
226
+
227
+ /**
228
+ * Check equality with another matrix within optional tolerance.
229
+ * @param {Matrix} other - Matrix to compare with
230
+ * @param {number|string|Decimal} [tolerance=0] - Maximum allowed difference per element
231
+ * @returns {boolean} True if matrices are equal within tolerance
232
+ */
233
+ equals(other, tolerance = 0) {
234
+ if (!(other instanceof Matrix)) return false;
235
+ if (this.rows !== other.rows || this.cols !== other.cols) return false;
236
+ const tol = D(tolerance);
237
+ for (let i = 0; i < this.rows; i++) {
238
+ for (let j = 0; j < this.cols; j++) {
239
+ const diff = this.data[i][j].minus(other.data[i][j]).abs();
240
+ if (diff.greaterThan(tol)) return false;
241
+ }
242
+ }
243
+ return true;
244
+ }
245
+
246
+ /**
247
+ * LU decomposition with partial pivoting.
248
+ * Returns L (lower triangular), U (upper triangular), and P (permutation matrix)
249
+ * such that P × A = L × U.
250
+ * @returns {{L: Matrix, U: Matrix, P: Matrix}} LU decomposition components
251
+ * @throws {Error} If matrix is not square or is singular
252
+ */
103
253
  lu() {
104
- if (this.rows !== this.cols) throw new Error('LU requires square');
254
+ if (!this.isSquare()) throw new Error('LU decomposition requires square matrix');
105
255
  const n = this.rows;
106
- const A = this.clone().data.map(r => r.map(v => new Decimal(v)));
256
+ const A = this.data.map(r => r.map(v => new Decimal(v)));
107
257
  const Pvec = Array.from({ length: n }, (_, i) => i);
108
258
  const L = Array.from({ length: n }, () => Array.from({ length: n }, () => new Decimal(0)));
109
259
  for (let i = 0; i < n; i++) L[i][i] = new Decimal(1);
110
260
 
111
261
  for (let k = 0; k < n; k++) {
262
+ // Find pivot
112
263
  let pivot = k;
113
264
  let maxAbs = A[k][k].abs();
114
265
  for (let i = k + 1; i < n; i++) {
115
266
  const aabs = A[i][k].abs();
116
267
  if (aabs.greaterThan(maxAbs)) { maxAbs = aabs; pivot = i; }
117
268
  }
118
- if (A[pivot][k].isZero()) throw new Error('Singular matrix in LU');
269
+ if (A[pivot][k].isZero()) throw new Error('Singular matrix: LU decomposition failed');
270
+ // Swap rows
119
271
  if (pivot !== k) {
120
272
  const tmp = A[k]; A[k] = A[pivot]; A[pivot] = tmp;
121
273
  const tmpIdx = Pvec[k]; Pvec[k] = Pvec[pivot]; Pvec[pivot] = tmpIdx;
@@ -123,6 +275,7 @@ export class Matrix {
123
275
  const t = L[k][j]; L[k][j] = L[pivot][j]; L[pivot][j] = t;
124
276
  }
125
277
  }
278
+ // Elimination
126
279
  for (let i = k + 1; i < n; i++) {
127
280
  const factor = A[i][k].div(A[k][k]);
128
281
  L[i][k] = factor;
@@ -136,40 +289,66 @@ export class Matrix {
136
289
  return { L: new Matrix(L), U: new Matrix(U), P: P };
137
290
  }
138
291
 
292
+ /**
293
+ * Compute the determinant of a square matrix.
294
+ * Uses LU decomposition.
295
+ * @returns {Decimal} The determinant
296
+ * @throws {Error} If matrix is not square
297
+ */
139
298
  determinant() {
140
- if (this.rows !== this.cols) throw new Error('Determinant only for square');
299
+ if (!this.isSquare()) throw new Error('Determinant only defined for square matrices');
141
300
  const n = this.rows;
142
301
  const { L, U, P } = this.lu();
143
302
  let det = new Decimal(1);
144
303
  for (let i = 0; i < n; i++) det = det.mul(U.data[i][i]);
145
- // permutation sign
304
+ // Compute permutation sign
146
305
  const perm = [];
147
306
  for (let i = 0; i < n; i++) {
148
307
  for (let j = 0; j < n; j++) if (P.data[i][j].equals(1)) perm.push(j);
149
308
  }
150
- let inv = 0;
151
- for (let i = 0; i < perm.length; i++) for (let j = i + 1; j < perm.length; j++) if (perm[i] > perm[j]) inv++;
152
- if (inv % 2 === 1) det = det.negated();
309
+ let inversions = 0;
310
+ for (let i = 0; i < perm.length; i++) {
311
+ for (let j = i + 1; j < perm.length; j++) {
312
+ if (perm[i] > perm[j]) inversions++;
313
+ }
314
+ }
315
+ if (inversions % 2 === 1) det = det.negated();
153
316
  return det;
154
317
  }
155
318
 
319
+ /**
320
+ * Compute the inverse of a square matrix.
321
+ * Uses Gauss-Jordan elimination with partial pivoting.
322
+ * @returns {Matrix} The inverse matrix
323
+ * @throws {Error} If matrix is not square or is singular
324
+ */
156
325
  inverse() {
157
- if (this.rows !== this.cols) throw new Error('Inverse only for square');
326
+ if (!this.isSquare()) throw new Error('Inverse only defined for square matrices');
158
327
  const n = this.rows;
159
- const aug = Array.from({ length: n }, (_, i) => Array.from({ length: 2 * n }, (_, j) => (j < n ? new Decimal(this.data[i][j]) : (j - n === i ? new Decimal(1) : new Decimal(0)))));
328
+ // Create augmented matrix [A | I]
329
+ const aug = Array.from({ length: n }, (_, i) =>
330
+ Array.from({ length: 2 * n }, (_, j) =>
331
+ (j < n ? new Decimal(this.data[i][j]) : (j - n === i ? new Decimal(1) : new Decimal(0)))
332
+ )
333
+ );
334
+ // Gauss-Jordan elimination
160
335
  for (let col = 0; col < n; col++) {
336
+ // Find pivot
161
337
  let pivot = col;
162
338
  let maxAbs = aug[col][col].abs();
163
339
  for (let r = col + 1; r < n; r++) {
164
340
  const aabs = aug[r][col].abs();
165
341
  if (aabs.greaterThan(maxAbs)) { maxAbs = aabs; pivot = r; }
166
342
  }
167
- if (aug[pivot][col].isZero()) throw new Error('Singular matrix');
343
+ if (aug[pivot][col].isZero()) throw new Error('Singular matrix: inverse does not exist');
344
+ // Swap rows
168
345
  if (pivot !== col) {
169
346
  const tmp = aug[col]; aug[col] = aug[pivot]; aug[pivot] = tmp;
170
347
  }
348
+ // Scale pivot row
171
349
  const pivval = aug[col][col];
172
350
  for (let j = 0; j < 2 * n; j++) aug[col][j] = aug[col][j].div(pivval);
351
+ // Eliminate column
173
352
  for (let r = 0; r < n; r++) {
174
353
  if (r === col) continue;
175
354
  const factor = aug[r][col];
@@ -177,22 +356,31 @@ export class Matrix {
177
356
  for (let j = 0; j < 2 * n; j++) aug[r][j] = aug[r][j].minus(factor.mul(aug[col][j]));
178
357
  }
179
358
  }
359
+ // Extract inverse from augmented matrix
180
360
  const inv = aug.map(row => row.slice(n));
181
361
  return new Matrix(inv);
182
362
  }
183
363
 
184
- // solve Ax = b where b is Vector or array; returns Vector
364
+ /**
365
+ * Solve the linear system Ax = b where A is this matrix.
366
+ * Uses Gaussian elimination with partial pivoting.
367
+ * @param {Vector|Array} b - Right-hand side vector
368
+ * @returns {Vector} Solution vector x
369
+ * @throws {Error} If matrix is not square, dimensions mismatch, or system is singular
370
+ */
185
371
  solve(b) {
186
372
  let B;
187
373
  if (b instanceof Vector) B = b;
188
374
  else if (Array.isArray(b)) B = Vector.from(b);
189
375
  else throw new Error('b must be Vector or array');
190
- if (this.rows !== this.cols) throw new Error('Solve only implemented for square A');
376
+ if (!this.isSquare()) throw new Error('solve() only implemented for square matrices');
191
377
  const n = this.rows;
192
- if (B.length !== n) throw new Error('dimension mismatch');
193
- // convert to augmented array
194
- const aug = Array.from({ length: n }, (_, i) => Array.from({ length: n + 1 }, (_, j) => new Decimal(j < n ? this.data[i][j] : B.data[i])));
195
- // forward elimination
378
+ if (B.length !== n) throw new Error('dimension mismatch: b length must equal matrix rows');
379
+ // Create augmented matrix [A | b]
380
+ const aug = Array.from({ length: n }, (_, i) =>
381
+ Array.from({ length: n + 1 }, (_, j) => new Decimal(j < n ? this.data[i][j] : B.data[i]))
382
+ );
383
+ // Forward elimination
196
384
  for (let col = 0; col < n; col++) {
197
385
  let pivot = col;
198
386
  let maxAbs = aug[col][col].abs();
@@ -200,7 +388,7 @@ export class Matrix {
200
388
  const aabs = aug[r][col].abs();
201
389
  if (aabs.greaterThan(maxAbs)) { maxAbs = aabs; pivot = r; }
202
390
  }
203
- if (aug[pivot][col].isZero()) throw new Error('Singular or no unique solution');
391
+ if (aug[pivot][col].isZero()) throw new Error('Singular matrix: no unique solution');
204
392
  if (pivot !== col) { const tmp = aug[col]; aug[col] = aug[pivot]; aug[pivot] = tmp; }
205
393
  for (let r = col + 1; r < n; r++) {
206
394
  const factor = aug[r][col].div(aug[col][col]);
@@ -208,7 +396,7 @@ export class Matrix {
208
396
  for (let j = col; j < n + 1; j++) aug[r][j] = aug[r][j].minus(factor.mul(aug[col][j]));
209
397
  }
210
398
  }
211
- // back substitution
399
+ // Back substitution
212
400
  const x = Array.from({ length: n }, () => new Decimal(0));
213
401
  for (let i = n - 1; i >= 0; i--) {
214
402
  let sum = new Decimal(0);
@@ -218,46 +406,76 @@ export class Matrix {
218
406
  return new Vector(x);
219
407
  }
220
408
 
221
- // QR via Householder (returns {Q, R})
409
+ /**
410
+ * QR decomposition via Householder reflections.
411
+ * Returns Q (orthogonal) and R (upper triangular) such that A = Q × R.
412
+ * @returns {{Q: Matrix, R: Matrix}} QR decomposition components
413
+ */
222
414
  qr() {
223
415
  const m = this.rows, n = this.cols;
224
- let A = this.clone().data.map(r => r.map(v => new Decimal(v)));
416
+ let A = this.data.map(r => r.map(v => new Decimal(v)));
225
417
  const Q = Matrix.identity(m).data;
418
+
226
419
  for (let k = 0; k < Math.min(m, n); k++) {
420
+ // Extract column k below diagonal
227
421
  const x = [];
228
422
  for (let i = k; i < m; i++) x.push(A[i][k]);
423
+
424
+ // Compute norm
229
425
  let normx = new Decimal(0);
230
426
  for (const xi of x) normx = normx.plus(xi.mul(xi));
231
427
  normx = normx.sqrt();
232
428
  if (normx.isZero()) continue;
429
+
430
+ // Compute Householder vector
233
431
  const sign = x[0].isNegative() ? new Decimal(-1) : new Decimal(1);
234
432
  const v = x.slice();
235
433
  v[0] = v[0].plus(sign.mul(normx));
434
+
435
+ // Normalize v
236
436
  let vnorm = new Decimal(0);
237
437
  for (const vi of v) vnorm = vnorm.plus(vi.mul(vi));
238
438
  vnorm = vnorm.sqrt();
239
439
  if (vnorm.isZero()) continue;
240
440
  for (let i = 0; i < v.length; i++) v[i] = v[i].div(vnorm);
441
+
442
+ // Apply Householder reflection to A
241
443
  for (let j = k; j < n; j++) {
242
444
  let dot = new Decimal(0);
243
445
  for (let i = 0; i < v.length; i++) dot = dot.plus(v[i].mul(A[k + i][j]));
244
446
  for (let i = 0; i < v.length; i++) A[k + i][j] = A[k + i][j].minus(new Decimal(2).mul(v[i]).mul(dot));
245
447
  }
448
+
449
+ // Apply Householder reflection to Q
246
450
  for (let j = 0; j < m; j++) {
247
451
  let dot = new Decimal(0);
248
452
  for (let i = 0; i < v.length; i++) dot = dot.plus(v[i].mul(Q[k + i][j]));
249
453
  for (let i = 0; i < v.length; i++) Q[k + i][j] = Q[k + i][j].minus(new Decimal(2).mul(v[i]).mul(dot));
250
454
  }
251
455
  }
252
- const R = Array.from({ length: m }, (_, i) => Array.from({ length: n }, (_, j) => (i <= j ? A[i][j] : new Decimal(0))));
456
+
457
+ // Extract R (upper triangular part of A)
458
+ const R = Array.from({ length: m }, (_, i) =>
459
+ Array.from({ length: n }, (_, j) => (i <= j ? A[i][j] : new Decimal(0)))
460
+ );
253
461
  return { Q: new Matrix(Q).transpose(), R: new Matrix(R) };
254
462
  }
255
463
 
256
- // simple matrix exponential via Taylor + scaling & squaring (practical default)
464
+ /**
465
+ * Compute matrix exponential using Taylor series with scaling and squaring.
466
+ * exp(A) = I + A + A²/2! + A³/3! + ...
467
+ * @param {Object} [options={}] - Options object
468
+ * @param {number} [options.maxIter=120] - Maximum Taylor series iterations
469
+ * @param {string} [options.tolerance='1e-40'] - Convergence tolerance
470
+ * @returns {Matrix} The matrix exponential exp(A)
471
+ * @throws {Error} If matrix is not square
472
+ */
257
473
  exp(options = {}) {
258
474
  const n = this.rows;
259
- if (n !== this.cols) throw new Error('exp requires square matrix');
475
+ if (!this.isSquare()) throw new Error('Matrix exponential requires square matrix');
260
476
  const ident = Matrix.identity(n);
477
+
478
+ // Compute infinity norm
261
479
  const normInf = (M) => {
262
480
  let max = new Decimal(0);
263
481
  for (let i = 0; i < M.rows; i++) {
@@ -267,6 +485,8 @@ export class Matrix {
267
485
  }
268
486
  return max;
269
487
  };
488
+
489
+ // Scaling: reduce norm below 1 for better convergence
270
490
  const maxNorm = normInf(this);
271
491
  let s = 0;
272
492
  if (maxNorm.greaterThan(new Decimal(1))) {
@@ -275,6 +495,8 @@ export class Matrix {
275
495
  }
276
496
  let A = this;
277
497
  if (s > 0) A = this.mul(new Decimal(1).div(new Decimal(2).pow(s)));
498
+
499
+ // Taylor series
278
500
  const maxIter = options.maxIter || 120;
279
501
  const tol = new Decimal(options.tolerance || '1e-40');
280
502
  let term = ident.clone();
@@ -282,12 +504,18 @@ export class Matrix {
282
504
  for (let k = 1; k < maxIter; k++) {
283
505
  term = term.mul(A).mul(new Decimal(1).div(k));
284
506
  result = result.add(term);
285
- // smallness check
507
+ // Check convergence
286
508
  let tnorm = new Decimal(0);
287
- for (let i = 0; i < term.rows; i++) for (let j = 0; j < term.cols; j++) tnorm = tnorm.plus(term.data[i][j].abs());
509
+ for (let i = 0; i < term.rows; i++) {
510
+ for (let j = 0; j < term.cols; j++) {
511
+ tnorm = tnorm.plus(term.data[i][j].abs());
512
+ }
513
+ }
288
514
  if (tnorm.lessThan(tol)) break;
289
515
  }
516
+
517
+ // Squaring: undo the scaling
290
518
  for (let i = 0; i < s; i++) result = result.mul(result);
291
519
  return result;
292
520
  }
293
- }
521
+ }
@@ -1,7 +1,28 @@
1
1
  import Decimal from 'decimal.js';
2
2
  import { Matrix } from './matrix.js';
3
+
4
+ /**
5
+ * Helper to convert any numeric input to Decimal.
6
+ * @param {number|string|Decimal} x - The value to convert
7
+ * @returns {Decimal} The Decimal representation
8
+ */
3
9
  const D = x => (x instanceof Decimal ? x : new Decimal(x));
4
10
 
11
+ /**
12
+ * 2D Affine Transforms using 3x3 homogeneous matrices.
13
+ *
14
+ * All transforms return 3x3 Matrix objects that can be composed via multiplication.
15
+ * Transform composition is right-to-left: T.mul(R).mul(S) applies S first, then R, then T.
16
+ *
17
+ * @module Transforms2D
18
+ */
19
+
20
+ /**
21
+ * Create a 2D translation matrix.
22
+ * @param {number|string|Decimal} tx - Translation in X direction
23
+ * @param {number|string|Decimal} ty - Translation in Y direction
24
+ * @returns {Matrix} 3x3 translation matrix
25
+ */
5
26
  export function translation(tx, ty) {
6
27
  return Matrix.from([
7
28
  [new Decimal(1), new Decimal(0), D(tx)],
@@ -10,6 +31,12 @@ export function translation(tx, ty) {
10
31
  ]);
11
32
  }
12
33
 
34
+ /**
35
+ * Create a 2D scaling matrix.
36
+ * @param {number|string|Decimal} sx - Scale factor in X direction
37
+ * @param {number|string|Decimal} [sy=sx] - Scale factor in Y direction (defaults to sx for uniform scaling)
38
+ * @returns {Matrix} 3x3 scaling matrix
39
+ */
13
40
  export function scale(sx, sy = null) {
14
41
  if (sy === null) sy = sx;
15
42
  return Matrix.from([
@@ -19,6 +46,17 @@ export function scale(sx, sy = null) {
19
46
  ]);
20
47
  }
21
48
 
49
+ /**
50
+ * Create a 2D rotation matrix (counterclockwise around origin).
51
+ *
52
+ * Uses standard rotation matrix:
53
+ * | cos(θ) -sin(θ) 0 |
54
+ * | sin(θ) cos(θ) 0 |
55
+ * | 0 0 1 |
56
+ *
57
+ * @param {number|string|Decimal} theta - Rotation angle in radians
58
+ * @returns {Matrix} 3x3 rotation matrix
59
+ */
22
60
  export function rotate(theta) {
23
61
  const t = D(theta);
24
62
  const c = new Decimal(Math.cos(t.toNumber()));
@@ -30,10 +68,32 @@ export function rotate(theta) {
30
68
  ]);
31
69
  }
32
70
 
71
+ /**
72
+ * Create a 2D rotation matrix around a specific point.
73
+ * Equivalent to: translate(px, py) × rotate(theta) × translate(-px, -py)
74
+ *
75
+ * @param {number|string|Decimal} theta - Rotation angle in radians
76
+ * @param {number|string|Decimal} px - X coordinate of rotation center
77
+ * @param {number|string|Decimal} py - Y coordinate of rotation center
78
+ * @returns {Matrix} 3x3 rotation matrix around point (px, py)
79
+ */
33
80
  export function rotateAroundPoint(theta, px, py) {
34
- return translation(px, py).mul(rotate(theta)).mul(translation(new Decimal(px).neg(), new Decimal(py).neg()));
81
+ const pxD = D(px);
82
+ const pyD = D(py);
83
+ return translation(pxD, pyD).mul(rotate(theta)).mul(translation(pxD.negated(), pyD.negated()));
35
84
  }
36
85
 
86
+ /**
87
+ * Create a 2D skew (shear) matrix.
88
+ *
89
+ * | 1 ax 0 |
90
+ * | ay 1 0 |
91
+ * | 0 0 1 |
92
+ *
93
+ * @param {number|string|Decimal} ax - Skew factor in X direction (affects X based on Y)
94
+ * @param {number|string|Decimal} ay - Skew factor in Y direction (affects Y based on X)
95
+ * @returns {Matrix} 3x3 skew matrix
96
+ */
37
97
  export function skew(ax, ay) {
38
98
  return Matrix.from([
39
99
  [new Decimal(1), D(ax), new Decimal(0)],
@@ -42,6 +102,17 @@ export function skew(ax, ay) {
42
102
  ]);
43
103
  }
44
104
 
105
+ /**
106
+ * Create a stretch matrix along a specified axis direction.
107
+ * Stretches by factor k along the unit vector (ux, uy).
108
+ *
109
+ * The axis should be normalized (ux² + uy² = 1), but this is not enforced.
110
+ *
111
+ * @param {number|string|Decimal} ux - X component of axis direction (unit vector)
112
+ * @param {number|string|Decimal} uy - Y component of axis direction (unit vector)
113
+ * @param {number|string|Decimal} k - Stretch factor along the axis
114
+ * @returns {Matrix} 3x3 stretch matrix
115
+ */
45
116
  export function stretchAlongAxis(ux, uy, k) {
46
117
  const uxD = D(ux), uyD = D(uy), kD = D(k);
47
118
  const one = new Decimal(1);
@@ -57,9 +128,56 @@ export function stretchAlongAxis(ux, uy, k) {
57
128
  ]);
58
129
  }
59
130
 
131
+ /**
132
+ * Apply a 2D transform matrix to a point.
133
+ * Uses homogeneous coordinates with perspective division.
134
+ *
135
+ * @param {Matrix} M - 3x3 transformation matrix
136
+ * @param {number|string|Decimal} x - X coordinate of point
137
+ * @param {number|string|Decimal} y - Y coordinate of point
138
+ * @returns {Decimal[]} Transformed point as [x', y'] array of Decimals
139
+ */
60
140
  export function applyTransform(M, x, y) {
61
141
  const P = Matrix.from([[D(x)], [D(y)], [new Decimal(1)]]);
62
142
  const R = M.mul(P);
63
143
  const rx = R.data[0][0], ry = R.data[1][0], rw = R.data[2][0];
144
+ // Perspective division (for affine transforms, rw is always 1)
64
145
  return [rx.div(rw), ry.div(rw)];
65
- }
146
+ }
147
+
148
+ /**
149
+ * Create a reflection matrix across the X axis (flips Y).
150
+ * @returns {Matrix} 3x3 reflection matrix
151
+ */
152
+ export function reflectX() {
153
+ return Matrix.from([
154
+ [new Decimal(1), new Decimal(0), new Decimal(0)],
155
+ [new Decimal(0), new Decimal(-1), new Decimal(0)],
156
+ [new Decimal(0), new Decimal(0), new Decimal(1)]
157
+ ]);
158
+ }
159
+
160
+ /**
161
+ * Create a reflection matrix across the Y axis (flips X).
162
+ * @returns {Matrix} 3x3 reflection matrix
163
+ */
164
+ export function reflectY() {
165
+ return Matrix.from([
166
+ [new Decimal(-1), new Decimal(0), new Decimal(0)],
167
+ [new Decimal(0), new Decimal(1), new Decimal(0)],
168
+ [new Decimal(0), new Decimal(0), new Decimal(1)]
169
+ ]);
170
+ }
171
+
172
+ /**
173
+ * Create a reflection matrix across the origin (flips both X and Y).
174
+ * Equivalent to rotation by π radians.
175
+ * @returns {Matrix} 3x3 reflection matrix
176
+ */
177
+ export function reflectOrigin() {
178
+ return Matrix.from([
179
+ [new Decimal(-1), new Decimal(0), new Decimal(0)],
180
+ [new Decimal(0), new Decimal(-1), new Decimal(0)],
181
+ [new Decimal(0), new Decimal(0), new Decimal(1)]
182
+ ]);
183
+ }