@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
@@ -14,15 +14,15 @@
14
14
  * @module verification
15
15
  */
16
16
 
17
- import Decimal from 'decimal.js';
18
- import { Matrix } from './matrix.js';
19
- import { Vector } from './vector.js';
20
- import * as Transforms2D from './transforms2d.js';
17
+ import Decimal from "decimal.js";
18
+ import { Matrix as _Matrix } from "./matrix.js";
19
+ import { Vector as _Vector } from "./vector.js";
20
+ import * as Transforms2D from "./transforms2d.js";
21
21
 
22
22
  // Use high precision for verifications
23
23
  Decimal.set({ precision: 80 });
24
24
 
25
- const D = x => (x instanceof Decimal ? x : new Decimal(x));
25
+ const D = (x) => (x instanceof Decimal ? x : new Decimal(x));
26
26
  const ZERO = new Decimal(0);
27
27
  const ONE = new Decimal(1);
28
28
 
@@ -81,8 +81,8 @@ export function verifyTransformRoundTrip(matrix, x, y) {
81
81
  valid: false,
82
82
  error: new Decimal(Infinity),
83
83
  tolerance,
84
- message: 'Matrix is not invertible (determinant = 0)',
85
- details: { determinant: matrix.determinant() }
84
+ message: "Matrix is not invertible (determinant = 0)",
85
+ details: { determinant: matrix.determinant() },
86
86
  };
87
87
  }
88
88
 
@@ -108,15 +108,15 @@ export function verifyTransformRoundTrip(matrix, x, y) {
108
108
  transformed: { x: fwdX.toString(), y: fwdY.toString() },
109
109
  recovered: { x: revX.toString(), y: revY.toString() },
110
110
  errorX: errorX.toExponential(),
111
- errorY: errorY.toExponential()
112
- }
111
+ errorY: errorY.toExponential(),
112
+ },
113
113
  };
114
114
  } catch (e) {
115
115
  return {
116
116
  valid: false,
117
117
  error: new Decimal(Infinity),
118
118
  tolerance,
119
- message: `Verification error: ${e.message}`
119
+ message: `Verification error: ${e.message}`,
120
120
  };
121
121
  }
122
122
  }
@@ -140,7 +140,7 @@ export function verifyTransformGeometry(matrix, points) {
140
140
  valid: false,
141
141
  error: ZERO,
142
142
  tolerance,
143
- message: 'Need at least 3 points for geometry verification'
143
+ message: "Need at least 3 points for geometry verification",
144
144
  };
145
145
  }
146
146
 
@@ -149,27 +149,43 @@ export function verifyTransformGeometry(matrix, points) {
149
149
  const absdet = det.abs();
150
150
 
151
151
  // Transform all points
152
- const transformed = points.map(p => {
152
+ const transformed = points.map((p) => {
153
153
  const [tx, ty] = Transforms2D.applyTransform(matrix, D(p.x), D(p.y));
154
154
  return { x: tx, y: ty };
155
155
  });
156
156
 
157
157
  // Verify area scaling (using first 3 points as triangle)
158
158
  const origArea = triangleArea(points[0], points[1], points[2]);
159
- const transArea = triangleArea(transformed[0], transformed[1], transformed[2]);
159
+ const transArea = triangleArea(
160
+ transformed[0],
161
+ transformed[1],
162
+ transformed[2],
163
+ );
160
164
 
161
165
  // Expected transformed area = |det| * original area
162
166
  const expectedArea = absdet.times(origArea);
163
167
  const areaError = transArea.minus(expectedArea).abs();
164
- const relativeAreaError = origArea.isZero() ? areaError : areaError.div(origArea);
168
+ const relativeAreaError = origArea.isZero()
169
+ ? areaError
170
+ : areaError.div(origArea);
165
171
 
166
172
  const areaValid = relativeAreaError.lessThan(tolerance);
167
173
 
168
174
  // Verify collinearity preservation (if 3+ points are collinear, they should remain so)
169
175
  let collinearityValid = true;
170
176
  if (points.length >= 3) {
171
- const origCollinear = areCollinear(points[0], points[1], points[2], tolerance);
172
- const transCollinear = areCollinear(transformed[0], transformed[1], transformed[2], tolerance);
177
+ const origCollinear = areCollinear(
178
+ points[0],
179
+ points[1],
180
+ points[2],
181
+ tolerance,
182
+ );
183
+ const transCollinear = areCollinear(
184
+ transformed[0],
185
+ transformed[1],
186
+ transformed[2],
187
+ tolerance,
188
+ );
173
189
  collinearityValid = origCollinear === transCollinear;
174
190
  }
175
191
 
@@ -181,23 +197,23 @@ export function verifyTransformGeometry(matrix, points) {
181
197
  error,
182
198
  tolerance,
183
199
  message: valid
184
- ? 'Geometric properties preserved'
185
- : `Geometry verification FAILED: ${!areaValid ? 'area scaling incorrect' : 'collinearity not preserved'}`,
200
+ ? "Geometric properties preserved"
201
+ : `Geometry verification FAILED: ${!areaValid ? "area scaling incorrect" : "collinearity not preserved"}`,
186
202
  details: {
187
203
  determinant: det.toString(),
188
204
  originalArea: origArea.toString(),
189
205
  transformedArea: transArea.toString(),
190
206
  expectedArea: expectedArea.toString(),
191
207
  areaError: relativeAreaError.toExponential(),
192
- collinearityPreserved: collinearityValid
193
- }
208
+ collinearityPreserved: collinearityValid,
209
+ },
194
210
  };
195
211
  } catch (e) {
196
212
  return {
197
213
  valid: false,
198
214
  error: new Decimal(Infinity),
199
215
  tolerance,
200
- message: `Verification error: ${e.message}`
216
+ message: `Verification error: ${e.message}`,
201
217
  };
202
218
  }
203
219
  }
