svg-path-simplify 0.4.5 → 0.4.7

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 (42) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/README.md +24 -14
  3. package/dist/svg-path-simplify.esm.js +1620 -1216
  4. package/dist/svg-path-simplify.esm.min.js +2 -2
  5. package/dist/svg-path-simplify.js +1620 -1215
  6. package/dist/svg-path-simplify.min.js +2 -2
  7. package/dist/svg-path-simplify.pathdata.esm.js +298 -665
  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 +11 -5
  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 +14 -34
  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 +259 -60
  20. package/src/pathSimplify-presets.js +1 -0
  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/svgii/geometry.js +130 -27
  26. package/src/svgii/geometry_bbox.js +1 -1
  27. package/src/svgii/pathData_analyze.js +6 -28
  28. package/src/svgii/pathData_convert.js +70 -15
  29. package/src/svgii/pathData_remove_collinear.js +32 -36
  30. package/src/svgii/pathData_simplify_refineExtremes.js +1 -3
  31. package/src/svgii/pathData_stringify.js +132 -6
  32. package/src/svgii/pathData_toPolygon.js +33 -23
  33. package/src/svgii/poly_analyze.js +272 -125
  34. package/src/svgii/poly_analyze_cleanup.js +311 -0
  35. package/src/svgii/poly_analyze_getTangents.js +125 -0
  36. package/src/svgii/poly_analyze_get_chunks.js +3 -1
  37. package/src/svgii/poly_normalize.js +12 -0
  38. package/src/svgii/poly_to_pathdata.js +477 -8
  39. package/src/svgii/rounding.js +29 -39
  40. package/src/svgii/svg_cleanup.js +42 -54
  41. package/src/svgii/svg_cleanup_normalize_transforms.js +1 -1
  42. package/src/svgii/visualize.js +33 -2
@@ -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,52 +1678,33 @@ 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.toFixed(8));
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
- dims = dims.sort();
1699
- let len = dims.length;
1700
- let dim_mid = dims[Math.floor(len*0.5)];
1701
-
1702
- // smallest 25% of values
1703
- let idx_q = Math.ceil(len*0.25);
1704
- let dims_min = dims.slice(0, idx_q);
1705
-
1706
- // average smallest values with mid value
1707
- let dim_min = ((dims_min.reduce((a, b) => a + b, 0) / idx_q) + dim_mid) * 0.5;
1708
-
1709
- let threshold = 75;
1710
- let decimalsAuto = dim_min > threshold * 1.5 ? 0 : Math.floor(threshold / dim_min).toString().length;
1711
-
1712
- // clamp
1713
- return Math.min(Math.max(0, decimalsAuto), 8)
1714
-
1715
- /*
1716
- let dim_min = dims.sort()
1717
-
1718
- let dim_mid = dim_min[Math.floor(dim_min.length*0.5)]
1692
+ dims = dims.sort((a,b)=>a-b);
1693
+ let len = dims.length;
1694
+ let dim_mid = dims[Math.floor(len * 0.5)];
1719
1695
 
1720
- let sliceIdx = Math.ceil(dim_min.length / 4);
1721
- dim_min = dim_min.slice(0, sliceIdx);
1722
- 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);
1723
1699
 
1724
- // average with mid value
1725
- minVal = (minVal+dim_mid)*0.5
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;
1726
1702
 
1727
- let threshold = 75
1728
- let decimalsAuto = minVal > threshold * 1.5 ? 0 : Math.floor(threshold / minVal).toString().length
1703
+ let threshold = 75;
1704
+ let decimalsAuto = dim_min > threshold * 1.5 ? 0 : Math.floor(threshold / dim_min).toString().length;
1729
1705
 
1730
1706
  // clamp
1731
1707
  return Math.min(Math.max(0, decimalsAuto), 8)
1732
- */
1733
1708
 
1734
1709
  }
1735
1710
 
@@ -1790,117 +1765,6 @@ function getPolyBBox(vertices, decimals = -1) {
1790
1765
  return bb;
1791
1766
  }
1792
1767
 
