svg-path-simplify 0.0.7 → 0.0.8

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.
@@ -1,5 +1,44 @@
1
1
  'use strict';
2
2
 
3
+ function renderPoint(
4
+ svg,
5
+ coords,
6
+ fill = "red",
7
+ r = "1%",
8
+ opacity = "1",
9
+ title = '',
10
+ render = true,
11
+ id = "",
12
+ className = ""
13
+ ) {
14
+ if (Array.isArray(coords)) {
15
+ coords = {
16
+ x: coords[0],
17
+ y: coords[1]
18
+ };
19
+ }
20
+ let marker = `<circle class="${className}" opacity="${opacity}" id="${id}" cx="${coords.x}" cy="${coords.y}" r="${r}" fill="${fill}">
21
+ <title>${title}</title></circle>`;
22
+
23
+ if (render) {
24
+ svg.insertAdjacentHTML("beforeend", marker);
25
+ } else {
26
+ return marker;
27
+ }
28
+ }
29
+
30
+ function renderPath(svg, d = '', stroke = 'green', strokeWidth = '1%', render = true) {
31
+
32
+ let path = `<path d="${d}" fill="none" stroke="${stroke}" stroke-width="${strokeWidth}" /> `;
33
+
34
+ if (render) {
35
+ svg.insertAdjacentHTML("beforeend", path);
36
+ } else {
37
+ return path;
38
+ }
39
+
40
+ }
41
+
3
42
  function detectInputType(input) {
4
43
  let type = 'string';
5
44
  /*
@@ -67,19 +106,24 @@ function getAngle(p1, p2, normalize = false) {
67
106
  * http://jsfiddle.net/justin_c_rounds/Gd2S2/light/
68
107
  */
69
108
 
70
- function checkLineIntersection(p1, p2, p3, p4, exact = true) {
109
+ function checkLineIntersection(p1=null, p2=null, p3=null, p4=null, exact = true, debug=false) {
71
110
  // 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
72
111
  let denominator, a, b, numerator1, numerator2;
73
112
  let intersectionPoint = {};
74
113
 
114
+ if(!p1 || !p2 || !p3 || !p4){
115
+ if(debug) console.warn('points missing');
116
+ return false
117
+ }
118
+
75
119
  try {
76
120
  denominator = ((p4.y - p3.y) * (p2.x - p1.x)) - ((p4.x - p3.x) * (p2.y - p1.y));
77
121
  if (denominator == 0) {
78
122
  return false;
79
123
  }
80
-
81
124
  } catch {
82
- console.log('!catch', p1, p2, 'p3:', p3, p4);
125
+ if(debug) console.warn('!catch', p1, p2, 'p3:', p3, 'p4:', p4);
126
+ return false
83
127
  }
84
128
 
85
129
  a = p1.y - p3.y;
@@ -96,8 +140,6 @@ function checkLineIntersection(p1, p2, p3, p4, exact = true) {
96
140
  y: p1.y + (a * (p2.y - p1.y))
97
141
  };
98
142
 
99
- // console.log('intersectionPoint', intersectionPoint, p1, p2);
100
-
101
143
  let intersection = false;
102
144
  // if line1 is a segment and line2 is infinite, they intersect if:
103
145
  if ((a > 0 && a < 1) && (b > 0 && b < 1)) {
@@ -497,6 +539,110 @@ function getBezierExtremeT(pts) {
497
539
  return tArr;
498
540
  }
499
541
 
542
+ /**
543
+ * based on Nikos M.'s answer
544
+ * how-do-you-calculate-the-axis-aligned-bounding-box-of-an-ellipse
545
+ * https://stackoverflow.com/questions/87734/#75031511
546
+ * See also: https://github.com/foo123/Geometrize
547
+ */
548
+ function getArcExtemes(p0, values) {
549
+ // compute point on ellipse from angle around ellipse (theta)
550
+ const arc = (theta, cx, cy, rx, ry, alpha) => {
551
+ // theta is angle in radians around arc
552
+ // alpha is angle of rotation of ellipse in radians
553
+ var cos = Math.cos(alpha),
554
+ sin = Math.sin(alpha),
555
+ x = rx * Math.cos(theta),
556
+ y = ry * Math.sin(theta);
557
+
558
+ return {
559
+ x: cx + cos * x - sin * y,
560
+ y: cy + sin * x + cos * y
561
+ };
562
+ };
563
+
564
+ let arcData = svgArcToCenterParam(p0.x, p0.y, values[0], values[1], values[2], values[3], values[4], values[5], values[6]);
565
+ let { rx, ry, cx, cy, endAngle, deltaAngle } = arcData;
566
+
567
+ // arc rotation
568
+ let deg = values[2];
569
+
570
+ // final on path point
571
+ let p = { x: values[5], y: values[6] };
572
+
573
+ // collect extreme points – add end point
574
+ let extremes = [p];
575
+
576
+ // rotation to radians
577
+ let alpha = deg * Math.PI / 180;
578
+ let tan = Math.tan(alpha),
579
+ p1, p2, p3, p4, theta;
580
+
581
+ /**
582
+ * find min/max from zeroes of directional derivative along x and y
583
+ * along x axis
584
+ */
585
+ theta = Math.atan2(-ry * tan, rx);
586
+
587
+ let angle1 = theta;
588
+ let angle2 = theta + Math.PI;
589
+ let angle3 = Math.atan2(ry, rx * tan);
590
+ let angle4 = angle3 + Math.PI;
591
+
592
+ // inner bounding box
593
+ let xArr = [p0.x, p.x];
594
+ let yArr = [p0.y, p.y];
595
+ let xMin = Math.min(...xArr);
596
+ let xMax = Math.max(...xArr);
597
+ let yMin = Math.min(...yArr);
598
+ let yMax = Math.max(...yArr);
599
+
600
+ // on path point close after start
601
+ let angleAfterStart = endAngle - deltaAngle * 0.001;
602
+ let pP2 = arc(angleAfterStart, cx, cy, rx, ry, alpha);
603
+
604
+ // on path point close before end
605
+ let angleBeforeEnd = endAngle - deltaAngle * 0.999;
606
+ let pP3 = arc(angleBeforeEnd, cx, cy, rx, ry, alpha);
607
+
608
+ /**
609
+ * expected extremes
610
+ * if leaving inner bounding box
611
+ * (between segment start and end point)
612
+ * otherwise exclude elliptic extreme points
613
+ */
614
+
615
+ // right
616
+ if (pP2.x > xMax || pP3.x > xMax) {
617
+ // get point for this theta
618
+ p1 = arc(angle1, cx, cy, rx, ry, alpha);
619
+ extremes.push(p1);
620
+ }
621
+
622
+ // left
623
+ if (pP2.x < xMin || pP3.x < xMin) {
624
+ // get anti-symmetric point
625
+ p2 = arc(angle2, cx, cy, rx, ry, alpha);
626
+ extremes.push(p2);
627
+ }
628
+
629
+ // top
630
+ if (pP2.y < yMin || pP3.y < yMin) {
631
+ // get anti-symmetric point
632
+ p4 = arc(angle4, cx, cy, rx, ry, alpha);
633
+ extremes.push(p4);
634
+ }
635
+
636
+ // bottom
637
+ if (pP2.y > yMax || pP3.y > yMax) {
638
+ // get point for this theta
639
+ p3 = arc(angle3, cx, cy, rx, ry, alpha);
640
+ extremes.push(p3);
641
+ }
642
+
643
+ return extremes;
644
+ }
645
+
500
646
  // cubic bezier.
501
647
  function cubicBezierExtremeT(p0, cp1, cp2, p) {
502
648
  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];
@@ -605,90 +751,6 @@ function quadraticBezierExtremeT(p0, cp1, p) {
605
751
  return extemeT
606
752
  }
607
753
 
608
- function commandIsFlat(points, tolerance = 0.025) {
609
-
610
- let p0 = points[0];
611
- let p = points[points.length - 1];
612
-
613
- let xArr = points.map(pt => { return pt.x });
614
- let yArr = points.map(pt => { return pt.y });
615
-
616
- let xMin = Math.min(...xArr);
617
- let xMax = Math.max(...xArr);
618
- let yMin = Math.min(...yArr);
619
- let yMax = Math.max(...yArr);
620
- let w = xMax - xMin;
621
- let h = yMax - yMin;
622
-
623
- if (points.length < 3 || (w === 0 || h === 0)) {
624
- return { area: 0, flat: true, thresh: 0.0001, ratio: 0 };
625
- }
626
-
627
- let squareDist = getSquareDistance(p0, p);
628
- let squareDist1 = getSquareDistance(p0, points[0]);
629
- let squareDist2 = points.length > 3 ? getSquareDistance(p, points[1]) : squareDist1;
630
- let squareDistAvg = (squareDist1 + squareDist2) / 2;
631
-
632
- tolerance = 0.5;
633
- let thresh = (w + h) * 0.5 * tolerance;
634
-
635
- let area = 0;
636
- for (let i = 0, l = points.length; i < l; i++) {
637
- let addX = points[i].x;
638
- let addY = points[i === points.length - 1 ? 0 : i + 1].y;
639
- let subX = points[i === points.length - 1 ? 0 : i + 1].x;
640
- let subY = points[i].y;
641
- area += addX * addY * 0.5 - subX * subY * 0.5;
642
- }
643
-
644
- area = +Math.abs(area).toFixed(9);
645
- let areaThresh = 1000;
646
-
647
- let ratio = area / (squareDistAvg);
648
-
649
- let isFlat = area === 0 ? true : area < squareDistAvg / areaThresh;
650
-
651
- return { area: area, flat: isFlat, thresh: thresh, ratio: ratio, squareDist: squareDist, areaThresh: squareDist / areaThresh };
652
- }
653
-
654
- function checkBezierFlatness(p0, cpts, p) {
655
-
656
- let isFlat = false;
657
-
658
- let isCubic = cpts.length===2;
659
-
660
- let cp1 = cpts[0];
661
- let cp2 = isCubic ? cpts[1] : cp1;
662
-
663
- if(p0.x===cp1.x && p0.y===cp1.y && p.x===cp2.x && p.y===cp2.y) return true;
664
-
665
- let dx1 = cp1.x - p0.x;
666
- let dy1 = cp1.y - p0.y;
667
-
668
- let dx2 = p.x - cp2.x;
669
- let dy2 = p.y - cp2.y;
670
-
671
- let cross1 = Math.abs(dx1 * dy2 - dy1 * dx2);
672
-
673
- if(!cross1) return true
674
-
675
- let dx0 = p.x - p0.x;
676
- let dy0 = p.y - p0.y;
677
- let cross0 = Math.abs(dx0 * dy1 - dy0 * dx1);
678
-
679
- if(!cross0) return true
680
-
681
- let rat = (cross0/cross1);
682
-
683
- if (rat<1.1 ) {
684
-
685
- isFlat = true;
686
- }
687
-
688
- return isFlat;
689
-
690
- }
691
-
692
754
  /**
693
755
  * sloppy distance calculation
694
756
  * based on x/y differences
@@ -1156,6 +1218,73 @@ function getPathDataPoly(pathData) {
1156
1218
  return poly;
1157
1219
  }
1158
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
+
1159
1288
  /**
1160
1289
  * get pathdata area
1161
1290
  */
@@ -1431,7 +1560,7 @@ function pathDataToD(pathData, optimize = 0) {
1431
1560
  return d;
1432
1561
  }
1433
1562
 
1434
- function getCombinedByDominant(com1, com2, maxDist = 0, tolerance = 1) {
1563
+ function getCombinedByDominant(com1, com2, maxDist = 0, tolerance = 1, debug = false) {
1435
1564
 
1436
1565
  // cubic Bézier derivative
1437
1566
  const cubicDerivative = (p0, p1, p2, p3, t) => {
@@ -1453,8 +1582,9 @@ function getCombinedByDominant(com1, com2, maxDist = 0, tolerance = 1) {
1453
1582
  let commands = [com1, com2];
1454
1583
 
1455
1584
  // detect dominant
1456
- let dist1 = getSquareDistance(com1.p0, com1.p);
1457
- let dist2 = getSquareDistance(com2.p0, com2.p);
1585
+ let dist1 = getDistAv(com1.p0, com1.p);
1586
+ let dist2 = getDistAv(com2.p0, com2.p);
1587
+
1458
1588
  let reverse = dist1 > dist2;
1459
1589
 
1460
1590
  // backup original commands
@@ -1513,11 +1643,9 @@ function getCombinedByDominant(com1, com2, maxDist = 0, tolerance = 1) {
1513
1643
  let dP = cubicDerivative(com2.p0, com2.cp1, com2.cp2, com2.p, t0);
1514
1644
  let r = sub(P, com1.p0);
1515
1645
 
1516
-
1517
1646
  t0 -= dot(r, dP) / dot(dP, dP);
1518
1647
 
1519
1648
  // construct merged cubic over [t0, 1]
1520
-
1521
1649
  let Q0 = pointAtT([com2.p0, com2.cp1, com2.cp2, com2.p], t0);
1522
1650
  let Q3 = com2.p;
1523
1651
 
@@ -1545,7 +1673,7 @@ function getCombinedByDominant(com1, com2, maxDist = 0, tolerance = 1) {
1545
1673
  };
1546
1674
  }
1547
1675
 
1548
- let tMid = (1 - t0)*0.5 ;
1676
+ let tMid = (1 - t0) * 0.5;
1549
1677
 
1550
1678
  let ptM = pointAtT([result.p0, result.cp1, result.cp2, result.p], tMid, false, true);
1551
1679
  let seg1_cp2 = ptM.cpts[2];
@@ -1557,17 +1685,18 @@ function getCombinedByDominant(com1, com2, maxDist = 0, tolerance = 1) {
1557
1685
  let cp2_2 = interpolate(result.p, ptI_2, 1.333);
1558
1686
 
1559
1687
  // test self intersections and exit
1560
- let cp_intersection = checkLineIntersection(com1_o.p0, cp1_2, com2_o.p, cp2_2, true );
1561
- if(cp_intersection){
1688
+ let cp_intersection = checkLineIntersection(com1_o.p0, cp1_2, com2_o.p, cp2_2, true);
1689
+ if (cp_intersection) {
1562
1690
 
1563
1691
  return commands;
1564
1692
  }
1565
1693
 
1694
+ if (debug) renderPoint(markers, ptM, 'purple');
1695
+
1566
1696
  result.cp1 = cp1_2;
1567
1697
  result.cp2 = cp2_2;
1568
1698
 
1569
- // check distances
1570
-
1699
+ // check distances between original starting point and extrapolated
1571
1700
  let dist3 = getDistAv(com1_o.p0, result.p0);
1572
1701
  let dist4 = getDistAv(com2_o.p, result.p);
1573
1702
  let dist5 = (dist3 + dist4);
@@ -1579,11 +1708,24 @@ function getCombinedByDominant(com1, com2, maxDist = 0, tolerance = 1) {
1579
1708
  result.corner = com2_o.corner;
1580
1709
  result.dimA = com2_o.dimA;
1581
1710
  result.directionChange = com2_o.directionChange;
1711
+ result.type = 'C';
1582
1712
  result.values = [result.cp1.x, result.cp1.y, result.cp2.x, result.cp2.y, result.p.x, result.p.y];
1583
1713
 
1584
- // check if completely off
1714
+ // extrapolated starting point is not completely off
1585
1715
  if (dist5 < maxDist) {
1586
1716
 
1717
+ // split t to meet original mid segment start point
1718
+ let tSplit = reverse ? 1 + t0 : Math.abs(t0);
1719
+
1720
+ let ptSplit = pointAtT([result.p0, result.cp1, result.cp2, result.p], tSplit);
1721
+ let distSplit = getDistAv(ptSplit, com1.p);
1722
+
1723
+ // not close enough - exit
1724
+ if (distSplit > maxDist * tolerance) {
1725
+
1726
+ return commands;
1727
+ }
1728
+
1587
1729
  // compare combined with original area
1588
1730
  let pathData0 = [
1589
1731
  { type: 'M', values: [com1_o.p0.x, com1_o.p0.y] },
@@ -1600,16 +1742,18 @@ function getCombinedByDominant(com1, com2, maxDist = 0, tolerance = 1) {
1600
1742
  let areaN = getPathArea(pathDataN);
1601
1743
  let areaDiff = Math.abs(areaN / area0 - 1);
1602
1744
 
1603
- result.error = areaDiff * 10 * tolerance;
1745
+ result.error = areaDiff * 5 * tolerance;
1604
1746
 
1605
- pathDataToD(pathDataN);
1747
+ if (debug) {
1748
+ let d = pathDataToD(pathDataN);
1749
+ renderPath(markers, d, 'orange');
1750
+ }
1606
1751
 
1607
- // success
1608
- if (areaDiff < 0.01) {
1752
+ // success!!!
1753
+ if (areaDiff < 0.05 * tolerance) {
1609
1754
  commands = [result];
1610
1755
 
1611
- }
1612
-
1756
+ }
1613
1757
  }
1614
1758
 
1615
1759
  return commands
@@ -1693,14 +1837,6 @@ function combineCubicPairs(com1, com2, extrapolateDominant = false, tolerance =
1693
1837
  } // end 1st try
1694
1838
 
1695
1839
 
1696
- /*
1697
- if (extrapolateDominant && com2.extreme) {
1698
- renderPoint(markers, com2.p)
1699
-
1700
- }
1701
- */
1702
-
1703
-
1704
1840
 
1705
1841
  // try extrapolated dominant curve
1706
1842
 
@@ -1769,25 +1905,59 @@ function getExtrapolatedCommand(com1, com2, t1 = 0, t2 = 0) {
1769
1905
 
1770
1906
  function findSplitT(com1, com2) {
1771
1907
 
1772
- // control tangent intersection
1773
- let pt1 = checkLineIntersection(com1.p0, com1.cp1, com2.cp2, com2.p, false);
1908
+ let len3 = getDistance(com1.cp2, com1.p);
1909
+ let len4 = getDistance(com1.cp2, com2.cp1);
1774
1910
 
1775
- // intersection 2nd cp1 tangent and global tangent intersection
1776
- let ptI = checkLineIntersection(pt1, com2.p, com2.p0, com2.cp1, false);
1911
+ let t = Math.min(len3) / len4;
1777
1912
 
1778
- let len1 = getDistance(pt1, com2.p);
1779
- let len2 = getDistance(ptI, com2.p);
1913
+ return t
1914
+ }
1915
+
1916
+ function checkBezierFlatness(p0, cpts, p) {
1780
1917
 
1781
- let t = 1 - len2 / len1;
1918
+ let isFlat = false;
1782
1919
 
1783
- // check self intersections
1920
+ let isCubic = cpts.length === 2;
1784
1921
 
1785
- let len3 = getDistance(com1.cp2, com1.p);
1786
- let len4 = getDistance(com1.cp2, com2.cp1);
1922
+ let cp1 = cpts[0];
1923
+ let cp2 = isCubic ? cpts[1] : cp1;
1787
1924
 
1788
- t = Math.min(len3) / len4;
1925
+ if (p0.x === cp1.x && p0.y === cp1.y && p.x === cp2.x && p.y === cp2.y) return true;
1789
1926
 
1790
- return t
1927
+ let dx1 = cp1.x - p0.x;
1928
+ let dy1 = cp1.y - p0.y;
1929
+
1930
+ let dx2 = p.x - cp2.x;
1931
+ let dy2 = p.y - cp2.y;
1932
+
1933
+ let cross1 = Math.abs(dx1 * dy2 - dy1 * dx2);
1934
+
1935
+ if (!cross1) return true
1936
+
1937
+ let dx0 = p.x - p0.x;
1938
+ let dy0 = p.y - p0.y;
1939
+ let cross0 = Math.abs(dx0 * dy1 - dy0 * dx1);
1940
+
1941
+ if (!cross0) return true
1942
+
1943
+ let area = getPolygonArea([p0,...cpts, p], true);
1944
+ let dist1 = getSquareDistance(p0, p);
1945
+ let thresh = dist1/200;
1946
+
1947
+ // if(area<thresh) return true;
1948
+ isFlat = area<thresh;
1949
+
1950
+ /*
1951
+
1952
+ let rat = (cross0 / cross1)
1953
+
1954
+ if (rat < 1.1) {
1955
+ console.log('cross', cross0, cross1, 'rat', rat );
1956
+ isFlat = true;
1957
+ }
1958
+ */
1959
+
1960
+ return isFlat;
1791
1961
 
1792
1962
  }
1793
1963
 
@@ -1833,7 +2003,6 @@ function analyzePathData(pathData = []) {
1833
2003
  * this way we can skip certain tests
1834
2004
  */
1835
2005
  let commandPts = [p0];
1836
- let isFlat = false;
1837
2006
 
1838
2007
  // init properties
1839
2008
  com.idx = c - 1;
@@ -1864,7 +2033,7 @@ function analyzePathData(pathData = []) {
1864
2033
  com.p0 = p0;
1865
2034
  com.p = p;
1866
2035
 
1867
- let cp1, cp2, cp1N, cp2N, pN, typeN, area1;
2036
+ let cp1, cp2, cp1N, pN, typeN, area1;
1868
2037
 
1869
2038
  let dimA = getDistAv(p0, p);
1870
2039
  com.dimA = dimA;
@@ -1904,6 +2073,7 @@ function analyzePathData(pathData = []) {
1904
2073
  if (type === 'C') commandPts.push(cp2);
1905
2074
  commandPts.push(p);
1906
2075
 
2076
+ /*
1907
2077
  let commandFlatness = commandIsFlat(commandPts);
1908
2078
  isFlat = commandFlatness.flat;
1909
2079
  com.flat = isFlat;
@@ -1911,6 +2081,7 @@ function analyzePathData(pathData = []) {
1911
2081
  if (isFlat) {
1912
2082
  com.extreme = false;
1913
2083
  }
2084
+ */
1914
2085
  }
1915
2086
 
1916
2087
  /**
@@ -1919,7 +2090,7 @@ function analyzePathData(pathData = []) {
1919
2090
  * so we interpret maximum x/y on-path points as well as extremes
1920
2091
  * but we ignore linetos to allow chunk compilation
1921
2092
  */
1922
- if (!isFlat && type !== 'L' && (p.x === left || p.y === top || p.x === right || p.y === bottom)) {
2093
+ if (type !== 'L' && (p.x === left || p.y === top || p.x === right || p.y === bottom)) {
1923
2094
  com.extreme = true;
1924
2095
  }
1925
2096
 
@@ -1932,7 +2103,7 @@ function analyzePathData(pathData = []) {
1932
2103
  pN = comN ? { x: comNValsL[0], y: comNValsL[1] } : null;
1933
2104
 
1934
2105
  cp1N = { x: comN.values[0], y: comN.values[1] };
1935
- cp2N = comN.type === 'C' ? { x: comN.values[2], y: comN.values[3] } : null;
2106
+ comN.type === 'C' ? { x: comN.values[2], y: comN.values[3] } : null;
1936
2107
  }
1937
2108
 
1938
2109
  /**
@@ -1962,19 +2133,15 @@ function analyzePathData(pathData = []) {
1962
2133
  // check extremes
1963
2134
  let cpts = commandPts.slice(1);
1964
2135
 
1965
- let w = pN ? Math.abs(pN.x - p0.x) : 0;
1966
- let h = pN ? Math.abs(pN.y - p0.y) : 0;
1967
- let thresh = (w + h) / 2 * 0.1;
1968
- let pts1 = type === 'C' ? [p, cp1N, cp2N, pN] : [p, cp1N, pN];
1969
-
1970
- let flatness2 = commandIsFlat(pts1, thresh);
1971
- let isFlat2 = flatness2.flat;
2136
+ pN ? Math.abs(pN.x - p0.x) : 0;
2137
+ pN ? Math.abs(pN.y - p0.y) : 0;
1972
2138
 
1973
2139
  /**
1974
2140
  * if current and next cubic are flat
1975
2141
  * we don't flag them as extremes to allow simplification
1976
2142
  */
1977
- let hasExtremes = (isFlat && isFlat2) ? false : (!com.extreme ? bezierhasExtreme(p0, cpts, angleThreshold) : true);
2143
+
2144
+ let hasExtremes = (!com.extreme ? bezierhasExtreme(p0, cpts, angleThreshold) : true);
1978
2145
 
1979
2146
  if (hasExtremes) {
1980
2147
  com.extreme = true;
@@ -2020,7 +2187,7 @@ function detectAccuracy(pathData) {
2020
2187
 
2021
2188
  // Reference first MoveTo command (M)
2022
2189
  let M = { x: pathData[0].values[0], y: pathData[0].values[1] };
2023
- let p0 = M;
2190
+ let p0 = M;
2024
2191
  let p = M;
2025
2192
  pathData[0].decimals = 0;
2026
2193
 
@@ -2032,28 +2199,33 @@ function detectAccuracy(pathData) {
2032
2199
  let { type, values } = com;
2033
2200
 
2034
2201
  let lastVals = values.length ? values.slice(-2) : [M.x, M.y];
2035
- p={x:lastVals[0], y:lastVals[1]};
2202
+ p = { x: lastVals[0], y: lastVals[1] };
2036
2203
 
2037
2204
  // use existing averave dimension value or calculate
2038
- let dimA = com.dimA ? +com.dimA.toFixed(8) : type!=='M' ? +getDistAv(p0, p).toFixed(8) : 0;
2205
+ let dimA = com.dimA ? +com.dimA.toFixed(8) : type !== 'M' ? +getDistAv(p0, p).toFixed(8) : 0;
2039
2206
 
2040
- if(dimA) dims.add(dimA);
2041
-
2207
+ if (dimA) dims.add(dimA);
2042
2208
 
2043
- if(type==='M'){
2044
- M=p;
2209
+ if (type === 'M') {
2210
+ M = p;
2045
2211
  }
2046
2212
  p0 = p;
2047
2213
  }
2048
2214
 
2049
2215
  let dim_min = Array.from(dims).sort();
2050
- let sliceIdx = Math.ceil(dim_min.length/8);
2051
- dim_min = dim_min.slice(0, sliceIdx );
2052
2216
 
2053
- let dimVal = dim_min.reduce((a,b)=>a+b, 0) / sliceIdx;
2217
+ /*
2218
+ let minVal = dim_min.length > 15 ?
2219
+ (dim_min[0] + dim_min[2]) / 2 :
2220
+ dim_min[0];
2221
+ */
2222
+
2223
+ let sliceIdx = Math.ceil(dim_min.length / 10);
2224
+ dim_min = dim_min.slice(0, sliceIdx);
2225
+ let minVal = dim_min.reduce((a, b) => a + b, 0) / sliceIdx;
2054
2226
 
2055
- let threshold = 50;
2056
- let decimalsAuto = dimVal > threshold ? 0 : Math.floor(threshold / dimVal).toString().length;
2227
+ let threshold = 40;
2228
+ let decimalsAuto = minVal > threshold*1.5 ? 0 : Math.floor(threshold / minVal).toString().length;
2057
2229
 
2058
2230
  // clamp
2059
2231
  return Math.min(Math.max(0, decimalsAuto), 8)
@@ -2069,13 +2241,13 @@ function roundPathData(pathData, decimals = -1) {
2069
2241
  // has recommended decimals
2070
2242
  let hasDecimal = decimals == 'auto' && pathData[0].hasOwnProperty('decimals') ? true : false;
2071
2243
 
2072
- for(let c=0, len=pathData.length; c<len; c++){
2073
- let com=pathData[c];
2244
+ for (let c = 0, len = pathData.length; c < len; c++) {
2245
+ let com = pathData[c];
2074
2246
 
2075
- if (decimals >-1 || hasDecimal) {
2247
+ if (decimals > -1 || hasDecimal) {
2076
2248
  decimals = hasDecimal ? com.decimals : decimals;
2077
2249
 
2078
- pathData[c].values = com.values.map(val=>{return val ? +val.toFixed(decimals) : val });
2250
+ pathData[c].values = com.values.map(val => { return val ? +val.toFixed(decimals) : val });
2079
2251
 
2080
2252
  }
2081
2253
  } return pathData;
@@ -3465,6 +3637,242 @@ function parsePathDataString(d, debug = true) {
3465
3637
 
3466
3638
  }
3467
3639
 
3640
+ function stringifyPathData(pathData) {
3641
+ return pathData.map(com => { return `${com.type} ${com.values.join(' ')}` }).join(' ');
3642
+ }
3643
+
3644
+ function shapeElToPath(el){
3645
+
3646
+ let nodeName = el.nodeName.toLowerCase();
3647
+ if(nodeName==='path')return el;
3648
+
3649
+ let pathData = getPathDataFromEl(el);
3650
+ let d = pathData.map(com=>{return `${com.type} ${com.values} `}).join(' ');
3651
+ let attributes = [...el.attributes].map(att=>att.name);
3652
+
3653
+ let pathN = document.createElementNS('http://www.w3.org/2000/svg', 'path');
3654
+ pathN.setAttribute('d', d );
3655
+
3656
+ let exclude = ['x', 'y', 'cx', 'cy', 'dx', 'dy', 'r', 'rx', 'ry', 'width', 'height', 'points'];
3657
+
3658
+ attributes.forEach(att=>{
3659
+ if(!exclude.includes(att)){
3660
+ let val = el.getAttribute(att);
3661
+ pathN.setAttribute(att, val);
3662
+ }
3663
+ });
3664
+
3665
+ return pathN
3666
+
3667
+ }
3668
+
3669
+ // retrieve pathdata from svg geometry elements
3670
+ function getPathDataFromEl(el, stringify=false) {
3671
+
3672
+ let pathData = [];
3673
+ let type = el.nodeName;
3674
+ let atts, attNames, d, x, y, width, height, r, rx, ry, cx, cy, x1, x2, y1, y2;
3675
+
3676
+ // convert relative or absolute units
3677
+ const svgElUnitsToPixel = (el, decimals = 9) => {
3678
+
3679
+ const svg = el.nodeName !== "svg" ? el.closest("svg") : el;
3680
+
3681
+ // convert real life units to pixels
3682
+ const translateUnitToPixel = (value) => {
3683
+
3684
+ if (value === null) {
3685
+ return 0
3686
+ }
3687
+
3688
+ let dpi = 96;
3689
+ let unit = value.match(/([a-z]+)/gi);
3690
+ unit = unit ? unit[0] : "";
3691
+ let val = parseFloat(value);
3692
+ let rat;
3693
+
3694
+ // no unit - already pixes/user unit
3695
+ if (!unit) {
3696
+ return val;
3697
+ }
3698
+
3699
+ switch (unit) {
3700
+ case "in":
3701
+ rat = dpi;
3702
+ break;
3703
+ case "pt":
3704
+ rat = (1 / 72) * 96;
3705
+ break;
3706
+ case "cm":
3707
+ rat = (1 / 2.54) * 96;
3708
+ break;
3709
+ case "mm":
3710
+ rat = ((1 / 2.54) * 96) / 10;
3711
+ break;
3712
+ // just a default approximation
3713
+ case "em":
3714
+ case "rem":
3715
+ rat = 16;
3716
+ break;
3717
+ default:
3718
+ rat = 1;
3719
+ }
3720
+ let valuePx = val * rat;
3721
+ return +valuePx.toFixed(decimals);
3722
+ };
3723
+
3724
+ // svg width and height attributes
3725
+ let width = svg.getAttribute("width");
3726
+ width = width ? translateUnitToPixel(width) : 300;
3727
+ let height = svg.getAttribute("height");
3728
+ height = width ? translateUnitToPixel(height) : 150;
3729
+
3730
+ let vB = svg.getAttribute("viewBox");
3731
+ vB = vB
3732
+ ? vB
3733
+ .replace(/,/g, " ")
3734
+ .split(" ")
3735
+ .filter(Boolean)
3736
+ .map((val) => {
3737
+ return +val;
3738
+ })
3739
+ : [];
3740
+
3741
+ let w = vB.length ? vB[2] : width;
3742
+ let h = vB.length ? vB[3] : height;
3743
+ let scaleX = w / 100;
3744
+ let scaleY = h / 100;
3745
+ let scalRoot = Math.sqrt((Math.pow(scaleX, 2) + Math.pow(scaleY, 2)) / 2);
3746
+
3747
+ let attsH = ["x", "width", "x1", "x2", "rx", "cx", "r"];
3748
+ let attsV = ["y", "height", "y1", "y2", "ry", "cy"];
3749
+
3750
+ let atts = el.getAttributeNames();
3751
+ atts.forEach((att) => {
3752
+ let val = el.getAttribute(att);
3753
+ let valAbs = val;
3754
+ if (attsH.includes(att) || attsV.includes(att)) {
3755
+ let scale = attsH.includes(att) ? scaleX : scaleY;
3756
+ scale = att === "r" && w != h ? scalRoot : scale;
3757
+ let unit = val.match(/([a-z|%]+)/gi);
3758
+ unit = unit ? unit[0] : "";
3759
+ if (val.includes("%")) {
3760
+ valAbs = parseFloat(val) * scale;
3761
+ }
3762
+
3763
+ else {
3764
+ valAbs = translateUnitToPixel(val);
3765
+ }
3766
+ el.setAttribute(att, +valAbs);
3767
+ }
3768
+ });
3769
+ };
3770
+
3771
+ svgElUnitsToPixel(el);
3772
+
3773
+ const getAtts = (attNames) => {
3774
+ atts = {};
3775
+ attNames.forEach(att => {
3776
+ atts[att] = +el.getAttribute(att);
3777
+ });
3778
+ return atts
3779
+ };
3780
+
3781
+ switch (type) {
3782
+ case 'path':
3783
+ d = el.getAttribute("d");
3784
+ pathData = parsePathDataNormalized(d);
3785
+ break;
3786
+
3787
+ case 'rect':
3788
+ attNames = ['x', 'y', 'width', 'height', 'rx', 'ry'];
3789
+ ({ x, y, width, height, rx, ry } = getAtts(attNames));
3790
+
3791
+ if (!rx && !ry) {
3792
+ pathData = [
3793
+ { type: "M", values: [x, y] },
3794
+ { type: "L", values: [x + width, y] },
3795
+ { type: "L", values: [x + width, y + height] },
3796
+ { type: "L", values: [x, y + height] },
3797
+ { type: "Z", values: [] }
3798
+ ];
3799
+ } else {
3800
+
3801
+ if (rx > width / 2) {
3802
+ rx = width / 2;
3803
+ }
3804
+ if (ry > height / 2) {
3805
+ ry = height / 2;
3806
+ }
3807
+ pathData = [
3808
+ { type: "M", values: [x + rx, y] },
3809
+ { type: "L", values: [x + width - rx, y] },
3810
+ { type: "A", values: [rx, ry, 0, 0, 1, x + width, y + ry] },
3811
+ { type: "L", values: [x + width, y + height - ry] },
3812
+ { type: "A", values: [rx, ry, 0, 0, 1, x + width - rx, y + height] },
3813
+ { type: "L", values: [x + rx, y + height] },
3814
+ { type: "A", values: [rx, ry, 0, 0, 1, x, y + height - ry] },
3815
+ { type: "L", values: [x, y + ry] },
3816
+ { type: "A", values: [rx, ry, 0, 0, 1, x + rx, y] },
3817
+ { type: "Z", values: [] }
3818
+ ];
3819
+ }
3820
+ break;
3821
+
3822
+ case 'circle':
3823
+ case 'ellipse':
3824
+
3825
+ attNames = ['cx', 'cy', 'rx', 'ry', 'r'];
3826
+ ({ cx, cy, r, rx, ry } = getAtts(attNames));
3827
+
3828
+ if (type === 'circle') {
3829
+ r = r;
3830
+ rx = r;
3831
+ ry = r;
3832
+ } else {
3833
+ rx = rx ? rx : r;
3834
+ ry = ry ? ry : r;
3835
+ }
3836
+
3837
+ pathData = [
3838
+ { type: "M", values: [cx + rx, cy] },
3839
+ { type: "A", values: [rx, ry, 0, 1, 1, cx - rx, cy] },
3840
+ { type: "A", values: [rx, ry, 0, 1, 1, cx + rx, cy] },
3841
+ ];
3842
+
3843
+ break;
3844
+ case 'line':
3845
+ attNames = ['x1', 'y1', 'x2', 'y2'];
3846
+ ({ x1, y1, x2, y2 } = getAtts(attNames));
3847
+ pathData = [
3848
+ { type: "M", values: [x1, y1] },
3849
+ { type: "L", values: [x2, y2] }
3850
+ ];
3851
+ break;
3852
+ case 'polygon':
3853
+ case 'polyline':
3854
+
3855
+ let points = el.getAttribute('points').replaceAll(',', ' ').split(' ').filter(Boolean);
3856
+
3857
+ for (let i = 0; i < points.length; i += 2) {
3858
+ pathData.push({
3859
+ type: (i === 0 ? "M" : "L"),
3860
+ values: [+points[i], +points[i + 1]]
3861
+ });
3862
+ }
3863
+ if (type === 'polygon') {
3864
+ pathData.push({
3865
+ type: "Z",
3866
+ values: []
3867
+ });
3868
+ }
3869
+ break;
3870
+ }
3871
+
3872
+ return stringify ? stringifyPathData(pathData): pathData;
3873
+
3874
+ }
3875
+
3468
3876
  function pathDataRemoveColinear(pathData, tolerance = 1, flatBezierToLinetos = true) {
3469
3877
 
3470
3878
  let pathDataN = [pathData[0]];
@@ -3504,6 +3912,7 @@ function pathDataRemoveColinear(pathData, tolerance = 1, flatBezierToLinetos = t
3504
3912
  (type === 'Q' ? [{ x: values[0], y: values[1] }] : []);
3505
3913
 
3506
3914
  isFlatBez = checkBezierFlatness(p0, cpts, p);
3915
+
3507
3916
  // console.log();
3508
3917
 
3509
3918
  if (isFlatBez && c < l - 1 && comPrev.type !== 'C') {
@@ -3545,20 +3954,43 @@ function pathDataRemoveColinear(pathData, tolerance = 1, flatBezierToLinetos = t
3545
3954
 
3546
3955
  }
3547
3956
 
3957
+ function removeOrphanedM(pathData) {
3958
+
3959
+ for (let i = 0, l = pathData.length; i < l; i++) {
3960
+ let com = pathData[i];
3961
+ if (!com) continue;
3962
+ let { type = null, values = [] } = com;
3963
+ let comN = pathData[i + 1] ? pathData[i + 1] : null;
3964
+ if ((type === 'M' || type === 'm')) {
3965
+
3966
+ if (!comN || (comN && (comN.type === 'Z' || comN.type === 'z'))) {
3967
+ pathData[i] = null;
3968
+ pathData[i + 1] = null;
3969
+ }
3970
+ }
3971
+ }
3972
+
3973
+ pathData = pathData.filter(Boolean);
3974
+ return pathData;
3975
+
3976
+ }
3977
+
3978
+ /*
3548
3979
  // remove zero-length segments introduced by rounding
3549
- function removeZeroLengthLinetos_post(pathData) {
3550
- let pathDataOpt = [];
3980
+ export function removeZeroLengthLinetos_post(pathData) {
3981
+ let pathDataOpt = []
3551
3982
  pathData.forEach((com, i) => {
3552
3983
  let { type, values } = com;
3553
3984
  if (type === 'l' || type === 'v' || type === 'h') {
3554
- let hasLength = type === 'l' ? (values.join('') !== '00') : values[0] !== 0;
3555
- if (hasLength) pathDataOpt.push(com);
3985
+ let hasLength = type === 'l' ? (values.join('') !== '00') : values[0] !== 0
3986
+ if (hasLength) pathDataOpt.push(com)
3556
3987
  } else {
3557
- pathDataOpt.push(com);
3988
+ pathDataOpt.push(com)
3558
3989
  }
3559
- });
3990
+ })
3560
3991
  return pathDataOpt
3561
3992
  }
3993
+ */
3562
3994
 
3563
3995
  function removeZeroLengthLinetos(pathData) {
3564
3996
 
@@ -3572,14 +4004,21 @@ function removeZeroLengthLinetos(pathData) {
3572
4004
  let com = pathData[c];
3573
4005
  let { type, values } = com;
3574
4006
 
3575
- let valsL = values.slice(-2);
3576
- p = { x: valsL[0], y: valsL[1] };
4007
+ let valsLen = values.length;
4008
+
4009
+ p = { x: values[valsLen-2], y: values[valsLen-1] };
3577
4010
 
3578
4011
  // skip lineto
3579
4012
  if (type === 'L' && p.x === p0.x && p.y === p0.y) {
3580
4013
  continue
3581
4014
  }
3582
4015
 
4016
+ // skip minified zero length
4017
+ if (type === 'l' || type === 'v' || type === 'h') {
4018
+ let noLength = type === 'l' ? (values.join('') === '00') : values[0] === 0;
4019
+ if(noLength) continue
4020
+ }
4021
+
3583
4022
  pathDataN.push(com);
3584
4023
  p0 = p;
3585
4024
  }
@@ -3918,6 +4357,145 @@ function reversePathData(pathData, {
3918
4357
  return pathDataNew;
3919
4358
  }
3920
4359
 
4360
+ function refineAdjacentExtremes(pathData, {
4361
+ threshold = null, tolerance = 1
4362
+ } = {}) {
4363
+
4364
+ if (!threshold) {
4365
+ let bb = getPathDataBBox(pathData);
4366
+ threshold = (bb.width + bb.height) / 2 * 0.05;
4367
+
4368
+ }
4369
+
4370
+ let l = pathData.length;
4371
+
4372
+ for (let i = 0; i < l; i++) {
4373
+ let com = pathData[i];
4374
+ let { type, values, extreme, corner=false, dimA, p0, p } = com;
4375
+ let comN = pathData[i + 1] ? pathData[i + 1] : null;
4376
+
4377
+ // adjacent
4378
+
4379
+ if (comN && type === 'C' && comN.type === 'C' && extreme && !corner) {
4380
+
4381
+ // check dist
4382
+ let diff = getDistAv(p, comN.p);
4383
+ let isCose = diff < threshold;
4384
+
4385
+ if (isCose) {
4386
+
4387
+ let dx1 = (com.cp1.x - comN.p0.x);
4388
+ let dy1 = (com.cp1.y - comN.p0.y);
4389
+
4390
+ let horizontal = Math.abs(dy1) < Math.abs(dx1);
4391
+
4392
+ let pN = comN.p;
4393
+ let ptI;
4394
+ let t = 1;
4395
+
4396
+ if (comN.extreme) {
4397
+
4398
+ // extend cp2
4399
+ if (horizontal) {
4400
+ t = Math.abs(Math.abs(comN.cp2.x - comN.p.x) / Math.abs(com.cp2.x - com.p.x));
4401
+
4402
+ ptI = interpolate(comN.p, com.cp2, 1 + t);
4403
+ com.cp2.x = ptI.x;
4404
+
4405
+ }
4406
+ else {
4407
+
4408
+ t = Math.abs(Math.abs(comN.cp2.y - comN.p.y) / Math.abs(com.cp2.y - com.p.y));
4409
+ ptI = interpolate(comN.p, com.cp2, 1 + t);
4410
+ com.cp2.y = ptI.y;
4411
+ }
4412
+
4413
+ pathData[i + 1].values = [com.cp1.x, com.cp1.y, com.cp2.x, com.cp2.y, pN.x, pN.y];
4414
+ pathData[i + 1].cp1 = com.cp1;
4415
+ pathData[i + 1].cp2 = com.cp2;
4416
+ pathData[i + 1].p0 = com.p0;
4417
+ pathData[i + 1].p = pN;
4418
+ pathData[i + 1].extreme = true;
4419
+
4420
+ // nullify 1st
4421
+ pathData[i] = null;
4422
+ continue
4423
+
4424
+ }
4425
+
4426
+ // extend fist command
4427
+ else {
4428
+
4429
+ let comN2 = pathData[i + 2] ? pathData[i + 2] : null;
4430
+ if (!comN2 && comN2.type !== 'C') continue
4431
+
4432
+ // extrapolate
4433
+ let comEx = getCombinedByDominant(comN, comN2, threshold, tolerance, false);
4434
+
4435
+ if (comEx.length === 1) {
4436
+ pathData[i + 1] = null;
4437
+
4438
+ comEx = comEx[0];
4439
+
4440
+ pathData[i + 2].values = [comEx.cp1.x, comEx.cp1.y, comEx.cp2.x, comEx.cp2.y, comEx.p.x, comEx.p.y];
4441
+ pathData[i + 2].cp1 = comEx.cp1;
4442
+ pathData[i + 2].cp2 = comEx.cp2;
4443
+ pathData[i + 2].p0 = comEx.p0;
4444
+ pathData[i + 2].p = comEx.p;
4445
+ pathData[i + 2].extreme = comEx.extreme;
4446
+
4447
+ i++;
4448
+ continue
4449
+ }
4450
+
4451
+ }
4452
+
4453
+ }
4454
+ }
4455
+ }
4456
+
4457
+ // remove commands
4458
+ pathData = pathData.filter(Boolean);
4459
+ l = pathData.length;
4460
+
4461
+ /**
4462
+ * refine closing commands
4463
+ */
4464
+
4465
+ let closed = pathData[l - 1].type.toLowerCase() === 'z';
4466
+ let lastIdx = closed ? l - 2 : l - 1;
4467
+ let lastCom = pathData[lastIdx];
4468
+ let penultimateCom = pathData[lastIdx - 1] || null;
4469
+ let M = { x: pathData[0].values[0], y: pathData[0].values[1] };
4470
+
4471
+ let dec = 8;
4472
+ let lastVals = lastCom.values.slice(-2);
4473
+ let isClosingTo = +lastVals[0].toFixed(dec) === +M.x.toFixed(dec) && +lastVals[1].toFixed(dec) === +M.y.toFixed(dec);
4474
+ let fistExt = pathData[1].type === 'C' && pathData[1].extreme ? pathData[1] : null;
4475
+
4476
+ let diff = getDistAv(lastCom.p0, lastCom.p);
4477
+ let isCose = diff < threshold;
4478
+
4479
+ if (penultimateCom && penultimateCom.type === 'C' && isCose && isClosingTo && fistExt) {
4480
+
4481
+ Math.abs(fistExt.cp1.x - M.x);
4482
+ Math.abs(fistExt.cp1.y - M.y);
4483
+
4484
+ let comEx = getCombinedByDominant(penultimateCom, lastCom, threshold, tolerance, false);
4485
+ console.log('comEx', comEx);
4486
+
4487
+ if (comEx.length === 1) {
4488
+ pathData[lastIdx - 1] = comEx[0];
4489
+ pathData[lastIdx] = null;
4490
+ pathData = pathData.filter(Boolean);
4491
+ }
4492
+
4493
+ }
4494
+
4495
+ return pathData
4496
+
4497
+ }
4498
+
3921
4499
  function removeEmptySVGEls(svg) {
3922
4500
  let els = svg.querySelectorAll('g, defs');
3923
4501
  els.forEach(el => {
@@ -3940,7 +4518,7 @@ function cleanUpSVG(svgMarkup, {
3940
4518
  .querySelector("svg");
3941
4519
 
3942
4520
 
3943
- let allowed=['viewBox', 'xmlns', 'width', 'height', 'id', 'class'];
4521
+ let allowed=['viewBox', 'xmlns', 'width', 'height', 'id', 'class', 'fill', 'stroke', 'stroke-width'];
3944
4522
  removeExcludedAttribues(svg, allowed);
3945
4523
 
3946
4524
  let removeEls = ['metadata', 'script'];
@@ -4033,11 +4611,13 @@ function svgPathSimplify(input = '', {
4033
4611
  flatBezierToLinetos = true,
4034
4612
  revertToQuadratics = true,
4035
4613
 
4614
+ refineExtremes = true,
4036
4615
  keepExtremes = true,
4037
4616
  keepCorners = true,
4038
4617
  extrapolateDominant = true,
4039
4618
  keepInflections = false,
4040
4619
  addExtremes = false,
4620
+ removeOrphanSubpaths = false,
4041
4621
 
4042
4622
  // svg path optimizations
4043
4623
  decimals = 3,
@@ -4051,6 +4631,7 @@ function svgPathSimplify(input = '', {
4051
4631
  mergePaths = false,
4052
4632
  removeHidden = true,
4053
4633
  removeUnused = true,
4634
+ shapesToPaths = true,
4054
4635
 
4055
4636
  } = {}) {
4056
4637
 
@@ -4093,6 +4674,14 @@ function svgPathSimplify(input = '', {
4093
4674
  svg = cleanUpSVG(input, { returnDom, removeHidden, removeUnused }
4094
4675
  );
4095
4676
 
4677
+ if(shapesToPaths){
4678
+ let shapes = svg.querySelectorAll('polygon, polyline, line, rect, circle, ellipse');
4679
+ shapes.forEach(shape=>{
4680
+ let path = shapeElToPath(shape);
4681
+ shape.replaceWith(path);
4682
+ });
4683
+ }
4684
+
4096
4685
  // collect paths
4097
4686
  let pathEls = svg.querySelectorAll('path');
4098
4687
  pathEls.forEach(path => {
@@ -4119,12 +4708,15 @@ function svgPathSimplify(input = '', {
4119
4708
 
4120
4709
  let pathDataO = parsePathDataNormalized(d, { quadraticToCubic, toAbsolute, arcToCubic });
4121
4710
 
4122
- // create clone for fallback
4123
- let pathData = JSON.parse(JSON.stringify(pathDataO));
4124
-
4125
4711
  // count commands for evaluation
4126
4712
  let comCount = pathDataO.length;
4127
4713
 
4714
+ // create clone for fallback
4715
+
4716
+ let pathData = pathDataO;
4717
+
4718
+ if(removeOrphanSubpaths) pathData = removeOrphanedM(pathData);
4719
+
4128
4720
  /**
4129
4721
  * get sub paths
4130
4722
  */
@@ -4162,6 +4754,12 @@ function svgPathSimplify(input = '', {
4162
4754
 
4163
4755
  pathData = simplifyBezier ? simplifyPathDataCubic(pathData, { simplifyBezier, keepInflections, keepExtremes, keepCorners, extrapolateDominant, revertToQuadratics, tolerance, reverse }) : pathData;
4164
4756
 
4757
+ // refine extremes
4758
+ if(refineExtremes){
4759
+ let thresholdEx = (bb.width + bb.height) / 2 * 0.05;
4760
+ pathData = refineAdjacentExtremes(pathData, {threshold:thresholdEx, tolerance});
4761
+ }
4762
+
4165
4763
  // cubic to arcs
4166
4764
  if (cubicToArc) {
4167
4765
 
@@ -4229,13 +4827,15 @@ function svgPathSimplify(input = '', {
4229
4827
  */
4230
4828
  if (autoAccuracy) {
4231
4829
  decimals = detectAccuracy(pathData);
4830
+ pathOptions.decimals = decimals;
4831
+
4232
4832
  }
4233
4833
 
4234
4834
  // optimize path data
4235
4835
  pathData = convertPathData(pathData, pathOptions);
4236
4836
 
4237
4837
  // remove zero-length segments introduced by rounding
4238
- pathData = removeZeroLengthLinetos_post(pathData);
4838
+ pathData = removeZeroLengthLinetos(pathData);
4239
4839
 
4240
4840
  // compare command count
4241
4841
  let comCountS = pathData.length;
@@ -4269,7 +4869,8 @@ function svgPathSimplify(input = '', {
4269
4869
  let pathData = convertPathData(pathData_merged, pathOptions);
4270
4870
 
4271
4871
  // remove zero-length segments introduced by rounding
4272
- pathData = removeZeroLengthLinetos_post(pathData);
4872
+
4873
+ pathData = removeZeroLengthLinetos(pathData);
4273
4874
 
4274
4875
  let dOpt = pathDataToD(pathData, minifyD);
4275
4876
 
@@ -4374,6 +4975,7 @@ function simplifyPathDataCubic(pathData, {
4374
4975
  // add cumulative error to prevent distortions
4375
4976
 
4376
4977
  error += combined[0].error * 0.5;
4978
+
4377
4979
  offset++;
4378
4980
  }
4379
4981
  com = combined[0];
@@ -4410,7 +5012,11 @@ const {
4410
5012
  log, hypot, max, min, pow, random, round, sin, sqrt, tan, PI
4411
5013
  } = Math;
4412
5014
 
4413
- // just for visual debugging
5015
+ /*
5016
+ import {XMLSerializerPoly, DOMParserPoly} from './dom_polyfills';
5017
+ export {XMLSerializerPoly as XMLSerializerPoly};
5018
+ export {DOMParserPoly as DOMParserPoly};
5019
+ */
4414
5020
 
4415
5021
  // IIFE
4416
5022
  if (typeof window !== 'undefined') {