@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.
Files changed (46) hide show
  1. package/README.md +325 -0
  2. package/bin/svg-matrix.js +985 -378
  3. package/bin/svglinter.cjs +4172 -433
  4. package/bin/svgm.js +723 -180
  5. package/package.json +16 -4
  6. package/src/animation-references.js +71 -52
  7. package/src/arc-length.js +160 -96
  8. package/src/bezier-analysis.js +257 -117
  9. package/src/bezier-intersections.js +411 -148
  10. package/src/browser-verify.js +240 -100
  11. package/src/clip-path-resolver.js +350 -142
  12. package/src/convert-path-data.js +279 -134
  13. package/src/css-specificity.js +78 -70
  14. package/src/flatten-pipeline.js +751 -263
  15. package/src/geometry-to-path.js +511 -182
  16. package/src/index.js +191 -46
  17. package/src/inkscape-support.js +18 -7
  18. package/src/marker-resolver.js +278 -164
  19. package/src/mask-resolver.js +209 -98
  20. package/src/matrix.js +147 -67
  21. package/src/mesh-gradient.js +187 -96
  22. package/src/off-canvas-detection.js +201 -104
  23. package/src/path-analysis.js +187 -107
  24. package/src/path-data-plugins.js +628 -167
  25. package/src/path-simplification.js +0 -1
  26. package/src/pattern-resolver.js +125 -88
  27. package/src/polygon-clip.js +111 -66
  28. package/src/svg-boolean-ops.js +194 -118
  29. package/src/svg-collections.js +22 -18
  30. package/src/svg-flatten.js +282 -164
  31. package/src/svg-parser.js +427 -200
  32. package/src/svg-rendering-context.js +147 -104
  33. package/src/svg-toolbox.js +16381 -3370
  34. package/src/svg2-polyfills.js +93 -224
  35. package/src/transform-decomposition.js +46 -41
  36. package/src/transform-optimization.js +89 -68
  37. package/src/transforms2d.js +49 -16
  38. package/src/transforms3d.js +58 -22
  39. package/src/use-symbol-resolver.js +150 -110
  40. package/src/vector.js +67 -15
  41. package/src/vendor/README.md +110 -0
  42. package/src/vendor/inkscape-hatch-polyfill.js +401 -0
  43. package/src/vendor/inkscape-hatch-polyfill.min.js +8 -0
  44. package/src/vendor/inkscape-mesh-polyfill.js +843 -0
  45. package/src/vendor/inkscape-mesh-polyfill.min.js +8 -0
  46. package/src/verification.js +288 -124
@@ -18,7 +18,7 @@
18
18
  * - Handles extreme coordinate ranges
19
19
  */
20
20
 
