@emasoft/svg-matrix 1.0.27 → 1.0.29

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 +994 -378
  3. package/bin/svglinter.cjs +4172 -433
  4. package/bin/svgm.js +744 -184
  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 +404 -0
  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 +48 -19
  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 +16411 -3298
  34. package/src/svg2-polyfills.js +114 -245
  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
@@ -11,49 +11,51 @@
11
11
  * @version 1.0.0
12
12
  */
13
13
 
14
- import Decimal from 'decimal.js';
14
+ import Decimal from "decimal.js";
15
15
  import {
16
16
  bezierPoint,
17
17
  bezierDerivative,
18
18
  bezierTangent,
19
- bezierBoundingBox
20
- } from './bezier-analysis.js';
21
- import { arcLength } from './arc-length.js';
19
+ bezierBoundingBox,
20
+ } from "./bezier-analysis.js";
21
+ import { arcLength } from "./arc-length.js";
22
22
 
23
23
  Decimal.set({ precision: 80 });
24
24
 
25
- const D = x => (x instanceof Decimal ? x : new Decimal(x));
26
- const PI = new Decimal('3.1415926535897932384626433832795028841971693993751058209749445923078164062862090');
25
+ const D = (x) => (x instanceof Decimal ? x : new Decimal(x));
26
+ const PI = new Decimal(
27
+ "3.1415926535897932384626433832795028841971693993751058209749445923078164062862090",
28
+ );
27
29
 
28
30
  // ============================================================================
29
31
  // NUMERICAL CONSTANTS (documented magic numbers)
30
32
  // ============================================================================
31
33
 
32
34
  /** Tolerance for boundary detection in point-in-path - very small to catch points on curve */
33
- const BOUNDARY_TOLERANCE = new Decimal('1e-20');
35
+ const BOUNDARY_TOLERANCE = new Decimal("1e-20");
34
36
 
35
37
  /** Default tolerance for path closed/continuous checks - detects microscopic gaps */
36
- const DEFAULT_CONTINUITY_TOLERANCE = '1e-20';
38
+ const DEFAULT_CONTINUITY_TOLERANCE = "1e-20";
37
39
 
38
40
  /** Default tolerance for path smoothness (tangent angle) checks - allows tiny angle differences */
39
- const DEFAULT_SMOOTHNESS_TOLERANCE = '1e-10';
41
+ const DEFAULT_SMOOTHNESS_TOLERANCE = "1e-10";
40
42
 
41
43
  /** Tolerance for centroid-based direction calculations - avoids division by near-zero */
42
- const CENTROID_ZERO_THRESHOLD = new Decimal('1e-30');
44
+ const CENTROID_ZERO_THRESHOLD = new Decimal("1e-30");
43
45
 
44
46
  /** Small epsilon for neighbor point testing - small offset for nearby point checks */
45
- const NEIGHBOR_TEST_EPSILON = new Decimal('1e-10');
47
+ const NEIGHBOR_TEST_EPSILON = new Decimal("1e-10");
46
48
 
47
49
  /** Threshold for considering tangents anti-parallel (180-degree turn) - dot product ~ -1 */
48
- const ANTI_PARALLEL_THRESHOLD = new Decimal('-0.99');
50
+ const ANTI_PARALLEL_THRESHOLD = new Decimal("-0.99");
49
51
 
50
52
  /** Tolerance for Newton-Raphson singular Jacobian detection - avoids division by zero */
51
- const JACOBIAN_SINGULARITY_THRESHOLD = new Decimal('1e-60');
53
+ const JACOBIAN_SINGULARITY_THRESHOLD = new Decimal("1e-60");
52
54
 
53
55
  /** Numerical precision tolerance for farthest point verification.
54
56
  * WHY: Accounts for floating-point rounding in distance comparisons, not sampling error.
55
57
  * The found distance should be >= max sampled within this numerical tolerance. */
56
- const FARTHEST_POINT_NUMERICAL_TOLERANCE = new Decimal('1e-10');
58
+ const FARTHEST_POINT_NUMERICAL_TOLERANCE = new Decimal("1e-10");
57
59
 
58
60
  // ============================================================================
59
61
  // AREA CALCULATION (GREEN'S THEOREM)
