svg-path-simplify 0.0.8 → 0.0.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. package/README.md +25 -5
  2. package/dist/svg-path-simplify.esm.js +576 -494
  3. package/dist/svg-path-simplify.esm.min.js +1 -1
  4. package/dist/svg-path-simplify.js +576 -494
  5. package/dist/svg-path-simplify.min.js +1 -1
  6. package/dist/svg-path-simplify.node.js +576 -494
  7. package/dist/svg-path-simplify.node.min.js +1 -1
  8. package/index.html +86 -29
  9. package/package.json +1 -1
  10. package/src/detect_input.js +17 -10
  11. package/src/index.js +3 -0
  12. package/src/pathData_simplify_cubic.js +113 -106
  13. package/src/pathData_simplify_cubic_extrapolate.js +25 -11
  14. package/src/pathSimplify-main.js +89 -182
  15. package/src/svgii/geometry_flatness.js +29 -36
  16. package/src/svgii/pathData_analyze.js +4 -0
  17. package/src/svgii/pathData_convert.js +26 -17
  18. package/src/svgii/pathData_interpolate.js +65 -0
  19. package/src/svgii/pathData_parse.js +25 -9
  20. package/src/svgii/pathData_parse_els.js +18 -12
  21. package/src/svgii/pathData_remove_collinear.js +31 -28
  22. package/src/svgii/pathData_remove_orphaned.js +5 -4
  23. package/src/svgii/pathData_remove_zerolength.js +8 -4
  24. package/src/svgii/pathData_reorder.js +6 -2
  25. package/src/svgii/pathData_simplify_refineCorners.js +160 -0
  26. package/src/svgii/{simplify_refineExtremes.js → pathData_simplify_refineExtremes.js} +78 -43
  27. package/src/svgii/pathData_split.js +42 -15
  28. package/src/svgii/pathData_stringify.js +3 -12
  29. package/src/svgii/rounding.js +16 -14
  30. package/src/svgii/svg_cleanup.js +1 -1
  31. package/src/pathData_simplify_cubic_arr.js +0 -50
  32. package/src/svgii/simplify.js +0 -248
  33. package/src/svgii/simplify_bezier.js +0 -470
  34. package/src/svgii/simplify_linetos.js +0 -93
@@ -50,23 +50,29 @@
50
50
  if (input instanceof ArrayBuffer) return "buffer";
51
51
  if (input instanceof Blob) return "blob";
52
52
  */
53
- if (Array.isArray(input)) return "array";
53
+ if (Array.isArray(input)) {
54
+ if (input[0]?.type && input[0]?.values
55
+ ) {
56
+ return "pathData";
57
+ }
58
+
59
+ return "array";
60
+ }
54
61
 