21
- import Decimal from 'decimal.js';
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 assertFinite(val, context) {
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('1e-50');
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('1e-100');
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('1e-8');
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('1e-40');
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('1e-60');
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('1e-70');
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('1e-15');
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('1e-50');
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('1e-10');
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('1e-10');
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('1e-20');
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('bezierPoint: points must be an array with at least 2 control points');
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('bezierPointHorner: points must be an array with at least 2 control points');
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('bezierDerivative: points must be an array with at least 2 control points');
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].minus(derivPoints[i][0]).times(currentDegree);
288
- const dy = derivPoints[i + 1][1].minus(derivPoints[i][1]).times(currentDegree);
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('bezierDerivativePoints: points must be an array with at least 2 control points');
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]).minus(D(points[i][0])).times(n);
323
- const dy = D(points[i + 1][1]).minus(D(points[i][1])).times(n);
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('bezierTangent: points must be an array with at least 2 control points');
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] = [D(points[points.length - 1][0]), D(points[points.length - 1][1])];
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('bezierNormal: points must be an array with at least 2 control points');
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('bezierCurvature: points must be an array with at least 2 control points');
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 (speedSquared.isZero() || speedSquared.lt(CURVATURE_SINGULARITY_THRESHOLD)) {
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('bezierRadiusOfCurvature: points must be an array with at least 2 control points');
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('bezierSplit: points must be an array with at least 2 control points');
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]]; // First point of left curve
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('bezierHalve: points must be an array with at least 2 control points');
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('bezierCrop: points must be an array with at least 2 control points');
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('bezierCrop: t0 must be less than t1');
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('bezierCrop: t0 must be in range [0, 1]');
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('bezierCrop: t1 must be in range [0, 1]');
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('bezierBoundingBox: points must be an array with at least 2 control points');
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, 'x')
651
- .concat(findBezierRoots1D(derivPts, 'y'))
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 === 'x' ? 0 : 1;
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 (coeffMag.gt(0) && a.abs().div(coeffMag).lt(QUADRATIC_DEGENERATE_THRESHOLD)) {
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 : (c.isZero() ? 0 : 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('bezierToPolynomial: points must be an array with at least 2 control points');
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(P[0][0].neg().plus(P[1][0].times(3)).minus(P[2][0].times(3)).plus(P[3][0]));
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(P[0][1].neg().plus(P[1][1].times(3)).minus(P[2][1].times(3)).plus(P[3][1]));
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('polynomialToBezier: xCoeffs must be an array with at least 2 coefficients');
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('polynomialToBezier: yCoeffs must be an array with at least 2 coefficients');
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('polynomialToBezier: xCoeffs and yCoeffs must have the same length');
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 [[x0, y0], [x1, y1], [x2, y2]];
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].plus(xCoeffs[1].times(2).div(3)).plus(xCoeffs[2].div(3));
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].plus(yCoeffs[1].times(2).div(3)).plus(yCoeffs[2].div(3));
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 [[x0, y0], [x1, y1], [x2, y2], [x3, y3]];
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 = '1e-60') {
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('verifyBezierPoint: points must be an array with at least 2 control points');
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 = '1e-50') {
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('verifyBezierSplit: points must be an array with at least 2 control points');
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]).minus(D(splitPoint[0])).abs()
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]).minus(D(splitPoint[0])).abs()
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]).minus(D(leftPt[0])).abs()
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]).minus(D(rightPt[0])).abs()
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 = '1e-50') {
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('verifyBezierCrop: points must be an array with at least 2 control points');
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]).minus(D(actualStart[0])).abs()
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]).minus(D(actualEnd[0])).abs()
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]).minus(D(actualMid[0])).abs()
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 = '1e-50') {
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('verifyPolynomialConversion: points must be an array with at least 2 control points');
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 = '1e-50') {
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('verifyTangentNormal: points must be an array with at least 2 control points');
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(`Tangent and normal not perpendicular, dot product = ${dotProduct}`);
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.minus(normalizedDx).abs().plus(ty.minus(normalizedDy).abs());
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 = '1e-10') {
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('verifyCurvature: points must be an array with at least 2 control points');
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 actualH = t2.minus(t1);
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]).minus(D(pt1[0])).pow(2)
1314
- .plus(D(pt2[1]).minus(D(pt1[1])).pow(2)).sqrt();
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.minus(finiteDiffCurvature).abs().div(analyticCurvature.abs());
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 = '1e-40') {
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('verifyBoundingBox: points must be an array with at least 2 control points');
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(`Point at t=${t} x=${x} outside x bounds [${bbox.xmin}, ${bbox.xmax}]`);
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(`Point at t=${t} y=${y} outside y bounds [${bbox.ymin}, ${bbox.ymax}]`);
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, xmaxAchieved = false, yminAchieved = false, ymaxAchieved = 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('xmin not achieved by any curve point');
1412
- if (!xmaxAchieved) errors.push('xmax not achieved by any curve point');
1413
- if (!yminAchieved) errors.push('ymin not achieved by any curve point');
1414
- if (!ymaxAchieved) errors.push('ymax not achieved by any curve point');
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 = '1e-8') {
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('verifyDerivative: points must be an array with at least 2 control points');
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(`verifyDerivative: finite difference for order ${order} not implemented`);
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 = '1e-30') {
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('verifyPointOnCurve: points must be an array with at least 2 control points');
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('verifyPointOnCurve: testPoint must be a valid 2D point [x, y]');
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.pow(2).plus(dy.pow(2)).plus(diffX.times(d2x)).plus(diffY.times(d2y)).times(2);
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.minus(finalX).pow(2).plus(py.minus(finalY).pow(2)).sqrt();
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
  };