@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/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/.github/workflows/publish.yml +0 -34
- package/.github/workflows/release.yml +0 -37
- package/.github/workflows/test.yml +0 -21
- package/CLAUDE.md +0 -60
- package/SVG-MATRIX/README.md +0 -22
- package/SVG-MATRIX/package.json +0 -33
- package/emasoft-svg-matrix-1.0.1.tgz +0 -0
- package/npm-shrinkwrap.json +0 -22
- package/scripts/bootstrap_repo.sh +0 -99
- package/test/example.js +0 -18
- package/test/examples.js +0 -50
package/src/transforms3d.js
CHANGED
|
@@ -1,7 +1,32 @@
|
|
|
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
|
+
* 3D Affine Transforms using 4x4 homogeneous matrices.
|
|
13
|
+
*
|
|
14
|
+
* All transforms return 4x4 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
|
+
* Rotation matrices use the right-hand rule: positive angles rotate counterclockwise
|
|
18
|
+
* when looking down the axis toward the origin.
|
|
19
|
+
*
|
|
20
|
+
* @module Transforms3D
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Create a 3D translation matrix.
|
|
25
|
+
* @param {number|string|Decimal} tx - Translation in X direction
|
|
26
|
+
* @param {number|string|Decimal} ty - Translation in Y direction
|
|
27
|
+
* @param {number|string|Decimal} tz - Translation in Z direction
|
|
28
|
+
* @returns {Matrix} 4x4 translation matrix
|
|
29
|
+
*/
|
|
5
30
|
export function translation(tx, ty, tz) {
|
|
6
31
|
return Matrix.from([
|
|
7
32
|
[new Decimal(1), new Decimal(0), new Decimal(0), D(tx)],
|
|
@@ -11,6 +36,13 @@ export function translation(tx, ty, tz) {
|
|
|
11
36
|
]);
|
|
12
37
|
}
|
|
13
38
|
|
|
39
|
+
/**
|
|
40
|
+
* Create a 3D scaling matrix.
|
|
41
|
+
* @param {number|string|Decimal} sx - Scale factor in X direction
|
|
42
|
+
* @param {number|string|Decimal} [sy=sx] - Scale factor in Y direction (defaults to sx)
|
|
43
|
+
* @param {number|string|Decimal} [sz=sx] - Scale factor in Z direction (defaults to sx)
|
|
44
|
+
* @returns {Matrix} 4x4 scaling matrix
|
|
45
|
+
*/
|
|
14
46
|
export function scale(sx, sy = null, sz = null) {
|
|
15
47
|
if (sy === null) sy = sx;
|
|
16
48
|
if (sz === null) sz = sx;
|
|
@@ -22,16 +54,103 @@ export function scale(sx, sy = null, sz = null) {
|
|
|
22
54
|
]);
|
|
23
55
|
}
|
|
24
56
|
|
|
25
|
-
|
|
57
|
+
/**
|
|
58
|
+
* Create a rotation matrix around the X axis.
|
|
59
|
+
*
|
|
60
|
+
* | 1 0 0 0 |
|
|
61
|
+
* | 0 cos(θ) -sin(θ) 0 |
|
|
62
|
+
* | 0 sin(θ) cos(θ) 0 |
|
|
63
|
+
* | 0 0 0 1 |
|
|
64
|
+
*
|
|
65
|
+
* @param {number|string|Decimal} theta - Rotation angle in radians
|
|
66
|
+
* @returns {Matrix} 4x4 rotation matrix
|
|
67
|
+
*/
|
|
68
|
+
export function rotateX(theta) {
|
|
69
|
+
const t = D(theta);
|
|
70
|
+
const c = new Decimal(Math.cos(t.toNumber()));
|
|
71
|
+
const s = new Decimal(Math.sin(t.toNumber()));
|
|
72
|
+
return Matrix.from([
|
|
73
|
+
[new Decimal(1), new Decimal(0), new Decimal(0), new Decimal(0)],
|
|
74
|
+
[new Decimal(0), c, s.negated(), new Decimal(0)],
|
|
75
|
+
[new Decimal(0), s, c, new Decimal(0)],
|
|
76
|
+
[new Decimal(0), new Decimal(0), new Decimal(0), new Decimal(1)]
|
|
77
|
+
]);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Create a rotation matrix around the Y axis.
|
|
82
|
+
*
|
|
83
|
+
* | cos(θ) 0 sin(θ) 0 |
|
|
84
|
+
* | 0 1 0 0 |
|
|
85
|
+
* | -sin(θ) 0 cos(θ) 0 |
|
|
86
|
+
* | 0 0 0 1 |
|
|
87
|
+
*
|
|
88
|
+
* @param {number|string|Decimal} theta - Rotation angle in radians
|
|
89
|
+
* @returns {Matrix} 4x4 rotation matrix
|
|
90
|
+
*/
|
|
91
|
+
export function rotateY(theta) {
|
|
92
|
+
const t = D(theta);
|
|
93
|
+
const c = new Decimal(Math.cos(t.toNumber()));
|
|
94
|
+
const s = new Decimal(Math.sin(t.toNumber()));
|
|
95
|
+
return Matrix.from([
|
|
96
|
+
[c, new Decimal(0), s, new Decimal(0)],
|
|
97
|
+
[new Decimal(0), new Decimal(1), new Decimal(0), new Decimal(0)],
|
|
98
|
+
[s.negated(), new Decimal(0), c, new Decimal(0)],
|
|
99
|
+
[new Decimal(0), new Decimal(0), new Decimal(0), new Decimal(1)]
|
|
100
|
+
]);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Create a rotation matrix around the Z axis.
|
|
105
|
+
*
|
|
106
|
+
* | cos(θ) -sin(θ) 0 0 |
|
|
107
|
+
* | sin(θ) cos(θ) 0 0 |
|
|
108
|
+
* | 0 0 1 0 |
|
|
109
|
+
* | 0 0 0 1 |
|
|
110
|
+
*
|
|
111
|
+
* @param {number|string|Decimal} theta - Rotation angle in radians
|
|
112
|
+
* @returns {Matrix} 4x4 rotation matrix
|
|
113
|
+
*/
|
|
114
|
+
export function rotateZ(theta) {
|
|
115
|
+
const t = D(theta);
|
|
116
|
+
const c = new Decimal(Math.cos(t.toNumber()));
|
|
117
|
+
const s = new Decimal(Math.sin(t.toNumber()));
|
|
118
|
+
return Matrix.from([
|
|
119
|
+
[c, s.negated(), new Decimal(0), new Decimal(0)],
|
|
120
|
+
[s, c, new Decimal(0), new Decimal(0)],
|
|
121
|
+
[new Decimal(0), new Decimal(0), new Decimal(1), new Decimal(0)],
|
|
122
|
+
[new Decimal(0), new Decimal(0), new Decimal(0), new Decimal(1)]
|
|
123
|
+
]);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Create a rotation matrix around an arbitrary axis through the origin.
|
|
128
|
+
* Uses Rodrigues' rotation formula.
|
|
129
|
+
*
|
|
130
|
+
* The axis vector (ux, uy, uz) is automatically normalized.
|
|
131
|
+
*
|
|
132
|
+
* @param {number|string|Decimal} ux - X component of rotation axis
|
|
133
|
+
* @param {number|string|Decimal} uy - Y component of rotation axis
|
|
134
|
+
* @param {number|string|Decimal} uz - Z component of rotation axis
|
|
135
|
+
* @param {number|string|Decimal} theta - Rotation angle in radians
|
|
136
|
+
* @returns {Matrix} 4x4 rotation matrix
|
|
137
|
+
* @throws {Error} If axis is zero vector
|
|
138
|
+
*/
|
|
26
139
|
export function rotateAroundAxis(ux, uy, uz, theta) {
|
|
27
140
|
const u = [D(ux), D(uy), D(uz)];
|
|
28
141
|
let norm = u[0].mul(u[0]).plus(u[1].mul(u[1])).plus(u[2].mul(u[2])).sqrt();
|
|
29
|
-
if (norm.isZero()) throw new Error('Rotation axis cannot be zero');
|
|
30
|
-
|
|
142
|
+
if (norm.isZero()) throw new Error('Rotation axis cannot be zero vector');
|
|
143
|
+
// Normalize axis
|
|
144
|
+
u[0] = u[0].div(norm);
|
|
145
|
+
u[1] = u[1].div(norm);
|
|
146
|
+
u[2] = u[2].div(norm);
|
|
147
|
+
|
|
31
148
|
const t = D(theta);
|
|
32
149
|
const c = new Decimal(Math.cos(t.toNumber()));
|
|
33
150
|
const s = new Decimal(Math.sin(t.toNumber()));
|
|
34
151
|
const one = new Decimal(1);
|
|
152
|
+
|
|
153
|
+
// Rodrigues' rotation formula components
|
|
35
154
|
const ux2 = u[0].mul(u[0]), uy2 = u[1].mul(u[1]), uz2 = u[2].mul(u[2]);
|
|
36
155
|
const m00 = ux2.plus(c.mul(one.minus(ux2)));
|
|
37
156
|
const m01 = u[0].mul(u[1]).mul(one.minus(c)).minus(u[2].mul(s));
|
|
@@ -42,10 +161,101 @@ export function rotateAroundAxis(ux, uy, uz, theta) {
|
|
|
42
161
|
const m20 = u[2].mul(u[0]).mul(one.minus(c)).minus(u[1].mul(s));
|
|
43
162
|
const m21 = u[2].mul(u[1]).mul(one.minus(c)).plus(u[0].mul(s));
|
|
44
163
|
const m22 = uz2.plus(c.mul(one.minus(uz2)));
|
|
164
|
+
|
|
45
165
|
return Matrix.from([
|
|
46
166
|
[m00, m01, m02, new Decimal(0)],
|
|
47
167
|
[m10, m11, m12, new Decimal(0)],
|
|
48
168
|
[m20, m21, m22, new Decimal(0)],
|
|
49
169
|
[new Decimal(0), new Decimal(0), new Decimal(0), new Decimal(1)]
|
|
50
170
|
]);
|
|
51
|
-
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Create a rotation matrix around an arbitrary axis through a specific point.
|
|
175
|
+
* Equivalent to: translate(px, py, pz) × rotateAroundAxis(...) × translate(-px, -py, -pz)
|
|
176
|
+
*
|
|
177
|
+
* @param {number|string|Decimal} ux - X component of rotation axis
|
|
178
|
+
* @param {number|string|Decimal} uy - Y component of rotation axis
|
|
179
|
+
* @param {number|string|Decimal} uz - Z component of rotation axis
|
|
180
|
+
* @param {number|string|Decimal} theta - Rotation angle in radians
|
|
181
|
+
* @param {number|string|Decimal} px - X coordinate of rotation center
|
|
182
|
+
* @param {number|string|Decimal} py - Y coordinate of rotation center
|
|
183
|
+
* @param {number|string|Decimal} pz - Z coordinate of rotation center
|
|
184
|
+
* @returns {Matrix} 4x4 rotation matrix around point (px, py, pz)
|
|
185
|
+
*/
|
|
186
|
+
export function rotateAroundPoint(ux, uy, uz, theta, px, py, pz) {
|
|
187
|
+
const pxD = D(px), pyD = D(py), pzD = D(pz);
|
|
188
|
+
return translation(pxD, pyD, pzD)
|
|
189
|
+
.mul(rotateAroundAxis(ux, uy, uz, theta))
|
|
190
|
+
.mul(translation(pxD.negated(), pyD.negated(), pzD.negated()));
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Apply a 3D transform matrix to a point.
|
|
195
|
+
* Uses homogeneous coordinates with perspective division.
|
|
196
|
+
*
|
|
197
|
+
* @param {Matrix} M - 4x4 transformation matrix
|
|
198
|
+
* @param {number|string|Decimal} x - X coordinate of point
|
|
199
|
+
* @param {number|string|Decimal} y - Y coordinate of point
|
|
200
|
+
* @param {number|string|Decimal} z - Z coordinate of point
|
|
201
|
+
* @returns {Decimal[]} Transformed point as [x', y', z'] array of Decimals
|
|
202
|
+
*/
|
|
203
|
+
export function applyTransform(M, x, y, z) {
|
|
204
|
+
const P = Matrix.from([[D(x)], [D(y)], [D(z)], [new Decimal(1)]]);
|
|
205
|
+
const R = M.mul(P);
|
|
206
|
+
const rx = R.data[0][0], ry = R.data[1][0], rz = R.data[2][0], rw = R.data[3][0];
|
|
207
|
+
// Perspective division (for affine transforms, rw is always 1)
|
|
208
|
+
return [rx.div(rw), ry.div(rw), rz.div(rw)];
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Create a reflection matrix across the XY plane (flips Z).
|
|
213
|
+
* @returns {Matrix} 4x4 reflection matrix
|
|
214
|
+
*/
|
|
215
|
+
export function reflectXY() {
|
|
216
|
+
return Matrix.from([
|
|
217
|
+
[new Decimal(1), new Decimal(0), new Decimal(0), new Decimal(0)],
|
|
218
|
+
[new Decimal(0), new Decimal(1), new Decimal(0), new Decimal(0)],
|
|
219
|
+
[new Decimal(0), new Decimal(0), new Decimal(-1), new Decimal(0)],
|
|
220
|
+
[new Decimal(0), new Decimal(0), new Decimal(0), new Decimal(1)]
|
|
221
|
+
]);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Create a reflection matrix across the XZ plane (flips Y).
|
|
226
|
+
* @returns {Matrix} 4x4 reflection matrix
|
|
227
|
+
*/
|
|
228
|
+
export function reflectXZ() {
|
|
229
|
+
return Matrix.from([
|
|
230
|
+
[new Decimal(1), new Decimal(0), new Decimal(0), new Decimal(0)],
|
|
231
|
+
[new Decimal(0), new Decimal(-1), new Decimal(0), new Decimal(0)],
|
|
232
|
+
[new Decimal(0), new Decimal(0), new Decimal(1), new Decimal(0)],
|
|
233
|
+
[new Decimal(0), new Decimal(0), new Decimal(0), new Decimal(1)]
|
|
234
|
+
]);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Create a reflection matrix across the YZ plane (flips X).
|
|
239
|
+
* @returns {Matrix} 4x4 reflection matrix
|
|
240
|
+
*/
|
|
241
|
+
export function reflectYZ() {
|
|
242
|
+
return Matrix.from([
|
|
243
|
+
[new Decimal(-1), new Decimal(0), new Decimal(0), new Decimal(0)],
|
|
244
|
+
[new Decimal(0), new Decimal(1), new Decimal(0), new Decimal(0)],
|
|
245
|
+
[new Decimal(0), new Decimal(0), new Decimal(1), new Decimal(0)],
|
|
246
|
+
[new Decimal(0), new Decimal(0), new Decimal(0), new Decimal(1)]
|
|
247
|
+
]);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Create a reflection matrix across the origin (flips X, Y, and Z).
|
|
252
|
+
* @returns {Matrix} 4x4 reflection matrix
|
|
253
|
+
*/
|
|
254
|
+
export function reflectOrigin() {
|
|
255
|
+
return Matrix.from([
|
|
256
|
+
[new Decimal(-1), new Decimal(0), new Decimal(0), new Decimal(0)],
|
|
257
|
+
[new Decimal(0), new Decimal(-1), new Decimal(0), new Decimal(0)],
|
|
258
|
+
[new Decimal(0), new Decimal(0), new Decimal(-1), new Decimal(0)],
|
|
259
|
+
[new Decimal(0), new Decimal(0), new Decimal(0), new Decimal(1)]
|
|
260
|
+
]);
|
|
261
|
+
}
|
package/src/vector.js
CHANGED
|
@@ -1,60 +1,147 @@
|
|
|
1
1
|
import Decimal from 'decimal.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Helper to convert any numeric input to Decimal.
|
|
5
|
+
* Accepts numbers, strings, or Decimal instances.
|
|
6
|
+
* @param {number|string|Decimal} x - The value to convert
|
|
7
|
+
* @returns {Decimal} The Decimal representation
|
|
8
|
+
*/
|
|
2
9
|
const D = x => (x instanceof Decimal ? x : new Decimal(x));
|
|
3
10
|
|
|
11
|
+
/**
|
|
12
|
+
* Vector - Decimal-backed vector class for arbitrary-precision vector operations.
|
|
13
|
+
*
|
|
14
|
+
* All numeric inputs are automatically converted to Decimal for high precision.
|
|
15
|
+
* Supports basic operations (add, sub, scale), products (dot, cross, outer),
|
|
16
|
+
* and geometric operations (norm, normalize, angle, projection).
|
|
17
|
+
*
|
|
18
|
+
* @example
|
|
19
|
+
* const v = Vector.from([1, 2, 3]);
|
|
20
|
+
* const w = Vector.from(['1.5', '2.5', '3.5']);
|
|
21
|
+
* console.log(v.dot(w).toString()); // dot product as Decimal
|
|
22
|
+
*/
|
|
4
23
|
export class Vector {
|
|
24
|
+
/**
|
|
25
|
+
* Create a new Vector from an array of components.
|
|
26
|
+
* @param {Array<number|string|Decimal>} components - Array of vector components
|
|
27
|
+
* @throws {Error} If components is not an array
|
|
28
|
+
*/
|
|
5
29
|
constructor(components) {
|
|
6
30
|
if (!Array.isArray(components)) throw new Error('Vector requires array');
|
|
7
31
|
this.data = components.map(c => D(c));
|
|
8
32
|
this.length = this.data.length;
|
|
9
33
|
}
|
|
10
34
|
|
|
35
|
+
/**
|
|
36
|
+
* Factory method to create a Vector from an array.
|
|
37
|
+
* @param {Array<number|string|Decimal>} arr - Array of vector components
|
|
38
|
+
* @returns {Vector} New Vector instance
|
|
39
|
+
*/
|
|
11
40
|
static from(arr) {
|
|
12
41
|
return new Vector(arr);
|
|
13
42
|
}
|
|
14
43
|
|
|
44
|
+
/**
|
|
45
|
+
* Create a deep copy of this vector.
|
|
46
|
+
* @returns {Vector} New Vector with copied values
|
|
47
|
+
*/
|
|
15
48
|
clone() {
|
|
16
49
|
return new Vector(this.data.map(v => new Decimal(v)));
|
|
17
50
|
}
|
|
18
51
|
|
|
52
|
+
/**
|
|
53
|
+
* Return the internal Decimal array (shallow copy).
|
|
54
|
+
* Use toNumberArray() or toStringArray() for converted values.
|
|
55
|
+
* @returns {Decimal[]} Array of Decimal values
|
|
56
|
+
*/
|
|
19
57
|
toArray() {
|
|
20
|
-
return this.data.
|
|
58
|
+
return this.data.slice();
|
|
21
59
|
}
|
|
22
60
|
|
|
61
|
+
/**
|
|
62
|
+
* Convert vector to array of JavaScript numbers.
|
|
63
|
+
* Note: May lose precision for very large or precise values.
|
|
64
|
+
* @returns {number[]} Array of number values
|
|
65
|
+
*/
|
|
23
66
|
toNumberArray() {
|
|
24
67
|
return this.data.map(v => v.toNumber());
|
|
25
68
|
}
|
|
26
69
|
|
|
70
|
+
/**
|
|
71
|
+
* Convert vector to array of string representations.
|
|
72
|
+
* Preserves full precision of Decimal values.
|
|
73
|
+
* @returns {string[]} Array of string values
|
|
74
|
+
*/
|
|
27
75
|
toStringArray() {
|
|
28
76
|
return this.data.map(v => v.toString());
|
|
29
77
|
}
|
|
30
78
|
|
|
31
|
-
|
|
79
|
+
/**
|
|
80
|
+
* Element-wise addition with another vector.
|
|
81
|
+
* @param {Vector} other - Vector to add
|
|
82
|
+
* @returns {Vector} New Vector with sum
|
|
83
|
+
* @throws {Error} If other is not a Vector or dimensions mismatch
|
|
84
|
+
*/
|
|
32
85
|
add(other) {
|
|
33
86
|
if (!(other instanceof Vector)) throw new Error('add expects Vector');
|
|
34
|
-
if (other.length !== this.length) throw new Error('shape mismatch');
|
|
87
|
+
if (other.length !== this.length) throw new Error('shape mismatch: vectors must have same length');
|
|
35
88
|
return new Vector(this.data.map((v, i) => v.plus(other.data[i])));
|
|
36
89
|
}
|
|
37
90
|
|
|
91
|
+
/**
|
|
92
|
+
* Element-wise subtraction with another vector.
|
|
93
|
+
* @param {Vector} other - Vector to subtract
|
|
94
|
+
* @returns {Vector} New Vector with difference
|
|
95
|
+
* @throws {Error} If other is not a Vector or dimensions mismatch
|
|
96
|
+
*/
|
|
38
97
|
sub(other) {
|
|
39
98
|
if (!(other instanceof Vector)) throw new Error('sub expects Vector');
|
|
40
|
-
if (other.length !== this.length) throw new Error('shape mismatch');
|
|
99
|
+
if (other.length !== this.length) throw new Error('shape mismatch: vectors must have same length');
|
|
41
100
|
return new Vector(this.data.map((v, i) => v.minus(other.data[i])));
|
|
42
101
|
}
|
|
43
102
|
|
|
44
|
-
|
|
103
|
+
/**
|
|
104
|
+
* Scalar multiplication.
|
|
105
|
+
* @param {number|string|Decimal} scalar - Scalar to multiply by
|
|
106
|
+
* @returns {Vector} New scaled Vector
|
|
107
|
+
*/
|
|
45
108
|
scale(scalar) {
|
|
46
109
|
const s = D(scalar);
|
|
47
110
|
return new Vector(this.data.map(v => v.mul(s)));
|
|
48
111
|
}
|
|
49
112
|
|
|
50
|
-
|
|
113
|
+
/**
|
|
114
|
+
* Negate the vector (multiply by -1).
|
|
115
|
+
* @returns {Vector} New Vector with negated components
|
|
116
|
+
*/
|
|
117
|
+
negate() {
|
|
118
|
+
return new Vector(this.data.map(v => v.negated()));
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Dot product (inner product) with another vector.
|
|
123
|
+
* @param {Vector} other - Vector to compute dot product with
|
|
124
|
+
* @returns {Decimal} The dot product as a Decimal
|
|
125
|
+
* @throws {Error} If other is not a Vector or dimensions mismatch
|
|
126
|
+
*/
|
|
51
127
|
dot(other) {
|
|
52
128
|
if (!(other instanceof Vector)) throw new Error('dot expects Vector');
|
|
53
|
-
if (other.length !== this.length) throw new Error('shape mismatch');
|
|
129
|
+
if (other.length !== this.length) throw new Error('shape mismatch: vectors must have same length');
|
|
54
130
|
return this.data.reduce((acc, v, i) => acc.plus(v.mul(other.data[i])), new Decimal(0));
|
|
55
131
|
}
|
|
56
132
|
|
|
57
|
-
|
|
133
|
+
/**
|
|
134
|
+
* Outer product (tensor product) with another vector.
|
|
135
|
+
* Returns a 2D array of Decimals representing an m×n matrix,
|
|
136
|
+
* where m is this vector's length and n is other's length.
|
|
137
|
+
*
|
|
138
|
+
* Note: Returns raw 2D array, not Matrix, to avoid circular dependency.
|
|
139
|
+
* Use Matrix.from(v.outer(w)) to get a Matrix object.
|
|
140
|
+
*
|
|
141
|
+
* @param {Vector} other - Vector to compute outer product with
|
|
142
|
+
* @returns {Decimal[][]} 2D array of Decimals (can be passed to Matrix.from())
|
|
143
|
+
* @throws {Error} If other is not a Vector
|
|
144
|
+
*/
|
|
58
145
|
outer(other) {
|
|
59
146
|
if (!(other instanceof Vector)) throw new Error('outer expects Vector');
|
|
60
147
|
const rows = this.length, cols = other.length;
|
|
@@ -64,9 +151,14 @@ export class Vector {
|
|
|
64
151
|
return out;
|
|
65
152
|
}
|
|
66
153
|
|
|
67
|
-
|
|
154
|
+
/**
|
|
155
|
+
* Cross product for 3D vectors only.
|
|
156
|
+
* @param {Vector} other - 3D Vector to compute cross product with
|
|
157
|
+
* @returns {Vector} New 3D Vector perpendicular to both inputs
|
|
158
|
+
* @throws {Error} If either vector is not 3D
|
|
159
|
+
*/
|
|
68
160
|
cross(other) {
|
|
69
|
-
if (this.length !== 3 || other.length !== 3) throw new Error('cross requires 3D vectors');
|
|
161
|
+
if (this.length !== 3 || other.length !== 3) throw new Error('cross product requires 3D vectors');
|
|
70
162
|
const [a1, a2, a3] = this.data;
|
|
71
163
|
const [b1, b2, b3] = other.data;
|
|
72
164
|
return new Vector([
|
|
@@ -76,35 +168,51 @@ export class Vector {
|
|
|
76
168
|
]);
|
|
77
169
|
}
|
|
78
170
|
|
|
79
|
-
|
|
171
|
+
/**
|
|
172
|
+
* Euclidean norm (L2 norm, magnitude) of the vector.
|
|
173
|
+
* @returns {Decimal} The norm as a Decimal
|
|
174
|
+
*/
|
|
80
175
|
norm() {
|
|
81
176
|
let sum = new Decimal(0);
|
|
82
177
|
for (const v of this.data) sum = sum.plus(v.mul(v));
|
|
83
178
|
return sum.sqrt();
|
|
84
179
|
}
|
|
85
180
|
|
|
86
|
-
|
|
181
|
+
/**
|
|
182
|
+
* Return a normalized (unit length) version of this vector.
|
|
183
|
+
* @returns {Vector} New unit Vector in same direction
|
|
184
|
+
* @throws {Error} If vector is zero (cannot normalize)
|
|
185
|
+
*/
|
|
87
186
|
normalize() {
|
|
88
187
|
const n = this.norm();
|
|
89
188
|
if (n.isZero()) throw new Error('Cannot normalize zero vector');
|
|
90
189
|
return this.scale(new Decimal(1).div(n));
|
|
91
190
|
}
|
|
92
191
|
|
|
93
|
-
|
|
192
|
+
/**
|
|
193
|
+
* Angle between this vector and another (in radians).
|
|
194
|
+
* @param {Vector} other - Vector to compute angle with
|
|
195
|
+
* @returns {Decimal} Angle in radians as a Decimal
|
|
196
|
+
* @throws {Error} If either vector is zero
|
|
197
|
+
*/
|
|
94
198
|
angleBetween(other) {
|
|
95
|
-
const
|
|
199
|
+
const dotProduct = this.dot(other);
|
|
96
200
|
const n1 = this.norm();
|
|
97
201
|
const n2 = other.norm();
|
|
98
|
-
if (n1.isZero() || n2.isZero()) throw new Error('Angle with zero vector undefined');
|
|
99
|
-
//
|
|
100
|
-
let cosv =
|
|
101
|
-
// toNumber clamping fallback:
|
|
202
|
+
if (n1.isZero() || n2.isZero()) throw new Error('Angle with zero vector is undefined');
|
|
203
|
+
// Clamp cosine to [-1, 1] for numerical safety
|
|
204
|
+
let cosv = dotProduct.div(n1.mul(n2));
|
|
102
205
|
const cosNum = cosv.toNumber();
|
|
103
206
|
const clamped = Math.min(1, Math.max(-1, cosNum));
|
|
104
207
|
return new Decimal(Math.acos(clamped));
|
|
105
208
|
}
|
|
106
209
|
|
|
107
|
-
|
|
210
|
+
/**
|
|
211
|
+
* Project this vector onto another vector.
|
|
212
|
+
* @param {Vector} other - Vector to project onto
|
|
213
|
+
* @returns {Vector} The projection of this onto other
|
|
214
|
+
* @throws {Error} If other is zero vector
|
|
215
|
+
*/
|
|
108
216
|
projectOnto(other) {
|
|
109
217
|
const denom = other.dot(other);
|
|
110
218
|
if (denom.isZero()) throw new Error('Cannot project onto zero vector');
|
|
@@ -112,29 +220,65 @@ export class Vector {
|
|
|
112
220
|
return other.scale(coef);
|
|
113
221
|
}
|
|
114
222
|
|
|
115
|
-
|
|
223
|
+
/**
|
|
224
|
+
* Compute a vector orthogonal to this one.
|
|
225
|
+
* For 2D: returns the perpendicular [-y, x].
|
|
226
|
+
* For nD: uses Gram-Schmidt with standard basis vectors.
|
|
227
|
+
* @returns {Vector} A normalized vector orthogonal to this
|
|
228
|
+
* @throws {Error} If unable to find orthogonal vector (e.g., zero vector)
|
|
229
|
+
*/
|
|
116
230
|
orthogonal() {
|
|
117
231
|
if (this.length === 2) {
|
|
118
|
-
//
|
|
232
|
+
// 2D perpendicular: rotate 90 degrees counterclockwise
|
|
119
233
|
return new Vector([this.data[1].negated(), this.data[0]]);
|
|
120
234
|
}
|
|
121
|
-
//
|
|
235
|
+
// For n > 2: find a standard basis vector not parallel to this,
|
|
236
|
+
// then use Gram-Schmidt orthogonalization
|
|
122
237
|
for (let i = 0; i < this.length; i++) {
|
|
123
238
|
const ei = Array.from({ length: this.length }, (_, j) => new Decimal(j === i ? 1 : 0));
|
|
124
|
-
|
|
125
|
-
//
|
|
126
|
-
const crossDim = Math.min(3, this.length);
|
|
127
|
-
// project candidate out of this
|
|
239
|
+
const candidate = new Vector(ei);
|
|
240
|
+
// Project candidate out of this vector's direction
|
|
128
241
|
const proj = candidate.projectOnto(this);
|
|
129
242
|
const orth = candidate.sub(proj);
|
|
130
|
-
const
|
|
131
|
-
if (!
|
|
243
|
+
const orthNorm = orth.norm();
|
|
244
|
+
if (!orthNorm.isZero()) return orth.normalize();
|
|
132
245
|
}
|
|
133
246
|
throw new Error('Unable to find orthogonal vector');
|
|
134
247
|
}
|
|
135
248
|
|
|
136
|
-
|
|
249
|
+
/**
|
|
250
|
+
* Check if this vector is orthogonal to another.
|
|
251
|
+
* @param {Vector} other - Vector to check orthogonality with
|
|
252
|
+
* @returns {boolean} True if vectors are orthogonal (dot product is zero)
|
|
253
|
+
*/
|
|
137
254
|
isOrthogonalTo(other) {
|
|
138
255
|
return this.dot(other).isZero();
|
|
139
256
|
}
|
|
140
|
-
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Euclidean distance to another vector.
|
|
260
|
+
* @param {Vector} other - Vector to compute distance to
|
|
261
|
+
* @returns {Decimal} The Euclidean distance
|
|
262
|
+
* @throws {Error} If dimensions mismatch
|
|
263
|
+
*/
|
|
264
|
+
distance(other) {
|
|
265
|
+
return this.sub(other).norm();
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* Check equality with another vector within optional tolerance.
|
|
270
|
+
* @param {Vector} other - Vector to compare with
|
|
271
|
+
* @param {number|string|Decimal} [tolerance=0] - Maximum allowed difference per component
|
|
272
|
+
* @returns {boolean} True if vectors are equal within tolerance
|
|
273
|
+
*/
|
|
274
|
+
equals(other, tolerance = 0) {
|
|
275
|
+
if (!(other instanceof Vector)) return false;
|
|
276
|
+
if (other.length !== this.length) return false;
|
|
277
|
+
const tol = D(tolerance);
|
|
278
|
+
for (let i = 0; i < this.length; i++) {
|
|
279
|
+
const diff = this.data[i].minus(other.data[i]).abs();
|
|
280
|
+
if (diff.greaterThan(tol)) return false;
|
|
281
|
+
}
|
|
282
|
+
return true;
|
|
283
|
+
}
|
|
284
|
+
}
|
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
name: Publish to npm
|
|
2
|
-
|
|
3
|
-
on:
|
|
4
|
-
push:
|
|
5
|
-
tags:
|
|
6
|
-
- 'v*'
|
|
7
|
-
workflow_dispatch:
|
|
8
|
-
|
|
9
|
-
permissions:
|
|
10
|
-
contents: read
|
|
11
|
-
id-token: write
|
|
12
|
-
|
|
13
|
-
jobs:
|
|
14
|
-
publish:
|
|
15
|
-
name: Publish to npm
|
|
16
|
-
runs-on: ubuntu-latest
|
|
17
|
-
|
|
18
|
-
steps:
|
|
19
|
-
- name: Checkout code
|
|
20
|
-
uses: actions/checkout@v4
|
|
21
|
-
|
|
22
|
-
- name: Setup Node.js 24 (npm 11.6.2 with OIDC support)
|
|
23
|
-
uses: actions/setup-node@v4
|
|
24
|
-
with:
|
|
25
|
-
node-version: '24'
|
|
26
|
-
|
|
27
|
-
- name: Install dependencies (for prepublishOnly script)
|
|
28
|
-
run: npm install --production=false
|
|
29
|
-
|
|
30
|
-
- name: Verify npm version (should be >= 11.5.1)
|
|
31
|
-
run: npm --version
|
|
32
|
-
|
|
33
|
-
- name: Publish to npm (using OIDC trusted publishing)
|
|
34
|
-
run: npm publish --access public
|
|
@@ -1,37 +0,0 @@
|
|
|
1
|
-
name: Release and Publish to npm
|
|
2
|
-
|
|
3
|
-
on:
|
|
4
|
-
workflow_dispatch:
|
|
5
|
-
push:
|
|
6
|
-
tags:
|
|
7
|
-
- 'v*.*.*'
|
|
8
|
-
|
|
9
|
-
permissions:
|
|
10
|
-
contents: write
|
|
11
|
-
packages: write
|
|
12
|
-
|
|
13
|
-
jobs:
|
|
14
|
-
release:
|
|
15
|
-
runs-on: ubuntu-latest
|
|
16
|
-
steps:
|
|
17
|
-
- uses: actions/checkout@v4
|
|
18
|
-
|
|
19
|
-
- name: Setup Node
|
|
20
|
-
uses: actions/setup-node@v4
|
|
21
|
-
with:
|
|
22
|
-
node-version: 18
|
|
23
|
-
registry-url: 'https://registry.npmjs.org'
|
|
24
|
-
|
|
25
|
-
- name: Install
|
|
26
|
-
run: npm ci
|
|
27
|
-
|
|
28
|
-
- name: Run tests
|
|
29
|
-
run: npm test
|
|
30
|
-
|
|
31
|
-
- name: Create GitHub Release (if tag present)
|
|
32
|
-
if: startsWith(github.ref, 'refs/tags/')
|
|
33
|
-
uses: softprops/action-gh-release@v1
|
|
34
|
-
with:
|
|
35
|
-
tag_name: ${{ github.ref_name }}
|
|
36
|
-
env:
|
|
37
|
-
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
name: CI Test
|
|
2
|
-
|
|
3
|
-
on:
|
|
4
|
-
push:
|
|
5
|
-
branches: [ main ]
|
|
6
|
-
pull_request:
|
|
7
|
-
branches: [ main ]
|
|
8
|
-
|
|
9
|
-
jobs:
|
|
10
|
-
test:
|
|
11
|
-
runs-on: ubuntu-latest
|
|
12
|
-
steps:
|
|
13
|
-
- uses: actions/checkout@v4
|
|
14
|
-
- name: Setup Node
|
|
15
|
-
uses: actions/setup-node@v4
|
|
16
|
-
with:
|
|
17
|
-
node-version: 24
|
|
18
|
-
- name: Install
|
|
19
|
-
run: npm ci
|
|
20
|
-
- name: Run tests/examples
|
|
21
|
-
run: npm test
|