svg-path-simplify 0.0.7 → 0.0.9

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 (38) hide show
  1. package/README.md +25 -5
  2. package/dist/svg-path-simplify.esm.js +1250 -562
  3. package/dist/svg-path-simplify.esm.min.js +1 -1
  4. package/dist/svg-path-simplify.js +4756 -4068
  5. package/dist/svg-path-simplify.min.js +1 -1
  6. package/dist/svg-path-simplify.node.js +1250 -562
  7. package/dist/svg-path-simplify.node.min.js +1 -1
  8. package/index.html +89 -29
  9. package/package.json +5 -3
  10. package/src/detect_input.js +17 -10
  11. package/src/dom-polyfill.js +29 -0
  12. package/src/dom-polyfill_back.js +22 -0
  13. package/src/index.js +10 -1
  14. package/src/pathData_simplify_cubic.js +114 -143
  15. package/src/pathData_simplify_cubic_extrapolate.js +64 -35
  16. package/src/pathSimplify-main.js +113 -165
  17. package/src/svgii/geometry.js +8 -155
  18. package/src/svgii/geometry_flatness.js +94 -0
  19. package/src/svgii/pathData_analyze.js +15 -596
  20. package/src/svgii/pathData_convert.js +26 -17
  21. package/src/svgii/pathData_interpolate.js +65 -0
  22. package/src/svgii/pathData_parse.js +25 -9
  23. package/src/svgii/pathData_parse_els.js +245 -0
  24. package/src/svgii/pathData_remove_collinear.js +33 -28
  25. package/src/svgii/pathData_remove_orphaned.js +21 -0
  26. package/src/svgii/pathData_remove_zerolength.js +17 -3
  27. package/src/svgii/pathData_reorder.js +9 -3
  28. package/src/svgii/pathData_simplify_refineCorners.js +160 -0
  29. package/src/svgii/pathData_simplify_refineExtremes.js +208 -0
  30. package/src/svgii/pathData_split.js +43 -15
  31. package/src/svgii/pathData_stringify.js +3 -12
  32. package/src/svgii/rounding.js +35 -27
  33. package/src/svgii/svg_cleanup.js +4 -1
  34. package/testSVG.js +39 -0
  35. package/src/pathData_simplify_cubic_arr.js +0 -50
  36. package/src/svgii/simplify.js +0 -248
  37. package/src/svgii/simplify_bezier.js +0 -470
  38. package/src/svgii/simplify_linetos.js +0 -93
