svg-path-simplify 0.4.4 → 0.4.6

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 (48) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/README.md +9 -1
  3. package/dist/svg-path-simplify.esm.js +1649 -1205
  4. package/dist/svg-path-simplify.esm.min.js +2 -2
  5. package/dist/svg-path-simplify.js +1649 -1204
  6. package/dist/svg-path-simplify.min.js +2 -2
  7. package/dist/svg-path-simplify.pathdata.esm.js +301 -637
  8. package/dist/svg-path-simplify.pathdata.esm.min.js +2 -2
  9. package/dist/svg-path-simplify.poly.cjs +8 -9
  10. package/drawing.svg +62 -0
  11. package/index.html +13 -8
  12. package/package.json +1 -1
  13. package/src/index.js +3 -1
  14. package/src/pathData_get_intersections.js +777 -0
  15. package/src/pathData_offset.js +201 -0
  16. package/src/pathData_simplify_cubic.js +19 -21
  17. package/src/pathData_simplify_cubic_extrapolate.js +147 -8
  18. package/src/pathData_simplify_cubicsToArcs.js +65 -21
  19. package/src/pathSimplify-main.js +267 -64
  20. package/src/pathSimplify-presets.js +1 -1
  21. package/src/poly-fit-curve-schneider.js +171 -132
  22. package/src/poly-fit-curve-schneider_check_bulge.js +131 -0
  23. package/src/poly-offset.js +133 -0
  24. package/src/simplify_poly_RC.js +29 -7
  25. package/src/svg-getAttributes.js +1 -1
  26. package/src/svgii/geometry.js +130 -27
  27. package/src/svgii/geometry_bbox.js +1 -1
  28. package/src/svgii/geometry_length.js +1 -1
  29. package/src/svgii/pathData_analyze.js +6 -28
  30. package/src/svgii/pathData_convert.js +63 -15
  31. package/src/svgii/pathData_remove_collinear.js +32 -36
  32. package/src/svgii/pathData_reorder.js +3 -2
  33. package/src/svgii/pathData_simplify_refineExtremes.js +1 -3
  34. package/src/svgii/pathData_stringify.js +132 -6
  35. package/src/svgii/pathData_toPolygon.js +33 -23
  36. package/src/svgii/poly_analyze.js +272 -125
  37. package/src/svgii/poly_analyze_cleanup.js +311 -0
  38. package/src/svgii/poly_analyze_getTangents.js +125 -0
  39. package/src/svgii/poly_analyze_get_chunks.js +3 -1
  40. package/src/svgii/poly_normalize.js +21 -8
  41. package/src/svgii/poly_to_pathdata.js +477 -8
  42. package/src/svgii/rounding.js +35 -14
  43. package/src/svgii/svg_cleanup.js +44 -51
  44. package/src/svgii/svg_cleanup_convertPathLength.js +5 -0
  45. package/src/svgii/svg_cleanup_normalize_transforms.js +2 -2
  46. package/src/svgii/visualize.js +33 -2
  47. package/v/0.4.5/index.html +1040 -0
  48. package/v/0.4.5/svg-path-simplify.js +13227 -0