@@ -88,7 +90,7 @@ const FARTHEST_POINT_NUMERICAL_TOLERANCE = new Decimal('1e-10');
88
90
  export function pathArea(segments, options = {}) {
89
91
  // WHY: Validate input to prevent undefined behavior and provide clear error messages
90
92
  if (!segments || !Array.isArray(segments)) {
91
- throw new Error('pathArea: segments must be an array');
93
+ throw new Error("pathArea: segments must be an array");
92
94
  }
93
95
 
94
96
  const { samples = 50 } = options;
@@ -118,11 +120,9 @@ export function pathArea(segments, options = {}) {
118
120
 
119
121
  // Green: (1/2) * integral of (x*dy - y*dx)
120
122
  area = area.plus(lineIntegralXdY.minus(lineIntegralYdX).div(2));
121
-
122
123
  } else if (n === 2 || n === 3) {
123
124
  // Quadratic or Cubic: use exact polynomial integration
124
125
  area = area.plus(bezierAreaContribution(points));
125
-
126
126
  } else {
127
127
  // Higher degree: numerical integration
128
128
  area = area.plus(numericalAreaContribution(points, samples));
@@ -157,23 +157,48 @@ function bezierAreaContribution(points) {
157
157
  // Integral of x(t)*y'(t) from 0 to 1
158
158
  // This expands to a polynomial integral that can be computed exactly
159
159
  // After expansion and integration:
160
- const integral_x_dy = x0.times(y1.minus(y0))
160
+ const integral_x_dy = x0
161
+ .times(y1.minus(y0))
161
162
  .plus(x0.times(y2.minus(y1.times(2)).plus(y0)).div(2))
162
163
  .plus(x1.times(2).minus(x0.times(2)).times(y1.minus(y0)).div(2))
163
- .plus(x1.times(2).minus(x0.times(2)).times(y2.minus(y1.times(2)).plus(y0)).div(3))
164
+ .plus(
165
+ x1
166
+ .times(2)
167
+ .minus(x0.times(2))
168
+ .times(y2.minus(y1.times(2)).plus(y0))
169
+ .div(3),
170
+ )
164
171
  .plus(x2.minus(x1.times(2)).plus(x0).times(y1.minus(y0)).div(3))
165
- .plus(x2.minus(x1.times(2)).plus(x0).times(y2.minus(y1.times(2)).plus(y0)).div(4));
172
+ .plus(
173
+ x2
174
+ .minus(x1.times(2))
175
+ .plus(x0)
176
+ .times(y2.minus(y1.times(2)).plus(y0))
177
+ .div(4),
178
+ );
166
179
 
167
180
  // Similarly for integral of y(t)*x'(t)
168
- const integral_y_dx = y0.times(x1.minus(x0))
181
+ const integral_y_dx = y0
182
+ .times(x1.minus(x0))
169
183
  .plus(y0.times(x2.minus(x1.times(2)).plus(x0)).div(2))
170
184
  .plus(y1.times(2).minus(y0.times(2)).times(x1.minus(x0)).div(2))
171
- .plus(y1.times(2).minus(y0.times(2)).times(x2.minus(x1.times(2)).plus(x0)).div(3))
185
+ .plus(
186
+ y1
187
+ .times(2)
188
+ .minus(y0.times(2))
189
+ .times(x2.minus(x1.times(2)).plus(x0))
190
+ .div(3),
191
+ )
172
192
  .plus(y2.minus(y1.times(2)).plus(y0).times(x1.minus(x0)).div(3))
173
- .plus(y2.minus(y1.times(2)).plus(y0).times(x2.minus(x1.times(2)).plus(x0)).div(4));
193
+ .plus(
194
+ y2
195
+ .minus(y1.times(2))
196
+ .plus(y0)
197
+ .times(x2.minus(x1.times(2)).plus(x0))
198
+ .div(4),
199
+ );
174
200
 
175
201
  return integral_x_dy.minus(integral_y_dx).div(2);
176
-
177
202
  } else if (n === 3) {
178
203
  // Cubic Bezier - use numerical integration
179
204
  // WHY: The exact polynomial integration for cubic Bezier area is complex
@@ -201,7 +226,7 @@ function numericalAreaContribution(points, samples) {
201
226
 
202
227
  for (let i = 0; i <= samples; i++) {
203
228
  const t = h.times(i);
204
- const weight = i === 0 || i === samples ? D(1) : (i % 2 === 0 ? D(2) : D(4));
229
+ const weight = i === 0 || i === samples ? D(1) : i % 2 === 0 ? D(2) : D(4);
205
230
 
206
231
  const [x, y] = bezierPoint(points, t);
207
232
  const [dx, dy] = bezierDerivative(points, t, 1);
@@ -226,7 +251,7 @@ function numericalAreaContribution(points, samples) {
226
251
  export function pathAbsoluteArea(segments, options = {}) {
227
252
  // WHY: Validate input to prevent undefined behavior and provide clear error messages
228
253
  if (!segments || !Array.isArray(segments)) {
229
- throw new Error('pathAbsoluteArea: segments must be an array');
254
+ throw new Error("pathAbsoluteArea: segments must be an array");
230
255
  }
231
256
 
232
257
  return pathArea(segments, options).abs();
@@ -254,13 +279,13 @@ export function pathAbsoluteArea(segments, options = {}) {
254
279
  export function closestPointOnPath(segments, point, options = {}) {
255
280
  // WHY: Validate input to prevent undefined behavior and provide clear error messages
256
281
  if (!segments || !Array.isArray(segments) || segments.length === 0) {
257
- throw new Error('closestPointOnPath: segments must be a non-empty array');
282
+ throw new Error("closestPointOnPath: segments must be a non-empty array");
258
283
  }
259
284
  if (!point || !Array.isArray(point) || point.length < 2) {
260
- throw new Error('closestPointOnPath: point must be an array [x, y]');
285
+ throw new Error("closestPointOnPath: point must be an array [x, y]");
261
286
  }
262
287
 
263
- const { samples = 50, maxIterations = 30, tolerance = '1e-30' } = options;
288
+ const { samples = 50, maxIterations = 30, tolerance = "1e-30" } = options;
264
289
 
265
290
  const px = D(point[0]);
266
291
  const py = D(point[1]);
@@ -303,8 +328,12 @@ export function closestPointOnPath(segments, point, options = {}) {
303
328
  const diffY = y.minus(py);
304
329
 
305
330
  const fPrime = diffX.times(dx).plus(diffY.times(dy)).times(2);
306
- const fDoublePrime = dx.pow(2).plus(dy.pow(2))
307
- .plus(diffX.times(d2x)).plus(diffY.times(d2y)).times(2);
331
+ const fDoublePrime = dx
332
+ .pow(2)
333
+ .plus(dy.pow(2))
334
+ .plus(diffX.times(d2x))
335
+ .plus(diffY.times(d2y))
336
+ .times(2);
308
337
 
309
338
  // WHY: Use named constant instead of magic number for clarity
310
339
  if (fDoublePrime.abs().lt(JACOBIAN_SINGULARITY_THRESHOLD)) break;
@@ -325,9 +354,9 @@ export function closestPointOnPath(segments, point, options = {}) {
325
354
  // WHY: Newton refinement finds local minima within a segment, but segment
326
355
  // endpoints might be closer than any interior critical point
327
356
  for (let segIdx = 0; segIdx < segments.length; segIdx++) {
328
- const pts = segments[segIdx];
357
+ const segPts = segments[segIdx];
329
358
  for (const tVal of [D(0), D(1)]) {
330
- const [x, y] = bezierPoint(pts, tVal);
359
+ const [x, y] = bezierPoint(segPts, tVal);
331
360
  const dist = px.minus(x).pow(2).plus(py.minus(y).pow(2));
332
361
  if (dist.lt(bestDist)) {
333
362
  bestDist = dist;
@@ -339,13 +368,17 @@ export function closestPointOnPath(segments, point, options = {}) {
339
368
 
340
369
  // Final result
341
370
  const [finalX, finalY] = bezierPoint(segments[bestSegment], bestT);
342
- const finalDist = px.minus(finalX).pow(2).plus(py.minus(finalY).pow(2)).sqrt();
371
+ const finalDist = px
372
+ .minus(finalX)
373
+ .pow(2)
374
+ .plus(py.minus(finalY).pow(2))
375
+ .sqrt();
343
376
 
344
377
  return {
345
378
  point: [finalX, finalY],
346
379
  distance: finalDist,
347
380
  segmentIndex: bestSegment,
348
- t: bestT
381
+ t: bestT,
349
382
  };
350
383
  }
351
384
 
@@ -360,13 +393,13 @@ export function closestPointOnPath(segments, point, options = {}) {
360
393
  export function farthestPointOnPath(segments, point, options = {}) {
361
394
  // WHY: Validate input to prevent undefined behavior and provide clear error messages
362
395
  if (!segments || !Array.isArray(segments) || segments.length === 0) {
363
- throw new Error('farthestPointOnPath: segments must be a non-empty array');
396
+ throw new Error("farthestPointOnPath: segments must be a non-empty array");
364
397
  }
365
398
  if (!point || !Array.isArray(point) || point.length < 2) {
366
- throw new Error('farthestPointOnPath: point must be an array [x, y]');
399
+ throw new Error("farthestPointOnPath: point must be an array [x, y]");
367
400
  }
368
401
 
369
- const { samples = 50, maxIterations = 30, tolerance = '1e-30' } = options;
402
+ const { samples = 50, maxIterations = 30, tolerance = "1e-30" } = options;
370
403
 
371
404
  const px = D(point[0]);
372
405
  const py = D(point[1]);
@@ -406,8 +439,12 @@ export function farthestPointOnPath(segments, point, options = {}) {
406
439
 
407
440
  // For maximum: f'(t) = 0, f''(t) < 0
408
441
  const fPrime = diffX.times(dx).plus(diffY.times(dy)).times(2);
409
- const fDoublePrime = dx.pow(2).plus(dy.pow(2))
410
- .plus(diffX.times(d2x)).plus(diffY.times(d2y)).times(2);
442
+ const fDoublePrime = dx
443
+ .pow(2)
444
+ .plus(dy.pow(2))
445
+ .plus(diffX.times(d2x))
446
+ .plus(diffY.times(d2y))
447
+ .times(2);
411
448
 
412
449
  // WHY: Use named constant instead of magic number for clarity
413
450
  if (fDoublePrime.abs().lt(JACOBIAN_SINGULARITY_THRESHOLD)) break;
@@ -426,9 +463,9 @@ export function farthestPointOnPath(segments, point, options = {}) {
426
463
 
427
464
  // Also check endpoints
428
465
  for (let segIdx = 0; segIdx < segments.length; segIdx++) {
429
- const pts = segments[segIdx];
466
+ const segPts = segments[segIdx];
430
467
  for (const t of [D(0), D(1)]) {
431
- const [x, y] = bezierPoint(pts, t);
468
+ const [x, y] = bezierPoint(segPts, t);
432
469
  const dist = px.minus(x).pow(2).plus(py.minus(y).pow(2));
433
470
  if (dist.gt(bestDist)) {
434
471
  bestDist = dist;
@@ -439,13 +476,17 @@ export function farthestPointOnPath(segments, point, options = {}) {
439
476
  }
440
477
 
441
478
  const [finalX, finalY] = bezierPoint(segments[bestSegment], bestT);
442
- const finalDist = px.minus(finalX).pow(2).plus(py.minus(finalY).pow(2)).sqrt();
479
+ const finalDist = px
480
+ .minus(finalX)
481
+ .pow(2)
482
+ .plus(py.minus(finalY).pow(2))
483
+ .sqrt();
443
484
 
444
485
  return {
445
486
  point: [finalX, finalY],
446
487
  distance: finalDist,
447
488
  segmentIndex: bestSegment,
448
- t: bestT
489
+ t: bestT,
449
490
  };
450
491
  }
451
492
 
@@ -468,10 +509,10 @@ export function farthestPointOnPath(segments, point, options = {}) {
468
509
  export function pointInPath(segments, point, options = {}) {
469
510
  // WHY: Validate input to prevent undefined behavior and provide clear error messages
470
511
  if (!segments || !Array.isArray(segments) || segments.length === 0) {
471
- throw new Error('pointInPath: segments must be a non-empty array');
512
+ throw new Error("pointInPath: segments must be a non-empty array");
472
513
  }
473
514
  if (!point || !Array.isArray(point) || point.length < 2) {
474
- throw new Error('pointInPath: point must be an array [x, y]');
515
+ throw new Error("pointInPath: point must be an array [x, y]");
475
516
  }
476
517
 
477
518
  const { samples = 100 } = options;
@@ -533,7 +574,7 @@ export function pointInPath(segments, point, options = {}) {
533
574
  return {
534
575
  inside: windingNumber !== 0,
535
576
  windingNumber,
536
- onBoundary: false
577
+ onBoundary: false,
537
578
  };
538
579
  }
539
580
 
@@ -548,10 +589,13 @@ export function pointInPath(segments, point, options = {}) {
548
589
  * @param {string} [tolerance='1e-20'] - Distance tolerance
549
590
  * @returns {boolean}
550
591
  */
551
- export function isPathClosed(segments, tolerance = DEFAULT_CONTINUITY_TOLERANCE) {
592
+ export function isPathClosed(
593
+ segments,
594
+ tolerance = DEFAULT_CONTINUITY_TOLERANCE,
595
+ ) {
552
596
  // WHY: Validate input to prevent undefined behavior and provide clear error messages
553
597
  if (!segments || !Array.isArray(segments)) {
554
- throw new Error('isPathClosed: segments must be an array');
598
+ throw new Error("isPathClosed: segments must be an array");
555
599
  }
556
600
  if (segments.length === 0) return false;
557
601
 
@@ -560,7 +604,10 @@ export function isPathClosed(segments, tolerance = DEFAULT_CONTINUITY_TOLERANCE)
560
604
  const lastSeg = segments[segments.length - 1];
561
605
 
562
606
  const [x0, y0] = [D(firstSeg[0][0]), D(firstSeg[0][1])];
563
- const [xn, yn] = [D(lastSeg[lastSeg.length - 1][0]), D(lastSeg[lastSeg.length - 1][1])];
607
+ const [xn, yn] = [
608
+ D(lastSeg[lastSeg.length - 1][0]),
609
+ D(lastSeg[lastSeg.length - 1][1]),
610
+ ];
564
611
 
565
612
  const dist = x0.minus(xn).pow(2).plus(y0.minus(yn).pow(2)).sqrt();
566
613
  return dist.lt(tol);
@@ -573,10 +620,13 @@ export function isPathClosed(segments, tolerance = DEFAULT_CONTINUITY_TOLERANCE)
573
620
  * @param {string} [tolerance='1e-20'] - Distance tolerance
574
621
  * @returns {{continuous: boolean, gaps: Array}}
575
622
  */
576
- export function isPathContinuous(segments, tolerance = DEFAULT_CONTINUITY_TOLERANCE) {
623
+ export function isPathContinuous(
624
+ segments,
625
+ tolerance = DEFAULT_CONTINUITY_TOLERANCE,
626
+ ) {
577
627
  // WHY: Validate input to prevent undefined behavior and provide clear error messages
578
628
  if (!segments || !Array.isArray(segments)) {
579
- throw new Error('isPathContinuous: segments must be an array');
629
+ throw new Error("isPathContinuous: segments must be an array");
580
630
  }
581
631
  if (segments.length <= 1) return { continuous: true, gaps: [] };
582
632
 
@@ -597,14 +647,14 @@ export function isPathContinuous(segments, tolerance = DEFAULT_CONTINUITY_TOLERA
597
647
  segmentIndex: i,
598
648
  gap: dist,
599
649
  from: [x1, y1],
600
- to: [x2, y2]
650
+ to: [x2, y2],
601
651
  });
602
652
  }
603
653
  }
604
654
 
605
655
  return {
606
656
  continuous: gaps.length === 0,
607
- gaps
657
+ gaps,
608
658
  };
609
659
  }
610
660
 
@@ -615,10 +665,13 @@ export function isPathContinuous(segments, tolerance = DEFAULT_CONTINUITY_TOLERA
615
665
  * @param {string} [tolerance='1e-10'] - Tangent angle tolerance (radians)
616
666
  * @returns {{smooth: boolean, kinks: Array}}
617
667
  */
618
- export function isPathSmooth(segments, tolerance = DEFAULT_SMOOTHNESS_TOLERANCE) {
668
+ export function isPathSmooth(
669
+ segments,
670
+ tolerance = DEFAULT_SMOOTHNESS_TOLERANCE,
671
+ ) {
619
672
  // WHY: Validate input to prevent undefined behavior and provide clear error messages
620
673
  if (!segments || !Array.isArray(segments)) {
621
- throw new Error('isPathSmooth: segments must be an array');
674
+ throw new Error("isPathSmooth: segments must be an array");
622
675
  }
623
676
  if (segments.length <= 1) return { smooth: true, kinks: [] };
624
677
 
@@ -653,14 +706,14 @@ export function isPathSmooth(segments, tolerance = DEFAULT_SMOOTHNESS_TOLERANCE)
653
706
  segmentIndex: i,
654
707
  angle: Decimal.atan2(cross, dot).abs(),
655
708
  tangent1: [tx1, ty1],
656
- tangent2: [tx2, ty2]
709
+ tangent2: [tx2, ty2],
657
710
  });
658
711
  }
659
712
  }
660
713
 
661
714
  return {
662
715
  smooth: kinks.length === 0,
663
- kinks
716
+ kinks,
664
717
  };
665
718
  }
666
719
 
@@ -674,18 +727,18 @@ export function isPathSmooth(segments, tolerance = DEFAULT_SMOOTHNESS_TOLERANCE)
674
727
  export function findKinks(segments, tolerance = DEFAULT_SMOOTHNESS_TOLERANCE) {
675
728
  // WHY: Validate input to prevent undefined behavior and provide clear error messages
676
729
  if (!segments || !Array.isArray(segments)) {
677
- throw new Error('findKinks: segments must be an array');
730
+ throw new Error("findKinks: segments must be an array");
678
731
  }
679
732
 
680
733
  const { kinks } = isPathSmooth(segments, tolerance);
681
734
 
682
735
  // Convert to path parameter
683
- return kinks.map((k, i) => ({
736
+ return kinks.map((k, _i) => ({
684
737
  segmentIndex: k.segmentIndex,
685
738
  globalT: k.segmentIndex + 1, // At junction between segments
686
739
  angle: k.angle,
687
740
  angleRadians: k.angle,
688
- angleDegrees: k.angle.times(180).div(PI)
741
+ angleDegrees: k.angle.times(180).div(PI),
689
742
  }));
690
743
  }
691
744
 
@@ -702,7 +755,7 @@ export function findKinks(segments, tolerance = DEFAULT_SMOOTHNESS_TOLERANCE) {
702
755
  export function pathBoundingBox(segments) {
703
756
  // WHY: Validate input to prevent undefined behavior and provide clear error messages
704
757
  if (!segments || !Array.isArray(segments)) {
705
- throw new Error('pathBoundingBox: segments must be an array');
758
+ throw new Error("pathBoundingBox: segments must be an array");
706
759
  }
707
760
  if (segments.length === 0) {
708
761
  return { xmin: D(0), xmax: D(0), ymin: D(0), ymax: D(0) };
@@ -735,21 +788,35 @@ export function boundingBoxesOverlap(bbox1, bbox2) {
735
788
  // INPUT VALIDATION
736
789
  // WHY: Prevent cryptic errors from undefined/null bounding boxes
737
790
  if (!bbox1 || !bbox2) {
738
- throw new Error('boundingBoxesOverlap: both bounding boxes are required');
791
+ throw new Error("boundingBoxesOverlap: both bounding boxes are required");
739
792
  }
740
- if (bbox1.xmin === undefined || bbox1.xmax === undefined ||
741
- bbox1.ymin === undefined || bbox1.ymax === undefined) {
742
- throw new Error('boundingBoxesOverlap: bbox1 must have xmin, xmax, ymin, ymax');
793
+ if (
794
+ bbox1.xmin === undefined ||
795
+ bbox1.xmax === undefined ||
796
+ bbox1.ymin === undefined ||
797
+ bbox1.ymax === undefined
798
+ ) {
799
+ throw new Error(
800
+ "boundingBoxesOverlap: bbox1 must have xmin, xmax, ymin, ymax",
801
+ );
743
802
  }
744
- if (bbox2.xmin === undefined || bbox2.xmax === undefined ||
745
- bbox2.ymin === undefined || bbox2.ymax === undefined) {
746
- throw new Error('boundingBoxesOverlap: bbox2 must have xmin, xmax, ymin, ymax');
803
+ if (
804
+ bbox2.xmin === undefined ||
805
+ bbox2.xmax === undefined ||
806
+ bbox2.ymin === undefined ||
807
+ bbox2.ymax === undefined
808
+ ) {
809
+ throw new Error(
810
+ "boundingBoxesOverlap: bbox2 must have xmin, xmax, ymin, ymax",
811
+ );
747
812
  }
748
813
 
749
- return !(bbox1.xmax.lt(bbox2.xmin) ||
750
- bbox1.xmin.gt(bbox2.xmax) ||
751
- bbox1.ymax.lt(bbox2.ymin) ||
752
- bbox1.ymin.gt(bbox2.ymax));
814
+ return !(
815
+ bbox1.xmax.lt(bbox2.xmin) ||
816
+ bbox1.xmin.gt(bbox2.xmax) ||
817
+ bbox1.ymax.lt(bbox2.ymin) ||
818
+ bbox1.ymin.gt(bbox2.ymax)
819
+ );
753
820
  }
754
821
 
755
822
  // ============================================================================
@@ -766,7 +833,7 @@ export function boundingBoxesOverlap(bbox1, bbox2) {
766
833
  export function pathLength(segments, options = {}) {
767
834
  // WHY: Validate input to prevent undefined behavior and provide clear error messages
768
835
  if (!segments || !Array.isArray(segments)) {
769
- throw new Error('pathLength: segments must be an array');
836
+ throw new Error("pathLength: segments must be an array");
770
837
  }
771
838
 
772
839
  let total = D(0);
@@ -791,10 +858,10 @@ export function pathLength(segments, options = {}) {
791
858
  * @param {number|string|Decimal} [tolerance='1e-5'] - Relative error tolerance
792
859
  * @returns {{valid: boolean, greenArea: Decimal, shoelaceArea: Decimal, relativeError: Decimal}}
793
860
  */
794
- export function verifyPathArea(segments, samples = 100, tolerance = '1e-5') {
861
+ export function verifyPathArea(segments, samples = 100, tolerance = "1e-5") {
795
862
  // WHY: Validate input to prevent undefined behavior and provide clear error messages
796
863
  if (!segments || !Array.isArray(segments)) {
797
- throw new Error('verifyPathArea: segments must be an array');
864
+ throw new Error("verifyPathArea: segments must be an array");
798
865
  }
799
866
 
800
867
  const tol = D(tolerance);
@@ -829,7 +896,7 @@ export function verifyPathArea(segments, samples = 100, tolerance = '1e-5') {
829
896
 
830
897
  let relativeError;
831
898
  // WHY: Use named constant to avoid division by near-zero values
832
- const AREA_ZERO_THRESHOLD = new Decimal('1e-30');
899
+ const AREA_ZERO_THRESHOLD = new Decimal("1e-30");
833
900
  if (absGreen.gt(AREA_ZERO_THRESHOLD)) {
834
901
  relativeError = absGreen.minus(absShoelace).abs().div(absGreen);
835
902
  } else {
@@ -841,7 +908,7 @@ export function verifyPathArea(segments, samples = 100, tolerance = '1e-5') {
841
908
  greenArea,
842
909
  shoelaceArea,
843
910
  relativeError,
844
- sameSign: greenArea.isNegative() === shoelaceArea.isNegative()
911
+ sameSign: greenArea.isNegative() === shoelaceArea.isNegative(),
845
912
  };
846
913
  }
847
914
 
@@ -854,13 +921,13 @@ export function verifyPathArea(segments, samples = 100, tolerance = '1e-5') {
854
921
  * @param {number|string|Decimal} [tolerance='1e-10'] - Perpendicularity tolerance
855
922
  * @returns {{valid: boolean, closestPoint: Object, dotProduct: Decimal, isEndpoint: boolean}}
856
923
  */
857
- export function verifyClosestPoint(segments, queryPoint, tolerance = '1e-10') {
924
+ export function verifyClosestPoint(segments, queryPoint, tolerance = "1e-10") {
858
925
  // WHY: Validate input to prevent undefined behavior and provide clear error messages
859
926
  if (!segments || !Array.isArray(segments)) {
860
- throw new Error('verifyClosestPoint: segments must be an array');
927
+ throw new Error("verifyClosestPoint: segments must be an array");
861
928
  }
862
929
  if (!queryPoint || !Array.isArray(queryPoint) || queryPoint.length < 2) {
863
- throw new Error('verifyClosestPoint: queryPoint must be an array [x, y]');
930
+ throw new Error("verifyClosestPoint: queryPoint must be an array [x, y]");
864
931
  }
865
932
 
866
933
  const tol = D(tolerance);
@@ -885,8 +952,9 @@ export function verifyClosestPoint(segments, queryPoint, tolerance = '1e-10') {
885
952
 
886
953
  // WHY: Check if at endpoint (where perpendicularity may not hold)
887
954
  // Use a small threshold to determine if t is effectively 0 or 1
888
- const ENDPOINT_THRESHOLD = new Decimal('1e-10');
889
- const isEndpoint = t.lt(ENDPOINT_THRESHOLD) || t.gt(D(1).minus(ENDPOINT_THRESHOLD));
955
+ const ENDPOINT_THRESHOLD = new Decimal("1e-10");
956
+ const isEndpoint =
957
+ t.lt(ENDPOINT_THRESHOLD) || t.gt(D(1).minus(ENDPOINT_THRESHOLD));
890
958
 
891
959
  return {
892
960
  valid: dotProduct.abs().lte(tol) || isEndpoint,
@@ -894,7 +962,7 @@ export function verifyClosestPoint(segments, queryPoint, tolerance = '1e-10') {
894
962
  dotProduct,
895
963
  isEndpoint,
896
964
  vectorToQuery: [vx, vy],
897
- tangent: [tx, ty]
965
+ tangent: [tx, ty],
898
966
  };
899
967
  }
900
968
 
@@ -910,10 +978,10 @@ export function verifyClosestPoint(segments, queryPoint, tolerance = '1e-10') {
910
978
  export function verifyFarthestPoint(segments, queryPoint, samples = 200) {
911
979
  // WHY: Validate input to prevent undefined behavior and provide clear error messages
912
980
  if (!segments || !Array.isArray(segments)) {
913
- throw new Error('verifyFarthestPoint: segments must be an array');
981
+ throw new Error("verifyFarthestPoint: segments must be an array");
914
982
  }
915
983
  if (!queryPoint || !Array.isArray(queryPoint) || queryPoint.length < 2) {
916
- throw new Error('verifyFarthestPoint: queryPoint must be an array [x, y]');
984
+ throw new Error("verifyFarthestPoint: queryPoint must be an array [x, y]");
917
985
  }
918
986
 
919
987
  const qx = D(queryPoint[0]);
@@ -942,13 +1010,15 @@ export function verifyFarthestPoint(segments, queryPoint, samples = 200) {
942
1010
  // This defeats the purpose of verification - we want to ensure the found point is actually the farthest
943
1011
  // Instead, we check that foundDistance is at least as large as maxSampledDistance
944
1012
  // with a small tolerance for numerical precision (not sampling error, but floating point rounding)
945
- const valid = foundDistance.gte(maxSampledDistance.minus(FARTHEST_POINT_NUMERICAL_TOLERANCE));
1013
+ const valid = foundDistance.gte(
1014
+ maxSampledDistance.minus(FARTHEST_POINT_NUMERICAL_TOLERANCE),
1015
+ );
946
1016
 
947
1017
  return {
948
1018
  valid,
949
1019
  farthestPoint: result,
950
1020
  maxSampledDistance,
951
- foundDistance
1021
+ foundDistance,
952
1022
  };
953
1023
  }
954
1024
 
@@ -964,10 +1034,10 @@ export function verifyFarthestPoint(segments, queryPoint, samples = 200) {
964
1034
  export function verifyPointInPath(segments, testPoint) {
965
1035
  // WHY: Validate input to prevent undefined behavior and provide clear error messages
966
1036
  if (!segments || !Array.isArray(segments)) {
967
- throw new Error('verifyPointInPath: segments must be an array');
1037
+ throw new Error("verifyPointInPath: segments must be an array");
968
1038
  }
969
1039
  if (!testPoint || !Array.isArray(testPoint) || testPoint.length < 2) {
970
- throw new Error('verifyPointInPath: testPoint must be an array [x, y]');
1040
+ throw new Error("verifyPointInPath: testPoint must be an array [x, y]");
971
1041
  }
972
1042
 
973
1043
  const result = pointInPath(segments, testPoint);
@@ -1009,10 +1079,16 @@ export function verifyPointInPath(segments, testPoint) {
1009
1079
  const unitDy = dy.div(len).times(epsilon);
1010
1080
 
1011
1081
  // Test point slightly toward centroid
1012
- const towardCentroid = pointInPath(segments, [px.plus(unitDx), py.plus(unitDy)]);
1082
+ const towardCentroid = pointInPath(segments, [
1083
+ px.plus(unitDx),
1084
+ py.plus(unitDy),
1085
+ ]);
1013
1086
 
1014
1087
  // Test point slightly away from centroid
1015
- const awayFromCentroid = pointInPath(segments, [px.minus(unitDx), py.minus(unitDy)]);
1088
+ const awayFromCentroid = pointInPath(segments, [
1089
+ px.minus(unitDx),
1090
+ py.minus(unitDy),
1091
+ ]);
1016
1092
 
1017
1093
  // If inside, moving toward centroid should stay inside
1018
1094
  // If outside, moving toward centroid should stay outside or become inside (not suddenly outside)
@@ -1030,7 +1106,7 @@ export function verifyPointInPath(segments, testPoint) {
1030
1106
  result,
1031
1107
  consistentWithNeighbors,
1032
1108
  towardCentroid,
1033
- awayFromCentroid
1109
+ awayFromCentroid,
1034
1110
  };
1035
1111
  }
1036
1112
 
@@ -1044,14 +1120,14 @@ export function verifyPointInPath(segments, testPoint) {
1044
1120
  export function verifyPathBoundingBox(segments, samples = 100) {
1045
1121
  // WHY: Validate input to prevent undefined behavior and provide clear error messages
1046
1122
  if (!segments || !Array.isArray(segments)) {
1047
- throw new Error('verifyPathBoundingBox: segments must be an array');
1123
+ throw new Error("verifyPathBoundingBox: segments must be an array");
1048
1124
  }
1049
1125
 
1050
1126
  const bbox = pathBoundingBox(segments);
1051
1127
  const errors = [];
1052
1128
  let allInside = true;
1053
1129
 
1054
- const tolerance = new Decimal('1e-40');
1130
+ const tolerance = new Decimal("1e-40");
1055
1131
 
1056
1132
  for (let segIdx = 0; segIdx < segments.length; segIdx++) {
1057
1133
  const pts = segments[segIdx];
@@ -1061,12 +1137,16 @@ export function verifyPathBoundingBox(segments, samples = 100) {
1061
1137
  const [x, y] = bezierPoint(pts, t);
1062
1138
 
1063
1139
  if (x.lt(bbox.xmin.minus(tolerance)) || x.gt(bbox.xmax.plus(tolerance))) {
1064
- errors.push(`Segment ${segIdx}, t=${t}: x=${x} outside [${bbox.xmin}, ${bbox.xmax}]`);
1140
+ errors.push(
1141
+ `Segment ${segIdx}, t=${t}: x=${x} outside [${bbox.xmin}, ${bbox.xmax}]`,
1142
+ );
1065
1143
  allInside = false;
1066
1144
  }
1067
1145
 
1068
1146
  if (y.lt(bbox.ymin.minus(tolerance)) || y.gt(bbox.ymax.plus(tolerance))) {
1069
- errors.push(`Segment ${segIdx}, t=${t}: y=${y} outside [${bbox.ymin}, ${bbox.ymax}]`);
1147
+ errors.push(
1148
+ `Segment ${segIdx}, t=${t}: y=${y} outside [${bbox.ymin}, ${bbox.ymax}]`,
1149
+ );
1070
1150
  allInside = false;
1071
1151
  }
1072
1152
  }
@@ -1076,7 +1156,7 @@ export function verifyPathBoundingBox(segments, samples = 100) {
1076
1156
  valid: errors.length === 0,
1077
1157
  bbox,
1078
1158
  allInside,
1079
- errors
1159
+ errors,
1080
1160
  };
1081
1161
  }
1082
1162
 
@@ -1089,7 +1169,7 @@ export function verifyPathBoundingBox(segments, samples = 100) {
1089
1169
  export function verifyPathContinuity(segments) {
1090
1170
  // WHY: Validate input to prevent undefined behavior and provide clear error messages
1091
1171
  if (!segments || !Array.isArray(segments)) {
1092
- throw new Error('verifyPathContinuity: segments must be an array');
1172
+ throw new Error("verifyPathContinuity: segments must be an array");
1093
1173
  }
1094
1174
 
1095
1175
  const { continuous, gaps } = isPathContinuous(segments);
@@ -1114,7 +1194,7 @@ export function verifyPathContinuity(segments) {
1114
1194
  valid: allValid,
1115
1195
  continuous,
1116
1196
  gaps,
1117
- maxGap
1197
+ maxGap,
1118
1198
  };
1119
1199
  }
1120
1200
 
@@ -1128,7 +1208,7 @@ export function verifyPathContinuity(segments) {
1128
1208
  export function verifyPathLength(segments) {
1129
1209
  // WHY: Validate input to prevent undefined behavior and provide clear error messages
1130
1210
  if (!segments || !Array.isArray(segments)) {
1131
- throw new Error('verifyPathLength: segments must be an array');
1211
+ throw new Error("verifyPathLength: segments must be an array");
1132
1212
  }
1133
1213
 
1134
1214
  const totalArcLength = pathLength(segments);
@@ -1147,7 +1227,7 @@ export function verifyPathLength(segments) {
1147
1227
  valid: totalArcLength.gte(chordSum),
1148
1228
  arcLength: totalArcLength,
1149
1229
  chordSum,
1150
- ratio // Should be >= 1
1230
+ ratio, // Should be >= 1
1151
1231
  };
1152
1232
  }
1153
1233
 
@@ -1158,10 +1238,10 @@ export function verifyPathLength(segments) {
1158
1238
  * @param {Object} [options] - Options
1159
1239
  * @returns {{valid: boolean, results: Object}}
1160
1240
  */
1161
- export function verifyAllPathFunctions(segments, options = {}) {
1241
+ export function verifyAllPathFunctions(segments, _options = {}) {
1162
1242
  // WHY: Validate input to prevent undefined behavior and provide clear error messages
1163
1243
  if (!segments || !Array.isArray(segments)) {
1164
- throw new Error('verifyAllPathFunctions: segments must be an array');
1244
+ throw new Error("verifyAllPathFunctions: segments must be an array");
1165
1245
  }
1166
1246
 
1167
1247
  const results = {};
@@ -1192,11 +1272,11 @@ export function verifyAllPathFunctions(segments, options = {}) {
1192
1272
  results.pointInPath = verifyPointInPath(segments, [centerX, centerY]);
1193
1273
  }
1194
1274
 
1195
- const allValid = Object.values(results).every(r => r.valid);
1275
+ const allValid = Object.values(results).every((r) => r.valid);
1196
1276
 
1197
1277
  return {
1198
1278
  valid: allValid,
1199
- results
1279
+ results,
1200
1280
  };
1201
1281
  }
1202
1282
 
@@ -1237,5 +1317,5 @@ export default {
1237
1317
  verifyPathBoundingBox,
1238
1318
  verifyPathContinuity,
1239
1319
  verifyPathLength,
1240
- verifyAllPathFunctions
1320
+ verifyAllPathFunctions,
1241
1321
  };