@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
@@ -108,20 +108,20 @@
108
108
  * @module off-canvas-detection
109
109
  */
110
110
 
111
- import Decimal from 'decimal.js';
112
- import { polygonsOverlap, point } from './gjk-collision.js';
111
+ import Decimal from "decimal.js";
112
+ import { polygonsOverlap, point } from "./gjk-collision.js";
113
113
 
114
114
  // Set high precision for all calculations
115
115
  Decimal.set({ precision: 80 });
116
116
 
117
117
  // Helper to convert to Decimal
118
- const D = x => (x instanceof Decimal ? x : new Decimal(x));
118
+ const D = (x) => (x instanceof Decimal ? x : new Decimal(x));
119
119
 
120
120
  // Near-zero threshold for comparisons
121
- const EPSILON = new Decimal('1e-40');
121
+ const EPSILON = new Decimal("1e-40");
122
122
 
123
123
  // Default tolerance for containment checks
124
- const DEFAULT_TOLERANCE = new Decimal('1e-10');
124
+ const DEFAULT_TOLERANCE = new Decimal("1e-10");
125
125
 
126
126
  // Number of samples for path/curve verification
127
127
  const VERIFICATION_SAMPLES = 100;
@@ -143,15 +143,20 @@ const VERIFICATION_SAMPLES = 100;
143
143
  * @throws {Error} If viewBox string is invalid
144
144
  */