55
62
  if (typeof input === "string") {
56
63
  input = input.trim();
57
64
  let isSVG = input.includes('<svg') && input.includes('</svg');
58
65
  let isPathData = input.startsWith('M') || input.startsWith('m');
59
- let isPolyString = !isNaN(input.substring(0, 1)) && !isNaN(input.substring(input.length-1, input.length));
66
+ let isPolyString = !isNaN(input.substring(0, 1)) && !isNaN(input.substring(input.length - 1, input.length));
60
67
 
61
-
62
- if(isSVG) {
63
- type='svgMarkup';
68
+ if (isSVG) {
69
+ type = 'svgMarkup';
64
70
  }
65
- else if(isPathData) {
66
- type='pathDataString';
71
+ else if (isPathData) {
72
+ type = 'pathDataString';
67
73
  }
68
- else if(isPolyString) {
69
- type='polyString';
74
+ else if (isPolyString) {
75
+ type = 'polyString';
70
76
  }
71
77
 
72
78
  else {
@@ -775,27 +781,23 @@
775
781
  * split compound paths into
776
782
  * sub path data array
777
783
  */
778
- function splitSubpaths(pathData) {
779
784
 
785
+ function splitSubpaths(pathData) {
780
786
  let subPathArr = [];
787
+ let current = [pathData[0]];
788
+ let l = pathData.length;
781
789
 
782
-
783
- try{
784
- let subPathIndices = pathData.map((com, i) => (com.type.toLowerCase() === 'm' ? i : -1)).filter(i => i !== -1);
790
+ for (let i = 1; i < l; i++) {
791
+ let com = pathData[i];
785
792
 
786
- }catch{
787
- console.log('catch', pathData);
793
+ if (com.type === 'M' || com.type === 'm') {
794
+ subPathArr.push(current);
795
+ current = [];
796
+ }
797
+ current.push(com);
788
798
  }
789
799
 
790
- let subPathIndices = pathData.map((com, i) => (com.type.toLowerCase() === 'm' ? i : -1)).filter(i => i !== -1);
791
-
792
- // no compound path
793
- if (subPathIndices.length === 1) {
794
- return [pathData]
795
- }
796
- subPathIndices.forEach((index, i) => {
797
- subPathArr.push(pathData.slice(index, subPathIndices[i + 1]));
798
- });
800
+ if (current.length) subPathArr.push(current);
799
801
 
800
802
  return subPathArr;
801
803
  }
@@ -1479,13 +1481,6 @@
1479
1481
  let beautify = optimize > 1;
1480
1482
  let minify = beautify || optimize ? false : true;
1481
1483
 
1482
- // Convert first "M" to "m" if followed by "l" (when minified)
1483
- /*
1484
- if (pathData[1].type === "l" && minify) {
1485
- pathData[0].type = "m";
1486
- }
1487
- */
1488
-
1489
1484
  let d = '';
1490
1485
  let separator_command = beautify ? `\n` : (minify ? '' : ' ');
1491
1486
  let separator_type = !minify ? ' ' : '';
@@ -1507,13 +1502,11 @@
1507
1502
  }
1508
1503
 
1509
1504
  // Omit type for repeated commands
1510
- type = (com0.type === com.type && com.type.toLowerCase() !== 'm' && minify)
1505
+ type = (minify && com0.type === com.type && com.type.toLowerCase() !== 'm' )
1511
1506
  ? " "
1512
- : (
1513
- (com0.type === "M" && com.type === "L")
1514
- ) && minify
1507
+ : (minify && com0.type === "M" && com.type === "L"
1515
1508
  ? " "
1516
- : com.type;
1509
+ : com.type);
1517
1510
 
1518
1511
  // concatenate subsequent floating point values
1519
1512
  if (minify) {
@@ -1663,6 +1656,7 @@
1663
1656
  cp1: Q1,
1664
1657
  cp2: Q2,
1665
1658
  p: Q3,
1659
+ t0
1666
1660
  };
1667
1661
 
1668
1662
  if (reverse) {
@@ -1671,6 +1665,7 @@
1671
1665
  cp1: Q2,
1672
1666
  cp2: Q1,
1673
1667
  p: Q0,
1668
+ t0
1674
1669
  };
1675
1670
  }
1676
1671
 
@@ -1682,8 +1677,8 @@
1682
1677
  let ptI_1 = checkLineIntersection(ptM, seg1_cp2, result.p0, ptI, false);
1683
1678
  let ptI_2 = checkLineIntersection(ptM, seg1_cp2, result.p, ptI, false);
1684
1679
 
1685
- let cp1_2 = interpolate(result.p0, ptI_1, 1.333);
1686
- let cp2_2 = interpolate(result.p, ptI_2, 1.333);
1680
+ let cp1_2 = interpolate(result.p0, ptI_1, 1.333 );
1681
+ let cp2_2 = interpolate(result.p, ptI_2, 1.333 );
1687
1682
 
1688
1683
  // test self intersections and exit
1689
1684
  let cp_intersection = checkLineIntersection(com1_o.p0, cp1_2, com2_o.p, cp2_2, true);
@@ -1715,14 +1710,24 @@
1715
1710
  // extrapolated starting point is not completely off
1716
1711
  if (dist5 < maxDist) {
1717
1712
 
1713
+ /*
1714
+ let tTotal = 1 + Math.abs(t0);
1715
+ let tSplit = reverse ? 1 + t0 : Math.abs(t0);
1716
+
1717
+ let pO = pointAtT([com2_o.p0, com2_o.cp1, com2_o.cp2, com2_o.p], t0);
1718
+ */
1719
+
1718
1720
  // split t to meet original mid segment start point
1719
1721
  let tSplit = reverse ? 1 + t0 : Math.abs(t0);
1720
1722
 
1723
+ let tTotal = 1 + Math.abs(t0);
1724
+ tSplit = reverse ? 1 + t0 : Math.abs(t0) / tTotal;
1725
+
1721
1726
  let ptSplit = pointAtT([result.p0, result.cp1, result.cp2, result.p], tSplit);
1722
1727
  let distSplit = getDistAv(ptSplit, com1.p);
1723
1728
 
1724
1729
  // not close enough - exit
1725
- if (distSplit > maxDist * tolerance) {
1730
+ if (distSplit > maxDist * tolerance ) {
1726
1731
 
1727
1732
  return commands;
1728
1733
  }
@@ -1761,21 +1766,122 @@
1761
1766
 
1762
1767
  }
1763
1768
 
1764
- function combineCubicPairs(com1, com2, extrapolateDominant = false, tolerance = 1) {
1769
+ function simplifyPathDataCubic(pathData, {
1770
+ keepExtremes = true,
1771
+ keepInflections = true,
1772
+ keepCorners = true,
1773
+ extrapolateDominant = true,
1774
+ tolerance = 1,
1775
+ } = {}) {
1776
+
1777
+ let pathDataN = [pathData[0]];
1778
+ let l = pathData.length;
1779
+
1780
+ for (let i = 2; l && i <= l; i++) {
1781
+ let com = pathData[i - 1];
1782
+ let comN = i < l ? pathData[i] : null;
1783
+ let typeN = comN?.type || null;
1784
+
1785
+ let isDirChange = com?.directionChange || null;
1786
+ let isDirChangeN = comN?.directionChange || null;
1787
+
1788
+ let { type, values, p0, p, cp1 = null, cp2 = null, extreme = false, corner = false, dimA = 0 } = com;
1789
+
1790
+ // next is also cubic
1791
+ if (type === 'C' && typeN === 'C') {
1792
+
1793
+ // cannot be combined as crossing extremes or corners
1794
+ if (
1795
+ (keepInflections && isDirChangeN) ||
1796
+ (keepCorners && corner) ||
1797
+ (!isDirChange && keepExtremes && extreme)
1798
+ ) {
1799
+
1800
+ pathDataN.push(com);
1801
+ }
1802
+
1803
+ // try simplification
1804
+ else {
1805
+
1806
+ let combined = combineCubicPairs(com, comN, {tolerance});
1807
+ let error = 0;
1808
+
1809
+ // combining successful! try next segment
1810
+ if (combined.length === 1) {
1811
+ com = combined[0];
1812
+ let offset = 1;
1813
+
1814
+ // add cumulative error to prevent distortions
1815
+ error += com.error;
1816
+
1817
+ // find next candidates
1818
+
1819
+ for (let n = i + 1; error < tolerance && n < l; n++) {
1820
+ let comN = pathData[n];
1821
+ if (comN.type !== 'C' ||
1822
+ (
1823
+ (keepInflections && comN.directionChange) ||
1824
+ (keepCorners && com.corner) ||
1825
+ (keepExtremes && com.extreme)
1826
+ )
1827
+ ) {
1828
+ break
1829
+ }
1830
+
1831
+ let combined = combineCubicPairs(com, comN, {tolerance});
1832
+ if (combined.length === 1) {
1833
+ // add cumulative error to prevent distortions
1834
+
1835
+ error += combined[0].error * 0.5;
1836
+
1837
+ offset++;
1838
+ }
1839
+ com = combined[0];
1840
+ }
1841
+
1842
+ pathDataN.push(com);
1843
+
1844
+ if (i < l) {
1845
+ i += offset;
1846
+ }
1847
+
1848
+ } else {
1849
+ pathDataN.push(com);
1850
+ }
1851
+ }
1852
+
1853
+ } // end of bezier command
1854
+
1855
+ // other commands
1856
+ else {
1857
+ pathDataN.push(com);
1858
+ }
1859
+
1860
+ } // end command loop
1861
+
1862
+ return pathDataN
1863
+ }
1864
+
1865
+ function combineCubicPairs(com1, com2, {
1866
+ tolerance = 1
1867
+ } = {}) {
1765
1868
 
1766
1869
  let commands = [com1, com2];
1870
+
1871
+ // assume 2 segments are result of a segment split
1767
1872
  let t = findSplitT(com1, com2);
1768
1873
 
1769
1874
  let distAv1 = getDistAv(com1.p0, com1.p);
1770
1875
  let distAv2 = getDistAv(com2.p0, com2.p);
1771
- let distMin = Math.min(distAv1, distAv2);
1876
+ let distMin = Math.max(0, Math.min(distAv1, distAv2));
1772
1877
 
1773
- let distScale = 0.05;
1878
+ let distScale = 0.06;
1774
1879
  let maxDist = distMin * distScale * tolerance;
1775
1880
 
1776
- let comS = getExtrapolatedCommand(com1, com2, t, t);
1881
+ // get hypothetical combined command
1882
+ let comS = getExtrapolatedCommand(com1, com2, t);
1777
1883
 
1778
- // test on path point against original
1884
+ // test new point-at-t against original mid segment starting point
1779
1885
  let pt = pointAtT([comS.p0, comS.cp1, comS.cp2, comS.p], t);
1780
1886
 
1781
1887
  let dist0 = getDistAv(com1.p, pt);
@@ -1786,12 +1892,6 @@
1786
1892
  // collect error data
1787
1893
  let error = dist0;
1788
1894
 
1789
- /*
1790
- if (com2.directionChange) {
1791
-
1792
- }
1793
- */
1794
-
1795
1895
  if (close) {
1796
1896
 
1797
1897
  /**
@@ -1837,29 +1937,9 @@
1837
1937
 
1838
1938
  } // end 1st try
1839
1939
 
1840
-
1841
-
1842
- // try extrapolated dominant curve
1843
-
1844
- // && !com1.extreme
1845
- if (extrapolateDominant && !success ) {
1846
-
1847
- let combinedEx = getCombinedByDominant(com1, com2, maxDist, tolerance);
1848
-
1849
- if(combinedEx.length===1){
1850
- success = true;
1851
- comS = combinedEx[0];
1852
- error = comS.error;
1853
-
1854
- }
1855
-
1856
-
1857
- }
1858
-
1859
1940
  // add meta
1860
1941
  if (success) {
1861
1942
 
1862
-
1863
1943
  // correct to exact start and end points
1864
1944
  comS.p0 = com1.p0;
1865
1945
  comS.p = com2.p;
@@ -1882,26 +1962,23 @@
1882
1962
  return commands;
1883
1963
  }
1884
1964
 
1885
- function getExtrapolatedCommand(com1, com2, t1 = 0, t2 = 0) {
1965
+ function getExtrapolatedCommand(com1, com2, t = 0) {
1886
1966
 
1887
1967
  let { p0, cp1 } = com1;
1888
1968
  let { p, cp2 } = com2;
1889
1969
 
1890
1970
  // extrapolate control points
1891
- let cp1_S = {
1892
- x: (cp1.x - (1 - t1) * p0.x) / t1,
1893
- y: (cp1.y - (1 - t1) * p0.y) / t1
1971
+ cp1 = {
1972
+ x: (cp1.x - (1 - t) * p0.x) / t,
1973
+ y: (cp1.y - (1 - t) * p0.y) / t
1894
1974
  };
1895
1975
 
1896
- let cp2_S = {
1897
- x: (cp2.x - t2 * p.x) / (1 - t2),
1898
- y: (cp2.y - t2 * p.y) / (1 - t2)
1976
+ cp2 = {
1977
+ x: (cp2.x - t * p.x) / (1 - t),
1978
+ y: (cp2.y - t * p.y) / (1 - t)
1899
1979
  };
1900
1980
 
1901
- let comS = { p0, cp1: cp1_S, cp2: cp2_S, p };
1902
-
1903
- return comS
1904
-
1981
+ return { p0, cp1, cp2, p };
1905
1982
  }
1906
1983
 
1907
1984
  function findSplitT(com1, com2) {
@@ -1914,52 +1991,40 @@
1914
1991
  return t
1915
1992
  }
1916
1993
 
1917
- function checkBezierFlatness(p0, cpts, p) {
1918
-
1919
- let isFlat = false;
1920
-
1921
- let isCubic = cpts.length === 2;
1922
-
1923
- let cp1 = cpts[0];
1924
- let cp2 = isCubic ? cpts[1] : cp1;
1925
-
1926
- if (p0.x === cp1.x && p0.y === cp1.y && p.x === cp2.x && p.y === cp2.y) return true;
1927
-
1928
- let dx1 = cp1.x - p0.x;
1929
- let dy1 = cp1.y - p0.y;
1930
-
1931
- let dx2 = p.x - cp2.x;
1932
- let dy2 = p.y - cp2.y;
1933
-
1934
- let cross1 = Math.abs(dx1 * dy2 - dy1 * dx2);
1994
+ function commandIsFlat(points, {
1995
+ tolerance = 1,
1996
+ debug=false
1997
+ } = {}) {
1935
1998
 
1936
- if (!cross1) return true
1999
+ let isFlat=false;
2000
+ let report = {
2001
+ flat:true,
2002
+ steepness:0
2003
+ };
1937
2004
 
1938
- let dx0 = p.x - p0.x;
1939
- let dy0 = p.y - p0.y;
1940
- let cross0 = Math.abs(dx0 * dy1 - dy0 * dx1);
2005
+ let p0 = points[0];
2006
+ let p = points[points.length - 1];
1941
2007
 
1942
- if (!cross0) return true
2008
+ let xSet = new Set([...points.map(pt => +pt.x.toFixed(8))]);
2009
+ let ySet = new Set([...points.map(pt => +pt.y.toFixed(8))]);
1943
2010
 
1944
- let area = getPolygonArea([p0,...cpts, p], true);
1945
- let dist1 = getSquareDistance(p0, p);
1946
- let thresh = dist1/200;
2011
+ // must be flat
2012
+ if(xSet.size===1 || ySet.size===1) return !debug ? true : report;
1947
2013
 
1948
- // if(area<thresh) return true;
1949
- isFlat = area<thresh;
2014
+ let squareDist = getSquareDistance(p0, p);
2015
+ let threshold = squareDist / 1000 * tolerance;
2016
+ let area = getPolygonArea(points, true);
1950
2017
 
1951
- /*
2018
+ // flat enough
2019
+ if(area < threshold) isFlat = true;
1952
2020
 
1953
- let rat = (cross0 / cross1)
2021
+ if(debug){
2022
+ report.flat = isFlat;
1954
2023
 
1955
- if (rat < 1.1) {
1956
- console.log('cross', cross0, cross1, 'rat', rat );
1957
- isFlat = true;
2024
+ report.steepness = area/squareDist*10;
1958
2025
  }
1959
- */
1960
-
1961
- return isFlat;
1962
2026
 
2027
+ return !debug ? isFlat : report;
1963
2028
  }
1964
2029
 
1965
2030
  function analyzePathData(pathData = []) {
@@ -2075,14 +2140,17 @@
2075
2140
  commandPts.push(p);
2076
2141
 
2077
2142
  /*
2143
+
2078
2144
  let commandFlatness = commandIsFlat(commandPts);
2079
2145
  isFlat = commandFlatness.flat;
2080
2146
  com.flat = isFlat;
2081
2147
 
2082
2148
  if (isFlat) {
2083
2149
  com.extreme = false;
2150
+
2084
2151
  }
2085
2152
  */
2153
+
2086
2154
  }
2087
2155
 
2088
2156
  /**
@@ -2192,7 +2260,7 @@
2192
2260
  let p = M;
2193
2261
  pathData[0].decimals = 0;
2194
2262
 
2195
- let dims = new Set();
2263
+ let dims = [];
2196
2264
 
2197
2265
  // add average distances
2198
2266
  for (let i = 0, len = pathData.length; i < len; i++) {
@@ -2205,7 +2273,7 @@
2205
2273
  // use existing averave dimension value or calculate
2206
2274
  let dimA = com.dimA ? +com.dimA.toFixed(8) : type !== 'M' ? +getDistAv(p0, p).toFixed(8) : 0;
2207
2275
 
2208
- if (dimA) dims.add(dimA);
2276
+ if (dimA) dims.push(dimA);
2209
2277
 
2210
2278
  if (type === 'M') {
2211
2279
  M = p;
@@ -2213,7 +2281,7 @@
2213
2281
  p0 = p;
2214
2282
  }
2215
2283
 
2216
- let dim_min = Array.from(dims).sort();
2284
+ let dim_min = dims.sort();
2217
2285
 
2218
2286
  /*
2219
2287
  let minVal = dim_min.length > 15 ?
@@ -2239,19 +2307,24 @@
2239
2307
  * based on suggested accuracy in path data
2240
2308
  */
2241
2309
  function roundPathData(pathData, decimals = -1) {
2242
- // has recommended decimals
2243
- let hasDecimal = decimals == 'auto' && pathData[0].hasOwnProperty('decimals') ? true : false;
2244
2310
 
2245
- for (let c = 0, len = pathData.length; c < len; c++) {
2246
- let com = pathData[c];
2311
+ let len = pathData.length;
2247
2312
 
2248
- if (decimals > -1 || hasDecimal) {
2249
- decimals = hasDecimal ? com.decimals : decimals;
2313
+ for (let c = 0; c < len; c++) {
2250
2314
 
2251
- pathData[c].values = com.values.map(val => { return val ? +val.toFixed(decimals) : val });
2315
+ let values = pathData[c].values;
2316
+ let valLen = values.length;
2252
2317
 
2318
+ if (valLen && (decimals > -1) ) {
2319
+
2320
+ for(let v=0; v<valLen; v++){
2321
+
2322
+ pathData[c].values[v] = +values[v].toFixed(decimals);
2323
+ }
2253
2324
  }
2254
- } return pathData;
2325
+ }
2326
+
2327
+ return pathData;
2255
2328
  }
2256
2329
 
2257
2330
  function revertCubicQuadratic(p0 = {}, cp1 = {}, cp2 = {}, p = {}) {
@@ -2269,7 +2342,7 @@
2269
2342
  let values = [cp1.x, cp1.y, cp2.x, cp2.y, p.x, p.y];
2270
2343
  let comN = {type, values};
2271
2344
 
2272
- if (dist1 < threshold) {
2345
+ if (dist1 && threshold && dist1 < threshold) {
2273
2346
  cp1_Q = checkLineIntersection(p0, cp1, p, cp2, false);
2274
2347
  if (cp1_Q) {
2275
2348
 
@@ -2294,9 +2367,10 @@
2294
2367
  if (toShorthands) pathData = pathDataToShorthands(pathData);
2295
2368
 
2296
2369
  // pre round - before relative conversion to minimize distortions
2297
- pathData = roundPathData(pathData, decimals);
2370
+ if(decimals>-1 && toRelative) pathData = roundPathData(pathData, decimals);
2298
2371
  if (toRelative) pathData = pathDataToRelative(pathData);
2299
2372
  if (decimals > -1) pathData = roundPathData(pathData, decimals);
2373
+
2300
2374
  return pathData
2301
2375
  }
2302
2376
 
@@ -2598,7 +2672,7 @@
2598
2672
  * L, L, C, Q => H, V, S, T
2599
2673
  * reversed method: pathDataToLonghands()
2600
2674
  */
2601
- function pathDataToShorthands(pathData, decimals = -1, test = true) {
2675
+ function pathDataToShorthands(pathData, decimals = -1, test = false) {
2602
2676
 
2603
2677
  /**
2604
2678
  * analyze pathdata – if you're sure your data is already absolute skip it via test=false
@@ -2609,29 +2683,28 @@
2609
2683
  hasRel = /[astvqmhlc]/g.test(commandTokens);
2610
2684
  }
2611
2685
 
2612
- pathData = test && hasRel ? pathDataToAbsolute(pathData, decimals) : pathData;
2686
+ pathData = test && hasRel ? pathDataToAbsoluteOrRelative(pathData) : pathData;
2687
+
2688
+ let len = pathData.length;
2689
+ let pathDataShorts = new Array(len);
2613
2690
 
2614
2691
  let comShort = {
2615
2692
  type: "M",
2616
2693
  values: pathData[0].values
2617
2694
  };
2618
2695
 
2619
- if (pathData[0].decimals) {
2620
-
2621
- comShort.decimals = pathData[0].decimals;
2622
- }
2623
-
2624
- let pathDataShorts = [comShort];
2696
+ pathDataShorts[0] = comShort;
2625
2697
 
2626
2698
  let p0 = { x: pathData[0].values[0], y: pathData[0].values[1] };
2627
2699
  let p;
2628
2700
  let tolerance = 0.01;
2629
2701
 
2630
- for (let i = 1, len = pathData.length; i < len; i++) {
2702
+ for (let i = 1; i < len; i++) {
2631
2703
 
2632
2704
  let com = pathData[i];
2633
2705
  let { type, values } = com;
2634
- let valuesLast = values.slice(-2);
2706
+ let valuesLen = values.length;
2707
+ let valuesLast = [values[valuesLen-2], values[valuesLen-1]];
2635
2708
 
2636
2709
  // previoius command
2637
2710
  let comPrev = pathData[i - 1];
@@ -2679,7 +2752,8 @@
2679
2752
  if (typePrev !== 'Q') {
2680
2753
 
2681
2754
  p0 = { x: valuesLast[0], y: valuesLast[1] };
2682
- pathDataShorts.push(com);
2755
+
2756
+ pathDataShorts[i] = com;
2683
2757
  continue;
2684
2758
  }
2685
2759
 
@@ -2708,7 +2782,8 @@
2708
2782
 
2709
2783
  if (typePrev !== 'C') {
2710
2784
 
2711
- pathDataShorts.push(com);
2785
+ pathDataShorts[i] = com;
2786
+
2712
2787
  p0 = { x: valuesLast[0], y: valuesLast[1] };
2713
2788
  continue;
2714
2789
  }
@@ -2750,8 +2825,10 @@
2750
2825
  }
2751
2826
 
2752
2827
  p0 = { x: valuesLast[0], y: valuesLast[1] };
2753
- pathDataShorts.push(comShort);
2828
+ pathDataShorts[i] = comShort;
2829
+
2754
2830
  }
2831
+
2755
2832
  return pathDataShorts;
2756
2833
  }
2757
2834
 
@@ -3196,10 +3273,10 @@
3196
3273
  quadraticToCubic = false,
3197
3274
  arcToCubic = false,
3198
3275
  arcAccuracy = 2,
3199
- } = {},
3200
3276
 
3201
- {
3277
+ // assume we need full normalization
3202
3278
  hasRelatives = true, hasShorthands = true, hasQuadratics = true, hasArcs = true, testTypes = false
3279
+
3203
3280
  } = {}
3204
3281
  ) {
3205
3282
 
@@ -3250,15 +3327,28 @@
3250
3327
  } = {}
3251
3328
  ) {
3252
3329
 
3253
- let pathDataObj = parsePathDataString(d);
3254
- let { hasRelatives, hasShorthands, hasQuadratics, hasArcs } = pathDataObj;
3255
- let pathData = pathDataObj.pathData;
3330
+ // is already array
3331
+ let isArray = Array.isArray(d);
3332
+
3333
+ // normalize native pathData to regular array
3334
+ let hasConstructor = isArray && typeof d[0] === 'object' && typeof d[0].constructor === 'function';
3335
+ /*
3336
+ if (hasConstructor) {
3337
+ d = d.map(com => { return { type: com.type, values: com.values } })
3338
+ console.log('hasConstructor', hasConstructor, (typeof d[0].constructor), d);
3339
+ }
3340
+ */
3341
+
3342
+ let pathDataObj = isArray ? d : parsePathDataString(d);
3343
+
3344
+ let { hasRelatives = true, hasShorthands = true, hasQuadratics = true, hasArcs = true } = pathDataObj;
3345
+ let pathData = hasConstructor ? pathDataObj : pathDataObj.pathData;
3256
3346
 
3257
3347
  // normalize
3258
3348
  pathData = normalizePathData(pathData,
3259
- { toAbsolute, toLonghands, quadraticToCubic, arcToCubic, arcAccuracy },
3260
-
3261
- { hasRelatives, hasShorthands, hasQuadratics, hasArcs }
3349
+ { toAbsolute, toLonghands, quadraticToCubic, arcToCubic, arcAccuracy,
3350
+ hasRelatives, hasShorthands, hasQuadratics, hasArcs
3351
+ },
3262
3352
  );
3263
3353
 
3264
3354
  return pathData;
@@ -3610,7 +3700,8 @@
3610
3700
  if (debug === 'log') {
3611
3701
  console.log(feedback);
3612
3702
  } else {
3613
- throw new Error(feedback)
3703
+
3704
+ console.warn(feedback);
3614
3705
  }
3615
3706
  }
3616
3707
 
@@ -3642,22 +3733,22 @@
3642
3733
  return pathData.map(com => { return `${com.type} ${com.values.join(' ')}` }).join(' ');
3643
3734
  }
3644
3735
 
3645
- function shapeElToPath(el){
3736
+ function shapeElToPath(el) {
3646
3737
 
3647
3738
  let nodeName = el.nodeName.toLowerCase();
3648
- if(nodeName==='path')return el;
3739
+ if (nodeName === 'path') return el;
3649
3740
 
3650
3741
  let pathData = getPathDataFromEl(el);
3651
- let d = pathData.map(com=>{return `${com.type} ${com.values} `}).join(' ');
3652
- let attributes = [...el.attributes].map(att=>att.name);
3742
+ let d = pathData.map(com => { return `${com.type} ${com.values} ` }).join(' ');
3743
+ let attributes = [...el.attributes].map(att => att.name);
3653
3744
 
3654
3745
  let pathN = document.createElementNS('http://www.w3.org/2000/svg', 'path');
3655
- pathN.setAttribute('d', d );
3746
+ pathN.setAttribute('d', d);
3656
3747
 
3657
3748
  let exclude = ['x', 'y', 'cx', 'cy', 'dx', 'dy', 'r', 'rx', 'ry', 'width', 'height', 'points'];
3658
3749
 
3659
- attributes.forEach(att=>{
3660
- if(!exclude.includes(att)){
3750
+ attributes.forEach(att => {
3751
+ if (!exclude.includes(att)) {
3661
3752
  let val = el.getAttribute(att);
3662
3753
  pathN.setAttribute(att, val);
3663
3754
  }
@@ -3668,7 +3759,7 @@
3668
3759
  }
3669
3760
 
3670
3761
  // retrieve pathdata from svg geometry elements
3671
- function getPathDataFromEl(el, stringify=false) {
3762
+ function getPathDataFromEl(el, stringify = false) {
3672
3763
 
3673
3764
  let pathData = [];
3674
3765
  let type = el.nodeName;
@@ -3826,7 +3917,9 @@
3826
3917
  attNames = ['cx', 'cy', 'rx', 'ry', 'r'];
3827
3918
  ({ cx, cy, r, rx, ry } = getAtts(attNames));
3828
3919
 
3829
- if (type === 'circle') {
3920
+ let isCircle = type === 'circle';
3921
+
3922
+ if (isCircle) {
3830
3923
  r = r;
3831
3924
  rx = r;
3832
3925
  ry = r;
@@ -3835,10 +3928,14 @@
3835
3928
  ry = ry ? ry : r;
3836
3929
  }
3837
3930
 
3931
+ // simplified radii for cirecles
3932
+ let rxS = isCircle && r>=1 ? 1 : rx;
3933
+ let ryS = isCircle && r>=1 ? 1 : rx;
3934
+
3838
3935
  pathData = [
3839
3936
  { type: "M", values: [cx + rx, cy] },
3840
- { type: "A", values: [rx, ry, 0, 1, 1, cx - rx, cy] },
3841
- { type: "A", values: [rx, ry, 0, 1, 1, cx + rx, cy] },
3937
+ { type: "A", values: [rxS, ryS, 0, 1, 1, cx - rx, cy] },
3938
+ { type: "A", values: [rxS, ryS, 0, 1, 1, cx + rx, cy] },
3842
3939
  ];
3843
3940
 
3844
3941
  break;
@@ -3870,22 +3967,28 @@
3870
3967
  break;
3871
3968
  }
3872
3969
 
3873
- return stringify ? stringifyPathData(pathData): pathData;
3970
+ return stringify ? stringifyPathData(pathData) : pathData;
3874
3971
 
3875
3972
  }
3876
3973
 
3877
- function pathDataRemoveColinear(pathData, tolerance = 1, flatBezierToLinetos = true) {
3974
+ function pathDataRemoveColinear(pathData, {
3975
+ tolerance = 1,
3976
+
3977
+ flatBezierToLinetos = true
3978
+ }={}) {
3878
3979
 
3879
3980
  let pathDataN = [pathData[0]];
3981
+
3880
3982
  let M = { x: pathData[0].values[0], y: pathData[0].values[1] };
3881
3983
  let p0 = M;
3882
3984
  let p = M;
3883
3985
  pathData[pathData.length - 1].type.toLowerCase() === 'z';
3884
3986
 
3885
3987
  for (let c = 1, l = pathData.length; c < l; c++) {
3886
- let comPrev = pathData[c - 1];
3988
+
3887
3989
  let com = pathData[c];
3888
3990
  let comN = pathData[c + 1] || pathData[l - 1];
3991
+
3889
3992
  let p1 = comN.type.toLowerCase() === 'z' ? M : { x: comN.values[comN.values.length - 2], y: comN.values[comN.values.length - 1] };
3890
3993
 
3891
3994
  let { type, values } = com;
@@ -3894,11 +3997,9 @@
3894
3997
 
3895
3998
  let area = getPolygonArea([p0, p, p1], true);
3896
3999
 
3897
- getSquareDistance(p0, p);
3898
- getSquareDistance(p, p1);
3899
4000
  let distSquare = getSquareDistance(p0, p1);
3900
4001
 
3901
- let distMax = distSquare / 200 * tolerance;
4002
+ let distMax = distSquare ? distSquare / 333 * tolerance : 0;
3902
4003
 
3903
4004
  let isFlat = area < distMax;
3904
4005
  let isFlatBez = false;
@@ -3912,31 +4013,38 @@
3912
4013
  [{ x: values[0], y: values[1] }, { x: values[2], y: values[3] }] :
3913
4014
  (type === 'Q' ? [{ x: values[0], y: values[1] }] : []);
3914
4015
 
3915
- isFlatBez = checkBezierFlatness(p0, cpts, p);
3916
-
3917
- // console.log();
4016
+ isFlatBez = commandIsFlat([p0, ...cpts, p],{tolerance});
3918
4017
 
3919
- if (isFlatBez && c < l - 1 && comPrev.type !== 'C') {
4018
+ if (isFlatBez && c < l - 1 ) {
3920
4019
  type = "L";
3921
4020
  com.type = "L";
3922
4021
  com.values = valsL;
3923
4022
 
3924
4023
  }
3925
-
3926
4024
  }
3927
4025
 
3928
- // update end point
3929
- p0 = p;
3930
-
3931
4026
  // colinear – exclude arcs (as always =) as semicircles won't have an area
3932
4027
 
3933
4028
  if ( isFlat && c < l - 1 && (type === 'L' || (flatBezierToLinetos && isFlatBez)) ) {
3934
-
3935
4029
 
4030
+ /*
4031
+ console.log(area, distMax );
4032
+
4033
+ if(p0.x === p.x && p0.y === p.y){
4034
+
4035
+ }
4036
+
4037
+ renderPoint(markers, p0, 'blue', '1.5%', '1')
4038
+ renderPoint(markers, p, 'red', '1%', '1')
4039
+ renderPoint(markers, p1, 'cyan', '0.5%', '1')
4040
+ */
3936
4041
 
3937
4042
  continue;
3938
4043
  }
3939
4044
 
4045
+ // update end point
4046
+ p0 = p;
4047
+
3940
4048
  if (type === 'M') {
3941
4049
  M = p;
3942
4050
  p0 = M;
@@ -3957,6 +4065,7 @@
3957
4065
 
3958
4066
  function removeOrphanedM(pathData) {
3959
4067
 
4068
+ let pathDataN = [];
3960
4069
  for (let i = 0, l = pathData.length; i < l; i++) {
3961
4070
  let com = pathData[i];
3962
4071
  if (!com) continue;
@@ -3965,14 +4074,14 @@
3965
4074
  if ((type === 'M' || type === 'm')) {
3966
4075
 
3967
4076
  if (!comN || (comN && (comN.type === 'Z' || comN.type === 'z'))) {
3968
- pathData[i] = null;
3969
- pathData[i + 1] = null;
4077
+ if(comN) i++;
4078
+ continue
3970
4079
  }
3971
4080
  }
4081
+ pathDataN.push(com);
3972
4082
  }
3973
4083
 
3974
- pathData = pathData.filter(Boolean);
3975
- return pathData;
4084
+ return pathDataN;
3976
4085
 
3977
4086
  }
3978
4087
 
@@ -4003,19 +4112,23 @@
4003
4112
 
4004
4113
  for (let c = 1, l = pathData.length; c < l; c++) {
4005
4114
  let com = pathData[c];
4115
+ let comPrev = pathData[c-1];
4116
+ let comNext = pathData[c+1] || null;
4006
4117
  let { type, values } = com;
4007
4118
 
4008
- let valsLen = values.length;
4119
+ // zero length segments are simetimes used in icons for dots
4120
+ let isDot = comPrev.type.toLowerCase() ==='m' && !comNext;
4009
4121
 
4122
+ let valsLen = values.length;
4010
4123
  p = { x: values[valsLen-2], y: values[valsLen-1] };
4011
4124
 
4012
4125
  // skip lineto
4013
- if (type === 'L' && p.x === p0.x && p.y === p0.y) {
4126
+ if (!isDot && type === 'L' && p.x === p0.x && p.y === p0.y) {
4014
4127
  continue
4015
4128
  }
4016
4129
 
4017
4130
  // skip minified zero length
4018
- if (type === 'l' || type === 'v' || type === 'h') {
4131
+ if (!isDot && (type === 'l' || type === 'v' || type === 'h')) {
4019
4132
  let noLength = type === 'l' ? (values.join('') === '00') : values[0] === 0;
4020
4133
  if(noLength) continue
4021
4134
  }
@@ -4117,7 +4230,7 @@
4117
4230
  }
4118
4231
  // use top most command
4119
4232
  else {
4120
- indices = indices.sort((a, b) => +a.y.toFixed(1) - +b.y.toFixed(1) || a.x - b.x);
4233
+ indices = indices.sort((a, b) => +a.y.toFixed(8) - +b.y.toFixed(8) || a.x - b.x);
4121
4234
  newIndex = indices[0].index;
4122
4235
  }
4123
4236
 
@@ -4125,7 +4238,7 @@
4125
4238
  pathData = newIndex ? shiftSvgStartingPoint(pathData, newIndex) : pathData;
4126
4239
  }
4127
4240
 
4128
- M = { x: +pathData[0].values[0].toFixed(8), y: +pathData[0].values[1].toFixed(7) };
4241
+ M = { x: +pathData[0].values[0].toFixed(8), y: +pathData[0].values[1].toFixed(8) };
4129
4242
 
4130
4243
  len = pathData.length;
4131
4244
 
@@ -4239,149 +4352,61 @@
4239
4352
  return pathData;
4240
4353
  }
4241
4354
 
4242
- /**
4243
- * reverse pathdata
4244
- * make sure all command coordinates are absolute and
4245
- * shorthands are converted to long notation
4246
- */
4247
- function reversePathData(pathData, {
4248
- arcToCubic = false,
4249
- quadraticToCubic = false,
4250
- toClockwise = false,
4251
- returnD = false
4355
+ function refineAdjacentExtremes(pathData, {
4356
+ threshold = null, tolerance = 1
4252
4357
  } = {}) {
4253
4358
 
4254
- /**
4255
- * Add closing lineto:
4256
- * needed for path reversing or adding points
4257
- */
4258
- const addClosePathLineto = (pathData) => {
4259
- let closed = pathData[pathData.length - 1].type.toLowerCase() === "z";
4260
- let M = pathData[0];
4261
- let [x0, y0] = [M.values[0], M.values[1]];
4262
- let lastCom = closed ? pathData[pathData.length - 2] : pathData[pathData.length - 1];
4263
- let [xE, yE] = [lastCom.values[lastCom.values.length - 2], lastCom.values[lastCom.values.length - 1]];
4264
-
4265
- if (closed && (x0 != xE || y0 != yE)) {
4266
-
4267
- pathData.pop();
4268
- pathData.push(
4269
- {
4270
- type: "L",
4271
- values: [x0, y0]
4272
- },
4273
- {
4274
- type: "Z",
4275
- values: []
4276
- }
4277
- );
4278
- }
4279
- return pathData;
4280
- };
4281
-
4282
- // helper to rearrange control points for all command types
4283
- const reverseControlPoints = (type, values) => {
4284
- let controlPoints = [];
4285
- let endPoints = [];
4286
- if (type !== "A") {
4287
- for (let p = 0; p < values.length; p += 2) {
4288
- controlPoints.push([values[p], values[p + 1]]);
4289
- }
4290
- endPoints = controlPoints.pop();
4291
- controlPoints.reverse();
4292
- }
4293
- // is arc
4294
- else {
4295
-
4296
- let sweep = values[4] == 0 ? 1 : 0;
4297
- controlPoints = [values[0], values[1], values[2], values[3], sweep];
4298
- endPoints = [values[5], values[6]];
4299
- }
4300
- return { controlPoints, endPoints };
4301
- };
4302
-
4303
- // start compiling new path data
4304
- let pathDataNew = [];
4359
+ if (!threshold) {
4360
+ let bb = getPathDataBBox(pathData);
4361
+ threshold = (bb.width + bb.height) / 2 * 0.05;
4305
4362
 
4306
- let closed =
4307
- pathData[pathData.length - 1].type.toLowerCase() === "z" ? true : false;
4308
- if (closed) {
4309
- // add lineto closing space between Z and M
4310
- pathData = addClosePathLineto(pathData);
4311
- // remove Z closepath
4312
- pathData.pop();
4313
4363
  }
4314
4364
 
4315
- // define last point as new M if path isn't closed
4316
- let valuesLast = pathData[pathData.length - 1].values;
4317
- let valuesLastL = valuesLast.length;
4318
- let M = closed
4319
- ? pathData[0]
4320
- : {
4321
- type: "M",
4322
- values: [valuesLast[valuesLastL - 2], valuesLast[valuesLastL - 1]]
4323
- };
4324
- // starting M stays the same – unless the path is not closed
4325
- pathDataNew.push(M);
4365
+ let l = pathData.length;
4326
4366
 
4327
- // reverse path data command order for processing
4328
- pathData.reverse();
4329
- for (let i = 1; i < pathData.length; i++) {
4367
+ for (let i = 0; i < l; i++) {
4330
4368
  let com = pathData[i];
4331
- let type = com.type;
4332
- let values = com.values;
4333
- let comPrev = pathData[i - 1];
4334
- let typePrev = comPrev.type;
4335
- let valuesPrev = comPrev.values;
4369
+ let { type, values, extreme, corner = false, dimA, p0, p } = com;
4370
+ let comN = pathData[i + 1] ? pathData[i + 1] : null;
4371
+ let comN2 = pathData[i + 2] ? pathData[i + 2] : null;
4336
4372
 
4337
- // get reversed control points and new end coordinates
4338
- let controlPointsPrev = reverseControlPoints(typePrev, valuesPrev).controlPoints;
4339
- let endPoints = reverseControlPoints(type, values).endPoints;
4373
+ // check dist
4374
+ let diff = comN ? getDistAv(p, comN.p) : Infinity;
4375
+ let isCose = diff < threshold;
4340
4376
 
4341
- // create new path data
4342
- let newValues = [];
4343
- newValues = [controlPointsPrev, endPoints].flat();
4344
- pathDataNew.push({
4345
- type: typePrev,
4346
- values: newValues.flat()
4347
- });
4348
- }
4377
+ let diff2 = comN2 ? getDistAv(comN2.p, comN.p) : Infinity;
4378
+ let isCose2 = diff2 < threshold;
4349
4379
 
4350
- // add previously removed Z close path
4351
- if (closed) {
4352
- pathDataNew.push({
4353
- type: "z",
4354
- values: []
4355
- });
4356
- }
4380
+ // next is extreme
4381
+ if (comN && type === 'C' && comN.type === 'C' && extreme && comN2 && comN2.extreme) {
4357
4382
 
4358
- return pathDataNew;
4359
- }
4383
+ if (isCose2 || isCose) {
4360
4384
 
4361
- function refineAdjacentExtremes(pathData, {
4362
- threshold = null, tolerance = 1
4363
- } = {}) {
4385
+ // extrapolate
4386
+ let comEx = getCombinedByDominant(comN, comN2, threshold, tolerance, false);
4364
4387
 
4365
- if (!threshold) {
4366
- let bb = getPathDataBBox(pathData);
4367
- threshold = (bb.width + bb.height) / 2 * 0.05;
4388
+ if (comEx.length === 1) {
4368
4389
 
4369
- }
4390
+ pathData[i + 1] = null;
4391
+ comEx = comEx[0];
4370
4392
 
4371
- let l = pathData.length;
4393
+ pathData[i + 2].values = [comEx.cp1.x, comEx.cp1.y, comEx.cp2.x, comEx.cp2.y, comEx.p.x, comEx.p.y];
4394
+ pathData[i + 2].cp1 = comEx.cp1;
4395
+ pathData[i + 2].cp2 = comEx.cp2;
4396
+ pathData[i + 2].p0 = comEx.p0;
4397
+ pathData[i + 2].p = comEx.p;
4398
+ pathData[i + 2].extreme = comEx.extreme;
4372
4399
 
4373
- for (let i = 0; i < l; i++) {
4374
- let com = pathData[i];
4375
- let { type, values, extreme, corner=false, dimA, p0, p } = com;
4376
- let comN = pathData[i + 1] ? pathData[i + 1] : null;
4400
+ i++;
4401
+ continue
4402
+ }
4403
+ }
4377
4404
 
4378
- // adjacent
4405
+ }
4379
4406
 
4380
- if (comN && type === 'C' && comN.type === 'C' && extreme && !corner) {
4407
+ // short after extreme
4381
4408
 
4382
- // check dist
4383
- let diff = getDistAv(p, comN.p);
4384
- let isCose = diff < threshold;
4409
+ if (comN && type === 'C' && comN.type === 'C' && extreme ) {
4385
4410
 
4386
4411
  if (isCose) {
4387
4412
 
@@ -4394,11 +4419,23 @@
4394
4419
  let ptI;
4395
4420
  let t = 1;
4396
4421
 
4422
+ let area0 = getPolygonArea([com.p0, com.p , comN.p]);
4423
+ // cpts area
4424
+ let area1 = getPolygonArea([com.p0, com.cp1, com.cp2, com.p]);
4425
+
4426
+ // sign change: is corner => skip
4427
+ if ( (area0<0 && area1>0) || (area0>0 && area1<0)) {
4428
+
4429
+ continue;
4430
+ }
4431
+
4432
+
4397
4433
  if (comN.extreme) {
4398
4434
 
4399
4435
  // extend cp2
4400
4436
  if (horizontal) {
4401
4437
  t = Math.abs(Math.abs(comN.cp2.x - comN.p.x) / Math.abs(com.cp2.x - com.p.x));
4438
+ t = Math.min(1, t);
4402
4439
 
4403
4440
  ptI = interpolate(comN.p, com.cp2, 1 + t);
4404
4441
  com.cp2.x = ptI.x;
@@ -4407,6 +4444,8 @@
4407
4444
  else {
4408
4445
 
4409
4446
  t = Math.abs(Math.abs(comN.cp2.y - comN.p.y) / Math.abs(com.cp2.y - com.p.y));
4447
+ t = Math.min(1, t);
4448
+
4410
4449
  ptI = interpolate(comN.p, com.cp2, 1 + t);
4411
4450
  com.cp2.y = ptI.y;
4412
4451
  }
@@ -4424,35 +4463,12 @@
4424
4463
 
4425
4464
  }
4426
4465
 
4427
- // extend fist command
4428
- else {
4429
-
4430
- let comN2 = pathData[i + 2] ? pathData[i + 2] : null;
4431
- if (!comN2 && comN2.type !== 'C') continue
4432
-
4433
- // extrapolate
4434
- let comEx = getCombinedByDominant(comN, comN2, threshold, tolerance, false);
4435
-
4436
- if (comEx.length === 1) {
4437
- pathData[i + 1] = null;
4438
-
4439
- comEx = comEx[0];
4440
-
4441
- pathData[i + 2].values = [comEx.cp1.x, comEx.cp1.y, comEx.cp2.x, comEx.cp2.y, comEx.p.x, comEx.p.y];
4442
- pathData[i + 2].cp1 = comEx.cp1;
4443
- pathData[i + 2].cp2 = comEx.cp2;
4444
- pathData[i + 2].p0 = comEx.p0;
4445
- pathData[i + 2].p = comEx.p;
4446
- pathData[i + 2].extreme = comEx.extreme;
4447
-
4448
- i++;
4449
- continue
4450
- }
4451
-
4452
- }
4453
-
4454
4466
  }
4455
4467
  }
4468
+
4469
+ /*
4470
+ */
4471
+
4456
4472
  }
4457
4473
 
4458
4474
  // remove commands
@@ -4479,11 +4495,7 @@
4479
4495
 
4480
4496
  if (penultimateCom && penultimateCom.type === 'C' && isCose && isClosingTo && fistExt) {
4481
4497
 
4482
- Math.abs(fistExt.cp1.x - M.x);
4483
- Math.abs(fistExt.cp1.y - M.y);
4484
-
4485
4498
  let comEx = getCombinedByDominant(penultimateCom, lastCom, threshold, tolerance, false);
4486
- console.log('comEx', comEx);
4487
4499
 
4488
4500
  if (comEx.length === 1) {
4489
4501
  pathData[lastIdx - 1] = comEx[0];
@@ -4519,7 +4531,7 @@
4519
4531
  .querySelector("svg");
4520
4532
 
4521
4533
 
4522
- let allowed=['viewBox', 'xmlns', 'width', 'height', 'id', 'class', 'fill', 'stroke', 'stroke-width'];
4534
+ let allowed=['viewBox', 'xmlns', 'width', 'height', 'id', 'class', 'fill', 'stroke', 'stroke-width', 'stroke-linecap', 'stroke-linejoin'];
4523
4535
  removeExcludedAttribues(svg, allowed);
4524
4536
 
4525
4537
  let removeEls = ['metadata', 'script'];
@@ -4590,6 +4602,156 @@
4590
4602
  return markup
4591
4603
  }
4592
4604
 
4605
+ function refineRoundedCorners(pathData, {
4606
+ threshold = 0,
4607
+ tolerance = 1
4608
+ } = {}) {
4609
+
4610
+ let l = pathData.length;
4611
+
4612
+ // add fist command
4613
+ let pathDataN = [pathData[0]];
4614
+
4615
+ let isClosed = pathData[l - 1].type.toLowerCase() === 'z';
4616
+ let lastOff = isClosed ? 2 : 1;
4617
+
4618
+ let comLast = pathData[l - lastOff];
4619
+ let lastIsLine = comLast.type === 'L';
4620
+ let lastIsBez = comLast.type === 'C';
4621
+ let firstIsLine = pathData[1].type === 'L';
4622
+ let firstIsBez = pathData[1].type === 'C';
4623
+
4624
+ let normalizeClose = isClosed && firstIsBez;
4625
+
4626
+ // normalize closepath to lineto
4627
+ if (normalizeClose) {
4628
+ pathData[l - 1].values = pathData[0].values;
4629
+ pathData[l - 1].type = 'L';
4630
+ lastIsLine = true;
4631
+ }
4632
+
4633
+ for (let i = 1; i < l; i++) {
4634
+ let com = pathData[i];
4635
+ let { type } = com;
4636
+ let comN = pathData[i + 1] ? pathData[i + 1] : null;
4637
+
4638
+ // search small cubic segments enclosed by linetos
4639
+ if ((type === 'L' && comN && comN.type === 'C') ||
4640
+ (type === 'C' && comN && comN.type === 'L')
4641
+
4642
+ ) {
4643
+ let comL0 = com;
4644
+ let comL1 = null;
4645
+ let comBez = [];
4646
+ let offset = 0;
4647
+
4648
+ // start to end
4649
+ if (i === 1 && firstIsBez && lastIsLine) {
4650
+ comBez = [pathData[1]];
4651
+ comL0 = pathData[l - 1];
4652
+ comL1 = comN;
4653
+
4654
+ }
4655
+
4656
+ // closing corner to start
4657
+ if (isClosed && lastIsBez && firstIsLine && i === l - lastOff - 1) {
4658
+ comL1 = pathData[1];
4659
+ comBez = [pathData[l - lastOff]];
4660
+
4661
+ }
4662
+
4663
+ for (let j = i + 1; j < l; j++) {
4664
+ let comN = pathData[j] ? pathData[j] : null;
4665
+ let comPrev = pathData[j - 1];
4666
+
4667
+ if (comPrev.type === 'C') {
4668
+ comBez.push(comPrev);
4669
+ }
4670
+
4671
+ if (comN.type === 'L' && comPrev.type === 'C') {
4672
+ comL1 = comN;
4673
+ break;
4674
+ }
4675
+ offset++;
4676
+ }
4677
+
4678
+ if (comL1) {
4679
+
4680
+ // linetos
4681
+ let len1 = getDistAv(comL0.p0, comL0.p);
4682
+ let len2 = getDistAv(comL1.p0, comL1.p);
4683
+
4684
+ // bezier
4685
+
4686
+ let comBezLen = comBez.length;
4687
+ let len3 = getDistAv(comBez[0].p0, comBez[comBezLen - 1].p);
4688
+
4689
+ // check concaveness by area sign change
4690
+ let area1 = getPolygonArea([comL0.p0, comL0.p, comL1.p0, comL1.p], false);
4691
+ let area2 = getPolygonArea([comBez[0].p0, comBez[0].cp1, comBez[0].cp2, comBez[0].p], false);
4692
+
4693
+ let signChange = (area1 < 0 && area2 > 0) || (area1 > 0 && area2 < 0);
4694
+
4695
+ if (comBez && !signChange && len3 < threshold && len1 > len3 && len2 > len3) {
4696
+
4697
+ let ptQ = checkLineIntersection(comL0.p0, comL0.p, comL1.p0, comL1.p, false);
4698
+ if (ptQ) {
4699
+
4700
+ /*
4701
+ let dist1 = getDistAv(ptQ, comL0.p)
4702
+ let dist2 = getDistAv(ptQ, comL1.p0)
4703
+ let diff = Math.abs(dist1-dist2)
4704
+ let rat = diff/Math.max(dist1, dist2)
4705
+ console.log('rat', rat);
4706
+ */
4707
+
4708
+ /*
4709
+ // adjust curve start and end to meet original
4710
+ let t = 1
4711
+
4712
+ let p0_2 = pointAtT([ptQ, comL0.p], t)
4713
+
4714
+ comL0.p = p0_2
4715
+ comL0.values = [p0_2.x, p0_2.y]
4716
+
4717
+ let p_2 = pointAtT([ptQ, comL1.p0], t)
4718
+
4719
+ comL1.p0 = p_2
4720
+
4721
+ */
4722
+
4723
+ let comQ = { type: 'Q', values: [ptQ.x, ptQ.y, comL1.p0.x, comL1.p0.y] };
4724
+ comQ.p0 = comL0.p;
4725
+ comQ.cp1 = ptQ;
4726
+ comQ.p = comL1.p0;
4727
+
4728
+ // add quadratic command
4729
+ pathDataN.push(comL0, comQ);
4730
+ i += offset;
4731
+ continue;
4732
+ }
4733
+ }
4734
+ }
4735
+ }
4736
+
4737
+ // skip last lineto
4738
+ if (normalizeClose && i === l - 1 && type === 'L') {
4739
+ continue
4740
+ }
4741
+
4742
+ pathDataN.push(com);
4743
+
4744
+ }
4745
+
4746
+ // revert close path normalization
4747
+ if (normalizeClose) {
4748
+ pathDataN.push({ type: 'Z', values: [] });
4749
+ }
4750
+
4751
+ return pathDataN;
4752
+
4753
+ }
4754
+
4593
4755
  function svgPathSimplify(input = '', {
4594
4756
 
4595
4757
  // return svg markup or object
@@ -4608,11 +4770,14 @@
4608
4770
 
4609
4771
  simplifyBezier = true,
4610
4772
  optimizeOrder = true,
4773
+ removeZeroLength = true,
4611
4774
  removeColinear = true,
4612
4775
  flatBezierToLinetos = true,
4613
4776
  revertToQuadratics = true,
4614
4777
 
4615
4778
  refineExtremes = true,
4779
+ refineCorners = false,
4780
+
4616
4781
  keepExtremes = true,
4617
4782
  keepCorners = true,
4618
4783
  extrapolateDominant = true,
@@ -4657,27 +4822,38 @@
4657
4822
  */
4658
4823
 
4659
4824
  // original size
4660
- svgSize = new Blob([input]).size;
4661
4825
 
4662
- // single path
4826
+ svgSize = input.length;
4827
+
4828
+ // mode:0 – single path
4663
4829
  if (!mode) {
4664
4830
  if (inputType === 'pathDataString') {
4665
4831
  d = input;
4666
4832
  } else if (inputType === 'polyString') {
4667
4833
  d = 'M' + input;
4668
4834
  }
4835
+ else if (inputType === 'pathData') {
4836
+ d = input;
4837
+
4838
+ // stringify to compare lengths
4839
+
4840
+ let dStr = d.map(com=>{return `${com.type} ${com.values.join(' ')}`}).join(' ') ;
4841
+ svgSize = dStr.length;
4842
+
4843
+ }
4844
+
4669
4845
  paths.push({ d, el: null });
4670
4846
  }
4671
- // process svg
4847
+ // mode:1 – process complete svg DOM
4672
4848
  else {
4673
4849
 
4674
4850
  let returnDom = true;
4675
4851
  svg = cleanUpSVG(input, { returnDom, removeHidden, removeUnused }
4676
4852
  );
4677
4853
 
4678
- if(shapesToPaths){
4854
+ if (shapesToPaths) {
4679
4855
  let shapes = svg.querySelectorAll('polygon, polyline, line, rect, circle, ellipse');
4680
- shapes.forEach(shape=>{
4856
+ shapes.forEach(shape => {
4681
4857
  let path = shapeElToPath(shape);
4682
4858
  shape.replaceWith(path);
4683
4859
  });
@@ -4692,6 +4868,7 @@
4692
4868
 
4693
4869
  /**
4694
4870
  * process all paths
4871
+ * try simplifications and removals
4695
4872
  */
4696
4873
 
4697
4874
  // SVG optimization options
@@ -4704,37 +4881,35 @@
4704
4881
  // combinded path data for SVGs with mergePaths enabled
4705
4882
  let pathData_merged = [];
4706
4883
 
4707
- paths.forEach(path => {
4884
+ for (let i = 0, l = paths.length; l && i < l; i++) {
4885
+
4886
+ let path = paths[i];
4708
4887
  let { d, el } = path;
4709
4888
 
4710
- let pathDataO = parsePathDataNormalized(d, { quadraticToCubic, toAbsolute, arcToCubic });
4889
+ let pathData = parsePathDataNormalized(d, { quadraticToCubic, toAbsolute, arcToCubic });
4711
4890
 
4712
4891
  // count commands for evaluation
4713
- let comCount = pathDataO.length;
4714
-
4715
- // create clone for fallback
4716
-
4717
- let pathData = pathDataO;
4892
+ let comCount = pathData.length;
4718
4893
 
4719
- if(removeOrphanSubpaths) pathData = removeOrphanedM(pathData);
4894
+ if (removeOrphanSubpaths) pathData = removeOrphanedM(pathData);
4720
4895
 
4721
4896
  /**
4722
4897
  * get sub paths
4723
4898
  */
4724
4899
  let subPathArr = splitSubpaths(pathData);
4900
+ let lenSub = subPathArr.length;
4725
4901
 
4726
4902
  // cleaned up pathData
4727
- let pathDataArrN = [];
4728
4903
 
4729
- for (let i = 0, l = subPathArr.length; i < l; i++) {
4904
+ // reset array
4905
+ let pathDataFlat = [];
4730
4906
 
4731
- let pathDataSub = subPathArr[i];
4907
+ for (let i = 0; i < lenSub; i++) {
4732
4908
 
4733
- // try simplification in reversed order
4734
- if (reverse) pathDataSub = reversePathData(pathDataSub);
4909
+ let pathDataSub = subPathArr[i];
4735
4910
 
4736
4911
  // remove zero length linetos
4737
- if (removeColinear) pathDataSub = removeZeroLengthLinetos(pathDataSub);
4912
+ if (removeColinear || removeZeroLength) pathDataSub = removeZeroLengthLinetos(pathDataSub);
4738
4913
 
4739
4914
  // add extremes
4740
4915
 
@@ -4744,56 +4919,63 @@
4744
4919
  // sort to top left
4745
4920
  if (optimizeOrder) pathDataSub = pathDataToTopLeft(pathDataSub);
4746
4921
 
4747
- // remove colinear/flat
4748
- if (removeColinear) pathDataSub = pathDataRemoveColinear(pathDataSub, tolerance, flatBezierToLinetos);
4922
+ // Preprocessing: remove colinear - ignore flat beziers (removed later)
4923
+ if (removeColinear) pathDataSub = pathDataRemoveColinear(pathDataSub, { tolerance, flatBezierToLinetos: false });
4749
4924
 
4750
4925
  // analyze pathdata to add info about signicant properties such as extremes, corners
4751
4926
  let pathDataPlus = analyzePathData(pathDataSub);
4752
4927
 
4753
4928
  // simplify beziers
4754
4929
  let { pathData, bb, dimA } = pathDataPlus;
4755
-
4756
4930
  pathData = simplifyBezier ? simplifyPathDataCubic(pathData, { simplifyBezier, keepInflections, keepExtremes, keepCorners, extrapolateDominant, revertToQuadratics, tolerance, reverse }) : pathData;
4757
4931
 
4758
4932
  // refine extremes
4759
- if(refineExtremes){
4933
+ if (refineExtremes) {
4760
4934
  let thresholdEx = (bb.width + bb.height) / 2 * 0.05;
4761
- pathData = refineAdjacentExtremes(pathData, {threshold:thresholdEx, tolerance});
4935
+ pathData = refineAdjacentExtremes(pathData, { threshold: thresholdEx, tolerance });
4762
4936
  }
4763
4937
 
4764
4938
  // cubic to arcs
4765
4939
  if (cubicToArc) {
4766
4940
 
4767
- let thresh = 3;
4941
+ let thresh = 1;
4768
4942
 
4769
- pathData.forEach((com, c) => {
4943
+ for(let c=0, l=pathData.length; c<l; c++){
4944
+ let com = pathData[c];
4770
4945
  let { type, values, p0, cp1 = null, cp2 = null, p = null } = com;
4771
4946
  if (type === 'C') {
4772
4947
 
4773
4948
  let comA = cubicCommandToArc(p0, cp1, cp2, p, thresh);
4774
4949
  if (comA.isArc) pathData[c] = comA.com;
4775
-
4776
4950
  }
4777
- });
4951
+ }
4778
4952
 
4779
4953
  // combine adjacent cubics
4780
4954
  pathData = combineArcs(pathData);
4781
4955
 
4782
4956
  }
4783
4957
 
4958
+ // post processing: remove flat beziers
4959
+ if (removeColinear && flatBezierToLinetos) {
4960
+ pathData = pathDataRemoveColinear(pathData, { tolerance, flatBezierToLinetos });
4961
+ }
4962
+
4963
+ // refine corners
4964
+ if(refineCorners){
4965
+ let threshold = (bb.width + bb.height) / 2 * 0.1;
4966
+ pathData = refineRoundedCorners(pathData, { threshold, tolerance });
4967
+
4968
+ }
4969
+
4784
4970
  // simplify to quadratics
4785
4971
  if (revertToQuadratics) {
4786
- pathData.forEach((com, c) => {
4972
+ for(let c=0, l=pathData.length; c<l; c++){
4973
+ let com = pathData[c];
4787
4974
  let { type, values, p0, cp1 = null, cp2 = null, p = null } = com;
4788
4975
  if (type === 'C') {
4789
4976
 
4790
4977
  let comQ = revertCubicQuadratic(p0, cp1, cp2, p);
4791
4978
  if (comQ.type === 'Q') {
4792
- /*
4793
- comQ.p0 = com.p0
4794
- comQ.cp1 = {x:comQ.values[0], y:comQ.values[1]}
4795
- comQ.p = com.p
4796
- */
4797
4979
  comQ.extreme = com.extreme;
4798
4980
  comQ.corner = com.corner;
4799
4981
  comQ.dimA = com.dimA;
@@ -4801,20 +4983,25 @@
4801
4983
  pathData[c] = comQ;
4802
4984
  }
4803
4985
  }
4804
- });
4986
+ }
4805
4987
  }
4806
4988
 
4807
4989
  // optimize close path
4808
4990
  if (optimizeOrder) pathData = optimizeClosePath(pathData);
4809
4991
 
4810
- // poly
4811
-
4812
4992
  // update
4813
- pathDataArrN.push(pathData);
4993
+ pathDataFlat.push(...pathData);
4994
+
4814
4995
  }
4815
4996
 
4816
4997
  // flatten compound paths
4817
- pathData = pathDataArrN.flat();
4998
+ pathData = pathDataFlat;
4999
+
5000
+ if (autoAccuracy) {
5001
+ decimals = detectAccuracy(pathData);
5002
+ pathOptions.decimals = decimals;
5003
+
5004
+ }
4818
5005
 
4819
5006
  // collect for merged svg paths
4820
5007
  if (el && mergePaths) {
@@ -4823,15 +5010,6 @@
4823
5010
  // single output
4824
5011
  else {
4825
5012
 
4826
- /**
4827
- * detect accuracy
4828
- */
4829
- if (autoAccuracy) {
4830
- decimals = detectAccuracy(pathData);
4831
- pathOptions.decimals = decimals;
4832
-
4833
- }
4834
-
4835
5013
  // optimize path data
4836
5014
  pathData = convertPathData(pathData, pathOptions);
4837
5015
 
@@ -4842,7 +5020,9 @@
4842
5020
  let comCountS = pathData.length;
4843
5021
 
4844
5022
  let dOpt = pathDataToD(pathData, minifyD);
4845
- svgSizeOpt = new Blob([dOpt]).size;
5023
+
5024
+ svgSizeOpt = dOpt.length;
5025
+
4846
5026
  compression = +(100 / svgSize * (svgSizeOpt)).toFixed(2);
4847
5027
 
4848
5028
  path.d = dOpt;
@@ -4858,19 +5038,18 @@
4858
5038
  // apply new path for svgs
4859
5039
  if (el) el.setAttribute('d', dOpt);
4860
5040
  }
4861
- });
4862
-
5041
+ }
4863
5042
  /**
4864
5043
  * stringify new SVG
4865
5044
  */
4866
5045
  if (mode) {
4867
5046
 
4868
5047
  if (pathData_merged.length) {
5048
+
4869
5049
  // optimize path data
4870
5050
  let pathData = convertPathData(pathData_merged, pathOptions);
4871
5051
 
4872
5052
  // remove zero-length segments introduced by rounding
4873
-
4874
5053
  pathData = removeZeroLengthLinetos(pathData);
4875
5054
 
4876
5055
  let dOpt = pathDataToD(pathData, minifyD);
@@ -4889,7 +5068,8 @@
4889
5068
  }
4890
5069
 
4891
5070
  svg = stringifySVG(svg);
4892
- svgSizeOpt = new Blob([svg]).size;
5071
+
5072
+ svgSizeOpt = svg.length;
4893
5073
 
4894
5074
  compression = +(100 / svgSize * (svgSizeOpt)).toFixed(2);
4895
5075
 
@@ -4910,104 +5090,6 @@
4910
5090
 
4911
5091
  }
4912
5092
 
4913
- function simplifyPathDataCubic(pathData, {
4914
- keepExtremes = true,
4915
- keepInflections = true,
4916
- keepCorners = true,
4917
- extrapolateDominant = true,
4918
- tolerance = 1,
4919
- reverse = false
4920
- } = {}) {
4921
-
4922
- let pathDataN = [pathData[0]];
4923
-
4924
- for (let i = 2, l = pathData.length; l && i <= l; i++) {
4925
- let com = pathData[i - 1];
4926
- let comN = i < l ? pathData[i] : null;
4927
- let typeN = comN?.type || null;
4928
-
4929
- let isDirChange = com?.directionChange || null;
4930
- let isDirChangeN = comN?.directionChange || null;
4931
-
4932
- let { type, values, p0, p, cp1 = null, cp2 = null, extreme = false, corner = false, dimA = 0 } = com;
4933
-
4934
- // next is also cubic
4935
- if (type === 'C' && typeN === 'C') {
4936
-
4937
- // cannot be combined as crossing extremes or corners
4938
- if (
4939
- (keepInflections && isDirChangeN) ||
4940
- (keepCorners && corner) ||
4941
- (!isDirChange && keepExtremes && extreme)
4942
- ) {
4943
-
4944
- pathDataN.push(com);
4945
- }
4946
-
4947
- // try simplification
4948
- else {
4949
-
4950
- let combined = combineCubicPairs(com, comN, extrapolateDominant, tolerance);
4951
- let error = 0;
4952
-
4953
- // combining successful! try next segment
4954
- if (combined.length === 1) {
4955
- com = combined[0];
4956
- let offset = 1;
4957
-
4958
- // add cumulative error to prevent distortions
4959
- error += com.error;
4960
-
4961
- // find next candidates
4962
- for (let n = i + 1; error < tolerance && n < l; n++) {
4963
- let comN = pathData[n];
4964
- if (comN.type !== 'C' ||
4965
- (
4966
- (keepInflections && comN.directionChange) ||
4967
- (keepCorners && com.corner) ||
4968
- (keepExtremes && com.extreme)
4969
- )
4970
- ) {
4971
- break
4972
- }
4973
-
4974
- let combined = combineCubicPairs(com, comN, extrapolateDominant, tolerance);
4975
- if (combined.length === 1) {
4976
- // add cumulative error to prevent distortions
4977
-
4978
- error += combined[0].error * 0.5;
4979
-
4980
- offset++;
4981
- }
4982
- com = combined[0];
4983
- }
4984
-
4985
- pathDataN.push(com);
4986
-
4987
- if (i < l) {
4988
- i += offset;
4989
- }
4990
-
4991
- } else {
4992
- pathDataN.push(com);
4993
- }
4994
- }
4995
-
4996
- } // end of bezier command
4997
-
4998
- // other commands
4999
- else {
5000
- pathDataN.push(com);
5001
- }
5002
-
5003
- } // end command loop
5004
-
5005
- // reverse back
5006
- if (reverse) pathDataN = reversePathData(pathDataN);
5007
-
5008
- return pathDataN
5009
- }
5010
-
5011
5093
  const {
5012
5094
  abs, acos, asin, atan, atan2, ceil, cos, exp, floor,
5013
5095
  log, hypot, max, min, pow, random, round, sin, sqrt, tan, PI