@@ -225,8 +241,8 @@ export function verifyMatrixInversion(matrix) {
225
241
  valid: false,
226
242
  error: new Decimal(Infinity),
227
243
  tolerance,
228
- message: 'Matrix is singular (not invertible)',
229
- details: { determinant: matrix.determinant().toString() }
244
+ message: "Matrix is singular (not invertible)",
245
+ details: { determinant: matrix.determinant().toString() },
230
246
  };
231
247
  }
232
248
 
@@ -250,7 +266,13 @@ export function verifyMatrixInversion(matrix) {
250
266
  }
251
267
 
252
268
  if (error.greaterThanOrEqualTo(tolerance)) {
253
- errors.push({ row: i, col: j, expected: expected.toString(), actual: actual.toString(), error: error.toExponential() });
269
+ errors.push({
270
+ row: i,
271
+ col: j,
272
+ expected: expected.toString(),
273
+ actual: actual.toString(),
274
+ error: error.toExponential(),
275
+ });
254
276
  }
255
277
  }
256
278
  }
@@ -267,15 +289,15 @@ export function verifyMatrixInversion(matrix) {
267
289
  details: {
268
290
  matrixSize: `${n}x${n}`,
269
291
  maxError: maxError.toExponential(),
270
- failedElements: errors.slice(0, 5) // First 5 failures
271
- }
292
+ failedElements: errors.slice(0, 5), // First 5 failures
293
+ },
272
294
  };
273
295
  } catch (e) {
274
296
  return {
275
297
  valid: false,
276
298
  error: new Decimal(Infinity),
277
299
  tolerance,
278
- message: `Verification error: ${e.message}`
300
+ message: `Verification error: ${e.message}`,
279
301
  };
280
302
  }
281
303
  }
@@ -320,14 +342,14 @@ export function verifyMultiplicationAssociativity(A, B, C) {
320
342
  tolerance,
321
343
  message: valid
322
344
  ? `Associativity verified: (A*B)*C = A*(B*C), max error ${maxError.toExponential()}`
323
- : `Associativity FAILED: max error ${maxError.toExponential()}`
345
+ : `Associativity FAILED: max error ${maxError.toExponential()}`,
324
346
  };
325
347
  } catch (e) {
326
348
  return {
327
349
  valid: false,
328
350
  error: new Decimal(Infinity),
329
351
  tolerance,
330
- message: `Verification error: ${e.message}`
352
+ message: `Verification error: ${e.message}`,
331
353
  };
332
354
  }
333
355
  }
@@ -349,17 +371,21 @@ export function verifyMultiplicationAssociativity(A, B, C) {
349
371
  * @param {Decimal} [distanceTolerance] - Max distance outside allowed (default: 1e-6)
350
372
  * @returns {VerificationResult} Verification result
351
373
  */
352
- export function verifyPolygonContainment(inner, outer, distanceTolerance = null) {
374
+ export function verifyPolygonContainment(
375
+ inner,
376
+ outer,
377
+ distanceTolerance = null,
378
+ ) {
353
379
  const tolerance = computeTolerance();
354
380
  // Distance tolerance for curve approximation - points can be slightly outside
355
- const maxDistOutside = distanceTolerance || new Decimal('1e-6');
381
+ const maxDistOutside = distanceTolerance || new Decimal("1e-6");
356
382
 
357
383
  if (inner.length < 3 || outer.length < 3) {
358
384
  return {
359
385
  valid: false,
360
386
  error: ZERO,
361
387
  tolerance,
362
- message: 'Polygons must have at least 3 vertices'
388
+ message: "Polygons must have at least 3 vertices",
363
389
  };
364
390
  }
365
391
 
@@ -385,7 +411,7 @@ export function verifyPolygonContainment(inner, outer, distanceTolerance = null)
385
411
  index: i,
386
412
  x: point.x.toString(),
387
413
  y: point.y.toString(),
388
- distanceOutside: distToEdge.toExponential()
414
+ distanceOutside: distToEdge.toExponential(),
389
415
  });
390
416
  }
391
417
  }
@@ -402,15 +428,15 @@ export function verifyPolygonContainment(inner, outer, distanceTolerance = null)
402
428
  innerVertices: inner.length,
403
429
  outerVertices: outer.length,
404
430
  maxOutsideDistance: maxOutsideDistance.toExponential(),
405
- outsidePoints: outsidePoints.slice(0, 5)
406
- }
431
+ outsidePoints: outsidePoints.slice(0, 5),
432
+ },
407
433
  };
408
434
  } catch (e) {
409
435
  return {
410
436
  valid: false,
411
437
  error: new Decimal(Infinity),
412
438
  tolerance,
413
- message: `Verification error: ${e.message}`
439
+ message: `Verification error: ${e.message}`,
414
440
  };
415
441
  }
416
442
  }
@@ -418,16 +444,23 @@ export function verifyPolygonContainment(inner, outer, distanceTolerance = null)
418
444
  /**
419
445
  * Compute minimum distance from a point to the edges of a polygon.
420
446
  * @private
447
+ * @param {{x: Decimal|number, y: Decimal|number}} point - Point to measure from
448
+ * @param {Array<{x: Decimal|number, y: Decimal|number}>} polygon - Polygon vertices
449
+ * @returns {Decimal} Minimum distance to any polygon edge
421
450
  */