@@ -1,3 +1,42 @@
1
+ function renderPoint(
2
+ svg,
3
+ coords,
4
+ fill = "red",
5
+ r = "1%",
6
+ opacity = "1",
7
+ title = '',
8
+ render = true,
9
+ id = "",
10
+ className = ""
11
+ ) {
12
+ if (Array.isArray(coords)) {
13
+ coords = {
14
+ x: coords[0],
15
+ y: coords[1]
16
+ };
17
+ }
18
+ let marker = `<circle class="${className}" opacity="${opacity}" id="${id}" cx="${coords.x}" cy="${coords.y}" r="${r}" fill="${fill}">
19
+ <title>${title}</title></circle>`;
20
+
21
+ if (render) {
22
+ svg.insertAdjacentHTML("beforeend", marker);
23
+ } else {
24
+ return marker;
25
+ }
26
+ }
27
+
28
+ function renderPath(svg, d = '', stroke = 'green', strokeWidth = '1%', render = true) {
29
+
30
+ let path = `<path d="${d}" fill="none" stroke="${stroke}" stroke-width="${strokeWidth}" /> `;
31
+
32
+ if (render) {
33
+ svg.insertAdjacentHTML("beforeend", path);
34
+ } else {
35
+ return path;
36
+ }
37
+
38
+ }
39
+
1
40
  function detectInputType(input) {
2
41
  let type = 'string';
3
42
  /*
@@ -8,23 +47,29 @@ function detectInputType(input) {
8
47
  if (input instanceof ArrayBuffer) return "buffer";
9
48
  if (input instanceof Blob) return "blob";
10
49
  */
11
- if (Array.isArray(input)) return "array";
50
+ if (Array.isArray(input)) {
51
+ if (input[0]?.type && input[0]?.values
52
+ ) {
53
+ return "pathData";
54
+ }
55
+
56
+ return "array";
57
+ }
12
58
 
13
59
  if (typeof input === "string") {
14
60
  input = input.trim();
15
61
  let isSVG = input.includes('<svg') && input.includes('</svg');
16
62
  let isPathData = input.startsWith('M') || input.startsWith('m');
17
- let isPolyString = !isNaN(input.substring(0, 1)) && !isNaN(input.substring(input.length-1, input.length));
63
+ let isPolyString = !isNaN(input.substring(0, 1)) && !isNaN(input.substring(input.length - 1, input.length));
18
64
 
19
-
20
- if(isSVG) {
21
- type='svgMarkup';
65
+ if (isSVG) {
66
+ type = 'svgMarkup';
22
67
  }
23
- else if(isPathData) {
24
- type='pathDataString';
68
+ else if (isPathData) {
69
+ type = 'pathDataString';
25
70
  }
26
- else if(isPolyString) {
27
- type='polyString';
71
+ else if (isPolyString) {
72
+ type = 'polyString';
28
73
  }
29
74
 
30
75
  else {
@@ -65,19 +110,24 @@ function getAngle(p1, p2, normalize = false) {
65
110
  * http://jsfiddle.net/justin_c_rounds/Gd2S2/light/
66
111
  */
67
112
 
68
- function checkLineIntersection(p1, p2, p3, p4, exact = true) {
113
+ function checkLineIntersection(p1=null, p2=null, p3=null, p4=null, exact = true, debug=false) {
69
114
  // if the lines intersect, the result contains the x and y of the intersection (treating the lines as infinite) and booleans for whether line segment 1 or line segment 2 contain the point
70
115
  let denominator, a, b, numerator1, numerator2;
71
116
  let intersectionPoint = {};
72
117
 
118
+ if(!p1 || !p2 || !p3 || !p4){
119
+ if(debug) console.warn('points missing');
120
+ return false
121
+ }
122
+
73
123
  try {
74
124
  denominator = ((p4.y - p3.y) * (p2.x - p1.x)) - ((p4.x - p3.x) * (p2.y - p1.y));
75
125
  if (denominator == 0) {
76
126
  return false;
77
127
  }
78
-
79
128
  } catch {
80
- console.log('!catch', p1, p2, 'p3:', p3, p4);
129
+ if(debug) console.warn('!catch', p1, p2, 'p3:', p3, 'p4:', p4);
130
+ return false
81
131
  }
82
132
 
83
133
  a = p1.y - p3.y;
@@ -94,8 +144,6 @@ function checkLineIntersection(p1, p2, p3, p4, exact = true) {
94
144
  y: p1.y + (a * (p2.y - p1.y))
95
145
  };
96
146
 
97
- // console.log('intersectionPoint', intersectionPoint, p1, p2);
98
-
99
147
  let intersection = false;
100
148
  // if line1 is a segment and line2 is infinite, they intersect if:
101
149
  if ((a > 0 && a < 1) && (b > 0 && b < 1)) {
@@ -495,6 +543,110 @@ function getBezierExtremeT(pts) {
495
543
  return tArr;
496
544
  }
497
545
 
546
+ /**
547
+ * based on Nikos M.'s answer
548
+ * how-do-you-calculate-the-axis-aligned-bounding-box-of-an-ellipse
549
+ * https://stackoverflow.com/questions/87734/#75031511
550
+ * See also: https://github.com/foo123/Geometrize
551
+ */
552
+ function getArcExtemes(p0, values) {
553
+ // compute point on ellipse from angle around ellipse (theta)
554
+ const arc = (theta, cx, cy, rx, ry, alpha) => {
555
+ // theta is angle in radians around arc
556
+ // alpha is angle of rotation of ellipse in radians
557
+ var cos = Math.cos(alpha),
558
+ sin = Math.sin(alpha),
559
+ x = rx * Math.cos(theta),
560
+ y = ry * Math.sin(theta);
561
+
562
+ return {
563
+ x: cx + cos * x - sin * y,
564
+ y: cy + sin * x + cos * y
565
+ };
566
+ };
567
+
568
+ let arcData = svgArcToCenterParam(p0.x, p0.y, values[0], values[1], values[2], values[3], values[4], values[5], values[6]);
569
+ let { rx, ry, cx, cy, endAngle, deltaAngle } = arcData;
570
+
571
+ // arc rotation
572
+ let deg = values[2];
573
+
574
+ // final on path point
575
+ let p = { x: values[5], y: values[6] };
576
+
577
+ // collect extreme points – add end point
578
+ let extremes = [p];
579
+
580
+ // rotation to radians
581
+ let alpha = deg * Math.PI / 180;
582
+ let tan = Math.tan(alpha),
583
+ p1, p2, p3, p4, theta;
584
+
585
+ /**
586
+ * find min/max from zeroes of directional derivative along x and y
587
+ * along x axis
588
+ */
589
+ theta = Math.atan2(-ry * tan, rx);
590
+
591
+ let angle1 = theta;
592
+ let angle2 = theta + Math.PI;
593
+ let angle3 = Math.atan2(ry, rx * tan);
594
+ let angle4 = angle3 + Math.PI;
595
+
596
+ // inner bounding box
597
+ let xArr = [p0.x, p.x];
598
+ let yArr = [p0.y, p.y];
599
+ let xMin = Math.min(...xArr);
600
+ let xMax = Math.max(...xArr);
601
+ let yMin = Math.min(...yArr);
602
+ let yMax = Math.max(...yArr);
603
+
604
+ // on path point close after start
605
+ let angleAfterStart = endAngle - deltaAngle * 0.001;
606
+ let pP2 = arc(angleAfterStart, cx, cy, rx, ry, alpha);
607
+
608
+ // on path point close before end
609
+ let angleBeforeEnd = endAngle - deltaAngle * 0.999;
610
+ let pP3 = arc(angleBeforeEnd, cx, cy, rx, ry, alpha);
611
+
612
+ /**
613
+ * expected extremes
614
+ * if leaving inner bounding box
615
+ * (between segment start and end point)
616
+ * otherwise exclude elliptic extreme points
617
+ */
618
+
619
+ // right
620
+ if (pP2.x > xMax || pP3.x > xMax) {
621
+ // get point for this theta
622
+ p1 = arc(angle1, cx, cy, rx, ry, alpha);
623
+ extremes.push(p1);
624
+ }
625
+
626
+ // left
627
+ if (pP2.x < xMin || pP3.x < xMin) {
628
+ // get anti-symmetric point
629
+ p2 = arc(angle2, cx, cy, rx, ry, alpha);
630
+ extremes.push(p2);
631
+ }
632
+
633
+ // top
634
+ if (pP2.y < yMin || pP3.y < yMin) {
635
+ // get anti-symmetric point
636
+ p4 = arc(angle4, cx, cy, rx, ry, alpha);
637
+ extremes.push(p4);
638
+ }
639
+
640
+ // bottom
641
+ if (pP2.y > yMax || pP3.y > yMax) {
642
+ // get point for this theta
643
+ p3 = arc(angle3, cx, cy, rx, ry, alpha);
644
+ extremes.push(p3);
645
+ }
646
+
647
+ return extremes;
648
+ }
649
+
498
650
  // cubic bezier.
499
651
  function cubicBezierExtremeT(p0, cp1, cp2, p) {
500
652
  let [x0, y0, x1, y1, x2, y2, x3, y3] = [p0.x, p0.y, cp1.x, cp1.y, cp2.x, cp2.y, p.x, p.y];
@@ -603,90 +755,6 @@ function quadraticBezierExtremeT(p0, cp1, p) {
603
755
  return extemeT
604
756
  }
605
757
 
606
- function commandIsFlat(points, tolerance = 0.025) {
607
-
608
- let p0 = points[0];
609
- let p = points[points.length - 1];
610
-
611
- let xArr = points.map(pt => { return pt.x });
612
- let yArr = points.map(pt => { return pt.y });
613
-
614
- let xMin = Math.min(...xArr);
615
- let xMax = Math.max(...xArr);
616
- let yMin = Math.min(...yArr);
617
- let yMax = Math.max(...yArr);
618
- let w = xMax - xMin;
619
- let h = yMax - yMin;
620
-
621
- if (points.length < 3 || (w === 0 || h === 0)) {
622
- return { area: 0, flat: true, thresh: 0.0001, ratio: 0 };
623
- }
624
-
625
- let squareDist = getSquareDistance(p0, p);
626
- let squareDist1 = getSquareDistance(p0, points[0]);
627
- let squareDist2 = points.length > 3 ? getSquareDistance(p, points[1]) : squareDist1;
628
- let squareDistAvg = (squareDist1 + squareDist2) / 2;
629
-
630
- tolerance = 0.5;
631
- let thresh = (w + h) * 0.5 * tolerance;
632
-
633
- let area = 0;
634
- for (let i = 0, l = points.length; i < l; i++) {
635
- let addX = points[i].x;
636
- let addY = points[i === points.length - 1 ? 0 : i + 1].y;
637
- let subX = points[i === points.length - 1 ? 0 : i + 1].x;
638
- let subY = points[i].y;
639
- area += addX * addY * 0.5 - subX * subY * 0.5;
640
- }
641
-
642
- area = +Math.abs(area).toFixed(9);
643
- let areaThresh = 1000;
644
-
645
- let ratio = area / (squareDistAvg);
646
-
647
- let isFlat = area === 0 ? true : area < squareDistAvg / areaThresh;
648
-
649
- return { area: area, flat: isFlat, thresh: thresh, ratio: ratio, squareDist: squareDist, areaThresh: squareDist / areaThresh };
650
- }
651
-
652
- function checkBezierFlatness(p0, cpts, p) {
653
-
654
- let isFlat = false;
655
-
656
- let isCubic = cpts.length===2;
657
-
658
- let cp1 = cpts[0];
659
- let cp2 = isCubic ? cpts[1] : cp1;
660
-
661
- if(p0.x===cp1.x && p0.y===cp1.y && p.x===cp2.x && p.y===cp2.y) return true;
662
-
663
- let dx1 = cp1.x - p0.x;
664
- let dy1 = cp1.y - p0.y;
665
-
666
- let dx2 = p.x - cp2.x;
667
- let dy2 = p.y - cp2.y;
668
-
669
- let cross1 = Math.abs(dx1 * dy2 - dy1 * dx2);
670
-
671
- if(!cross1) return true
672
-
673
- let dx0 = p.x - p0.x;
674
- let dy0 = p.y - p0.y;
675
- let cross0 = Math.abs(dx0 * dy1 - dy0 * dx1);
676
-
677
- if(!cross0) return true
678
-
679
- let rat = (cross0/cross1);
680
-
681
- if (rat<1.1 ) {
682
-
683
- isFlat = true;
684
- }
685
-
686
- return isFlat;
687
-
688
- }
689
-
690
758
  /**
691
759
  * sloppy distance calculation
692
760
  * based on x/y differences
@@ -710,27 +778,23 @@ function getDistAv(pt1, pt2) {
710
778
  * split compound paths into
711
779
  * sub path data array
712
780
  */
713
- function splitSubpaths(pathData) {
714
781
 
782
+ function splitSubpaths(pathData) {
715
783
  let subPathArr = [];
784
+ let current = [pathData[0]];
785
+ let l = pathData.length;
716
786
 
717
-
718
- try{
719
- let subPathIndices = pathData.map((com, i) => (com.type.toLowerCase() === 'm' ? i : -1)).filter(i => i !== -1);
787
+ for (let i = 1; i < l; i++) {
788
+ let com = pathData[i];
720
789
 
721
- }catch{
722
- console.log('catch', pathData);
790
+ if (com.type === 'M' || com.type === 'm') {
791
+ subPathArr.push(current);
792
+ current = [];
793
+ }
794
+ current.push(com);
723
795
  }
724
796
 
725
- let subPathIndices = pathData.map((com, i) => (com.type.toLowerCase() === 'm' ? i : -1)).filter(i => i !== -1);
726
-
727
- // no compound path
728
- if (subPathIndices.length === 1) {
729
- return [pathData]
730
- }
731
- subPathIndices.forEach((index, i) => {
732
- subPathArr.push(pathData.slice(index, subPathIndices[i + 1]));
733
- });
797
+ if (current.length) subPathArr.push(current);
734
798
 
735
799
  return subPathArr;
736
800
  }
@@ -1154,6 +1218,73 @@ function getPathDataPoly(pathData) {
1154
1218
  return poly;
1155
1219
  }
1156
1220
 
1221
+ /**
1222
+ * get exact path BBox
1223
+ * calculating extremes for all command types
1224
+ */
1225
+
1226
+ function getPathDataBBox(pathData) {
1227
+
1228
+ // save extreme values
1229
+ let xMin = Infinity;
1230
+ let xMax = -Infinity;
1231
+ let yMin = Infinity;
1232
+ let yMax = -Infinity;
1233
+
1234
+ const setXYmaxMin = (pt) => {
1235
+ if (pt.x < xMin) {
1236
+ xMin = pt.x;
1237
+ }
1238
+ if (pt.x > xMax) {
1239
+ xMax = pt.x;
1240
+ }
1241
+ if (pt.y < yMin) {
1242
+ yMin = pt.y;
1243
+ }
1244
+ if (pt.y > yMax) {
1245
+ yMax = pt.y;
1246
+ }
1247
+ };
1248
+
1249
+ for (let i = 0; i < pathData.length; i++) {
1250
+ let com = pathData[i];
1251
+ let { type, values } = com;
1252
+ let valuesL = values.length;
1253
+ let comPrev = pathData[i - 1] ? pathData[i - 1] : pathData[i];
1254
+ let valuesPrev = comPrev.values;
1255
+ let valuesPrevL = valuesPrev.length;
1256
+
1257
+ if (valuesL) {
1258
+ let p0 = { x: valuesPrev[valuesPrevL - 2], y: valuesPrev[valuesPrevL - 1] };
1259
+ let p = { x: values[valuesL - 2], y: values[valuesL - 1] };
1260
+ // add final on path point
1261
+ setXYmaxMin(p);
1262
+
1263
+ if (type === 'C' || type === 'Q') {
1264
+ let cp1 = { x: values[0], y: values[1] };
1265
+ let cp2 = type === 'C' ? { x: values[2], y: values[3] } : cp1;
1266
+ let pts = type === 'C' ? [p0, cp1, cp2, p] : [p0, cp1, p];
1267
+
1268
+ let bezierExtremesT = getBezierExtremeT(pts);
1269
+ bezierExtremesT.forEach(t => {
1270
+ let pt = pointAtT(pts, t);
1271
+ setXYmaxMin(pt);
1272
+ });
1273
+ }
1274
+
1275
+ else if (type === 'A') {
1276
+ let arcExtremes = getArcExtemes(p0, values);
1277
+ arcExtremes.forEach(pt => {
1278
+ setXYmaxMin(pt);
1279
+ });
1280
+ }
1281
+ }
1282
+ }
1283
+
1284
+ let bbox = { x: xMin, y: yMin, width: xMax - xMin, height: yMax - yMin };
1285
+ return bbox
1286
+ }
1287
+
1157
1288
  /**
1158
1289
  * get pathdata area
1159
1290
  */
@@ -1347,13 +1478,6 @@ function pathDataToD(pathData, optimize = 0) {
1347
1478
  let beautify = optimize > 1;
1348
1479
  let minify = beautify || optimize ? false : true;
1349
1480
 
1350
- // Convert first "M" to "m" if followed by "l" (when minified)
1351
- /*
1352
- if (pathData[1].type === "l" && minify) {
1353
- pathData[0].type = "m";
1354
- }
1355
- */
1356
-
1357
1481
  let d = '';
1358
1482
  let separator_command = beautify ? `\n` : (minify ? '' : ' ');
1359
1483
  let separator_type = !minify ? ' ' : '';
@@ -1375,13 +1499,11 @@ function pathDataToD(pathData, optimize = 0) {
1375
1499
  }
1376
1500
 
1377
1501
  // Omit type for repeated commands
1378
- type = (com0.type === com.type && com.type.toLowerCase() !== 'm' && minify)
1502
+ type = (minify && com0.type === com.type && com.type.toLowerCase() !== 'm' )
1379
1503
  ? " "
1380
- : (
1381
- (com0.type === "M" && com.type === "L")
1382
- ) && minify
1504
+ : (minify && com0.type === "M" && com.type === "L"
1383
1505
  ? " "
1384
- : com.type;
1506
+ : com.type);
1385
1507
 
1386
1508
  // concatenate subsequent floating point values
1387
1509
  if (minify) {
@@ -1429,7 +1551,7 @@ function pathDataToD(pathData, optimize = 0) {
1429
1551
  return d;
1430
1552
  }
1431
1553
 
1432
- function getCombinedByDominant(com1, com2, maxDist = 0, tolerance = 1) {
1554
+ function getCombinedByDominant(com1, com2, maxDist = 0, tolerance = 1, debug = false) {
1433
1555
 
1434
1556
  // cubic Bézier derivative
1435
1557
  const cubicDerivative = (p0, p1, p2, p3, t) => {
@@ -1451,8 +1573,9 @@ function getCombinedByDominant(com1, com2, maxDist = 0, tolerance = 1) {
1451
1573
  let commands = [com1, com2];
1452
1574
 
1453
1575
  // detect dominant
1454
- let dist1 = getSquareDistance(com1.p0, com1.p);
1455
- let dist2 = getSquareDistance(com2.p0, com2.p);
1576
+ let dist1 = getDistAv(com1.p0, com1.p);
1577
+ let dist2 = getDistAv(com2.p0, com2.p);
1578
+
1456
1579
  let reverse = dist1 > dist2;
1457
1580
 
1458
1581
  // backup original commands
@@ -1511,11 +1634,9 @@ function getCombinedByDominant(com1, com2, maxDist = 0, tolerance = 1) {
1511
1634
  let dP = cubicDerivative(com2.p0, com2.cp1, com2.cp2, com2.p, t0);
1512
1635
  let r = sub(P, com1.p0);
1513
1636
 
1514
-
1515
1637
  t0 -= dot(r, dP) / dot(dP, dP);
1516
1638
 
1517
1639
  // construct merged cubic over [t0, 1]
1518
-
1519
1640
  let Q0 = pointAtT([com2.p0, com2.cp1, com2.cp2, com2.p], t0);
1520
1641
  let Q3 = com2.p;
1521
1642
 
@@ -1532,6 +1653,7 @@ function getCombinedByDominant(com1, com2, maxDist = 0, tolerance = 1) {
1532
1653
  cp1: Q1,
1533
1654
  cp2: Q2,
1534
1655
  p: Q3,
1656
+ t0
1535
1657
  };
1536
1658
 
1537
1659
  if (reverse) {
@@ -1540,10 +1662,11 @@ function getCombinedByDominant(com1, com2, maxDist = 0, tolerance = 1) {
1540
1662
  cp1: Q2,
1541
1663
  cp2: Q1,
1542
1664
  p: Q0,
1665
+ t0
1543
1666
  };
1544
1667
  }
1545
1668
 
1546
- let tMid = (1 - t0)*0.5 ;
1669
+ let tMid = (1 - t0) * 0.5;
1547
1670
 
1548
1671
  let ptM = pointAtT([result.p0, result.cp1, result.cp2, result.p], tMid, false, true);
1549
1672
  let seg1_cp2 = ptM.cpts[2];
@@ -1551,21 +1674,22 @@ function getCombinedByDominant(com1, com2, maxDist = 0, tolerance = 1) {
1551
1674
  let ptI_1 = checkLineIntersection(ptM, seg1_cp2, result.p0, ptI, false);
1552
1675
  let ptI_2 = checkLineIntersection(ptM, seg1_cp2, result.p, ptI, false);
1553
1676
 
1554
- let cp1_2 = interpolate(result.p0, ptI_1, 1.333);
1555
- let cp2_2 = interpolate(result.p, ptI_2, 1.333);
1677
+ let cp1_2 = interpolate(result.p0, ptI_1, 1.333 );
1678
+ let cp2_2 = interpolate(result.p, ptI_2, 1.333 );
1556
1679
 
1557
1680
  // test self intersections and exit
1558
- let cp_intersection = checkLineIntersection(com1_o.p0, cp1_2, com2_o.p, cp2_2, true );
1559
- if(cp_intersection){
1681
+ let cp_intersection = checkLineIntersection(com1_o.p0, cp1_2, com2_o.p, cp2_2, true);
1682
+ if (cp_intersection) {
1560
1683
 
1561
1684
  return commands;
1562
1685
  }
1563
1686
 
1687
+ if (debug) renderPoint(markers, ptM, 'purple');
1688
+
1564
1689
  result.cp1 = cp1_2;
1565
1690
  result.cp2 = cp2_2;
1566
1691
 
1567
- // check distances
1568
-
1692
+ // check distances between original starting point and extrapolated
1569
1693
  let dist3 = getDistAv(com1_o.p0, result.p0);
1570
1694
  let dist4 = getDistAv(com2_o.p, result.p);
1571
1695
  let dist5 = (dist3 + dist4);
@@ -1577,11 +1701,34 @@ function getCombinedByDominant(com1, com2, maxDist = 0, tolerance = 1) {
1577
1701
  result.corner = com2_o.corner;
1578
1702
  result.dimA = com2_o.dimA;
1579
1703
  result.directionChange = com2_o.directionChange;
1704
+ result.type = 'C';
1580
1705
  result.values = [result.cp1.x, result.cp1.y, result.cp2.x, result.cp2.y, result.p.x, result.p.y];
1581
1706
 
1582
- // check if completely off
1707
+ // extrapolated starting point is not completely off
1583
1708
  if (dist5 < maxDist) {
1584
1709
 
1710
+ /*
1711
+ let tTotal = 1 + Math.abs(t0);
1712
+ let tSplit = reverse ? 1 + t0 : Math.abs(t0);
1713
+
1714
+ let pO = pointAtT([com2_o.p0, com2_o.cp1, com2_o.cp2, com2_o.p], t0);
1715
+ */
1716
+
1717
+ // split t to meet original mid segment start point
1718
+ let tSplit = reverse ? 1 + t0 : Math.abs(t0);
1719
+
1720
+ let tTotal = 1 + Math.abs(t0);
1721
+ tSplit = reverse ? 1 + t0 : Math.abs(t0) / tTotal;
1722
+
1723
+ let ptSplit = pointAtT([result.p0, result.cp1, result.cp2, result.p], tSplit);
1724
+ let distSplit = getDistAv(ptSplit, com1.p);
1725
+
1726
+ // not close enough - exit
1727
+ if (distSplit > maxDist * tolerance ) {
1728
+
1729
+ return commands;
1730
+ }
1731
+
1585
1732
  // compare combined with original area
1586
1733
  let pathData0 = [
1587
1734
  { type: 'M', values: [com1_o.p0.x, com1_o.p0.y] },
@@ -1598,129 +1745,198 @@ function getCombinedByDominant(com1, com2, maxDist = 0, tolerance = 1) {
1598
1745
  let areaN = getPathArea(pathDataN);
1599
1746
  let areaDiff = Math.abs(areaN / area0 - 1);
1600
1747
 
1601
- result.error = areaDiff * 10 * tolerance;
1748
+ result.error = areaDiff * 5 * tolerance;
1602
1749
 
1603
- pathDataToD(pathDataN);
1750
+ if (debug) {
1751
+ let d = pathDataToD(pathDataN);
1752
+ renderPath(markers, d, 'orange');
1753
+ }
1604
1754
 
1605
- // success
1606
- if (areaDiff < 0.01) {
1755
+ // success!!!
1756
+ if (areaDiff < 0.05 * tolerance) {
1607
1757
  commands = [result];
1608
1758
 
1609
- }
1610
-
1759
+ }
1611
1760
  }
1612
1761
 
1613
1762
  return commands
1614
1763
 
1615
1764
  }
1616
1765
 
1617
- function combineCubicPairs(com1, com2, extrapolateDominant = false, tolerance = 1) {
1766
+ function simplifyPathDataCubic(pathData, {
1767
+ keepExtremes = true,
1768
+ keepInflections = true,
1769
+ keepCorners = true,
1770
+ extrapolateDominant = true,
1771
+ tolerance = 1,
1772
+ } = {}) {
1618
1773
 
1619
- let commands = [com1, com2];
1620
- let t = findSplitT(com1, com2);
1774
+ let pathDataN = [pathData[0]];
1775
+ let l = pathData.length;
1621
1776
 
1622
- let distAv1 = getDistAv(com1.p0, com1.p);
1623
- let distAv2 = getDistAv(com2.p0, com2.p);
1624
- let distMin = Math.min(distAv1, distAv2);
1777
+ for (let i = 2; l && i <= l; i++) {
1778
+ let com = pathData[i - 1];
1779
+ let comN = i < l ? pathData[i] : null;
1780
+ let typeN = comN?.type || null;
1625
1781
 
1626
- let distScale = 0.05;
1627
- let maxDist = distMin * distScale * tolerance;
1782
+ let isDirChange = com?.directionChange || null;
1783
+ let isDirChangeN = comN?.directionChange || null;
1628
1784
 
1629
- let comS = getExtrapolatedCommand(com1, com2, t, t);
1785
+ let { type, values, p0, p, cp1 = null, cp2 = null, extreme = false, corner = false, dimA = 0 } = com;
1630
1786
 
1631
- // test on path point against original
1632
- let pt = pointAtT([comS.p0, comS.cp1, comS.cp2, comS.p], t);
1787
+ // next is also cubic
1788
+ if (type === 'C' && typeN === 'C') {
1633
1789
 
1634
- let dist0 = getDistAv(com1.p, pt);
1635
- let dist1 = 0, dist2 = 0;
1636
- let close = dist0 < maxDist;
1637
- let success = false;
1790
+ // cannot be combined as crossing extremes or corners
1791
+ if (
1792
+ (keepInflections && isDirChangeN) ||
1793
+ (keepCorners && corner) ||
1794
+ (!isDirChange && keepExtremes && extreme)
1795
+ ) {
1638
1796
 
1639
- // collect error data
1640
- let error = dist0;
1797
+ pathDataN.push(com);
1798
+ }
1641
1799
 
1642
- /*
1643
- if (com2.directionChange) {
1800
+ // try simplification
1801
+ else {
1644
1802
 
1645
- }
1646
- */
1803
+ let combined = combineCubicPairs(com, comN, {tolerance});
1804
+ let error = 0;
1647
1805
 
1648
- if (close) {
1806
+ // combining successful! try next segment
1807
+ if (combined.length === 1) {
1808
+ com = combined[0];
1809
+ let offset = 1;
1649
1810
 
1650
- /**
1651
- * check additional points
1652
- * to prevent distortions
1653
- */
1811
+ // add cumulative error to prevent distortions
1812
+ error += com.error;
1654
1813
 
1655
- // 2nd segment mid
1656
- let pt_2 = pointAtT([com2.p0, com2.cp1, com2.cp2, com2.p], 0.5);
1814
+ // find next candidates
1657
1815
 
1658
- // simplified path
1659
- let t3 = (1 + t) * 0.5;
1660
- let ptS_2 = pointAtT([comS.p0, comS.cp1, comS.cp2, comS.p], t3);
1661
- dist1 = getDistAv(pt_2, ptS_2);
1816
+ for (let n = i + 1; error < tolerance && n < l; n++) {
1817
+ let comN = pathData[n];
1818
+ if (comN.type !== 'C' ||
1819
+ (
1820
+ (keepInflections && comN.directionChange) ||
1821
+ (keepCorners && com.corner) ||
1822
+ (keepExtremes && com.extreme)
1823
+ )
1824
+ ) {
1825
+ break
1826
+ }
1662
1827
 
1663
- error += dist1;
1828
+ let combined = combineCubicPairs(com, comN, {tolerance});
1829
+ if (combined.length === 1) {
1830
+ // add cumulative error to prevent distortions
1664
1831
 
1665
- // quit - paths not congruent
1832
+ error += combined[0].error * 0.5;
1666
1833
 
1667
- if (dist1 < maxDist) {
1834
+ offset++;
1835
+ }
1836
+ com = combined[0];
1837
+ }
1668
1838
 
1669
- // 1st segment mid
1670
- let pt_1 = pointAtT([com1.p0, com1.cp1, com1.cp2, com1.p], 0.5);
1839
+ pathDataN.push(com);
1671
1840
 
1672
- let t2 = t * 0.5;
1673
- let ptS_1 = pointAtT([comS.p0, comS.cp1, comS.cp2, comS.p], t2);
1674
- dist2 = getDistAv(pt_1, ptS_1);
1841
+ if (i < l) {
1842
+ i += offset;
1843
+ }
1675
1844
 
1676
- /*
1677
- if(dist1>tolerance){
1678
- renderPoint(markers, pt_1, 'blue')
1679
- renderPoint(markers, ptS_1, 'orange', '0.5%')
1845
+ } else {
1846
+ pathDataN.push(com);
1847
+ }
1680
1848
  }
1681
- */
1682
-
1683
- // quit - paths not congruent
1684
- if (dist1 + dist2 < maxDist) success = true;
1685
1849
 
1686
- // collect error data
1687
- error += dist2;
1850
+ } // end of bezier command
1688
1851
 
1852
+ // other commands
1853
+ else {
1854
+ pathDataN.push(com);
1689
1855
  }
1690
1856
 
1691
- } // end 1st try
1857
+ } // end command loop
1692
1858
 
1693
-
1694
- /*
1695
- if (extrapolateDominant && com2.extreme) {
1696
- renderPoint(markers, com2.p)
1859
+ return pathDataN
1860
+ }
1697
1861
 
1698
- }
1699
- */
1862
+ function combineCubicPairs(com1, com2, {
1863
+ tolerance = 1
1864
+ } = {}) {
1865
+
1866
+ let commands = [com1, com2];
1867
+
1868
+ // assume 2 segments are result of a segment split
1869
+ let t = findSplitT(com1, com2);
1870
+
1871
+ let distAv1 = getDistAv(com1.p0, com1.p);
1872
+ let distAv2 = getDistAv(com2.p0, com2.p);
1873
+ let distMin = Math.max(0, Math.min(distAv1, distAv2));
1874
+
1875
+ let distScale = 0.06;
1876
+ let maxDist = distMin * distScale * tolerance;
1877
+
1878
+ // get hypothetical combined command
1879
+ let comS = getExtrapolatedCommand(com1, com2, t);
1880
+
1881
+ // test new point-at-t against original mid segment starting point
1882
+ let pt = pointAtT([comS.p0, comS.cp1, comS.cp2, comS.p], t);
1883
+
1884
+ let dist0 = getDistAv(com1.p, pt);
1885
+ let dist1 = 0, dist2 = 0;
1886
+ let close = dist0 < maxDist;
1887
+ let success = false;
1888
+
1889
+ // collect error data
1890
+ let error = dist0;
1891
+
1892
+ if (close) {
1893
+
1894
+ /**
1895
+ * check additional points
1896
+ * to prevent distortions
1897
+ */
1898
+
1899
+ // 2nd segment mid
1900
+ let pt_2 = pointAtT([com2.p0, com2.cp1, com2.cp2, com2.p], 0.5);
1901
+
1902
+ // simplified path
1903
+ let t3 = (1 + t) * 0.5;
1904
+ let ptS_2 = pointAtT([comS.p0, comS.cp1, comS.cp2, comS.p], t3);
1905
+ dist1 = getDistAv(pt_2, ptS_2);
1906
+
1907
+ error += dist1;
1908
+
1909
+ // quit - paths not congruent
1910
+
1911
+ if (dist1 < maxDist) {
1700
1912
 
1701
-
1913
+ // 1st segment mid
1914
+ let pt_1 = pointAtT([com1.p0, com1.cp1, com1.cp2, com1.p], 0.5);
1702
1915
 
1703
- // try extrapolated dominant curve
1916
+ let t2 = t * 0.5;
1917
+ let ptS_1 = pointAtT([comS.p0, comS.cp1, comS.cp2, comS.p], t2);
1918
+ dist2 = getDistAv(pt_1, ptS_1);
1704
1919
 
1705
- // && !com1.extreme
1706
- if (extrapolateDominant && !success ) {
1920
+ /*
1921
+ if(dist1>tolerance){
1922
+ renderPoint(markers, pt_1, 'blue')
1923
+ renderPoint(markers, ptS_1, 'orange', '0.5%')
1924
+ }
1925
+ */
1707
1926
 
1708
- let combinedEx = getCombinedByDominant(com1, com2, maxDist, tolerance);
1927
+ // quit - paths not congruent
1928
+ if (dist1 + dist2 < maxDist) success = true;
1709
1929
 
1710
- if(combinedEx.length===1){
1711
- success = true;
1712
- comS = combinedEx[0];
1713
- error = comS.error;
1930
+ // collect error data
1931
+ error += dist2;
1714
1932
 
1715
1933
  }
1716
1934
 
1717
-
1718
- }
1935
+ } // end 1st try
1719
1936
 
1720
1937
  // add meta
1721
1938
  if (success) {
1722
1939
 
1723
-
1724
1940
  // correct to exact start and end points
1725
1941
  comS.p0 = com1.p0;
1726
1942
  comS.p = com2.p;
@@ -1743,50 +1959,69 @@ function combineCubicPairs(com1, com2, extrapolateDominant = false, tolerance =
1743
1959
  return commands;
1744
1960
  }
1745
1961
 
1746
- function getExtrapolatedCommand(com1, com2, t1 = 0, t2 = 0) {
1962
+ function getExtrapolatedCommand(com1, com2, t = 0) {
1747
1963
 
1748
1964
  let { p0, cp1 } = com1;
1749
1965
  let { p, cp2 } = com2;
1750
1966
 
1751
1967
  // extrapolate control points
1752
- let cp1_S = {
1753
- x: (cp1.x - (1 - t1) * p0.x) / t1,
1754
- y: (cp1.y - (1 - t1) * p0.y) / t1
1968
+ cp1 = {
1969
+ x: (cp1.x - (1 - t) * p0.x) / t,
1970
+ y: (cp1.y - (1 - t) * p0.y) / t
1755
1971
  };
1756
1972
 
1757
- let cp2_S = {
1758
- x: (cp2.x - t2 * p.x) / (1 - t2),
1759
- y: (cp2.y - t2 * p.y) / (1 - t2)
1973
+ cp2 = {
1974
+ x: (cp2.x - t * p.x) / (1 - t),
1975
+ y: (cp2.y - t * p.y) / (1 - t)
1760
1976
  };
1761
1977
 
1762
- let comS = { p0, cp1: cp1_S, cp2: cp2_S, p };
1978
+ return { p0, cp1, cp2, p };
1979
+ }
1980
+
1981
+ function findSplitT(com1, com2) {
1982
+
1983
+ let len3 = getDistance(com1.cp2, com1.p);
1984
+ let len4 = getDistance(com1.cp2, com2.cp1);
1763
1985
 
1764
- return comS
1986
+ let t = Math.min(len3) / len4;
1765
1987
 
1988
+ return t
1766
1989
  }
1767
1990
 
1768
- function findSplitT(com1, com2) {
1991
+ function commandIsFlat(points, {
1992
+ tolerance = 1,
1993
+ debug=false
1994
+ } = {}) {
1769
1995
 
1770
- // control tangent intersection
1771
- let pt1 = checkLineIntersection(com1.p0, com1.cp1, com2.cp2, com2.p, false);
1996
+ let isFlat=false;
1997
+ let report = {
1998
+ flat:true,
1999
+ steepness:0
2000
+ };
1772
2001
 
1773
- // intersection 2nd cp1 tangent and global tangent intersection
1774
- let ptI = checkLineIntersection(pt1, com2.p, com2.p0, com2.cp1, false);
2002
+ let p0 = points[0];
2003
+ let p = points[points.length - 1];
1775
2004
 
1776
- let len1 = getDistance(pt1, com2.p);
1777
- let len2 = getDistance(ptI, com2.p);
2005
+ let xSet = new Set([...points.map(pt => +pt.x.toFixed(8))]);
2006
+ let ySet = new Set([...points.map(pt => +pt.y.toFixed(8))]);
1778
2007
 
1779
- let t = 1 - len2 / len1;
2008
+ // must be flat
2009
+ if(xSet.size===1 || ySet.size===1) return !debug ? true : report;
1780
2010
 
1781
- // check self intersections
2011
+ let squareDist = getSquareDistance(p0, p);
2012
+ let threshold = squareDist / 1000 * tolerance;
2013
+ let area = getPolygonArea(points, true);
1782
2014
 
1783
- let len3 = getDistance(com1.cp2, com1.p);
1784
- let len4 = getDistance(com1.cp2, com2.cp1);
2015
+ // flat enough
2016
+ if(area < threshold) isFlat = true;
1785
2017
 
1786
- t = Math.min(len3) / len4;
2018
+ if(debug){
2019
+ report.flat = isFlat;
1787
2020
 
1788
- return t
2021
+ report.steepness = area/squareDist*10;
2022
+ }
1789
2023
 
2024
+ return !debug ? isFlat : report;
1790
2025
  }
1791
2026
 
1792
2027
  function analyzePathData(pathData = []) {
@@ -1831,7 +2066,6 @@ function analyzePathData(pathData = []) {
1831
2066
  * this way we can skip certain tests
1832
2067
  */
1833
2068
  let commandPts = [p0];
1834
- let isFlat = false;
1835
2069
 
1836
2070
  // init properties
1837
2071
  com.idx = c - 1;
@@ -1862,7 +2096,7 @@ function analyzePathData(pathData = []) {
1862
2096
  com.p0 = p0;
1863
2097
  com.p = p;
1864
2098
 
1865
- let cp1, cp2, cp1N, cp2N, pN, typeN, area1;
2099
+ let cp1, cp2, cp1N, pN, typeN, area1;
1866
2100
 
1867
2101
  let dimA = getDistAv(p0, p);
1868
2102
  com.dimA = dimA;
@@ -1902,13 +2136,18 @@ function analyzePathData(pathData = []) {
1902
2136
  if (type === 'C') commandPts.push(cp2);
1903
2137
  commandPts.push(p);
1904
2138
 
2139
+ /*
2140
+
1905
2141
  let commandFlatness = commandIsFlat(commandPts);
1906
2142
  isFlat = commandFlatness.flat;
1907
2143
  com.flat = isFlat;
1908
2144
 
1909
2145
  if (isFlat) {
1910
2146
  com.extreme = false;
2147
+
1911
2148
  }
2149
+ */
2150
+
1912
2151
  }
1913
2152
 
1914
2153
  /**
@@ -1917,7 +2156,7 @@ function analyzePathData(pathData = []) {
1917
2156
  * so we interpret maximum x/y on-path points as well as extremes
1918
2157
  * but we ignore linetos to allow chunk compilation
1919
2158
  */
1920
- if (!isFlat && type !== 'L' && (p.x === left || p.y === top || p.x === right || p.y === bottom)) {
2159
+ if (type !== 'L' && (p.x === left || p.y === top || p.x === right || p.y === bottom)) {
1921
2160
  com.extreme = true;
1922
2161
  }
1923
2162
 
@@ -1930,7 +2169,7 @@ function analyzePathData(pathData = []) {
1930
2169
  pN = comN ? { x: comNValsL[0], y: comNValsL[1] } : null;
1931
2170
 
1932
2171
  cp1N = { x: comN.values[0], y: comN.values[1] };
1933
- cp2N = comN.type === 'C' ? { x: comN.values[2], y: comN.values[3] } : null;
2172
+ comN.type === 'C' ? { x: comN.values[2], y: comN.values[3] } : null;
1934
2173
  }
1935
2174
 
1936
2175
  /**
@@ -1960,19 +2199,15 @@ function analyzePathData(pathData = []) {
1960
2199
  // check extremes
1961
2200
  let cpts = commandPts.slice(1);
1962
2201
 
1963
- let w = pN ? Math.abs(pN.x - p0.x) : 0;
1964
- let h = pN ? Math.abs(pN.y - p0.y) : 0;
1965
- let thresh = (w + h) / 2 * 0.1;
1966
- let pts1 = type === 'C' ? [p, cp1N, cp2N, pN] : [p, cp1N, pN];
1967
-
1968
- let flatness2 = commandIsFlat(pts1, thresh);
1969
- let isFlat2 = flatness2.flat;
2202
+ pN ? Math.abs(pN.x - p0.x) : 0;
2203
+ pN ? Math.abs(pN.y - p0.y) : 0;
1970
2204
 
1971
2205
  /**
1972
2206
  * if current and next cubic are flat
1973
2207
  * we don't flag them as extremes to allow simplification
1974
2208
  */
1975
- let hasExtremes = (isFlat && isFlat2) ? false : (!com.extreme ? bezierhasExtreme(p0, cpts, angleThreshold) : true);
2209
+
2210
+ let hasExtremes = (!com.extreme ? bezierhasExtreme(p0, cpts, angleThreshold) : true);
1976
2211
 
1977
2212
  if (hasExtremes) {
1978
2213
  com.extreme = true;
@@ -2018,11 +2253,11 @@ function detectAccuracy(pathData) {
2018
2253
 
2019
2254
  // Reference first MoveTo command (M)
2020
2255
  let M = { x: pathData[0].values[0], y: pathData[0].values[1] };
2021
- let p0 = M;
2256
+ let p0 = M;
2022
2257
  let p = M;
2023
2258
  pathData[0].decimals = 0;
2024
2259
 
2025
- let dims = new Set();
2260
+ let dims = [];
2026
2261
 
2027
2262
  // add average distances
2028
2263
  for (let i = 0, len = pathData.length; i < len; i++) {
@@ -2030,28 +2265,33 @@ function detectAccuracy(pathData) {
2030
2265
  let { type, values } = com;
2031
2266
 
2032
2267
  let lastVals = values.length ? values.slice(-2) : [M.x, M.y];
2033
- p={x:lastVals[0], y:lastVals[1]};
2268
+ p = { x: lastVals[0], y: lastVals[1] };
2034
2269
 
2035
2270
  // use existing averave dimension value or calculate
2036
- let dimA = com.dimA ? +com.dimA.toFixed(8) : type!=='M' ? +getDistAv(p0, p).toFixed(8) : 0;
2271
+ let dimA = com.dimA ? +com.dimA.toFixed(8) : type !== 'M' ? +getDistAv(p0, p).toFixed(8) : 0;
2037
2272
 
2038
- if(dimA) dims.add(dimA);
2039
-
2273
+ if (dimA) dims.push(dimA);
2040
2274
 
2041
- if(type==='M'){
2042
- M=p;
2275
+ if (type === 'M') {
2276
+ M = p;
2043
2277
  }
2044
2278
  p0 = p;
2045
2279
  }
2046
2280
 
2047
- let dim_min = Array.from(dims).sort();
2048
- let sliceIdx = Math.ceil(dim_min.length/8);
2049
- dim_min = dim_min.slice(0, sliceIdx );
2281
+ let dim_min = dims.sort();
2050
2282
 
2051
- let dimVal = dim_min.reduce((a,b)=>a+b, 0) / sliceIdx;
2283
+ /*
2284
+ let minVal = dim_min.length > 15 ?
2285
+ (dim_min[0] + dim_min[2]) / 2 :
2286
+ dim_min[0];
2287
+ */
2288
+
2289
+ let sliceIdx = Math.ceil(dim_min.length / 10);
2290
+ dim_min = dim_min.slice(0, sliceIdx);
2291
+ let minVal = dim_min.reduce((a, b) => a + b, 0) / sliceIdx;
2052
2292
 
2053
- let threshold = 50;
2054
- let decimalsAuto = dimVal > threshold ? 0 : Math.floor(threshold / dimVal).toString().length;
2293
+ let threshold = 40;
2294
+ let decimalsAuto = minVal > threshold*1.5 ? 0 : Math.floor(threshold / minVal).toString().length;
2055
2295
 
2056
2296
  // clamp
2057
2297
  return Math.min(Math.max(0, decimalsAuto), 8)
@@ -2064,19 +2304,24 @@ function detectAccuracy(pathData) {
2064
2304
  * based on suggested accuracy in path data
2065
2305
  */
2066
2306
  function roundPathData(pathData, decimals = -1) {
2067
- // has recommended decimals
2068
- let hasDecimal = decimals == 'auto' && pathData[0].hasOwnProperty('decimals') ? true : false;
2069
2307
 
2070
- for(let c=0, len=pathData.length; c<len; c++){
2071
- let com=pathData[c];
2308
+ let len = pathData.length;
2309
+
2310
+ for (let c = 0; c < len; c++) {
2311
+
2312
+ let values = pathData[c].values;
2313
+ let valLen = values.length;
2072
2314
 
2073
- if (decimals >-1 || hasDecimal) {
2074
- decimals = hasDecimal ? com.decimals : decimals;
2315
+ if (valLen && (decimals > -1) ) {
2075
2316
 
2076
- pathData[c].values = com.values.map(val=>{return val ? +val.toFixed(decimals) : val });
2317
+ for(let v=0; v<valLen; v++){
2077
2318
 
2319
+ pathData[c].values[v] = +values[v].toFixed(decimals);
2320
+ }
2078
2321
  }
2079
- } return pathData;
2322
+ }
2323
+
2324
+ return pathData;
2080
2325
  }
2081
2326
 
2082
2327
  function revertCubicQuadratic(p0 = {}, cp1 = {}, cp2 = {}, p = {}) {
@@ -2094,7 +2339,7 @@ function revertCubicQuadratic(p0 = {}, cp1 = {}, cp2 = {}, p = {}) {
2094
2339
  let values = [cp1.x, cp1.y, cp2.x, cp2.y, p.x, p.y];
2095
2340
  let comN = {type, values};
2096
2341
 
2097
- if (dist1 < threshold) {
2342
+ if (dist1 && threshold && dist1 < threshold) {
2098
2343
  cp1_Q = checkLineIntersection(p0, cp1, p, cp2, false);
2099
2344
  if (cp1_Q) {
2100
2345
 
@@ -2119,9 +2364,10 @@ function convertPathData(pathData, {
2119
2364
  if (toShorthands) pathData = pathDataToShorthands(pathData);
2120
2365
 
2121
2366
  // pre round - before relative conversion to minimize distortions
2122
- pathData = roundPathData(pathData, decimals);
2367
+ if(decimals>-1 && toRelative) pathData = roundPathData(pathData, decimals);
2123
2368
  if (toRelative) pathData = pathDataToRelative(pathData);
2124
2369
  if (decimals > -1) pathData = roundPathData(pathData, decimals);
2370
+
2125
2371
  return pathData
2126
2372
  }
2127
2373
 
@@ -2423,7 +2669,7 @@ function pathDataToLonghands(pathData, decimals = -1, test = true) {
2423
2669
  * L, L, C, Q => H, V, S, T
2424
2670
  * reversed method: pathDataToLonghands()
2425
2671
  */
2426
- function pathDataToShorthands(pathData, decimals = -1, test = true) {
2672
+ function pathDataToShorthands(pathData, decimals = -1, test = false) {
2427
2673
 
2428
2674
  /**
2429
2675
  * analyze pathdata – if you're sure your data is already absolute skip it via test=false
@@ -2434,29 +2680,28 @@ function pathDataToShorthands(pathData, decimals = -1, test = true) {
2434
2680
  hasRel = /[astvqmhlc]/g.test(commandTokens);
2435
2681
  }
2436
2682
 
2437
- pathData = test && hasRel ? pathDataToAbsolute(pathData, decimals) : pathData;
2683
+ pathData = test && hasRel ? pathDataToAbsoluteOrRelative(pathData) : pathData;
2684
+
2685
+ let len = pathData.length;
2686
+ let pathDataShorts = new Array(len);
2438
2687
 
2439
2688
  let comShort = {
2440
2689
  type: "M",
2441
2690
  values: pathData[0].values
2442
2691
  };
2443
2692
 
2444
- if (pathData[0].decimals) {
2445
-
2446
- comShort.decimals = pathData[0].decimals;
2447
- }
2448
-
2449
- let pathDataShorts = [comShort];
2693
+ pathDataShorts[0] = comShort;
2450
2694
 
2451
2695
  let p0 = { x: pathData[0].values[0], y: pathData[0].values[1] };
2452
2696
  let p;
2453
2697
  let tolerance = 0.01;
2454
2698
 
2455
- for (let i = 1, len = pathData.length; i < len; i++) {
2699
+ for (let i = 1; i < len; i++) {
2456
2700
 
2457
2701
  let com = pathData[i];
2458
2702
  let { type, values } = com;
2459
- let valuesLast = values.slice(-2);
2703
+ let valuesLen = values.length;
2704
+ let valuesLast = [values[valuesLen-2], values[valuesLen-1]];
2460
2705
 
2461
2706
  // previoius command
2462
2707
  let comPrev = pathData[i - 1];
@@ -2504,7 +2749,8 @@ function pathDataToShorthands(pathData, decimals = -1, test = true) {
2504
2749
  if (typePrev !== 'Q') {
2505
2750
 
2506
2751
  p0 = { x: valuesLast[0], y: valuesLast[1] };
2507
- pathDataShorts.push(com);
2752
+
2753
+ pathDataShorts[i] = com;
2508
2754
  continue;
2509
2755
  }
2510
2756
 
@@ -2533,7 +2779,8 @@ function pathDataToShorthands(pathData, decimals = -1, test = true) {
2533
2779
 
2534
2780
  if (typePrev !== 'C') {
2535
2781
 
2536
- pathDataShorts.push(com);
2782
+ pathDataShorts[i] = com;
2783
+
2537
2784
  p0 = { x: valuesLast[0], y: valuesLast[1] };
2538
2785
  continue;
2539
2786
  }
@@ -2575,8 +2822,10 @@ function pathDataToShorthands(pathData, decimals = -1, test = true) {
2575
2822
  }
2576
2823
 
2577
2824
  p0 = { x: valuesLast[0], y: valuesLast[1] };
2578
- pathDataShorts.push(comShort);
2825
+ pathDataShorts[i] = comShort;
2826
+
2579
2827
  }
2828
+
2580
2829
  return pathDataShorts;
2581
2830
  }
2582
2831
 
@@ -3021,10 +3270,10 @@ function normalizePathData(pathData = [],
3021
3270
  quadraticToCubic = false,
3022
3271
  arcToCubic = false,
3023
3272
  arcAccuracy = 2,
3024
- } = {},
3025
3273
 
3026
- {
3274
+ // assume we need full normalization
3027
3275
  hasRelatives = true, hasShorthands = true, hasQuadratics = true, hasArcs = true, testTypes = false
3276
+
3028
3277
  } = {}
3029
3278
  ) {
3030
3279
 
@@ -3075,15 +3324,28 @@ function parsePathDataNormalized(d,
3075
3324
  } = {}
3076
3325
  ) {
3077
3326
 
3078
- let pathDataObj = parsePathDataString(d);
3079
- let { hasRelatives, hasShorthands, hasQuadratics, hasArcs } = pathDataObj;
3080
- let pathData = pathDataObj.pathData;
3327
+ // is already array
3328
+ let isArray = Array.isArray(d);
3329
+
3330
+ // normalize native pathData to regular array
3331
+ let hasConstructor = isArray && typeof d[0] === 'object' && typeof d[0].constructor === 'function';
3332
+ /*
3333
+ if (hasConstructor) {
3334
+ d = d.map(com => { return { type: com.type, values: com.values } })
3335
+ console.log('hasConstructor', hasConstructor, (typeof d[0].constructor), d);
3336
+ }
3337
+ */
3338
+
3339
+ let pathDataObj = isArray ? d : parsePathDataString(d);
3340
+
3341
+ let { hasRelatives = true, hasShorthands = true, hasQuadratics = true, hasArcs = true } = pathDataObj;
3342
+ let pathData = hasConstructor ? pathDataObj : pathDataObj.pathData;
3081
3343
 
3082
3344
  // normalize
3083
3345
  pathData = normalizePathData(pathData,
3084
- { toAbsolute, toLonghands, quadraticToCubic, arcToCubic, arcAccuracy },
3085
-
3086
- { hasRelatives, hasShorthands, hasQuadratics, hasArcs }
3346
+ { toAbsolute, toLonghands, quadraticToCubic, arcToCubic, arcAccuracy,
3347
+ hasRelatives, hasShorthands, hasQuadratics, hasArcs
3348
+ },
3087
3349
  );
3088
3350
 
3089
3351
  return pathData;
@@ -3435,7 +3697,8 @@ function parsePathDataString(d, debug = true) {
3435
3697
  if (debug === 'log') {
3436
3698
  console.log(feedback);
3437
3699
  } else {
3438
- throw new Error(feedback)
3700
+
3701
+ console.warn(feedback);
3439
3702
  }
3440
3703
  }
3441
3704
 
@@ -3463,18 +3726,266 @@ function parsePathDataString(d, debug = true) {
3463
3726
 
3464
3727
  }
3465
3728
 
3466
- function pathDataRemoveColinear(pathData, tolerance = 1, flatBezierToLinetos = true) {
3729
+ function stringifyPathData(pathData) {
3730
+ return pathData.map(com => { return `${com.type} ${com.values.join(' ')}` }).join(' ');
3731
+ }
3732
+
3733
+ function shapeElToPath(el) {
3734
+
3735
+ let nodeName = el.nodeName.toLowerCase();
3736
+ if (nodeName === 'path') return el;
3737
+
3738
+ let pathData = getPathDataFromEl(el);
3739
+ let d = pathData.map(com => { return `${com.type} ${com.values} ` }).join(' ');
3740
+ let attributes = [...el.attributes].map(att => att.name);
3741
+
3742
+ let pathN = document.createElementNS('http://www.w3.org/2000/svg', 'path');
3743
+ pathN.setAttribute('d', d);
3744
+
3745
+ let exclude = ['x', 'y', 'cx', 'cy', 'dx', 'dy', 'r', 'rx', 'ry', 'width', 'height', 'points'];
3746
+
3747
+ attributes.forEach(att => {
3748
+ if (!exclude.includes(att)) {
3749
+ let val = el.getAttribute(att);
3750
+ pathN.setAttribute(att, val);
3751
+ }
3752
+ });
3753
+
3754
+ return pathN
3755
+
3756
+ }
3757
+
3758
+ // retrieve pathdata from svg geometry elements
3759
+ function getPathDataFromEl(el, stringify = false) {
3760
+
3761
+ let pathData = [];
3762
+ let type = el.nodeName;
3763
+ let atts, attNames, d, x, y, width, height, r, rx, ry, cx, cy, x1, x2, y1, y2;
3764
+
3765
+ // convert relative or absolute units
3766
+ const svgElUnitsToPixel = (el, decimals = 9) => {
3767
+
3768
+ const svg = el.nodeName !== "svg" ? el.closest("svg") : el;
3769
+
3770
+ // convert real life units to pixels
3771
+ const translateUnitToPixel = (value) => {
3772
+
3773
+ if (value === null) {
3774
+ return 0
3775
+ }
3776
+
3777
+ let dpi = 96;
3778
+ let unit = value.match(/([a-z]+)/gi);
3779
+ unit = unit ? unit[0] : "";
3780
+ let val = parseFloat(value);
3781
+ let rat;
3782
+
3783
+ // no unit - already pixes/user unit
3784
+ if (!unit) {
3785
+ return val;
3786
+ }
3787
+
3788
+ switch (unit) {
3789
+ case "in":
3790
+ rat = dpi;
3791
+ break;
3792
+ case "pt":
3793
+ rat = (1 / 72) * 96;
3794
+ break;
3795
+ case "cm":
3796
+ rat = (1 / 2.54) * 96;
3797
+ break;
3798
+ case "mm":
3799
+ rat = ((1 / 2.54) * 96) / 10;
3800
+ break;
3801
+ // just a default approximation
3802
+ case "em":
3803
+ case "rem":
3804
+ rat = 16;
3805
+ break;
3806
+ default:
3807
+ rat = 1;
3808
+ }
3809
+ let valuePx = val * rat;
3810
+ return +valuePx.toFixed(decimals);
3811
+ };
3812
+
3813
+ // svg width and height attributes
3814
+ let width = svg.getAttribute("width");
3815
+ width = width ? translateUnitToPixel(width) : 300;
3816
+ let height = svg.getAttribute("height");
3817
+ height = width ? translateUnitToPixel(height) : 150;
3818
+
3819
+ let vB = svg.getAttribute("viewBox");
3820
+ vB = vB
3821
+ ? vB
3822
+ .replace(/,/g, " ")
3823
+ .split(" ")
3824
+ .filter(Boolean)
3825
+ .map((val) => {
3826
+ return +val;
3827
+ })
3828
+ : [];
3829
+
3830
+ let w = vB.length ? vB[2] : width;
3831
+ let h = vB.length ? vB[3] : height;
3832
+ let scaleX = w / 100;
3833
+ let scaleY = h / 100;
3834
+ let scalRoot = Math.sqrt((Math.pow(scaleX, 2) + Math.pow(scaleY, 2)) / 2);
3835
+
3836
+ let attsH = ["x", "width", "x1", "x2", "rx", "cx", "r"];
3837
+ let attsV = ["y", "height", "y1", "y2", "ry", "cy"];
3838
+
3839
+ let atts = el.getAttributeNames();
3840
+ atts.forEach((att) => {
3841
+ let val = el.getAttribute(att);
3842
+ let valAbs = val;
3843
+ if (attsH.includes(att) || attsV.includes(att)) {
3844
+ let scale = attsH.includes(att) ? scaleX : scaleY;
3845
+ scale = att === "r" && w != h ? scalRoot : scale;
3846
+ let unit = val.match(/([a-z|%]+)/gi);
3847
+ unit = unit ? unit[0] : "";
3848
+ if (val.includes("%")) {
3849
+ valAbs = parseFloat(val) * scale;
3850
+ }
3851
+
3852
+ else {
3853
+ valAbs = translateUnitToPixel(val);
3854
+ }
3855
+ el.setAttribute(att, +valAbs);
3856
+ }
3857
+ });
3858
+ };
3859
+
3860
+ svgElUnitsToPixel(el);
3861
+
3862
+ const getAtts = (attNames) => {
3863
+ atts = {};
3864
+ attNames.forEach(att => {
3865
+ atts[att] = +el.getAttribute(att);
3866
+ });
3867
+ return atts
3868
+ };
3869
+
3870
+ switch (type) {
3871
+ case 'path':
3872
+ d = el.getAttribute("d");
3873
+ pathData = parsePathDataNormalized(d);
3874
+ break;
3875
+
3876
+ case 'rect':
3877
+ attNames = ['x', 'y', 'width', 'height', 'rx', 'ry'];
3878
+ ({ x, y, width, height, rx, ry } = getAtts(attNames));
3879
+
3880
+ if (!rx && !ry) {
3881
+ pathData = [
3882
+ { type: "M", values: [x, y] },
3883
+ { type: "L", values: [x + width, y] },
3884
+ { type: "L", values: [x + width, y + height] },
3885
+ { type: "L", values: [x, y + height] },
3886
+ { type: "Z", values: [] }
3887
+ ];
3888
+ } else {
3889
+
3890
+ if (rx > width / 2) {
3891
+ rx = width / 2;
3892
+ }
3893
+ if (ry > height / 2) {
3894
+ ry = height / 2;
3895
+ }
3896
+ pathData = [
3897
+ { type: "M", values: [x + rx, y] },
3898
+ { type: "L", values: [x + width - rx, y] },
3899
+ { type: "A", values: [rx, ry, 0, 0, 1, x + width, y + ry] },
3900
+ { type: "L", values: [x + width, y + height - ry] },
3901
+ { type: "A", values: [rx, ry, 0, 0, 1, x + width - rx, y + height] },
3902
+ { type: "L", values: [x + rx, y + height] },
3903
+ { type: "A", values: [rx, ry, 0, 0, 1, x, y + height - ry] },
3904
+ { type: "L", values: [x, y + ry] },
3905
+ { type: "A", values: [rx, ry, 0, 0, 1, x + rx, y] },
3906
+ { type: "Z", values: [] }
3907
+ ];
3908
+ }
3909
+ break;
3910
+
3911
+ case 'circle':
3912
+ case 'ellipse':
3913
+
3914
+ attNames = ['cx', 'cy', 'rx', 'ry', 'r'];
3915
+ ({ cx, cy, r, rx, ry } = getAtts(attNames));
3916
+
3917
+ let isCircle = type === 'circle';
3918
+
3919
+ if (isCircle) {
3920
+ r = r;
3921
+ rx = r;
3922
+ ry = r;
3923
+ } else {
3924
+ rx = rx ? rx : r;
3925
+ ry = ry ? ry : r;
3926
+ }
3927
+
3928
+ // simplified radii for cirecles
3929
+ let rxS = isCircle && r>=1 ? 1 : rx;
3930
+ let ryS = isCircle && r>=1 ? 1 : rx;
3931
+
3932
+ pathData = [
3933
+ { type: "M", values: [cx + rx, cy] },
3934
+ { type: "A", values: [rxS, ryS, 0, 1, 1, cx - rx, cy] },
3935
+ { type: "A", values: [rxS, ryS, 0, 1, 1, cx + rx, cy] },
3936
+ ];
3937
+
3938
+ break;
3939
+ case 'line':
3940
+ attNames = ['x1', 'y1', 'x2', 'y2'];
3941
+ ({ x1, y1, x2, y2 } = getAtts(attNames));
3942
+ pathData = [
3943
+ { type: "M", values: [x1, y1] },
3944
+ { type: "L", values: [x2, y2] }
3945
+ ];
3946
+ break;
3947
+ case 'polygon':
3948
+ case 'polyline':
3949
+
3950
+ let points = el.getAttribute('points').replaceAll(',', ' ').split(' ').filter(Boolean);
3951
+
3952
+ for (let i = 0; i < points.length; i += 2) {
3953
+ pathData.push({
3954
+ type: (i === 0 ? "M" : "L"),
3955
+ values: [+points[i], +points[i + 1]]
3956
+ });
3957
+ }
3958
+ if (type === 'polygon') {
3959
+ pathData.push({
3960
+ type: "Z",
3961
+ values: []
3962
+ });
3963
+ }
3964
+ break;
3965
+ }
3966
+
3967
+ return stringify ? stringifyPathData(pathData) : pathData;
3968
+
3969
+ }
3970
+
3971
+ function pathDataRemoveColinear(pathData, {
3972
+ tolerance = 1,
3973
+
3974
+ flatBezierToLinetos = true
3975
+ }={}) {
3467
3976
 
3468
3977
  let pathDataN = [pathData[0]];
3978
+
3469
3979
  let M = { x: pathData[0].values[0], y: pathData[0].values[1] };
3470
3980
  let p0 = M;
3471
3981
  let p = M;
3472
3982
  pathData[pathData.length - 1].type.toLowerCase() === 'z';
3473
3983
 
3474
3984
  for (let c = 1, l = pathData.length; c < l; c++) {
3475
- let comPrev = pathData[c - 1];
3985
+
3476
3986
  let com = pathData[c];
3477
3987
  let comN = pathData[c + 1] || pathData[l - 1];
3988
+
3478
3989
  let p1 = comN.type.toLowerCase() === 'z' ? M : { x: comN.values[comN.values.length - 2], y: comN.values[comN.values.length - 1] };
3479
3990
 
3480
3991
  let { type, values } = com;
@@ -3483,11 +3994,9 @@ function pathDataRemoveColinear(pathData, tolerance = 1, flatBezierToLinetos = t
3483
3994
 
3484
3995
  let area = getPolygonArea([p0, p, p1], true);
3485
3996
 
3486
- getSquareDistance(p0, p);
3487
- getSquareDistance(p, p1);
3488
3997
  let distSquare = getSquareDistance(p0, p1);
3489
3998
 
3490
- let distMax = distSquare / 200 * tolerance;
3999
+ let distMax = distSquare ? distSquare / 333 * tolerance : 0;
3491
4000
 
3492
4001
  let isFlat = area < distMax;
3493
4002
  let isFlatBez = false;
@@ -3501,30 +4010,38 @@ function pathDataRemoveColinear(pathData, tolerance = 1, flatBezierToLinetos = t
3501
4010
  [{ x: values[0], y: values[1] }, { x: values[2], y: values[3] }] :
3502
4011
  (type === 'Q' ? [{ x: values[0], y: values[1] }] : []);
3503
4012
 
3504
- isFlatBez = checkBezierFlatness(p0, cpts, p);
3505
- // console.log();
4013
+ isFlatBez = commandIsFlat([p0, ...cpts, p],{tolerance});
3506
4014
 
3507
- if (isFlatBez && c < l - 1 && comPrev.type !== 'C') {
4015
+ if (isFlatBez && c < l - 1 ) {
3508
4016
  type = "L";
3509
4017
  com.type = "L";
3510
4018
  com.values = valsL;
3511
4019
 
3512
4020
  }
3513
-
3514
4021
  }
3515
4022
 
3516
- // update end point
3517
- p0 = p;
3518
-
3519
4023
  // colinear – exclude arcs (as always =) as semicircles won't have an area
3520
4024
 
3521
4025
  if ( isFlat && c < l - 1 && (type === 'L' || (flatBezierToLinetos && isFlatBez)) ) {
3522
-
3523
4026
 
4027
+ /*
4028
+ console.log(area, distMax );
4029
+
4030
+ if(p0.x === p.x && p0.y === p.y){
4031
+
4032
+ }
4033
+
4034
+ renderPoint(markers, p0, 'blue', '1.5%', '1')
4035
+ renderPoint(markers, p, 'red', '1%', '1')
4036
+ renderPoint(markers, p1, 'cyan', '0.5%', '1')
4037
+ */
3524
4038
 
3525
4039
  continue;
3526
4040
  }
3527
4041
 
4042
+ // update end point
4043
+ p0 = p;
4044
+
3528
4045
  if (type === 'M') {
3529
4046
  M = p;
3530
4047
  p0 = M;
@@ -3543,20 +4060,44 @@ function pathDataRemoveColinear(pathData, tolerance = 1, flatBezierToLinetos = t
3543
4060
 
3544
4061
  }
3545
4062
 
4063
+ function removeOrphanedM(pathData) {
4064
+
4065
+ let pathDataN = [];
4066
+ for (let i = 0, l = pathData.length; i < l; i++) {
4067
+ let com = pathData[i];
4068
+ if (!com) continue;
4069
+ let { type = null, values = [] } = com;
4070
+ let comN = pathData[i + 1] ? pathData[i + 1] : null;
4071
+ if ((type === 'M' || type === 'm')) {
4072
+
4073
+ if (!comN || (comN && (comN.type === 'Z' || comN.type === 'z'))) {
4074
+ if(comN) i++;
4075
+ continue
4076
+ }
4077
+ }
4078
+ pathDataN.push(com);
4079
+ }
4080
+
4081
+ return pathDataN;
4082
+
4083
+ }
4084
+
4085
+ /*
3546
4086
  // remove zero-length segments introduced by rounding
3547
- function removeZeroLengthLinetos_post(pathData) {
3548
- let pathDataOpt = [];
4087
+ export function removeZeroLengthLinetos_post(pathData) {
4088
+ let pathDataOpt = []
3549
4089
  pathData.forEach((com, i) => {
3550
4090
  let { type, values } = com;
3551
4091
  if (type === 'l' || type === 'v' || type === 'h') {
3552
- let hasLength = type === 'l' ? (values.join('') !== '00') : values[0] !== 0;
3553
- if (hasLength) pathDataOpt.push(com);
4092
+ let hasLength = type === 'l' ? (values.join('') !== '00') : values[0] !== 0
4093
+ if (hasLength) pathDataOpt.push(com)
3554
4094
  } else {
3555
- pathDataOpt.push(com);
4095
+ pathDataOpt.push(com)
3556
4096
  }
3557
- });
4097
+ })
3558
4098
  return pathDataOpt
3559
4099
  }
4100
+ */
3560
4101
 
3561
4102
  function removeZeroLengthLinetos(pathData) {
3562
4103
 
@@ -3568,16 +4109,27 @@ function removeZeroLengthLinetos(pathData) {
3568
4109
 
3569
4110
  for (let c = 1, l = pathData.length; c < l; c++) {
3570
4111
  let com = pathData[c];
4112
+ let comPrev = pathData[c-1];
4113
+ let comNext = pathData[c+1] || null;
3571
4114
  let { type, values } = com;
3572
4115
 
3573
- let valsL = values.slice(-2);
3574
- p = { x: valsL[0], y: valsL[1] };
4116
+ // zero length segments are simetimes used in icons for dots
4117
+ let isDot = comPrev.type.toLowerCase() ==='m' && !comNext;
4118
+
4119
+ let valsLen = values.length;
4120
+ p = { x: values[valsLen-2], y: values[valsLen-1] };
3575
4121
 
3576
4122
  // skip lineto
3577
- if (type === 'L' && p.x === p0.x && p.y === p0.y) {
4123
+ if (!isDot && type === 'L' && p.x === p0.x && p.y === p0.y) {
3578
4124
  continue
3579
4125
  }
3580
4126
 
4127
+ // skip minified zero length
4128
+ if (!isDot && (type === 'l' || type === 'v' || type === 'h')) {
4129
+ let noLength = type === 'l' ? (values.join('') === '00') : values[0] === 0;
4130
+ if(noLength) continue
4131
+ }
4132
+
3581
4133
  pathDataN.push(com);
3582
4134
  p0 = p;
3583
4135
  }
@@ -3675,7 +4227,7 @@ function optimizeClosePath(pathData, removeFinalLineto = true, reorder = true) {
3675
4227
  }
3676
4228
  // use top most command
3677
4229
  else {
3678
- indices = indices.sort((a, b) => +a.y.toFixed(1) - +b.y.toFixed(1) || a.x - b.x);
4230
+ indices = indices.sort((a, b) => +a.y.toFixed(8) - +b.y.toFixed(8) || a.x - b.x);
3679
4231
  newIndex = indices[0].index;
3680
4232
  }
3681
4233
 
@@ -3683,7 +4235,7 @@ function optimizeClosePath(pathData, removeFinalLineto = true, reorder = true) {
3683
4235
  pathData = newIndex ? shiftSvgStartingPoint(pathData, newIndex) : pathData;
3684
4236
  }
3685
4237
 
3686
- M = { x: +pathData[0].values[0].toFixed(8), y: +pathData[0].values[1].toFixed(7) };
4238
+ M = { x: +pathData[0].values[0].toFixed(8), y: +pathData[0].values[1].toFixed(8) };
3687
4239
 
3688
4240
  len = pathData.length;
3689
4241
 
@@ -3797,123 +4349,161 @@ function addClosePathLineto(pathData) {
3797
4349
  return pathData;
3798
4350
  }
3799
4351
 
3800
- /**
3801
- * reverse pathdata
3802
- * make sure all command coordinates are absolute and
3803
- * shorthands are converted to long notation
3804
- */
3805
- function reversePathData(pathData, {
3806
- arcToCubic = false,
3807
- quadraticToCubic = false,
3808
- toClockwise = false,
3809
- returnD = false
4352
+ function refineAdjacentExtremes(pathData, {
4353
+ threshold = null, tolerance = 1
3810
4354
  } = {}) {
3811
4355
 
3812
- /**
3813
- * Add closing lineto:
3814
- * needed for path reversing or adding points
3815
- */
3816
- const addClosePathLineto = (pathData) => {
3817
- let closed = pathData[pathData.length - 1].type.toLowerCase() === "z";
3818
- let M = pathData[0];
3819
- let [x0, y0] = [M.values[0], M.values[1]];
3820
- let lastCom = closed ? pathData[pathData.length - 2] : pathData[pathData.length - 1];
3821
- let [xE, yE] = [lastCom.values[lastCom.values.length - 2], lastCom.values[lastCom.values.length - 1]];
3822
-
3823
- if (closed && (x0 != xE || y0 != yE)) {
3824
-
3825
- pathData.pop();
3826
- pathData.push(
3827
- {
3828
- type: "L",
3829
- values: [x0, y0]
3830
- },
3831
- {
3832
- type: "Z",
3833
- values: []
3834
- }
3835
- );
3836
- }
3837
- return pathData;
3838
- };
4356
+ if (!threshold) {
4357
+ let bb = getPathDataBBox(pathData);
4358
+ threshold = (bb.width + bb.height) / 2 * 0.05;
4359
+
4360
+ }
4361
+
4362
+ let l = pathData.length;
4363
+
4364
+ for (let i = 0; i < l; i++) {
4365
+ let com = pathData[i];
4366
+ let { type, values, extreme, corner = false, dimA, p0, p } = com;
4367
+ let comN = pathData[i + 1] ? pathData[i + 1] : null;
4368
+ let comN2 = pathData[i + 2] ? pathData[i + 2] : null;
4369
+
4370
+ // check dist
4371
+ let diff = comN ? getDistAv(p, comN.p) : Infinity;
4372
+ let isCose = diff < threshold;
4373
+
4374
+ let diff2 = comN2 ? getDistAv(comN2.p, comN.p) : Infinity;
4375
+ let isCose2 = diff2 < threshold;
4376
+
4377
+ // next is extreme
4378
+ if (comN && type === 'C' && comN.type === 'C' && extreme && comN2 && comN2.extreme) {
4379
+
4380
+ if (isCose2 || isCose) {
4381
+
4382
+ // extrapolate
4383
+ let comEx = getCombinedByDominant(comN, comN2, threshold, tolerance, false);
3839
4384
 
3840
- // helper to rearrange control points for all command types
3841
- const reverseControlPoints = (type, values) => {
3842
- let controlPoints = [];
3843
- let endPoints = [];
3844
- if (type !== "A") {
3845
- for (let p = 0; p < values.length; p += 2) {
3846
- controlPoints.push([values[p], values[p + 1]]);
4385
+ if (comEx.length === 1) {
4386
+
4387
+ pathData[i + 1] = null;
4388
+ comEx = comEx[0];
4389
+
4390
+ pathData[i + 2].values = [comEx.cp1.x, comEx.cp1.y, comEx.cp2.x, comEx.cp2.y, comEx.p.x, comEx.p.y];
4391
+ pathData[i + 2].cp1 = comEx.cp1;
4392
+ pathData[i + 2].cp2 = comEx.cp2;
4393
+ pathData[i + 2].p0 = comEx.p0;
4394
+ pathData[i + 2].p = comEx.p;
4395
+ pathData[i + 2].extreme = comEx.extreme;
4396
+
4397
+ i++;
4398
+ continue
4399
+ }
3847
4400
  }
3848
- endPoints = controlPoints.pop();
3849
- controlPoints.reverse();
4401
+
3850
4402
  }
3851
- // is arc
3852
- else {
3853
4403
 
3854
- let sweep = values[4] == 0 ? 1 : 0;
3855
- controlPoints = [values[0], values[1], values[2], values[3], sweep];
3856
- endPoints = [values[5], values[6]];
4404
+ // short after extreme
4405
+
4406
+ if (comN && type === 'C' && comN.type === 'C' && extreme ) {
4407
+
4408
+ if (isCose) {
4409
+
4410
+ let dx1 = (com.cp1.x - comN.p0.x);
4411
+ let dy1 = (com.cp1.y - comN.p0.y);
4412
+
4413
+ let horizontal = Math.abs(dy1) < Math.abs(dx1);
4414
+
4415
+ let pN = comN.p;
4416
+ let ptI;
4417
+ let t = 1;
4418
+
4419
+ let area0 = getPolygonArea([com.p0, com.p , comN.p]);
4420
+ // cpts area
4421
+ let area1 = getPolygonArea([com.p0, com.cp1, com.cp2, com.p]);
4422
+
4423
+ // sign change: is corner => skip
4424
+ if ( (area0<0 && area1>0) || (area0>0 && area1<0)) {
4425
+
4426
+ continue;
4427
+ }
4428
+
4429
+
4430
+ if (comN.extreme) {
4431
+
4432
+ // extend cp2
4433
+ if (horizontal) {
4434
+ t = Math.abs(Math.abs(comN.cp2.x - comN.p.x) / Math.abs(com.cp2.x - com.p.x));
4435
+ t = Math.min(1, t);
4436
+
4437
+ ptI = interpolate(comN.p, com.cp2, 1 + t);
4438
+ com.cp2.x = ptI.x;
4439
+
4440
+ }
4441
+ else {
4442
+
4443
+ t = Math.abs(Math.abs(comN.cp2.y - comN.p.y) / Math.abs(com.cp2.y - com.p.y));
4444
+ t = Math.min(1, t);
4445
+
4446
+ ptI = interpolate(comN.p, com.cp2, 1 + t);
4447
+ com.cp2.y = ptI.y;
4448
+ }
4449
+
4450
+ pathData[i + 1].values = [com.cp1.x, com.cp1.y, com.cp2.x, com.cp2.y, pN.x, pN.y];
4451
+ pathData[i + 1].cp1 = com.cp1;
4452
+ pathData[i + 1].cp2 = com.cp2;
4453
+ pathData[i + 1].p0 = com.p0;
4454
+ pathData[i + 1].p = pN;
4455
+ pathData[i + 1].extreme = true;
4456
+
4457
+ // nullify 1st
4458
+ pathData[i] = null;
4459
+ continue
4460
+
4461
+ }
4462
+
4463
+ }
3857
4464
  }
3858
- return { controlPoints, endPoints };
3859
- };
3860
4465
 
3861
- // start compiling new path data
3862
- let pathDataNew = [];
4466
+ /*
4467
+ */
3863
4468
 
3864
- let closed =
3865
- pathData[pathData.length - 1].type.toLowerCase() === "z" ? true : false;
3866
- if (closed) {
3867
- // add lineto closing space between Z and M
3868
- pathData = addClosePathLineto(pathData);
3869
- // remove Z closepath
3870
- pathData.pop();
3871
4469
  }
3872
4470
 
3873
- // define last point as new M if path isn't closed
3874
- let valuesLast = pathData[pathData.length - 1].values;
3875
- let valuesLastL = valuesLast.length;
3876
- let M = closed
3877
- ? pathData[0]
3878
- : {
3879
- type: "M",
3880
- values: [valuesLast[valuesLastL - 2], valuesLast[valuesLastL - 1]]
3881
- };
3882
- // starting M stays the same – unless the path is not closed
3883
- pathDataNew.push(M);
4471
+ // remove commands
4472
+ pathData = pathData.filter(Boolean);
4473
+ l = pathData.length;
3884
4474
 
3885
- // reverse path data command order for processing
3886
- pathData.reverse();
3887
- for (let i = 1; i < pathData.length; i++) {
3888
- let com = pathData[i];
3889
- let type = com.type;
3890
- let values = com.values;
3891
- let comPrev = pathData[i - 1];
3892
- let typePrev = comPrev.type;
3893
- let valuesPrev = comPrev.values;
4475
+ /**
4476
+ * refine closing commands
4477
+ */
3894
4478
 
3895
- // get reversed control points and new end coordinates
3896
- let controlPointsPrev = reverseControlPoints(typePrev, valuesPrev).controlPoints;
3897
- let endPoints = reverseControlPoints(type, values).endPoints;
4479
+ let closed = pathData[l - 1].type.toLowerCase() === 'z';
4480
+ let lastIdx = closed ? l - 2 : l - 1;
4481
+ let lastCom = pathData[lastIdx];
4482
+ let penultimateCom = pathData[lastIdx - 1] || null;
4483
+ let M = { x: pathData[0].values[0], y: pathData[0].values[1] };
3898
4484
 
3899
- // create new path data
3900
- let newValues = [];
3901
- newValues = [controlPointsPrev, endPoints].flat();
3902
- pathDataNew.push({
3903
- type: typePrev,
3904
- values: newValues.flat()
3905
- });
3906
- }
4485
+ let dec = 8;
4486
+ let lastVals = lastCom.values.slice(-2);
4487
+ let isClosingTo = +lastVals[0].toFixed(dec) === +M.x.toFixed(dec) && +lastVals[1].toFixed(dec) === +M.y.toFixed(dec);
4488
+ let fistExt = pathData[1].type === 'C' && pathData[1].extreme ? pathData[1] : null;
4489
+
4490
+ let diff = getDistAv(lastCom.p0, lastCom.p);
4491
+ let isCose = diff < threshold;
4492
+
4493
+ if (penultimateCom && penultimateCom.type === 'C' && isCose && isClosingTo && fistExt) {
4494
+
4495
+ let comEx = getCombinedByDominant(penultimateCom, lastCom, threshold, tolerance, false);
4496
+
4497
+ if (comEx.length === 1) {
4498
+ pathData[lastIdx - 1] = comEx[0];
4499
+ pathData[lastIdx] = null;
4500
+ pathData = pathData.filter(Boolean);
4501
+ }
3907
4502
 
3908
- // add previously removed Z close path
3909
- if (closed) {
3910
- pathDataNew.push({
3911
- type: "z",
3912
- values: []
3913
- });
3914
4503
  }
3915
4504
 
3916
- return pathDataNew;
4505
+ return pathData
4506
+
3917
4507
  }
3918
4508
 
3919
4509
  function removeEmptySVGEls(svg) {
@@ -3938,7 +4528,7 @@ function cleanUpSVG(svgMarkup, {
3938
4528
  .querySelector("svg");
3939
4529
 
3940
4530
 
3941
- let allowed=['viewBox', 'xmlns', 'width', 'height', 'id', 'class'];
4531
+ let allowed=['viewBox', 'xmlns', 'width', 'height', 'id', 'class', 'fill', 'stroke', 'stroke-width', 'stroke-linecap', 'stroke-linejoin'];
3942
4532
  removeExcludedAttribues(svg, allowed);
3943
4533
 
3944
4534
  let removeEls = ['metadata', 'script'];
@@ -4009,6 +4599,156 @@ function stringifySVG(svg){
4009
4599
  return markup
4010
4600
  }
4011
4601
 
4602
+ function refineRoundedCorners(pathData, {
4603
+ threshold = 0,
4604
+ tolerance = 1
4605
+ } = {}) {
4606
+
4607
+ let l = pathData.length;
4608
+
4609
+ // add fist command
4610
+ let pathDataN = [pathData[0]];
4611
+
4612
+ let isClosed = pathData[l - 1].type.toLowerCase() === 'z';
4613
+ let lastOff = isClosed ? 2 : 1;
4614
+
4615
+ let comLast = pathData[l - lastOff];
4616
+ let lastIsLine = comLast.type === 'L';
4617
+ let lastIsBez = comLast.type === 'C';
4618
+ let firstIsLine = pathData[1].type === 'L';
4619
+ let firstIsBez = pathData[1].type === 'C';
4620
+
4621
+ let normalizeClose = isClosed && firstIsBez;
4622
+
4623
+ // normalize closepath to lineto
4624
+ if (normalizeClose) {
4625
+ pathData[l - 1].values = pathData[0].values;
4626
+ pathData[l - 1].type = 'L';
4627
+ lastIsLine = true;
4628
+ }
4629
+
4630
+ for (let i = 1; i < l; i++) {
4631
+ let com = pathData[i];
4632
+ let { type } = com;
4633
+ let comN = pathData[i + 1] ? pathData[i + 1] : null;
4634
+
4635
+ // search small cubic segments enclosed by linetos
4636
+ if ((type === 'L' && comN && comN.type === 'C') ||
4637
+ (type === 'C' && comN && comN.type === 'L')
4638
+
4639
+ ) {
4640
+ let comL0 = com;
4641
+ let comL1 = null;
4642
+ let comBez = [];
4643
+ let offset = 0;
4644
+
4645
+ // start to end
4646
+ if (i === 1 && firstIsBez && lastIsLine) {
4647
+ comBez = [pathData[1]];
4648
+ comL0 = pathData[l - 1];
4649
+ comL1 = comN;
4650
+
4651
+ }
4652
+
4653
+ // closing corner to start
4654
+ if (isClosed && lastIsBez && firstIsLine && i === l - lastOff - 1) {
4655
+ comL1 = pathData[1];
4656
+ comBez = [pathData[l - lastOff]];
4657
+
4658
+ }
4659
+
4660
+ for (let j = i + 1; j < l; j++) {
4661
+ let comN = pathData[j] ? pathData[j] : null;
4662
+ let comPrev = pathData[j - 1];
4663
+
4664
+ if (comPrev.type === 'C') {
4665
+ comBez.push(comPrev);
4666
+ }
4667
+
4668
+ if (comN.type === 'L' && comPrev.type === 'C') {
4669
+ comL1 = comN;
4670
+ break;
4671
+ }
4672
+ offset++;
4673
+ }
4674
+
4675
+ if (comL1) {
4676
+
4677
+ // linetos
4678
+ let len1 = getDistAv(comL0.p0, comL0.p);
4679
+ let len2 = getDistAv(comL1.p0, comL1.p);
4680
+
4681
+ // bezier
4682
+
4683
+ let comBezLen = comBez.length;
4684
+ let len3 = getDistAv(comBez[0].p0, comBez[comBezLen - 1].p);
4685
+
4686
+ // check concaveness by area sign change
4687
+ let area1 = getPolygonArea([comL0.p0, comL0.p, comL1.p0, comL1.p], false);
4688
+ let area2 = getPolygonArea([comBez[0].p0, comBez[0].cp1, comBez[0].cp2, comBez[0].p], false);
4689
+
4690
+ let signChange = (area1 < 0 && area2 > 0) || (area1 > 0 && area2 < 0);
4691
+
4692
+ if (comBez && !signChange && len3 < threshold && len1 > len3 && len2 > len3) {
4693
+
4694
+ let ptQ = checkLineIntersection(comL0.p0, comL0.p, comL1.p0, comL1.p, false);
4695
+ if (ptQ) {
4696
+
4697
+ /*
4698
+ let dist1 = getDistAv(ptQ, comL0.p)
4699
+ let dist2 = getDistAv(ptQ, comL1.p0)
4700
+ let diff = Math.abs(dist1-dist2)
4701
+ let rat = diff/Math.max(dist1, dist2)
4702
+ console.log('rat', rat);
4703
+ */
4704
+
4705
+ /*
4706
+ // adjust curve start and end to meet original
4707
+ let t = 1
4708
+
4709
+ let p0_2 = pointAtT([ptQ, comL0.p], t)
4710
+
4711
+ comL0.p = p0_2
4712
+ comL0.values = [p0_2.x, p0_2.y]
4713
+
4714
+ let p_2 = pointAtT([ptQ, comL1.p0], t)
4715
+
4716
+ comL1.p0 = p_2
4717
+
4718
+ */
4719
+
4720
+ let comQ = { type: 'Q', values: [ptQ.x, ptQ.y, comL1.p0.x, comL1.p0.y] };
4721
+ comQ.p0 = comL0.p;
4722
+ comQ.cp1 = ptQ;
4723
+ comQ.p = comL1.p0;
4724
+
4725
+ // add quadratic command
4726
+ pathDataN.push(comL0, comQ);
4727
+ i += offset;
4728
+ continue;
4729
+ }
4730
+ }
4731
+ }
4732
+ }
4733
+
4734
+ // skip last lineto
4735
+ if (normalizeClose && i === l - 1 && type === 'L') {
4736
+ continue
4737
+ }
4738
+
4739
+ pathDataN.push(com);
4740
+
4741
+ }
4742
+
4743
+ // revert close path normalization
4744
+ if (normalizeClose) {
4745
+ pathDataN.push({ type: 'Z', values: [] });
4746
+ }
4747
+
4748
+ return pathDataN;
4749
+
4750
+ }
4751
+
4012
4752
  function svgPathSimplify(input = '', {
4013
4753
 
4014
4754
  // return svg markup or object
@@ -4027,15 +4767,20 @@ function svgPathSimplify(input = '', {
4027
4767
 
4028
4768
  simplifyBezier = true,
4029
4769
  optimizeOrder = true,
4770
+ removeZeroLength = true,
4030
4771
  removeColinear = true,
4031
4772
  flatBezierToLinetos = true,
4032
4773
  revertToQuadratics = true,
4033
4774
 
4775
+ refineExtremes = true,
4776
+ refineCorners = false,
4777
+
4034
4778
  keepExtremes = true,
4035
4779
  keepCorners = true,
4036
4780
  extrapolateDominant = true,
4037
4781
  keepInflections = false,
4038
4782
  addExtremes = false,
4783
+ removeOrphanSubpaths = false,
4039
4784
 
4040
4785
  // svg path optimizations
4041
4786
  decimals = 3,
@@ -4049,6 +4794,7 @@ function svgPathSimplify(input = '', {
4049
4794
  mergePaths = false,
4050
4795
  removeHidden = true,
4051
4796
  removeUnused = true,
4797
+ shapesToPaths = true,
4052
4798
 
4053
4799
  } = {}) {
4054
4800
 
@@ -4073,24 +4819,43 @@ function svgPathSimplify(input = '', {
4073
4819
  */
4074
4820
 
4075
4821
  // original size
4076
- svgSize = new Blob([input]).size;
4077
4822
 
4078
- // single path
4823
+ svgSize = input.length;
4824
+
4825
+ // mode:0 – single path
4079
4826
  if (!mode) {
4080
4827
  if (inputType === 'pathDataString') {
4081
4828
  d = input;
4082
4829
  } else if (inputType === 'polyString') {
4083
4830
  d = 'M' + input;
4084
4831
  }
4832
+ else if (inputType === 'pathData') {
4833
+ d = input;
4834
+
4835
+ // stringify to compare lengths
4836
+
4837
+ let dStr = d.map(com=>{return `${com.type} ${com.values.join(' ')}`}).join(' ') ;
4838
+ svgSize = dStr.length;
4839
+
4840
+ }
4841
+
4085
4842
  paths.push({ d, el: null });
4086
4843
  }
4087
- // process svg
4844
+ // mode:1 – process complete svg DOM
4088
4845
  else {
4089
4846
 
4090
4847
  let returnDom = true;
4091
4848
  svg = cleanUpSVG(input, { returnDom, removeHidden, removeUnused }
4092
4849
  );
4093
4850
 
4851
+ if (shapesToPaths) {
4852
+ let shapes = svg.querySelectorAll('polygon, polyline, line, rect, circle, ellipse');
4853
+ shapes.forEach(shape => {
4854
+ let path = shapeElToPath(shape);
4855
+ shape.replaceWith(path);
4856
+ });
4857
+ }
4858
+
4094
4859
  // collect paths
4095
4860
  let pathEls = svg.querySelectorAll('path');
4096
4861
  pathEls.forEach(path => {
@@ -4100,6 +4865,7 @@ function svgPathSimplify(input = '', {
4100
4865
 
4101
4866
  /**
4102
4867
  * process all paths
4868
+ * try simplifications and removals
4103
4869
  */
4104
4870
 
4105
4871
  // SVG optimization options
@@ -4112,34 +4878,35 @@ function svgPathSimplify(input = '', {
4112
4878
  // combinded path data for SVGs with mergePaths enabled
4113
4879
  let pathData_merged = [];
4114
4880
 
4115
- paths.forEach(path => {
4116
- let { d, el } = path;
4881
+ for (let i = 0, l = paths.length; l && i < l; i++) {
4117
4882
 
4118
- let pathDataO = parsePathDataNormalized(d, { quadraticToCubic, toAbsolute, arcToCubic });
4883
+ let path = paths[i];
4884
+ let { d, el } = path;
4119
4885
 
4120
- // create clone for fallback
4121
- let pathData = JSON.parse(JSON.stringify(pathDataO));
4886
+ let pathData = parsePathDataNormalized(d, { quadraticToCubic, toAbsolute, arcToCubic });
4122
4887
 
4123
4888
  // count commands for evaluation
4124
- let comCount = pathDataO.length;
4889
+ let comCount = pathData.length;
4890
+
4891
+ if (removeOrphanSubpaths) pathData = removeOrphanedM(pathData);
4125
4892
 
4126
4893
  /**
4127
4894
  * get sub paths
4128
4895
  */
4129
4896
  let subPathArr = splitSubpaths(pathData);
4897
+ let lenSub = subPathArr.length;
4130
4898
 
4131
4899
  // cleaned up pathData
4132
- let pathDataArrN = [];
4133
4900
 
4134
- for (let i = 0, l = subPathArr.length; i < l; i++) {
4901
+ // reset array
4902
+ let pathDataFlat = [];
4135
4903
 
4136
- let pathDataSub = subPathArr[i];
4904
+ for (let i = 0; i < lenSub; i++) {
4137
4905
 
4138
- // try simplification in reversed order
4139
- if (reverse) pathDataSub = reversePathData(pathDataSub);
4906
+ let pathDataSub = subPathArr[i];
4140
4907
 
4141
4908
  // remove zero length linetos
4142
- if (removeColinear) pathDataSub = removeZeroLengthLinetos(pathDataSub);
4909
+ if (removeColinear || removeZeroLength) pathDataSub = removeZeroLengthLinetos(pathDataSub);
4143
4910
 
4144
4911
  // add extremes
4145
4912
 
@@ -4149,50 +4916,63 @@ function svgPathSimplify(input = '', {
4149
4916
  // sort to top left
4150
4917
  if (optimizeOrder) pathDataSub = pathDataToTopLeft(pathDataSub);
4151
4918
 
4152
- // remove colinear/flat
4153
- if (removeColinear) pathDataSub = pathDataRemoveColinear(pathDataSub, tolerance, flatBezierToLinetos);
4919
+ // Preprocessing: remove colinear - ignore flat beziers (removed later)
4920
+ if (removeColinear) pathDataSub = pathDataRemoveColinear(pathDataSub, { tolerance, flatBezierToLinetos: false });
4154
4921
 
4155
4922
  // analyze pathdata to add info about signicant properties such as extremes, corners
4156
4923
  let pathDataPlus = analyzePathData(pathDataSub);
4157
4924
 
4158
4925
  // simplify beziers
4159
4926
  let { pathData, bb, dimA } = pathDataPlus;
4160
-
4161
4927
  pathData = simplifyBezier ? simplifyPathDataCubic(pathData, { simplifyBezier, keepInflections, keepExtremes, keepCorners, extrapolateDominant, revertToQuadratics, tolerance, reverse }) : pathData;
4162
4928
 
4929
+ // refine extremes
4930
+ if (refineExtremes) {
4931
+ let thresholdEx = (bb.width + bb.height) / 2 * 0.05;
4932
+ pathData = refineAdjacentExtremes(pathData, { threshold: thresholdEx, tolerance });
4933
+ }
4934
+
4163
4935
  // cubic to arcs
4164
4936
  if (cubicToArc) {
4165
4937
 
4166
- let thresh = 3;
4938
+ let thresh = 1;
4167
4939
 
4168
- pathData.forEach((com, c) => {
4940
+ for(let c=0, l=pathData.length; c<l; c++){
4941
+ let com = pathData[c];
4169
4942
  let { type, values, p0, cp1 = null, cp2 = null, p = null } = com;
4170
4943
  if (type === 'C') {
4171
4944
 
4172
4945
  let comA = cubicCommandToArc(p0, cp1, cp2, p, thresh);
4173
4946
  if (comA.isArc) pathData[c] = comA.com;
4174
-
4175
4947
  }
4176
- });
4948
+ }
4177
4949
 
4178
4950
  // combine adjacent cubics
4179
4951
  pathData = combineArcs(pathData);
4180
4952
 
4181
4953
  }
4182
4954
 
4955
+ // post processing: remove flat beziers
4956
+ if (removeColinear && flatBezierToLinetos) {
4957
+ pathData = pathDataRemoveColinear(pathData, { tolerance, flatBezierToLinetos });
4958
+ }
4959
+
4960
+ // refine corners
4961
+ if(refineCorners){
4962
+ let threshold = (bb.width + bb.height) / 2 * 0.1;
4963
+ pathData = refineRoundedCorners(pathData, { threshold, tolerance });
4964
+
4965
+ }
4966
+
4183
4967
  // simplify to quadratics
4184
4968
  if (revertToQuadratics) {
4185
- pathData.forEach((com, c) => {
4969
+ for(let c=0, l=pathData.length; c<l; c++){
4970
+ let com = pathData[c];
4186
4971
  let { type, values, p0, cp1 = null, cp2 = null, p = null } = com;
4187
4972
  if (type === 'C') {
4188
4973
 
4189
4974
  let comQ = revertCubicQuadratic(p0, cp1, cp2, p);
4190
4975
  if (comQ.type === 'Q') {
4191
- /*
4192
- comQ.p0 = com.p0
4193
- comQ.cp1 = {x:comQ.values[0], y:comQ.values[1]}
4194
- comQ.p = com.p
4195
- */
4196
4976
  comQ.extreme = com.extreme;
4197
4977
  comQ.corner = com.corner;
4198
4978
  comQ.dimA = com.dimA;
@@ -4200,20 +4980,25 @@ function svgPathSimplify(input = '', {
4200
4980
  pathData[c] = comQ;
4201
4981
  }
4202
4982
  }
4203
- });
4983
+ }
4204
4984
  }
4205
4985
 
4206
4986
  // optimize close path
4207
4987
  if (optimizeOrder) pathData = optimizeClosePath(pathData);
4208
4988
 
4209
- // poly
4210
-
4211
4989
  // update
4212
- pathDataArrN.push(pathData);
4990
+ pathDataFlat.push(...pathData);
4991
+
4213
4992
  }
4214
4993
 
4215
4994
  // flatten compound paths
4216
- pathData = pathDataArrN.flat();
4995
+ pathData = pathDataFlat;
4996
+
4997
+ if (autoAccuracy) {
4998
+ decimals = detectAccuracy(pathData);
4999
+ pathOptions.decimals = decimals;
5000
+
5001
+ }
4217
5002
 
4218
5003
  // collect for merged svg paths
4219
5004
  if (el && mergePaths) {
@@ -4222,24 +5007,19 @@ function svgPathSimplify(input = '', {
4222
5007
  // single output
4223
5008
  else {
4224
5009
 
4225
- /**
4226
- * detect accuracy
4227
- */
4228
- if (autoAccuracy) {
4229
- decimals = detectAccuracy(pathData);
4230
- }
4231
-
4232
5010
  // optimize path data
4233
5011
  pathData = convertPathData(pathData, pathOptions);
4234
5012
 
4235
5013
  // remove zero-length segments introduced by rounding
4236
- pathData = removeZeroLengthLinetos_post(pathData);
5014
+ pathData = removeZeroLengthLinetos(pathData);
4237
5015
 
4238
5016
  // compare command count
4239
5017
  let comCountS = pathData.length;
4240
5018
 
4241
5019
  let dOpt = pathDataToD(pathData, minifyD);
4242
- svgSizeOpt = new Blob([dOpt]).size;
5020
+
5021
+ svgSizeOpt = dOpt.length;
5022
+
4243
5023
  compression = +(100 / svgSize * (svgSizeOpt)).toFixed(2);
4244
5024
 
4245
5025
  path.d = dOpt;
@@ -4255,19 +5035,19 @@ function svgPathSimplify(input = '', {
4255
5035
  // apply new path for svgs
4256
5036
  if (el) el.setAttribute('d', dOpt);
4257
5037
  }
4258
- });
4259
-
5038
+ }
4260
5039
  /**
4261
5040
  * stringify new SVG
4262
5041
  */
4263
5042
  if (mode) {
4264
5043
 
4265
5044
  if (pathData_merged.length) {
5045
+
4266
5046
  // optimize path data
4267
5047
  let pathData = convertPathData(pathData_merged, pathOptions);
4268
5048
 
4269
5049
  // remove zero-length segments introduced by rounding
4270
- pathData = removeZeroLengthLinetos_post(pathData);
5050
+ pathData = removeZeroLengthLinetos(pathData);
4271
5051
 
4272
5052
  let dOpt = pathDataToD(pathData, minifyD);
4273
5053
 
@@ -4285,7 +5065,8 @@ function svgPathSimplify(input = '', {
4285
5065
  }
4286
5066
 
4287
5067
  svg = stringifySVG(svg);
4288
- svgSizeOpt = new Blob([svg]).size;
5068
+
5069
+ svgSizeOpt = svg.length;
4289
5070
 
4290
5071
  compression = +(100 / svgSize * (svgSizeOpt)).toFixed(2);
4291
5072
 
@@ -4306,109 +5087,16 @@ function svgPathSimplify(input = '', {
4306
5087
 
4307
5088
  }
4308
5089
 
4309
- function simplifyPathDataCubic(pathData, {
4310
- keepExtremes = true,
4311
- keepInflections = true,
4312
- keepCorners = true,
4313
- extrapolateDominant = true,
4314
- tolerance = 1,
4315
- reverse = false
4316
- } = {}) {
4317
-
4318
- let pathDataN = [pathData[0]];
4319
-
4320
- for (let i = 2, l = pathData.length; l && i <= l; i++) {
4321
- let com = pathData[i - 1];
4322
- let comN = i < l ? pathData[i] : null;
4323
- let typeN = comN?.type || null;
4324
-
4325
- let isDirChange = com?.directionChange || null;
4326
- let isDirChangeN = comN?.directionChange || null;
4327
-
4328
- let { type, values, p0, p, cp1 = null, cp2 = null, extreme = false, corner = false, dimA = 0 } = com;
4329
-
4330
- // next is also cubic
4331
- if (type === 'C' && typeN === 'C') {
4332
-
4333
- // cannot be combined as crossing extremes or corners
4334
- if (
4335
- (keepInflections && isDirChangeN) ||
4336
- (keepCorners && corner) ||
4337
- (!isDirChange && keepExtremes && extreme)
4338
- ) {
4339
-
4340
- pathDataN.push(com);
4341
- }
4342
-
4343
- // try simplification
4344
- else {
4345
-
4346
- let combined = combineCubicPairs(com, comN, extrapolateDominant, tolerance);
4347
- let error = 0;
4348
-
4349
- // combining successful! try next segment
4350
- if (combined.length === 1) {
4351
- com = combined[0];
4352
- let offset = 1;
4353
-
4354
- // add cumulative error to prevent distortions
4355
- error += com.error;
4356
-
4357
- // find next candidates
4358
- for (let n = i + 1; error < tolerance && n < l; n++) {
4359
- let comN = pathData[n];
4360
- if (comN.type !== 'C' ||
4361
- (
4362
- (keepInflections && comN.directionChange) ||
4363
- (keepCorners && com.corner) ||
4364
- (keepExtremes && com.extreme)
4365
- )
4366
- ) {
4367
- break
4368
- }
4369
-
4370
- let combined = combineCubicPairs(com, comN, extrapolateDominant, tolerance);
4371
- if (combined.length === 1) {
4372
- // add cumulative error to prevent distortions
4373
-
4374
- error += combined[0].error * 0.5;
4375
- offset++;
4376
- }
4377
- com = combined[0];
4378
- }
4379
-
4380
- pathDataN.push(com);
4381
-
4382
- if (i < l) {
4383
- i += offset;
4384
- }
4385
-
4386
- } else {
4387
- pathDataN.push(com);
4388
- }
4389
- }
4390
-
4391
- } // end of bezier command
4392
-
4393
- // other commands
4394
- else {
4395
- pathDataN.push(com);
4396
- }
4397
-
4398
- } // end command loop
4399
-
4400
- // reverse back
4401
- if (reverse) pathDataN = reversePathData(pathDataN);
4402
-
4403
- return pathDataN
4404
- }
4405
-
4406
5090
  const {
4407
5091
  abs, acos, asin, atan, atan2, ceil, cos, exp, floor,
4408
5092
  log, hypot, max, min, pow, random, round, sin, sqrt, tan, PI
4409
5093
  } = Math;
4410
5094
 
4411
- // just for visual debugging
5095
+ /*
5096
+ import {XMLSerializerPoly, DOMParserPoly} from './dom_polyfills';
5097
+ export {XMLSerializerPoly as XMLSerializerPoly};
5098
+ export {DOMParserPoly as DOMParserPoly};
5099
+ */
4412
5100
 
4413
5101
  // IIFE
4414
5102
  if (typeof window !== 'undefined') {