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