1793
- function getSubPathBBoxes(subPaths) {
1794
- let bboxArr = [];
1795
- subPaths.forEach((pathData) => {
1796
-
1797
- let bb = getPathDataBBox_sloppy(pathData);
1798
- bboxArr.push(bb);
1799
- });
1800
-
1801
- return bboxArr;
1802
- }
1803
-
1804
- function checkBBoxIntersections(bb, bb1) {
1805
- let [x, y, width, height, right, bottom] = [
1806
- bb.x,
1807
- bb.y,
1808
- bb.width,
1809
- bb.height,
1810
- bb.x + bb.width,
1811
- bb.y + bb.height
1812
- ];
1813
- let [x1, y1, width1, height1, right1, bottom1] = [
1814
- bb1.x,
1815
- bb1.y,
1816
- bb1.width,
1817
- bb1.height,
1818
- bb1.x + bb1.width,
1819
- bb1.y + bb1.height
1820
- ];
1821
- let intersects = false;
1822
- if (width * height != width1 * height1) {
1823
- if (width * height > width1 * height1) {
1824
- if (x < x1 && right > right1 && y < y1 && bottom > bottom1) {
1825
- intersects = true;
1826
- }
1827
- }
1828
- }
1829
- return intersects;
1830
- }
1831
-
1832
- /**
1833
- * sloppy path bbox aaproximation
1834
- */
1835
-
1836
- function getPathDataBBox_sloppy(pathData) {
1837
- let pts = getPathDataPoly(pathData);
1838
- let bb = getPolyBBox(pts);
1839
- return bb;
1840
- }
1841
-
1842
- /**
1843
- * get path data poly
1844
- * including command points
1845
- * handy for faster/sloppy bbox approximations
1846
- */
1847
-
1848
- function getPathDataPoly(pathData) {
1849
-
1850
- let poly = [];
1851
- for (let i = 0; i < pathData.length; i++) {
1852
- let com = pathData[i];
1853
- let prev = i > 0 ? pathData[i - 1] : pathData[i];
1854
- let { type, values } = com;
1855
- let p0 = { x: prev.values[prev.values.length - 2], y: prev.values[prev.values.length - 1] };
1856
- let p = values.length ? { x: values[values.length - 2], y: values[values.length - 1] } : '';
1857
- let cp1 = values.length ? { x: values[0], y: values[1] } : '';
1858
-
1859
- switch (type) {
1860
-
1861
- // convert to cubic to get polygon
1862
- case 'A':
1863
-
1864
- if (typeof arcToBezier !== 'function') {
1865
-
1866
- // get real radii
1867
- let rx = getDistance(p0, p) / 2;
1868
- let ptMid = interpolate(p0, p, 0.5);
1869
-
1870
- let pt1 = getPointOnEllipse(ptMid.x, ptMid.y, rx, rx, 0);
1871
- let pt2 = getPointOnEllipse(ptMid.x, ptMid.y, rx, rx, Math.PI);
1872
- poly.push(pt1, pt2, p);
1873
-
1874
- break;
1875
- }
1876
- let cubic = arcToBezier(p0, values);
1877
- cubic.forEach(com => {
1878
- let vals = com.values;
1879
- let cp1 = { x: vals[0], y: vals[1] };
1880
- let cp2 = { x: vals[2], y: vals[3] };
1881
- let p = { x: vals[4], y: vals[5] };
1882
- poly.push(cp1, cp2, p);
1883
- });
1884
- break;
1885
-
1886
- case 'C':
1887
- let cp2 = { x: values[2], y: values[3] };
1888
- poly.push(cp1, cp2);
1889
- break;
1890
- case 'Q':
1891
- poly.push(cp1);
1892
- break;
1893
- }
1894
-
1895
- // M and L commands
1896
- if (type.toLowerCase() !== 'z') {
1897
- poly.push(p);
1898
- }
1899
- }
1900
-
1901
- return poly;
1902
- }
1903
-
1904
1768
  /**
1905
1769
  * get exact path BBox
1906
1770
  * calculating extremes for all command types
@@ -1964,166 +1828,10 @@ function getPathDataBBox(pathData) {
1964
1828
  }
1965
1829
  }
1966
1830
 
1967
- 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 };
1968
1832
  return bbox
1969
1833
  }
1970
1834
 
1971
- /**
1972
- * get pathdata area
1973
- */
1974
-
1975
- function getPathArea(pathData, decimals = 9) {
1976
- let totalArea = 0;
1977
- let polyPoints = [];
1978
-
1979
- let subPathsData = splitSubpaths(pathData);
1980
- let isCompoundPath = subPathsData.length > 1 ? true : false;
1981
- let counterShapes = [];
1982
-
1983
- // check intersections for compund paths
1984
- if (isCompoundPath) {
1985
- let bboxArr = getSubPathBBoxes(subPathsData);
1986
-
1987
- bboxArr.forEach(function (bb, b) {
1988
-
1989
- for (let i = 0; i < bboxArr.length; i++) {
1990
- let bb2 = bboxArr[i];
1991
- if (bb != bb2) {
1992
- let intersects = checkBBoxIntersections(bb, bb2);
1993
- if (intersects) {
1994
- counterShapes.push(i);
1995
- }
1996
- }
1997
- }
1998
- });
1999
- }
2000
-
2001
- subPathsData.forEach((pathData, d) => {
2002
-
2003
- polyPoints = [];
2004
- let comArea = 0;
2005
- let pathArea = 0;
2006
- let multiplier = 1;
2007
- let pts = [];
2008
-
2009
- pathData.forEach(function (com, i) {
2010
- let [type, values] = [com.type, com.values];
2011
- let valuesL = values.length;
2012
-
2013
- if (values.length) {
2014
- let prevC = i > 0 ? pathData[i - 1] : pathData[0];
2015
- let prevCVals = prevC.values;
2016
- let prevCValsL = prevCVals.length;
2017
- let p0 = { x: prevCVals[prevCValsL - 2], y: prevCVals[prevCValsL - 1] };
2018
- let p = { x: values[valuesL - 2], y: values[valuesL - 1] };
2019
-
2020
- // C commands
2021
- if (type === 'C' || type === 'Q') {
2022
- let cp1 = { x: values[0], y: values[1] };
2023
- pts = type === 'C' ? [p0, cp1, { x: values[2], y: values[3] }, p] : [p0, cp1, p];
2024
- let areaBez = Math.abs(getBezierArea(pts));
2025
- comArea += areaBez;
2026
-
2027
- polyPoints.push(p0, p);
2028
- }
2029
-
2030
- // A commands
2031
- else if (type === 'A') {
2032
- 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);
2033
- let { cx, cy, rx, ry, startAngle, endAngle, deltaAngle } = arcData;
2034
-
2035
- let arcArea = Math.abs(getEllipseArea(rx, ry, startAngle, endAngle));
2036
-
2037
- // subtract remaining polygon between p0, center and p
2038
- let polyArea = Math.abs(getPolygonArea([p0, { x: cx, y: cy }, p]));
2039
- arcArea -= polyArea;
2040
-
2041
- polyPoints.push(p0, p);
2042
- comArea += arcArea;
2043
- }
2044
-
2045
- // L commands
2046
- else {
2047
- polyPoints.push(p0, p);
2048
- }
2049
- }
2050
- });
2051
-
2052
- let areaPoly = getPolygonArea(polyPoints);
2053
-
2054
- if (counterShapes.indexOf(d) !== -1) {
2055
- multiplier = -1;
2056
- }
2057
-
2058
- if (
2059
- (areaPoly < 0 && comArea < 0)
2060
- ) {
2061
- // are negative
2062
- pathArea = (Math.abs(comArea) - Math.abs(areaPoly)) * multiplier;
2063
-
2064
- } else {
2065
- pathArea = (Math.abs(comArea) + Math.abs(areaPoly)) * multiplier;
2066
- }
2067
-
2068
- totalArea += pathArea;
2069
- });
2070
-
2071
- return totalArea;
2072
- }
2073
-
2074
- /**
2075
- * get ellipse area
2076
- * skips to circle calculation if rx===ry
2077
- */
2078
-
2079
- function getEllipseArea(rx, ry, startAngle, endAngle) {
2080
- const totalArea = Math.PI * rx * ry;
2081
- let angleDiff = (endAngle - startAngle + 2 * Math.PI) % (2 * Math.PI);
2082
- // If circle, use simple circular formula
2083
- if (rx === ry) return totalArea * (angleDiff / (2 * Math.PI));
2084
-
2085
- // Convert absolute angles to parametric angles
2086
- const absoluteToParametric = (phi)=>{
2087
- return Math.atan2(rx * Math.sin(phi), ry * Math.cos(phi));
2088
- };
2089
- startAngle = absoluteToParametric(startAngle);
2090
- endAngle = absoluteToParametric(endAngle);
2091
- angleDiff = (endAngle - startAngle + 2 * Math.PI) % (2 * Math.PI);
2092
- return totalArea * (angleDiff / (2 * Math.PI));
2093
- }
2094
-
2095
- /**
2096
- * get bezier area
2097
- */
2098
- function getBezierArea(pts, absolute=false) {
2099
-
2100
- let [p0, cp1, cp2, p] = [pts[0], pts[1], pts[2], pts[pts.length - 1]];
2101
- let area;
2102
-
2103
- if (pts.length < 3) return 0;
2104
-
2105
- // quadratic beziers
2106
- if (pts.length === 3) {
2107
- cp1 = {
2108
- x: pts[0].x * 1 / 3 + pts[1].x * 2 / 3,
2109
- y: pts[0].y * 1 / 3 + pts[1].y * 2 / 3
2110
- };
2111
-
2112
- cp2 = {
2113
- x: pts[2].x * 1 / 3 + pts[1].x * 2 / 3,
2114
- y: pts[2].y * 1 / 3 + pts[1].y * 2 / 3
2115
- };
2116
- }
2117
-
2118
- area = ((p0.x * (-2 * cp1.y - cp2.y + 3 * p.y) +
2119
- cp1.x * (2 * p0.y - cp2.y - p.y) +
2120
- cp2.x * (p0.y + cp1.y - 2 * p.y) +
2121
- p.x * (-3 * p0.y + cp1.y + 2 * cp2.y)) *
2122
- 3) / 20;
2123
-
2124
- return absolute ? Math.abs(area) : area;
2125
- }
2126
-
2127
1835
  function getPolygonArea(points, absolute=false) {
2128
1836
  let area = 0;
2129
1837
  let l = points.length;
@@ -2143,7 +1851,7 @@ function getPolygonArea(points, absolute=false) {
2143
1851
  * d attribute string
2144
1852
  */
2145
1853
 
2146
- function pathDataToD(pathData, mode = 0) {
1854
+ function pathDataToD(pathData = [], mode = 0) {
2147
1855
 
2148
1856
  mode = parseFloat(mode);
2149
1857
  /*
@@ -2152,80 +1860,99 @@ function pathDataToD(pathData, mode = 0) {
2152
1860
  1 = verbose
2153
1861
  2 = beautify
2154
1862
  */
1863
+
2155
1864
  let len = pathData.length;
1865
+ let d = '';
2156
1866
 
2157
- let valsString = pathData[0].values.join(" ");
2158
- let separator_command = mode > 1 ? `\n` :
2159
- ((mode < 1) ? '' : ' ');
2160
- 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';
2161
1870
 
2162
- // 1st command
2163
- let d = `${pathData[0].type}${separator_type}${valsString}${separator_command}`;
1871
+ if (mode < 1) {
1872
+ pathDataGrouped = [pathData[0]];
2164
1873
 
2165
- for (let i = 1; i < len; i++) {
2166
- let com0 = pathData[i - 1];
2167
- let com = pathData[i];
2168
- let { type, values } = com;
2169
- valsString = '';
2170
-
2171
- // Minify Arc commands (A/a) – actually sucks!
2172
- if (!mode && (type === 'A' || type === 'a')) {
2173
- values = [
2174
- values[0], values[1], values[2],
2175
- `${values[3]}${values[4]}${values[5]}`,
2176
- values[6]
2177
- ];
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;
2178
1892
  }
1893
+ }
2179
1894
 
2180
- // Omit type for repeated commands
2181
- type = ((mode < 1) && com0.type === com.type && com.type.toLowerCase() !== 'm')
2182
- ? " "
2183
- : ((mode < 1) && com0.type === "M" && com.type === "L"
2184
- ? " "
2185
- : com.type);
1895
+ // stringify grouped
1896
+ len = pathDataGrouped.length;
1897
+ let separator_type = mode < 1 ? '' : ' ';
1898
+ let separator_command = mode < 1 ? '' : (mode === 1 ? ' ' : `\n`);
2186
1899
 
2187
- // concatenate subsequent floating point values
2188
- if (!mode) {
1900
+ typePrev = 'M';
2189
1901
 
2190
- let prevWasFloat = false;
1902
+ for (let i = 0; i < len; i++) {
1903
+ let com = pathDataGrouped[i];
1904
+ let { type, values } = com;
2191
1905
 
2192
- for (let v = 0, l = values.length; v < l; v++) {
2193
- let val = values[v];
2194
- let valStr = val.toString();
2195
- let isFloat = valStr.includes('.');
2196
- 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'));
2197
1908
 
2198
- // Remove leading zero from small floats *only* if the previous was also a float
2199
- if (isSmallFloat && prevWasFloat) {
2200
- valStr = valStr.replace(/^0\./, '.');
2201
- }
1909
+ // add type
1910
+ if (!omitType) d += type + separator_type;
2202
1911
 
2203
- // Add space unless this is the first value OR previous was a small float
2204
- if (v > 0 && !(prevWasFloat && isSmallFloat)) {
2205
- valsString += ' ';
2206
- }
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 ? '' : ' ');
2207
1936
 
2208
- valsString += valStr;
2209
- prevWasFloat = isSmallFloat;
2210
1937
  }
2211
1938
 
2212
- }
2213
- // regular non-minified output
2214
- else {
2215
- 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
+
2216
1948
  }
2217
1949
 
2218
- if(i===len-1) separator_command='';
2219
- d += `${type}${separator_type}${valsString}${separator_command}`;
2220
- }
1950
+ // add command separator
1951
+ if (mode) d += separator_command;
1952
+
1953
+ // update previous type
1954
+ typePrev = type;
2221
1955
 
2222
- if (mode < 1) {
2223
- d = d
2224
- .replace(/[A-Za-z]0(?=\.)/g, m => m[0])
2225
- .replace(/ 0\./g, " .") // Space before small decimals
2226
- .replace(/ -/g, "-") // Remove space before negatives
2227
- .replace(/-0\./g, "-.") // Remove leading zero from negative decimals
2228
- .replace(/Z/g, "z"); // Convert uppercase 'Z' to lowercase
2229
1956
  }
2230
1957
 
2231
1958
  return d;
@@ -2233,39 +1960,26 @@ function pathDataToD(pathData, mode = 0) {
2233
1960
 
2234
1961
  function getCombinedByDominant(com1, com2, maxDist = 0, tolerance = 1, debug = false) {
2235
1962
 
2236
- // cubic Bézier derivative
2237
- const cubicDerivative = (p0, p1, p2, p3, t) => {
2238
- let mt = 1 - t;
2239
-
2240
- return {
2241
- x:
2242
- 3 * mt * mt * (p1.x - p0.x) +
2243
- 6 * mt * t * (p2.x - p1.x) +
2244
- 3 * t * t * (p3.x - p2.x),
2245
- y:
2246
- 3 * mt * mt * (p1.y - p0.y) +
2247
- 6 * mt * t * (p2.y - p1.y) +
2248
- 3 * t * t * (p3.y - p2.y)
2249
- };
2250
- };
2251
-
2252
1963
  // if combining fails return original commands
2253
1964
  let commands = [com1, com2];
2254
1965
 
2255
1966
  // detect dominant
2256
- let dist1 = getDistAv(com1.p0, com1.p);
2257
- 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;
2258
1970
 
2259
- let reverse = dist1 > dist2;
1971
+ // take longer command
1972
+ let reverse = dist1 < dist2;
2260
1973
 
2261
1974
  // backup original commands
2262
1975
  let com1_o = JSON.parse(JSON.stringify(com1));
2263
1976
  let com2_o = JSON.parse(JSON.stringify(com2));
2264
1977
 
2265
- 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);
2266
1980
 
1981
+ // no intersection - we can't combine
2267
1982
  if (!ptI) {
2268
-
2269
1983
  return commands
2270
1984
  }
2271
1985
 
@@ -2288,153 +2002,70 @@ function getCombinedByDominant(com1, com2, maxDist = 0, tolerance = 1, debug = f
2288
2002
  com2 = com2_R;
2289
2003
  }
2290
2004
 
2291
- let add = (a, b) => ({ x: a.x + b.x, y: a.y + b.y });
2292
- let sub = (a, b) => ({ x: a.x - b.x, y: a.y - b.y });
2293
- let mul = (a, s) => ({ x: a.x * s, y: a.y * s });
2294
- let dot = (a, b) => a.x * b.x + a.y * b.y;
2295
-
2296
- // estimate extrapolation parameter t0
2297
-
2298
- let B0 = com2.p0;
2299
- let D0 = cubicDerivative(
2300
- com2.p0,
2301
- com2.cp1,
2302
- com2.cp2,
2303
- com2.p,
2304
- 0
2305
- );
2306
-
2307
- let v = sub(com1.p0, B0);
2308
-
2309
- // first-order projection onto tangent
2310
- let t0 = dot(v, D0) / dot(D0, D0);
2311
-
2312
- // refine with one Newton iteration (optional but cheap)
2313
- let P = pointAtT([com2.p0, com2.cp1, com2.cp2, com2.p], t0);
2314
- let dP = cubicDerivative(com2.p0, com2.cp1, com2.cp2, com2.p, t0);
2315
- let r = sub(P, com1.p0);
2316
-
2317
- t0 -= dot(r, dP) / dot(dP, dP);
2318
-
2319
- // construct merged cubic over [t0, 1]
2320
- let Q0 = pointAtT([com2.p0, com2.cp1, com2.cp2, com2.p], t0);
2321
- let Q3 = com2.p;
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);
2322
2008
 
2323
- let d0 = cubicDerivative(com2.p0, com2.cp1, com2.cp2, com2.p, t0);
2324
- let d1 = cubicDerivative(com2.p0, com2.cp1, com2.cp2, com2.p, 1);
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;
2325
2012
 
2326
- let scale = 1 - t0;
2013
+ // extrapolate
2014
+ let segs = pointAtT([com1.p0, com1.cp1, com1.cp2, com1.p], t, false, true).segments;
2327
2015
 
2328
- let Q1 = add(Q0, mul(d0, scale / 3));
2329
- let Q2 = sub(Q3, mul(d1, scale / 3));
2016
+ let seg = segs[0];
2330
2017
 
2331
- let result = {
2332
- p0: Q0,
2333
- cp1: Q1,
2334
- cp2: Q2,
2335
- p: Q3,
2336
- t0
2337
- };
2018
+ // if it worked - points should be nearby
2019
+ let dist = getDistManhattan(seg.p, com2.p);
2338
2020
 
2339
- if (reverse) {
2340
- result = {
2341
- p0: Q3,
2342
- cp1: Q2,
2343
- cp2: Q1,
2344
- p: Q0,
2345
- t0
2346
- };
2347
- }
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);
2348
2025
 
2349
- let tMid = (1 - t0) * 0.5;
2026
+ let angleDiff = (angle2 - angle);
2350
2027
 
2351
- let ptM = pointAtT([result.p0, result.cp1, result.cp2, result.p], tMid, false, true);
2352
- let seg1_cp2 = ptM.cpts[2];
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);
2353
2031
 
2354
- let ptI_1 = checkLineIntersection(ptM, seg1_cp2, result.p0, ptI, false);
2355
- let ptI_2 = checkLineIntersection(ptM, seg1_cp2, result.p, ptI, false);
2356
-
2357
- let cp1_2 = interpolate(result.p0, ptI_1, 1.333 );
2358
- let cp2_2 = interpolate(result.p, ptI_2, 1.333 );
2359
-
2360
- // test self intersections and exit
2361
- let cp_intersection = checkLineIntersection(com1_o.p0, cp1_2, com2_o.p, cp2_2, true);
2362
- if (cp_intersection) {
2363
-
2364
- return commands;
2365
- }
2032
+ // copy original final point coordinates
2033
+ seg.p = com2.p;
2366
2034
 
2367
- result.cp1 = cp1_2;
2368
- result.cp2 = cp2_2;
2035
+ // after rotation
2036
+ let dist2 = getDistManhattan(seg.p, seg.cp2);
2037
+ let scale = dist2 / dist1;
2369
2038
 
2370
- // check distances between original starting point and extrapolated
2371
- let dist3 = getDistAv(com1_o.p0, result.p0);
2372
- let dist4 = getDistAv(com2_o.p, result.p);
2373
- let dist5 = (dist3 + dist4);
2039
+ // adjust tangent length
2040
+ seg.cp2 = interpolate(seg.p, seg.cp2, scale);
2374
2041
 
2375
- // use original points
2376
- result.p0 = com1_o.p0;
2377
- result.p = com2_o.p;
2378
- result.extreme = com2_o.extreme;
2379
- result.corner = com2_o.corner;
2380
- result.dimA = com2_o.dimA;
2381
- result.directionChange = com2_o.directionChange;
2382
- result.type = 'C';
2383
- result.values = [result.cp1.x, result.cp1.y, result.cp2.x, result.cp2.y, result.p.x, result.p.y];
2384
-
2385
- // extrapolated starting point is not completely off
2386
- if (dist5 < maxDist) {
2387
-
2388
- /*
2389
- let tTotal = 1 + Math.abs(t0);
2390
- let tSplit = reverse ? 1 + t0 : Math.abs(t0);
2391
-
2392
- let pO = pointAtT([com2_o.p0, com2_o.cp1, com2_o.cp2, com2_o.p], t0);
2393
- */
2394
-
2395
- // split t to meet original mid segment start point
2396
- let tSplit = reverse ? 1 + t0 : Math.abs(t0);
2397
-
2398
- let tTotal = 1 + Math.abs(t0);
2399
- tSplit = reverse ? 1 + t0 : Math.abs(t0) / tTotal;
2400
-
2401
- let ptSplit = pointAtT([result.p0, result.cp1, result.cp2, result.p], tSplit);
2402
- let distSplit = getDistAv(ptSplit, com1.p);
2403
-
2404
- // not close enough - exit
2405
- if (distSplit > maxDist * tolerance ) {
2406
-
2407
- 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
+ };
2408
2050
  }
2409
2051
 
2410
- // compare combined with original area
2411
- let pathData0 = [
2412
- { type: 'M', values: [com1_o.p0.x, com1_o.p0.y] },
2413
- { 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] },
2414
- { 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] },
2415
- ];
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
2416
2065
 