422
451
  function minDistanceToPolygonEdge(point, polygon) {
423
452
  let minDist = new Decimal(Infinity);
424
- const px = D(point.x), py = D(point.y);
453
+ const px = D(point.x),
454
+ py = D(point.y);
425
455
 
426
456
  for (let i = 0; i < polygon.length; i++) {
427
457
  const j = (i + 1) % polygon.length;
428
- const p1 = polygon[i], p2 = polygon[j];
429
- const x1 = D(p1.x), y1 = D(p1.y);
430
- const x2 = D(p2.x), y2 = D(p2.y);
458
+ const p1 = polygon[i],
459
+ p2 = polygon[j];
460
+ const x1 = D(p1.x),
461
+ y1 = D(p1.y);
462
+ const x2 = D(p2.x),
463
+ y2 = D(p2.y);
431
464
 
432
465
  // Distance from point to line segment [p1, p2]
433
466
  const dx = x2.minus(x1);
@@ -440,9 +473,13 @@ function minDistanceToPolygonEdge(point, polygon) {
440
473
  dist = pointDistance(point, p1);
441
474
  } else {
442
475
  // Project point onto line, clamp to segment
443
- const t = Decimal.max(ZERO, Decimal.min(ONE,
444
- px.minus(x1).times(dx).plus(py.minus(y1).times(dy)).div(lenSq)
445
- ));
476
+ const t = Decimal.max(
477
+ ZERO,
478
+ Decimal.min(
479
+ ONE,
480
+ px.minus(x1).times(dx).plus(py.minus(y1).times(dy)).div(lenSq),
481
+ ),
482
+ );
446
483
  const projX = x1.plus(t.times(dx));
447
484
  const projY = y1.plus(t.times(dy));
448
485
  dist = px.minus(projX).pow(2).plus(py.minus(projY).pow(2)).sqrt();
@@ -476,8 +513,8 @@ export function verifyPolygonIntersection(poly1, poly2, intersection) {
476
513
  valid: true,
477
514
  error: ZERO,
478
515
  tolerance,
479
- message: 'Intersection is empty or degenerate (valid result)',
480
- details: { intersectionVertices: intersection.length }
516
+ message: "Intersection is empty or degenerate (valid result)",
517
+ details: { intersectionVertices: intersection.length },
481
518
  };
482
519
  }
483
520
 
@@ -494,7 +531,9 @@ export function verifyPolygonIntersection(poly1, poly2, intersection) {
494
531
  const minArea = Decimal.min(area1, area2);
495
532
 
496
533
  // Allow small tolerance for floating point in area calculation
497
- const areaValid = areaInt.lessThanOrEqualTo(minArea.times(ONE.plus(tolerance)));
534
+ const areaValid = areaInt.lessThanOrEqualTo(
535
+ minArea.times(ONE.plus(tolerance)),
536
+ );
498
537
 
499
538
  const valid = containment1.valid && containment2.valid && areaValid;
500
539
 
@@ -503,23 +542,23 @@ export function verifyPolygonIntersection(poly1, poly2, intersection) {
503
542
  error: valid ? ZERO : ONE,
504
543
  tolerance,
505
544
  message: valid
506
- ? 'Intersection verified: contained in both polygons, area valid'
507
- : `Intersection FAILED: ${!containment1.valid ? 'not in poly1, ' : ''}${!containment2.valid ? 'not in poly2, ' : ''}${!areaValid ? 'area too large' : ''}`,
545
+ ? "Intersection verified: contained in both polygons, area valid"
546
+ : `Intersection FAILED: ${!containment1.valid ? "not in poly1, " : ""}${!containment2.valid ? "not in poly2, " : ""}${!areaValid ? "area too large" : ""}`,
508
547
  details: {
509
548
  containedInPoly1: containment1.valid,
510
549
  containedInPoly2: containment2.valid,
511
550
  area1: area1.toString(),
512
551
  area2: area2.toString(),
513
552
  intersectionArea: areaInt.toString(),
514
- areaValid
515
- }
553
+ areaValid,
554
+ },
516
555
  };
517
556
  } catch (e) {
518
557
  return {
519
558
  valid: false,
520
559
  error: new Decimal(Infinity),
521
560
  tolerance,
522
- message: `Verification error: ${e.message}`
561
+ message: `Verification error: ${e.message}`,
523
562
  };
524
563
  }
525
564
  }
@@ -540,15 +579,17 @@ export function verifyPolygonIntersection(poly1, poly2, intersection) {
540
579
  */
541
580
  export function verifyCircleToPath(cx, cy, r, pathData) {
542
581
  const tolerance = computeTolerance();
543
- const cxD = D(cx), cyD = D(cy), rD = D(r);
582
+ const cxD = D(cx),
583
+ cyD = D(cy),
584
+ rD = D(r);
544
585
 
545
586
  try {
546
587
  // Expected key points (cardinal points)
547
588
  const expectedPoints = [
548
- { x: cxD.plus(rD), y: cyD, name: 'right' },
549
- { x: cxD, y: cyD.plus(rD), name: 'bottom' },
550
- { x: cxD.minus(rD), y: cyD, name: 'left' },
551
- { x: cxD, y: cyD.minus(rD), name: 'top' }
589
+ { x: cxD.plus(rD), y: cyD, name: "right" },
590
+ { x: cxD, y: cyD.plus(rD), name: "bottom" },
591
+ { x: cxD.minus(rD), y: cyD, name: "left" },
592
+ { x: cxD, y: cyD.minus(rD), name: "top" },
552
593
  ];
553
594
 
554
595
  // Extract points from path data
@@ -566,7 +607,10 @@ export function verifyCircleToPath(cx, cy, r, pathData) {
566
607
  maxError = error;
567
608
  }
568
609
  if (error.greaterThanOrEqualTo(tolerance)) {
569
- missingPoints.push({ ...expected, nearestError: error.toExponential() });
610
+ missingPoints.push({
611
+ ...expected,
612
+ nearestError: error.toExponential(),
613
+ });
570
614
  }
571
615
  } else {
572
616
  missingPoints.push(expected);
@@ -587,15 +631,15 @@ export function verifyCircleToPath(cx, cy, r, pathData) {
587
631
  center: { x: cxD.toString(), y: cyD.toString() },
588
632
  radius: rD.toString(),
589
633
  pathPointCount: pathPoints.length,
590
- missingPoints: missingPoints.map(p => p.name || `(${p.x}, ${p.y})`)
591
- }
634
+ missingPoints: missingPoints.map((p) => p.name || `(${p.x}, ${p.y})`),
635
+ },
592
636
  };
593
637
  } catch (e) {
594
638
  return {
595
639
  valid: false,
596
640
  error: new Decimal(Infinity),
597
641
  tolerance,
598
- message: `Verification error: ${e.message}`
642
+ message: `Verification error: ${e.message}`,
599
643
  };
600
644
  }