145
145
  export function parseViewBox(viewBoxString) {
146
- if (typeof viewBoxString !== 'string') {
147
- throw new Error('ViewBox must be a string');
146
+ if (typeof viewBoxString !== "string") {
147
+ throw new Error("ViewBox must be a string");
148
148
  }
149
149
 
150
150
  // Split on whitespace and/or commas
151
- const parts = viewBoxString.trim().split(/[\s,]+/).filter(p => p.length > 0);
151
+ const parts = viewBoxString
152
+ .trim()
153
+ .split(/[\s,]+/)
154
+ .filter((p) => p.length > 0);
152
155
 
153
156
  if (parts.length !== 4) {
154
- throw new Error(`Invalid viewBox format: expected 4 values, got ${parts.length}`);
157
+ throw new Error(
158
+ `Invalid viewBox format: expected 4 values, got ${parts.length}`,
159
+ );
155
160
  }
156
161
 
157
162
  try {
@@ -162,12 +167,12 @@ export function parseViewBox(viewBoxString) {
162
167
 
163
168
  // Validate positive dimensions
164
169
  if (width.lessThanOrEqualTo(0) || height.lessThanOrEqualTo(0)) {
165
- throw new Error('ViewBox width and height must be positive');
170
+ throw new Error("ViewBox width and height must be positive");
166
171
  }
167
172
 
168
173
  // VERIFICATION: Reconstruct string and compare
169
174
  const reconstructed = `${x.toString()} ${y.toString()} ${width.toString()} ${height.toString()}`;
170
- const normalizedOriginal = parts.join(' ');
175
+ const normalizedOriginal = parts.join(" ");
171
176
  const verified = reconstructed === normalizedOriginal;
172
177
 
173
178
  return { x, y, width, height, verified };
@@ -196,7 +201,7 @@ function createBBox(minX, minY, maxX, maxY) {
196
201
  maxX,
197
202
  maxY,
198
203
  width: maxX.minus(minX),
199
- height: maxY.minus(minY)
204
+ height: maxY.minus(minY),
200
205
  };
201
206
  }
202
207
 
@@ -208,12 +213,12 @@ function createBBox(minX, minY, maxX, maxY) {
208
213
  * @param {Decimal} y - Point Y coordinate
209
214
  * @returns {{minX: Decimal, minY: Decimal, maxX: Decimal, maxY: Decimal}}
210
215
  */
211
- function expandBBox(bbox, x, y) {
216
+ function _expandBBox(bbox, x, y) {
212
217
  return {
213
218
  minX: Decimal.min(bbox.minX, x),
214
219
  minY: Decimal.min(bbox.minY, y),
215
220
  maxX: Decimal.max(bbox.maxX, x),
216
- maxY: Decimal.max(bbox.maxY, y)
221
+ maxY: Decimal.max(bbox.maxY, y),
217
222
  };
218
223
  }
219
224
 
@@ -243,12 +248,14 @@ function sampleCubicBezier(x0, y0, x1, y1, x2, y2, x3, y3, samples = 20) {
243
248
  const oneMinusT2 = oneMinusT.mul(oneMinusT);
244
249
  const oneMinusT3 = oneMinusT2.mul(oneMinusT);
245
250
 
246
- const x = oneMinusT3.mul(x0)
251
+ const x = oneMinusT3
252
+ .mul(x0)
247
253
  .plus(D(3).mul(oneMinusT2).mul(t).mul(x1))
248
254
  .plus(D(3).mul(oneMinusT).mul(t2).mul(x2))
249
255
  .plus(t3.mul(x3));
250
256
 
251
- const y = oneMinusT3.mul(y0)
257
+ const y = oneMinusT3
258
+ .mul(y0)
252
259
  .plus(D(3).mul(oneMinusT2).mul(t).mul(y1))
253
260
  .plus(D(3).mul(oneMinusT).mul(t2).mul(y2))
254
261
  .plus(t3.mul(y3));
@@ -280,11 +287,13 @@ function sampleQuadraticBezier(x0, y0, x1, y1, x2, y2, samples = 20) {
280
287
  const oneMinusT2 = oneMinusT.mul(oneMinusT);
281
288
  const t2 = t.mul(t);
282
289
 
283
- const x = oneMinusT2.mul(x0)
290
+ const x = oneMinusT2
291
+ .mul(x0)
284
292
  .plus(D(2).mul(oneMinusT).mul(t).mul(x1))
285
293
  .plus(t2.mul(x2));
286
294
 
287
- const y = oneMinusT2.mul(y0)
295
+ const y = oneMinusT2
296
+ .mul(y0)
288
297
  .plus(D(2).mul(oneMinusT).mul(t).mul(y1))
289
298
  .plus(t2.mul(y2));
290
299
 
@@ -302,10 +311,12 @@ function sampleQuadraticBezier(x0, y0, x1, y1, x2, y2, samples = 20) {
302
311
  * @returns {boolean} True if point is inside or on boundary
303
312
  */
304
313
  function pointInBBox(pt, bbox, tolerance = DEFAULT_TOLERANCE) {
305
- return pt.x.greaterThanOrEqualTo(bbox.minX.minus(tolerance)) &&
314
+ return (
315
+ pt.x.greaterThanOrEqualTo(bbox.minX.minus(tolerance)) &&
306
316
  pt.x.lessThanOrEqualTo(bbox.maxX.plus(tolerance)) &&
307
317
  pt.y.greaterThanOrEqualTo(bbox.minY.minus(tolerance)) &&
308
- pt.y.lessThanOrEqualTo(bbox.maxY.plus(tolerance));
318
+ pt.y.lessThanOrEqualTo(bbox.maxY.plus(tolerance))
319
+ );
309
320
  }
310
321
 
311
322
  // ============================================================================
@@ -326,7 +337,7 @@ function pointInBBox(pt, bbox, tolerance = DEFAULT_TOLERANCE) {
326
337
  */
327
338
  export function pathBoundingBox(pathCommands) {
328
339
  if (!pathCommands || pathCommands.length === 0) {
329
- throw new Error('Path commands array is empty');
340
+ throw new Error("Path commands array is empty");
330
341
  }
331
342
 
332
343
  let minX = D(Infinity);
@@ -352,7 +363,7 @@ export function pathBoundingBox(pathCommands) {
352
363
  const isRelative = cmd.type === cmd.type.toLowerCase();
353
364
 
354
365
  switch (type) {
355
- case 'M': // Move
366
+ case "M": // Move
356
367
  {
357
368
  // BUG 1 FIX: Handle relative coordinates
358
369
  const x = isRelative ? currentX.plus(D(cmd.x)) : D(cmd.x);
@@ -370,11 +381,11 @@ export function pathBoundingBox(pathCommands) {
370
381
  // BUG 3 FIX: Reset lastControl after non-curve command
371
382
  lastControlX = currentX;
372
383
  lastControlY = currentY;
373
- lastCommand = 'M';
384
+ lastCommand = "M";
374
385
  }
375
386
  break;
376
387
 
377
- case 'L': // Line to
388
+ case "L": // Line to
378
389
  {
379
390
  // BUG 1 FIX: Handle relative coordinates
380
391
  const x = isRelative ? currentX.plus(D(cmd.x)) : D(cmd.x);
@@ -390,11 +401,11 @@ export function pathBoundingBox(pathCommands) {
390
401
  // BUG 3 FIX: Reset lastControl after non-curve command
391
402
  lastControlX = currentX;
392
403
  lastControlY = currentY;
393
- lastCommand = 'L';
404
+ lastCommand = "L";
394
405
  }
395
406
  break;
396
407
 
397
- case 'H': // Horizontal line
408
+ case "H": // Horizontal line
398
409
  {
399
410
  // BUG 1 FIX: Handle relative coordinates
400
411
  const x = isRelative ? currentX.plus(D(cmd.x)) : D(cmd.x);
@@ -406,11 +417,11 @@ export function pathBoundingBox(pathCommands) {
406
417
  // BUG 3 FIX: Reset lastControl after non-curve command
407
418
  lastControlX = currentX;
408
419
  lastControlY = currentY;
409
- lastCommand = 'H';
420
+ lastCommand = "H";
410
421
  }
411
422
  break;
412
423
 
413
- case 'V': // Vertical line
424
+ case "V": // Vertical line
414
425
  {
415
426
  // BUG 1 FIX: Handle relative coordinates
416
427
  const y = isRelative ? currentY.plus(D(cmd.y)) : D(cmd.y);
@@ -422,11 +433,11 @@ export function pathBoundingBox(pathCommands) {
422
433
  // BUG 3 FIX: Reset lastControl after non-curve command
423
434
  lastControlX = currentX;
424
435
  lastControlY = currentY;
425
- lastCommand = 'V';
436
+ lastCommand = "V";
426
437
  }
427
438
  break;
428
439
 
429
- case 'C': // Cubic Bezier
440
+ case "C": // Cubic Bezier
430
441
  {
431
442
  // BUG 1 FIX: Handle relative coordinates
432
443
  const x1 = isRelative ? currentX.plus(D(cmd.x1)) : D(cmd.x1);
@@ -437,7 +448,17 @@ export function pathBoundingBox(pathCommands) {
437
448
  const y = isRelative ? currentY.plus(D(cmd.y)) : D(cmd.y);
438
449
 
439
450
  // Sample curve points
440
- const curvePoints = sampleCubicBezier(currentX, currentY, x1, y1, x2, y2, x, y, 20);
451
+ const curvePoints = sampleCubicBezier(
452
+ currentX,
453
+ currentY,
454
+ x1,
455
+ y1,
456
+ x2,
457
+ y2,
458
+ x,
459
+ y,
460
+ 20,
461
+ );
441
462
  for (const pt of curvePoints) {
442
463
  minX = Decimal.min(minX, pt.x);
443
464
  minY = Decimal.min(minY, pt.y);
@@ -450,11 +471,11 @@ export function pathBoundingBox(pathCommands) {
450
471
  lastControlY = y2;
451
472
  currentX = x;
452
473
  currentY = y;
453
- lastCommand = 'C';
474
+ lastCommand = "C";
454
475
  }
455
476
  break;
456
477
 
457
- case 'S': // Smooth cubic Bezier
478
+ case "S": // Smooth cubic Bezier
458
479
  {
459
480
  // BUG 1 FIX: Handle relative coordinates
460
481
  const x2 = isRelative ? currentX.plus(D(cmd.x2)) : D(cmd.x2);
@@ -465,7 +486,7 @@ export function pathBoundingBox(pathCommands) {
465
486
  // Reflect last control point
466
487
  // BUG 3 FIX: lastControlX/Y are now always initialized, safe to use
467
488
  let x1, y1;
468
- if (lastCommand === 'C' || lastCommand === 'S') {
489
+ if (lastCommand === "C" || lastCommand === "S") {
469
490
  x1 = currentX.mul(2).minus(lastControlX);
470
491
  y1 = currentY.mul(2).minus(lastControlY);
471
492
  } else {
@@ -473,7 +494,17 @@ export function pathBoundingBox(pathCommands) {
473
494
  y1 = currentY;
474
495
  }
475
496
 
476
- const curvePoints = sampleCubicBezier(currentX, currentY, x1, y1, x2, y2, x, y, 20);
497
+ const curvePoints = sampleCubicBezier(
498
+ currentX,
499
+ currentY,
500
+ x1,
501
+ y1,
502
+ x2,
503
+ y2,
504
+ x,
505
+ y,
506
+ 20,
507
+ );
477
508
  for (const pt of curvePoints) {
478
509
  minX = Decimal.min(minX, pt.x);
479
510
  minY = Decimal.min(minY, pt.y);
@@ -486,11 +517,11 @@ export function pathBoundingBox(pathCommands) {
486
517
  lastControlY = y2;
487
518
  currentX = x;
488
519
  currentY = y;
489
- lastCommand = 'S';
520
+ lastCommand = "S";
490
521
  }
491
522
  break;
492
523
 
493
- case 'Q': // Quadratic Bezier
524
+ case "Q": // Quadratic Bezier
494
525
  {
495
526
  // BUG 1 FIX: Handle relative coordinates
496
527
  const x1 = isRelative ? currentX.plus(D(cmd.x1)) : D(cmd.x1);
@@ -498,7 +529,15 @@ export function pathBoundingBox(pathCommands) {
498
529
  const x = isRelative ? currentX.plus(D(cmd.x)) : D(cmd.x);
499
530
  const y = isRelative ? currentY.plus(D(cmd.y)) : D(cmd.y);
500
531
 
501
- const curvePoints = sampleQuadraticBezier(currentX, currentY, x1, y1, x, y, 20);
532
+ const curvePoints = sampleQuadraticBezier(
533
+ currentX,
534
+ currentY,
535
+ x1,
536
+ y1,
537
+ x,
538
+ y,
539
+ 20,
540
+ );
502
541
  for (const pt of curvePoints) {
503
542
  minX = Decimal.min(minX, pt.x);
504
543
  minY = Decimal.min(minY, pt.y);
@@ -511,11 +550,11 @@ export function pathBoundingBox(pathCommands) {
511
550
  lastControlY = y1;
512
551
  currentX = x;
513
552
  currentY = y;
514
- lastCommand = 'Q';
553
+ lastCommand = "Q";
515
554
  }
516
555
  break;
517
556
 
518
- case 'T': // Smooth quadratic Bezier
557
+ case "T": // Smooth quadratic Bezier
519
558
  {
520
559
  // BUG 1 FIX: Handle relative coordinates
521
560
  const x = isRelative ? currentX.plus(D(cmd.x)) : D(cmd.x);
@@ -524,7 +563,7 @@ export function pathBoundingBox(pathCommands) {
524
563
  // Reflect last control point
525
564
  // BUG 3 FIX: lastControlX/Y are now always initialized, safe to use
526
565
  let x1, y1;
527
- if (lastCommand === 'Q' || lastCommand === 'T') {
566
+ if (lastCommand === "Q" || lastCommand === "T") {
528
567
  x1 = currentX.mul(2).minus(lastControlX);
529
568
  y1 = currentY.mul(2).minus(lastControlY);
530
569
  } else {
@@ -532,7 +571,15 @@ export function pathBoundingBox(pathCommands) {
532
571
  y1 = currentY;
533
572
  }
534
573
 
535
- const curvePoints = sampleQuadraticBezier(currentX, currentY, x1, y1, x, y, 20);
574
+ const curvePoints = sampleQuadraticBezier(
575
+ currentX,
576
+ currentY,
577
+ x1,
578
+ y1,
579
+ x,
580
+ y,
581
+ 20,
582
+ );
536
583
  for (const pt of curvePoints) {
537
584
  minX = Decimal.min(minX, pt.x);
538
585
  minY = Decimal.min(minY, pt.y);
@@ -545,11 +592,11 @@ export function pathBoundingBox(pathCommands) {
545
592
  lastControlY = y1;
546
593
  currentX = x;
547
594
  currentY = y;
548
- lastCommand = 'T';
595
+ lastCommand = "T";
549
596
  }
550
597
  break;
551
598
 
552
- case 'A': // Arc (approximate with samples)
599
+ case "A": // Arc (approximate with samples)
553
600
  {
554
601
  // BUG 4: Arc bounding box ignores actual arc geometry
555
602
  // TODO: Implement proper arc-to-bezier conversion or calculate arc extrema
@@ -583,17 +630,17 @@ export function pathBoundingBox(pathCommands) {
583
630
  // BUG 3 FIX: Reset lastControl after non-curve command
584
631
  lastControlX = currentX;
585
632
  lastControlY = currentY;
586
- lastCommand = 'A';
633
+ lastCommand = "A";
587
634
  }
588
635
  break;
589
636
 
590
- case 'Z': // Close path
637
+ case "Z": // Close path
591
638
  currentX = startX;
592
639
  currentY = startY;
593
640
  // BUG 3 FIX: Reset lastControl after non-curve command
594
641
  lastControlX = currentX;
595
642
  lastControlY = currentY;
596
- lastCommand = 'Z';
643
+ lastCommand = "Z";
597
644
  break;
598
645
 
599
646
  default:
@@ -601,8 +648,13 @@ export function pathBoundingBox(pathCommands) {
601
648
  }
602
649
  }
603
650
 
604
- if (!minX.isFinite() || !minY.isFinite() || !maxX.isFinite() || !maxY.isFinite()) {
605
- throw new Error('Invalid bounding box: no valid coordinates found');
651
+ if (
652
+ !minX.isFinite() ||
653
+ !minY.isFinite() ||
654
+ !maxX.isFinite() ||
655
+ !maxY.isFinite()
656
+ ) {
657
+ throw new Error("Invalid bounding box: no valid coordinates found");
606
658
  }
607
659
 
608
660
  const bbox = createBBox(minX, minY, maxX, maxY);
@@ -636,7 +688,7 @@ export function pathBoundingBox(pathCommands) {
636
688
  */
637
689
  export function shapeBoundingBox(shape) {
638
690
  if (!shape || !shape.type) {
639
- throw new Error('Shape object must have a type property');
691
+ throw new Error("Shape object must have a type property");
640
692
  }
641
693
 
642
694
  const type = shape.type.toLowerCase();
@@ -644,7 +696,7 @@ export function shapeBoundingBox(shape) {
644
696
  let samplePoints = [];
645
697
 
646
698
  switch (type) {
647
- case 'rect':
699
+ case "rect":
648
700
  {
649
701
  const x = D(shape.x);
650
702
  const y = D(shape.y);
@@ -658,12 +710,12 @@ export function shapeBoundingBox(shape) {
658
710
  { x, y },
659
711
  { x: x.plus(width), y },
660
712
  { x: x.plus(width), y: y.plus(height) },
661
- { x, y: y.plus(height) }
713
+ { x, y: y.plus(height) },
662
714
  ];
663
715
  }
664
716
  break;
665
717
 
666
- case 'circle':
718
+ case "circle":
667
719
  {
668
720
  const cx = D(shape.cx);
669
721
  const cy = D(shape.cy);
@@ -683,7 +735,7 @@ export function shapeBoundingBox(shape) {
683
735
  }
684
736
  break;
685
737
 
686
- case 'ellipse':
738
+ case "ellipse":
687
739
  {
688
740
  const cx = D(shape.cx);
689
741
  const cy = D(shape.cy);
@@ -704,7 +756,7 @@ export function shapeBoundingBox(shape) {
704
756
  }
705
757
  break;
706
758
 
707
- case 'line':
759
+ case "line":
708
760
  {
709
761
  const x1 = D(shape.x1);
710
762
  const y1 = D(shape.y1);
@@ -717,12 +769,15 @@ export function shapeBoundingBox(shape) {
717
769
  const maxY = Decimal.max(y1, y2);
718
770
 
719
771
  bbox = createBBox(minX, minY, maxX, maxY);
720
- samplePoints = [{ x: x1, y: y1 }, { x: x2, y: y2 }];
772
+ samplePoints = [
773
+ { x: x1, y: y1 },
774
+ { x: x2, y: y2 },
775
+ ];
721
776
  }
722
777
  break;
723
778
 
724
- case 'polygon':
725
- case 'polyline':
779
+ case "polygon":
780
+ case "polyline":
726
781
  {
727
782
  if (!shape.points || shape.points.length === 0) {
728
783
  throw new Error(`${type} must have points array`);
@@ -778,7 +833,7 @@ function bboxToPolygon(bbox) {
778
833
  point(bbox.minX, bbox.minY),
779
834
  point(bbox.maxX, bbox.minY),
780
835
  point(bbox.maxX, bbox.maxY),
781
- point(bbox.minX, bbox.maxY)
836
+ point(bbox.minX, bbox.maxY),
782
837
  ];
783
838
  }
784
839
 
@@ -801,7 +856,7 @@ export function bboxIntersectsViewBox(bbox, viewBox) {
801
856
  minX: viewBox.x,
802
857
  minY: viewBox.y,
803
858
  maxX: viewBox.x.plus(viewBox.width),
804
- maxY: viewBox.y.plus(viewBox.height)
859
+ maxY: viewBox.y.plus(viewBox.height),
805
860
  });
806
861
 
807
862
  // Use GJK algorithm
@@ -809,7 +864,7 @@ export function bboxIntersectsViewBox(bbox, viewBox) {
809
864
 
810
865
  return {
811
866
  intersects: result.overlaps,
812
- verified: result.verified
867
+ verified: result.verified,
813
868
  };
814
869
  }
815
870
 
@@ -840,7 +895,7 @@ export function isPathOffCanvas(pathCommands, viewBox) {
840
895
  return {
841
896
  offCanvas: false,
842
897
  bbox,
843
- verified: intersection.verified && bbox.verified
898
+ verified: intersection.verified && bbox.verified,
844
899
  };
845
900
  }
846
901
 
@@ -850,13 +905,13 @@ export function isPathOffCanvas(pathCommands, viewBox) {
850
905
  minX: viewBox.x,
851
906
  minY: viewBox.y,
852
907
  maxX: viewBox.x.plus(viewBox.width),
853
- maxY: viewBox.y.plus(viewBox.height)
908
+ maxY: viewBox.y.plus(viewBox.height),
854
909
  };
855
910
 
856
911
  // Sample a few points from the path to verify
857
912
  let verified = true;
858
- let currentX = D(0);
859
- let currentY = D(0);
913
+ let _currentX = D(0);
914
+ let _currentY = D(0);
860
915
  let sampleCount = 0;
861
916
  const maxSamples = 10; // Limit verification samples
862
917
 
@@ -864,15 +919,15 @@ export function isPathOffCanvas(pathCommands, viewBox) {
864
919
  if (sampleCount >= maxSamples) break;
865
920
 
866
921
  const type = cmd.type.toUpperCase();
867
- if (type === 'M' || type === 'L') {
922
+ if (type === "M" || type === "L") {
868
923
  const x = D(cmd.x);
869
924
  const y = D(cmd.y);
870
925
  if (pointInBBox({ x, y }, viewBoxBBox)) {
871
926
  verified = false;
872
927
  break;
873
928
  }
874
- currentX = x;
875
- currentY = y;
929
+ _currentX = x;
930
+ _currentY = y;
876
931
  sampleCount++;
877
932
  }
878
933
  }
@@ -880,7 +935,7 @@ export function isPathOffCanvas(pathCommands, viewBox) {
880
935
  return {
881
936
  offCanvas: true,
882
937
  bbox,
883
- verified: verified && bbox.verified
938
+ verified: verified && bbox.verified,
884
939
  };
885
940
  }
886
941
 
@@ -907,7 +962,7 @@ export function isShapeOffCanvas(shape, viewBox) {
907
962
  return {
908
963
  offCanvas: false,
909
964
  bbox,
910
- verified: intersection.verified && bbox.verified
965
+ verified: intersection.verified && bbox.verified,
911
966
  };
912
967
  }
913
968
 
@@ -915,7 +970,7 @@ export function isShapeOffCanvas(shape, viewBox) {
915
970
  return {
916
971
  offCanvas: true,
917
972
  bbox,
918
- verified: intersection.verified && bbox.verified
973
+ verified: intersection.verified && bbox.verified,
919
974
  };
920
975
  }
921
976
 
@@ -934,10 +989,10 @@ export function isShapeOffCanvas(shape, viewBox) {
934
989
  function clipLine(p1, p2, bounds) {
935
990
  // Cohen-Sutherland outcodes
936
991
  const INSIDE = 0; // 0000
937
- const LEFT = 1; // 0001
938
- const RIGHT = 2; // 0010
992
+ const LEFT = 1; // 0001
993
+ const RIGHT = 2; // 0010
939
994
  const BOTTOM = 4; // 0100
940
- const TOP = 8; // 1000
995
+ const TOP = 8; // 1000
941
996
 
942
997
  const computeOutcode = (x, y) => {
943
998
  let code = INSIDE;
@@ -948,8 +1003,10 @@ function clipLine(p1, p2, bounds) {
948
1003
  return code;
949
1004
  };
950
1005
 
951
- let x1 = p1.x, y1 = p1.y;
952
- let x2 = p2.x, y2 = p2.y;
1006
+ let x1 = p1.x,
1007
+ y1 = p1.y;
1008
+ let x2 = p2.x,
1009
+ y2 = p2.y;
953
1010
  let outcode1 = computeOutcode(x1, y1);
954
1011
  let outcode2 = computeOutcode(x2, y2);
955
1012
 
@@ -962,7 +1019,10 @@ function clipLine(p1, p2, bounds) {
962
1019
  while (true) {
963
1020
  if ((outcode1 | outcode2) === 0) {
964
1021
  // Both points inside - accept
965
- return [{ x: x1, y: y1 }, { x: x2, y: y2 }];
1022
+ return [
1023
+ { x: x1, y: y1 },
1024
+ { x: x2, y: y2 },
1025
+ ];
966
1026
  } else if ((outcode1 & outcode2) !== 0) {
967
1027
  // Both points outside same edge - reject
968
1028
  return [];
@@ -1010,7 +1070,8 @@ function clipLine(p1, p2, bounds) {
1010
1070
  } else if ((outcodeOut & RIGHT) !== 0) {
1011
1071
  y = y1.plus(dy.mul(bounds.maxX.minus(x1)).div(dx));
1012
1072
  x = bounds.maxX;
1013
- } else { // LEFT
1073
+ } else {
1074
+ // LEFT
1014
1075
  y = y1.plus(dy.mul(bounds.minX.minus(x1)).div(dx));
1015
1076
  x = bounds.minX;
1016
1077
  }
@@ -1046,7 +1107,7 @@ export function clipPathToViewBox(pathCommands, viewBox) {
1046
1107
  minX: viewBox.x,
1047
1108
  minY: viewBox.y,
1048
1109
  maxX: viewBox.x.plus(viewBox.width),
1049
- maxY: viewBox.y.plus(viewBox.height)
1110
+ maxY: viewBox.y.plus(viewBox.height),
1050
1111
  };
1051
1112
 
1052
1113
  const clippedCommands = [];
@@ -1058,14 +1119,14 @@ export function clipPathToViewBox(pathCommands, viewBox) {
1058
1119
  const type = cmd.type.toUpperCase();
1059
1120
 
1060
1121
  switch (type) {
1061
- case 'M': // Move
1122
+ case "M": // Move
1062
1123
  {
1063
1124
  const x = D(cmd.x);
1064
1125
  const y = D(cmd.y);
1065
1126
 
1066
1127
  // Only add move if point is inside bounds
1067
1128
  if (pointInBBox({ x, y }, bounds)) {
1068
- clippedCommands.push({ type: 'M', x, y });
1129
+ clippedCommands.push({ type: "M", x, y });
1069
1130
  pathStarted = true;
1070
1131
  } else {
1071
1132
  pathStarted = false;
@@ -1076,21 +1137,33 @@ export function clipPathToViewBox(pathCommands, viewBox) {
1076
1137
  }
1077
1138
  break;
1078
1139
 
1079
- case 'L': // Line to
1140
+ case "L": // Line to
1080
1141
  {
1081
1142
  const x = D(cmd.x);
1082
1143
  const y = D(cmd.y);
1083
1144
 
1084
1145
  // Clip line segment
1085
- const clipped = clipLine({ x: currentX, y: currentY }, { x, y }, bounds);
1146
+ const clipped = clipLine(
1147
+ { x: currentX, y: currentY },
1148
+ { x, y },
1149
+ bounds,
1150
+ );
1086
1151
 
1087
1152
  if (clipped.length === 2) {
1088
1153
  // Line segment visible after clipping
1089
1154
  if (!pathStarted) {
1090
- clippedCommands.push({ type: 'M', x: clipped[0].x, y: clipped[0].y });
1155
+ clippedCommands.push({
1156
+ type: "M",
1157
+ x: clipped[0].x,
1158
+ y: clipped[0].y,
1159
+ });
1091
1160
  pathStarted = true;
1092
1161
  }
1093
- clippedCommands.push({ type: 'L', x: clipped[1].x, y: clipped[1].y });
1162
+ clippedCommands.push({
1163
+ type: "L",
1164
+ x: clipped[1].x,
1165
+ y: clipped[1].y,
1166
+ });
1094
1167
  } else {
1095
1168
  // Line segment completely clipped
1096
1169
  pathStarted = false;
@@ -1101,17 +1174,29 @@ export function clipPathToViewBox(pathCommands, viewBox) {
1101
1174
  }
1102
1175
  break;
1103
1176
 
1104
- case 'H': // Horizontal line
1177
+ case "H": // Horizontal line
1105
1178
  {
1106
1179
  const x = D(cmd.x);
1107
- const clipped = clipLine({ x: currentX, y: currentY }, { x, y: currentY }, bounds);
1180
+ const clipped = clipLine(
1181
+ { x: currentX, y: currentY },
1182
+ { x, y: currentY },
1183
+ bounds,
1184
+ );
1108
1185
 
1109
1186
  if (clipped.length === 2) {
1110
1187
  if (!pathStarted) {
1111
- clippedCommands.push({ type: 'M', x: clipped[0].x, y: clipped[0].y });
1188
+ clippedCommands.push({
1189
+ type: "M",
1190
+ x: clipped[0].x,
1191
+ y: clipped[0].y,
1192
+ });
1112
1193
  pathStarted = true;
1113
1194
  }
1114
- clippedCommands.push({ type: 'L', x: clipped[1].x, y: clipped[1].y });
1195
+ clippedCommands.push({
1196
+ type: "L",
1197
+ x: clipped[1].x,
1198
+ y: clipped[1].y,
1199
+ });
1115
1200
  } else {
1116
1201
  pathStarted = false;
1117
1202
  }
@@ -1120,17 +1205,29 @@ export function clipPathToViewBox(pathCommands, viewBox) {
1120
1205
  }
1121
1206
  break;
1122
1207
 
1123
- case 'V': // Vertical line
1208
+ case "V": // Vertical line
1124
1209
  {
1125
1210
  const y = D(cmd.y);
1126
- const clipped = clipLine({ x: currentX, y: currentY }, { x: currentX, y }, bounds);
1211
+ const clipped = clipLine(
1212
+ { x: currentX, y: currentY },
1213
+ { x: currentX, y },
1214
+ bounds,
1215
+ );
1127
1216
 
1128
1217
  if (clipped.length === 2) {
1129
1218
  if (!pathStarted) {
1130
- clippedCommands.push({ type: 'M', x: clipped[0].x, y: clipped[0].y });
1219
+ clippedCommands.push({
1220
+ type: "M",
1221
+ x: clipped[0].x,
1222
+ y: clipped[0].y,
1223
+ });
1131
1224
  pathStarted = true;
1132
1225
  }
1133
- clippedCommands.push({ type: 'L', x: clipped[1].x, y: clipped[1].y });
1226
+ clippedCommands.push({
1227
+ type: "L",
1228
+ x: clipped[1].x,
1229
+ y: clipped[1].y,
1230
+ });
1134
1231
  } else {
1135
1232
  pathStarted = false;
1136
1233
  }
@@ -1139,11 +1236,11 @@ export function clipPathToViewBox(pathCommands, viewBox) {
1139
1236
  }
1140
1237
  break;
1141
1238
 
1142
- case 'C': // Cubic Bezier - sample as polyline
1143
- case 'S': // Smooth cubic - sample as polyline
1144
- case 'Q': // Quadratic - sample as polyline
1145
- case 'T': // Smooth quadratic - sample as polyline
1146
- case 'A': // Arc - sample as polyline
1239
+ case "C": // Cubic Bezier - sample as polyline
1240
+ case "S": // Smooth cubic - sample as polyline
1241
+ case "Q": // Quadratic - sample as polyline
1242
+ case "T": // Smooth quadratic - sample as polyline
1243
+ case "A": // Arc - sample as polyline
1147
1244
  {
1148
1245
  // For simplicity, just include the endpoint
1149
1246
  // A full implementation would sample the curve
@@ -1152,10 +1249,10 @@ export function clipPathToViewBox(pathCommands, viewBox) {
1152
1249
 
1153
1250
  if (pointInBBox({ x, y }, bounds)) {
1154
1251
  if (!pathStarted) {
1155
- clippedCommands.push({ type: 'M', x, y });
1252
+ clippedCommands.push({ type: "M", x, y });
1156
1253
  pathStarted = true;
1157
1254
  } else {
1158
- clippedCommands.push({ type: 'L', x, y });
1255
+ clippedCommands.push({ type: "L", x, y });
1159
1256
  }
1160
1257
  } else {
1161
1258
  pathStarted = false;
@@ -1166,9 +1263,9 @@ export function clipPathToViewBox(pathCommands, viewBox) {
1166
1263
  }
1167
1264
  break;
1168
1265
 
1169
- case 'Z': // Close path
1266
+ case "Z": // Close path
1170
1267
  if (pathStarted) {
1171
- clippedCommands.push({ type: 'Z' });
1268
+ clippedCommands.push({ type: "Z" });
1172
1269
  }
1173
1270
  pathStarted = false;
1174
1271
  break;
@@ -1182,7 +1279,7 @@ export function clipPathToViewBox(pathCommands, viewBox) {
1182
1279
  // VERIFICATION: Check that all points are within bounds
1183
1280
  let verified = true;
1184
1281
  for (const cmd of clippedCommands) {
1185
- if (cmd.type === 'M' || cmd.type === 'L') {
1282
+ if (cmd.type === "M" || cmd.type === "L") {
1186
1283
  if (!pointInBBox({ x: cmd.x, y: cmd.y }, bounds)) {
1187
1284
  verified = false;
1188
1285
  break;
@@ -1218,5 +1315,5 @@ export default {
1218
1315
  // Constants
1219
1316
  EPSILON,
1220
1317
  DEFAULT_TOLERANCE,
1221
- VERIFICATION_SAMPLES
1318
+ VERIFICATION_SAMPLES,
1222
1319
  };