2417
- let area0 = getPathArea(pathData0);
2418
- let pathDataN = [
2419
- { type: 'M', values: [result.p0.x, result.p0.y] },
2420
- { type: 'C', values: [result.cp1.x, result.cp1.y, result.cp2.x, result.cp2.y, result.p.x, result.p.y] },
2066
+ }
2421
2067
  ];
2422
2068
 
2423
- let areaN = getPathArea(pathDataN);
2424
- let areaDiff = Math.abs(areaN / area0 - 1);
2425
-
2426
- result.error = areaDiff * 5 * tolerance;
2427
-
2428
- if (debug) {
2429
- pathDataToD(pathDataN);
2430
-
2431
- }
2432
-
2433
- // success!!!
2434
- if (areaDiff < 0.05 * tolerance) {
2435
- commands = [result];
2436
-
2437
- }
2438
2069
  }
2439
2070
 
2440
2071
  return commands
@@ -2557,11 +2188,12 @@ function combineCubicPairs(com1, com2, {
2557
2188
  // quit if t is start
2558
2189
  if (!t) return commands;
2559
2190
 
2191
+ // get averaged threshold
2560
2192
  let distAv1 = getDistManhattan(com1.p0, com1.p);
2561
2193
  let distAv2 = getDistManhattan(com2.p0, com2.p);
2562
2194
  let distMin = Math.max(0, Math.min(distAv1, distAv2));
2563
2195
 
2564
- let distScale = 0.08;
2196
+ let distScale = 0.075;
2565
2197
  let maxDist = distMin * distScale * tolerance;
2566
2198
 
2567
2199
  // get hypothetical combined command
@@ -2609,16 +2241,6 @@ function combineCubicPairs(com1, com2, {
2609
2241
 
2610
2242
  if (error < maxDist) success = true;
2611
2243
 
2612
- /*
2613
- renderPoint(markers, ptM_seg1, 'cyan')
2614
- renderPoint(markers, pt, 'orange', '1.5%', '1')
2615
- renderPoint(markers, ptM_seg2, 'orange')
2616
-
2617
- renderPoint(markers, com1.p, 'green')
2618
-
2619
- renderPoint(markers, ptI_seg1, 'purple')
2620
- */
2621
-
2622
2244
  }
2623
2245
 
2624
2246
  } // end 1st try
@@ -2637,14 +2259,6 @@ function combineCubicPairs(com1, com2, {
2637
2259
  comS.directionChange = com2.directionChange;
2638
2260
  comS.corner = com2.corner;
2639
2261
 
2640
- if (comS.extreme || comS.corner) ;
2641
-
2642
- /*
2643
- comS.extreme = com1.extreme;
2644
- comS.directionChange = com1.directionChange;
2645
- comS.corner = com1.corner;
2646
- */
2647
-
2648
2262
  comS.values = [comS.cp1.x, comS.cp1.y, comS.cp2.x, comS.cp2.y, comS.p.x, comS.p.y];
2649
2263
 
2650
2264
  // relative error
@@ -2703,7 +2317,9 @@ function findSplitT(com1, com2) {
2703
2317
  }
2704
2318
  */
2705
2319
 
2706
- return l1 / l3
2320
+ let t = l1 / l3;
2321
+
2322
+ return t;
2707
2323
  }
2708
2324
 
2709
2325
  function commandIsFlat(points, {
@@ -2756,7 +2372,7 @@ function analyzePathData(pathData = [], {
2756
2372
  detectDirection = true,
2757
2373
  detectSemiExtremes = false,
2758
2374
  debug = false,
2759
- addSquareLength = true,
2375
+ addSquareLength = false,
2760
2376
  addArea = true,
2761
2377
 
2762
2378
  } = {}) {
@@ -2805,8 +2421,8 @@ function analyzePathData(pathData = [], {
2805
2421
  let commandPts = (type === 'C' || type === 'Q') ?
2806
2422
  (type === 'C' ? [p0, cp1, cp2, p] : [p0, cp1, p]) :
2807
2423
  ([p0, p]);
2808
- let thresholdLength = dimA * 0.1;
2809
- let threshold = thresholdLength * 0.01;
2424
+
2425
+ let threshold = dimA * 0.005;
2810
2426
 
2811
2427
  // bezier types
2812
2428
  let isBezier = type === 'Q' || type === 'C';
@@ -2819,7 +2435,7 @@ function analyzePathData(pathData = [], {
2819
2435
  */
2820
2436
  let hasExtremes = false;
2821
2437
 
2822
- if (isBezier) {
2438
+ if (detectExtremes && isBezier) {
2823
2439
 
2824
2440
  let dx = type === 'C' ? Math.abs(com.cp2.x - com.p.x) : Math.abs(com.cp1.x - com.p.x);
2825
2441
  let dy = type === 'C' ? Math.abs(com.cp2.y - com.p.y) : Math.abs(com.cp1.y - com.p.y);
@@ -2858,7 +2474,7 @@ function analyzePathData(pathData = [], {
2858
2474
  }
2859
2475
 
2860
2476
  // check extremes introduce by small arcs
2861
- 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')) ){
2862
2478
  let distN = comN ? comN.dimA : 0;
2863
2479
  let isShort = com.dimA < (comPrev.dimA + distN) * 0.1;
2864
2480
  let smallRadius = com.values[0] === com.values[1] && (com.values[0] < 1);
@@ -2876,28 +2492,7 @@ function analyzePathData(pathData = [], {
2876
2492
  if (hasExtremes) com.extreme = true;
2877
2493
 
2878
2494
  // Corners and semi extremes
2879
- if (isBezier && isBezierN) {
2880
-
2881
- // semi extremes
2882
- if (detectSemiExtremes && !com.extreme) {
2883
-
2884
- let dx1 = Math.abs(p.x - cp2.x);
2885
- let dy1 = Math.abs(p.y - cp2.y);
2886
- let hasSemiExtreme = false;
2887
-
2888
- // exclude extremes or small deltas
2889
- if (dx1 && dy1 && dx1 > thresholdLength || dy1 > thresholdLength) {
2890
- let ang1 = getAngle(cp2, p);
2891
- let ang2 = getAngle(p, comN.cp1);
2892
-
2893
- let ang3 = Math.abs(ang1 + ang2) / 2;
2894
- hasSemiExtreme = isMultipleOf45(ang3);
2895
- }
2896
-
2897
- if (hasSemiExtreme) {
2898
- com.semiExtreme = true;
2899
- }
2900
- }
2495
+ if (detectCorners && isBezier && isBezierN) {
2901
2496
 
2902
2497
  /**
2903
2498
  * Detect direction change points
@@ -3473,7 +3068,12 @@ function convertPathData(pathData, {
3473
3068
  if (hasShorthands && toLonghands) pathData = pathDataToLonghands(pathData);
3474
3069
 
3475
3070
  // minify semicircle radii
3476
- if (optimizeArcs) pathData = optimizeArcPathData(pathData);
3071
+ if (optimizeArcs) {
3072
+ pathData = optimizeArcPathData(pathData);
3073
+ } else {
3074
+ // get true absolute radii
3075
+ pathData = pathDataToTrueArcValues(pathData);
3076
+ }
3477
3077
 
3478
3078
  if (toShorthands) pathData = pathDataToShorthands(pathData);
3479
3079
 
@@ -3563,9 +3163,42 @@ function parsePathDataNormalized(d,
3563
3163
  }
3564
3164
 
3565
3165
  /**
3566
- *
3567
- * @param {*} pathData
3568
- * @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
3569
3202
  */
3570
3203
 
3571
3204
  function optimizeArcPathData(pathData = []) {
@@ -3583,6 +3216,11 @@ function optimizeArcPathData(pathData = []) {
3583
3216
 
3584
3217
  let [rx, ry, largeArc, x, y] = [values[0], values[1], values[3], values[5], values[6]];
3585
3218
  let comPrev = pathData[i - 1];
3219
+
3220
+ // force absolute
3221
+ rx = Math.abs(rx);
3222
+ ry = Math.abs(ry);
3223
+
3586
3224
  let [x0, y0] = [comPrev.values[comPrev.values.length - 2], comPrev.values[comPrev.values.length - 1]];
3587
3225
  let M = { x: x0, y: y0 };
3588
3226
  let p = { x, y };
@@ -3622,7 +3260,7 @@ function optimizeArcPathData(pathData = []) {
3622
3260
  if (isHorizontal || isVertical) {
3623
3261
 
3624
3262
  // check if semi circle
3625
- let needsTrueR = isHorizontal ? rx*1.9 > diffX : ry*1.9 > diffY;
3263
+ let needsTrueR = isHorizontal ? rx * 1.9 > diffX : ry * 1.9 > diffY;
3626
3264
 
3627
3265
  // is semicircle we can simplify rx
3628
3266
  if (!needsTrueR) {
@@ -3727,7 +3365,7 @@ function pathDataArcsToCubics(pathData, {
3727
3365
 
3728
3366
  if (com.type === 'A') {
3729
3367
  // add all C commands instead of Arc
3730
- let cubicArcs = arcToBezier$1(p0, com.values, arcAccuracy);
3368
+ let cubicArcs = arcToBezier(p0, com.values, arcAccuracy);
3731
3369
  cubicArcs.forEach((cubicArc) => {
3732
3370
  pathDataCubic.push(cubicArc);
3733
3371
  });
@@ -4228,7 +3866,7 @@ function arcToBezierResolved({
4228
3866
  * returns pathData array
4229
3867
  */
4230
3868
 
4231
- function arcToBezier$1(p0, values, splitSegments = 1) {
3869
+ function arcToBezier(p0, values, splitSegments = 1) {
4232
3870
  const TAU = Math.PI * 2;
4233
3871
  let [rx, ry, rotation, largeArcFlag, sweepFlag, x, y] = values;
4234
3872
 
@@ -4239,8 +3877,8 @@ function arcToBezier$1(p0, values, splitSegments = 1) {
4239
3877
  let phi = rotation ? rotation * TAU / 360 : 0;
4240
3878
  let sinphi = phi ? Math.sin(phi) : 0;
4241
3879
  let cosphi = phi ? Math.cos(phi) : 1;
4242
- let pxp = cosphi * (p0.x - x) / 2 + sinphi * (p0.y - y) / 2;
4243
- let pyp = -sinphi * (p0.x - x) / 2 + cosphi * (p0.y - y) / 2;
3880
+ let pxp = cosphi * (p0.x - x) *0.5 + sinphi * (p0.y - y) *0.5;
3881
+ let pyp = -sinphi * (p0.x - x) *0.5 + cosphi * (p0.y - y) *0.5;
4244
3882
 
4245
3883
  if (pxp === 0 && pyp === 0) {
4246
3884
  return []
@@ -4276,8 +3914,8 @@ function arcToBezier$1(p0, values, splitSegments = 1) {
4276
3914
 
4277
3915
  let centerxp = radicant ? radicant * rx / ry * pyp : 0;
4278
3916
  let centeryp = radicant ? radicant * -ry / rx * pxp : 0;
4279
- let centerx = cosphi * centerxp - sinphi * centeryp + (p0.x + x) / 2;
4280
- let centery = sinphi * centerxp + cosphi * centeryp + (p0.y + y) / 2;
3917
+ let centerx = cosphi * centerxp - sinphi * centeryp + (p0.x + x) * 0.5;
3918
+ let centery = sinphi * centerxp + cosphi * centeryp + (p0.y + y) * 0.5;
4281
3919
 
4282
3920
  let vx1 = (pxp - centerxp) / rx;
4283
3921
  let vy1 = (pyp - centeryp) / ry;
@@ -4299,15 +3937,17 @@ function arcToBezier$1(p0, values, splitSegments = 1) {
4299
3937
  ang2 = vectorAngle(vx1, vy1, vx2, vy2);
4300
3938
 
4301
3939
  if (sweepFlag === 0 && ang2 > 0) {
4302
- ang2 -= Math.PI * 2;
3940
+
3941
+ ang2 -= TAU;
4303
3942
  }
4304
3943
  else if (sweepFlag === 1 && ang2 < 0) {
4305
- ang2 += Math.PI * 2;
3944
+
3945
+ ang2 += TAU;
4306
3946
  }
4307
3947
 
4308
- let ratio = +(Math.abs(ang2) / (TAU / 4)).toFixed(0) || 1;
3948
+ let ratio = +(Math.abs(ang2) / (TAU * 0.25)).toFixed(0) || 1;
4309
3949
 
4310
- // increase segments for more accureate length calculations
3950
+ // increase segments for more accurate length calculations
4311
3951
  let segments = ratio * splitSegments;
4312
3952
  ang2 /= segments;
4313
3953
  let pathDataArc = [];
@@ -4319,7 +3959,8 @@ function arcToBezier$1(p0, values, splitSegments = 1) {
4319
3959
  const k = 0.551785;
4320
3960
  let a = ang2 === angle90 ? k :
4321
3961
  (
4322
- ang2 === -angle90 ? -k : 4 / 3 * Math.tan(ang2 / 4)
3962
+
3963
+ ang2 === -angle90 ? -k : 1.33333 * Math.tan(ang2 * 0.25)
4323
3964
  );
4324
3965
 
4325
3966
  let cos2 = ang2 ? Math.cos(ang2) : 1;
@@ -4369,59 +4010,50 @@ function pathDataRemoveColinear(pathData, {
4369
4010
  pathData[pathData.length - 1].type.toLowerCase() === 'z';
4370
4011
 
4371
4012
  for (let c = 1, l = pathData.length; c < l; c++) {
4372
-
4373
4013
  let com = pathData[c];
4014
+ let { type, values } = com;
4374
4015
  let comN = pathData[c + 1] || pathData[l - 1];
4375
-
4016
+ let valuesN = comN.values;
4376
4017
  let p1 = comN.type.toLowerCase() === 'z' ? M : { x: comN.values[comN.values.length - 2], y: comN.values[comN.values.length - 1] };
4377
4018
 
4378
- let { type, values } = com;
4379
4019
  let valsL = values.slice(-2);
4380
4020
  p = type !== 'Z' ? { x: valsL[0], y: valsL[1] } : M;
4381
4021
 
4022
+ let nextBezier = type!==comN.type && (comN.type==='C' || comN.type==='Q');
4023
+
4382
4024
  let area = p1 ? getPolygonArea([p0, p, p1], true) : Infinity;
4383
4025
  let distSquare = getSquareDistance(p0, p1);
4384
4026
  let distMax = distSquare ? distSquare / 333 * tolerance : 0;
4385
4027
 
4386
4028
  let isFlat = area < distMax;
4387
-
4388
4029
  let isFlatBez = false;
4030
+ let cpts = [];
4389
4031
 
4390
- /*
4391
- // flatness by cross product
4392
- let dx0 = Math.abs(p1.x - p0.x)
4393
- let dy0 = Math.abs(p1.y - p0.y)
4394
-
4395
- let dx1 = Math.abs(p.x - p0.x)
4396
- let dy1 = Math.abs(p.y - p0.y)
4397
-
4398
- let dx2 = Math.abs(p1.x - p.x)
4399
- let dy2 = Math.abs(p1.y - p.y)
4400
-
4401
- // zero length segments are flat
4402
- let isZeroLength = (!dy1 && !dx1) || (!dy2 && !dx2)
4403
- if (isZeroLength) isFlat = true;
4404
-
4405
- // check cross products for colinearity
4406
- if (!isFlat) {
4407
-
4408
- let cross0 = Math.abs(dx0 * dy1 - dy0 * dx1);
4032
+ /**
4033
+ * type change
4034
+ * check flatness
4035
+ */
4036
+ if (nextBezier) {
4037
+ cpts = comN.type === 'C' ?
4038
+ [{ x: valuesN[0], y: valuesN[1] }, { x: valuesN[2], y: valuesN[3] }] :
4039
+ (comN.type === 'Q' ? [{ x: valuesN[0], y: valuesN[1] }] : []);
4409
4040
 
4410
- let thresh = (dx0 + dy0) * 0.1
4041
+ isFlat = commandIsFlat([p0, ...cpts, p], { tolerance });
4411
4042
 
4412
- if ( cross0 < thresh) {
4043
+ /*
4413
4044
 
4414
- isFlat = true
4045
+ if(!isFlatBez){
4046
+ pathDataN.push(com)
4047
+ continue
4415
4048
  }
4416
- }
4417
- */
4049
+ */
4418
4050
 
4419
- if (!flatBezierToLinetos && type === 'C') isFlat = false;
4051
+ }
4420
4052
 
4421
4053
  // convert flat beziers to linetos
4422
4054
  if (flatBezierToLinetos && (type === 'C' || type === 'Q')) {
4423
4055
 
4424
- let cpts = type === 'C' ?
4056
+ cpts = type === 'C' ?
4425
4057
  [{ x: values[0], y: values[1] }, { x: values[2], y: values[3] }] :
4426
4058
  (type === 'Q' ? [{ x: values[0], y: values[1] }] : []);
4427
4059
 
@@ -4435,10 +4067,13 @@ function pathDataRemoveColinear(pathData, {
4435
4067
  }
4436
4068
  }
4437
4069
 
4438
- // colinear – exclude arcs (as always =) as semicircles won't have an area
4070
+ /**
4071
+ * colinear = simplification success
4072
+ * exclude arcs (as always =)
4073
+ * as semicircles won't have an area
4074
+ */
4439
4075
 
4440
4076
  if (isFlat && c < l - 1 && comN.type !== 'A' && (type === 'L' || (flatBezierToLinetos && isFlatBez))) {
4441
-
4442
4077
  continue;
4443
4078
  }
4444
4079
 
@@ -4803,9 +4438,7 @@ function refineAdjacentExtremes(pathData, {
4803
4438
  if (comEx.length === 1) {
4804
4439
 
4805
4440
  comEx = comEx[0];
4806
-
4807
4441
  pathData[i + 1] = null;
4808
-
4809
4442
  pathData[i + 2].values = [comEx.cp1.x, comEx.cp1.y, comEx.cp2.x, comEx.cp2.y, comEx.p.x, comEx.p.y];
4810
4443
  pathData[i + 2].cp1 = comEx.cp1;
4811
4444
  pathData[i + 2].cp2 = comEx.cp2;