601
645
  }
@@ -612,15 +656,18 @@ export function verifyCircleToPath(cx, cy, r, pathData) {
612
656
  */
613
657
  export function verifyRectToPath(x, y, width, height, pathData) {
614
658
  const tolerance = computeTolerance();
615
- const xD = D(x), yD = D(y), wD = D(width), hD = D(height);
659
+ const xD = D(x),
660
+ yD = D(y),
661
+ wD = D(width),
662
+ hD = D(height);
616
663
 
617
664
  try {
618
665
  // Expected corners
619
666
  const expectedCorners = [
620
- { x: xD, y: yD, name: 'top-left' },
621
- { x: xD.plus(wD), y: yD, name: 'top-right' },
622
- { x: xD.plus(wD), y: yD.plus(hD), name: 'bottom-right' },
623
- { x: xD, y: yD.plus(hD), name: 'bottom-left' }
667
+ { x: xD, y: yD, name: "top-left" },
668
+ { x: xD.plus(wD), y: yD, name: "top-right" },
669
+ { x: xD.plus(wD), y: yD.plus(hD), name: "bottom-right" },
670
+ { x: xD, y: yD.plus(hD), name: "bottom-left" },
624
671
  ];
625
672
 
626
673
  const pathPoints = extractPathPoints(pathData);
@@ -636,7 +683,10 @@ export function verifyRectToPath(x, y, width, height, pathData) {
636
683
  maxError = error;
637
684
  }
638
685
  if (error.greaterThanOrEqualTo(tolerance)) {
639
- missingCorners.push({ ...corner, nearestError: error.toExponential() });
686
+ missingCorners.push({
687
+ ...corner,
688
+ nearestError: error.toExponential(),
689
+ });
640
690
  }
641
691
  } else {
642
692
  missingCorners.push(corner);
@@ -654,17 +704,22 @@ export function verifyRectToPath(x, y, width, height, pathData) {
654
704
  ? `Rect to path verified: all corners present, max error ${maxError.toExponential()}`
655
705
  : `Rect to path FAILED: ${missingCorners.length} corners missing or inaccurate`,
656
706
  details: {
657
- rect: { x: xD.toString(), y: yD.toString(), width: wD.toString(), height: hD.toString() },
707
+ rect: {
708
+ x: xD.toString(),
709
+ y: yD.toString(),
710
+ width: wD.toString(),
711
+ height: hD.toString(),
712
+ },
658
713
  pathPointCount: pathPoints.length,
659
- missingCorners: missingCorners.map(c => c.name)
660
- }
714
+ missingCorners: missingCorners.map((c) => c.name),
715
+ },
661
716
  };
662
717
  } catch (e) {
663
718
  return {
664
719
  valid: false,
665
720
  error: new Decimal(Infinity),
666
721
  tolerance,
667
- message: `Verification error: ${e.message}`
722
+ message: `Verification error: ${e.message}`,
668
723
  };
669
724
  }
670
725
  }
@@ -687,8 +742,16 @@ export function verifyLinearGradientTransform(original, baked, matrix) {
687
742
 
688
743
  try {
689
744
  // Transform original points using the provided matrix
690
- const [expX1, expY1] = Transforms2D.applyTransform(matrix, D(original.x1 || 0), D(original.y1 || 0));
691
- const [expX2, expY2] = Transforms2D.applyTransform(matrix, D(original.x2 || 1), D(original.y2 || 0));
745
+ const [expX1, expY1] = Transforms2D.applyTransform(
746
+ matrix,
747
+ D(original.x1 || 0),
748
+ D(original.y1 || 0),
749
+ );
750
+ const [expX2, expY2] = Transforms2D.applyTransform(
751
+ matrix,
752
+ D(original.x2 || 1),
753
+ D(original.y2 || 0),
754
+ );
692
755
 
693
756
  // Compare with baked values
694
757
  const errorX1 = D(baked.x1).minus(expX1).abs();
@@ -707,16 +770,21 @@ export function verifyLinearGradientTransform(original, baked, matrix) {
707
770
  ? `Linear gradient transform verified: max error ${maxError.toExponential()}`
708
771
  : `Linear gradient transform FAILED: max error ${maxError.toExponential()}`,
709
772
  details: {
710
- expected: { x1: expX1.toString(), y1: expY1.toString(), x2: expX2.toString(), y2: expY2.toString() },
711
- actual: baked
712
- }
773
+ expected: {
774
+ x1: expX1.toString(),
775
+ y1: expY1.toString(),
776
+ x2: expX2.toString(),
777
+ y2: expY2.toString(),
778
+ },
779
+ actual: baked,
780
+ },
713
781
  };
714
782
  } catch (e) {
715
783
  return {
716
784
  valid: false,
717
785
  error: new Decimal(Infinity),
718
786
  tolerance,
719
- message: `Verification error: ${e.message}`
787
+ message: `Verification error: ${e.message}`,
720
788
  };
721
789
  }
