@emasoft/svg-matrix 1.0.28 → 1.0.30
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 +325 -0
- package/bin/svg-matrix.js +985 -378
- package/bin/svglinter.cjs +4172 -433
- package/bin/svgm.js +723 -180
- package/package.json +16 -4
- package/src/animation-references.js +71 -52
- package/src/arc-length.js +160 -96
- package/src/bezier-analysis.js +257 -117
- package/src/bezier-intersections.js +411 -148
- package/src/browser-verify.js +240 -100
- package/src/clip-path-resolver.js +350 -142
- package/src/convert-path-data.js +279 -134
- package/src/css-specificity.js +78 -70
- package/src/flatten-pipeline.js +751 -263
- package/src/geometry-to-path.js +511 -182
- package/src/index.js +191 -46
- package/src/inkscape-support.js +18 -7
- package/src/marker-resolver.js +278 -164
- package/src/mask-resolver.js +209 -98
- package/src/matrix.js +147 -67
- package/src/mesh-gradient.js +187 -96
- package/src/off-canvas-detection.js +201 -104
- package/src/path-analysis.js +187 -107
- package/src/path-data-plugins.js +628 -167
- package/src/path-simplification.js +0 -1
- package/src/pattern-resolver.js +125 -88
- package/src/polygon-clip.js +111 -66
- package/src/svg-boolean-ops.js +194 -118
- package/src/svg-collections.js +22 -18
- package/src/svg-flatten.js +282 -164
- package/src/svg-parser.js +427 -200
- package/src/svg-rendering-context.js +147 -104
- package/src/svg-toolbox.js +16381 -3370
- package/src/svg2-polyfills.js +93 -224
- package/src/transform-decomposition.js +46 -41
- package/src/transform-optimization.js +89 -68
- package/src/transforms2d.js +49 -16
- package/src/transforms3d.js +58 -22
- package/src/use-symbol-resolver.js +150 -110
- package/src/vector.js +67 -15
- package/src/vendor/README.md +110 -0
- package/src/vendor/inkscape-hatch-polyfill.js +401 -0
- package/src/vendor/inkscape-hatch-polyfill.min.js +8 -0
- package/src/vendor/inkscape-mesh-polyfill.js +843 -0
- package/src/vendor/inkscape-mesh-polyfill.min.js +8 -0
- package/src/verification.js +288 -124
package/src/bezier-analysis.js
CHANGED
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
* - Handles extreme coordinate ranges
|
|
19
19
|
*/
|
|
20
20
|
|
|
21
|
-
import Decimal from
|
|
21
|
+
import Decimal from "decimal.js";
|
|
22
22
|
|
|
23
23
|
// Ensure high precision is set
|
|
24
24
|
Decimal.set({ precision: 80 });
|
|
@@ -28,7 +28,7 @@ Decimal.set({ precision: 80 });
|
|
|
28
28
|
* @param {number|string|Decimal} x - Value to convert
|
|
29
29
|
* @returns {Decimal}
|
|
30
30
|
*/
|
|
31
|
-
const D = x => (x instanceof Decimal ? x : new Decimal(x));
|
|
31
|
+
const D = (x) => (x instanceof Decimal ? x : new Decimal(x));
|
|
32
32
|
|
|
33
33
|
/**
|
|
34
34
|
* Validate that a value is a finite number (not NaN or Infinity).
|
|
@@ -38,7 +38,7 @@ const D = x => (x instanceof Decimal ? x : new Decimal(x));
|
|
|
38
38
|
* @param {string} context - Function name for error message
|
|
39
39
|
* @throws {Error} If value is not finite
|
|
40
40
|
*/
|
|
41
|
-
function
|
|
41
|
+
function _assertFinite(val, context) {
|
|
42
42
|
if (!val.isFinite()) {
|
|
43
43
|
throw new Error(`${context}: encountered non-finite value ${val}`);
|
|
44
44
|
}
|
|
@@ -52,50 +52,50 @@ function assertFinite(val, context) {
|
|
|
52
52
|
|
|
53
53
|
/** Threshold below which derivative magnitude is considered zero (cusp detection).
|
|
54
54
|
* WHY: Prevents division by zero in tangent/normal calculations at cusps. */
|
|
55
|
-
const DERIVATIVE_ZERO_THRESHOLD = new Decimal(
|
|
55
|
+
const DERIVATIVE_ZERO_THRESHOLD = new Decimal("1e-50");
|
|
56
56
|
|
|
57
57
|
/** Threshold for curvature denominator to detect cusps.
|
|
58
58
|
* WHY: Curvature formula has (x'^2 + y'^2)^(3/2) in denominator; this threshold
|
|
59
59
|
* prevents division by near-zero values that would produce spurious infinities. */
|
|
60
|
-
const CURVATURE_SINGULARITY_THRESHOLD = new Decimal(
|
|
60
|
+
const CURVATURE_SINGULARITY_THRESHOLD = new Decimal("1e-100");
|
|
61
61
|
|
|
62
62
|
/** Threshold for finite difference step size.
|
|
63
63
|
* WHY: Used in numerical derivative approximations. Balance between truncation error
|
|
64
64
|
* (too large) and cancellation error (too small). */
|
|
65
|
-
const FINITE_DIFFERENCE_STEP = new Decimal(
|
|
65
|
+
const FINITE_DIFFERENCE_STEP = new Decimal("1e-8");
|
|
66
66
|
|
|
67
67
|
/** Newton-Raphson convergence threshold.
|
|
68
68
|
* WHY: Iteration stops when change is below this threshold, indicating convergence. */
|
|
69
|
-
const NEWTON_CONVERGENCE_THRESHOLD = new Decimal(
|
|
69
|
+
const NEWTON_CONVERGENCE_THRESHOLD = new Decimal("1e-40");
|
|
70
70
|
|
|
71
71
|
/** Near-zero threshold for general comparisons.
|
|
72
72
|
* WHY: Used throughout for detecting effectively zero values in high-precision arithmetic. */
|
|
73
|
-
const NEAR_ZERO_THRESHOLD = new Decimal(
|
|
73
|
+
const NEAR_ZERO_THRESHOLD = new Decimal("1e-60");
|
|
74
74
|
|
|
75
75
|
/** Threshold for degenerate quadratic equations.
|
|
76
76
|
* WHY: When 'a' coefficient is below this relative to other coefficients,
|
|
77
77
|
* equation degenerates to linear case, avoiding division by near-zero. */
|
|
78
|
-
const QUADRATIC_DEGENERATE_THRESHOLD = new Decimal(
|
|
78
|
+
const QUADRATIC_DEGENERATE_THRESHOLD = new Decimal("1e-70");
|
|
79
79
|
|
|
80
80
|
/** Subdivision convergence threshold for root finding.
|
|
81
81
|
* WHY: When interval becomes smaller than this, subdivision has converged to a root. */
|
|
82
|
-
const SUBDIVISION_CONVERGENCE_THRESHOLD = new Decimal(
|
|
82
|
+
const SUBDIVISION_CONVERGENCE_THRESHOLD = new Decimal("1e-15");
|
|
83
83
|
|
|
84
84
|
/** Threshold for arc length comparison in curvature verification.
|
|
85
85
|
* WHY: Arc lengths below this are too small for reliable finite difference approximation. */
|
|
86
|
-
const ARC_LENGTH_THRESHOLD = new Decimal(
|
|
86
|
+
const ARC_LENGTH_THRESHOLD = new Decimal("1e-50");
|
|
87
87
|
|
|
88
88
|
/** Relative error threshold for curvature comparison.
|
|
89
89
|
* WHY: Curvature verification uses relative error; this threshold balances precision vs noise. */
|
|
90
|
-
const CURVATURE_RELATIVE_ERROR_THRESHOLD = new Decimal(
|
|
90
|
+
const CURVATURE_RELATIVE_ERROR_THRESHOLD = new Decimal("1e-10");
|
|
91
91
|
|
|
92
92
|
/** Finite difference step for derivative verification (higher order).
|
|
93
93
|
* WHY: Smaller step than general finite difference for more accurate verification. */
|
|
94
|
-
const DERIVATIVE_VERIFICATION_STEP = new Decimal(
|
|
94
|
+
const DERIVATIVE_VERIFICATION_STEP = new Decimal("1e-10");
|
|
95
95
|
|
|
96
96
|
/** Threshold for magnitude comparison in derivative verification.
|
|
97
97
|
* WHY: Used to determine if derivative magnitude is large enough for relative error. */
|
|
98
|
-
const DERIVATIVE_MAGNITUDE_THRESHOLD = new Decimal(
|
|
98
|
+
const DERIVATIVE_MAGNITUDE_THRESHOLD = new Decimal("1e-20");
|
|
99
99
|
|
|
100
100
|
/**
|
|
101
101
|
* 2D Point represented as [Decimal, Decimal]
|
|
@@ -130,7 +130,9 @@ export function bezierPoint(points, t) {
|
|
|
130
130
|
// INPUT VALIDATION: Ensure points array is valid
|
|
131
131
|
// WHY: Empty or invalid arrays would cause crashes in the de Casteljau iteration
|
|
132
132
|
if (!points || !Array.isArray(points) || points.length < 2) {
|
|
133
|
-
throw new Error(
|
|
133
|
+
throw new Error(
|
|
134
|
+
"bezierPoint: points must be an array with at least 2 control points",
|
|
135
|
+
);
|
|
134
136
|
}
|
|
135
137
|
|
|
136
138
|
const tD = D(t);
|
|
@@ -177,7 +179,9 @@ export function bezierPointHorner(points, t) {
|
|
|
177
179
|
// INPUT VALIDATION: Ensure points array is valid
|
|
178
180
|
// WHY: Horner's rule requires at least 2 points; invalid arrays cause index errors
|
|
179
181
|
if (!points || !Array.isArray(points) || points.length < 2) {
|
|
180
|
-
throw new Error(
|
|
182
|
+
throw new Error(
|
|
183
|
+
"bezierPointHorner: points must be an array with at least 2 control points",
|
|
184
|
+
);
|
|
181
185
|
}
|
|
182
186
|
|
|
183
187
|
const tD = D(t);
|
|
@@ -187,10 +191,7 @@ export function bezierPointHorner(points, t) {
|
|
|
187
191
|
// Line: P0 + t(P1 - P0)
|
|
188
192
|
const [x0, y0] = [D(points[0][0]), D(points[0][1])];
|
|
189
193
|
const [x1, y1] = [D(points[1][0]), D(points[1][1])];
|
|
190
|
-
return [
|
|
191
|
-
x0.plus(tD.times(x1.minus(x0))),
|
|
192
|
-
y0.plus(tD.times(y1.minus(y0)))
|
|
193
|
-
];
|
|
194
|
+
return [x0.plus(tD.times(x1.minus(x0))), y0.plus(tD.times(y1.minus(y0)))];
|
|
194
195
|
}
|
|
195
196
|
|
|
196
197
|
if (n === 2) {
|
|
@@ -206,7 +207,7 @@ export function bezierPointHorner(points, t) {
|
|
|
206
207
|
|
|
207
208
|
return [
|
|
208
209
|
x0.plus(tD.times(c1x.plus(tD.times(c2x)))),
|
|
209
|
-
y0.plus(tD.times(c1y.plus(tD.times(c2y))))
|
|
210
|
+
y0.plus(tD.times(c1y.plus(tD.times(c2y)))),
|
|
210
211
|
];
|
|
211
212
|
}
|
|
212
213
|
|
|
@@ -231,7 +232,7 @@ export function bezierPointHorner(points, t) {
|
|
|
231
232
|
|
|
232
233
|
return [
|
|
233
234
|
x0.plus(tD.times(c1x.plus(tD.times(c2x.plus(tD.times(c3x)))))),
|
|
234
|
-
y0.plus(tD.times(c1y.plus(tD.times(c2y.plus(tD.times(c3y))))))
|
|
235
|
+
y0.plus(tD.times(c1y.plus(tD.times(c2y.plus(tD.times(c3y)))))),
|
|
235
236
|
];
|
|
236
237
|
}
|
|
237
238
|
|
|
@@ -262,7 +263,9 @@ export function bezierDerivative(points, t, n = 1) {
|
|
|
262
263
|
// INPUT VALIDATION: Ensure points array is valid
|
|
263
264
|
// WHY: Derivative computation requires iterating over control points
|
|
264
265
|
if (!points || !Array.isArray(points) || points.length < 2) {
|
|
265
|
-
throw new Error(
|
|
266
|
+
throw new Error(
|
|
267
|
+
"bezierDerivative: points must be an array with at least 2 control points",
|
|
268
|
+
);
|
|
266
269
|
}
|
|
267
270
|
|
|
268
271
|
if (n === 0) {
|
|
@@ -284,8 +287,12 @@ export function bezierDerivative(points, t, n = 1) {
|
|
|
284
287
|
const newPoints = [];
|
|
285
288
|
|
|
286
289
|
for (let i = 0; i < currentDegree; i++) {
|
|
287
|
-
const dx = derivPoints[i + 1][0]
|
|
288
|
-
|
|
290
|
+
const dx = derivPoints[i + 1][0]
|
|
291
|
+
.minus(derivPoints[i][0])
|
|
292
|
+
.times(currentDegree);
|
|
293
|
+
const dy = derivPoints[i + 1][1]
|
|
294
|
+
.minus(derivPoints[i][1])
|
|
295
|
+
.times(currentDegree);
|
|
289
296
|
newPoints.push([dx, dy]);
|
|
290
297
|
}
|
|
291
298
|
|
|
@@ -312,15 +319,21 @@ export function bezierDerivativePoints(points) {
|
|
|
312
319
|
// INPUT VALIDATION: Ensure points array is valid
|
|
313
320
|
// WHY: Need at least 2 points to compute derivative control points
|
|
314
321
|
if (!points || !Array.isArray(points) || points.length < 2) {
|
|
315
|
-
throw new Error(
|
|
322
|
+
throw new Error(
|
|
323
|
+
"bezierDerivativePoints: points must be an array with at least 2 control points",
|
|
324
|
+
);
|
|
316
325
|
}
|
|
317
326
|
|
|
318
327
|
const n = points.length - 1;
|
|
319
328
|
const result = [];
|
|
320
329
|
|
|
321
330
|
for (let i = 0; i < n; i++) {
|
|
322
|
-
const dx = D(points[i + 1][0])
|
|
323
|
-
|
|
331
|
+
const dx = D(points[i + 1][0])
|
|
332
|
+
.minus(D(points[i][0]))
|
|
333
|
+
.times(n);
|
|
334
|
+
const dy = D(points[i + 1][1])
|
|
335
|
+
.minus(D(points[i][1]))
|
|
336
|
+
.times(n);
|
|
324
337
|
result.push([dx, dy]);
|
|
325
338
|
}
|
|
326
339
|
|
|
@@ -346,7 +359,9 @@ export function bezierTangent(points, t) {
|
|
|
346
359
|
// INPUT VALIDATION: Ensure points array is valid
|
|
347
360
|
// WHY: Tangent calculation requires derivative computation which needs valid points
|
|
348
361
|
if (!points || !Array.isArray(points) || points.length < 2) {
|
|
349
|
-
throw new Error(
|
|
362
|
+
throw new Error(
|
|
363
|
+
"bezierTangent: points must be an array with at least 2 control points",
|
|
364
|
+
);
|
|
350
365
|
}
|
|
351
366
|
|
|
352
367
|
const [dx, dy] = bezierDerivative(points, t, 1);
|
|
@@ -364,7 +379,10 @@ export function bezierTangent(points, t) {
|
|
|
364
379
|
if (mag2.isZero() || mag2.lt(DERIVATIVE_ZERO_THRESHOLD)) {
|
|
365
380
|
// Fallback to direction from start to end
|
|
366
381
|
const [x0, y0] = [D(points[0][0]), D(points[0][1])];
|
|
367
|
-
const [xn, yn] = [
|
|
382
|
+
const [xn, yn] = [
|
|
383
|
+
D(points[points.length - 1][0]),
|
|
384
|
+
D(points[points.length - 1][1]),
|
|
385
|
+
];
|
|
368
386
|
const ddx = xn.minus(x0);
|
|
369
387
|
const ddy = yn.minus(y0);
|
|
370
388
|
const magFallback = ddx.times(ddx).plus(ddy.times(ddy)).sqrt();
|
|
@@ -395,7 +413,9 @@ export function bezierNormal(points, t) {
|
|
|
395
413
|
// INPUT VALIDATION: Ensure points array is valid
|
|
396
414
|
// WHY: Normal is computed from tangent which requires valid points
|
|
397
415
|
if (!points || !Array.isArray(points) || points.length < 2) {
|
|
398
|
-
throw new Error(
|
|
416
|
+
throw new Error(
|
|
417
|
+
"bezierNormal: points must be an array with at least 2 control points",
|
|
418
|
+
);
|
|
399
419
|
}
|
|
400
420
|
|
|
401
421
|
const [tx, ty] = bezierTangent(points, t);
|
|
@@ -425,7 +445,9 @@ export function bezierCurvature(points, t) {
|
|
|
425
445
|
// INPUT VALIDATION: Ensure points array is valid
|
|
426
446
|
// WHY: Curvature requires first and second derivatives which need valid points
|
|
427
447
|
if (!points || !Array.isArray(points) || points.length < 2) {
|
|
428
|
-
throw new Error(
|
|
448
|
+
throw new Error(
|
|
449
|
+
"bezierCurvature: points must be an array with at least 2 control points",
|
|
450
|
+
);
|
|
429
451
|
}
|
|
430
452
|
|
|
431
453
|
const [dx, dy] = bezierDerivative(points, t, 1);
|
|
@@ -438,7 +460,10 @@ export function bezierCurvature(points, t) {
|
|
|
438
460
|
const speedSquared = dx.times(dx).plus(dy.times(dy));
|
|
439
461
|
|
|
440
462
|
// WHY: Use named constant for curvature singularity detection
|
|
441
|
-
if (
|
|
463
|
+
if (
|
|
464
|
+
speedSquared.isZero() ||
|
|
465
|
+
speedSquared.lt(CURVATURE_SINGULARITY_THRESHOLD)
|
|
466
|
+
) {
|
|
442
467
|
// At a cusp, curvature is undefined (infinity)
|
|
443
468
|
return new Decimal(Infinity);
|
|
444
469
|
}
|
|
@@ -461,7 +486,9 @@ export function bezierRadiusOfCurvature(points, t) {
|
|
|
461
486
|
// INPUT VALIDATION: Ensure points array is valid
|
|
462
487
|
// WHY: Radius computation requires curvature which needs valid points
|
|
463
488
|
if (!points || !Array.isArray(points) || points.length < 2) {
|
|
464
|
-
throw new Error(
|
|
489
|
+
throw new Error(
|
|
490
|
+
"bezierRadiusOfCurvature: points must be an array with at least 2 control points",
|
|
491
|
+
);
|
|
465
492
|
}
|
|
466
493
|
|
|
467
494
|
const k = bezierCurvature(points, t);
|
|
@@ -495,7 +522,9 @@ export function bezierSplit(points, t) {
|
|
|
495
522
|
// INPUT VALIDATION: Ensure points array is valid
|
|
496
523
|
// WHY: de Casteljau algorithm requires iterating over control points
|
|
497
524
|
if (!points || !Array.isArray(points) || points.length < 2) {
|
|
498
|
-
throw new Error(
|
|
525
|
+
throw new Error(
|
|
526
|
+
"bezierSplit: points must be an array with at least 2 control points",
|
|
527
|
+
);
|
|
499
528
|
}
|
|
500
529
|
|
|
501
530
|
const tD = D(t);
|
|
@@ -512,7 +541,7 @@ export function bezierSplit(points, t) {
|
|
|
512
541
|
// Convert to Decimal
|
|
513
542
|
let pts = points.map(([x, y]) => [D(x), D(y)]);
|
|
514
543
|
|
|
515
|
-
const left = [pts[0]];
|
|
544
|
+
const left = [pts[0]]; // First point of left curve
|
|
516
545
|
const right = [];
|
|
517
546
|
|
|
518
547
|
// de Casteljau iterations, saving the edges
|
|
@@ -553,7 +582,9 @@ export function bezierHalve(points) {
|
|
|
553
582
|
// INPUT VALIDATION: Ensure points array is valid
|
|
554
583
|
// WHY: bezierHalve delegates to bezierSplit which needs valid points
|
|
555
584
|
if (!points || !Array.isArray(points) || points.length < 2) {
|
|
556
|
-
throw new Error(
|
|
585
|
+
throw new Error(
|
|
586
|
+
"bezierHalve: points must be an array with at least 2 control points",
|
|
587
|
+
);
|
|
557
588
|
}
|
|
558
589
|
|
|
559
590
|
return bezierSplit(points, 0.5);
|
|
@@ -573,23 +604,25 @@ export function bezierCrop(points, t0, t1) {
|
|
|
573
604
|
// INPUT VALIDATION: Ensure points array is valid
|
|
574
605
|
// WHY: bezierCrop uses bezierSplit which requires valid points
|
|
575
606
|
if (!points || !Array.isArray(points) || points.length < 2) {
|
|
576
|
-
throw new Error(
|
|
607
|
+
throw new Error(
|
|
608
|
+
"bezierCrop: points must be an array with at least 2 control points",
|
|
609
|
+
);
|
|
577
610
|
}
|
|
578
611
|
|
|
579
612
|
const t0D = D(t0);
|
|
580
613
|
const t1D = D(t1);
|
|
581
614
|
|
|
582
615
|
if (t0D.gte(t1D)) {
|
|
583
|
-
throw new Error(
|
|
616
|
+
throw new Error("bezierCrop: t0 must be less than t1");
|
|
584
617
|
}
|
|
585
618
|
|
|
586
619
|
// PARAMETER BOUNDS: Ensure t0 and t1 are within valid range [0, 1]
|
|
587
620
|
// WHY: Parameters outside [0,1] don't correspond to points on the curve segment
|
|
588
621
|
if (t0D.lt(0) || t0D.gt(1)) {
|
|
589
|
-
throw new Error(
|
|
622
|
+
throw new Error("bezierCrop: t0 must be in range [0, 1]");
|
|
590
623
|
}
|
|
591
624
|
if (t1D.lt(0) || t1D.gt(1)) {
|
|
592
|
-
throw new Error(
|
|
625
|
+
throw new Error("bezierCrop: t1 must be in range [0, 1]");
|
|
593
626
|
}
|
|
594
627
|
|
|
595
628
|
// First split at t0, take the right portion
|
|
@@ -624,7 +657,9 @@ export function bezierBoundingBox(points) {
|
|
|
624
657
|
// INPUT VALIDATION: Ensure points array is valid
|
|
625
658
|
// WHY: Bounding box computation requires accessing control points and computing derivatives
|
|
626
659
|
if (!points || !Array.isArray(points) || points.length < 2) {
|
|
627
|
-
throw new Error(
|
|
660
|
+
throw new Error(
|
|
661
|
+
"bezierBoundingBox: points must be an array with at least 2 control points",
|
|
662
|
+
);
|
|
628
663
|
}
|
|
629
664
|
|
|
630
665
|
const n = points.length;
|
|
@@ -647,9 +682,9 @@ export function bezierBoundingBox(points) {
|
|
|
647
682
|
const derivPts = bezierDerivativePoints(points);
|
|
648
683
|
|
|
649
684
|
// Find critical points (where derivative = 0) for x and y separately
|
|
650
|
-
const criticalTs = findBezierRoots1D(derivPts,
|
|
651
|
-
.concat(findBezierRoots1D(derivPts,
|
|
652
|
-
.filter(t => t.gt(0) && t.lt(1));
|
|
685
|
+
const criticalTs = findBezierRoots1D(derivPts, "x")
|
|
686
|
+
.concat(findBezierRoots1D(derivPts, "y"))
|
|
687
|
+
.filter((t) => t.gt(0) && t.lt(1));
|
|
653
688
|
|
|
654
689
|
// Evaluate at critical points
|
|
655
690
|
for (const t of criticalTs) {
|
|
@@ -678,11 +713,11 @@ function findBezierRoots1D(points, component) {
|
|
|
678
713
|
return []; // No roots possible for empty input
|
|
679
714
|
}
|
|
680
715
|
|
|
681
|
-
const idx = component ===
|
|
716
|
+
const idx = component === "x" ? 0 : 1;
|
|
682
717
|
const roots = [];
|
|
683
718
|
|
|
684
719
|
// Extract 1D control points
|
|
685
|
-
const coeffs = points.map(p => D(p[idx]));
|
|
720
|
+
const coeffs = points.map((p) => D(p[idx]));
|
|
686
721
|
|
|
687
722
|
// For quadratic (2 points) and cubic (3 points), use analytical solutions
|
|
688
723
|
if (coeffs.length === 2) {
|
|
@@ -721,7 +756,7 @@ function findBezierRoots1D(points, component) {
|
|
|
721
756
|
} else {
|
|
722
757
|
// Higher degree: use subdivision
|
|
723
758
|
const subdivisionRoots = findRootsBySubdivision(coeffs, D(0), D(1), 50);
|
|
724
|
-
roots.push(...subdivisionRoots.filter(t => t.gt(0) && t.lt(1)));
|
|
759
|
+
roots.push(...subdivisionRoots.filter((t) => t.gt(0) && t.lt(1)));
|
|
725
760
|
}
|
|
726
761
|
|
|
727
762
|
return roots;
|
|
@@ -746,7 +781,10 @@ function solveQuadratic(a, b, c) {
|
|
|
746
781
|
// WHY: Absolute thresholds fail when coefficients are scaled; relative threshold adapts
|
|
747
782
|
const coeffMag = Decimal.max(a.abs(), b.abs(), c.abs());
|
|
748
783
|
|
|
749
|
-
if (
|
|
784
|
+
if (
|
|
785
|
+
coeffMag.gt(0) &&
|
|
786
|
+
a.abs().div(coeffMag).lt(QUADRATIC_DEGENERATE_THRESHOLD)
|
|
787
|
+
) {
|
|
750
788
|
// Linear equation: bx + c = 0
|
|
751
789
|
if (b.isZero()) return [];
|
|
752
790
|
return [c.neg().div(b)];
|
|
@@ -805,7 +843,7 @@ function solveQuadratic(a, b, c) {
|
|
|
805
843
|
*/
|
|
806
844
|
function findRootsBySubdivision(coeffs, t0, t1, maxDepth) {
|
|
807
845
|
// Check if interval might contain a root (sign change in convex hull)
|
|
808
|
-
const signs = coeffs.map(c => c.isNegative() ? -1 :
|
|
846
|
+
const signs = coeffs.map((c) => (c.isNegative() ? -1 : c.isZero() ? 0 : 1));
|
|
809
847
|
const minSign = Math.min(...signs);
|
|
810
848
|
const maxSign = Math.max(...signs);
|
|
811
849
|
|
|
@@ -833,11 +871,17 @@ function findRootsBySubdivision(coeffs, t0, t1, maxDepth) {
|
|
|
833
871
|
}
|
|
834
872
|
|
|
835
873
|
/**
|
|
836
|
-
* Subdivide 1D Bezier at t=0.5.
|
|
874
|
+
* Subdivide 1D Bezier at t=0.5 using de Casteljau's algorithm.
|
|
875
|
+
*
|
|
876
|
+
* Splits a 1D Bezier curve (array of scalar control values) into two halves.
|
|
877
|
+
* Used internally by root-finding subdivision algorithm.
|
|
878
|
+
*
|
|
879
|
+
* @param {Decimal[]} coeffs - 1D control values (scalars, not points)
|
|
880
|
+
* @returns {{left: Decimal[], right: Decimal[]}} Two 1D Bezier curves representing left and right halves
|
|
837
881
|
*/
|
|
838
882
|
function subdivideBezier1D(coeffs) {
|
|
839
883
|
const half = D(0.5);
|
|
840
|
-
let pts = coeffs.map(c => D(c));
|
|
884
|
+
let pts = coeffs.map((c) => D(c));
|
|
841
885
|
|
|
842
886
|
const left = [pts[0]];
|
|
843
887
|
const right = [];
|
|
@@ -877,7 +921,9 @@ export function bezierToPolynomial(points) {
|
|
|
877
921
|
// INPUT VALIDATION: Ensure points array is valid
|
|
878
922
|
// WHY: Polynomial conversion requires accessing control points by index
|
|
879
923
|
if (!points || !Array.isArray(points) || points.length < 2) {
|
|
880
|
-
throw new Error(
|
|
924
|
+
throw new Error(
|
|
925
|
+
"bezierToPolynomial: points must be an array with at least 2 control points",
|
|
926
|
+
);
|
|
881
927
|
}
|
|
882
928
|
|
|
883
929
|
const n = points.length - 1;
|
|
@@ -907,12 +953,24 @@ export function bezierToPolynomial(points) {
|
|
|
907
953
|
xCoeffs.push(P[0][0]);
|
|
908
954
|
xCoeffs.push(P[1][0].minus(P[0][0]).times(3));
|
|
909
955
|
xCoeffs.push(P[0][0].minus(P[1][0].times(2)).plus(P[2][0]).times(3));
|
|
910
|
-
xCoeffs.push(
|
|
956
|
+
xCoeffs.push(
|
|
957
|
+
P[0][0]
|
|
958
|
+
.neg()
|
|
959
|
+
.plus(P[1][0].times(3))
|
|
960
|
+
.minus(P[2][0].times(3))
|
|
961
|
+
.plus(P[3][0]),
|
|
962
|
+
);
|
|
911
963
|
|
|
912
964
|
yCoeffs.push(P[0][1]);
|
|
913
965
|
yCoeffs.push(P[1][1].minus(P[0][1]).times(3));
|
|
914
966
|
yCoeffs.push(P[0][1].minus(P[1][1].times(2)).plus(P[2][1]).times(3));
|
|
915
|
-
yCoeffs.push(
|
|
967
|
+
yCoeffs.push(
|
|
968
|
+
P[0][1]
|
|
969
|
+
.neg()
|
|
970
|
+
.plus(P[1][1].times(3))
|
|
971
|
+
.minus(P[2][1].times(3))
|
|
972
|
+
.plus(P[3][1]),
|
|
973
|
+
);
|
|
916
974
|
} else {
|
|
917
975
|
throw new Error(`Polynomial conversion for degree ${n} not implemented`);
|
|
918
976
|
}
|
|
@@ -930,13 +988,19 @@ export function bezierToPolynomial(points) {
|
|
|
930
988
|
export function polynomialToBezier(xCoeffs, yCoeffs) {
|
|
931
989
|
// INPUT VALIDATION
|
|
932
990
|
if (!xCoeffs || !Array.isArray(xCoeffs) || xCoeffs.length < 2) {
|
|
933
|
-
throw new Error(
|
|
991
|
+
throw new Error(
|
|
992
|
+
"polynomialToBezier: xCoeffs must be an array with at least 2 coefficients",
|
|
993
|
+
);
|
|
934
994
|
}
|
|
935
995
|
if (!yCoeffs || !Array.isArray(yCoeffs) || yCoeffs.length < 2) {
|
|
936
|
-
throw new Error(
|
|
996
|
+
throw new Error(
|
|
997
|
+
"polynomialToBezier: yCoeffs must be an array with at least 2 coefficients",
|
|
998
|
+
);
|
|
937
999
|
}
|
|
938
1000
|
if (xCoeffs.length !== yCoeffs.length) {
|
|
939
|
-
throw new Error(
|
|
1001
|
+
throw new Error(
|
|
1002
|
+
"polynomialToBezier: xCoeffs and yCoeffs must have the same length",
|
|
1003
|
+
);
|
|
940
1004
|
}
|
|
941
1005
|
|
|
942
1006
|
const n = xCoeffs.length - 1;
|
|
@@ -944,7 +1008,7 @@ export function polynomialToBezier(xCoeffs, yCoeffs) {
|
|
|
944
1008
|
if (n === 1) {
|
|
945
1009
|
return [
|
|
946
1010
|
[xCoeffs[0], yCoeffs[0]],
|
|
947
|
-
[xCoeffs[0].plus(xCoeffs[1]), yCoeffs[0].plus(yCoeffs[1])]
|
|
1011
|
+
[xCoeffs[0].plus(xCoeffs[1]), yCoeffs[0].plus(yCoeffs[1])],
|
|
948
1012
|
];
|
|
949
1013
|
}
|
|
950
1014
|
|
|
@@ -957,21 +1021,34 @@ export function polynomialToBezier(xCoeffs, yCoeffs) {
|
|
|
957
1021
|
const y1 = yCoeffs[0].plus(yCoeffs[1].div(2));
|
|
958
1022
|
const y2 = yCoeffs[0].plus(yCoeffs[1]).plus(yCoeffs[2]);
|
|
959
1023
|
|
|
960
|
-
return [
|
|
1024
|
+
return [
|
|
1025
|
+
[x0, y0],
|
|
1026
|
+
[x1, y1],
|
|
1027
|
+
[x2, y2],
|
|
1028
|
+
];
|
|
961
1029
|
}
|
|
962
1030
|
|
|
963
1031
|
if (n === 3) {
|
|
964
1032
|
const x0 = xCoeffs[0];
|
|
965
1033
|
const x1 = xCoeffs[0].plus(xCoeffs[1].div(3));
|
|
966
|
-
const x2 = xCoeffs[0]
|
|
1034
|
+
const x2 = xCoeffs[0]
|
|
1035
|
+
.plus(xCoeffs[1].times(2).div(3))
|
|
1036
|
+
.plus(xCoeffs[2].div(3));
|
|
967
1037
|
const x3 = xCoeffs[0].plus(xCoeffs[1]).plus(xCoeffs[2]).plus(xCoeffs[3]);
|
|
968
1038
|
|
|
969
1039
|
const y0 = yCoeffs[0];
|
|
970
1040
|
const y1 = yCoeffs[0].plus(yCoeffs[1].div(3));
|
|
971
|
-
const y2 = yCoeffs[0]
|
|
1041
|
+
const y2 = yCoeffs[0]
|
|
1042
|
+
.plus(yCoeffs[1].times(2).div(3))
|
|
1043
|
+
.plus(yCoeffs[2].div(3));
|
|
972
1044
|
const y3 = yCoeffs[0].plus(yCoeffs[1]).plus(yCoeffs[2]).plus(yCoeffs[3]);
|
|
973
1045
|
|
|
974
|
-
return [
|
|
1046
|
+
return [
|
|
1047
|
+
[x0, y0],
|
|
1048
|
+
[x1, y1],
|
|
1049
|
+
[x2, y2],
|
|
1050
|
+
[x3, y3],
|
|
1051
|
+
];
|
|
975
1052
|
}
|
|
976
1053
|
|
|
977
1054
|
throw new Error(`Bezier conversion for degree ${n} not implemented`);
|
|
@@ -990,11 +1067,13 @@ export function polynomialToBezier(xCoeffs, yCoeffs) {
|
|
|
990
1067
|
* @param {number|string|Decimal} [tolerance='1e-60'] - Maximum difference
|
|
991
1068
|
* @returns {{valid: boolean, deCasteljau: Point2D, horner: Point2D, difference: Decimal}}
|
|
992
1069
|
*/
|
|
993
|
-
export function verifyBezierPoint(points, t, tolerance =
|
|
1070
|
+
export function verifyBezierPoint(points, t, tolerance = "1e-60") {
|
|
994
1071
|
// INPUT VALIDATION: Ensure points array is valid
|
|
995
1072
|
// WHY: Verification functions need valid input to produce meaningful results
|
|
996
1073
|
if (!points || !Array.isArray(points) || points.length < 2) {
|
|
997
|
-
throw new Error(
|
|
1074
|
+
throw new Error(
|
|
1075
|
+
"verifyBezierPoint: points must be an array with at least 2 control points",
|
|
1076
|
+
);
|
|
998
1077
|
}
|
|
999
1078
|
|
|
1000
1079
|
const tol = D(tolerance);
|
|
@@ -1009,7 +1088,7 @@ export function verifyBezierPoint(points, t, tolerance = '1e-60') {
|
|
|
1009
1088
|
valid: maxDiff.lte(tol),
|
|
1010
1089
|
deCasteljau,
|
|
1011
1090
|
horner,
|
|
1012
|
-
difference: maxDiff
|
|
1091
|
+
difference: maxDiff,
|
|
1013
1092
|
};
|
|
1014
1093
|
}
|
|
1015
1094
|
|
|
@@ -1026,11 +1105,13 @@ export function verifyBezierPoint(points, t, tolerance = '1e-60') {
|
|
|
1026
1105
|
* @param {number|string|Decimal} [tolerance='1e-50'] - Maximum error
|
|
1027
1106
|
* @returns {{valid: boolean, errors: string[], splitPoint: Point2D, leftEnd: Point2D, rightStart: Point2D}}
|
|
1028
1107
|
*/
|
|
1029
|
-
export function verifyBezierSplit(points, splitT, tolerance =
|
|
1108
|
+
export function verifyBezierSplit(points, splitT, tolerance = "1e-50") {
|
|
1030
1109
|
// INPUT VALIDATION: Ensure points array and split parameter are valid
|
|
1031
1110
|
// WHY: Split verification requires valid curve and parameter
|
|
1032
1111
|
if (!points || !Array.isArray(points) || points.length < 2) {
|
|
1033
|
-
throw new Error(
|
|
1112
|
+
throw new Error(
|
|
1113
|
+
"verifyBezierSplit: points must be an array with at least 2 control points",
|
|
1114
|
+
);
|
|
1034
1115
|
}
|
|
1035
1116
|
|
|
1036
1117
|
const tol = D(tolerance);
|
|
@@ -1041,7 +1122,9 @@ export function verifyBezierSplit(points, splitT, tolerance = '1e-50') {
|
|
|
1041
1122
|
|
|
1042
1123
|
// Check 1: Left curve ends at split point
|
|
1043
1124
|
const leftEnd = bezierPoint(left, 1);
|
|
1044
|
-
const leftDiff = D(leftEnd[0])
|
|
1125
|
+
const leftDiff = D(leftEnd[0])
|
|
1126
|
+
.minus(D(splitPoint[0]))
|
|
1127
|
+
.abs()
|
|
1045
1128
|
.plus(D(leftEnd[1]).minus(D(splitPoint[1])).abs());
|
|
1046
1129
|
if (leftDiff.gt(tol)) {
|
|
1047
1130
|
errors.push(`Left curve end differs from split point by ${leftDiff}`);
|
|
@@ -1049,7 +1132,9 @@ export function verifyBezierSplit(points, splitT, tolerance = '1e-50') {
|
|
|
1049
1132
|
|
|
1050
1133
|
// Check 2: Right curve starts at split point
|
|
1051
1134
|
const rightStart = bezierPoint(right, 0);
|
|
1052
|
-
const rightDiff = D(rightStart[0])
|
|
1135
|
+
const rightDiff = D(rightStart[0])
|
|
1136
|
+
.minus(D(splitPoint[0]))
|
|
1137
|
+
.abs()
|
|
1053
1138
|
.plus(D(rightStart[1]).minus(D(splitPoint[1])).abs());
|
|
1054
1139
|
if (rightDiff.gt(tol)) {
|
|
1055
1140
|
errors.push(`Right curve start differs from split point by ${rightDiff}`);
|
|
@@ -1062,7 +1147,9 @@ export function verifyBezierSplit(points, splitT, tolerance = '1e-50') {
|
|
|
1062
1147
|
const origT = D(testT).times(tD);
|
|
1063
1148
|
const origPt = bezierPoint(points, origT);
|
|
1064
1149
|
const leftPt = bezierPoint(left, testT);
|
|
1065
|
-
const leftTestDiff = D(origPt[0])
|
|
1150
|
+
const leftTestDiff = D(origPt[0])
|
|
1151
|
+
.minus(D(leftPt[0]))
|
|
1152
|
+
.abs()
|
|
1066
1153
|
.plus(D(origPt[1]).minus(D(leftPt[1])).abs());
|
|
1067
1154
|
if (leftTestDiff.gt(tol)) {
|
|
1068
1155
|
errors.push(`Left half at t=${testT} differs by ${leftTestDiff}`);
|
|
@@ -1072,7 +1159,9 @@ export function verifyBezierSplit(points, splitT, tolerance = '1e-50') {
|
|
|
1072
1159
|
const origT2 = tD.plus(D(testT).times(D(1).minus(tD)));
|
|
1073
1160
|
const origPt2 = bezierPoint(points, origT2);
|
|
1074
1161
|
const rightPt = bezierPoint(right, testT);
|
|
1075
|
-
const rightTestDiff = D(origPt2[0])
|
|
1162
|
+
const rightTestDiff = D(origPt2[0])
|
|
1163
|
+
.minus(D(rightPt[0]))
|
|
1164
|
+
.abs()
|
|
1076
1165
|
.plus(D(origPt2[1]).minus(D(rightPt[1])).abs());
|
|
1077
1166
|
if (rightTestDiff.gt(tol)) {
|
|
1078
1167
|
errors.push(`Right half at t=${testT} differs by ${rightTestDiff}`);
|
|
@@ -1084,7 +1173,7 @@ export function verifyBezierSplit(points, splitT, tolerance = '1e-50') {
|
|
|
1084
1173
|
errors,
|
|
1085
1174
|
splitPoint,
|
|
1086
1175
|
leftEnd,
|
|
1087
|
-
rightStart
|
|
1176
|
+
rightStart,
|
|
1088
1177
|
};
|
|
1089
1178
|
}
|
|
1090
1179
|
|
|
@@ -1097,11 +1186,13 @@ export function verifyBezierSplit(points, splitT, tolerance = '1e-50') {
|
|
|
1097
1186
|
* @param {number|string|Decimal} [tolerance='1e-50'] - Maximum error
|
|
1098
1187
|
* @returns {{valid: boolean, errors: string[], expectedStart: Point2D, actualStart: Point2D, expectedEnd: Point2D, actualEnd: Point2D}}
|
|
1099
1188
|
*/
|
|
1100
|
-
export function verifyBezierCrop(points, t0, t1, tolerance =
|
|
1189
|
+
export function verifyBezierCrop(points, t0, t1, tolerance = "1e-50") {
|
|
1101
1190
|
// INPUT VALIDATION: Ensure points array and parameters are valid
|
|
1102
1191
|
// WHY: Crop verification requires valid curve and parameter range
|
|
1103
1192
|
if (!points || !Array.isArray(points) || points.length < 2) {
|
|
1104
|
-
throw new Error(
|
|
1193
|
+
throw new Error(
|
|
1194
|
+
"verifyBezierCrop: points must be an array with at least 2 control points",
|
|
1195
|
+
);
|
|
1105
1196
|
}
|
|
1106
1197
|
|
|
1107
1198
|
const tol = D(tolerance);
|
|
@@ -1116,13 +1207,17 @@ export function verifyBezierCrop(points, t0, t1, tolerance = '1e-50') {
|
|
|
1116
1207
|
const actualStart = bezierPoint(cropped, 0);
|
|
1117
1208
|
const actualEnd = bezierPoint(cropped, 1);
|
|
1118
1209
|
|
|
1119
|
-
const startDiff = D(expectedStart[0])
|
|
1210
|
+
const startDiff = D(expectedStart[0])
|
|
1211
|
+
.minus(D(actualStart[0]))
|
|
1212
|
+
.abs()
|
|
1120
1213
|
.plus(D(expectedStart[1]).minus(D(actualStart[1])).abs());
|
|
1121
1214
|
if (startDiff.gt(tol)) {
|
|
1122
1215
|
errors.push(`Cropped start differs by ${startDiff}`);
|
|
1123
1216
|
}
|
|
1124
1217
|
|
|
1125
|
-
const endDiff = D(expectedEnd[0])
|
|
1218
|
+
const endDiff = D(expectedEnd[0])
|
|
1219
|
+
.minus(D(actualEnd[0]))
|
|
1220
|
+
.abs()
|
|
1126
1221
|
.plus(D(expectedEnd[1]).minus(D(actualEnd[1])).abs());
|
|
1127
1222
|
if (endDiff.gt(tol)) {
|
|
1128
1223
|
errors.push(`Cropped end differs by ${endDiff}`);
|
|
@@ -1132,7 +1227,9 @@ export function verifyBezierCrop(points, t0, t1, tolerance = '1e-50') {
|
|
|
1132
1227
|
const midT = D(t0).plus(D(t1)).div(2);
|
|
1133
1228
|
const expectedMid = bezierPoint(points, midT);
|
|
1134
1229
|
const actualMid = bezierPoint(cropped, 0.5);
|
|
1135
|
-
const midDiff = D(expectedMid[0])
|
|
1230
|
+
const midDiff = D(expectedMid[0])
|
|
1231
|
+
.minus(D(actualMid[0]))
|
|
1232
|
+
.abs()
|
|
1136
1233
|
.plus(D(expectedMid[1]).minus(D(actualMid[1])).abs());
|
|
1137
1234
|
if (midDiff.gt(tol)) {
|
|
1138
1235
|
errors.push(`Cropped midpoint differs by ${midDiff}`);
|
|
@@ -1144,7 +1241,7 @@ export function verifyBezierCrop(points, t0, t1, tolerance = '1e-50') {
|
|
|
1144
1241
|
expectedStart,
|
|
1145
1242
|
actualStart,
|
|
1146
1243
|
expectedEnd,
|
|
1147
|
-
actualEnd
|
|
1244
|
+
actualEnd,
|
|
1148
1245
|
};
|
|
1149
1246
|
}
|
|
1150
1247
|
|
|
@@ -1155,11 +1252,13 @@ export function verifyBezierCrop(points, t0, t1, tolerance = '1e-50') {
|
|
|
1155
1252
|
* @param {number|string|Decimal} [tolerance='1e-50'] - Maximum error
|
|
1156
1253
|
* @returns {{valid: boolean, maxError: Decimal, originalPoints: BezierPoints, reconstructedPoints: BezierPoints}}
|
|
1157
1254
|
*/
|
|
1158
|
-
export function verifyPolynomialConversion(points, tolerance =
|
|
1255
|
+
export function verifyPolynomialConversion(points, tolerance = "1e-50") {
|
|
1159
1256
|
// INPUT VALIDATION: Ensure points array is valid
|
|
1160
1257
|
// WHY: Polynomial conversion verification requires valid control points
|
|
1161
1258
|
if (!points || !Array.isArray(points) || points.length < 2) {
|
|
1162
|
-
throw new Error(
|
|
1259
|
+
throw new Error(
|
|
1260
|
+
"verifyPolynomialConversion: points must be an array with at least 2 control points",
|
|
1261
|
+
);
|
|
1163
1262
|
}
|
|
1164
1263
|
|
|
1165
1264
|
const tol = D(tolerance);
|
|
@@ -1189,7 +1288,7 @@ export function verifyPolynomialConversion(points, tolerance = '1e-50') {
|
|
|
1189
1288
|
valid: maxError.lte(tol),
|
|
1190
1289
|
maxError,
|
|
1191
1290
|
originalPoints: points,
|
|
1192
|
-
reconstructedPoints: reconstructed
|
|
1291
|
+
reconstructedPoints: reconstructed,
|
|
1193
1292
|
};
|
|
1194
1293
|
}
|
|
1195
1294
|
|
|
@@ -1205,11 +1304,13 @@ export function verifyPolynomialConversion(points, tolerance = '1e-50') {
|
|
|
1205
1304
|
* @param {number|string|Decimal} [tolerance='1e-50'] - Maximum error
|
|
1206
1305
|
* @returns {{valid: boolean, errors: string[], tangent: Point2D, normal: Point2D, tangentMagnitude: Decimal, normalMagnitude: Decimal, dotProduct: Decimal}}
|
|
1207
1306
|
*/
|
|
1208
|
-
export function verifyTangentNormal(points, t, tolerance =
|
|
1307
|
+
export function verifyTangentNormal(points, t, tolerance = "1e-50") {
|
|
1209
1308
|
// INPUT VALIDATION: Ensure points array and parameter are valid
|
|
1210
1309
|
// WHY: Tangent/normal verification requires valid curve and parameter
|
|
1211
1310
|
if (!points || !Array.isArray(points) || points.length < 2) {
|
|
1212
|
-
throw new Error(
|
|
1311
|
+
throw new Error(
|
|
1312
|
+
"verifyTangentNormal: points must be an array with at least 2 control points",
|
|
1313
|
+
);
|
|
1213
1314
|
}
|
|
1214
1315
|
|
|
1215
1316
|
const tol = D(tolerance);
|
|
@@ -1238,7 +1339,9 @@ export function verifyTangentNormal(points, t, tolerance = '1e-50') {
|
|
|
1238
1339
|
// Check perpendicularity
|
|
1239
1340
|
const dotProduct = tx.times(nx).plus(ty.times(ny));
|
|
1240
1341
|
if (dotProduct.abs().gt(tol)) {
|
|
1241
|
-
errors.push(
|
|
1342
|
+
errors.push(
|
|
1343
|
+
`Tangent and normal not perpendicular, dot product = ${dotProduct}`,
|
|
1344
|
+
);
|
|
1242
1345
|
}
|
|
1243
1346
|
|
|
1244
1347
|
// Check tangent aligns with derivative direction
|
|
@@ -1246,7 +1349,10 @@ export function verifyTangentNormal(points, t, tolerance = '1e-50') {
|
|
|
1246
1349
|
if (derivMag.gt(tol)) {
|
|
1247
1350
|
const normalizedDx = dx.div(derivMag);
|
|
1248
1351
|
const normalizedDy = dy.div(derivMag);
|
|
1249
|
-
const alignDiff = tx
|
|
1352
|
+
const alignDiff = tx
|
|
1353
|
+
.minus(normalizedDx)
|
|
1354
|
+
.abs()
|
|
1355
|
+
.plus(ty.minus(normalizedDy).abs());
|
|
1250
1356
|
if (alignDiff.gt(tol)) {
|
|
1251
1357
|
errors.push(`Tangent doesn't align with derivative direction`);
|
|
1252
1358
|
}
|
|
@@ -1259,7 +1365,7 @@ export function verifyTangentNormal(points, t, tolerance = '1e-50') {
|
|
|
1259
1365
|
normal,
|
|
1260
1366
|
tangentMagnitude: tangentMag,
|
|
1261
1367
|
normalMagnitude: normalMag,
|
|
1262
|
-
dotProduct
|
|
1368
|
+
dotProduct,
|
|
1263
1369
|
};
|
|
1264
1370
|
}
|
|
1265
1371
|
|
|
@@ -1272,11 +1378,13 @@ export function verifyTangentNormal(points, t, tolerance = '1e-50') {
|
|
|
1272
1378
|
* @param {number|string|Decimal} [tolerance='1e-10'] - Maximum relative error
|
|
1273
1379
|
* @returns {{valid: boolean, errors: string[], analyticCurvature: Decimal, finiteDiffCurvature: Decimal, radiusVerified: boolean}}
|
|
1274
1380
|
*/
|
|
1275
|
-
export function verifyCurvature(points, t, tolerance =
|
|
1381
|
+
export function verifyCurvature(points, t, tolerance = "1e-10") {
|
|
1276
1382
|
// INPUT VALIDATION: Ensure points array and parameter are valid
|
|
1277
1383
|
// WHY: Curvature verification requires valid curve and parameter
|
|
1278
1384
|
if (!points || !Array.isArray(points) || points.length < 2) {
|
|
1279
|
-
throw new Error(
|
|
1385
|
+
throw new Error(
|
|
1386
|
+
"verifyCurvature: points must be an array with at least 2 control points",
|
|
1387
|
+
);
|
|
1280
1388
|
}
|
|
1281
1389
|
|
|
1282
1390
|
const tol = D(tolerance);
|
|
@@ -1292,7 +1400,7 @@ export function verifyCurvature(points, t, tolerance = '1e-10') {
|
|
|
1292
1400
|
|
|
1293
1401
|
const t1 = Decimal.max(D(0), tD.minus(h));
|
|
1294
1402
|
const t2 = Decimal.min(D(1), tD.plus(h));
|
|
1295
|
-
const
|
|
1403
|
+
const _actualH = t2.minus(t1);
|
|
1296
1404
|
|
|
1297
1405
|
const tan1 = bezierTangent(points, t1);
|
|
1298
1406
|
const tan2 = bezierTangent(points, t2);
|
|
@@ -1310,8 +1418,11 @@ export function verifyCurvature(points, t, tolerance = '1e-10') {
|
|
|
1310
1418
|
// Arc length over interval
|
|
1311
1419
|
const pt1 = bezierPoint(points, t1);
|
|
1312
1420
|
const pt2 = bezierPoint(points, t2);
|
|
1313
|
-
const arcLen = D(pt2[0])
|
|
1314
|
-
.
|
|
1421
|
+
const arcLen = D(pt2[0])
|
|
1422
|
+
.minus(D(pt1[0]))
|
|
1423
|
+
.pow(2)
|
|
1424
|
+
.plus(D(pt2[1]).minus(D(pt1[1])).pow(2))
|
|
1425
|
+
.sqrt();
|
|
1315
1426
|
|
|
1316
1427
|
let finiteDiffCurvature;
|
|
1317
1428
|
// WHY: Use named constant for arc length threshold in curvature verification
|
|
@@ -1326,7 +1437,10 @@ export function verifyCurvature(points, t, tolerance = '1e-10') {
|
|
|
1326
1437
|
// Skip comparison for extreme curvatures (cusps)
|
|
1327
1438
|
} else if (analyticCurvature.abs().gt(CURVATURE_RELATIVE_ERROR_THRESHOLD)) {
|
|
1328
1439
|
// WHY: Use named constant for curvature magnitude threshold
|
|
1329
|
-
const relError = analyticCurvature
|
|
1440
|
+
const relError = analyticCurvature
|
|
1441
|
+
.minus(finiteDiffCurvature)
|
|
1442
|
+
.abs()
|
|
1443
|
+
.div(analyticCurvature.abs());
|
|
1330
1444
|
if (relError.gt(tol)) {
|
|
1331
1445
|
errors.push(`Curvature relative error ${relError} exceeds tolerance`);
|
|
1332
1446
|
}
|
|
@@ -1352,7 +1466,7 @@ export function verifyCurvature(points, t, tolerance = '1e-10') {
|
|
|
1352
1466
|
errors,
|
|
1353
1467
|
analyticCurvature,
|
|
1354
1468
|
finiteDiffCurvature,
|
|
1355
|
-
radiusVerified
|
|
1469
|
+
radiusVerified,
|
|
1356
1470
|
};
|
|
1357
1471
|
}
|
|
1358
1472
|
|
|
@@ -1364,11 +1478,13 @@ export function verifyCurvature(points, t, tolerance = '1e-10') {
|
|
|
1364
1478
|
* @param {number|string|Decimal} [tolerance='1e-40'] - Maximum error
|
|
1365
1479
|
* @returns {{valid: boolean, errors: string[], bbox: Object, allPointsInside: boolean, criticalPointsOnEdge: boolean}}
|
|
1366
1480
|
*/
|
|
1367
|
-
export function verifyBoundingBox(points, samples = 100, tolerance =
|
|
1481
|
+
export function verifyBoundingBox(points, samples = 100, tolerance = "1e-40") {
|
|
1368
1482
|
// INPUT VALIDATION: Ensure points array is valid
|
|
1369
1483
|
// WHY: Bounding box verification requires valid control points
|
|
1370
1484
|
if (!points || !Array.isArray(points) || points.length < 2) {
|
|
1371
|
-
throw new Error(
|
|
1485
|
+
throw new Error(
|
|
1486
|
+
"verifyBoundingBox: points must be an array with at least 2 control points",
|
|
1487
|
+
);
|
|
1372
1488
|
}
|
|
1373
1489
|
|
|
1374
1490
|
const tol = D(tolerance);
|
|
@@ -1384,17 +1500,24 @@ export function verifyBoundingBox(points, samples = 100, tolerance = '1e-40') {
|
|
|
1384
1500
|
const [x, y] = bezierPoint(points, t);
|
|
1385
1501
|
|
|
1386
1502
|
if (D(x).lt(bbox.xmin.minus(tol)) || D(x).gt(bbox.xmax.plus(tol))) {
|
|
1387
|
-
errors.push(
|
|
1503
|
+
errors.push(
|
|
1504
|
+
`Point at t=${t} x=${x} outside x bounds [${bbox.xmin}, ${bbox.xmax}]`,
|
|
1505
|
+
);
|
|
1388
1506
|
allPointsInside = false;
|
|
1389
1507
|
}
|
|
1390
1508
|
if (D(y).lt(bbox.ymin.minus(tol)) || D(y).gt(bbox.ymax.plus(tol))) {
|
|
1391
|
-
errors.push(
|
|
1509
|
+
errors.push(
|
|
1510
|
+
`Point at t=${t} y=${y} outside y bounds [${bbox.ymin}, ${bbox.ymax}]`,
|
|
1511
|
+
);
|
|
1392
1512
|
allPointsInside = false;
|
|
1393
1513
|
}
|
|
1394
1514
|
}
|
|
1395
1515
|
|
|
1396
1516
|
// Verify bounding box edges are achieved by some point
|
|
1397
|
-
let xminAchieved = false,
|
|
1517
|
+
let xminAchieved = false,
|
|
1518
|
+
xmaxAchieved = false,
|
|
1519
|
+
yminAchieved = false,
|
|
1520
|
+
ymaxAchieved = false;
|
|
1398
1521
|
|
|
1399
1522
|
for (let i = 0; i <= samples; i++) {
|
|
1400
1523
|
const t = D(i).div(samples);
|
|
@@ -1408,10 +1531,10 @@ export function verifyBoundingBox(points, samples = 100, tolerance = '1e-40') {
|
|
|
1408
1531
|
|
|
1409
1532
|
if (!xminAchieved || !xmaxAchieved || !yminAchieved || !ymaxAchieved) {
|
|
1410
1533
|
criticalPointsOnEdge = false;
|
|
1411
|
-
if (!xminAchieved) errors.push(
|
|
1412
|
-
if (!xmaxAchieved) errors.push(
|
|
1413
|
-
if (!yminAchieved) errors.push(
|
|
1414
|
-
if (!ymaxAchieved) errors.push(
|
|
1534
|
+
if (!xminAchieved) errors.push("xmin not achieved by any curve point");
|
|
1535
|
+
if (!xmaxAchieved) errors.push("xmax not achieved by any curve point");
|
|
1536
|
+
if (!yminAchieved) errors.push("ymin not achieved by any curve point");
|
|
1537
|
+
if (!ymaxAchieved) errors.push("ymax not achieved by any curve point");
|
|
1415
1538
|
}
|
|
1416
1539
|
|
|
1417
1540
|
return {
|
|
@@ -1419,7 +1542,7 @@ export function verifyBoundingBox(points, samples = 100, tolerance = '1e-40') {
|
|
|
1419
1542
|
errors,
|
|
1420
1543
|
bbox,
|
|
1421
1544
|
allPointsInside,
|
|
1422
|
-
criticalPointsOnEdge
|
|
1545
|
+
criticalPointsOnEdge,
|
|
1423
1546
|
};
|
|
1424
1547
|
}
|
|
1425
1548
|
|
|
@@ -1432,11 +1555,13 @@ export function verifyBoundingBox(points, samples = 100, tolerance = '1e-40') {
|
|
|
1432
1555
|
* @param {number|string|Decimal} [tolerance='1e-8'] - Maximum relative error
|
|
1433
1556
|
* @returns {{valid: boolean, analytic: Point2D, finiteDiff: Point2D, relativeError: Decimal}}
|
|
1434
1557
|
*/
|
|
1435
|
-
export function verifyDerivative(points, t, order = 1, tolerance =
|
|
1558
|
+
export function verifyDerivative(points, t, order = 1, tolerance = "1e-8") {
|
|
1436
1559
|
// INPUT VALIDATION: Ensure points array, parameter, and order are valid
|
|
1437
1560
|
// WHY: Derivative verification requires valid inputs for meaningful results
|
|
1438
1561
|
if (!points || !Array.isArray(points) || points.length < 2) {
|
|
1439
|
-
throw new Error(
|
|
1562
|
+
throw new Error(
|
|
1563
|
+
"verifyDerivative: points must be an array with at least 2 control points",
|
|
1564
|
+
);
|
|
1440
1565
|
}
|
|
1441
1566
|
|
|
1442
1567
|
const tol = D(tolerance);
|
|
@@ -1457,7 +1582,7 @@ export function verifyDerivative(points, t, order = 1, tolerance = '1e-8') {
|
|
|
1457
1582
|
const dt = t2.minus(t1);
|
|
1458
1583
|
finiteDiff = [
|
|
1459
1584
|
D(pt2[0]).minus(D(pt1[0])).div(dt),
|
|
1460
|
-
D(pt2[1]).minus(D(pt1[1])).div(dt)
|
|
1585
|
+
D(pt2[1]).minus(D(pt1[1])).div(dt),
|
|
1461
1586
|
];
|
|
1462
1587
|
} else if (order === 2) {
|
|
1463
1588
|
const t0 = tD;
|
|
@@ -1470,14 +1595,16 @@ export function verifyDerivative(points, t, order = 1, tolerance = '1e-8') {
|
|
|
1470
1595
|
const h2 = h.pow(2);
|
|
1471
1596
|
finiteDiff = [
|
|
1472
1597
|
D(pt2[0]).minus(D(pt0[0]).times(2)).plus(D(pt1[0])).div(h2),
|
|
1473
|
-
D(pt2[1]).minus(D(pt0[1]).times(2)).plus(D(pt1[1])).div(h2)
|
|
1598
|
+
D(pt2[1]).minus(D(pt0[1]).times(2)).plus(D(pt1[1])).div(h2),
|
|
1474
1599
|
];
|
|
1475
1600
|
} else {
|
|
1476
1601
|
// Higher orders: not implemented in finite difference approximation
|
|
1477
1602
|
// WHY: Higher order finite differences require more sample points and
|
|
1478
1603
|
// have increasing numerical instability. For verification purposes,
|
|
1479
1604
|
// we note this limitation.
|
|
1480
|
-
console.warn(
|
|
1605
|
+
console.warn(
|
|
1606
|
+
`verifyDerivative: finite difference for order ${order} not implemented`,
|
|
1607
|
+
);
|
|
1481
1608
|
finiteDiff = analytic; // Use analytic as fallback (always "passes")
|
|
1482
1609
|
}
|
|
1483
1610
|
|
|
@@ -1497,7 +1624,7 @@ export function verifyDerivative(points, t, order = 1, tolerance = '1e-8') {
|
|
|
1497
1624
|
valid: relativeError.lte(tol),
|
|
1498
1625
|
analytic,
|
|
1499
1626
|
finiteDiff,
|
|
1500
|
-
relativeError
|
|
1627
|
+
relativeError,
|
|
1501
1628
|
};
|
|
1502
1629
|
}
|
|
1503
1630
|
|
|
@@ -1509,14 +1636,18 @@ export function verifyDerivative(points, t, order = 1, tolerance = '1e-8') {
|
|
|
1509
1636
|
* @param {number|string|Decimal} [tolerance='1e-30'] - Maximum distance
|
|
1510
1637
|
* @returns {{valid: boolean, t: Decimal|null, distance: Decimal}}
|
|
1511
1638
|
*/
|
|
1512
|
-
export function verifyPointOnCurve(points, testPoint, tolerance =
|
|
1639
|
+
export function verifyPointOnCurve(points, testPoint, tolerance = "1e-30") {
|
|
1513
1640
|
// INPUT VALIDATION: Ensure points array and test point are valid
|
|
1514
1641
|
// WHY: Point-on-curve verification requires valid curve and test point
|
|
1515
1642
|
if (!points || !Array.isArray(points) || points.length < 2) {
|
|
1516
|
-
throw new Error(
|
|
1643
|
+
throw new Error(
|
|
1644
|
+
"verifyPointOnCurve: points must be an array with at least 2 control points",
|
|
1645
|
+
);
|
|
1517
1646
|
}
|
|
1518
1647
|
if (!testPoint || !Array.isArray(testPoint) || testPoint.length < 2) {
|
|
1519
|
-
throw new Error(
|
|
1648
|
+
throw new Error(
|
|
1649
|
+
"verifyPointOnCurve: testPoint must be a valid 2D point [x, y]",
|
|
1650
|
+
);
|
|
1520
1651
|
}
|
|
1521
1652
|
|
|
1522
1653
|
const [px, py] = [D(testPoint[0]), D(testPoint[1])];
|
|
@@ -1555,7 +1686,12 @@ export function verifyPointOnCurve(points, testPoint, tolerance = '1e-30') {
|
|
|
1555
1686
|
|
|
1556
1687
|
// f''(t) for Newton's method
|
|
1557
1688
|
const [d2x, d2y] = bezierDerivative(points, bestT, 2);
|
|
1558
|
-
const fDoublePrime = dx
|
|
1689
|
+
const fDoublePrime = dx
|
|
1690
|
+
.pow(2)
|
|
1691
|
+
.plus(dy.pow(2))
|
|
1692
|
+
.plus(diffX.times(d2x))
|
|
1693
|
+
.plus(diffY.times(d2y))
|
|
1694
|
+
.times(2);
|
|
1559
1695
|
|
|
1560
1696
|
// WHY: Use named constant for zero second derivative check
|
|
1561
1697
|
if (fDoublePrime.abs().lt(NEAR_ZERO_THRESHOLD)) break;
|
|
@@ -1573,12 +1709,16 @@ export function verifyPointOnCurve(points, testPoint, tolerance = '1e-30') {
|
|
|
1573
1709
|
|
|
1574
1710
|
// Final distance check
|
|
1575
1711
|
const [finalX, finalY] = bezierPoint(points, bestT);
|
|
1576
|
-
const finalDist = px
|
|
1712
|
+
const finalDist = px
|
|
1713
|
+
.minus(finalX)
|
|
1714
|
+
.pow(2)
|
|
1715
|
+
.plus(py.minus(finalY).pow(2))
|
|
1716
|
+
.sqrt();
|
|
1577
1717
|
|
|
1578
1718
|
return {
|
|
1579
1719
|
valid: finalDist.lte(tol),
|
|
1580
1720
|
t: finalDist.lte(tol) ? bestT : null,
|
|
1581
|
-
distance: finalDist
|
|
1721
|
+
distance: finalDist,
|
|
1582
1722
|
};
|
|
1583
1723
|
}
|
|
1584
1724
|
|
|
@@ -1622,5 +1762,5 @@ export default {
|
|
|
1622
1762
|
verifyCurvature,
|
|
1623
1763
|
verifyBoundingBox,
|
|
1624
1764
|
verifyDerivative,
|
|
1625
|
-
verifyPointOnCurve
|
|
1765
|
+
verifyPointOnCurve,
|
|
1626
1766
|
};
|