@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.
@@ -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
- // rotation around arbitrary axis (ux,uy,uz) through origin by angle theta (radians)
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
- u[0] = u[0].div(norm); u[1] = u[1].div(norm); u[2] = u[2].div(norm);
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.map(v => v);
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
- // elementwise add/sub
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
- // scalar multiplication
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
- // dot product
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
- // outer / tensor product returns Matrix-like 2D array (Decimal)
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
- // cross product for 3D vectors
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
- // norm (Euclidean)
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
- // normalize: returns a new Vector
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
- // angle between vectors (radians)
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 dot = this.dot(other);
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
- // clamp cosine to [-1,1] for numerical safety
100
- let cosv = dot.div(n1.mul(n2));
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
- // projection of this onto other (vector)
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
- // compute an orthogonal vector (for 2D returns perpendicular; for higher dims returns Gram-Schmidt complement)
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
- // perp: [-y, x]
232
+ // 2D perpendicular: rotate 90 degrees counterclockwise
119
233
  return new Vector([this.data[1].negated(), this.data[0]]);
120
234
  }
121
- // for n>2: find a vector not colinear with this and make orthogonal via Gram-Schmidt with standard basis
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
- let candidate = new Vector(ei);
125
- // check not parallel
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 norm = orth.norm();
131
- if (!norm.isZero()) return orth.normalize();
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
- // check orthogonality with other
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
+ }