@emasoft/svg-matrix 1.0.2 → 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/README.md +264 -12
- package/package.json +17 -3
- package/src/index.js +25 -1
- package/src/matrix.js +263 -35
- package/src/transforms2d.js +120 -2
- package/src/transforms3d.js +214 -4
- package/src/vector.js +174 -30
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
254
|
+
if (!this.isSquare()) throw new Error('LU decomposition requires square matrix');
|
|
105
255
|
const n = this.rows;
|
|
106
|
-
const A = this.
|
|
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
|
|
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.
|
|
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
|
|
151
|
-
for (let i = 0; i < perm.length; i++)
|
|
152
|
-
|
|
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.
|
|
326
|
+
if (!this.isSquare()) throw new Error('Inverse only defined for square matrices');
|
|
158
327
|
const n = this.rows;
|
|
159
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
//
|
|
194
|
-
const aug = Array.from({ length: n }, (_, i) =>
|
|
195
|
-
|
|
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
|
|
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
|
-
//
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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 (
|
|
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
|
-
//
|
|
507
|
+
// Check convergence
|
|
286
508
|
let tnorm = new Decimal(0);
|
|
287
|
-
for (let i = 0; i < term.rows; i++)
|
|
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
|
+
}
|
package/src/transforms2d.js
CHANGED
|
@@ -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
|
-
|
|
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
|
+
}
|