@@ -363,7 +363,7 @@ const {
363
363
 
364
364
  // get angle helper
365
365
  function getAngle(p1, p2, normalize = false) {
366
- let angle = atan2(p2.y - p1.y, p2.x - p1.x);
366
+ let angle = Math.atan2(p2.y - p1.y, p2.x - p1.x);
367
367
  // normalize negative angles
368
368
  if (normalize && angle < 0) angle += Math.PI * 2;
369
369
  return angle
@@ -459,7 +459,9 @@ function checkLineIntersection(p1 = null, p2 = null, p3 = null, p4 = null, exact
459
459
  // if we cast these lines infinitely in both directions, they intersect here:
460
460
  intersectionPoint = {
461
461
  x: p1.x + (a * (p2.x - p1.x)),
462
- y: p1.y + (a * (p2.y - p1.y))
462
+ y: p1.y + (a * (p2.y - p1.y)),
463
+ t1: a,
464
+ t2: b
463
465
  };
464
466
 
465
467
  let intersection = false;
@@ -523,13 +525,13 @@ function pointAtT(pts, t = 0.5, getTangent = false, getCpts = false, returnArray
523
525
  if (t === 0 && !shortCp1) {
524
526
  pt.x = p0.x;
525
527
  pt.y = p0.y;
526
- pt.angle = getAngle(p0, cp1);
528
+ if (getTangent) pt.angle = getAngle(p0, cp1);
527
529
  }
528
530
 
529
531
  else if (t === 1 && !shortCp2) {
530
532
  pt.x = p.x;
531
533
  pt.y = p.y;
532
- pt.angle = getAngle(cp2, p);
534
+ if (getTangent) pt.angle = getAngle(cp2, p);
533
535
  }
534
536
 
535
537
  else {
@@ -546,18 +548,38 @@ function pointAtT(pts, t = 0.5, getTangent = false, getCpts = false, returnArray
546
548
  pt = interpolate(m3, m4, t);
547
549
 
548
550
  // add angles
549
- pt.angle = getAngle(m3, m4);
551
+ if (getTangent) pt.angle = getAngle(m3, m4);
552
+
553
+ /*
554
+ renderPoint(markers, m0, 'cyan')
555
+ renderPoint(markers, m3, 'magenta')
556
+ renderPoint(markers, pt, 'green')
557
+ renderPoint(markers, m4, 'cyan')
558
+ renderPoint(markers, m2, 'magenta')
559
+ */
550
560
 
551
561
  // add control points
552
- if (getCpts) pt.cpts = [m1, m2, m3, m4];
562
+ if (getCpts) {
563
+ pt.cpts = [m1, m2, m3, m4];
564
+ pt.segments = [
565
+ { p0, cp1: m0, cp2: m3, p: pt },
566
+ { p0: pt, cp1: m4, cp2: m2, p }
567
+ ];
568
+ }
553
569
  } else {
554
570
  m1 = interpolate(p0, cp1, t);
555
571
  m2 = interpolate(cp1, p, t);
556
572
  pt = interpolate(m1, m2, t);
557
- pt.angle = getAngle(m1, m2);
573
+ if (getTangent) pt.angle = getAngle(m1, m2);
558
574
 
559
575
  // add control points
560
- if (getCpts) pt.cpts = [m1, m2];
576
+ if (getCpts) {
577
+ pt.cpts = [m1, m2];
578
+ pt.segments = [
579
+ { p0, cp1: m1, p: pt },
580
+ { p0: p, cp1: m2, p }
581
+ ];
582
+ }
561
583
  }
562
584
  }
563
585
 
@@ -641,7 +663,7 @@ function pointAtT(pts, t = 0.5, getTangent = false, getCpts = false, returnArray
641
663
  * get vertices from path command final on-path points
642
664
  */
643
665
 
644
- function getPathDataVertices(pathData=[], includeCpts = false, decimals = -1) {
666
+ function getPathDataVertices(pathData = [], includeCpts = false, decimals = -1) {
645
667
  let polyPoints = [];
646
668
 
647
669
  pathData.forEach((com) => {
@@ -728,7 +750,8 @@ function svgArcToCenterParam(x1, y1, rx, ry, xAxisRotation, largeArc, sweep, x2,
728
750
 
729
751
  if (rx == 0 || ry == 0) {
730
752
  // invalid arguments
731
- throw Error("rx and ry can not be 0");
753
+ console.warn("rx and ry can not be 0");
754
+ return arcData
732
755
  }
733
756
 
734
757
  /**
@@ -741,10 +764,10 @@ function svgArcToCenterParam(x1, y1, rx, ry, xAxisRotation, largeArc, sweep, x2,
741
764
  let s_phi = !phi ? 0 : Math.sin(phi);
742
765
  let c_phi = !phi ? 1 : Math.cos(phi);
743
766
 
744
- let hd_x = (x1 - x2) / 2;
745
- let hd_y = (y1 - y2) / 2;
746
- let hs_x = (x1 + x2) / 2;
747
- let hs_y = (y1 + y2) / 2;
767
+ let hd_x = (x1 - x2) * 0.5;
768
+ let hd_y = (y1 - y2) * 0.5;
769
+ let hs_x = (x1 + x2) * 0.5;
770
+ let hs_y = (y1 + y2) * 0.5;
748
771
 
749
772
  // F6.5.1
750
773
  let x1_ = !phi ? hd_x : c_phi * hd_x + s_phi * hd_y;
@@ -766,10 +789,11 @@ function svgArcToCenterParam(x1, y1, rx, ry, xAxisRotation, largeArc, sweep, x2,
766
789
  let rxry = rx * ry;
767
790
  let rxy1_ = rx * y1_;
768
791
  let ryx1_ = ry * x1_;
769
- let sum_of_sq = rxy1_ ** 2 + ryx1_ ** 2; // sum of square
792
+ let sum_of_sq = rxy1_ * rxy1_ + ryx1_ * ryx1_; // sum of square
770
793
  if (!sum_of_sq) {
794
+ console.warn("start point can not be same as end point");
795
+ return arcData
771
796
 
772
- throw Error("start point can not be same as end point");
773
797
  }
774
798
  let coe = Math.sqrt(Math.abs((rxry * rxry - sum_of_sq) / sum_of_sq));
775
799
  if (largeArc === sweep) {
@@ -834,39 +858,6 @@ function rotatePoint(pt, cx, cy, rotation = 0, convertToRadians = false) {
834
858
  };
835
859
  }
836
860
 
837
- function getPointOnEllipse(cx, cy, rx, ry, angle, ellipseRotation = 0, parametricAngle = true, degrees = false) {
838
-
839
- // Convert degrees to radians
840
- angle = degrees ? (angle * PI) / 180 : angle;
841
- ellipseRotation = degrees ? (ellipseRotation * PI) / 180 : ellipseRotation;
842
- // reset rotation for circles or 360 degree
843
- ellipseRotation = rx !== ry ? (ellipseRotation !== PI * 2 ? ellipseRotation : 0) : 0;
844
-
845
- // is ellipse
846
- if (parametricAngle && rx !== ry) {
847
- // adjust angle for ellipse rotation
848
- angle = ellipseRotation ? angle - ellipseRotation : angle;
849
- // Get the parametric angle for the ellipse
850
- let angleParametric = atan(tan(angle) * (rx / ry));
851
- // Ensure the parametric angle is in the correct quadrant
852
- angle = cos(angle) < 0 ? angleParametric + PI : angleParametric;
853
- }
854
-
855
- // Calculate the point on the ellipse without rotation
856
- let x = cx + rx * cos(angle),
857
- y = cy + ry * sin(angle);
858
- let pt = {
859
- x: x,
860
- y: y
861
- };
862
-
863
- if (ellipseRotation) {
864
- pt.x = cx + (x - cx) * cos(ellipseRotation) - (y - cy) * sin(ellipseRotation);
865
- pt.y = cy + (x - cx) * sin(ellipseRotation) + (y - cy) * cos(ellipseRotation);
866
- }
867
- return pt
868
- }
869
-
870
861
  // to parametric angle helper
871
862
  function toParametricAngle(angle, rx, ry) {
872
863
 
@@ -918,7 +909,11 @@ function bezierhasExtreme(p0 = null, cpts = []) {
918
909
 
919
910
  function getBezierExtremeT(pts, { addExtremes = true, addSemiExtremes = false } = {}) {
920
911
  let tArr = pts.length === 4 ? cubicBezierExtremeT(pts[0], pts[1], pts[2], pts[3], { addExtremes, addSemiExtremes }) : quadraticBezierExtremeT(pts[0], pts[1], pts[2], { addExtremes, addSemiExtremes });
921
- return tArr;
912
+ if (tArr.length) {
913
+ tArr = tArr.map(t => +t.toFixed(9)).sort();
914
+ }
915
+
916
+ return tArr
922
917
  }
923
918
 
924
919
  function getArcExtemes(p0, values) {
@@ -1248,13 +1243,6 @@ function cubicBezierExtremeT(p0, cp1, cp2, p,
1248
1243
  return tArr;
1249
1244
  }
1250
1245
 
1251
- function isMultipleOf45(angleRad, epsilon = 0.001) {
1252
- let rad90 = Math.PI / 2;
1253
- let rad45 = rad90 / 2;
1254
- let isRightAngle = Math.abs(angleRad / rad90 - Math.round(angleRad / rad90)) < epsilon;
1255
- return !isRightAngle ? Math.abs(angleRad / rad45 - Math.round(angleRad / rad45)) < epsilon : false;
1256
- }
1257
-
1258
1246
  function quadraticBezierExtremeT(p0, cp1, p, { addExtremes = true, addSemiExtremes = false } = {}) {
1259
1247
 
1260
1248
  // rotate cpts for semi extremes
@@ -1318,6 +1306,11 @@ function getDistance(p1, p2, isArray = false) {
1318
1306
  let dx = isArray ? p2[0] - p1[0] : (p2.x - p1.x);
1319
1307
  let dy = isArray ? p2[1] - p1[1] : (p2.y - p1.y);
1320
1308
 
1309
+ /*
1310
+ let sqrt2 = 1.4142135623730951
1311
+ return dx===dy ? Math.abs(dx) * sqrt2 : Math.sqrt(dx * dx + dy * dy);
1312
+ */
1313
+
1321
1314
  return Math.sqrt(dx * dx + dy * dy);
1322
1315
  }
1323
1316
 
@@ -1333,6 +1326,7 @@ function getSquareDistance(p1, p2) {
1333
1326
  * sloppy but fast
1334
1327
  */
1335
1328
  function getDistManhattan(pt1, pt2) {
1329
+
1336
1330
  let dx = Math.abs(pt2.x - pt1.x);
1337
1331
  let dy = Math.abs(pt2.y - pt1.y);
1338
1332
  return dx + dy;
@@ -1657,7 +1651,7 @@ function roundPathData(pathData, decimalsGlobal = -1) {
1657
1651
 
1658
1652
  let len = pathData.length;
1659
1653
  let decimals = decimalsGlobal;
1660
- let decimalsArc = decimals < 3 ? decimals+2 : decimals;
1654
+ let decimalsArc = decimals < 3 ? decimals + 1 : decimals;
1661
1655
 
1662
1656
  for (let c = 0; c < len; c++) {
1663
1657
  let com = pathData[c];
@@ -1684,25 +1678,30 @@ function detectAccuracy(pathData) {
1684
1678
  let com = pathData[i];
1685
1679
  let { type, values, p0 = null, p = null, dimA = 0 } = com;
1686
1680
 
1687
- // use existing averave dimension value or calculate
1681
+ // use existing average dimension value or calculate
1688
1682
  if (values.length && p && p0) {
1689
-
1690
1683
  dimA = dimA ? dimA : getDistManhattan(p0, p);
1691
1684
 
1692
- if (dimA) dims.push(dimA);
1685
+ if (type === 'A') dimA *= 0.5;
1693
1686
 
1687
+ if (dimA) dims.push(+dimA.toFixed(8));
1694
1688
  }
1695
1689
 
1696
1690
  }
1697
1691
 
1698
- let dim_min = dims.sort();
1692
+ dims = dims.sort((a,b)=>a-b);
1693
+ let len = dims.length;
1694
+ let dim_mid = dims[Math.floor(len * 0.5)];
1699
1695
 
1700
- let sliceIdx = Math.ceil(dim_min.length / 6);
1701
- dim_min = dim_min.slice(0, sliceIdx);
1702
- let minVal = dim_min.reduce((a, b) => a + b, 0) / sliceIdx;
1696
+ // smallest 25% of values
1697
+ let idx_q = Math.ceil(len * 0.25);
1698
+ let dims_min = dims.slice(0, idx_q);
1699
+
1700
+ // average smallest values with mid value
1701
+ let dim_min = ((dims_min.reduce((a, b) => a + b, 0) / idx_q) + dim_mid) * 0.5;
1703
1702
 
1704
1703
  let threshold = 75;
1705
- let decimalsAuto = minVal > threshold * 1.5 ? 0 : Math.floor(threshold / minVal).toString().length;
1704
+ let decimalsAuto = dim_min > threshold * 1.5 ? 0 : Math.floor(threshold / dim_min).toString().length;
1706
1705
 
1707
1706
  // clamp
1708
1707
  return Math.min(Math.max(0, decimalsAuto), 8)
@@ -1766,117 +1765,6 @@ function getPolyBBox(vertices, decimals = -1) {
1766
1765
  return bb;
1767
1766
  }
1768
1767
 
1769
- function getSubPathBBoxes(subPaths) {
1770
- let bboxArr = [];
1771
- subPaths.forEach((pathData) => {
1772
-
1773
- let bb = getPathDataBBox_sloppy(pathData);
1774
- bboxArr.push(bb);
1775
- });
1776
-
1777
- return bboxArr;
1778
- }
1779
-
1780
- function checkBBoxIntersections(bb, bb1) {
1781
- let [x, y, width, height, right, bottom] = [
1782
- bb.x,
1783
- bb.y,
1784
- bb.width,
1785
- bb.height,
1786
- bb.x + bb.width,
1787
- bb.y + bb.height
1788
- ];
1789
- let [x1, y1, width1, height1, right1, bottom1] = [
1790
- bb1.x,
1791
- bb1.y,
1792
- bb1.width,
1793
- bb1.height,
1794
- bb1.x + bb1.width,
1795
- bb1.y + bb1.height
1796
- ];
1797
- let intersects = false;
1798
- if (width * height != width1 * height1) {
1799
- if (width * height > width1 * height1) {
1800
- if (x < x1 && right > right1 && y < y1 && bottom > bottom1) {
1801
- intersects = true;
1802
- }
1803
- }
1804
- }
1805
- return intersects;
1806
- }
1807
-
1808
- /**
1809
- * sloppy path bbox aaproximation
1810
- */
1811
-
1812
- function getPathDataBBox_sloppy(pathData) {
1813
- let pts = getPathDataPoly(pathData);
1814
- let bb = getPolyBBox(pts);
1815
- return bb;
1816
- }
1817
-
1818
- /**
1819
- * get path data poly
1820
- * including command points
1821
- * handy for faster/sloppy bbox approximations
1822
- */
1823
-
1824
- function getPathDataPoly(pathData) {
1825
-
1826
- let poly = [];
1827
- for (let i = 0; i < pathData.length; i++) {
1828
- let com = pathData[i];
1829
- let prev = i > 0 ? pathData[i - 1] : pathData[i];
1830
- let { type, values } = com;
1831
- let p0 = { x: prev.values[prev.values.length - 2], y: prev.values[prev.values.length - 1] };
1832
- let p = values.length ? { x: values[values.length - 2], y: values[values.length - 1] } : '';
1833
- let cp1 = values.length ? { x: values[0], y: values[1] } : '';
1834
-
1835
- switch (type) {
1836
-
1837
- // convert to cubic to get polygon
1838
- case 'A':
1839
-
1840
- if (typeof arcToBezier !== 'function') {
1841
-
1842
- // get real radii
1843
- let rx = getDistance(p0, p) / 2;
1844
- let ptMid = interpolate(p0, p, 0.5);
1845
-
1846
- let pt1 = getPointOnEllipse(ptMid.x, ptMid.y, rx, rx, 0);
1847
- let pt2 = getPointOnEllipse(ptMid.x, ptMid.y, rx, rx, Math.PI);
1848
- poly.push(pt1, pt2, p);
1849
-
1850
- break;
1851
- }
1852
- let cubic = arcToBezier(p0, values);
1853
- cubic.forEach(com => {
1854
- let vals = com.values;
1855
- let cp1 = { x: vals[0], y: vals[1] };
1856
- let cp2 = { x: vals[2], y: vals[3] };
1857
- let p = { x: vals[4], y: vals[5] };
1858
- poly.push(cp1, cp2, p);
1859
- });
1860
- break;
1861
-
1862
- case 'C':
1863
- let cp2 = { x: values[2], y: values[3] };
1864
- poly.push(cp1, cp2);
1865
- break;
1866
- case 'Q':
1867
- poly.push(cp1);
1868
- break;
1869
- }
1870
-
1871
- // M and L commands
1872
- if (type.toLowerCase() !== 'z') {
1873
- poly.push(p);
1874
- }
1875
- }
1876
-
1877
- return poly;
1878
- }
1879
-
1880
1768
  /**
1881
1769
  * get exact path BBox
1882
1770
  * calculating extremes for all command types
@@ -1940,166 +1828,10 @@ function getPathDataBBox(pathData) {
1940
1828
  }
1941
1829
  }
1942
1830
 
1943
- let bbox = { x: xMin, y: yMin, width: xMax - xMin, height: yMax - yMin };
1831
+ let bbox = { x: xMin, y: yMin, right: xMax, width: xMax - xMin, bottom: yMax, height: yMax - yMin };
1944
1832
  return bbox
1945
1833
  }
1946
1834
 
1947
- /**
1948
- * get pathdata area
1949
- */
1950
-
1951
- function getPathArea(pathData, decimals = 9) {
1952
- let totalArea = 0;
1953
- let polyPoints = [];
1954
-
1955
- let subPathsData = splitSubpaths(pathData);
1956
- let isCompoundPath = subPathsData.length > 1 ? true : false;
1957
- let counterShapes = [];
1958
-
1959
- // check intersections for compund paths
1960
- if (isCompoundPath) {
1961
- let bboxArr = getSubPathBBoxes(subPathsData);
1962
-
1963
- bboxArr.forEach(function (bb, b) {
1964
-
1965
- for (let i = 0; i < bboxArr.length; i++) {
1966
- let bb2 = bboxArr[i];
1967
- if (bb != bb2) {
1968
- let intersects = checkBBoxIntersections(bb, bb2);
1969
- if (intersects) {
1970
- counterShapes.push(i);
1971
- }
1972
- }
1973
- }
1974
- });
1975
- }
1976
-
1977
- subPathsData.forEach((pathData, d) => {
1978
-
1979
- polyPoints = [];
1980
- let comArea = 0;
1981
- let pathArea = 0;
1982
- let multiplier = 1;
1983
- let pts = [];
1984
-
1985
- pathData.forEach(function (com, i) {
1986
- let [type, values] = [com.type, com.values];
1987
- let valuesL = values.length;
1988
-
1989
- if (values.length) {
1990
- let prevC = i > 0 ? pathData[i - 1] : pathData[0];
1991
- let prevCVals = prevC.values;
1992
- let prevCValsL = prevCVals.length;
1993
- let p0 = { x: prevCVals[prevCValsL - 2], y: prevCVals[prevCValsL - 1] };
1994
- let p = { x: values[valuesL - 2], y: values[valuesL - 1] };
1995
-
1996
- // C commands
1997
- if (type === 'C' || type === 'Q') {
1998
- let cp1 = { x: values[0], y: values[1] };
1999
- pts = type === 'C' ? [p0, cp1, { x: values[2], y: values[3] }, p] : [p0, cp1, p];
2000
- let areaBez = Math.abs(getBezierArea(pts));
2001
- comArea += areaBez;
2002
-
2003
- polyPoints.push(p0, p);
2004
- }
2005
-
2006
- // A commands
2007
- else if (type === 'A') {
2008
- let arcData = svgArcToCenterParam(p0.x, p0.y, com.values[0], com.values[1], com.values[2], com.values[3], com.values[4], p.x, p.y);
2009
- let { cx, cy, rx, ry, startAngle, endAngle, deltaAngle } = arcData;
2010
-
2011
- let arcArea = Math.abs(getEllipseArea(rx, ry, startAngle, endAngle));
2012
-
2013
- // subtract remaining polygon between p0, center and p
2014
- let polyArea = Math.abs(getPolygonArea([p0, { x: cx, y: cy }, p]));
2015
- arcArea -= polyArea;
2016
-
2017
- polyPoints.push(p0, p);
2018
- comArea += arcArea;
2019
- }
2020
-
2021
- // L commands
2022
- else {
2023
- polyPoints.push(p0, p);
2024
- }
2025
- }
2026
- });
2027
-
2028
- let areaPoly = getPolygonArea(polyPoints);
2029
-
2030
- if (counterShapes.indexOf(d) !== -1) {
2031
- multiplier = -1;
2032
- }
2033
-
2034
- if (
2035
- (areaPoly < 0 && comArea < 0)
2036
- ) {
2037
- // are negative
2038
- pathArea = (Math.abs(comArea) - Math.abs(areaPoly)) * multiplier;
2039
-
2040
- } else {
2041
- pathArea = (Math.abs(comArea) + Math.abs(areaPoly)) * multiplier;
2042
- }
2043
-
2044
- totalArea += pathArea;
2045
- });
2046
-
2047
- return totalArea;
2048
- }
2049
-
2050
- /**
2051
- * get ellipse area
2052
- * skips to circle calculation if rx===ry
2053
- */
2054
-
2055
- function getEllipseArea(rx, ry, startAngle, endAngle) {
2056
- const totalArea = Math.PI * rx * ry;
2057
- let angleDiff = (endAngle - startAngle + 2 * Math.PI) % (2 * Math.PI);
2058
- // If circle, use simple circular formula
2059
- if (rx === ry) return totalArea * (angleDiff / (2 * Math.PI));
2060
-
2061
- // Convert absolute angles to parametric angles
2062
- const absoluteToParametric = (phi)=>{
2063
- return Math.atan2(rx * Math.sin(phi), ry * Math.cos(phi));
2064
- };
2065
- startAngle = absoluteToParametric(startAngle);
2066
- endAngle = absoluteToParametric(endAngle);
2067
- angleDiff = (endAngle - startAngle + 2 * Math.PI) % (2 * Math.PI);
2068
- return totalArea * (angleDiff / (2 * Math.PI));
2069
- }
2070
-
2071
- /**
2072
- * get bezier area
2073
- */
2074
- function getBezierArea(pts, absolute=false) {
2075
-
2076
- let [p0, cp1, cp2, p] = [pts[0], pts[1], pts[2], pts[pts.length - 1]];
2077
- let area;
2078
-
2079
- if (pts.length < 3) return 0;
2080
-
2081
- // quadratic beziers
2082
- if (pts.length === 3) {
2083
- cp1 = {
2084
- x: pts[0].x * 1 / 3 + pts[1].x * 2 / 3,
2085
- y: pts[0].y * 1 / 3 + pts[1].y * 2 / 3
2086
- };
2087
-
2088
- cp2 = {
2089
- x: pts[2].x * 1 / 3 + pts[1].x * 2 / 3,
2090
- y: pts[2].y * 1 / 3 + pts[1].y * 2 / 3
2091
- };
2092
- }
2093
-
2094
- area = ((p0.x * (-2 * cp1.y - cp2.y + 3 * p.y) +
2095
- cp1.x * (2 * p0.y - cp2.y - p.y) +
2096
- cp2.x * (p0.y + cp1.y - 2 * p.y) +
2097
- p.x * (-3 * p0.y + cp1.y + 2 * cp2.y)) *
2098
- 3) / 20;
2099
-
2100
- return absolute ? Math.abs(area) : area;
2101
- }
2102
-
2103
1835
  function getPolygonArea(points, absolute=false) {
2104
1836
  let area = 0;
2105
1837
  let l = points.length;
@@ -2119,7 +1851,7 @@ function getPolygonArea(points, absolute=false) {
2119
1851
  * d attribute string
2120
1852
  */
2121
1853
 
2122
- function pathDataToD(pathData, mode = 0) {
1854
+ function pathDataToD(pathData = [], mode = 0) {
2123
1855
 
2124
1856
  mode = parseFloat(mode);
2125
1857
  /*
@@ -2128,80 +1860,99 @@ function pathDataToD(pathData, mode = 0) {
2128
1860
  1 = verbose
2129
1861
  2 = beautify
2130
1862
  */
1863
+
2131
1864
  let len = pathData.length;
1865
+ let d = '';
2132
1866
 
2133
- let valsString = pathData[0].values.join(" ");
2134
- let separator_command = mode > 1 ? `\n` :
2135
- ((mode < 1) ? '' : ' ');
2136
- let separator_type = mode > 0.5 ? ' ' : '';
1867
+ // group same types
1868
+ let pathDataGrouped = mode >0.5 ? JSON.parse(JSON.stringify(pathData)) : [];
1869
+ let typePrev = 'M';
2137
1870
 
2138
- // 1st command
2139
- let d = `${pathData[0].type}${separator_type}${valsString}${separator_command}`;
1871
+ if (mode < 1) {
1872
+ pathDataGrouped = [pathData[0]];
2140
1873
 
2141
- for (let i = 1; i < len; i++) {
2142
- let com0 = pathData[i - 1];
2143
- let com = pathData[i];
2144
- let { type, values } = com;
2145
- valsString = '';
2146
-
2147
- // Minify Arc commands (A/a) – actually sucks!
2148
- if (!mode && (type === 'A' || type === 'a')) {
2149
- values = [
2150
- values[0], values[1], values[2],
2151
- `${values[3]}${values[4]}${values[5]}`,
2152
- values[6]
2153
- ];
1874
+ let idx = 0;
1875
+
1876
+ for (let i = 1; i < len; i++) {
1877
+ let com = pathData[i];
1878
+ let { type } = com;
1879
+ // decouple from object
1880
+ let values = [...com.values];
1881
+
1882
+ // new type
1883
+ if (type !== typePrev) {
1884
+ pathDataGrouped.push({type, values});
1885
+ idx++;
1886
+ } else {
1887
+ pathDataGrouped[idx].values.push(...values);
1888
+ }
1889
+
1890
+ // update type
1891
+ typePrev = type;
2154
1892
  }
1893
+ }
2155
1894
 
2156
- // Omit type for repeated commands
2157
- type = ((mode < 1) && com0.type === com.type && com.type.toLowerCase() !== 'm')
2158
- ? " "
2159
- : ((mode < 1) && com0.type === "M" && com.type === "L"
2160
- ? " "
2161
- : com.type);
1895
+ // stringify grouped
1896
+ len = pathDataGrouped.length;
1897
+ let separator_type = mode < 1 ? '' : ' ';
1898
+ let separator_command = mode < 1 ? '' : (mode === 1 ? ' ' : `\n`);
2162
1899
 
2163
- // concatenate subsequent floating point values
2164
- if (!mode) {
1900
+ typePrev = 'M';
2165
1901
 
2166
- let prevWasFloat = false;
1902
+ for (let i = 0; i < len; i++) {
1903
+ let com = pathDataGrouped[i];
1904
+ let { type, values } = com;
2167
1905
 
2168
- for (let v = 0, l = values.length; v < l; v++) {
2169
- let val = values[v];
2170
- let valStr = val.toString();
2171
- let isFloat = valStr.includes('.');
2172
- let isSmallFloat = isFloat && Math.abs(val) < 1;
1906
+ // we're always starting a path with absolute M!
1907
+ let omitType = mode < 1 && ((typePrev === 'M' && type === 'L') || (typePrev === 'm' && type === 'l'));
2173
1908
 
2174
- // Remove leading zero from small floats *only* if the previous was also a float
2175
- if (isSmallFloat && prevWasFloat) {
2176
- valStr = valStr.replace(/^0\./, '.');
2177
- }
1909
+ // add type
1910
+ if (!omitType) d += type + separator_type;
2178
1911
 
2179
- // Add space unless this is the first value OR previous was a small float
2180
- if (v > 0 && !(prevWasFloat && isSmallFloat)) {
2181
- valsString += ' ';
2182
- }
1912
+ // add values
1913
+ let wasSmallFloat = false;
1914
+ let separatorVal = ' ';
1915
+
1916
+ for (let v = 0, vlen = values.length; vlen && v < vlen; v++) {
1917
+ let val = values[v];
1918
+ let valAbs = Math.abs(val);
1919
+ let valStr = val.toString();
1920
+ let isNegative = val < 0;
1921
+ let sign = isNegative ? '-' : '';
1922
+ let isSmallFloat = mode > 0.5 ? false : (val && valAbs < 1);
1923
+ let idxSub = isSmallFloat ? (isNegative ? 2 : 1) : 0;
1924
+
1925
+ // we don't need whitespace for first value
1926
+ separatorVal = v === 0 || isNegative ? '' : ' ';
1927
+
1928
+ if (mode < 1) {
1929
+ // omit leading zero
1930
+ if (isSmallFloat) valStr = sign + valStr.substring(idxSub);
1931
+
1932
+ // omit whitespace for subsequent small floats
1933
+ separatorVal = (v === 0 && !omitType) || (wasSmallFloat && isSmallFloat) ?
1934
+ (!mode ? '' : (isNegative ? '' : ' '))
1935
+ : (isNegative ? '' : ' ');
2183
1936
 
2184
- valsString += valStr;
2185
- prevWasFloat = isSmallFloat;
2186
1937
  }
2187
1938
 
2188
- }
2189
- // regular non-minified output
2190
- else {
2191
- valsString = values.join(' ');
1939
+ // omit separator between large Arc sweep and final x in minify mode
1940
+ if (!mode && (type === 'a' || type === 'A')) {
1941
+ let pos = (v % 7);
1942
+ if (pos > 3 && pos < 6) separatorVal = '';
1943
+ }
1944
+
1945
+ d += `${separatorVal}${valStr}`;
1946
+ wasSmallFloat = isSmallFloat;
1947
+
2192
1948
  }
2193
1949
 
2194
- if(i===len-1) separator_command='';
2195
- d += `${type}${separator_type}${valsString}${separator_command}`;
2196
- }
1950
+ // add command separator
1951
+ if (mode) d += separator_command;
1952
+
1953
+ // update previous type
1954
+ typePrev = type;
2197
1955
 
2198
- if (mode < 1) {
2199
- d = d
2200
- .replace(/[A-Za-z]0(?=\.)/g, m => m[0])
2201
- .replace(/ 0\./g, " .") // Space before small decimals
2202
- .replace(/ -/g, "-") // Remove space before negatives
2203
- .replace(/-0\./g, "-.") // Remove leading zero from negative decimals
2204
- .replace(/Z/g, "z"); // Convert uppercase 'Z' to lowercase
2205
1956
  }
2206
1957
 
2207
1958
  return d;
@@ -2209,39 +1960,26 @@ function pathDataToD(pathData, mode = 0) {
2209
1960
 
2210
1961
  function getCombinedByDominant(com1, com2, maxDist = 0, tolerance = 1, debug = false) {
2211
1962
 
2212
- // cubic Bézier derivative
2213
- const cubicDerivative = (p0, p1, p2, p3, t) => {
2214
- let mt = 1 - t;
2215
-
2216
- return {
2217
- x:
2218
- 3 * mt * mt * (p1.x - p0.x) +
2219
- 6 * mt * t * (p2.x - p1.x) +
2220
- 3 * t * t * (p3.x - p2.x),
2221
- y:
2222
- 3 * mt * mt * (p1.y - p0.y) +
2223
- 6 * mt * t * (p2.y - p1.y) +
2224
- 3 * t * t * (p3.y - p2.y)
2225
- };
2226
- };
2227
-
2228
1963
  // if combining fails return original commands
2229
1964
  let commands = [com1, com2];
2230
1965
 
2231
1966
  // detect dominant
2232
- let dist1 = getDistAv(com1.p0, com1.p);
2233
- let dist2 = getDistAv(com2.p0, com2.p);
1967
+ let dist1 = getDistManhattan(com1.p0, com1.p);
1968
+ let dist2 = getDistManhattan(com2.p0, com2.p);
1969
+ let thresh = (dist1 + dist2) * 0.5 * 0.075 * tolerance;
2234
1970
 
2235
- let reverse = dist1 > dist2;
1971
+ // take longer command
1972
+ let reverse = dist1 < dist2;
2236
1973
 
2237
1974
  // backup original commands
2238
1975
  let com1_o = JSON.parse(JSON.stringify(com1));
2239
1976
  let com2_o = JSON.parse(JSON.stringify(com2));
2240
1977
 
2241
- let ptI = checkLineIntersection(com1_o.p0, com1_o.cp1, com2_o.p, com2_o.cp2, false);
1978
+ // intersection of control tangents
1979
+ let ptI = checkLineIntersection(com1_o.p0, com1_o.cp1, com2_o.p, com2_o.cp2, false, true);
2242
1980
 
1981
+ // no intersection - we can't combine
2243
1982
  if (!ptI) {
2244
-
2245
1983
  return commands
2246
1984
  }
2247
1985
 
@@ -2264,153 +2002,70 @@ function getCombinedByDominant(com1, com2, maxDist = 0, tolerance = 1, debug = f
2264
2002
  com2 = com2_R;
2265
2003
  }
2266
2004
 
2267
- let add = (a, b) => ({ x: a.x + b.x, y: a.y + b.y });
2268
- let sub = (a, b) => ({ x: a.x - b.x, y: a.y - b.y });
2269
- let mul = (a, s) => ({ x: a.x * s, y: a.y * s });
2270
- let dot = (a, b) => a.x * b.x + a.y * b.y;
2005
+ // etsimate t for extrapolation
2006
+ let PtI = checkLineIntersection(com1.cp2, com1.p, com2.p, com2.cp2, false, true);
2007
+ let cp1_I = interpolate(com1.p, PtI, 0.666);
2271
2008
 
2272
- // estimate extrapolation parameter t0
2009
+ let dist1_2 = getDistManhattan(com1.cp2, com1.p);
2010
+ let dist2_2 = getDistManhattan(com1.cp2, cp1_I);
2011
+ let t = dist2_2 / dist1_2;
2273
2012
 
2274
- let B0 = com2.p0;
2275
- let D0 = cubicDerivative(
2276
- com2.p0,
2277
- com2.cp1,
2278
- com2.cp2,
2279
- com2.p,
2280
- 0
2281
- );
2013
+ // extrapolate
2014
+ let segs = pointAtT([com1.p0, com1.cp1, com1.cp2, com1.p], t, false, true).segments;
2282
2015
 
2283
- let v = sub(com1.p0, B0);
2016
+ let seg = segs[0];
2284
2017
 
2285
- // first-order projection onto tangent
2286
- let t0 = dot(v, D0) / dot(D0, D0);
2018
+ // if it worked - points should be nearby
2019
+ let dist = getDistManhattan(seg.p, com2.p);
2287
2020
 
2288
- // refine with one Newton iteration (optional but cheap)
2289
- let P = pointAtT([com2.p0, com2.cp1, com2.cp2, com2.p], t0);
2290
- let dP = cubicDerivative(com2.p0, com2.cp1, com2.cp2, com2.p, t0);
2291
- let r = sub(P, com1.p0);
2021
+ // if close enough adjust
2022
+ if (dist < thresh) {
2023
+ let angle = getAngle(seg.p, seg.cp2);
2024
+ let angle2 = getAngle(com2.p, com2.cp2);
2292
2025
 
2293
- t0 -= dot(r, dP) / dot(dP, dP);
2026
+ let angleDiff = (angle2 - angle);
2294
2027
 
2295
- // construct merged cubic over [t0, 1]
2296
- let Q0 = pointAtT([com2.p0, com2.cp1, com2.cp2, com2.p], t0);
2297
- let Q3 = com2.p;
2028
+ // adjust cp angle
2029
+ seg.cp2 = rotatePoint(seg.cp2, seg.p.x, seg.p.y, angleDiff);
2030
+ let dist1 = getDistManhattan(seg.p, seg.cp2);
2298
2031
 
2299
- let d0 = cubicDerivative(com2.p0, com2.cp1, com2.cp2, com2.p, t0);
2300
- let d1 = cubicDerivative(com2.p0, com2.cp1, com2.cp2, com2.p, 1);
2032
+ // copy original final point coordinates
2033
+ seg.p = com2.p;
2301
2034
 
2302
- let scale = 1 - t0;
2035
+ // after rotation
2036
+ let dist2 = getDistManhattan(seg.p, seg.cp2);
2037
+ let scale = dist2 / dist1;
2303
2038
 
2304
- let Q1 = add(Q0, mul(d0, scale / 3));
2305
- let Q2 = sub(Q3, mul(d1, scale / 3));
2039
+ // adjust tangent length
2040
+ seg.cp2 = interpolate(seg.p, seg.cp2, scale);
2306
2041
 
2307
- let result = {
2308
- p0: Q0,
2309
- cp1: Q1,
2310
- cp2: Q2,
2311
- p: Q3,
2312
- t0
2313
- };
2314
-
2315
- if (reverse) {
2316
- result = {
2317
- p0: Q3,
2318
- cp1: Q2,
2319
- cp2: Q1,
2320
- p: Q0,
2321
- t0
2322
- };
2323
- }
2324
-
2325
- let tMid = (1 - t0) * 0.5;
2326
-
2327
- let ptM = pointAtT([result.p0, result.cp1, result.cp2, result.p], tMid, false, true);
2328
- let seg1_cp2 = ptM.cpts[2];
2329
-
2330
- let ptI_1 = checkLineIntersection(ptM, seg1_cp2, result.p0, ptI, false);
2331
- let ptI_2 = checkLineIntersection(ptM, seg1_cp2, result.p, ptI, false);
2332
-
2333
- let cp1_2 = interpolate(result.p0, ptI_1, 1.333 );
2334
- let cp2_2 = interpolate(result.p, ptI_2, 1.333 );
2335
-
2336
- // test self intersections and exit
2337
- let cp_intersection = checkLineIntersection(com1_o.p0, cp1_2, com2_o.p, cp2_2, true);
2338
- if (cp_intersection) {
2339
-
2340
- return commands;
2341
- }
2342
-
2343
- result.cp1 = cp1_2;
2344
- result.cp2 = cp2_2;
2345
-
2346
- // check distances between original starting point and extrapolated
2347
- let dist3 = getDistAv(com1_o.p0, result.p0);
2348
- let dist4 = getDistAv(com2_o.p, result.p);
2349
- let dist5 = (dist3 + dist4);
2350
-
2351
- // use original points
2352
- result.p0 = com1_o.p0;
2353
- result.p = com2_o.p;
2354
- result.extreme = com2_o.extreme;
2355
- result.corner = com2_o.corner;
2356
- result.dimA = com2_o.dimA;
2357
- result.directionChange = com2_o.directionChange;
2358
- result.type = 'C';
2359
- result.values = [result.cp1.x, result.cp1.y, result.cp2.x, result.cp2.y, result.p.x, result.p.y];
2360
-
2361
- // extrapolated starting point is not completely off
2362
- if (dist5 < maxDist) {
2363
-
2364
- /*
2365
- let tTotal = 1 + Math.abs(t0);
2366
- let tSplit = reverse ? 1 + t0 : Math.abs(t0);
2367
-
2368
- let pO = pointAtT([com2_o.p0, com2_o.cp1, com2_o.cp2, com2_o.p], t0);
2369
- */
2370
-
2371
- // split t to meet original mid segment start point
2372
- let tSplit = reverse ? 1 + t0 : Math.abs(t0);
2373
-
2374
- let tTotal = 1 + Math.abs(t0);
2375
- tSplit = reverse ? 1 + t0 : Math.abs(t0) / tTotal;
2376
-
2377
- let ptSplit = pointAtT([result.p0, result.cp1, result.cp2, result.p], tSplit);
2378
- let distSplit = getDistAv(ptSplit, com1.p);
2379
-
2380
- // not close enough - exit
2381
- if (distSplit > maxDist * tolerance ) {
2382
-
2383
- return commands;
2042
+ // reverse back
2043
+ if (reverse) {
2044
+ seg = {
2045
+ p0: seg.p,
2046
+ p: seg.p0,
2047
+ cp1: seg.cp2,
2048
+ cp2: seg.cp1,
2049
+ };
2384
2050
  }
2385
2051
 
2386
- // compare combined with original area
2387
- let pathData0 = [
2388
- { type: 'M', values: [com1_o.p0.x, com1_o.p0.y] },
2389
- { type: 'C', values: [com1_o.cp1.x, com1_o.cp1.y, com1_o.cp2.x, com1_o.cp2.y, com1_o.p.x, com1_o.p.y] },
2390
- { type: 'C', values: [com2_o.cp1.x, com2_o.cp1.y, com2_o.cp2.x, com2_o.cp2.y, com2_o.p.x, com2_o.p.y] },
2391
- ];
2052
+ commands = [
2053
+ {
2054
+ type: 'C',
2055
+ values: [seg.cp1.x, seg.cp1.y, seg.cp2.x, seg.cp2.y, seg.p.x, seg.p.y],
2056
+ p0: seg.p0,
2057
+ cp1: seg.cp1,
2058
+ cp2: seg.cp2,
2059
+ p: seg.p,
2060
+ extreme: com2_o.extreme,
2061
+ corner: com2_o.corner,
2062
+ directionChange: com2_o.directionChange,
2063
+ dimA: getDistManhattan(seg.p0, seg.p),
2064
+ error: dist
2392
2065
 
2393
- let area0 = getPathArea(pathData0);
2394
- let pathDataN = [
2395
- { type: 'M', values: [result.p0.x, result.p0.y] },
2396
- { type: 'C', values: [result.cp1.x, result.cp1.y, result.cp2.x, result.cp2.y, result.p.x, result.p.y] },
2066
+ }
2397
2067
  ];
2398
2068
 
2399
- let areaN = getPathArea(pathDataN);
2400
- let areaDiff = Math.abs(areaN / area0 - 1);
2401
-
2402
- result.error = areaDiff * 5 * tolerance;
2403
-
2404
- if (debug) {
2405
- pathDataToD(pathDataN);
2406
-
2407
- }
2408
-
2409
- // success!!!
2410
- if (areaDiff < 0.05 * tolerance) {
2411
- commands = [result];
2412
-
2413
- }
2414
2069
  }
2415
2070
 
2416
2071
  return commands
@@ -2462,7 +2117,7 @@ function simplifyPathDataCubic(pathData, {
2462
2117
  error += com.error;
2463
2118
 
2464
2119
  // find next candidates
2465
- for (let n = i + 1; error < tolerance && n < l; n++) {
2120
+ for (let n = i + offset; error < tolerance && n < l; n++) {
2466
2121
  let comN = pathData[n];
2467
2122
 
2468
2123
  if (comN.type !== 'C' ||
@@ -2472,6 +2127,7 @@ function simplifyPathDataCubic(pathData, {
2472
2127
  (keepExtremes && com.extreme)
2473
2128
  )
2474
2129
  ) {
2130
+
2475
2131
  break
2476
2132
  }
2477
2133
 
@@ -2479,6 +2135,7 @@ function simplifyPathDataCubic(pathData, {
2479
2135
 
2480
2136
  // failure - could not be combined - exit loop
2481
2137
  if (combined.length > 1) {
2138
+
2482
2139
  break
2483
2140
  }
2484
2141
 
@@ -2492,6 +2149,7 @@ function simplifyPathDataCubic(pathData, {
2492
2149
 
2493
2150
  // return combined
2494
2151
  com = combined[0];
2152
+
2495
2153
  }
2496
2154
 
2497
2155
  pathDataN.push(com);
@@ -2530,11 +2188,12 @@ function combineCubicPairs(com1, com2, {
2530
2188
  // quit if t is start
2531
2189
  if (!t) return commands;
2532
2190
 
2191
+ // get averaged threshold
2533
2192
  let distAv1 = getDistManhattan(com1.p0, com1.p);
2534
2193
  let distAv2 = getDistManhattan(com2.p0, com2.p);
2535
2194
  let distMin = Math.max(0, Math.min(distAv1, distAv2));
2536
2195
 
2537
- let distScale = 0.08;
2196
+ let distScale = 0.075;
2538
2197
  let maxDist = distMin * distScale * tolerance;
2539
2198
 
2540
2199
  // get hypothetical combined command
@@ -2582,16 +2241,6 @@ function combineCubicPairs(com1, com2, {
2582
2241
 
2583
2242
  if (error < maxDist) success = true;
2584
2243
 
2585
- /*
2586
- renderPoint(markers, ptM_seg1, 'cyan')
2587
- renderPoint(markers, pt, 'orange', '1.5%', '1')
2588
- renderPoint(markers, ptM_seg2, 'orange')
2589
-
2590
- renderPoint(markers, com1.p, 'green')
2591
-
2592
- renderPoint(markers, ptI_seg1, 'purple')
2593
- */
2594
-
2595
2244
  }
2596
2245
 
2597
2246
  } // end 1st try
@@ -2605,9 +2254,9 @@ function combineCubicPairs(com1, com2, {
2605
2254
 
2606
2255
  comS.dimA = getDistManhattan(comS.p0, comS.p);
2607
2256
  comS.type = 'C';
2257
+
2608
2258
  comS.extreme = com2.extreme;
2609
2259
  comS.directionChange = com2.directionChange;
2610
-
2611
2260
  comS.corner = com2.corner;
2612
2261
 
2613
2262
  comS.values = [comS.cp1.x, comS.cp1.y, comS.cp2.x, comS.cp2.y, comS.p.x, comS.p.y];
@@ -2668,7 +2317,9 @@ function findSplitT(com1, com2) {
2668
2317
  }
2669
2318
  */
2670
2319
 
2671
- return l1 / l3
2320
+ let t = l1 / l3;
2321
+
2322
+ return t;
2672
2323
  }
2673
2324
 
2674
2325
  function commandIsFlat(points, {
@@ -2721,7 +2372,7 @@ function analyzePathData(pathData = [], {
2721
2372
  detectDirection = true,
2722
2373
  detectSemiExtremes = false,
2723
2374
  debug = false,
2724
- addSquareLength = true,
2375
+ addSquareLength = false,
2725
2376
  addArea = true,
2726
2377
 
2727
2378
  } = {}) {
@@ -2770,8 +2421,8 @@ function analyzePathData(pathData = [], {
2770
2421
  let commandPts = (type === 'C' || type === 'Q') ?
2771
2422
  (type === 'C' ? [p0, cp1, cp2, p] : [p0, cp1, p]) :
2772
2423
  ([p0, p]);
2773
- let thresholdLength = dimA * 0.1;
2774
- let threshold = thresholdLength * 0.01;
2424
+
2425
+ let threshold = dimA * 0.005;
2775
2426
 
2776
2427
  // bezier types
2777
2428
  let isBezier = type === 'Q' || type === 'C';
@@ -2784,7 +2435,7 @@ function analyzePathData(pathData = [], {
2784
2435
  */
2785
2436
  let hasExtremes = false;
2786
2437
 
2787
- if (isBezier) {
2438
+ if (detectExtremes && isBezier) {
2788
2439
 
2789
2440
  let dx = type === 'C' ? Math.abs(com.cp2.x - com.p.x) : Math.abs(com.cp1.x - com.p.x);
2790
2441
  let dy = type === 'C' ? Math.abs(com.cp2.y - com.p.y) : Math.abs(com.cp1.y - com.p.y);
@@ -2823,7 +2474,7 @@ function analyzePathData(pathData = [], {
2823
2474
  }
2824
2475
 
2825
2476
  // check extremes introduce by small arcs
2826
- else if(isArc && comN && ((comPrev.type==='C' || comPrev.type==='Q') || (comN.type==='C' || comN.type==='Q')) ){
2477
+ else if(detectExtremes && isArc && comN && ((comPrev.type==='C' || comPrev.type==='Q') || (comN.type==='C' || comN.type==='Q')) ){
2827
2478
  let distN = comN ? comN.dimA : 0;
2828
2479
  let isShort = com.dimA < (comPrev.dimA + distN) * 0.1;
2829
2480
  let smallRadius = com.values[0] === com.values[1] && (com.values[0] < 1);
@@ -2841,28 +2492,7 @@ function analyzePathData(pathData = [], {
2841
2492
  if (hasExtremes) com.extreme = true;
2842
2493
 
2843
2494
  // Corners and semi extremes
2844
- if (isBezier && isBezierN) {
2845
-
2846
- // semi extremes
2847
- if (detectSemiExtremes && !com.extreme) {
2848
-
2849
- let dx1 = Math.abs(p.x - cp2.x);
2850
- let dy1 = Math.abs(p.y - cp2.y);
2851
- let hasSemiExtreme = false;
2852
-
2853
- // exclude extremes or small deltas
2854
- if (dx1 && dy1 && dx1 > thresholdLength || dy1 > thresholdLength) {
2855
- let ang1 = getAngle(cp2, p);
2856
- let ang2 = getAngle(p, comN.cp1);
2857
-
2858
- let ang3 = Math.abs(ang1 + ang2) / 2;
2859
- hasSemiExtreme = isMultipleOf45(ang3);
2860
- }
2861
-
2862
- if (hasSemiExtreme) {
2863
- com.semiExtreme = true;
2864
- }
2865
- }
2495
+ if (detectCorners && isBezier && isBezierN) {
2866
2496
 
2867
2497
  /**
2868
2498
  * Detect direction change points
@@ -3438,7 +3068,12 @@ function convertPathData(pathData, {
3438
3068
  if (hasShorthands && toLonghands) pathData = pathDataToLonghands(pathData);
3439
3069
 
3440
3070
  // minify semicircle radii
3441
- if (optimizeArcs) pathData = optimizeArcPathData(pathData);
3071
+ if (optimizeArcs) {
3072
+ pathData = optimizeArcPathData(pathData);
3073
+ } else {
3074
+ // get true absolute radii
3075
+ pathData = pathDataToTrueArcValues(pathData);
3076
+ }
3442
3077
 
3443
3078
  if (toShorthands) pathData = pathDataToShorthands(pathData);
3444
3079
 
@@ -3528,9 +3163,42 @@ function parsePathDataNormalized(d,
3528
3163
  }
3529
3164
 
3530
3165
  /**
3531
- *
3532
- * @param {*} pathData
3533
- * @returns
3166
+ * Converts minified arc
3167
+ * values to true rx and ry values
3168
+ */
3169
+ function pathDataToTrueArcValues(pathData = []) {
3170
+ let l = pathData.length;
3171
+ let pathDataN = [pathData[0]];
3172
+
3173
+ for (let i = 1; i < l; i++) {
3174
+ let com = pathData[i];
3175
+ let comPrev = pathData[i-1];
3176
+ let { type, values } = com;
3177
+
3178
+ if (type === 'A') {
3179
+
3180
+ // previous commands final on-path point
3181
+ let [x1, y1] = comPrev.values.slice(-2);
3182
+ let [rx, ry, xAxisRotation, largeArc, sweep, x2, y2] = values;
3183
+ let arcData = svgArcToCenterParam(x1, y1, rx, ry, xAxisRotation, largeArc, sweep, x2, y2);
3184
+
3185
+ // set true arc values
3186
+ com.values[0] = arcData.rx;
3187
+ com.values[1] = arcData.ry;
3188
+
3189
+ }
3190
+
3191
+ pathDataN.push(com);
3192
+
3193
+ }
3194
+ return pathDataN;
3195
+ }
3196
+
3197
+ /**
3198
+ * Minify arc radii
3199
+ * for semi circles
3200
+ * returns smaller rx and ry
3201
+ * values
3534
3202
  */
3535
3203
 
3536
3204
  function optimizeArcPathData(pathData = []) {
@@ -3587,7 +3255,7 @@ function optimizeArcPathData(pathData = []) {
3587
3255
  if (isHorizontal || isVertical) {
3588
3256
 
3589
3257
  // check if semi circle
3590
- let needsTrueR = isHorizontal ? rx*1.9 > diffX : ry*1.9 > diffY;
3258
+ let needsTrueR = isHorizontal ? rx * 1.9 > diffX : ry * 1.9 > diffY;
3591
3259
 
3592
3260
  // is semicircle we can simplify rx
3593
3261
  if (!needsTrueR) {
@@ -3692,7 +3360,7 @@ function pathDataArcsToCubics(pathData, {
3692
3360
 
3693
3361
  if (com.type === 'A') {
3694
3362
  // add all C commands instead of Arc
3695
- let cubicArcs = arcToBezier$1(p0, com.values, arcAccuracy);
3363
+ let cubicArcs = arcToBezier(p0, com.values, arcAccuracy);
3696
3364
  cubicArcs.forEach((cubicArc) => {
3697
3365
  pathDataCubic.push(cubicArc);
3698
3366
  });
@@ -4193,7 +3861,7 @@ function arcToBezierResolved({
4193
3861
  * returns pathData array
4194
3862
  */
4195
3863
 
4196
- function arcToBezier$1(p0, values, splitSegments = 1) {
3864
+ function arcToBezier(p0, values, splitSegments = 1) {
4197
3865
  const TAU = Math.PI * 2;
4198
3866
  let [rx, ry, rotation, largeArcFlag, sweepFlag, x, y] = values;
4199
3867
 
@@ -4204,8 +3872,8 @@ function arcToBezier$1(p0, values, splitSegments = 1) {
4204
3872
  let phi = rotation ? rotation * TAU / 360 : 0;
4205
3873
  let sinphi = phi ? Math.sin(phi) : 0;
4206
3874
  let cosphi = phi ? Math.cos(phi) : 1;
4207
- let pxp = cosphi * (p0.x - x) / 2 + sinphi * (p0.y - y) / 2;
4208
- let pyp = -sinphi * (p0.x - x) / 2 + cosphi * (p0.y - y) / 2;
3875
+ let pxp = cosphi * (p0.x - x) *0.5 + sinphi * (p0.y - y) *0.5;
3876
+ let pyp = -sinphi * (p0.x - x) *0.5 + cosphi * (p0.y - y) *0.5;
4209
3877
 
4210
3878
  if (pxp === 0 && pyp === 0) {
4211
3879
  return []
@@ -4241,8 +3909,8 @@ function arcToBezier$1(p0, values, splitSegments = 1) {
4241
3909
 
4242
3910
  let centerxp = radicant ? radicant * rx / ry * pyp : 0;
4243
3911
  let centeryp = radicant ? radicant * -ry / rx * pxp : 0;
4244
- let centerx = cosphi * centerxp - sinphi * centeryp + (p0.x + x) / 2;
4245
- let centery = sinphi * centerxp + cosphi * centeryp + (p0.y + y) / 2;
3912
+ let centerx = cosphi * centerxp - sinphi * centeryp + (p0.x + x) * 0.5;
3913
+ let centery = sinphi * centerxp + cosphi * centeryp + (p0.y + y) * 0.5;
4246
3914
 
4247
3915
  let vx1 = (pxp - centerxp) / rx;
4248
3916
  let vy1 = (pyp - centeryp) / ry;
@@ -4264,15 +3932,17 @@ function arcToBezier$1(p0, values, splitSegments = 1) {
4264
3932
  ang2 = vectorAngle(vx1, vy1, vx2, vy2);
4265
3933
 
4266
3934
  if (sweepFlag === 0 && ang2 > 0) {
4267
- ang2 -= Math.PI * 2;
3935
+
3936
+ ang2 -= TAU;
4268
3937
  }
4269
3938
  else if (sweepFlag === 1 && ang2 < 0) {
4270
- ang2 += Math.PI * 2;
3939
+
3940
+ ang2 += TAU;
4271
3941
  }
4272
3942
 
4273
- let ratio = +(Math.abs(ang2) / (TAU / 4)).toFixed(0) || 1;
3943
+ let ratio = +(Math.abs(ang2) / (TAU * 0.25)).toFixed(0) || 1;
4274
3944
 
4275
- // increase segments for more accureate length calculations
3945
+ // increase segments for more accurate length calculations
4276
3946
  let segments = ratio * splitSegments;
4277
3947
  ang2 /= segments;
4278
3948
  let pathDataArc = [];
@@ -4284,7 +3954,8 @@ function arcToBezier$1(p0, values, splitSegments = 1) {
4284
3954
  const k = 0.551785;
4285
3955
  let a = ang2 === angle90 ? k :
4286
3956
  (
4287
- ang2 === -angle90 ? -k : 4 / 3 * Math.tan(ang2 / 4)
3957
+
3958
+ ang2 === -angle90 ? -k : 1.33333 * Math.tan(ang2 * 0.25)
4288
3959
  );
4289
3960
 
4290
3961
  let cos2 = ang2 ? Math.cos(ang2) : 1;
@@ -4334,59 +4005,50 @@ function pathDataRemoveColinear(pathData, {
4334
4005
  pathData[pathData.length - 1].type.toLowerCase() === 'z';
4335
4006
 
4336
4007
  for (let c = 1, l = pathData.length; c < l; c++) {
4337
-
4338
4008
  let com = pathData[c];
4009
+ let { type, values } = com;
4339
4010
  let comN = pathData[c + 1] || pathData[l - 1];
4340
-
4011
+ let valuesN = comN.values;
4341
4012
  let p1 = comN.type.toLowerCase() === 'z' ? M : { x: comN.values[comN.values.length - 2], y: comN.values[comN.values.length - 1] };
4342
4013
 
4343
- let { type, values } = com;
4344
4014
  let valsL = values.slice(-2);
4345
4015
  p = type !== 'Z' ? { x: valsL[0], y: valsL[1] } : M;
4346
4016
 
4017
+ let nextBezier = type!==comN.type && (comN.type==='C' || comN.type==='Q');
4018
+
4347
4019
  let area = p1 ? getPolygonArea([p0, p, p1], true) : Infinity;
4348
4020
  let distSquare = getSquareDistance(p0, p1);
4349
4021
  let distMax = distSquare ? distSquare / 333 * tolerance : 0;
4350
4022
 
4351
4023
  let isFlat = area < distMax;
4352
-
4353
4024
  let isFlatBez = false;
4025
+ let cpts = [];
4354
4026
 
4355
- /*
4356
- // flatness by cross product
4357
- let dx0 = Math.abs(p1.x - p0.x)
4358
- let dy0 = Math.abs(p1.y - p0.y)
4359
-
4360
- let dx1 = Math.abs(p.x - p0.x)
4361
- let dy1 = Math.abs(p.y - p0.y)
4362
-
4363
- let dx2 = Math.abs(p1.x - p.x)
4364
- let dy2 = Math.abs(p1.y - p.y)
4365
-
4366
- // zero length segments are flat
4367
- let isZeroLength = (!dy1 && !dx1) || (!dy2 && !dx2)
4368
- if (isZeroLength) isFlat = true;
4369
-
4370
- // check cross products for colinearity
4371
- if (!isFlat) {
4372
-
4373
- let cross0 = Math.abs(dx0 * dy1 - dy0 * dx1);
4027
+ /**
4028
+ * type change
4029
+ * check flatness
4030
+ */
4031
+ if (nextBezier) {
4032
+ cpts = comN.type === 'C' ?
4033
+ [{ x: valuesN[0], y: valuesN[1] }, { x: valuesN[2], y: valuesN[3] }] :
4034
+ (comN.type === 'Q' ? [{ x: valuesN[0], y: valuesN[1] }] : []);
4374
4035
 
4375
- let thresh = (dx0 + dy0) * 0.1
4036
+ isFlat = commandIsFlat([p0, ...cpts, p], { tolerance });
4376
4037
 
4377
- if ( cross0 < thresh) {
4038
+ /*
4378
4039
 
4379
- isFlat = true
4040
+ if(!isFlatBez){
4041
+ pathDataN.push(com)
4042
+ continue
4380
4043
  }
4381
- }
4382
- */
4044
+ */
4383
4045
 
4384
- if (!flatBezierToLinetos && type === 'C') isFlat = false;
4046
+ }
4385
4047
 
4386
4048
  // convert flat beziers to linetos
4387
4049
  if (flatBezierToLinetos && (type === 'C' || type === 'Q')) {
4388
4050
 
4389
- let cpts = type === 'C' ?
4051
+ cpts = type === 'C' ?
4390
4052
  [{ x: values[0], y: values[1] }, { x: values[2], y: values[3] }] :
4391
4053
  (type === 'Q' ? [{ x: values[0], y: values[1] }] : []);
4392
4054
 
@@ -4400,10 +4062,13 @@ function pathDataRemoveColinear(pathData, {
4400
4062
  }
4401
4063
  }
4402
4064
 
4403
- // colinear – exclude arcs (as always =) as semicircles won't have an area
4065
+ /**
4066
+ * colinear = simplification success
4067
+ * exclude arcs (as always =)
4068
+ * as semicircles won't have an area
4069
+ */
4404
4070
 
4405
4071
  if (isFlat && c < l - 1 && comN.type !== 'A' && (type === 'L' || (flatBezierToLinetos && isFlatBez))) {
4406
-
4407
4072
  continue;
4408
4073
  }
4409
4074
 
@@ -4519,7 +4184,8 @@ function pathDataToTopLeft(pathData) {
4519
4184
  let { type, values } = com;
4520
4185
  let valsLen = values.length;
4521
4186
  if (valsLen) {
4522
- let p = { type: type, x: values[valsLen - 2], y: values[valsLen - 1], index: 0 };
4187
+ // we need rounding otherwise sorting may crash due to e notation
4188
+ let p = { type: type, x: +values[valsLen - 2].toFixed(8), y: +values[valsLen - 1].toFixed(8), index: 0 };
4523
4189
  p.index = i;
4524
4190
  indices.push(p);
4525
4191
  }
@@ -4527,7 +4193,7 @@ function pathDataToTopLeft(pathData) {
4527
4193
 
4528
4194
  // reorder to top left most
4529
4195
 
4530
- indices = indices.sort((a, b) => +a.y.toFixed(8) - +b.y.toFixed(8) || a.x - b.x);
4196
+ indices = indices.sort((a, b) => a.y - b.y || a.x - b.x);
4531
4197
  newIndex = indices[0].index;
4532
4198
 
4533
4199
  return newIndex ? shiftSvgStartingPoint(pathData, newIndex) : pathData;
@@ -4767,9 +4433,7 @@ function refineAdjacentExtremes(pathData, {
4767
4433
  if (comEx.length === 1) {
4768
4434
 
4769
4435
  comEx = comEx[0];
4770
-
4771
4436
  pathData[i + 1] = null;
4772
-
4773
4437
  pathData[i + 2].values = [comEx.cp1.x, comEx.cp1.y, comEx.cp2.x, comEx.cp2.y, comEx.p.x, comEx.p.y];
4774
4438
  pathData[i + 2].cp1 = comEx.cp1;
4775
4439
  pathData[i + 2].cp2 = comEx.cp2;