722
790
  }
@@ -728,14 +796,22 @@ export function verifyLinearGradientTransform(original, baked, matrix) {
728
796
  /**
729
797
  * Compute signed area of triangle using cross product.
730
798
  * @private
799
+ * @param {{x: Decimal|number, y: Decimal|number}} p1 - First triangle vertex
800
+ * @param {{x: Decimal|number, y: Decimal|number}} p2 - Second triangle vertex
801
+ * @param {{x: Decimal|number, y: Decimal|number}} p3 - Third triangle vertex
802
+ * @returns {Decimal} Signed area of the triangle
731
803
  */
732
804
  function triangleArea(p1, p2, p3) {
733
- const x1 = D(p1.x), y1 = D(p1.y);
734
- const x2 = D(p2.x), y2 = D(p2.y);
735
- const x3 = D(p3.x), y3 = D(p3.y);
805
+ const x1 = D(p1.x),
806
+ y1 = D(p1.y);
807
+ const x2 = D(p2.x),
808
+ y2 = D(p2.y);
809
+ const x3 = D(p3.x),
810
+ y3 = D(p3.y);
736
811
 
737
812
  // Area = 0.5 * |x1(y2-y3) + x2(y3-y1) + x3(y1-y2)|
738
- const area = x1.times(y2.minus(y3))
813
+ const area = x1
814
+ .times(y2.minus(y3))
739
815
  .plus(x2.times(y3.minus(y1)))
740
816
  .plus(x3.times(y1.minus(y2)))
741
817
  .abs()
@@ -747,6 +823,11 @@ function triangleArea(p1, p2, p3) {
747
823
  /**
748
824
  * Check if three points are collinear.
749
825
  * @private
826
+ * @param {{x: Decimal|number, y: Decimal|number}} p1 - First point
827
+ * @param {{x: Decimal|number, y: Decimal|number}} p2 - Second point
828
+ * @param {{x: Decimal|number, y: Decimal|number}} p3 - Third point
829
+ * @param {Decimal|number} tolerance - Collinearity tolerance
830
+ * @returns {boolean} True if points are collinear within tolerance
750
831
  */
751
832
  function areCollinear(p1, p2, p3, tolerance) {
752
833
  const area = triangleArea(p1, p2, p3);
@@ -756,6 +837,8 @@ function areCollinear(p1, p2, p3, tolerance) {
756
837
  /**
757
838
  * Compute signed area of a polygon using shoelace formula.
758
839
  * @private
840
+ * @param {Array<{x: Decimal|number, y: Decimal|number}>} polygon - Polygon vertices
841
+ * @returns {Decimal} Absolute area of the polygon
759
842
  */
760
843
  function polygonArea(polygon) {
761
844
  if (polygon.length < 3) return ZERO;
@@ -765,8 +848,10 @@ function polygonArea(polygon) {
765
848
 
766
849
  for (let i = 0; i < n; i++) {
767
850
  const j = (i + 1) % n;
768
- const xi = D(polygon[i].x), yi = D(polygon[i].y);
769
- const xj = D(polygon[j].x), yj = D(polygon[j].y);
851
+ const xi = D(polygon[i].x),
852
+ yi = D(polygon[i].y);
853
+ const xj = D(polygon[j].x),
854
+ yj = D(polygon[j].y);
770
855
  area = area.plus(xi.times(yj).minus(xj.times(yi)));
771
856
  }
772
857
 
@@ -776,22 +861,29 @@ function polygonArea(polygon) {
776
861
  /**
777
862
  * Check if point is inside polygon using ray casting.
778
863
  * @private
864
+ * @param {{x: Decimal|number, y: Decimal|number}} point - Point to test
865
+ * @param {Array<{x: Decimal|number, y: Decimal|number}>} polygon - Polygon vertices
866
+ * @returns {boolean} True if point is inside or on polygon boundary
779
867
  */
780
868
  function isPointInPolygon(point, polygon) {
781
- const px = D(point.x), py = D(point.y);
869
+ const px = D(point.x),
870
+ py = D(point.y);
782
871
  const n = polygon.length;
783
872
  let inside = false;
784
873
 
785
874
  for (let i = 0, j = n - 1; i < n; j = i++) {
786
- const xi = D(polygon[i].x), yi = D(polygon[i].y);
787
- const xj = D(polygon[j].x), yj = D(polygon[j].y);
875
+ const xi = D(polygon[i].x),
876
+ yi = D(polygon[i].y);
877
+ const xj = D(polygon[j].x),
878
+ yj = D(polygon[j].y);
788
879
 
789
880
  // Check if point is on the edge (with tolerance)
790
881
  const onEdge = isPointOnSegment(point, polygon[i], polygon[j]);
791
882
  if (onEdge) return true;
792
883
 
793
884
  // Ray casting
794
- const intersect = yi.greaterThan(py) !== yj.greaterThan(py) &&
885
+ const intersect =
886
+ yi.greaterThan(py) !== yj.greaterThan(py) &&
795
887
  px.lessThan(xj.minus(xi).times(py.minus(yi)).div(yj.minus(yi)).plus(xi));
796
888
 
797
889
  if (intersect) inside = !inside;
@@ -803,12 +895,19 @@ function isPointInPolygon(point, polygon) {
803
895
  /**
804
896
  * Check if point is on line segment.
805
897
  * @private
898
+ * @param {{x: Decimal|number, y: Decimal|number}} point - Point to test
899
+ * @param {{x: Decimal|number, y: Decimal|number}} segStart - Segment start point
900
+ * @param {{x: Decimal|number, y: Decimal|number}} segEnd - Segment end point
901
+ * @returns {boolean} True if point is on the segment within tolerance
806
902
  */
807
903
  function isPointOnSegment(point, segStart, segEnd) {
808
904
  const tolerance = computeTolerance();
809
- const px = D(point.x), py = D(point.y);
810
- const x1 = D(segStart.x), y1 = D(segStart.y);
811
- const x2 = D(segEnd.x), y2 = D(segEnd.y);
905
+ const px = D(point.x),
906
+ py = D(point.y);
907
+ const x1 = D(segStart.x),
908
+ y1 = D(segStart.y);
909
+ const x2 = D(segEnd.x),
910
+ y2 = D(segEnd.y);
812
911
 
813
912
  // Check if point is within bounding box
814
913
  const minX = Decimal.min(x1, x2).minus(tolerance);
@@ -816,12 +915,21 @@ function isPointOnSegment(point, segStart, segEnd) {
816
915
  const minY = Decimal.min(y1, y2).minus(tolerance);
817
916
  const maxY = Decimal.max(y1, y2).plus(tolerance);
818
917
 
819
- if (px.lessThan(minX) || px.greaterThan(maxX) || py.lessThan(minY) || py.greaterThan(maxY)) {
918
+ if (
919
+ px.lessThan(minX) ||
920
+ px.greaterThan(maxX) ||
921
+ py.lessThan(minY) ||
922
+ py.greaterThan(maxY)
923
+ ) {
820
924
  return false;
821
925
  }
822
926
 
823
927
  // Check collinearity (cross product should be ~0)
824
- const cross = x2.minus(x1).times(py.minus(y1)).minus(y2.minus(y1).times(px.minus(x1))).abs();
928
+ const cross = x2
929
+ .minus(x1)
930
+ .times(py.minus(y1))
931
+ .minus(y2.minus(y1).times(px.minus(x1)))
932
+ .abs();
825
933
  const segLength = pointDistance(segStart, segEnd);
826
934
 
827
935
  return cross.div(segLength.plus(tolerance)).lessThan(tolerance);
@@ -830,6 +938,9 @@ function isPointOnSegment(point, segStart, segEnd) {
830
938
  /**
831
939
  * Compute distance between two points.
832
940
  * @private
941
+ * @param {{x: Decimal|number, y: Decimal|number}} p1 - First point
942
+ * @param {{x: Decimal|number, y: Decimal|number}} p2 - Second point
943
+ * @returns {Decimal} Euclidean distance between the points
833
944
  */
834
945
  function pointDistance(p1, p2) {
835
946
  const dx = D(p2.x).minus(D(p1.x));
@@ -840,11 +951,14 @@ function pointDistance(p1, p2) {
840
951
  /**
841
952
  * Extract coordinate points from SVG path data.
842
953
  * @private
954
+ * @param {string} pathData - SVG path data string
955
+ * @returns {Array<{x: Decimal, y: Decimal}>} Array of extracted coordinate points
843
956
  */
844
957
  function extractPathPoints(pathData) {
845
958
  const points = [];
846
959
  // Match all number pairs in path data using matchAll
847
- const regex = /([+-]?\d*\.?\d+(?:[eE][+-]?\d+)?)[,\s]+([+-]?\d*\.?\d+(?:[eE][+-]?\d+)?)/g;
960
+ const regex =
961
+ /([+-]?\d*\.?\d+(?:[eE][+-]?\d+)?)[,\s]+([+-]?\d*\.?\d+(?:[eE][+-]?\d+)?)/g;
848
962
  const matches = pathData.matchAll(regex);
849
963
 
850
964
  for (const match of matches) {
@@ -857,6 +971,9 @@ function extractPathPoints(pathData) {
857
971
  /**
858
972
  * Find the nearest point in a list to a target point.
859
973
  * @private
974
+ * @param {{x: Decimal|number, y: Decimal|number}} target - Target point
975
+ * @param {Array<{x: Decimal|number, y: Decimal|number}>} points - Array of points to search
976
+ * @returns {{x: Decimal, y: Decimal}|null} Nearest point or null if array is empty
860
977
  */
861
978
  function findNearestPoint(target, points) {
862
979
  if (points.length === 0) return null;
@@ -914,7 +1031,9 @@ export function computePolygonDifference(subject, clip) {
914
1031
  outsidePoints.push(current);
915
1032
  if (!nextOutside) {
916
1033
  // Crossing from outside to inside - add intersection point
917
- outsidePoints.push(lineIntersectE2E(current, next, edgeStart, edgeEnd));
1034
+ outsidePoints.push(
1035
+ lineIntersectE2E(current, next, edgeStart, edgeEnd),
1036
+ );
918
1037
  }
919
1038
  } else if (nextOutside) {
920
1039
  // Crossing from inside to outside - add intersection point
@@ -957,9 +1076,17 @@ export function computePolygonDifference(subject, clip) {
957
1076
  * @param {string|Decimal} [customTolerance='1e-10'] - Custom tolerance (string or Decimal)
958
1077
  * @returns {VerificationResult} Verification result
959
1078
  */
960
- export function verifyClipPathE2E(original, clipped, outsideFragments = [], customTolerance = '1e-10') {
1079
+ export function verifyClipPathE2E(
1080
+ original,
1081
+ clipped,
1082
+ outsideFragments = [],
1083
+ customTolerance = "1e-10",
1084
+ ) {
961
1085
  // Use configurable tolerance - higher clipSegments allows tighter tolerance
962
- const tolerance = customTolerance instanceof Decimal ? customTolerance : new Decimal(customTolerance);
1086
+ const tolerance =
1087
+ customTolerance instanceof Decimal
1088
+ ? customTolerance
1089
+ : new Decimal(customTolerance);
963
1090
  // Ensure outsideFragments is an array
964
1091
  const fragments = outsideFragments || [];
965
1092
 
@@ -976,13 +1103,19 @@ export function verifyClipPathE2E(original, clipped, outsideFragments = [], cust
976
1103
  // 1. Clipped area must be <= original area (intersection property)
977
1104
  // 2. Clipped area must be >= 0 (non-negative area)
978
1105
  // 3. For overlapping polygons, clipped area should be > 0
979
- const clippedValid = clippedArea.lessThanOrEqualTo(originalArea.times(ONE.plus(tolerance)));
980
- const outsideValid = outsideArea.greaterThanOrEqualTo(ZERO.minus(tolerance.times(originalArea)));
1106
+ const clippedValid = clippedArea.lessThanOrEqualTo(
1107
+ originalArea.times(ONE.plus(tolerance)),
1108
+ );
1109
+ const outsideValid = outsideArea.greaterThanOrEqualTo(
1110
+ ZERO.minus(tolerance.times(originalArea)),
1111
+ );
981
1112
 
982
1113
  // The "error" for E2E is how close we are to perfect area conservation
983
1114
  // Since we compute outside = original - clipped, the error is exactly 0 by construction
984
1115
  // What we're really verifying is that the clipped area is reasonable
985
- const areaRatio = originalArea.isZero() ? ONE : clippedArea.div(originalArea);
1116
+ const areaRatio = originalArea.isZero()
1117
+ ? ONE
1118
+ : clippedArea.div(originalArea);
986
1119
  const error = ZERO; // By construction, original = clipped + outside is exact
987
1120
 
988
1121
  const valid = clippedValid && outsideValid;
@@ -997,19 +1130,19 @@ export function verifyClipPathE2E(original, clipped, outsideFragments = [], cust
997
1130
  details: {
998
1131
  originalArea: originalArea.toString(),
999
1132
  clippedArea: clippedArea.toString(),
1000
- outsideArea: outsideArea.toString(), // Computed exactly as original - clipped
1133
+ outsideArea: outsideArea.toString(), // Computed exactly as original - clipped
1001
1134
  areaRatio: areaRatio.toFixed(6),
1002
1135
  fragmentCount: fragments.length,
1003
1136
  clippedValid,
1004
- outsideValid
1005
- }
1137
+ outsideValid,
1138
+ },
1006
1139
  };
1007
1140
  } catch (e) {
1008
1141
  return {
1009
1142
  valid: false,
1010
1143
  error: new Decimal(Infinity),
1011
1144
  tolerance,
1012
- message: `E2E verification error: ${e.message}`
1145
+ message: `E2E verification error: ${e.message}`,
1013
1146
  };
1014
1147
  }
1015
1148
  }
@@ -1033,7 +1166,7 @@ export function verifyPipelineE2E(params) {
1033
1166
  valid: false,
1034
1167
  error: new Decimal(Infinity),
1035
1168
  tolerance,
1036
- message: 'E2E verification failed: point count mismatch'
1169
+ message: "E2E verification failed: point count mismatch",
1037
1170
  };
1038
1171
  }
1039
1172
 
@@ -1049,7 +1182,7 @@ export function verifyPipelineE2E(params) {
1049
1182
  const [expectedX, expectedY] = Transforms2D.applyTransform(
1050
1183
  expectedTransform,
1051
1184
  D(orig.x),
1052
- D(orig.y)
1185
+ D(orig.y),
1053
1186
  );
1054
1187
 
1055
1188
  // Compare with actual flattened point
@@ -1066,7 +1199,7 @@ export function verifyPipelineE2E(params) {
1066
1199
  pointIndex: i,
1067
1200
  expected: { x: expectedX.toString(), y: expectedY.toString() },
1068
1201
  actual: { x: flat.x.toString(), y: flat.y.toString() },
1069
- error: error.toExponential()
1202
+ error: error.toExponential(),
1070
1203
  });
1071
1204
  }
1072
1205
  }
@@ -1083,15 +1216,15 @@ export function verifyPipelineE2E(params) {
1083
1216
  details: {
1084
1217
  pointsChecked: originalPoints.length,
1085
1218
  maxError: maxError.toExponential(),
1086
- failedPoints: errors.slice(0, 5)
1087
- }
1219
+ failedPoints: errors.slice(0, 5),
1220
+ },
1088
1221
  };
1089
1222
  } catch (e) {
1090
1223
  return {
1091
1224
  valid: false,
1092
1225
  error: new Decimal(Infinity),
1093
1226
  tolerance,
1094
- message: `E2E verification error: ${e.message}`
1227
+ message: `E2E verification error: ${e.message}`,
1095
1228
  };
1096
1229
  }
1097
1230
  }
@@ -1116,7 +1249,9 @@ export function verifyPolygonUnionArea(polygons, expectedArea) {
1116
1249
  }
1117
1250
 
1118
1251
  const error = totalArea.minus(D(expectedArea)).abs();
1119
- const relativeError = D(expectedArea).isZero() ? error : error.div(D(expectedArea));
1252
+ const relativeError = D(expectedArea).isZero()
1253
+ ? error
1254
+ : error.div(D(expectedArea));
1120
1255
  const valid = relativeError.lessThan(tolerance);
1121
1256
 
1122
1257
  return {
@@ -1129,20 +1264,28 @@ export function verifyPolygonUnionArea(polygons, expectedArea) {
1129
1264
  details: {
1130
1265
  totalArea: totalArea.toString(),
1131
1266
  expectedArea: expectedArea.toString(),
1132
- polygonCount: polygons.length
1133
- }
1267
+ polygonCount: polygons.length,
1268
+ },
1134
1269
  };
1135
1270
  } catch (e) {
1136
1271
  return {
1137
1272
  valid: false,
1138
1273
  error: new Decimal(Infinity),
1139
1274
  tolerance,
1140
- message: `Union verification error: ${e.message}`
1275
+ message: `Union verification error: ${e.message}`,
1141
1276
  };
1142
1277
  }
1143
1278
  }
1144
1279
 
1145
- // E2E helper: check if point is inside edge (for difference computation)
1280
+ /**
1281
+ * Check if point is inside edge (for difference computation).
1282
+ * E2E helper function.
1283
+ * @private
1284
+ * @param {{x: Decimal|number, y: Decimal|number}} point - Point to test
1285
+ * @param {{x: Decimal|number, y: Decimal|number}} edgeStart - Edge start point
1286
+ * @param {{x: Decimal|number, y: Decimal|number}} edgeEnd - Edge end point
1287
+ * @returns {boolean} True if point is on the inside side of the edge
1288
+ */
1146
1289
  function isInsideEdgeE2E(point, edgeStart, edgeEnd) {
1147
1290
  const px = D(point.x).toNumber();
1148
1291
  const py = D(point.y).toNumber();
@@ -1154,12 +1297,25 @@ function isInsideEdgeE2E(point, edgeStart, edgeEnd) {
1154
1297
  return (ex - sx) * (py - sy) - (ey - sy) * (px - sx) >= 0;
1155
1298
  }
1156
1299
 
1157
- // E2E helper: line intersection for difference computation
1300
+ /**
1301
+ * Compute line intersection for difference computation.
1302
+ * E2E helper function.
1303
+ * @private
1304
+ * @param {{x: Decimal|number, y: Decimal|number}} p1 - First point on line 1
1305
+ * @param {{x: Decimal|number, y: Decimal|number}} p2 - Second point on line 1
1306
+ * @param {{x: Decimal|number, y: Decimal|number}} p3 - First point on line 2
1307
+ * @param {{x: Decimal|number, y: Decimal|number}} p4 - Second point on line 2
1308
+ * @returns {{x: Decimal, y: Decimal}} Intersection point
1309
+ */
1158
1310
  function lineIntersectE2E(p1, p2, p3, p4) {
1159
- const x1 = D(p1.x).toNumber(), y1 = D(p1.y).toNumber();
1160
- const x2 = D(p2.x).toNumber(), y2 = D(p2.y).toNumber();
1161
- const x3 = D(p3.x).toNumber(), y3 = D(p3.y).toNumber();
1162
- const x4 = D(p4.x).toNumber(), y4 = D(p4.y).toNumber();
1311
+ const x1 = D(p1.x).toNumber(),
1312
+ y1 = D(p1.y).toNumber();
1313
+ const x2 = D(p2.x).toNumber(),
1314
+ y2 = D(p2.y).toNumber();
1315
+ const x3 = D(p3.x).toNumber(),
1316
+ y3 = D(p3.y).toNumber();
1317
+ const x4 = D(p4.x).toNumber(),
1318
+ y4 = D(p4.y).toNumber();
1163
1319
 
1164
1320
  const denom = (x1 - x2) * (y3 - y4) - (y1 - y2) * (x3 - x4);
1165
1321
  if (Math.abs(denom) < 1e-10) {
@@ -1170,7 +1326,7 @@ function lineIntersectE2E(p1, p2, p3, p4) {
1170
1326
 
1171
1327
  return {
1172
1328
  x: D(x1 + t * (x2 - x1)),
1173
- y: D(y1 + t * (y2 - y1))
1329
+ y: D(y1 + t * (y2 - y1)),
1174
1330
  };
1175
1331
  }
1176
1332
 
@@ -1189,29 +1345,37 @@ function lineIntersectE2E(p1, p2, p3, p4) {
1189
1345
  * @returns {Object} Comprehensive verification report
1190
1346
  */
1191
1347
  export function verifyPathTransformation(params) {
1192
- const { matrix, originalPath, transformedPath, testPoints = [] } = params;
1348
+ const {
1349
+ matrix,
1350
+ originalPath: _originalPath,
1351
+ transformedPath: _transformedPath,
1352
+ testPoints = [],
1353
+ } = params;
1193
1354
  const results = {
1194
1355
  allPassed: true,
1195
- verifications: []
1356
+ verifications: [],
1196
1357
  };
1197
1358
 
1198
1359
  // Verify matrix is valid
1199
1360
  const invResult = verifyMatrixInversion(matrix);
1200
- results.verifications.push({ name: 'Matrix Inversion', ...invResult });
1361
+ results.verifications.push({ name: "Matrix Inversion", ...invResult });
1201
1362
  if (!invResult.valid) results.allPassed = false;
1202
1363
 
1203
1364
  // Verify round-trip for test points
1204
1365
  for (let i = 0; i < testPoints.length; i++) {
1205
1366
  const pt = testPoints[i];
1206
1367
  const rtResult = verifyTransformRoundTrip(matrix, pt.x, pt.y);
1207
- results.verifications.push({ name: `Round-trip Point ${i + 1}`, ...rtResult });
1368
+ results.verifications.push({
1369
+ name: `Round-trip Point ${i + 1}`,
1370
+ ...rtResult,
1371
+ });
1208
1372
  if (!rtResult.valid) results.allPassed = false;
1209
1373
  }
1210
1374
 
1211
1375
  // Verify geometry preservation
1212
1376
  if (testPoints.length >= 3) {
1213
1377
  const geoResult = verifyTransformGeometry(matrix, testPoints);
1214
- results.verifications.push({ name: 'Geometry Preservation', ...geoResult });
1378
+ results.verifications.push({ name: "Geometry Preservation", ...geoResult });
1215
1379
  if (!geoResult.valid) results.allPassed = false;
1216
1380
  }
1217
1381