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
@@ -377,7 +377,14 @@ const {
377
377
 
378
378
  // get angle helper
379
379
  function getAngle(p1, p2, normalize = false) {
380
- let angle = atan2(p2.y - p1.y, p2.x - p1.x);
380
+ let angle = Math.atan2(p2.y - p1.y, p2.x - p1.x);
381
+ // normalize negative angles
382
+ if (normalize && angle < 0) angle += Math.PI * 2;
383
+ return angle
384
+ }
385
+
386
+ function getAngleFromDelta(dx, dy, normalize = false) {
387
+ let angle = Math.atan2(dy, dx);
381
388
  // normalize negative angles
382
389
  if (normalize && angle < 0) angle += Math.PI * 2;
383
390
  return angle
@@ -473,7 +480,9 @@ function checkLineIntersection(p1 = null, p2 = null, p3 = null, p4 = null, exact
473
480
  // if we cast these lines infinitely in both directions, they intersect here:
474
481
  intersectionPoint = {
475
482
  x: p1.x + (a * (p2.x - p1.x)),
476
- y: p1.y + (a * (p2.y - p1.y))
483
+ y: p1.y + (a * (p2.y - p1.y)),
484
+ t1: a,
485
+ t2: b
477
486
  };
478
487
 
479
488
  let intersection = false;
@@ -575,13 +584,13 @@ function pointAtT(pts, t = 0.5, getTangent = false, getCpts = false, returnArray
575
584
  if (t === 0 && !shortCp1) {
576
585
  pt.x = p0.x;
577
586
  pt.y = p0.y;
578
- pt.angle = getAngle(p0, cp1);
587
+ if (getTangent) pt.angle = getAngle(p0, cp1);
579
588
  }
580
589
 
581
590
  else if (t === 1 && !shortCp2) {
582
591
  pt.x = p.x;
583
592
  pt.y = p.y;
584
- pt.angle = getAngle(cp2, p);
593
+ if (getTangent) pt.angle = getAngle(cp2, p);
585
594
  }
586
595
 
587
596
  else {
@@ -598,18 +607,38 @@ function pointAtT(pts, t = 0.5, getTangent = false, getCpts = false, returnArray
598
607
  pt = interpolate(m3, m4, t);
599
608
 
600
609
  // add angles
601
- pt.angle = getAngle(m3, m4);
610
+ if (getTangent) pt.angle = getAngle(m3, m4);
611
+
612
+ /*
613
+ renderPoint(markers, m0, 'cyan')
614
+ renderPoint(markers, m3, 'magenta')
615
+ renderPoint(markers, pt, 'green')
616
+ renderPoint(markers, m4, 'cyan')
617
+ renderPoint(markers, m2, 'magenta')
618
+ */
602
619
 
603
620
  // add control points
604
- if (getCpts) pt.cpts = [m1, m2, m3, m4];
621
+ if (getCpts) {
622
+ pt.cpts = [m1, m2, m3, m4];
623
+ pt.segments = [
624
+ { p0, cp1: m0, cp2: m3, p: pt },
625
+ { p0: pt, cp1: m4, cp2: m2, p }
626
+ ];
627
+ }
605
628
  } else {
606
629
  m1 = interpolate(p0, cp1, t);
607
630
  m2 = interpolate(cp1, p, t);
608
631
  pt = interpolate(m1, m2, t);
609
- pt.angle = getAngle(m1, m2);
632
+ if (getTangent) pt.angle = getAngle(m1, m2);
610
633
 
611
634
  // add control points
612
- if (getCpts) pt.cpts = [m1, m2];
635
+ if (getCpts) {
636
+ pt.cpts = [m1, m2];
637
+ pt.segments = [
638
+ { p0, cp1: m1, p: pt },
639
+ { p0: p, cp1: m2, p }
640
+ ];
641
+ }
613
642
  }
614
643
  }
615
644
 
@@ -693,7 +722,7 @@ function pointAtT(pts, t = 0.5, getTangent = false, getCpts = false, returnArray
693
722
  * get vertices from path command final on-path points
694
723
  */
695
724
 
696
- function getPathDataVertices(pathData=[], includeCpts = false, decimals = -1) {
725
+ function getPathDataVertices(pathData = [], includeCpts = false, decimals = -1) {
697
726
  let polyPoints = [];
698
727
 
699
728
  pathData.forEach((com) => {
@@ -780,7 +809,8 @@ function svgArcToCenterParam(x1, y1, rx, ry, xAxisRotation, largeArc, sweep, x2,
780
809
 
781
810
  if (rx == 0 || ry == 0) {
782
811
  // invalid arguments
783
- throw Error("rx and ry can not be 0");
812
+ console.warn("rx and ry can not be 0");
813
+ return arcData
784
814
  }
785
815
 
786
816
  /**
@@ -793,10 +823,10 @@ function svgArcToCenterParam(x1, y1, rx, ry, xAxisRotation, largeArc, sweep, x2,
793
823
  let s_phi = !phi ? 0 : Math.sin(phi);
794
824
  let c_phi = !phi ? 1 : Math.cos(phi);
795
825
 
796
- let hd_x = (x1 - x2) / 2;
797
- let hd_y = (y1 - y2) / 2;
798
- let hs_x = (x1 + x2) / 2;
799
- let hs_y = (y1 + y2) / 2;
826
+ let hd_x = (x1 - x2) * 0.5;
827
+ let hd_y = (y1 - y2) * 0.5;
828
+ let hs_x = (x1 + x2) * 0.5;
829
+ let hs_y = (y1 + y2) * 0.5;
800
830
 
801
831
  // F6.5.1
802
832
  let x1_ = !phi ? hd_x : c_phi * hd_x + s_phi * hd_y;
@@ -818,10 +848,11 @@ function svgArcToCenterParam(x1, y1, rx, ry, xAxisRotation, largeArc, sweep, x2,
818
848
  let rxry = rx * ry;
819
849
  let rxy1_ = rx * y1_;
820
850
  let ryx1_ = ry * x1_;
821
- let sum_of_sq = rxy1_ ** 2 + ryx1_ ** 2; // sum of square
851
+ let sum_of_sq = rxy1_ * rxy1_ + ryx1_ * ryx1_; // sum of square
822
852
  if (!sum_of_sq) {
853
+ console.warn("start point can not be same as end point");
854
+ return arcData
823
855
 
824
- throw Error("start point can not be same as end point");
825
856
  }
826
857
  let coe = Math.sqrt(Math.abs((rxry * rxry - sum_of_sq) / sum_of_sq));
827
858
  if (largeArc === sweep) {
@@ -970,7 +1001,11 @@ function bezierhasExtreme(p0 = null, cpts = []) {
970
1001
 
971
1002
  function getBezierExtremeT(pts, { addExtremes = true, addSemiExtremes = false } = {}) {
972
1003
  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 });
973
- return tArr;
1004
+ if (tArr.length) {
1005
+ tArr = tArr.map(t => +t.toFixed(9)).sort();
1006
+ }
1007
+
1008
+ return tArr
974
1009
  }
975
1010
 
976
1011
  function getArcExtemes(p0, values) {
@@ -1300,13 +1335,6 @@ function cubicBezierExtremeT(p0, cp1, cp2, p,
1300
1335
  return tArr;
1301
1336
  }
1302
1337
 
1303
- function isMultipleOf45(angleRad, epsilon = 0.001) {
1304
- let rad90 = Math.PI / 2;
1305
- let rad45 = rad90 / 2;
1306
- let isRightAngle = Math.abs(angleRad / rad90 - Math.round(angleRad / rad90)) < epsilon;
1307
- return !isRightAngle ? Math.abs(angleRad / rad45 - Math.round(angleRad / rad45)) < epsilon : false;
1308
- }
1309
-
1310
1338
  function quadraticBezierExtremeT(p0, cp1, p, { addExtremes = true, addSemiExtremes = false } = {}) {
1311
1339
 
1312
1340
  // rotate cpts for semi extremes
@@ -1370,6 +1398,11 @@ function getDistance(p1, p2, isArray = false) {
1370
1398
  let dx = isArray ? p2[0] - p1[0] : (p2.x - p1.x);
1371
1399
  let dy = isArray ? p2[1] - p1[1] : (p2.y - p1.y);
1372
1400
 
1401
+ /*
1402
+ let sqrt2 = 1.4142135623730951
1403
+ return dx===dy ? Math.abs(dx) * sqrt2 : Math.sqrt(dx * dx + dy * dy);
1404
+ */
1405
+
1373
1406
  return Math.sqrt(dx * dx + dy * dy);
1374
1407
  }
1375
1408
 
@@ -1385,6 +1418,7 @@ function getSquareDistance(p1, p2) {
1385
1418
  * sloppy but fast
1386
1419
  */
1387
1420
  function getDistManhattan(pt1, pt2) {
1421
+
1388
1422
  let dx = Math.abs(pt2.x - pt1.x);
1389
1423
  let dy = Math.abs(pt2.y - pt1.y);
1390
1424
  return dx + dy;
@@ -1852,7 +1886,7 @@ function roundPathData(pathData, decimalsGlobal = -1) {
1852
1886
 
1853
1887
  let len = pathData.length;
1854
1888
  let decimals = decimalsGlobal;
1855
- let decimalsArc = decimals < 3 ? decimals + 2 : decimals;
1889
+ let decimalsArc = decimals < 3 ? decimals + 1 : decimals;
1856
1890
 
1857
1891
  for (let c = 0; c < len; c++) {
1858
1892
  let com = pathData[c];
@@ -1887,7 +1921,7 @@ function detectAccuracyPoly(pts) {
1887
1921
  }
1888
1922
  }
1889
1923
 
1890
- let dim_min = dims.sort();
1924
+ let dim_min = dims.sort((a,b)=>a-b);
1891
1925
  let sliceIdx = Math.ceil(dim_min.length / 8);
1892
1926
  dim_min = dim_min.slice(0, sliceIdx);
1893
1927
  let minVal = dim_min.reduce((a, b) => a + b, 0) / sliceIdx;
@@ -1907,52 +1941,47 @@ function detectAccuracy(pathData) {
1907
1941
  let com = pathData[i];
1908
1942
  let { type, values, p0 = null, p = null, dimA = 0 } = com;
1909
1943
 
1910
- // use existing averave dimension value or calculate
1944
+ // use existing average dimension value or calculate
1911
1945
  if (values.length && p && p0) {
1912
-
1913
1946
  dimA = dimA ? dimA : getDistManhattan(p0, p);
1914
1947
 
1915
- if (dimA) dims.push(+dimA.toFixed(8));
1948
+ if (type === 'A') dimA *= 0.5;
1916
1949
 
1950
+ if (dimA) dims.push(+dimA.toFixed(8));
1917
1951
  }
1918
1952
 
1919
1953
  }
1920
1954
 
1921
- dims = dims.sort();
1922
- let len = dims.length;
1923
- let dim_mid = dims[Math.floor(len*0.5)];
1955
+ dims = dims.sort((a,b)=>a-b);
1956
+ let len = dims.length;
1957
+ let dim_mid = dims[Math.floor(len * 0.5)];
1924
1958
 
1925
- // smallest 25% of values
1926
- let idx_q = Math.ceil(len*0.25);
1927
- let dims_min = dims.slice(0, idx_q);
1959
+ // smallest 25% of values
1960
+ let idx_q = Math.ceil(len * 0.25);
1961
+ let dims_min = dims.slice(0, idx_q);
1928
1962
 
1929
- // average smallest values with mid value
1930
- let dim_min = ((dims_min.reduce((a, b) => a + b, 0) / idx_q) + dim_mid) * 0.5;
1963
+ // average smallest values with mid value
1964
+ let dim_min = ((dims_min.reduce((a, b) => a + b, 0) / idx_q) + dim_mid) * 0.5;
1931
1965
 
1932
- let threshold = 75;
1933
- let decimalsAuto = dim_min > threshold * 1.5 ? 0 : Math.floor(threshold / dim_min).toString().length;
1934
-
1935
- // clamp
1936
- return Math.min(Math.max(0, decimalsAuto), 8)
1937
-
1938
- /*
1939
- let dim_min = dims.sort()
1940
-
1941
- let dim_mid = dim_min[Math.floor(dim_min.length*0.5)]
1966
+ let threshold = 75;
1967
+ let decimalsAuto = dim_min > threshold * 1.5 ? 0 : Math.floor(threshold / dim_min).toString().length;
1942
1968
 
1943
- let sliceIdx = Math.ceil(dim_min.length / 4);
1944
- dim_min = dim_min.slice(0, sliceIdx);
1945
- let minVal = dim_min.reduce((a, b) => a + b, 0) / sliceIdx;
1969
+ // clamp
1970
+ return Math.min(Math.max(0, decimalsAuto), 8)
1946
1971
 
1947
- // average with mid value
1948
- minVal = (minVal+dim_mid)*0.5
1972
+ }
1949
1973
 
1950
- let threshold = 75
1951
- let decimalsAuto = minVal > threshold * 1.5 ? 0 : Math.floor(threshold / minVal).toString().length
1974
+ function roundPoly(poly = [], decimals = 3) {
1975
+ if (!poly.length || decimals < 0) return poly;
1976
+ poly = poly.map(pt => roundPoint(pt, decimals));
1977
+ return poly
1978
+ }
1952
1979
 
1953
- // clamp
1954
- return Math.min(Math.max(0, decimalsAuto), 8)
1955
- */
1980
+ function roundPoint(pt = {}, decimals = 3) {
1981
+ if (pt.x === undefined || pt.y === undefined || decimals < 0) return pt;
1982
+ pt.x=roundTo(pt.x, decimals);
1983
+ pt.y=roundTo(pt.y, decimals);
1984
+ return pt
1956
1985
 
1957
1986
  }
1958
1987
 
@@ -2730,7 +2759,7 @@ function getPathDataBBox(pathData) {
2730
2759
  }
2731
2760
  }
2732
2761
 
2733
- let bbox = { x: xMin, y: yMin, width: xMax - xMin, height: yMax - yMin };
2762
+ let bbox = { x: xMin, y: yMin, right: xMax, width: xMax - xMin, bottom: yMax, height: yMax - yMin };
2734
2763
  return bbox
2735
2764
  }
2736
2765
 
@@ -2920,7 +2949,7 @@ function getPolygonArea(points, absolute=false) {
2920
2949
  * d attribute string
2921
2950
  */
2922
2951
 
2923
- function pathDataToD(pathData, mode = 0) {
2952
+ function pathDataToD(pathData = [], mode = 0) {
2924
2953
 
2925
2954
  mode = parseFloat(mode);
2926
2955
  /*
@@ -2929,80 +2958,99 @@ function pathDataToD(pathData, mode = 0) {
2929
2958
  1 = verbose
2930
2959
  2 = beautify
2931
2960
  */
2961
+
2932
2962
  let len = pathData.length;
2963
+ let d = '';
2933
2964
 
2934
- let valsString = pathData[0].values.join(" ");
2935
- let separator_command = mode > 1 ? `\n` :
2936
- ((mode < 1) ? '' : ' ');
2937
- let separator_type = mode > 0.5 ? ' ' : '';
2965
+ // group same types
2966
+ let pathDataGrouped = mode >0.5 ? JSON.parse(JSON.stringify(pathData)) : [];
2967
+ let typePrev = 'M';
2938
2968
 
2939
- // 1st command
2940
- let d = `${pathData[0].type}${separator_type}${valsString}${separator_command}`;
2969
+ if (mode < 1) {
2970
+ pathDataGrouped = [pathData[0]];
2941
2971
 
2942
- for (let i = 1; i < len; i++) {
2943
- let com0 = pathData[i - 1];
2944
- let com = pathData[i];
2945
- let { type, values } = com;
2946
- valsString = '';
2947
-
2948
- // Minify Arc commands (A/a) – actually sucks!
2949
- if (!mode && (type === 'A' || type === 'a')) {
2950
- values = [
2951
- values[0], values[1], values[2],
2952
- `${values[3]}${values[4]}${values[5]}`,
2953
- values[6]
2954
- ];
2972
+ let idx = 0;
2973
+
2974
+ for (let i = 1; i < len; i++) {
2975
+ let com = pathData[i];
2976
+ let { type } = com;
2977
+ // decouple from object
2978
+ let values = [...com.values];
2979
+
2980
+ // new type
2981
+ if (type !== typePrev) {
2982
+ pathDataGrouped.push({type, values});
2983
+ idx++;
2984
+ } else {
2985
+ pathDataGrouped[idx].values.push(...values);
2986
+ }
2987
+
2988
+ // update type
2989
+ typePrev = type;
2955
2990
  }
2991
+ }
2992
+
2993
+ // stringify grouped
2994
+ len = pathDataGrouped.length;
2995
+ let separator_type = mode < 1 ? '' : ' ';
2996
+ let separator_command = mode < 1 ? '' : (mode === 1 ? ' ' : `\n`);
2997
+
2998
+ typePrev = 'M';
2956
2999
 
2957
- // Omit type for repeated commands
2958
- type = ((mode < 1) && com0.type === com.type && com.type.toLowerCase() !== 'm')
2959
- ? " "
2960
- : ((mode < 1) && com0.type === "M" && com.type === "L"
2961
- ? " "
2962
- : com.type);
3000
+ for (let i = 0; i < len; i++) {
3001
+ let com = pathDataGrouped[i];
3002
+ let { type, values } = com;
2963
3003
 
2964
- // concatenate subsequent floating point values
2965
- if (!mode) {
3004
+ // we're always starting a path with absolute M!
3005
+ let omitType = mode < 1 && ((typePrev === 'M' && type === 'L') || (typePrev === 'm' && type === 'l'));
2966
3006
 
2967
- let prevWasFloat = false;
3007
+ // add type
3008
+ if (!omitType) d += type + separator_type;
2968
3009
 
2969
- for (let v = 0, l = values.length; v < l; v++) {
2970
- let val = values[v];
2971
- let valStr = val.toString();
2972
- let isFloat = valStr.includes('.');
2973
- let isSmallFloat = isFloat && Math.abs(val) < 1;
3010
+ // add values
3011
+ let wasSmallFloat = false;
3012
+ let separatorVal = ' ';
2974
3013
 
2975
- // Remove leading zero from small floats *only* if the previous was also a float
2976
- if (isSmallFloat && prevWasFloat) {
2977
- valStr = valStr.replace(/^0\./, '.');
2978
- }
3014
+ for (let v = 0, vlen = values.length; vlen && v < vlen; v++) {
3015
+ let val = values[v];
3016
+ let valAbs = Math.abs(val);
3017
+ let valStr = val.toString();
3018
+ let isNegative = val < 0;
3019
+ let sign = isNegative ? '-' : '';
3020
+ let isSmallFloat = mode > 0.5 ? false : (val && valAbs < 1);
3021
+ let idxSub = isSmallFloat ? (isNegative ? 2 : 1) : 0;
2979
3022
 
2980
- // Add space unless this is the first value OR previous was a small float
2981
- if (v > 0 && !(prevWasFloat && isSmallFloat)) {
2982
- valsString += ' ';
2983
- }
3023
+ // we don't need whitespace for first value
3024
+ separatorVal = v === 0 || isNegative ? '' : ' ';
3025
+
3026
+ if (mode < 1) {
3027
+ // omit leading zero
3028
+ if (isSmallFloat) valStr = sign + valStr.substring(idxSub);
3029
+
3030
+ // omit whitespace for subsequent small floats
3031
+ separatorVal = (v === 0 && !omitType) || (wasSmallFloat && isSmallFloat) ?
3032
+ (!mode ? '' : (isNegative ? '' : ' '))
3033
+ : (isNegative ? '' : ' ');
2984
3034
 
2985
- valsString += valStr;
2986
- prevWasFloat = isSmallFloat;
2987
3035
  }
2988
3036
 
2989
- }
2990
- // regular non-minified output
2991
- else {
2992
- valsString = values.join(' ');
3037
+ // omit separator between large Arc sweep and final x in minify mode
3038
+ if (!mode && (type === 'a' || type === 'A')) {
3039
+ let pos = (v % 7);
3040
+ if (pos > 3 && pos < 6) separatorVal = '';
3041
+ }
3042
+
3043
+ d += `${separatorVal}${valStr}`;
3044
+ wasSmallFloat = isSmallFloat;
3045
+
2993
3046
  }
2994
3047
 
2995
- if(i===len-1) separator_command='';
2996
- d += `${type}${separator_type}${valsString}${separator_command}`;
2997
- }
3048
+ // add command separator
3049
+ if (mode) d += separator_command;
3050
+
3051
+ // update previous type
3052
+ typePrev = type;
2998
3053
 
2999
- if (mode < 1) {
3000
- d = d
3001
- .replace(/[A-Za-z]0(?=\.)/g, m => m[0])
3002
- .replace(/ 0\./g, " .") // Space before small decimals
3003
- .replace(/ -/g, "-") // Remove space before negatives
3004
- .replace(/-0\./g, "-.") // Remove leading zero from negative decimals
3005
- .replace(/Z/g, "z"); // Convert uppercase 'Z' to lowercase
3006
3054
  }
3007
3055
 
3008
3056
  return d;
@@ -3010,39 +3058,26 @@ function pathDataToD(pathData, mode = 0) {
3010
3058
 
3011
3059
  function getCombinedByDominant(com1, com2, maxDist = 0, tolerance = 1, debug = false) {
3012
3060
 
3013
- // cubic Bézier derivative
3014
- const cubicDerivative = (p0, p1, p2, p3, t) => {
3015
- let mt = 1 - t;
3016
-
3017
- return {
3018
- x:
3019
- 3 * mt * mt * (p1.x - p0.x) +
3020
- 6 * mt * t * (p2.x - p1.x) +
3021
- 3 * t * t * (p3.x - p2.x),
3022
- y:
3023
- 3 * mt * mt * (p1.y - p0.y) +
3024
- 6 * mt * t * (p2.y - p1.y) +
3025
- 3 * t * t * (p3.y - p2.y)
3026
- };
3027
- };
3028
-
3029
3061
  // if combining fails return original commands
3030
3062
  let commands = [com1, com2];
3031
3063
 
3032
3064
  // detect dominant
3033
- let dist1 = getDistAv(com1.p0, com1.p);
3034
- let dist2 = getDistAv(com2.p0, com2.p);
3065
+ let dist1 = getDistManhattan(com1.p0, com1.p);
3066
+ let dist2 = getDistManhattan(com2.p0, com2.p);
3067
+ let thresh = (dist1 + dist2) * 0.5 * 0.075 * tolerance;
3035
3068
 
3036
- let reverse = dist1 > dist2;
3069
+ // take longer command
3070
+ let reverse = dist1 < dist2;
3037
3071
 
3038
3072
  // backup original commands
3039
3073
  let com1_o = JSON.parse(JSON.stringify(com1));
3040
3074
  let com2_o = JSON.parse(JSON.stringify(com2));
3041
3075
 
3042
- let ptI = checkLineIntersection(com1_o.p0, com1_o.cp1, com2_o.p, com2_o.cp2, false);
3076
+ // intersection of control tangents
3077
+ let ptI = checkLineIntersection(com1_o.p0, com1_o.cp1, com2_o.p, com2_o.cp2, false, true);
3043
3078
 
3079
+ // no intersection - we can't combine
3044
3080
  if (!ptI) {
3045
-
3046
3081
  return commands
3047
3082
  }
3048
3083
 
@@ -3065,153 +3100,70 @@ function getCombinedByDominant(com1, com2, maxDist = 0, tolerance = 1, debug = f
3065
3100
  com2 = com2_R;
3066
3101
  }
3067
3102
 
3068
- let add = (a, b) => ({ x: a.x + b.x, y: a.y + b.y });
3069
- let sub = (a, b) => ({ x: a.x - b.x, y: a.y - b.y });
3070
- let mul = (a, s) => ({ x: a.x * s, y: a.y * s });
3071
- let dot = (a, b) => a.x * b.x + a.y * b.y;
3072
-
3073
- // estimate extrapolation parameter t0
3074
-
3075
- let B0 = com2.p0;
3076
- let D0 = cubicDerivative(
3077
- com2.p0,
3078
- com2.cp1,
3079
- com2.cp2,
3080
- com2.p,
3081
- 0
3082
- );
3083
-
3084
- let v = sub(com1.p0, B0);
3085
-
3086
- // first-order projection onto tangent
3087
- let t0 = dot(v, D0) / dot(D0, D0);
3088
-
3089
- // refine with one Newton iteration (optional but cheap)
3090
- let P = pointAtT([com2.p0, com2.cp1, com2.cp2, com2.p], t0);
3091
- let dP = cubicDerivative(com2.p0, com2.cp1, com2.cp2, com2.p, t0);
3092
- let r = sub(P, com1.p0);
3093
-
3094
- t0 -= dot(r, dP) / dot(dP, dP);
3095
-
3096
- // construct merged cubic over [t0, 1]
3097
- let Q0 = pointAtT([com2.p0, com2.cp1, com2.cp2, com2.p], t0);
3098
- let Q3 = com2.p;
3099
-
3100
- let d0 = cubicDerivative(com2.p0, com2.cp1, com2.cp2, com2.p, t0);
3101
- let d1 = cubicDerivative(com2.p0, com2.cp1, com2.cp2, com2.p, 1);
3102
-
3103
- let scale = 1 - t0;
3104
-
3105
- let Q1 = add(Q0, mul(d0, scale / 3));
3106
- let Q2 = sub(Q3, mul(d1, scale / 3));
3107
-
3108
- let result = {
3109
- p0: Q0,
3110
- cp1: Q1,
3111
- cp2: Q2,
3112
- p: Q3,
3113
- t0
3114
- };
3115
-
3116
- if (reverse) {
3117
- result = {
3118
- p0: Q3,
3119
- cp1: Q2,
3120
- cp2: Q1,
3121
- p: Q0,
3122
- t0
3123
- };
3124
- }
3125
-
3126
- let tMid = (1 - t0) * 0.5;
3127
-
3128
- let ptM = pointAtT([result.p0, result.cp1, result.cp2, result.p], tMid, false, true);
3129
- let seg1_cp2 = ptM.cpts[2];
3103
+ // etsimate t for extrapolation
3104
+ let PtI = checkLineIntersection(com1.cp2, com1.p, com2.p, com2.cp2, false, true);
3105
+ let cp1_I = interpolate(com1.p, PtI, 0.666);
3130
3106
 
3131
- let ptI_1 = checkLineIntersection(ptM, seg1_cp2, result.p0, ptI, false);
3132
- let ptI_2 = checkLineIntersection(ptM, seg1_cp2, result.p, ptI, false);
3107
+ let dist1_2 = getDistManhattan(com1.cp2, com1.p);
3108
+ let dist2_2 = getDistManhattan(com1.cp2, cp1_I);
3109
+ let t = dist2_2 / dist1_2;
3133
3110
 
3134
- let cp1_2 = interpolate(result.p0, ptI_1, 1.333 );
3135
- let cp2_2 = interpolate(result.p, ptI_2, 1.333 );
3111
+ // extrapolate
3112
+ let segs = pointAtT([com1.p0, com1.cp1, com1.cp2, com1.p], t, false, true).segments;
3136
3113
 
3137
- // test self intersections and exit
3138
- let cp_intersection = checkLineIntersection(com1_o.p0, cp1_2, com2_o.p, cp2_2, true);
3139
- if (cp_intersection) {
3114
+ let seg = segs[0];
3140
3115
 
3141
- return commands;
3142
- }
3143
-
3144
- result.cp1 = cp1_2;
3145
- result.cp2 = cp2_2;
3146
-
3147
- // check distances between original starting point and extrapolated
3148
- let dist3 = getDistAv(com1_o.p0, result.p0);
3149
- let dist4 = getDistAv(com2_o.p, result.p);
3150
- let dist5 = (dist3 + dist4);
3151
-
3152
- // use original points
3153
- result.p0 = com1_o.p0;
3154
- result.p = com2_o.p;
3155
- result.extreme = com2_o.extreme;
3156
- result.corner = com2_o.corner;
3157
- result.dimA = com2_o.dimA;
3158
- result.directionChange = com2_o.directionChange;
3159
- result.type = 'C';
3160
- result.values = [result.cp1.x, result.cp1.y, result.cp2.x, result.cp2.y, result.p.x, result.p.y];
3116
+ // if it worked - points should be nearby
3117
+ let dist = getDistManhattan(seg.p, com2.p);
3161
3118
 
3162
- // extrapolated starting point is not completely off
3163
- if (dist5 < maxDist) {
3119
+ // if close enough adjust
3120
+ if (dist < thresh) {
3121
+ let angle = getAngle(seg.p, seg.cp2);
3122
+ let angle2 = getAngle(com2.p, com2.cp2);
3164
3123
 
3165
- /*
3166
- let tTotal = 1 + Math.abs(t0);
3167
- let tSplit = reverse ? 1 + t0 : Math.abs(t0);
3168
-
3169
- let pO = pointAtT([com2_o.p0, com2_o.cp1, com2_o.cp2, com2_o.p], t0);
3170
- */
3124
+ let angleDiff = (angle2 - angle);
3171
3125
 
3172
- // split t to meet original mid segment start point
3173
- let tSplit = reverse ? 1 + t0 : Math.abs(t0);
3126
+ // adjust cp angle
3127
+ seg.cp2 = rotatePoint(seg.cp2, seg.p.x, seg.p.y, angleDiff);
3128
+ let dist1 = getDistManhattan(seg.p, seg.cp2);
3174
3129
 
3175
- let tTotal = 1 + Math.abs(t0);
3176
- tSplit = reverse ? 1 + t0 : Math.abs(t0) / tTotal;
3130
+ // copy original final point coordinates
3131
+ seg.p = com2.p;
3177
3132
 
3178
- let ptSplit = pointAtT([result.p0, result.cp1, result.cp2, result.p], tSplit);
3179
- let distSplit = getDistAv(ptSplit, com1.p);
3133
+ // after rotation
3134
+ let dist2 = getDistManhattan(seg.p, seg.cp2);
3135
+ let scale = dist2 / dist1;
3180
3136
 
3181
- // not close enough - exit
3182
- if (distSplit > maxDist * tolerance ) {
3137
+ // adjust tangent length
3138
+ seg.cp2 = interpolate(seg.p, seg.cp2, scale);
3183
3139
 
3184
- return commands;
3140
+ // reverse back
3141
+ if (reverse) {
3142
+ seg = {
3143
+ p0: seg.p,
3144
+ p: seg.p0,
3145
+ cp1: seg.cp2,
3146
+ cp2: seg.cp1,
3147
+ };
3185
3148
  }
3186
3149
 
3187
- // compare combined with original area
3188
- let pathData0 = [
3189
- { type: 'M', values: [com1_o.p0.x, com1_o.p0.y] },
3190
- { 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] },
3191
- { 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] },
3192
- ];
3150
+ commands = [
3151
+ {
3152
+ type: 'C',
3153
+ values: [seg.cp1.x, seg.cp1.y, seg.cp2.x, seg.cp2.y, seg.p.x, seg.p.y],
3154
+ p0: seg.p0,
3155
+ cp1: seg.cp1,
3156
+ cp2: seg.cp2,
3157
+ p: seg.p,
3158
+ extreme: com2_o.extreme,
3159
+ corner: com2_o.corner,
3160
+ directionChange: com2_o.directionChange,
3161
+ dimA: getDistManhattan(seg.p0, seg.p),
3162
+ error: dist
3193
3163
 
3194
- let area0 = getPathArea(pathData0);
3195
- let pathDataN = [
3196
- { type: 'M', values: [result.p0.x, result.p0.y] },
3197
- { type: 'C', values: [result.cp1.x, result.cp1.y, result.cp2.x, result.cp2.y, result.p.x, result.p.y] },
3164
+ }
3198
3165
  ];
3199
3166
 
3200
- let areaN = getPathArea(pathDataN);
3201
- let areaDiff = Math.abs(areaN / area0 - 1);
3202
-
3203
- result.error = areaDiff * 5 * tolerance;
3204
-
3205
- if (debug) {
3206
- pathDataToD(pathDataN);
3207
-
3208
- }
3209
-
3210
- // success!!!
3211
- if (areaDiff < 0.05 * tolerance) {
3212
- commands = [result];
3213
-
3214
- }
3215
3167
  }
3216
3168
 
3217
3169
  return commands
@@ -3334,11 +3286,12 @@ function combineCubicPairs(com1, com2, {
3334
3286
  // quit if t is start
3335
3287
  if (!t) return commands;
3336
3288
 
3289
+ // get averaged threshold
3337
3290
  let distAv1 = getDistManhattan(com1.p0, com1.p);
3338
3291
  let distAv2 = getDistManhattan(com2.p0, com2.p);
3339
3292
  let distMin = Math.max(0, Math.min(distAv1, distAv2));
3340
3293
 
3341
- let distScale = 0.08;
3294
+ let distScale = 0.075;
3342
3295
  let maxDist = distMin * distScale * tolerance;
3343
3296
 
3344
3297
  // get hypothetical combined command
@@ -3386,16 +3339,6 @@ function combineCubicPairs(com1, com2, {
3386
3339
 
3387
3340
  if (error < maxDist) success = true;
3388
3341
 
3389
- /*
3390
- renderPoint(markers, ptM_seg1, 'cyan')
3391
- renderPoint(markers, pt, 'orange', '1.5%', '1')
3392
- renderPoint(markers, ptM_seg2, 'orange')
3393
-
3394
- renderPoint(markers, com1.p, 'green')
3395
-
3396
- renderPoint(markers, ptI_seg1, 'purple')
3397
- */
3398
-
3399
3342
  }
3400
3343
 
3401
3344
  } // end 1st try
@@ -3414,14 +3357,6 @@ function combineCubicPairs(com1, com2, {
3414
3357
  comS.directionChange = com2.directionChange;
3415
3358
  comS.corner = com2.corner;
3416
3359
 
3417
- if (comS.extreme || comS.corner) ;
3418
-
3419
- /*
3420
- comS.extreme = com1.extreme;
3421
- comS.directionChange = com1.directionChange;
3422
- comS.corner = com1.corner;
3423
- */
3424
-
3425
3360
  comS.values = [comS.cp1.x, comS.cp1.y, comS.cp2.x, comS.cp2.y, comS.p.x, comS.p.y];
3426
3361
 
3427
3362
  // relative error
@@ -3480,7 +3415,9 @@ function findSplitT(com1, com2) {
3480
3415
  }
3481
3416
  */
3482
3417
 
3483
- return l1 / l3
3418
+ let t = l1 / l3;
3419
+
3420
+ return t;
3484
3421
  }
3485
3422
 
3486
3423
  function commandIsFlat(points, {
@@ -3533,7 +3470,7 @@ function analyzePathData(pathData = [], {
3533
3470
  detectDirection = true,
3534
3471
  detectSemiExtremes = false,
3535
3472
  debug = false,
3536
- addSquareLength = true,
3473
+ addSquareLength = false,
3537
3474
  addArea = true,
3538
3475
 
3539
3476
  } = {}) {
@@ -3582,8 +3519,8 @@ function analyzePathData(pathData = [], {
3582
3519
  let commandPts = (type === 'C' || type === 'Q') ?
3583
3520
  (type === 'C' ? [p0, cp1, cp2, p] : [p0, cp1, p]) :
3584
3521
  ([p0, p]);
3585
- let thresholdLength = dimA * 0.1;
3586
- let threshold = thresholdLength * 0.01;
3522
+
3523
+ let threshold = dimA * 0.005;
3587
3524
 
3588
3525
  // bezier types
3589
3526
  let isBezier = type === 'Q' || type === 'C';
@@ -3596,7 +3533,7 @@ function analyzePathData(pathData = [], {
3596
3533
  */
3597
3534
  let hasExtremes = false;
3598
3535
 
3599
- if (isBezier) {
3536
+ if (detectExtremes && isBezier) {
3600
3537
 
3601
3538
  let dx = type === 'C' ? Math.abs(com.cp2.x - com.p.x) : Math.abs(com.cp1.x - com.p.x);
3602
3539
  let dy = type === 'C' ? Math.abs(com.cp2.y - com.p.y) : Math.abs(com.cp1.y - com.p.y);
@@ -3635,7 +3572,7 @@ function analyzePathData(pathData = [], {
3635
3572
  }
3636
3573
 
3637
3574
  // check extremes introduce by small arcs
3638
- else if(isArc && comN && ((comPrev.type==='C' || comPrev.type==='Q') || (comN.type==='C' || comN.type==='Q')) ){
3575
+ else if(detectExtremes && isArc && comN && ((comPrev.type==='C' || comPrev.type==='Q') || (comN.type==='C' || comN.type==='Q')) ){
3639
3576
  let distN = comN ? comN.dimA : 0;
3640
3577
  let isShort = com.dimA < (comPrev.dimA + distN) * 0.1;
3641
3578
  let smallRadius = com.values[0] === com.values[1] && (com.values[0] < 1);
@@ -3653,28 +3590,7 @@ function analyzePathData(pathData = [], {
3653
3590
  if (hasExtremes) com.extreme = true;
3654
3591
 
3655
3592
  // Corners and semi extremes
3656
- if (isBezier && isBezierN) {
3657
-
3658
- // semi extremes
3659
- if (detectSemiExtremes && !com.extreme) {
3660
-
3661
- let dx1 = Math.abs(p.x - cp2.x);
3662
- let dy1 = Math.abs(p.y - cp2.y);
3663
- let hasSemiExtreme = false;
3664
-
3665
- // exclude extremes or small deltas
3666
- if (dx1 && dy1 && dx1 > thresholdLength || dy1 > thresholdLength) {
3667
- let ang1 = getAngle(cp2, p);
3668
- let ang2 = getAngle(p, comN.cp1);
3669
-
3670
- let ang3 = Math.abs(ang1 + ang2) / 2;
3671
- hasSemiExtreme = isMultipleOf45(ang3);
3672
- }
3673
-
3674
- if (hasSemiExtreme) {
3675
- com.semiExtreme = true;
3676
- }
3677
- }
3593
+ if (detectCorners && isBezier && isBezierN) {
3678
3594
 
3679
3595
  /**
3680
3596
  * Detect direction change points
@@ -4254,7 +4170,12 @@ function convertPathData(pathData, {
4254
4170
  if (hasShorthands && toLonghands) pathData = pathDataToLonghands(pathData);
4255
4171
 
4256
4172
  // minify semicircle radii
4257
- if (optimizeArcs) pathData = optimizeArcPathData(pathData);
4173
+ if (optimizeArcs) {
4174
+ pathData = optimizeArcPathData(pathData);
4175
+ } else {
4176
+ // get true absolute radii
4177
+ pathData = pathDataToTrueArcValues(pathData);
4178
+ }
4258
4179
 
4259
4180
  if (toShorthands) pathData = pathDataToShorthands(pathData);
4260
4181
 
@@ -4344,9 +4265,42 @@ function parsePathDataNormalized(d,
4344
4265
  }
4345
4266
 
4346
4267
  /**
4347
- *
4348
- * @param {*} pathData
4349
- * @returns
4268
+ * Converts minified arc
4269
+ * values to true rx and ry values
4270
+ */
4271
+ function pathDataToTrueArcValues(pathData = []) {
4272
+ let l = pathData.length;
4273
+ let pathDataN = [pathData[0]];
4274
+
4275
+ for (let i = 1; i < l; i++) {
4276
+ let com = pathData[i];
4277
+ let comPrev = pathData[i-1];
4278
+ let { type, values } = com;
4279
+
4280
+ if (type === 'A') {
4281
+
4282
+ // previous commands final on-path point
4283
+ let [x1, y1] = comPrev.values.slice(-2);
4284
+ let [rx, ry, xAxisRotation, largeArc, sweep, x2, y2] = values;
4285
+ let arcData = svgArcToCenterParam(x1, y1, rx, ry, xAxisRotation, largeArc, sweep, x2, y2);
4286
+
4287
+ // set true arc values
4288
+ com.values[0] = arcData.rx;
4289
+ com.values[1] = arcData.ry;
4290
+
4291
+ }
4292
+
4293
+ pathDataN.push(com);
4294
+
4295
+ }
4296
+ return pathDataN;
4297
+ }
4298
+
4299
+ /**
4300
+ * Minify arc radii
4301
+ * for semi circles
4302
+ * returns smaller rx and ry
4303
+ * values
4350
4304
  */
4351
4305
 
4352
4306
  function optimizeArcPathData(pathData = []) {
@@ -4364,6 +4318,11 @@ function optimizeArcPathData(pathData = []) {
4364
4318
 
4365
4319
  let [rx, ry, largeArc, x, y] = [values[0], values[1], values[3], values[5], values[6]];
4366
4320
  let comPrev = pathData[i - 1];
4321
+
4322
+ // force absolute
4323
+ rx = Math.abs(rx);
4324
+ ry = Math.abs(ry);
4325
+
4367
4326
  let [x0, y0] = [comPrev.values[comPrev.values.length - 2], comPrev.values[comPrev.values.length - 1]];
4368
4327
  let M = { x: x0, y: y0 };
4369
4328
  let p = { x, y };
@@ -4403,7 +4362,7 @@ function optimizeArcPathData(pathData = []) {
4403
4362
  if (isHorizontal || isVertical) {
4404
4363
 
4405
4364
  // check if semi circle
4406
- let needsTrueR = isHorizontal ? rx*1.9 > diffX : ry*1.9 > diffY;
4365
+ let needsTrueR = isHorizontal ? rx * 1.9 > diffX : ry * 1.9 > diffY;
4407
4366
 
4408
4367
  // is semicircle we can simplify rx
4409
4368
  if (!needsTrueR) {
@@ -5099,8 +5058,8 @@ function arcToBezier$1(p0, values, splitSegments = 1) {
5099
5058
  let phi = rotation ? rotation * TAU / 360 : 0;
5100
5059
  let sinphi = phi ? Math.sin(phi) : 0;
5101
5060
  let cosphi = phi ? Math.cos(phi) : 1;
5102
- let pxp = cosphi * (p0.x - x) / 2 + sinphi * (p0.y - y) / 2;
5103
- let pyp = -sinphi * (p0.x - x) / 2 + cosphi * (p0.y - y) / 2;
5061
+ let pxp = cosphi * (p0.x - x) *0.5 + sinphi * (p0.y - y) *0.5;
5062
+ let pyp = -sinphi * (p0.x - x) *0.5 + cosphi * (p0.y - y) *0.5;
5104
5063
 
5105
5064
  if (pxp === 0 && pyp === 0) {
5106
5065
  return []
@@ -5136,8 +5095,8 @@ function arcToBezier$1(p0, values, splitSegments = 1) {
5136
5095
 
5137
5096
  let centerxp = radicant ? radicant * rx / ry * pyp : 0;
5138
5097
  let centeryp = radicant ? radicant * -ry / rx * pxp : 0;
5139
- let centerx = cosphi * centerxp - sinphi * centeryp + (p0.x + x) / 2;
5140
- let centery = sinphi * centerxp + cosphi * centeryp + (p0.y + y) / 2;
5098
+ let centerx = cosphi * centerxp - sinphi * centeryp + (p0.x + x) * 0.5;
5099
+ let centery = sinphi * centerxp + cosphi * centeryp + (p0.y + y) * 0.5;
5141
5100
 
5142
5101
  let vx1 = (pxp - centerxp) / rx;
5143
5102
  let vy1 = (pyp - centeryp) / ry;
@@ -5159,15 +5118,17 @@ function arcToBezier$1(p0, values, splitSegments = 1) {
5159
5118
  ang2 = vectorAngle(vx1, vy1, vx2, vy2);
5160
5119
 
5161
5120
  if (sweepFlag === 0 && ang2 > 0) {
5162
- ang2 -= Math.PI * 2;
5121
+
5122
+ ang2 -= TAU;
5163
5123
  }
5164
5124
  else if (sweepFlag === 1 && ang2 < 0) {
5165
- ang2 += Math.PI * 2;
5125
+
5126
+ ang2 += TAU;
5166
5127
  }
5167
5128
 
5168
- let ratio = +(Math.abs(ang2) / (TAU / 4)).toFixed(0) || 1;
5129
+ let ratio = +(Math.abs(ang2) / (TAU * 0.25)).toFixed(0) || 1;
5169
5130
 
5170
- // increase segments for more accureate length calculations
5131
+ // increase segments for more accurate length calculations
5171
5132
  let segments = ratio * splitSegments;
5172
5133
  ang2 /= segments;
5173
5134
  let pathDataArc = [];
@@ -5179,7 +5140,8 @@ function arcToBezier$1(p0, values, splitSegments = 1) {
5179
5140
  const k = 0.551785;
5180
5141
  let a = ang2 === angle90 ? k :
5181
5142
  (
5182
- ang2 === -angle90 ? -k : 4 / 3 * Math.tan(ang2 / 4)
5143
+
5144
+ ang2 === -angle90 ? -k : 1.33333 * Math.tan(ang2 * 0.25)
5183
5145
  );
5184
5146
 
5185
5147
  let cos2 = ang2 ? Math.cos(ang2) : 1;
@@ -5226,7 +5188,7 @@ function pathDataCubicsToArc(pathData, { areaThreshold = 2.5 } = {}) {
5226
5188
  let comA = cubicCommandToArc(p0, cp1, cp2, p, areaThreshold);
5227
5189
  let comAN = cubicCommandToArc(comN.p0, comN.cp1, comN.cp2, comN.p, areaThreshold);
5228
5190
 
5229
- if (comA.isArc && comAN.isArc) {
5191
+ if (comA.isArc && comAN.isArc) {
5230
5192
 
5231
5193
  let dist = getDistManhattan(p0, comN.p);
5232
5194
  let maxDist = dist * 0.01;
@@ -5241,8 +5203,18 @@ function pathDataCubicsToArc(pathData, { areaThreshold = 2.5 } = {}) {
5241
5203
  let sweep = area < 0 ? 0 : 1;
5242
5204
 
5243
5205
  if (vertical || horizontal) {
5206
+
5244
5207
  rx = Math.min(rx, comAN.rx);
5245
5208
  ry = Math.min(ry, comAN.ry);
5209
+
5210
+ let diffR = Math.abs(rx - ry) / rx;
5211
+ let isSemiCircle = diffR < 0.025;
5212
+
5213
+ if (isSemiCircle) {
5214
+ rx = rx > 1 ? 1 : Math.min(rx, ry);
5215
+ ry = rx;
5216
+ }
5217
+
5246
5218
  pathData[c] = null;
5247
5219
  pathData[c + 1].type = 'A';
5248
5220
  pathData[c + 1].values = [rx, ry, 0, 0, sweep, comN.p.x, comN.p.y];
@@ -5265,33 +5237,61 @@ function cubicCommandToArc(p0, cp1, cp2, p, tolerance = 7.5) {
5265
5237
  let arcSegArea = 0, isArc = false;
5266
5238
 
5267
5239
  // check angles
5268
- let angle1 = getAngle(p0, cp1, true);
5269
- let angle2 = getAngle(p, cp2, true);
5240
+ let dx1 = (cp1.x - p0.x);
5241
+ let dy1 = (cp1.y - p0.y);
5242
+ let dx2 = (cp2.x - p.x);
5243
+ let dy2 = (cp2.y - p.y);
5244
+
5245
+ let thresh = getDistManhattan(p0, p) * 0.001;
5246
+
5247
+ let isVertical1 = dx1 < thresh;
5248
+ let isVertical2 = dx2 < thresh;
5249
+
5250
+ let isHorizontal1 = dy1 < thresh;
5251
+ let isHorizontal2 = dy2 < thresh;
5252
+
5253
+ let isRightAngle = (isVertical1 || isVertical2) && (isHorizontal1 || isHorizontal2);
5254
+
5255
+ /*
5256
+ let angle1 = getAngleFromDelta(dx1, dy1, true);
5257
+ let angle2 = getAngleFromDelta(dx2, dy2, true);
5270
5258
  let deltaAngle = Math.abs(angle1 - angle2) * 180 / Math.PI;
5271
5259
 
5272
5260
  let angleDiff = Math.abs((deltaAngle % 180) - 90);
5273
5261
  let isRightAngle = angleDiff < 3;
5262
+ */
5274
5263
 
5275
5264
  let rx = 0;
5276
5265
  let ry = 0;
5277
- let ptC;
5266
+ let ptC = p0;
5267
+ let r1 = 0, r2 = 0;
5278
5268
 
5279
5269
  if (isRightAngle) {
5280
- // point between cps
5281
5270
 
5282
- // center point
5283
- let cp1_r = rotatePoint(cp1, p0.x, p0.y, (Math.PI * -0.5));
5284
- let cp2_r = rotatePoint(cp2, p.x, p.y, (Math.PI * 0.5));
5271
+ if (isHorizontal1 && isVertical2) {
5272
+ ptC = { x: p0.x, y: p.y };
5273
+ r1 = Math.abs(p.x-p0.x);
5274
+ r2 = Math.abs(p.y-p0.y);
5275
+ }
5276
+ else if (isHorizontal2 && isVertical1) {
5277
+ ptC = { x: p.x, y: p0.y };
5278
+ r2 = Math.abs(p0.x-p.x);
5279
+ r1 = Math.abs(p0.y-p.y);
5280
+ }
5281
+
5282
+ /*
5283
+ if (r1 && r2) {
5285
5284
 
5286
- // assumed centroid
5287
- ptC = checkLineIntersection(p0, cp1_r, p, cp2_r, false);
5285
+ }
5288
5286
 
5289
- let pI = checkLineIntersection(p0, cp1, p, cp2, false);
5287
+ */
5290
5288
 
5291
- if (pI) {
5289
+ if (r1 && r2) {
5292
5290
 
5291
+ /*
5293
5292
  let r1 = getDistance(p0, pI);
5294
5293
  let r2 = getDistance(p, pI);
5294
+ */
5295
5295
 
5296
5296
  let rMax = +Math.max(r1, r2).toFixed(8);
5297
5297
  let rMin = +Math.min(r1, r2).toFixed(8);
@@ -5309,7 +5309,6 @@ function cubicCommandToArc(p0, cp1, cp2, p, tolerance = 7.5) {
5309
5309
  let circular = (100 / rx * Math.abs(rx - ry)) < 5;
5310
5310
 
5311
5311
  if (circular) {
5312
-
5313
5312
  rx = rMax;
5314
5313
  ry = rx;
5315
5314
  }
@@ -5335,7 +5334,7 @@ function cubicCommandToArc(p0, cp1, cp2, p, tolerance = 7.5) {
5335
5334
  arcSegArea = (Math.PI * (rx * ry)) / 4;
5336
5335
 
5337
5336
  // subtract polygon between start, end and center point
5338
- arcSegArea -= Math.abs(getPolygonArea([p0, p, pI]));
5337
+ arcSegArea -= Math.abs(getPolygonArea([p0, p, ptC]));
5339
5338
 
5340
5339
  let areaDiff = getRelativeAreaDiff(comArea, arcSegArea);
5341
5340
 
@@ -6405,59 +6404,50 @@ function pathDataRemoveColinear(pathData, {
6405
6404
  pathData[pathData.length - 1].type.toLowerCase() === 'z';
6406
6405
 
6407
6406
  for (let c = 1, l = pathData.length; c < l; c++) {
6408
-
6409
6407
  let com = pathData[c];
6408
+ let { type, values } = com;
6410
6409
  let comN = pathData[c + 1] || pathData[l - 1];
6411
-
6410
+ let valuesN = comN.values;
6412
6411
  let p1 = comN.type.toLowerCase() === 'z' ? M : { x: comN.values[comN.values.length - 2], y: comN.values[comN.values.length - 1] };
6413
6412
 
6414
- let { type, values } = com;
6415
6413
  let valsL = values.slice(-2);
6416
6414
  p = type !== 'Z' ? { x: valsL[0], y: valsL[1] } : M;
6417
6415
 
6416
+ let nextBezier = type!==comN.type && (comN.type==='C' || comN.type==='Q');
6417
+
6418
6418
  let area = p1 ? getPolygonArea([p0, p, p1], true) : Infinity;
6419
6419
  let distSquare = getSquareDistance(p0, p1);
6420
6420
  let distMax = distSquare ? distSquare / 333 * tolerance : 0;
6421
6421
 
6422
6422
  let isFlat = area < distMax;
6423
-
6424
6423
  let isFlatBez = false;
6424
+ let cpts = [];
6425
6425
 
6426
- /*
6427
- // flatness by cross product
6428
- let dx0 = Math.abs(p1.x - p0.x)
6429
- let dy0 = Math.abs(p1.y - p0.y)
6430
-
6431
- let dx1 = Math.abs(p.x - p0.x)
6432
- let dy1 = Math.abs(p.y - p0.y)
6433
-
6434
- let dx2 = Math.abs(p1.x - p.x)
6435
- let dy2 = Math.abs(p1.y - p.y)
6436
-
6437
- // zero length segments are flat
6438
- let isZeroLength = (!dy1 && !dx1) || (!dy2 && !dx2)
6439
- if (isZeroLength) isFlat = true;
6440
-
6441
- // check cross products for colinearity
6442
- if (!isFlat) {
6443
-
6444
- let cross0 = Math.abs(dx0 * dy1 - dy0 * dx1);
6426
+ /**
6427
+ * type change
6428
+ * check flatness
6429
+ */
6430
+ if (nextBezier) {
6431
+ cpts = comN.type === 'C' ?
6432
+ [{ x: valuesN[0], y: valuesN[1] }, { x: valuesN[2], y: valuesN[3] }] :
6433
+ (comN.type === 'Q' ? [{ x: valuesN[0], y: valuesN[1] }] : []);
6445
6434
 
6446
- let thresh = (dx0 + dy0) * 0.1
6435
+ isFlat = commandIsFlat([p0, ...cpts, p], { tolerance });
6447
6436
 
6448
- if ( cross0 < thresh) {
6437
+ /*
6449
6438
 
6450
- isFlat = true
6439
+ if(!isFlatBez){
6440
+ pathDataN.push(com)
6441
+ continue
6451
6442
  }
6452
- }
6453
- */
6443
+ */
6454
6444
 
6455
- if (!flatBezierToLinetos && type === 'C') isFlat = false;
6445
+ }
6456
6446
 
6457
6447
  // convert flat beziers to linetos
6458
6448
  if (flatBezierToLinetos && (type === 'C' || type === 'Q')) {
6459
6449
 
6460
- let cpts = type === 'C' ?
6450
+ cpts = type === 'C' ?
6461
6451
  [{ x: values[0], y: values[1] }, { x: values[2], y: values[3] }] :
6462
6452
  (type === 'Q' ? [{ x: values[0], y: values[1] }] : []);
6463
6453
 
@@ -6471,10 +6461,13 @@ function pathDataRemoveColinear(pathData, {
6471
6461
  }
6472
6462
  }
6473
6463
 
6474
- // colinear – exclude arcs (as always =) as semicircles won't have an area
6464
+ /**
6465
+ * colinear = simplification success
6466
+ * exclude arcs (as always =)
6467
+ * as semicircles won't have an area
6468
+ */
6475
6469
 
6476
6470
  if (isFlat && c < l - 1 && comN.type !== 'A' && (type === 'L' || (flatBezierToLinetos && isFlatBez))) {
6477
-
6478
6471
  continue;
6479
6472
  }
6480
6473
 
@@ -6958,9 +6951,7 @@ function refineAdjacentExtremes(pathData, {
6958
6951
  if (comEx.length === 1) {
6959
6952
 
6960
6953
  comEx = comEx[0];
6961
-
6962
6954
  pathData[i + 1] = null;
6963
-
6964
6955
  pathData[i + 2].values = [comEx.cp1.x, comEx.cp1.y, comEx.cp2.x, comEx.cp2.y, comEx.p.x, comEx.p.y];
6965
6956
  pathData[i + 2].cp1 = comEx.cp1;
6966
6957
  pathData[i + 2].cp2 = comEx.cp2;
@@ -7140,7 +7131,16 @@ function normalizePoly(pts, {
7140
7131
  return pts
7141
7132
  }
7142
7133
 
7134
+ if (pts.length && typeof pts[0] === 'string') {
7135
+ pts = pts.map(pt => {
7136
+ return toPointArray(pt.split(/,| /).filter(Boolean).map(Number))
7137
+ });
7138
+ pts = pts.flat(2);
7139
+
7140
+ }
7141
+
7143
7142
  if (flatten) pts = pts.flat(2);
7143
+
7144
7144
  let poly = toArray ? polyPtsToArray(pts) : polyArrayToObject(pts);
7145
7145
  return poly
7146
7146
  }
@@ -8500,7 +8500,7 @@ function setNormalizedTransformsToEl(el, {
8500
8500
  styleProps.remove.push('transform');
8501
8501
 
8502
8502
  // scale props like stroke width or dash-array
8503
- styleProps = scaleProps(styleProps, { props: ['stroke-width', 'stroke-dasharray'], scale: scaleX });
8503
+ styleProps = scaleProps(styleProps, { props: ['stroke-width', 'stroke-dasharray', 'stroke-dashoffset'], scale: scaleX });
8504
8504
 
8505
8505
  } else {
8506
8506
  el.setAttribute('transform', transComponents.matrixAtt);
@@ -9047,6 +9047,38 @@ function cleanUpSVG(svgMarkup, {
9047
9047
  });
9048
9048
  });
9049
9049
 
9050
+ /**
9051
+ * remove els and attributes
9052
+ */
9053
+
9054
+ // remove meta
9055
+ if (!allowMeta) removeElements.push('meta', 'metadata', 'desc', 'title');
9056
+
9057
+ if (removeClassNames) {
9058
+ removeSVGAttributes.push('class');
9059
+ removeElAttributes.push('class');
9060
+ }
9061
+
9062
+ if (removeIds) {
9063
+ removeSVGAttributes.push('id');
9064
+ removeElAttributes.push('id');
9065
+ }
9066
+
9067
+ // remove hidden elements
9068
+ removeHiddenSvgEls(svg);
9069
+
9070
+ // remove SVG elements
9071
+ removeSvgEls(svg, { removeElements, removeNameSpaced });
9072
+
9073
+ // remove SVG attributes
9074
+ removeSvgAtts(svg, removeSVGAttributes);
9075
+
9076
+ // remove SVG child element attributes
9077
+ removeSvgChildAtts(svg, removeElAttributes);
9078
+
9079
+ // general cleanup
9080
+ if (cleanupSVGAtts) cleanupSVGAttributes(svg, { removeIds, removeClassNames, removeDimensions, stylesToAttributes, allowMeta, allowAriaAtts, allowDataAtts });
9081
+
9050
9082
  // collect all elements' properties
9051
9083
  let svgElProps = [];
9052
9084
  let els = svg.querySelectorAll(`${renderedEls.join(', ')}`);
@@ -9067,14 +9099,15 @@ function cleanUpSVG(svgMarkup, {
9067
9099
  let styleProps = parseStylesProperties(el, propOptions);
9068
9100
  let stylePropsFiltered = {};
9069
9101
 
9070
- // convert pathLength before transforming
9071
- if(convertTransforms || attributesToGroup) convertPathLength=true;
9102
+ // reset remove array
9103
+ remove = [];
9072
9104
 
9073
- if (convertPathLength ) {
9105
+ // convert pathLength before transforming
9106
+ if (convertTransforms || attributesToGroup) convertPathLength = true;
9074
9107
 
9108
+ if (convertPathLength) {
9075
9109
  styleProps = convertPathLengthAtt(el, { styleProps });
9076
9110
  remove = [...new Set([...remove, ...styleProps.remove])];
9077
-
9078
9111
  }
9079
9112
 
9080
9113
  // get parent styles
@@ -9124,37 +9157,7 @@ function cleanUpSVG(svgMarkup, {
9124
9157
 
9125
9158
  remove = [...new Set([...remove, ...styleProps.remove])];
9126
9159
 
9127
- /**
9128
- * remove els and attributes
9129
- */
9130
-
9131
- // remove meta
9132
- if (!allowMeta) removeElements.push('meta', 'metadata', 'desc', 'title');
9133
-
9134
- if (removeClassNames) {
9135
- removeSVGAttributes.push('class');
9136
- removeElAttributes.push('class');
9137
- }
9138
-
9139
- if (removeIds) {
9140
- removeSVGAttributes.push('id');
9141
- removeElAttributes.push('id');
9142
- }
9143
-
9144
- // remove hidden elements
9145
- removeHiddenSvgEls(svg);
9146
-
9147
- // remove SVG elements
9148
- removeSvgEls(svg, { removeElements, removeNameSpaced });
9149
-
9150
- // remove SVG attributes
9151
- removeSvgAtts(svg, removeSVGAttributes);
9152
-
9153
- // remove SVG child element attributes
9154
- removeSvgChildAtts(svg, removeElAttributes);
9155
-
9156
- // general cleanup
9157
- if (cleanupSVGAtts) cleanupSVGAttributes(svg, { removeIds, removeClassNames, removeDimensions, stylesToAttributes, allowMeta, allowAriaAtts, allowDataAtts });
9160
+
9158
9161
 
9159
9162
  // all relative units to absolute
9160
9163
  if (toAbsoluteUnits) {
@@ -9199,7 +9202,6 @@ function cleanUpSVG(svgMarkup, {
9199
9202
  styleProps = setNormalizedTransformsToEl(el, { styleProps });
9200
9203
 
9201
9204
  remove = [...new Set([...remove, ...styleProps.remove])];
9202
-
9203
9205
  }
9204
9206
 
9205
9207
  /**
@@ -9224,13 +9226,6 @@ function cleanUpSVG(svgMarkup, {
9224
9226
  */
9225
9227
  removeAtts(el, remove);
9226
9228
 
9227
- /*
9228
- for (let i = 0; i < remove.length; i++) {
9229
- let att = remove[i];
9230
- el.removeAttribute(att)
9231
- }
9232
- */
9233
-
9234
9229
  } // endof style processing
9235
9230
 
9236
9231
  /**
@@ -10521,71 +10516,6 @@ function pathDataRevertCubicToQuadratic(pathData, tolerance=1) {
10521
10516
  return pathData
10522
10517
  }
10523
10518
 
10524
- function fixIntersectingCpts(pathData = []) {
10525
-
10526
- let l = pathData.length;
10527
- let p0 = { x: pathData[0].values[0], y: pathData[0].values[1] };
10528
- let p = p0;
10529
-
10530
- for (let i = 1; i < l; i++) {
10531
- let com = pathData[i];
10532
- let comPrev = pathData[i - 1] || null;
10533
- let { type, values } = com;
10534
- pathData[i + 1] ? pathData[i + 1] : null;
10535
-
10536
- if (type === 'C') {
10537
- let tx = 1.2;
10538
- let cp1 = { x: values[0], y: values[1] };
10539
- p = { x: values[4], y: values[5] };
10540
- let cp2 = { x: values[2], y: values[3] };
10541
-
10542
- interpolate(p0, cp1, tx );
10543
- let cp2_ex = interpolate( p, cp2, tx);
10544
-
10545
- comPrev.values.slice(-2);
10546
-
10547
- // extend tangents
10548
-
10549
- let ptI = checkLineIntersection(p0, cp1, p, cp2_ex, true, true);
10550
-
10551
-
10552
- /*
10553
- renderPoly(markers, [p0, cp1], 'orange', '0.75')
10554
- renderPoly(markers, [p, cp2], 'blue', '0.75')
10555
- renderPoly(markers, [p, cp2_ex], 'magenta', '0.75')
10556
-
10557
- renderPoint(markers, p0, 'orange', '0.75')
10558
- renderPoint(markers, cp1, 'magenta', '0.75')
10559
- renderPoint(markers, cp1_ex, 'blue', '0.5')
10560
-
10561
- renderPoint(markers, cp2, 'cyan', '0.75')
10562
- */
10563
-
10564
-
10565
-
10566
- /*
10567
- renderPoint(markers, p0, 'orange')
10568
- renderPoint(markers, p, 'magenta', '0.5')
10569
- */
10570
-
10571
- if (ptI) {
10572
- let t = 0.666;
10573
- cp1 = interpolate(p0, ptI, t);
10574
- cp2 = interpolate(p, ptI, t);
10575
- com.values = [cp1.x, cp1.y, cp2.x, cp2.y, p.x, p.y];
10576
-
10577
- }
10578
-
10579
- }
10580
-
10581
- if (values.length) {
10582
- p0 = p;
10583
- }
10584
- }
10585
-
10586
- return pathData;
10587
- }
10588
-
10589
10519
  function simplifyPolyRDP(pts, {quality = 0.9, width = 0, height = 0}={}) {
10590
10520
 
10591
10521
  /**
@@ -10780,860 +10710,817 @@ function pathDataFromPoly(pts, closed = true) {
10780
10710
 
10781
10711
  }
10782
10712
 
10783
- function getPolyCentroid(pts) {
10713
+ function refineAdjacentPolyExtremes(pts = []) {
10784
10714
 
10785
10715
  let l = pts.length;
10786
- let x = 0, y = 0;
10787
- for (let i = 0; l && i < l; i++) {
10788
- let pt = pts[i];
10789
- x += pt.x;
10790
- y += pt.y;
10791
- }
10792
-
10793
- let centroid = {x: x/l, y:y/l};
10794
- return centroid
10795
-
10796
- }
10797
-
10798
- function detectRegularPolygon(pts, centroid={x:0, y:0}) {
10799
- let rSq = getSquareDistance(pts[0], centroid);
10800
- let isRegular = true;
10801
-
10802
- for (let i = 1, l = pts.length; i < l; i++) {
10803
- let pt1 = pts[i];
10804
- let dist = getSquareDistance(pt1, centroid);
10805
-
10806
- let diff = Math.abs(rSq-dist);
10807
- let diffRel = diff/rSq;
10808
10716
 
10809
- if (diffRel > 0.05) {
10810
- return false;
10811
- }
10717
+ let { x, y, width, height, top, bottom, left, right } = getPolyBBox(pts);
10718
+ let threshShort = (width + height) * 0.05;
10719
+ let thresh = (width + height) * 0.001;
10812
10720
 
10813
- }
10814
- return isRegular;
10815
- }
10721
+ let pt0 = pts[0];
10722
+ let ptLast = pts[l - 1];
10816
10723
 
10817
- function analyzePoly(pts, {
10818
- x = 0,
10819
- y = 0,
10820
- width = 0,
10821
- height = 0,
10822
- debug = false
10823
- } = {}) {
10724
+ /**
10725
+ * cleanup close path - almost vertical or horizontal
10726
+ * average start and end extremes
10727
+ */
10728
+ let dx = Math.abs(ptLast.x - pt0.x);
10729
+ let dy = Math.abs(ptLast.y - pt0.y);
10824
10730
 
10825
- let l = pts.length;
10731
+ if (dy < threshShort || dx < threshShort) {
10826
10732
 
10827
- // bounding box of this sub poly
10828
- let bb0 = getPolyBBox(pts);
10733
+ if (pt0.isExtreme && !pt0.isCorner) {
10829
10734
 
10830
- if (!width || !height) {
10831
- ({ x, y, width, height } = bb0);
10832
- }
10735
+ let xAv = (pt0.x + ptLast.x) * 0.5;
10736
+ let yAv = (pt0.y + ptLast.y) * 0.5;
10833
10737
 
10834
- let thresh = (width + height) * 0.01;
10738
+ pt0.x = xAv;
10739
+ pt0.y = yAv;
10835
10740
 
10836
- // get areas an distances
10837
- for (let i = 0; i < l; i++) {
10838
- let p0 = i > 0 ? pts[i - 1] : pts[l - 1];
10839
- let p1 = pts[i];
10840
- let p2 = i < l - 1 ? pts[i + 1] : pts[l - 1];
10741
+ ptLast.x = xAv;
10742
+ ptLast.y = yAv;
10743
+ ptLast.isExtreme = true;
10841
10744
 
10842
- let area = getPolygonArea([p0, p1, p2], false);
10843
- p1.area = area;
10844
- p1.dist = getDistManhattan(p0, p1);
10845
- p1.idx = i;
10745
+ if (dy < thresh) {
10746
+ ptLast.tangentR.y = pt0.y;
10747
+ ptLast.tangentL.y = pt0.y;
10846
10748
 
10749
+ }
10750
+ if (dx < thresh) {
10751
+ ptLast.tangentR.x = pt0.x;
10752
+ ptLast.tangentL.x = pt0.x;
10753
+ }
10754
+ }
10847
10755
  }
10848
10756
 
10849
- for (let i = 0; i < l; i++) {
10850
- i > 1 ? pts[i - 2] : pts[l - 1];
10851
- let p0 = i > 0 ? pts[i - 1] : pts[l - 1];
10757
+ for (let i = 1; i < l; i++) {
10758
+ i > 0 ? pts[i - 1] : pts[l - 1];
10852
10759
  let p1 = pts[i];
10853
10760
  let p2 = i < l - 1 ? pts[i + 1] : pts[l - 1];
10761
+ let dist = getDistManhattan(p1, p2);
10854
10762
 
10855
- getSquareDistance(p0, p2) * 0.01;
10856
-
10857
- Math.abs(p0.area);
10858
- let area1 = Math.abs(p1.area);
10859
- Math.abs(p2.area);
10860
- let isCorner = false;
10861
-
10862
- let flat = !p1.area || area1 < thresh;
10863
-
10864
- getDistManhattan(p1, p0);
10865
-
10866
- /**
10867
- * check extremes
10868
- */
10869
-
10870
- let isExtreme = false;
10763
+ let { isHorizontal, isVertical, isCorner, isLong, isExtreme, isSemiExtreme, isDirChange } = p1;
10871
10764
 
10872
- // 1. total extreme
10873
- if ((p1.x === bb0.left || p1.x === bb0.right || p1.y === bb0.top || p1.y === bb0.bottom)) {
10874
- isExtreme = true;
10875
- }
10765
+ let extremes = [];
10876
10766
 
10877
- // 1.2 horizontal or vertical
10878
- let isHorizontal = p1.y === p0.y && p1.x !== p0.x;
10879
- let isVertical = p1.x === p0.x && p1.y !== p0.y;
10767
+ /*
10768
+ if(isExtreme && p0.isCorner && !isLong && !isCorner){
10769
+ isExtreme= false
10770
+ p1.isExtreme = false
10771
+ p1.isHorizontal = false
10772
+ p1.isVertical = false
10880
10773
 
10881
- if ((isHorizontal || isVertical)) {
10882
- p0.isExtreme = true;
10883
- isExtreme = true;
10774
+ continue;
10884
10775
  }
10776
+ */
10885
10777
 
10886
- // 1.3 is local or absolute extreme
10887
- let bb = getPolyBBox([p0, p2]); // local bb
10888
- let { left, right, top, bottom } = bb;
10889
-
10890
- let extremeLocal = (p1.x < left || p1.x > right || p1.y < top || p1.y > bottom);
10891
- if (!isExtreme && extremeLocal) {
10892
- isExtreme = true;
10778
+ /*
10779
+ if(isExtreme && p2.isCorner && !isLong && !isCorner && dist<threshShort*0.5){
10780
+ isExtreme= false
10781
+ p1.isExtreme = false
10782
+ p1.isHorizontal = false
10783
+ p1.isVertical = false
10893
10784
 
10785
+ if(isVertical){
10786
+ p2.tangentL.x = p2.x
10787
+ }
10788
+ if(isHorizontal){
10789
+ p2.tangentL.y = p2.y
10790
+ }
10791
+ continue;
10894
10792
  }
10793
+ */
10895
10794
 
10896
- /**
10897
- * 2. sign changes
10898
- */
10899
- let signChange = (p0.area < 0 && p1.area > 0) || (p0.area > 0 && p1.area < 0);
10900
- let isDirChange = signChange && !flat && !p0.isDirChange;
10901
-
10902
- /**
10903
- * 3. corners
10904
- */
10905
-
10906
- if (isExtreme) {
10907
-
10908
- let delta = getDeltaAngle(p1, p2, p0);
10909
- let { deltaAngleDeg } = delta;
10910
- deltaAngleDeg = Math.abs(deltaAngleDeg);
10795
+ if (isExtreme && !isCorner && p2.isExtreme) {
10796
+ extremes.push(p1);
10911
10797
 
10912
- let isCornerDelta = deltaAngleDeg > 10 && deltaAngleDeg < 160;
10913
- if (isCornerDelta) {
10798
+ for (let j = i + 1; j < l; j++) {
10799
+ let p2 = pts[j];
10800
+ dist = getDistManhattan(p1, p2);
10914
10801
 
10915
- isCorner = true;
10802
+ if (dist * 0.75 >= threshShort || p2.isCorner || p2.isDirChange) {
10803
+ break
10804
+ }
10805
+ if (p2.isExtreme && !p2.isDirChange && !p2.isCorner) {
10806
+ extremes.push(p2);
10807
+ }
10916
10808
  }
10917
10809
 
10918
- }
10810
+ if (extremes.length > 1) {
10919
10811
 
10920
- /*
10921
- let debug = true
10922
- if (debug) {
10812
+ // find best extreme according to angle
10813
+ let angleDiffMin = Infinity;
10923
10814
 
10924
- if ((isHorizontal || isVertical)) {
10925
- renderPoint(markers, p1, 'blue', '2%', '0.5')
10926
- renderPoint(markers, p0, 'blue', '2%', '0.5')
10927
- }
10815
+ let bestMatch = extremes[0];
10928
10816
 
10929
- if (isExtreme) {
10930
- renderPoint(markers, p1, 'cyan', '1.5%', '0.5')
10931
- }
10817
+
10818
+ extremes.forEach(pt => {
10932
10819
 
10933
- if (isDirChange) {
10934
- renderPoint(markers, p1, 'orange', '0.75%', '0.5')
10935
- }
10820
+ let angle = Math.abs(getAngleFromDelta(pt.dx2, pt.dy2, false)) * rad2Deg;
10821
+ let angleDiff = angle > 160 ? Math.abs(180 - angle) : (angle > 60 ? Math.abs(90 - angle) : angle);
10822
+ pt.angle = angle;
10823
+ pt.angleDiff = angleDiff;
10936
10824
 
10937
- if (isCorner) {
10938
- renderPoint(markers, p1, 'magenta', '2.75%', '0.5')
10939
- }
10940
- }
10941
- */
10825
+ if (angleDiff < angleDiffMin) {
10826
+ bestMatch = pt;
10827
+ angleDiffMin = angleDiff;
10942
10828
 
10943
- p1.isCorner = isCorner;
10944
- p1.isExtreme = isExtreme;
10945
- p1.isHorizontal = isHorizontal;
10946
- p1.isVertical = isVertical;
10947
- p1.isDirChange = isDirChange;
10829
+ }
10830
+ });
10948
10831
 
10949
- }
10832
+ let extremes2 = [];
10950
10833
 
10951
- // filter adjacent extremes
10952
- let pts1 = [];
10953
- let exclude = [];
10834
+ extremes.forEach((pt, i) => {
10954
10835
 
10955
- {
10956
- for (let i = 0; i < pts.length; i++) {
10957
- let p = pts[i];
10958
- let p1 = pts[i + 1] || null;
10959
- let p2 = pts[i + 2] || null;
10836
+ let isBestMatch = pt === bestMatch;
10960
10837
 
10961
- let extremes = [];
10838
+ if (isBestMatch) {
10962
10839
 
10963
- if (p1 && p1.isExtreme && p.isExtreme && !p.isCorner) {
10964
- let has2nd = p1.dist < thresh * 2 && !p1.isCorner;
10965
- let has3rd = p2 && p2.isExtreme && p2.dist < thresh * 2 && !p2.isCorner;
10840
+ if (pt.isHorizontal) {
10841
+ pt.tangentL.y = pt.y;
10842
+ pt.tangentR.y = pt.y;
10843
+ }
10844
+ if (pt.isVertical) {
10845
+ pt.tangentL.x = pt.x;
10846
+ pt.tangentR.x = pt.x;
10847
+ }
10966
10848
 
10967
- if (has2nd && !has3rd) {
10968
- extremes.push(p, p1);
10849
+ // renderPoint(markers, pt, 'green', '3%', '0.5')
10969
10850
 
10970
- } else if (has3rd) {
10971
- extremes.push(p, p1, p2);
10972
- /*
10973
- renderPoint(markers, p, 'green', '1%', '0.5')
10974
- renderPoint(markers, p1, 'red', '1%', '0.5')
10975
- renderPoint(markers, p2, 'blue', '1%', '0.5')
10976
- */
10977
- }
10851
+ }
10852
+ else {
10978
10853
 
10979
- if (extremes.length) {
10980
- // average extreme
10854
+ if (bestMatch) {
10981
10855
 
10982
- let x = extremes.reduce((a, b) => a + b.x, 0) / extremes.length;
10983
- let y = extremes.reduce((a, b) => a + b.y, 0) / extremes.length;
10856
+ if (!isBestMatch && (pt.x === bestMatch.x || pt.y === bestMatch.y)) {
10857
+ extremes2.push(pt);
10858
+ }
10859
+ pt.isExtreme = false;
10860
+ pt.isHorizontal = false;
10861
+ pt.isVertical = false;
10862
+ }
10984
10863
 
10985
- p.x = x;
10986
- p.y = y;
10864
+ }
10987
10865
 
10988
- i += extremes.length - 1;
10989
- }
10990
- }
10866
+ });
10991
10867
 
10992
- if (p.isExtreme || p.isCorner || p.isDirChange) {
10993
- exclude.push(pts1.length);
10994
- }
10868
+ // average coordinates
10869
+ if (extremes2.length) {
10870
+ bestMatch.x = (extremes2[0].x + bestMatch.x) * 0.5;
10871
+ bestMatch.y = (extremes2[0].y + bestMatch.y) * 0.5;
10995
10872
 
10996
- pts1.push(p);
10873
+ }
10997
10874
 
10998
- // remove last nearby extreme
10999
- let l2 = pts1.length;
11000
- let p0 = pts1[0];
11001
- let pL = pts1[l2 - 1];
11002
- let near0 = getDistManhattan(p0, pL) < thresh * 2;
11003
- if (p0.isExtreme && pL.isExtreme && near0) {
11004
- pL.x = p0.x;
11005
- pL.y = p0.y;
10875
+ i += extremes.length;
10876
+ continue;
11006
10877
  }
11007
10878
  }
11008
10879
 
11009
- pts = pts1;
11010
10880
  }
11011
10881
 
11012
- return pts
11013
10882
  }
11014
10883
 
11015
- function getPolyChunks(pts,
11016
- { closed = true,
11017
- keepCorners = true,
11018
- keepExtremes = true,
11019
- keepInflections = false
11020
- } = {}
11021
- ) {
11022
- let chunks = [[pts[0]]];
11023
-
11024
- let idx = 0;
11025
- let lastChunk = chunks[idx];
10884
+ function cleanupPolyKeypoints(pts = []) {
11026
10885
 
11027
10886
  let l = pts.length;
11028
10887
 
11029
- // render
10888
+ getPolyBBox(pts);
10889
+
10890
+ pts[0];
10891
+
11030
10892
  for (let i = 1; i < l; i++) {
11031
- i > 0 ? pts[i] : pts[l - 1];
10893
+ i > 0 ? pts[i - 1] : pts[l - 1];
11032
10894
  let p1 = pts[i];
11033
10895
  i < l - 1 ? pts[i + 1] : pts[l - 1];
11034
10896
 
11035
- // start new chunk
11036
- // keepInflections && p1.isDirChange
11037
- if ((keepExtremes && p1.isExtreme || keepCorners && p1.isCorner )) {
11038
- idx++;
11039
- chunks.push([]);
11040
- }
10897
+ let { isHorizontal, isVertical, isCorner, isLong, isExtreme, isSemiExtreme, isDirChange } = p1;
10898
+ let offset = 0;
11041
10899
 
11042
- lastChunk = chunks[idx];
11043
- lastChunk.push(p1);
11044
- }
10900
+ if (!isSemiExtreme){
11045
10901
 
11046
- // test render
10902
+ continue
10903
+ }
11047
10904
 
11048
- return chunks;
11049
- }
10905
+ if (isSemiExtreme || isExtreme) {
10906
+ let semiExtremes = isSemiExtreme ? [p1] : [];
11050
10907
 
11051
- /**
11052
- * Fit one or more Bezier curves to a set of pts.
11053
- *
11054
- */
11055
- function fitCurveSchneider(pts, {
11056
- maxError = 0,
11057
- adjustCpts = true,
11058
- harmonize = true,
11059
- keepCorners = true
11060
- } = {}) {
10908
+ for (let j = i + 1; j < l; j++) {
10909
+ let p2 = pts[j];
11061
10910
 
11062
- if (pts.length === 1) {
11063
- return [];
11064
- }
10911
+ if (!p2.isSemiExtreme || p2.isExtreme || p2.isCorner){
10912
+ break
10913
+ }
10914
+ semiExtremes.push(p2);
10915
+ }
11065
10916
 
11066
- // single lineto
11067
- if (pts.length === 2) {
11068
- return [
11069
- { type: 'L', values: [pts[0].x, pts[0].y] },
11070
- { type: 'L', values: [pts[1].x, pts[1].y] }
11071
- ]
11072
- }
10917
+ if (semiExtremes.length > 1) {
11073
10918
 
11074
- // prevent bulging
10919
+ let semiExtremeMid = semiExtremes[Math.floor(semiExtremes.length*0.5)];
10920
+ let p1_1 = semiExtremes[0];
10921
+ let p2_1 = semiExtremes[semiExtremes.length - 1];
10922
+ let ptI = checkLineIntersection(p1_1, p1_1.tangentR, p2_1, p2_1.tangentL, false, true);
11075
10923
 
11076
- let len = pts.length;
11077
- let leftTangent = createTangent(pts[1], pts[0]);
11078
- let rightTangent = createTangent(pts[len - 2], pts[len - 1]);
11079
- let beziers = fitCubic(pts, leftTangent, rightTangent, maxError, keepCorners);
10924
+ semiExtremes.forEach(pt=>{
10925
+ pt.isSemiExtreme=false;
10926
+ });
10927
+ semiExtremeMid.isSemiExtreme=true;
11080
10928
 
11081
- // create pathdata
11082
- let pathData = bezierPtsToPathData(beziers);
11083
- let cp1, cp2;
10929
+ // interpolate mid point
10930
+ if (ptI) {
10931
+ let pI_1 = interpolate(p1_1, ptI, 0.5);
10932
+ let pI_2 = interpolate(p2_1, ptI, 0.5);
10933
+ let pI_3 = interpolate(pI_2, pI_1, 0.5);
11084
10934
 
11085
- adjustCpts = false;
10935
+ semiExtremeMid.x = pI_3.x;
10936
+ semiExtremeMid.y = pI_3.y;
10937
+ semiExtremeMid.tangentL = pI_1;
10938
+ semiExtremeMid.tangentR = pI_2;
11086
10939
 
11087
- adjustCpts = true;
10940
+ i += offset;
10941
+ continue
10942
+ }
11088
10943
 
11089
- if (adjustCpts) {
10944
+ }
10945
+ }
10946
+ // find significant of same type
11090
10947
 
11091
- let len2 = pathData.length;
11092
- let com1 = pathData[0];
10948
+ }
11093
10949
 
11094
- // last cubic segment
11095
- let com2 = pathData[len2 - 1];
10950
+ /*
10951
+ // update index
10952
+ ptsClean.forEach((pt, i) => {
10953
+ pt.idx = i
10954
+ })
10955
+ */
11096
10956
 
11097
- let p0 = { x: pts[0].x, y: pts[0].y };
11098
- let p1 = { x: pts[1].x, y: pts[1].y };
11099
- let p2 = pts[2] ? { x: pts[2].x, y: pts[2].y } : null;
10957
+ return pts;
11100
10958
 
11101
- if (p2) {
11102
- cp1 = { x: com1.values[0], y: com1.values[1] };
11103
- cp1 = adjustTangentAngle(cp1, p0, p1, p2);
11104
- com1.values[0] = cp1.x;
11105
- com1.values[1] = cp1.y;
11106
- }
10959
+ }
11107
10960
 
11108
- let pL = { x: pts[len - 1].x, y: pts[len - 1].y };
11109
- let pL1 = { x: pts[len - 2].x, y: pts[len - 2].y };
11110
- let pL2 = pts[len - 3] ? { x: pts[len - 3][0], y: pts[len - 3][1] } : null;
10961
+ function adjustTangentAngle(cp, p0, p1, p2) {
10962
+ let ang1 = getAngle(p0, p1);
10963
+ let ang2 = getAngle(p0, p2);
10964
+ let angDiff = (ang2 - ang1);
11111
10965
 
11112
- if (pL2) {
11113
- cp2 = { x: com2.values[2], y: com2.values[3] };
11114
- cp2 = adjustTangentAngle(cp2, pL, pL1, pL2);
11115
- com2.values[2] = cp2.x;
11116
- com2.values[3] = cp2.y;
11117
- }
10966
+ let f = 0.666;
10967
+ f = 1;
11118
10968
 
11119
- /*
11120
- // harmonize too tight tangents
11121
- let harmonize = true;
11122
- harmonize = false;
11123
- if (harmonize) {
11124
- pathData = harmonizeCubicCptsThird([{ type: 'M', values: [pts[0].x, pts[0].y] },
11125
- ...pathData])
11126
- pathData.shift()
11127
- }
11128
- */
10969
+ cp = rotatePoint(cp, p0.x, p0.y, -angDiff * f);
10970
+ return cp
10971
+ }
11129
10972
 
11130
- }
10973
+ function getTangents(pts = [], {
10974
+ x = 0,
10975
+ y = 0,
10976
+ width = 0,
10977
+ height = 0,
10978
+ debug = false,
10979
+ closed=false,
10980
+ } = {}) {
11131
10981
 
11132
- return pathData
11133
- }
10982
+ let l = pts.length;
11134
10983
 
11135
- /**
11136
- * Fit a Bezier curve to a (sub)set of digitized pts.
11137
- * Your code should not call this function directly. Use fitCurve instead.
11138
- *control-point-1, control-point-2, second-point] and pts are [x, y]
11139
- */
11140
- function fitCubic(pts, leftTangent, rightTangent, error, keepCorners = false) {
10984
+ // bounding box of this sub poly
10985
+ if (!width || !height) {
10986
+ ({ x, y, width, height } = getPolyBBox(pts));
10987
+ }
11141
10988
 
11142
- let MaxIterations = 20;
11143
- let bezCurve;
10989
+ // threshold for horizontal or vertical detection
11144
10990
 
11145
- /*
10991
+ for (let i = 0; i < l; i++) {
10992
+ let p0 = i > 0 ? pts[i - 1] : pts[l - 1];
10993
+ let p1 = pts[i];
10994
+ let p2 = i < l - 1 ? pts[i + 1] : pts[l - 1];
10995
+ let p3 = i < l - 1 ? pts[i + 2] : pts[l - 1];
11146
10996
 
11147
- if (pts.length === 2) {
11148
- let dist = getDistance(pts[0], pts[1], false) * 0.333;
11149
- bezCurve = [pts[0], addArrays(pts[0], mulItems(leftTangent, dist)), addArrays(pts[1], mulItems(rightTangent, dist)), pts[1]];
11150
- return [bezCurve];
11151
- }
11152
- */
10997
+ let { isHorizontal, isVertical, isCorner, isLong, isExtreme, isSemiExtreme, isDirChange } = p1;
11153
10998
 
11154
- let u = chordLengthParameterize(pts);
11155
- let _generateAndReport = generateAndReport(pts, u, u, leftTangent, rightTangent);
10999
+ // default
11000
+ let tangentL = { x: p1.x - p1.dx2 * 0.5, y: p1.y - p1.dy2 * 0.5 };
11001
+ let tangentR = { x: p1.x + p1.dx2 * 0.5, y: p1.y + p1.dy2 * 0.5 };
11156
11002
 
11157
- bezCurve = _generateAndReport[0];
11003
+ // average first tangent
11004
+ if(i===0){
11005
+ tangentR = adjustTangentAngle(p2, p1, p2, p3);
11006
+ }
11158
11007
 
11159
- let maxError = _generateAndReport[1];
11160
- let splitPoint = _generateAndReport[2];
11008
+ /**
11009
+ * add left and right tangents
11010
+ * for later curve fitting
11011
+ */
11161
11012
 
11162
- // check if curve is bulged
11163
- if (keepCorners) {
11164
- let checkBulge = areaDeviationTooLarge(pts, bezCurve);
11165
- let { isBulged, bezierNew } = checkBulge;
11166
- if (isBulged) {
11167
- // return cubic corner segment
11168
- bezCurve = bezierNew;
11013
+ if (isHorizontal && !isCorner) {
11014
+ tangentL = { x: p1.x - p1.dx2*0.5, y: p1.y };
11015
+ tangentR = { x: p1.x + p1.dx2*0.5, y: p1.y };
11016
+ }
11017
+ else if (isVertical) {
11018
+ tangentL = { x: p1.x , y: p1.y - p1.dy2*0.5 };
11019
+ tangentR = { x: p1.x , y: p1.y + p1.dy2*0.5 };
11169
11020
  }
11170
- }
11171
11021
 
11172
- if (maxError === 0 || maxError < error) {
11173
- return [bezCurve];
11174
- }
11022
+ if (!isExtreme && p1.isLong) {
11023
+ tangentL = { x: p1.x - p1.dx*0.5, y: p1.y - p1.dy*0.5 };
11024
+ tangentR = { x: p1.x + p1.dx*0.5, y: p1.y + p1.dy*0.5 };
11025
+ }
11175
11026
 
11176
- if (maxError < error * error) {
11027
+ /*
11177
11028
 
11178
- let uPrime = u;
11179
- let prevErr = maxError;
11180
- let prevSplit = splitPoint;
11029
+ if (isDirChange && !isCorner && !isExtreme) {
11030
+ p1.tangentL = { x: p1.x-p1.dx2*0.5, y: p1.y-p1.dy2*0.5 }
11031
+ p1.tangentR = { x: p1.x+p1.dx2*0.5, y: p1.y+p1.dy2*0.5 }
11032
+ }
11033
+ */
11181
11034
 
11182
- for (let i = 0; i < MaxIterations; i++) {
11035
+ if (isCorner) {
11183
11036
 
11184
- uPrime = reparameterize(bezCurve, pts, uPrime);
11037
+ tangentL = {x:p0.x, y:p0.y};
11038
+ tangentR = {x:p2.x, y:p2.y};
11185
11039
 
11186
- let _generateAndReport2 = generateAndReport(pts, u, uPrime, leftTangent, rightTangent);
11040
+ let p0_1 = pts[i - 2] ? pts[i - 2] : pts[l - 1];
11187
11041
 
11188
- bezCurve = _generateAndReport2[0];
11189
- maxError = _generateAndReport2[1];
11190
- splitPoint = _generateAndReport2[2];
11042
+ let p2_1 = pts[i + 2] ? pts[i + 2] : pts[1];
11191
11043
 
11192
- if (maxError < error) {
11193
- return [bezCurve];
11044
+ // adjust angle
11045
+ if (!p0.isCorner) {
11046
+ tangentL = adjustTangentAngle(p0, p1, p0, p0_1);
11194
11047
  }
11195
11048
 
11196
- else if (splitPoint === prevSplit) {
11197
- let errChange = maxError / prevErr;
11198
- if (errChange > .9999 && errChange < 1.0001) {
11199
- break;
11200
- }
11049
+ if (!p2.isCorner) {
11050
+ tangentR = adjustTangentAngle(tangentR, p1, p2, p2_1);
11201
11051
  }
11202
11052
 
11203
- prevErr = maxError;
11204
- prevSplit = splitPoint;
11053
+ /*
11054
+ renderPoint(markers, p0, 'darkblue', '0.75%', '0.5')
11055
+ // renderPoint(markers, p0_1, 'blue', '0.5%')
11056
+ renderPoint(markers, tangentL, 'blue', '0.5%', '0.5')
11057
+ renderPoint(markers, tangentR, 'blue', '0.5%', '0.5')
11058
+ */
11059
+
11205
11060
  }
11206
- }
11207
11061
 
11208
- let beziers = [];
11209
- let centerVector = subtract(pts[splitPoint - 1], pts[splitPoint + 1]);
11062
+ p1.tangentL = tangentL;
11063
+ p1.tangentR = tangentR;
11064
+
11065
+ /*
11066
+ debug = true
11067
+ if(debug){
11068
+ if (isCorner || isSemiExtreme || isDirChange || isExtreme) {
11069
+ renderPoint(markers, p1.tangentL, 'darkred', '0.5%')
11070
+ renderPoint(markers, p1.tangentR, 'darkblue', '0.5%')
11071
+ }
11072
+ }
11073
+ */
11210
11074
 
11211
- if (centerVector.x === 0 && centerVector.y === 0) {
11212
- centerVector = subtract(pts[splitPoint - 1], pts[splitPoint]);
11213
- let _ref = { x: -centerVector.y, y: centerVector.x };
11214
- centerVector.x = _ref.x;
11215
- centerVector.y = _ref.y;
11216
11075
  }
11217
11076
 
11218
- let toCenterTangent = normalize(centerVector);
11077
+ }
11219
11078
 
11220
- let fromCenterTangent = mulItems(toCenterTangent, -1);
11079
+ function getPolyCentroid(pts) {
11221
11080
 
11222
- if (pts.length === 3) ;
11081
+ let l = pts.length;
11082
+ let x = 0, y = 0;
11083
+ for (let i = 0; l && i < l; i++) {
11084
+ let pt = pts[i];
11085
+ x += pt.x;
11086
+ y += pt.y;
11087
+ }
11223
11088
 
11224
- beziers.push(
11225
- ...fitCubic(pts.slice(0, splitPoint + 1), leftTangent, toCenterTangent, error, keepCorners),
11226
- ...fitCubic(pts.slice(splitPoint), fromCenterTangent, rightTangent, error, keepCorners)
11227
- );
11089
+ let centroid = { x: x / l, y: y / l };
11090
+ return centroid
11228
11091
 
11229
- return beziers;
11230
11092
  }
11231
11093
 
11232
- /**
11233
- * Use least-squares method to find Bezier control pts for region.
11234
- */
11235
- function generateBezier(pts, parameters, leftTangent, rightTangent) {
11094
+ function detectRegularPolygon(pts, centroid = { x: 0, y: 0 }) {
11095
+ let rSq = getSquareDistance(pts[0], centroid);
11096
+ let isRegular = true;
11236
11097
 
11237
- let a, tmp, u, ux, firstPoint = pts[0], lastPoint = pts[pts.length - 1];
11098
+ for (let i = 1, l = pts.length; i < l; i++) {
11099
+ let pt1 = pts[i];
11100
+ let dist = getSquareDistance(pt1, centroid);
11238
11101
 
11239
- let bezCurve = [firstPoint, null, null, lastPoint];
11240
- let A = zeros_Xx2x2(parameters.length);
11241
- let len = parameters.length;
11102
+ let diff = Math.abs(rSq - dist);
11103
+ let diffRel = diff / rSq;
11242
11104
 
11243
- for (let i = 0; i < len; i++) {
11244
- u = parameters[i];
11245
- ux = 1 - u;
11246
- a = A[i];
11105
+ if (diffRel > 0.05) {
11106
+ return false;
11107
+ }
11247
11108
 
11248
- a[0] = mulItems(leftTangent, 3 * u * (ux * ux));
11249
- a[1] = mulItems(rightTangent, 3 * ux * (u * u));
11250
11109
  }
11110
+ return isRegular;
11111
+ }
11112
+
11113
+ function analyzePoly(pts, {
11114
+ x = 0,
11115
+ y = 0,
11116
+ width = 0,
11117
+ height = 0,
11118
+ debug = false
11119
+ } = {}) {
11251
11120
 
11252
- let C = [[0, 0], [0, 0]];
11253
- let X = [0, 0];
11254
11121
  let l = pts.length;
11122
+ let left = x;
11123
+ let top = y;
11124
+ let right = x + width;
11125
+ let bottom = y + height;
11255
11126
 
11256
- for (let i = 0; i < l; i++) {
11257
- u = parameters[i];
11258
- a = A[i];
11127
+ if (!width || !height) {
11128
+ ({ x, y, width, height, top, bottom, left, right } = getPolyBBox(pts));
11129
+ }
11259
11130
 
11260
- C[0][0] += dot(a[0], a[0]);
11261
- C[0][1] += dot(a[0], a[1]);
11262
- C[1][0] += dot(a[0], a[1]);
11263
- C[1][1] += dot(a[1], a[1]);
11131
+ // round
11132
+ [x, y, width, height, top, bottom, left, right] = [x, y, width, height, top, bottom, left, right].map(val => +val.toFixed(8));
11264
11133
 
11265
- tmp = subtract(pts[i], pointAtT([firstPoint, firstPoint, lastPoint, lastPoint], u));
11134
+ // bounding box of this sub poly
11135
+ let bb0 = { x, y, top, left, width, height, right, bottom };
11266
11136
 
11267
- X[0] += dot(a[0], tmp);
11268
- X[1] += dot(a[1], tmp);
11269
- }
11137
+ let thresh = (width + height) * 0.01;
11270
11138
 
11271
- let det_C0_C1 = C[0][0] * C[1][1] - C[1][0] * C[0][1];
11272
- let det_C0_X = C[0][0] * X[1] - C[1][0] * X[0];
11273
- let det_X_C1 = X[0] * C[1][1] - X[1] * C[0][1];
11139
+ // threshold for horizontal or vertical detection
11140
+ let thresh2 = thresh * 0.75;
11274
11141
 
11275
- let alpha_l = det_C0_C1 === 0 ? 0 : det_X_C1 / det_C0_C1;
11276
- let alpha_r = det_C0_C1 === 0 ? 0 : det_C0_X / det_C0_C1;
11277
- let segLength = getDistance(firstPoint, lastPoint, false);
11278
- let epsilon = 1.0e-6 * segLength;
11142
+ let dims = [];
11279
11143
 
11280
- if (alpha_l < epsilon || alpha_r < epsilon) {
11144
+ /*
11145
+ pts.forEach(pt=>{
11146
+ renderPoint(markers, pt, 'red', '2.5%')
11147
+ })
11148
+ */
11149
+
11150
+ /**
11151
+ * 1st run:
11152
+ * collect more details
11153
+ * area for sign change detection
11154
+ * deltas and distances
11155
+ */
11156
+ for (let i = 0; i < l; i++) {
11157
+ let p0 = i > 0 ? pts[i - 1] : pts[l - 1];
11158
+ let p1 = pts[i];
11159
+ let p2 = i < l - 1 ? pts[i + 1] : pts[l - 1];
11160
+
11161
+ let area = getPolygonArea([p0, p1, p2], false);
11162
+ let dx = i > 0 ? +(p1.x - p0.x).toFixed(7) : 0;
11163
+ let dy = i > 0 ? +(p1.y - p0.y).toFixed(7) : 0;
11164
+
11165
+ let dx2 = +(p2.x - p0.x).toFixed(7);
11166
+ let dy2 = +(p2.y - p0.y).toFixed(7);
11167
+
11168
+ p1.area = area;
11169
+ p1.dist = i > 0 ? getDistManhattan(p0, p1) : 0;
11170
+ // add dist for long/short segment detection
11171
+ dims.push(p1.dist);
11172
+ p1.idx = i;
11173
+ p1.dx = dx;
11174
+ p1.dy = dy;
11175
+ p1.dx2 = dx2;
11176
+ p1.dy2 = dy2;
11281
11177
 
11282
- bezCurve[1] = addArrays(firstPoint, mulItems(leftTangent, segLength * 0.333));
11283
- bezCurve[2] = addArrays(lastPoint, mulItems(rightTangent, segLength * 0.333));
11284
- } else {
11285
- // First and last control pts of the Bezier curve
11286
- bezCurve[1] = addArrays(firstPoint, mulItems(leftTangent, alpha_l));
11287
- bezCurve[2] = addArrays(lastPoint, mulItems(rightTangent, alpha_r));
11288
11178
  }
11289
11179
 
11290
- return bezCurve;
11291
- }
11180
+ /**
11181
+ * find average segment length
11182
+ * for long/short segment detection
11183
+ */
11184
+ dims = dims.filter(Boolean).sort((a, b) => a - b);
11185
+ let lenD = dims.length;
11186
+ let dimMin = dims[0];
11187
+ dims[lenD - 1];
11292
11188
 
11293
- function generateAndReport(pts, paramsOrig, paramsPrime, leftTangent, rightTangent) {
11294
- let bezCurve, maxError, splitPoint;
11189
+ let dimAv = dims.reduce((a, b) => a + b, 0) / lenD;
11190
+ let dimShort = (dimMin + dimAv) * 0.5;
11191
+ let dimLong = dimAv * 2;
11295
11192
 
11296
- bezCurve = generateBezier(pts, paramsPrime, leftTangent, rightTangent);
11297
- let _computeMaxError = computeMaxError(pts, bezCurve, paramsOrig);
11193
+ /*
11194
+ // round to adjust for minor deviations
11195
+ let idx_q = Math.ceil(lenD * 0.25);
11196
+ let dim_mid = dims[Math.floor(lenD * 0.5)]
11197
+ let dims_min = dims.slice(0, Math.floor(lenD * 0.25));
11198
+ let dim_min = ((dims_min.reduce((a, b) => a + b, 0) / idx_q) + dim_mid) * 0.5;
11298
11199
 
11299
- maxError = _computeMaxError[0];
11300
- splitPoint = _computeMaxError[1];
11200
+ let threshold = 75
11201
+ let decimalsAuto = dim_min > threshold * 1.5 ? 0 : Math.floor(threshold / dim_min).toString().length
11301
11202
 
11302
- return [bezCurve, maxError, splitPoint];
11303
- }
11203
+ // clamp
11204
+ decimalsAuto = Math.min(Math.max(0, decimalsAuto), 8)
11304
11205
 
11305
- /**
11306
- * Given set of pts and their parameterization, try to find a better parameterization.
11307
- */
11308
- function reparameterize(bezier, pts, parameters) {
11309
- return parameters.map((p, i) => {
11310
- return newtonRaphsonRootFind(bezier, pts[i], p);
11311
- });
11312
- }
11313
- /**
11314
- * Use Newton-Raphson iteration to find better root.
11315
- */
11206
+ pts = roundPoly(pts, 2)
11207
+ console.log(pts);
11208
+ */
11316
11209
 
11317
- function newtonRaphsonRootFind(bez, point, u) {
11318
- // bez is [p0, cp1, cp2, p1] where each is [x, y]
11319
- // point is the target point [x, y] we're trying to get close to
11320
- // u is our current parameter value (0-1)
11210
+ /**
11211
+ * analyze topology:
11212
+ * find significant commands:
11213
+ * extremes, inflections etc.
11214
+ */
11215
+ for (let i = 0; i < l; i++) {
11321
11216
 
11322
- // Calculate q(u) - point (the vector from target to curve point)
11217
+ let p0 = i > 0 ? pts[i - 1] : pts[l - 1];
11218
+ let p1 = pts[i];
11219
+ let p2 = i < l - 1 ? pts[i + 1] : pts[l - 1];
11323
11220
 
11324
- let q = pointAtT(bez, u);
11221
+ // convert area to absolute for flatness checks
11222
+ let area1 = Math.abs(p1.area);
11223
+ let isCorner = false;
11224
+ let isSemiExtreme = false;
11225
+ let isShort = false;
11226
+ let isLong = false;
11325
11227
 
11326
- let dx = q.x - point.x;
11327
- let dy = q.y - point.y;
11228
+ /**
11229
+ * detect short or long
11230
+ */
11231
+ if (p1.dist > dimLong) {
11232
+ isLong = true;
11233
+ }
11328
11234
 
11329
- // First derivative (tangent vector at u)
11330
- let qp = bezierQprime(bez, u);
11235
+ if (p1.dist < dimShort) {
11236
+ isShort = true;
11237
+ }
11331
11238
 
11332
- // Numerator: dot product of (q(u) - point) and q'(u)
11333
- // This represents how much the error aligns with the tangent
11334
- let numerator = dx * qp[0] + dy * qp[1];
11239
+ let flat = !p1.area || area1 < thresh;
11335
11240
 
11336
- // Denominator: |q'(u)|² + 2 * (q(u)-point) · q''(u)
11337
- // First part: squared length of tangent vector
11338
- let qpLenSq = qp[0] * qp[0] + qp[1] * qp[1];
11241
+ /**
11242
+ * check extremes
11243
+ */
11244
+ let isExtreme = false;
11339
11245
 
11340
- // Second derivative
11341
- let qpp = bezierQprime(bez, u, true);
11246
+ // 1. total extreme
11247
+ let isTop = p1.y === bb0.top;
11248
+ let isBottom = p1.y === bb0.bottom;
11249
+ let isLeft = p1.x === bb0.left;
11250
+ let isRight = p1.x === bb0.right;
11342
11251
 
11343
- // Second part: 2 * (q(u)-point) · q''(u)
11344
- let secondPart = 2 * (dx * qpp[0] + dy * qpp[1]);
11252
+ if (isTop || isBottom || isLeft || isRight) {
11253
+ isExtreme = true;
11345
11254
 
11346
- let denominator = qpLenSq + secondPart;
11255
+ }
11347
11256
 
11348
- if (Math.abs(denominator) < 1e-10) { // Avoid division by zero
11349
- return u;
11350
- }
11257
+ // 1.2 horizontal or vertical
11258
+ /*
11259
+ let isHorizontal = isTop || isBottom || (p1.y === p0.y && p1.x !== p0.x);
11260
+ let isVertical = isLeft || isRight || (p1.x === p0.x && p1.y !== p0.y)
11351
11261
 
11352
- // Newton-Raphson step: u_new = u - f(u)/f'(u)
11353
- return u - numerator / denominator;
11354
- }
11262
+ if ((isHorizontal || isVertical)) {
11355
11263
 
11356
- /**
11357
- * Assign parameter values to digitized pts using relative distances between pts.
11358
- */
11359
- function chordLengthParameterize(pts) {
11360
- let u = [];
11361
- let l = pts.length;
11362
- let p0 = pts[0];
11363
- let p = pts[1];
11364
- let currU = 0;
11365
- let prevU = 0;
11264
+ let diffX = Math.abs(p0.x - p1.x)
11265
+ let diffY = Math.abs(p0.y - p1.y)
11366
11266
 
11367
- for (let i = 0; i < l; i++) {
11368
- p = pts[i];
11267
+ if (isLong) {
11268
+ }
11369
11269
 
11370
- currU = prevU + getDistance(p, p0, false);
11371
- u.push(currU);
11372
- prevU = currU;
11270
+ if (isLong && (diffY < thresh2) && diffX > thresh) {
11271
+ p0.isExtreme = true;
11272
+ p0.isHorizontal = true;
11273
+ }
11274
+ else if (isLong && (diffX < thresh2) && diffY > thresh) {
11275
+ p0.isExtreme = true;
11276
+ p0.isVertical = true;
11277
+ }
11373
11278
 
11374
- p0 = p;
11375
- }
11279
+ isExtreme = true
11280
+ }
11281
+ */
11376
11282
 
11377
- u = u.map(function (x) {
11378
- return x / prevU;
11379
- });
11283
+ let dx = Math.abs(p0.x - p1.x);
11284
+ let dy = Math.abs(p0.y - p1.y);
11380
11285
 
11381
- return u;
11382
- }
11383
- /**
11384
- * Find the maximum squared distance of digitized pts to fitted curve.
11385
- */
11386
- function computeMaxError(pts, bez, parameters) {
11387
- let dist,
11388
- maxDist,
11389
- splitPoint,
11390
- i, point, t;
11286
+ let vh_thresh = thresh * 0.05;
11287
+ // vh_thresh = thresh * 0.25
11288
+ let isHorizontal = isTop || isBottom || (p1.y === p0.y && p1.x !== p0.x) || (dy <= vh_thresh);
11289
+ let isVertical = (isLeft || isRight || (p1.x === p0.x && p1.y !== p0.y) || (dx <= vh_thresh));
11391
11290
 
11392
- maxDist = 0;
11393
- splitPoint = Math.floor(pts.length * 0.5);
11291
+ // renderPoint(markers, p1, 'red', '0.5%')
11394
11292
 
11395
- let t_distMap = mapTtoRelativeDistances(bez, 10);
11396
- let l = pts.length;
11397
- let ptOnPath = null;
11293
+ if (p1.y === p0.y) ;
11398
11294
 
11399
- for (i = 0; i < l; i++) {
11400
- point = pts[i];
11295
+ if ((isHorizontal || isVertical)) {
11401
11296
 
11402
- t = find_t(parameters[i], t_distMap, 10);
11297
+ if (isLong && isHorizontal) {
11298
+ p0.isExtreme = true;
11299
+ p0.isHorizontal = true;
11403
11300
 
11404
- ptOnPath = pointAtT(bez, t);
11405
- dist = getSquareDistance(ptOnPath, point);
11301
+ }
11302
+ else if (isLong && isVertical) {
11303
+ p0.isExtreme = true;
11304
+ p0.isVertical = true;
11305
+ }
11406
11306
 
11407
- /*
11408
- console.log('v', v);
11409
- v = subtract(pointAtT(bez, t), point);
11410
- dist = v.x * v.x + v.y * v.y;
11411
- */
11307
+ isExtreme = true;
11308
+ }
11412
11309
 
11413
- if (dist > maxDist) {
11310
+ // 1.3 is local or absolute extreme
11311
+ let bb = getPolyBBox([p0, p2]); // local bb
11312
+ let { left, right, top, bottom } = bb;
11313
+
11314
+ let extremeLocal = (p1.x < left || p1.x > right || p1.y < top || p1.y > bottom);
11315
+ if (!isExtreme && extremeLocal) {
11316
+ isExtreme = true;
11414
11317
 
11415
- maxDist = dist;
11416
- splitPoint = i;
11417
11318
  }
11418
- }
11419
11319
 
11420
- return [maxDist, splitPoint];
11421
- }
11320
+ /**
11321
+ * 2. sign changes
11322
+ */
11323
+ let signChange = (p0.area < 0 && p1.area > 0) || (p0.area > 0 && p1.area < 0);
11324
+ let isDirChange = signChange && !flat && !p0.isDirChange && isLong;
11422
11325
 
11423
- function mapTtoRelativeDistances(bez, B_parts) {
11424
- let B_t_curr;
11425
- let B_t_dist = [0];
11426
- let B_t_prev = bez[0];
11427
- let sumLen = 0;
11326
+ /**
11327
+ * 3. corners
11328
+ */
11428
11329
 
11429
- for (let i = 1; i <= B_parts; i++) {
11430
- B_t_curr = pointAtT(bez, i / B_parts);
11431
- sumLen += getDistance(B_t_curr, B_t_prev);
11432
- B_t_dist.push(sumLen);
11433
- B_t_prev = B_t_curr;
11434
- }
11330
+ if (isExtreme) {
11435
11331
 
11436
- B_t_dist = B_t_dist.map((x) => {
11437
- return x / sumLen;
11438
- });
11439
- return B_t_dist;
11440
- }
11441
- function find_t(param, t_distMap, B_parts) {
11332
+ let delta = getDeltaAngle(p1, p2, p0);
11333
+ let { deltaAngleDeg } = delta;
11334
+ deltaAngleDeg = Math.abs(deltaAngleDeg);
11442
11335
 
11443
- if (param < 0) {
11444
- return 0;
11445
- }
11446
- if (param > 1) {
11447
- return 1;
11448
- }
11336
+ let isCornerDelta = deltaAngleDeg > 10 && deltaAngleDeg < 160;
11337
+ if (isCornerDelta) {
11449
11338
 
11450
- let lenMax, lenMin, tMax, tMin, t;
11339
+ isCorner = true;
11451
11340
 
11452
- for (let i = 1; i <= B_parts; i++) {
11341
+ }
11453
11342
 
11454
- if (param <= t_distMap[i]) {
11455
- tMin = (i - 1) / B_parts;
11456
- tMax = i / B_parts;
11457
- lenMin = t_distMap[i - 1];
11458
- lenMax = t_distMap[i];
11459
- t = (param - lenMin) / (lenMax - lenMin) * (tMax - tMin) + tMin;
11460
- break;
11461
11343
  }
11462
- }
11463
- return t;
11464
- }
11465
11344
 
11466
- function areaDeviationTooLarge(pts, bezCurve) {
11345
+ if (isExtreme && !isCorner) {
11467
11346
 
11468
- let split = 4;
11469
- let step = 1 / split;
11470
- let l = pts.length;
11347
+ if ((Math.abs(p1.dy2) < thresh2) && Math.abs(p1.dx2) > thresh) {
11348
+ isHorizontal = true;
11349
+ }
11350
+ else if (Math.abs(p1.dx2) < thresh2 && Math.abs(p1.dy2) > thresh) {
11351
+ isVertical = true;
11352
+ }
11353
+ }
11471
11354
 
11472
- // create polygon from curve candidate
11473
- let poly = [bezCurve[0]];
11474
- let pt;
11355
+ /**
11356
+ * semi extremes
11357
+ * ~ 45deg tangent
11358
+ */
11359
+ let diffX = Math.abs(p1.dx2);
11360
+ let diffY = Math.abs(p1.dy2);
11475
11361
 
11476
- for (let i = 1; i < split; i++) {
11477
- let t = step * i;
11478
- pt = pointAtT(bezCurve, t);
11479
- poly.push(pt);
11480
- }
11362
+ let ratDelta = (diffX / diffY);
11363
+
11364
+ if (ratDelta > 0.8 && ratDelta <= 1.2) {
11365
+ isSemiExtreme = true;
11366
+ }
11481
11367
 
11482
- poly.push(bezCurve[bezCurve.length - 1]);
11368
+ p1.isCorner = isCorner;
11369
+ p1.isExtreme = isExtreme;
11370
+ p1.isSemiExtreme = isSemiExtreme;
11371
+ p1.isLong = isLong;
11372
+ p1.isShort = isShort;
11483
11373
 
11484
- // Original area
11485
- let polyArea = getPolygonArea(pts, false);
11374
+ p1.isHorizontal = isHorizontal;
11375
+ p1.isVertical = isVertical;
11376
+ p1.isDirChange = isDirChange;
11486
11377
 
11487
- // flat line
11488
- if (!polyArea && pts.length === 2) {
11489
- polyArea = getSquareDistance(pts[0], pts[1]) * 0.01;
11490
11378
  }
11491
11379
 
11492
- let curveArea = getPolygonArea(poly, false);
11493
- let rat = curveArea / polyArea;
11380
+ // add tangents
11381
+ getTangents(pts, { x, y, width, height });
11494
11382
 
11495
- let isBulged = rat > 1.1;
11383
+ refineAdjacentPolyExtremes(pts);
11496
11384
 
11497
- // isBulged = rat > 2
11498
- let bezierNew = bezCurve;
11499
- if (isBulged) {
11500
- let ptMid = pts[Math.floor(l * 0.5)];
11501
- let p = pts[l - 1];
11502
- /*
11503
- let cp1 = pointAtT([pts[0], ptMid], 0.666);
11504
- let cp2 = pointAtT([p, ptMid], 0.666);
11385
+ // filter adjacent significant points
11386
+ cleanupPolyKeypoints(pts);
11505
11387
 
11506
- bezierNew = [pts[0], cp1, cp2, p]
11388
+ renderPolyTopology(pts);
11507
11389
 
11508
-
11390
+ return pts
11391
+ }
11509
11392
 
11510
-
11511
- cp1 = pointAtT([bezCurve[0], bezCurve[3]], 0.333);
11512
- cp2 = pointAtT([bezCurve[0], bezCurve[3]], 0.666);
11513
- bezCurve = [bezCurve[0], cp1, cp2, bezCurve[3]]
11514
- */
11393
+ /*
11515
11394
 
11516
- bezCurve = [pts[0], ptMid, ptMid, p];
11517
- bezierNew = bezCurve;
11518
- }
11395
+ */
11519
11396
 
11520
- return { isBulged, bezierNew };
11521
- }
11397
+ // just for visualization
11398
+ function renderPolyTopology(pts, showTangents = true) {
11522
11399
 
11523
- /**
11524
- * Creates a vector of length 1 which shows the direction from B to A
11525
- */
11526
- function createTangent(p1, p2) {
11527
- // Returns unit vector pointing from B to A
11528
- let dx = p1.x - p2.x;
11529
- let dy = p1.y - p2.y;
11530
- let length = Math.sqrt(dx * dx + dy * dy);
11400
+ let l = pts.length;
11531
11401
 
11532
- if (length === 0) return { x: 0, y: 0 };
11533
- return { x: dx / length, y: dy / length };
11534
- }
11402
+ // render
11403
+ for (let i = 0; i < l; i++) {
11404
+ i > 0 ? pts[i - 1] : pts[l - 1];
11405
+ let p1 = pts[i];
11406
+ i < l - 1 ? pts[i + 1] : pts[l - 1];
11535
11407
 
11536
- /**
11537
- * Math helpers
11538
- */
11408
+ if (p1.isDirChange) {
11409
+ renderPoint(markers, p1, 'orange', '1%', '0.75');
11410
+ }
11539
11411
 
11540
- // Basic vector utilities (only what's absolutely necessary)
11541
- function subtract(a, b) {
11542
- return { x: a.x - b.x, y: a.y - b.y };
11543
- }
11412
+ if (p1.isSemiExtreme) {
11413
+ renderPoint(markers, p1, 'red', '1%', '0.5');
11414
+ }
11544
11415
 
11545
- function addArrays(a, b) {
11546
- return { x: a.x + b.x, y: a.y + b.y };
11547
- }
11416
+ /*
11417
+ if (p1.isLong && (p1.isDirChange || p1.isExtreme || p1.isCorner || p1.isSemiExtreme)) {
11418
+ renderPoint(markers, p1, 'green', '1.5%', '0.5')
11419
+ }
11420
+ */
11548
11421
 
11549
- function mulItems(v, s) {
11550
- return { x: v.x * s, y: v.y * s };
11551
- }
11422
+ if (p1.isDirChange) {
11423
+ renderPoint(markers, p1, 'green', '1.5%', '0.5');
11424
+ }
11552
11425
 
11553
- function normalize(v) {
11554
- let len = Math.sqrt(v.x * v.x + v.y * v.y);
11555
- return len === 0 ? { x: 0, y: 0 } : { x: v.x / len, y: v.y / len };
11556
- }
11426
+ if (p1.isExtreme) {
11427
+ renderPoint(markers, p1, 'cyan', '1%', '0.5');
11428
+ }
11557
11429
 
11558
- function dot(a, b) {
11559
- return a.x * b.x + a.y * b.y;
11560
- }
11430
+ if (p1.isHorizontal) {
11431
+ renderPoint(markers, p1, 'blue', '1.5%', '0.25');
11432
+ }
11433
+
11434
+ if (p1.isVertical) {
11435
+ renderPoint(markers, p1, 'purple', '1.5%', '0.25');
11436
+ }
11437
+
11438
+ if (p1.isCorner) {
11439
+ renderPoint(markers, p1, 'magenta', '1%', '1');
11440
+ }
11441
+
11442
+ if (showTangents && (p1.isCorner || p1.isSemiExtreme || p1.isDirChange || p1.isExtreme)) {
11443
+ renderPoint(markers, p1.tangentL, 'darkred', '0.5%');
11444
+ renderPoint(markers, p1.tangentR, 'darkblue', '0.5%');
11445
+
11446
+ /*
11447
+ if (p1.isDirChange) {
11448
+ renderPoint(markers, p1.tangentL, 'darkred', '1.5%')
11449
+ renderPoint(markers, p1.tangentR, 'darkblue', '1.5%')
11450
+ }
11451
+ */
11452
+
11453
+ }
11561
11454
 
11562
- function zeros_Xx2x2(x) {
11563
- let zs = [];
11564
- while (x--) {
11565
- zs.push([0, 0]);
11566
11455
  }
11567
- return zs;
11456
+
11568
11457
  }
11569
11458
 
11570
- // First derivative (tangent vector)
11571
- function bezierQprime(bez, u, second = false) {
11572
- let p0 = bez[0], cp1 = bez[1], cp2 = bez[2], p1 = bez[3];
11573
- let t = u;
11574
- let mt = 1 - t;
11575
- let mt2 = mt * mt;
11576
- let t2 = t * t;
11577
- let dx, dy;
11459
+ function getPolyChunks(pts,
11460
+ { closed = true,
11461
+ keepCorners = true,
11462
+ keepExtremes = true,
11463
+ keepInflections = false
11464
+ } = {}
11465
+ ) {
11466
+ let chunks = [[pts[0]]];
11467
+
11468
+ let idx = 0;
11469
+ let lastChunk = chunks[idx];
11578
11470
 
11579
- if (second) {
11580
- dx = 6 * mt * (cp2.x - 2 * cp1.x + p0.x) +
11581
- 6 * t * (p1.x - 2 * cp2.x + cp1.x);
11471
+ let l = pts.length;
11582
11472
 
11583
- dy = 6 * mt * (cp2.y - 2 * cp1.y + p0.y) +
11584
- 6 * t * (p1.y - 2 * cp2.y + cp1.y);
11473
+ // render
11474
+ for (let i = 1; i < l; i++) {
11475
+ i > 0 ? pts[i] : pts[l - 1];
11476
+ let p1 = pts[i];
11477
+ i < l - 1 ? pts[i + 1] : pts[l - 1];
11585
11478
 
11586
- } else {
11587
- dx = 3 * mt2 * (cp1.x - p0.x) +
11588
- 6 * mt * t * (cp2.x - cp1.x) +
11589
- 3 * t2 * (p1.x - cp2.x);
11479
+ // start new chunk
11480
+ // keepInflections && p1.isDirChange
11481
+ if ((keepExtremes && p1.isExtreme || keepCorners && p1.isCorner ||
11482
+ (keepInflections && p1.isDirChange && !p1.isExtreme && !p1.isCorner )
11483
+ )) {
11484
+ idx++;
11485
+ chunks.push([]);
11486
+ }
11590
11487
 
11591
- dy = 3 * mt2 * (cp1.y - p0.y) +
11592
- 6 * mt * t * (cp2.y - cp1.y) +
11593
- 3 * t2 * (p1.y - cp2.y);
11488
+ lastChunk = chunks[idx];
11489
+ lastChunk.push(p1);
11594
11490
  }
11595
11491
 
11596
- return [dx, dy];
11597
- }
11492
+ // test render
11598
11493
 
11599
- function adjustTangentAngle(cp, p0, p1, p2) {
11600
- let ang1 = getAngle(p0, p1);
11601
- let ang2 = getAngle(p0, p2);
11602
- let angDiff = (ang2 - ang1);
11603
- cp = rotatePoint(cp, p0.x, p0.y, -angDiff);
11604
- return cp
11494
+ return chunks;
11605
11495
  }
11606
11496
 
11607
- // convert to pathdata
11608
- function bezierPtsToPathData(beziers = []) {
11609
- let pathData = [];
11610
- beziers.forEach(bez => {
11611
-
11612
- let type = bez.length === 4 ? 'C' : (bez.length === 3 ? 'Q' : 'L');
11497
+ function removeCoincidingVertices(pts = []) {
11498
+ let l = pts.length;
11499
+ if (!l) return pts;
11613
11500
 
11614
- let cp1 = type === 'C' || type === 'Q' ? bez[1] : null;
11615
- let cp2 = type === 'C' ? bez[2] : null;
11616
- let p = bez[bez.length - 1];
11501
+ let ptsN = [pts[0]];
11502
+ let pt1, pt2;
11617
11503
 
11618
- let values = type === 'C' ?
11619
- [cp1.x, cp1.y, cp2.x, cp2.y, p.x, p.y] :
11620
- (type === 'Q' ?
11621
- [cp1.x, cp1.y, p.x, p.y] :
11622
- [p.x, p.y]
11623
- );
11504
+ for (let i = 1; i < l; i++) {
11505
+ pt1 = pts[i - 1];
11506
+ pt2 = pts[i];
11624
11507
 
11625
- let com = { type, values };
11626
- pathData.push(com);
11627
- });
11508
+ /**
11509
+ * 1. Skip zero-length segments
11510
+ */
11511
+ if (pt1.x === pt2.x && pt1.y === pt2.y) {
11512
+ continue;
11513
+ }
11514
+ ptsN.push(pt2);
11515
+ }
11516
+ return ptsN
11628
11517
 
11629
- return pathData
11630
11518
  }
11631
11519
 
11632
- function simplifyRC(pts, quality = 1, shiftStart = true) {
11633
-
11634
- if (pts.length < 4) return pts;
11520
+ function simplifyRC(pts = [], quality = 1, shiftStart = true) {
11635
11521
 
11636
11522
  let l = pts.length;
11523
+ if (l < 4) return pts;
11637
11524
 
11638
11525
  // starting point
11639
11526
  let M = pts[0];
@@ -11698,7 +11585,7 @@ function simplifyRC(pts, quality = 1, shiftStart = true) {
11698
11585
  let thresh = getSquareDistance(pt0, pt2) * 0.005;
11699
11586
 
11700
11587
  // flat
11701
- if ( area <= thresh && i<l-1) {
11588
+ if (area <= thresh && i < l - 1) {
11702
11589
 
11703
11590
  pt0 = pt1;
11704
11591
  continue
@@ -11719,10 +11606,10 @@ function simplifyRC(pts, quality = 1, shiftStart = true) {
11719
11606
  }
11720
11607
 
11721
11608
  // 1st and last are colinear
11722
- let area0 = getPolygonArea([ptsSmp[1], M, ptsSmp[ptsSmp.length-1]], true);
11723
- let thresh0 = getSquareDistance (ptsSmp[1], ptsSmp[ptsSmp.length-1]) * 0.005;
11609
+ let area0 = getPolygonArea([ptsSmp[1], M, ptsSmp[ptsSmp.length - 1]], true);
11610
+ let thresh0 = getSquareDistance(ptsSmp[1], ptsSmp[ptsSmp.length - 1]) * 0.005;
11724
11611
  // remove first point
11725
- if(area0 < thresh0) ptsSmp.shift();
11612
+ if (area0 < thresh0) ptsSmp.shift();
11726
11613
 
11727
11614
  return ptsSmp;
11728
11615
  }
@@ -11741,6 +11628,7 @@ function simplifyPolygonToPathData(pts, {
11741
11628
  tolerance = 1,
11742
11629
  simplifyRD = 1,
11743
11630
  simplifyRDP = 1,
11631
+ isClosed = true,
11744
11632
  } = {}) {
11745
11633
 
11746
11634
  let polyPath = [];
@@ -11818,6 +11706,25 @@ function simplifyPolygonToPathData(pts, {
11818
11706
 
11819
11707
  // remove colinear
11820
11708
 
11709
+ keepExtremes = false;
11710
+ keepCorners = false;
11711
+
11712
+ keepExtremes = true;
11713
+ keepCorners = true;
11714
+
11715
+ // check if closed
11716
+ /*
11717
+ let bb = getPolyBBox(pts)
11718
+ let thresh = (bb.width+bb.height)*0.25
11719
+ let dist0 = getDistManhattan(pts[0], pts[pts.length-1])
11720
+
11721
+ */
11722
+
11723
+ // copy 1st first to end
11724
+ if (isClosed) {
11725
+ pts.push(pts[0]);
11726
+ }
11727
+
11821
11728
  // get topology of poly
11822
11729
  let polyAnalyzed = !keepExtremes && !keepCorners ? pts : analyzePoly(pts, {
11823
11730
  debug: false
@@ -11825,69 +11732,369 @@ function simplifyPolygonToPathData(pts, {
11825
11732
  });
11826
11733
 
11827
11734
  // split into segment chunks
11828
- let chunks = !keepExtremes && !keepCorners ? [polyAnalyzed] : getPolyChunks(polyAnalyzed, { keepCorners, keepExtremes, keepInflections });
11735
+
11736
+ let chunks = getPolyChunks(polyAnalyzed, { keepCorners, keepExtremes, keepInflections: true });
11829
11737
 
11830
11738
  // Schneider curve fit
11831
11739
  let threshold = width && height ? (width + height) / 2 * 0.004 * tolerance : 2.5;
11740
+ threshold = width && height ? (width + height) / 2 * 0.004 * tolerance : 2.5;
11832
11741
 
11833
- polyPath = simplifyPolyChunks(chunks, {
11834
- closed,
11835
- tolerance: threshold,
11836
- keepCorners,
11837
- keepExtremes: true,
11838
- });
11742
+ {
11839
11743
 
11840
- polyPath = fixIntersectingCpts(polyPath);
11744
+ polyPath = simplifyPolyChunksTopology(chunks, {
11745
+ closed,
11746
+ tolerance: threshold,
11747
+ keepCorners,
11748
+ keepExtremes: true,
11749
+ });
11750
+ }
11841
11751
 
11842
11752
  return polyPath;
11843
11753
  }
11844
11754
 
11845
11755
  /**
11846
- * convert polygon chunks
11847
- * to cubic beziers
11756
+ * topology based curve fit
11848
11757
  */
11849
-
11850
- function simplifyPolyChunks(chunks = [], {
11758
+ function simplifyPolyChunksTopology(chunks = [], {
11851
11759
  closed = true,
11852
11760
  keepCorners = true,
11853
11761
  tolerance = 1,
11854
11762
  } = {}) {
11855
11763
 
11764
+ console.log(chunks);
11765
+
11856
11766
  let l = chunks.length;
11857
11767
 
11858
11768
  // new pathData
11769
+
11859
11770
  let pathData = [{ type: 'M', values: [chunks[0][0].x, chunks[0][0].y] }];
11860
11771
 
11772
+ // loop chunks
11861
11773
  for (let i = 0; i < l; i++) {
11862
11774
 
11775
+ let chunkPrev = i > 0 ? chunks[i - 1] : (closed ? chunks[l - 1] : null);
11863
11776
  let chunk = chunks[i];
11864
- let chunkN = chunks[i + 1] ? chunks[i + 1] : null;
11777
+ let chunkN = chunks[i + 1] ? chunks[i + 1] : (closed ? chunks[0] : null);
11865
11778
  let segments = [];
11866
- let chunklen = chunk.length;
11867
- chunk[chunk.length - 1];
11868
11779
 
11869
11780
  // add from next command
11870
11781
  if (chunkN) {
11871
11782
  chunk.push(chunkN[0]);
11872
11783
  }
11873
11784
 
11874
- // nothing to simplify
11875
- if (chunklen < 2 || (chunklen === 2 && chunk[1].isExtreme)) {
11876
- chunk[chunk.length - 1];
11877
- segments = chunk.map(com => { return { type: 'L', values: [com.x, com.y] } });
11785
+ let chunklen = chunk.length;
11786
+ let hasInflection = false;
11787
+ let segments_1 = [], segments_2 = [], segments_3 = [];
11788
+ let segsRequired = 3;
11789
+
11790
+ // 1st point
11791
+ let p1 = chunk[0];
11792
+ // last point in chunk
11793
+ let p2 = chunk[chunklen - 1];
11794
+
11795
+ // nothing to simplify - lineto
11796
+
11797
+ // if (chunklen < 2 || (chunklen === 2 && (chunk[1].isExtreme || i===l-1 && !closed) )) {
11798
+
11799
+ if (chunklen < 2 || (chunklen === 2 && (chunk[1].isExtreme))) {
11800
+
11801
+ if (chunklen === 2) {
11802
+ segsRequired = 2;
11803
+ segments_2 = [
11804
+ {
11805
+ type: 'L',
11806
+ values: [p1.x, p1.y],
11807
+ p0: chunkPrev,
11808
+ p: p1,
11809
+ },
11810
+ {
11811
+ type: 'L',
11812
+ values: [chunk[1].x, chunk[1].y],
11813
+ p0: p1,
11814
+ p: p2,
11815
+ }
11816
+ ];
11817
+ } else {
11818
+ segsRequired = 1;
11819
+ segments_1 = [
11820
+ {
11821
+ type: 'L',
11822
+ values: [p1.x, p1.y],
11823
+ p0: chunkPrev,
11824
+ p: p2,
11825
+ },
11826
+ ];
11827
+ }
11878
11828
 
11879
11829
  } else {
11880
- segments = fitCurveSchneider(chunk, {
11881
- maxError: tolerance, keepCorners
11882
- });
11830
+
11831
+ // point before inflection
11832
+ let p3 = chunk[chunklen - 2];
11833
+
11834
+ chunk.filter(pt => pt.isExtreme);
11835
+ let semiExtremes = chunk.filter(pt => pt.isSemiExtreme);
11836
+ chunk.filter(pt => pt.isCorner);
11837
+ let inflections = chunk.filter(pt => pt.isDirChange && !pt.isCorner && !pt.isExtreme);
11838
+ hasInflection = inflections.length && inflections[0] !== p1;
11839
+
11840
+ let idxMid = Math.floor(chunklen * 0.5);
11841
+ let pMid = semiExtremes.length ? semiExtremes[Math.floor(semiExtremes.length * 0.5)] : chunk[idxMid];
11842
+
11843
+ let dist0 = getDistManhattan(p1, p3);
11844
+ let dist1 = getDistManhattan(pMid, p3);
11845
+ let dist2 = getDistManhattan(pMid, p1);
11846
+ let thresh = dist0 * 0.25;
11847
+ let shortMidSegment = dist1 < thresh || dist2 < thresh;
11848
+
11849
+ /**
11850
+ * we have 3 modes
11851
+ * 1 segment: only 1 segment between extremes/corners
11852
+ * 2 segments: semiextreme/mid in between
11853
+ * 3 segments: inflection
11854
+ */
11855
+
11856
+ segsRequired = shortMidSegment ? (!hasInflection ? 1 : 2) : (hasInflection ? 3 : 2);
11857
+
11858
+ let cp1_1 = p1.tangentR;
11859
+ let cp2_1 = pMid.tangentL;
11860
+ let p_1 = pMid;
11861
+
11862
+ // renderPoint(markers, pMid, 'orange', '2%')
11863
+
11864
+ let cp2_2 = p2.tangentL;
11865
+ let cp1_2 = pMid.tangentR;
11866
+ let p_2 = p2;
11867
+
11868
+ let cp1_3 = null;
11869
+ let cp2_3 = null;
11870
+ let p_3 = null;
11871
+
11872
+ // general extrapolation
11873
+ let t = 0.666;
11874
+ let ptI_1 = null, ptI_2 = null, ptI_3 = null;
11875
+
11876
+ // 1 segment
11877
+ ptI_1 = checkLineIntersection(p1, p1.tangentR, p2, p2.tangentL, false, true);
11878
+ if (ptI_1) {
11879
+ cp1_1 = interpolate(p1, ptI_1, t);
11880
+ cp2_1 = interpolate(p2, ptI_1, t);
11881
+ p_1 = p2;
11882
+
11883
+ segments_1 = [
11884
+
11885
+ {
11886
+ type: 'C',
11887
+ values: [cp1_1.x, cp1_1.y, cp2_1.x, cp2_1.y, p_1.x, p_1.y],
11888
+ p0: p1,
11889
+ cp1: cp1_1,
11890
+ cp2: cp2_1,
11891
+ p: p_1,
11892
+ }
11893
+ ];
11894
+
11895
+ }
11896
+
11897
+ // 2 segments
11898
+ ptI_1 = checkLineIntersection(p1, p1.tangentR, pMid, pMid.tangentL, false, true);
11899
+ ptI_2 = checkLineIntersection(p2, p2.tangentL, pMid, pMid.tangentR, false, true);
11900
+
11901
+ if (ptI_1 && ptI_2) {
11902
+ cp1_1 = interpolate(p1, ptI_1, t);
11903
+ cp2_1 = interpolate(pMid, ptI_1, t);
11904
+ p_1 = pMid;
11905
+
11906
+ cp1_2 = interpolate(pMid, ptI_2, t);
11907
+ cp2_2 = interpolate(p2, ptI_2, t);
11908
+ p_2 = p2;
11909
+
11910
+ segments_2 = [
11911
+
11912
+ {
11913
+ type: 'C',
11914
+ values: [cp1_1.x, cp1_1.y, cp2_1.x, cp2_1.y, p_1.x, p_1.y],
11915
+ p0: p1,
11916
+ cp1: cp1_1,
11917
+ cp2: cp2_1,
11918
+ p: p_1,
11919
+ isExtreme: p_1.isExtreme
11920
+ },
11921
+ {
11922
+ type: 'C',
11923
+ values: [cp1_2.x, cp1_2.y, cp2_2.x, cp2_2.y, p_2.x, p_2.y],
11924
+ p0: p_1,
11925
+ cp1: cp1_2,
11926
+ cp2: cp2_2,
11927
+ p: p_2,
11928
+ isExtreme: p_2.isExtreme
11929
+
11930
+ },
11931
+ ];
11932
+
11933
+ }
11934
+
11935
+ // 3 segments
11936
+
11937
+ if (hasInflection) {
11938
+
11939
+ // get pt between dir change and mid
11940
+ let idx_3_4 = Math.floor(chunklen * 0.75);
11941
+ p3 = chunk[idx_3_4];
11942
+ ptI_3 = checkLineIntersection(p3, p3.tangentR, p2, p2.tangentL, false, false);
11943
+
11944
+ if (ptI_3) {
11945
+ let tangentR_beforeDirChange = interpolate(p3, ptI_3, t);
11946
+
11947
+ // extend right tangent
11948
+ p3.tangentR.x = tangentR_beforeDirChange.x;
11949
+ p3.tangentR.y = tangentR_beforeDirChange.y;
11950
+
11951
+ // extend dir change tangent
11952
+ let tangentL_dirChange = interpolate(p2, ptI_3, t);
11953
+ p2.tangentL.x = tangentL_dirChange.x;
11954
+ p2.tangentL.y = tangentL_dirChange.y;
11955
+ } else {
11956
+
11957
+ if (p3 === p2) {
11958
+
11959
+ idx_3_4 = Math.floor(chunklen * 0.3);
11960
+ p3 = chunk[idx_3_4];
11961
+
11962
+ }
11963
+ checkLineIntersection(p3, p3.tangentR, p2, p2.tangentL, false, false);
11964
+
11965
+ cp1_1 = interpolate(p1, p1.tangentR, 1.333);
11966
+ cp2_1 = interpolate(p2, p2.tangentL, 1.333);
11967
+
11968
+ segments_3 = [
11969
+ {
11970
+ type: 'C',
11971
+ values: [cp1_1.x, cp1_1.y, cp2_1.x, cp2_1.y, p2.x, p2.y],
11972
+ p0: p1,
11973
+ cp1: cp1_1,
11974
+ cp2: cp2_1,
11975
+ p: p2,
11976
+ isExtreme: p2.isExtreme
11977
+
11978
+ },
11979
+ ];
11980
+
11981
+ /*
11982
+ let tangentR_beforeDirChange = interpolate(p3, ptI_3, t)
11983
+
11984
+ // extend right tangent
11985
+ p3.tangentR.x = tangentR_beforeDirChange.x
11986
+ p3.tangentR.y = tangentR_beforeDirChange.y
11987
+
11988
+ let tangentL_dirChange = interpolate(p2, ptI_3, t)
11989
+ p2.tangentL.x = tangentL_dirChange.x
11990
+ p2.tangentL.y = tangentL_dirChange.y
11991
+ */
11992
+
11993
+ pathDataToD([{ type: 'M', values: [p1.x, p1.y] }, ...segments_3]);
11994
+
11995
+ }
11996
+
11997
+ cp1_3 = p3.tangentR;
11998
+ cp2_3 = p2.tangentL;
11999
+ p_3 = p2;
12000
+
12001
+ ptI_1 = checkLineIntersection(p1, p1.tangentR, pMid, pMid.tangentL, false, true);
12002
+ ptI_2 = checkLineIntersection(pMid, pMid.tangentR, p3, p3.tangentL, false, true);
12003
+
12004
+ if (ptI_1 && ptI_2 && ptI_3) {
12005
+
12006
+ cp1_1 = interpolate(p1, ptI_1, t);
12007
+ cp2_1 = interpolate(pMid, ptI_1, t);
12008
+ p_1 = pMid;
12009
+
12010
+ cp1_2 = interpolate(pMid, ptI_2, t);
12011
+ cp2_2 = interpolate(p3, ptI_2, t);
12012
+ p_2 = p3;
12013
+
12014
+ segments_3 = [
12015
+ {
12016
+ type: 'C',
12017
+ values: [cp1_1.x, cp1_1.y, cp2_1.x, cp2_1.y, p_1.x, p_1.y],
12018
+ p0: p1,
12019
+ cp1: cp1_1,
12020
+ cp2: cp2_1,
12021
+ p: p_1,
12022
+ isExtreme: p_1.isExtreme
12023
+
12024
+ },
12025
+ {
12026
+ type: 'C',
12027
+ values: [cp1_2.x, cp1_2.y, cp2_2.x, cp2_2.y, p_2.x, p_2.y],
12028
+ p0: p_1,
12029
+ cp1: cp1_2,
12030
+ cp2: cp2_2,
12031
+ p: p_2,
12032
+ },
12033
+ {
12034
+ type: 'C',
12035
+ values: [cp1_3.x, cp1_3.y, cp2_3.x, cp2_3.y, p_3.x, p_3.y],
12036
+ p0: p_2,
12037
+ cp1: cp1_3,
12038
+ cp2: cp2_3,
12039
+ p: p_3,
12040
+ isExtreme: p_3.isExtreme
12041
+
12042
+ }
12043
+ ];
12044
+
12045
+ pathDataToD([{ type: 'M', values: [p1.x, p1.y] }, ...segments_3]);
12046
+
12047
+ }
12048
+
12049
+ }
12050
+
12051
+ }
12052
+
12053
+ if (segsRequired === 1) {
12054
+ segments = segments_1;
12055
+ } else if (segsRequired === 2 && segments_2.length) {
12056
+ segments = segments_2;
12057
+ }
12058
+ else ;
12059
+
12060
+ segments = segments_3.length ? segments_3 : segments_2;
12061
+ /*
12062
+ if (simplify && !isLinetoSeg && segments.length > 1) {
12063
+
12064
+ let com1 = segments[0]
12065
+ let com2 = segments[1]
12066
+
12067
+ tolerance = 1.1
12068
+ let combined = combineCubicPairs(com1, com2, { tolerance })
12069
+
12070
+ let error = 0;
12071
+ let comsSimp =[]
12072
+
12073
+ console.log('!!!combined', segments.length, combined);
12074
+
12075
+ // success
12076
+ if (combined.length === 1) {
12077
+
12078
+ if(segments.length === 2){
12079
+ segments = combined
12080
+ }
12081
+
12082
+ let com = combined[0]
12083
+ }
12084
+
11883
12085
  }
12086
+ */
11884
12087
 
11885
12088
  // remove first segment to connect to last segment
11886
12089
  pathData.push(...segments);
11887
12090
 
11888
12091
  }
11889
12092
 
11890
- if (closed) pathData.push({ type: 'Z', values: [] });
12093
+ if (closed) {
12094
+ pathData.push({ type: 'Z', values: [] });
12095
+ }
12096
+
12097
+ // refine extremes
11891
12098
  return pathData
11892
12099
 
11893
12100
  }
@@ -11898,13 +12105,18 @@ function simplifyPolyChunks(chunks = [], {
11898
12105
  */
11899
12106
  function pathDataToPolygonOpt(pathData, {
11900
12107
  precisionPoly = 1,
11901
- autoAccuracy=false,
11902
- polyFormat='points',
11903
- decimals=-1,
11904
- simplifyRD=1,
11905
- simplifyRDP=1,
12108
+ autoAccuracy = false,
12109
+ polyFormat = 'object',
12110
+ decimals = -1,
12111
+ simplifyRD = 1,
12112
+ simplifyRDP = 1,
11906
12113
  } = {}) {
11907
12114
 
12115
+ pathData = convertPathData(pathData, {toAbsolute:true, toLonghands:true, arcToCubic:true});
12116
+ pathData = addExtremePoints(pathData);
12117
+
12118
+ pathData = getPathDataVerbose(pathData);
12119
+
11908
12120
  let l = pathData.length;
11909
12121
  let M = { x: pathData[0].values[0], y: pathData[0].values[1] };
11910
12122
  let p0 = M;
@@ -11931,7 +12143,7 @@ simplifyRDP=1,
11931
12143
  let pts2 = [pts[0]];
11932
12144
 
11933
12145
  // adjustments for very small or large paths
11934
- dims = dims.filter(Boolean).sort();
12146
+ dims = dims.filter(Boolean).sort((a,b)=>a-b);
11935
12147
  let dimMax = dims[dims.length - 1];
11936
12148
 
11937
12149
  let scale = dimMax > 2 && dimMax < 25 ? 1 : (20 / dimMax);
@@ -11970,31 +12182,33 @@ simplifyRDP=1,
11970
12182
  }
11971
12183
 
11972
12184
  // simplify polygon
11973
- if(simplifyRD>0){
11974
- pts2 = simplifyPolyRD(pts2, {quality:simplifyRD});
12185
+ if (simplifyRD > 0) {
12186
+ pts2 = simplifyPolyRD(pts2, { quality: simplifyRD });
11975
12187
  }
11976
12188
 
11977
- if(simplifyRDP>0){
11978
- pts2 = simplifyPolyRDP(pts2, {quality:simplifyRDP});
12189
+ if (simplifyRDP > 0) {
12190
+ pts2 = simplifyPolyRDP(pts2, { quality: simplifyRDP });
11979
12191
  }
11980
12192
 
11981
- pathDataPoly = pathDataFromPoly(pts2);
11982
- pathData = pathDataPoly;
11983
-
11984
- if(autoAccuracy){
12193
+ if (autoAccuracy) {
11985
12194
  decimals = detectAccuracyPoly(pts);
11986
12195
  }
11987
12196
 
11988
- let poly = decimals>-1 ? pts2.map(pt => { return { x: roundTo(pt.x, decimals), y: roundTo(pt.y, decimals) } }) : pts2.map(pt => { return { x: pt.x, y: pt.y } });
12197
+ let poly = decimals > -1 ? pts2.map(pt => { return { x: roundTo(pt.x, decimals), y: roundTo(pt.y, decimals) } }) : pts2.map(pt => { return { x: pt.x, y: pt.y } });
11989
12198
 
11990
- if(polyFormat==='array'){
12199
+ pathDataPoly = pathDataFromPoly(poly);
12200
+ pathData = pathDataPoly;
12201
+
12202
+ if (polyFormat === 'array') {
11991
12203
  poly = poly.map(pt => { return [pt.x, pt.y] });
11992
12204
  }
11993
- else if(polyFormat==='string'){
12205
+ else if (polyFormat === 'string') {
11994
12206
  poly = poly.map(pt => { return [pt.x, pt.y].join(',') }).flat().join(' ');
11995
12207
  }
11996
12208
 
11997
- return { pathData, poly }
12209
+ let d= pathDataToD(pathData);
12210
+
12211
+ return { pathData, poly, d }
11998
12212
 
11999
12213
  }
12000
12214
 
@@ -12235,6 +12449,7 @@ let settingsDefaults = {
12235
12449
  // polygon
12236
12450
  toPolygon: false,
12237
12451
  smoothPoly: false,
12452
+ isClosed:true,
12238
12453
  polyFormat: 'object',
12239
12454
  precisionPoly: 1,
12240
12455
  simplifyRD: 0,
@@ -12531,6 +12746,122 @@ function checkBBoxIntersections2(bb, bb1) {
12531
12746
  }
12532
12747
  */
12533
12748
 
12749
+ function SlickVGObj(props = {}) {
12750
+
12751
+ Object.assign(this, props);
12752
+ }
12753
+
12754
+ SlickVGObj.prototype.getD = function () {
12755
+ let d = this.d;
12756
+ return d;
12757
+ };
12758
+
12759
+ SlickVGObj.prototype.getSvg = function () {
12760
+ let svg = this.svg;
12761
+
12762
+ if (!svg) {
12763
+ let xArr = [];
12764
+ let yArr = [];
12765
+ let d = this.d;
12766
+ let pathDataPlusArr = this.pathDataPlusArr || [];
12767
+ pathDataPlusArr.forEach(path => {
12768
+ path.forEach(sub => {
12769
+ let { pathData } = sub;
12770
+ let bb = getPathDataBBox(pathData);
12771
+ let { x, y, right, bottom } = bb;
12772
+ xArr.push(x, right);
12773
+ yArr.push(y, bottom);
12774
+ });
12775
+ });
12776
+
12777
+ let x = Math.min(...xArr);
12778
+ let right = Math.max(...xArr);
12779
+ let y = Math.min(...yArr);
12780
+ let bottom = Math.max(...yArr);
12781
+ let width = right - x;
12782
+ let height = bottom - y;
12783
+
12784
+ svg = `<svg xmlns="${svgNs}" viewBox="${[x, y, width, height].join(' ')}"><path d="${d}"/></svg>`;
12785
+
12786
+ }
12787
+ return svg;
12788
+ };
12789
+
12790
+ /**
12791
+ * retrieve poly
12792
+ * formats: points, array, string, pathData, d,
12793
+ */
12794
+ SlickVGObj.prototype.getPoly = function (options = {}
12795
+ ) {
12796
+
12797
+ options = {
12798
+ ...{
12799
+ precisionPoly: 1,
12800
+ simplifyRDP: 0,
12801
+ simplifyRD: 0,
12802
+ autoAccuracy: true,
12803
+ decimals: 3,
12804
+ format: 'object'
12805
+ },
12806
+ ...options
12807
+ };
12808
+
12809
+ let { precisionPoly, simplifyRDP, simplifyRD, autoAccuracy, decimals, format } = options;
12810
+
12811
+ let polyFormat = format;
12812
+
12813
+ let polys = this.polys;
12814
+ if (!polys.length) {
12815
+ let pathDataPlusArr = this.pathDataPlusArr || [];
12816
+ let poly = [];
12817
+ let polyPaths = [];
12818
+ let dPoly = '';
12819
+ pathDataPlusArr.forEach(path => {
12820
+ path.forEach(sub => {
12821
+ let { pathData } = sub;
12822
+
12823
+ let polyData = pathDataToPolygonOpt(pathData, {
12824
+ precisionPoly,
12825
+ autoAccuracy,
12826
+ decimals,
12827
+ simplifyRD,
12828
+ simplifyRDP,
12829
+ polyFormat
12830
+ });
12831
+
12832
+ dPoly += polyData.d;
12833
+ poly.push(polyData.poly);
12834
+ polyPaths.push(polyData.pathData);
12835
+
12836
+ });
12837
+ });
12838
+
12839
+ if (polyFormat === 'object' || polyFormat === 'array' || polyFormat === 'string') {
12840
+ polys = poly;
12841
+ }
12842
+ else if (polyFormat === 'pathData') {
12843
+ polys = polyPaths.flat();
12844
+ }
12845
+
12846
+ else if (polyFormat === 'd') {
12847
+ polys = dPoly;
12848
+ }
12849
+
12850
+ }
12851
+ return polys;
12852
+ };
12853
+
12854
+ /*
12855
+ export function PathLengthObject(props = {}) {
12856
+ Object.assign(this, props);
12857
+ }
12858
+ */
12859
+
12860
+ function SlickVG(input = '', settings = {}) {
12861
+ settings.getObject = true;
12862
+ return svgPathSimplify(input, settings)
12863
+ }
12864
+
12534
12865
  function svgPathSimplify(input = '', settings = {}) {
12535
12866
 
12536
12867
  let preset = settings['preset'] !== undefined && settings['preset'] ? settings['preset'] : null;
@@ -12542,7 +12873,7 @@ function svgPathSimplify(input = '', settings = {}) {
12542
12873
  ...settings
12543
12874
  };
12544
12875
 
12545
- let { getObject = false, removeComments, removeOffCanvas, unGroup, mergePaths, removeElements, removeDimensions, removeIds, removeClassNames, omitNamespace, cleanUpStrokes, addViewBox, addDimensions, removePrologue, removeHidden, removeUnused, cleanupDefs, cleanupClip, cleanupSVGAtts, removeNameSpaced, removeNameSpacedAtts, attributesToGroup, minifyRgbColors, stylesToAttributes, fixHref, legacyHref, allowMeta, allowDataAtts, allowAriaAtts, removeSVGAttributes, removeElAttributes, shapesToPaths, shapeConvert, convertShapes, simplifyBezier, optimizeOrder, autoClose, removeZeroLength, refineClosing, removeColinear, flatBezierToLinetos, revertToQuadratics, refineExtremes, simplifyCorners, fixDirections, keepExtremes, keepCorners, keepInflections, addExtremes, reversePath, toAbsolute, toRelative, toMixed, toShorthands, toLonghands, quadraticToCubic, arcToCubic, cubicToArc, lineToCubic, decimals, autoAccuracy, minifyD, tolerance, toPolygon, smoothPoly, polyFormat, precisionPoly, simplifyRD, simplifyRDP, harmonizeCpts, removeOrphanSubpaths, simplifyRound, simplifyQuadraticCorners, scale, scaleTo, crop, alignToOrigin, convertTransforms, keepSmaller, splitCompound, convertPathLength, toAbsoluteUnits } = settings;
12876
+ let { getObject = false, removeComments, removeOffCanvas, unGroup, mergePaths, removeElements, removeDimensions, removeIds, removeClassNames, omitNamespace, cleanUpStrokes, addViewBox, addDimensions, removePrologue, removeHidden, removeUnused, cleanupDefs, cleanupClip, cleanupSVGAtts, removeNameSpaced, removeNameSpacedAtts, attributesToGroup, minifyRgbColors, stylesToAttributes, fixHref, legacyHref, allowMeta, allowDataAtts, allowAriaAtts, removeSVGAttributes, removeElAttributes, shapesToPaths, shapeConvert, convertShapes, simplifyBezier, optimizeOrder, autoClose, removeZeroLength, refineClosing, removeColinear, flatBezierToLinetos, revertToQuadratics, refineExtremes, simplifyCorners, fixDirections, keepExtremes, keepCorners, keepInflections, addExtremes, reversePath, toAbsolute, toRelative, toMixed, toShorthands, toLonghands, quadraticToCubic, arcToCubic, cubicToArc, lineToCubic, decimals, autoAccuracy, minifyD, tolerance, toPolygon, smoothPoly, polyFormat, isClosed, precisionPoly, simplifyRD, simplifyRDP, harmonizeCpts, removeOrphanSubpaths, simplifyRound, simplifyQuadraticCorners, scale, scaleTo, crop, alignToOrigin, convertTransforms, keepSmaller, splitCompound, convertPathLength, toAbsoluteUnits } = settings;
12546
12877
 
12547
12878
  // clamp tolerance and scale
12548
12879
  tolerance = Math.max(0.1, tolerance);
@@ -12553,6 +12884,10 @@ function svgPathSimplify(input = '', settings = {}) {
12553
12884
  settings.convertTransforms = true;
12554
12885
  }
12555
12886
 
12887
+ if (shapeConvert === 'toShapes' || shapeConvert === 'shapesToPaths') {
12888
+ keepSmaller = false;
12889
+ }
12890
+
12556
12891
  /**
12557
12892
  * intercept
12558
12893
  * invalid inputs
@@ -12730,6 +13065,8 @@ function svgPathSimplify(input = '', settings = {}) {
12730
13065
  toRelative,
12731
13066
  toMixed,
12732
13067
  toShorthands,
13068
+ // return true arc radii or minified/parametrized
13069
+ optimizeArcs: minifyD < 1,
12733
13070
  decimals,
12734
13071
  };
12735
13072
 
@@ -12741,7 +13078,6 @@ function svgPathSimplify(input = '', settings = {}) {
12741
13078
  let pathDataPlusArr = [];
12742
13079
  let path = paths[i];
12743
13080
  let { d, el } = path;
12744
- let isPoly = false;
12745
13081
 
12746
13082
  // disable reordering for elements with stroke dash-array
12747
13083
  if (el && (el.hasAttribute('stroke-dasharray') || el.hasAttribute('stroke-dashoffset'))) {
@@ -12752,6 +13088,7 @@ function svgPathSimplify(input = '', settings = {}) {
12752
13088
  // if polygon we already heave absolute coordinates
12753
13089
 
12754
13090
  let pathData = parsePathDataNormalized(d, { quadraticToCubic, arcToCubic });
13091
+ console.log('!!!pathData', pathData, arcToCubic);
12755
13092
 
12756
13093
  // get polygon bbox
12757
13094
  let bb_poly = smoothPoly || toPolygon ? getPolyBBox(getPathDataVertices(pathData)) : null;
@@ -12788,7 +13125,7 @@ function svgPathSimplify(input = '', settings = {}) {
12788
13125
  // count commands for evaluation
12789
13126
  comCount += pathData.length;
12790
13127
 
12791
- if (!isPoly && removeOrphanSubpaths) pathData = removeOrphanedM(pathData);
13128
+ if (removeOrphanSubpaths) pathData = removeOrphanedM(pathData);
12792
13129
 
12793
13130
  /**
12794
13131
  * get sub paths
@@ -12802,8 +13139,9 @@ function svgPathSimplify(input = '', settings = {}) {
12802
13139
  let pathDataSub = subPathArr[i];
12803
13140
  let poly = [];
12804
13141
  let coms = Array.from(new Set(pathDataSub.map(com => com.type))).join('');
12805
- isPoly = !(/[acqts]/gi).test(coms);
12806
- let closed = isPoly ? true : false;
13142
+ let isPoly = !(/[acqts]/gi).test(coms);
13143
+
13144
+ let closed = (/z/gi).test(coms);
12807
13145
 
12808
13146
  if (isPoly && !mode) {
12809
13147
 
@@ -12820,7 +13158,6 @@ function svgPathSimplify(input = '', settings = {}) {
12820
13158
 
12821
13159
  }
12822
13160
 
12823
- toPolygon = false;
12824
13161
  pathDataSub = pathDataFromPoly(poly, closed);
12825
13162
 
12826
13163
  }
@@ -12829,26 +13166,56 @@ function svgPathSimplify(input = '', settings = {}) {
12829
13166
  * convert curves to polygon
12830
13167
  * flattening
12831
13168
  */
12832
- else if (toPolygon) {
13169
+
13170
+ if (toPolygon) {
12833
13171
  simplifyBezier = false;
12834
13172
  smoothPoly = false;
12835
13173
  harmonizeCpts = false;
12836
13174
 
12837
- pathDataSub = getPathDataVerbose(pathDataSub);
13175
+ /**
13176
+ * if pathdata is already polygon- pass through
13177
+ * otherwise create precise polygon by curve splitting
13178
+ * */
12838
13179
 
12839
- let polyData = pathDataToPolygonOpt(pathDataSub, {
12840
- precisionPoly,
12841
- autoAccuracy,
13180
+ if (!isPoly) {
13181
+ pathDataSub = getPathDataVerbose(pathDataSub);
13182
+ let polyData = pathDataToPolygonOpt(pathDataSub, {
13183
+ precisionPoly,
13184
+ autoAccuracy,
13185
+ polyFormat,
12842
13186
 
12843
- simplifyRD,
12844
- simplifyRDP
12845
- });
13187
+ simplifyRD,
13188
+ simplifyRDP
13189
+ });
13190
+
13191
+ poly = polyData.poly;
13192
+ pathDataSub = polyData.pathData;
13193
+ isPoly = true;
13194
+
13195
+ }
13196
+
13197
+ polys.push(poly);
13198
+
13199
+ }
13200
+
13201
+ // harmonize cpts
13202
+ // if (harmonizeCpts) pathDataSub = harmonizeCubicCpts(pathDataSub)
12846
13203
 
12847
- pathDataSub = polyData.pathData;
12848
- isPoly = true;
13204
+ if (smoothPoly) {
12849
13205
 
13206
+ removeZeroLength=true;
13207
+ optimizeOrder=true;
12850
13208
  }
12851
13209
 
13210
+ // remove zero length linetos
13211
+ if (removeColinear || removeZeroLength) pathDataSub = removeZeroLengthLinetos(pathDataSub);
13212
+
13213
+ // sort to top left
13214
+ if (optimizeOrder) pathDataSub = pathDataToTopLeft(pathDataSub);
13215
+
13216
+ // Preprocessing: remove colinear - ignore flat beziers (removed later)
13217
+ if (removeColinear) pathDataSub = pathDataRemoveColinear(pathDataSub, { tolerance, flatBezierToLinetos: false });
13218
+
12852
13219
  /**
12853
13220
  * poly to beziers via
12854
13221
  * Philip J. Schneider's
@@ -12857,12 +13224,18 @@ function svgPathSimplify(input = '', settings = {}) {
12857
13224
  if (smoothPoly) {
12858
13225
 
12859
13226
  if (isPoly) {
12860
- pathDataSub = removeZeroLengthLinetos(pathDataSub);
13227
+ /*
13228
+ pathDataSub = pathDataToTopLeft(pathDataSub)
13229
+ pathDataSub = removeZeroLengthLinetos(pathDataSub)
13230
+ pathDataSub = pathDataRemoveColinear(pathDataSub, { tolerance, flatBezierToLinetos: true });
13231
+ */
13232
+
12861
13233
  let poly = getPathDataVertices(pathDataSub);
12862
13234
 
12863
13235
  // options for poly simplification
12864
13236
  let optionsPoly = {
12865
- denoise: 0.8,
13237
+
13238
+ denoise: 0,
12866
13239
  tolerance,
12867
13240
  width: bb_poly.width,
12868
13241
  height: bb_poly.height,
@@ -12874,26 +13247,15 @@ function svgPathSimplify(input = '', settings = {}) {
12874
13247
  closed,
12875
13248
  simplifyRD,
12876
13249
  simplifyRDP,
13250
+ isClosed,
12877
13251
  };
12878
13252
 
12879
13253
  pathDataSub = simplifyPolygonToPathData(poly, optionsPoly);
12880
13254
  // flag as non poly as we're smoothing to curves
12881
-
13255
+ isPoly = false;
12882
13256
  }
12883
13257
  }
12884
13258
 
12885
- // harmonize cpts
12886
- // if (harmonizeCpts) pathDataSub = harmonizeCubicCpts(pathDataSub)
12887
-
12888
- // remove zero length linetos
12889
- if (removeColinear || removeZeroLength) pathDataSub = removeZeroLengthLinetos(pathDataSub);
12890
-
12891
- // sort to top left
12892
- if (optimizeOrder) pathDataSub = pathDataToTopLeft(pathDataSub);
12893
-
12894
- // Preprocessing: remove colinear - ignore flat beziers (removed later)
12895
- if (removeColinear) pathDataSub = pathDataRemoveColinear(pathDataSub, { tolerance, flatBezierToLinetos: false });
12896
-
12897
13259
  let tMin = 0, tMax = 1;
12898
13260
  if (addExtremes) pathDataSub = addExtremePoints(pathDataSub,
12899
13261
  { tMin, tMax, addExtremes, angles: [30] });
@@ -12916,11 +13278,13 @@ function svgPathSimplify(input = '', settings = {}) {
12916
13278
  pathDataPlus.bb = getPolyBBox(getPathDataVertices(pathDataCubic));
12917
13279
  }
12918
13280
  pathDataPlus.dimA = pathDataPlus.bb.width + pathDataPlus.bb.height;
13281
+
12919
13282
  pathDataPlus.pathData = getPathDataVerbose(pathDataSub, {
12920
13283
  addSquareLength: false,
12921
13284
  addArea: false,
12922
13285
  addAverageDim: false
12923
13286
  });
13287
+
12924
13288
  }
12925
13289
 
12926
13290
  // simplify beziers
@@ -12931,6 +13295,12 @@ function svgPathSimplify(input = '', settings = {}) {
12931
13295
 
12932
13296
  if (refineClosing) pathData = refineClosingCommand(pathData, { threshold: dimA * 0.001 });
12933
13297
 
13298
+ // refine round segment sequences
13299
+ if (simplifyRound) {
13300
+ pathData = refineRoundSegments(pathData);
13301
+ pathData = simplifyAdjacentRound(pathData);
13302
+ }
13303
+
12934
13304
  pathData = simplifyBezier ? simplifyPathDataCubic(pathData, { simplifyBezier, keepInflections, keepExtremes, keepCorners, revertToQuadratics, tolerance }) : pathData;
12935
13305
 
12936
13306
  // refine extremes
@@ -12954,12 +13324,6 @@ function svgPathSimplify(input = '', settings = {}) {
12954
13324
  pathData = refineRoundedCorners(pathData, { threshold, tolerance, simplifyQuadraticCorners });
12955
13325
  }
12956
13326
 
12957
- // refine round segment sequences
12958
- if (simplifyRound) {
12959
- pathData = refineRoundSegments(pathData);
12960
- pathData = simplifyAdjacentRound(pathData);
12961
- }
12962
-
12963
13327
  // simplify to quadratics
12964
13328
  if (revertToQuadratics) pathData = pathDataRevertCubicToQuadratic(pathData, tolerance);
12965
13329
 
@@ -12976,6 +13340,8 @@ function svgPathSimplify(input = '', settings = {}) {
12976
13340
 
12977
13341
  }
12978
13342
 
13343
+ // offset path
13344
+
12979
13345
  // update
12980
13346
  pathDataPlusArr.push({ pathData, bb });
12981
13347
 
@@ -13035,17 +13401,18 @@ function svgPathSimplify(input = '', settings = {}) {
13035
13401
  }
13036
13402
 
13037
13403
  // add simplified poly - if not populated by toPoly conversion
13404
+ /*
13038
13405
  if (isPoly) {
13039
13406
 
13040
13407
  pathDataPlusArr.forEach(sub => {
13041
- let poly = getPathDataVertices(sub.pathData, false, decimals);
13408
+ let poly = getPathDataVertices(sub.pathData, false, decimals)
13042
13409
  if (polyFormat === 'array') {
13043
- poly = polyPtsToArray(poly);
13410
+ poly = polyPtsToArray(poly)
13044
13411
  }
13045
- polys.push(poly);
13046
- });
13047
-
13412
+ polys.push(poly)
13413
+ })
13048
13414
  }
13415
+ */
13049
13416
 
13050
13417
  // split into sub paths - returns svg with multiple paths
13051
13418
  if (splitCompound && !mode && pathDataPlusArr.length > 1) {
@@ -13144,7 +13511,6 @@ function svgPathSimplify(input = '', settings = {}) {
13144
13511
  svg = stringifySVG(svg, { omitNamespace, removeComments, format: minifyD });
13145
13512
 
13146
13513
  svgSizeOpt = svg.length;
13147
-
13148
13514
  compression = +(100 / svgSize * (svgSizeOpt)).toFixed(2);
13149
13515
 
13150
13516
  svgSize = +(svgSize / 1024).toFixed(3);
@@ -13170,15 +13536,52 @@ function svgPathSimplify(input = '', settings = {}) {
13170
13536
  ({ d, report } = paths[0]);
13171
13537
  }
13172
13538
 
13173
- if (polys.length && polys.length === 1) {
13174
- polys = polys[0];
13175
- }
13539
+ // sanitize poly output
13540
+ if (polys.length) {
13541
+
13542
+ // round point data
13543
+ polys.forEach((poly, i) => {
13544
+
13545
+ if (polyFormat === 'string') poly = normalizePoly(poly);
13546
+
13547
+ poly = roundPoly(poly, decimals);
13548
+ // remove coinciding points
13549
+ polys[i] = removeCoincidingVertices(poly);
13550
+ });
13551
+
13552
+ if (polys.length === 1) {
13553
+ polys = polys[0];
13554
+ }
13555
+
13556
+ if (polyFormat === 'string') {
13557
+
13558
+ polys = normalizePoly(polys, { toArray: true, flatten: true });
13559
+
13560
+ polys = polys.flat().join(' ');
13561
+ }
13176
13562
 
13177
- if (polyFormat === 'string' && polys.length) {
13178
- polys = polys.flat().map(pt => `${pt.x},${pt.y}`).join(' ');
13179
13563
  }
13180
13564
 
13181
- return !getObject ? (d ? d : svg) : { svg, d, polys, report, pathDataPlusArr: pathDataPlusArr_global, inputType, dOriginal };
13565
+ // create object
13566
+ let svgObj = new SlickVGObj({ svg, d, polys, report, pathDataPlusArr: pathDataPlusArr_global, inputType, dOriginal });
13567
+
13568
+ /*
13569
+ let d2 = svgObj.getD()
13570
+ console.log('d2', d2);
13571
+
13572
+ let svg2 = svgObj.getSvg()
13573
+ console.log('svg2', svg2);
13574
+
13575
+ let format = 'd'
13576
+ format = 'pathData'
13577
+ format = 'd'
13578
+ format = 'points'
13579
+ format = 'array'
13580
+ let poly2 = svgObj.getPoly({ format })
13581
+ console.log('poly2', 'format', format, poly2);
13582
+ */
13583
+
13584
+ return !getObject ? (d ? d : svg) : svgObj;
13182
13585
 
13183
13586
  }
13184
13587
 
@@ -13187,6 +13590,7 @@ function svgPathSimplify(input = '', settings = {}) {
13187
13590
  // IIFE
13188
13591
  if (typeof window !== 'undefined') {
13189
13592
  window.svgPathSimplify = svgPathSimplify;
13593
+ window.SlickVG = SlickVG;
13190
13594
  window.getElementTransform = getElementTransform;
13191
13595
  window.validateSVG = validateSVG;
13192
13596
  window.detectInputType = detectInputType;
@@ -13195,4 +13599,4 @@ if (typeof window !== 'undefined') {
13195
13599
 
13196
13600
  }
13197
13601
 
13198
- export { PI$1 as PI, abs$1 as abs, acos$1 as acos, asin$1 as asin, atan$1 as atan, atan2$1 as atan2, ceil$1 as ceil, cos$1 as cos, detectInputType, exp$1 as exp, floor$1 as floor, getElementTransform, getViewBox, hypot, log$1 as log, max$1 as max, min$1 as min, pow$1 as pow, random$1 as random, round$1 as round, sin$1 as sin, sqrt$1 as sqrt, svgPathSimplify, tan$1 as tan, validateSVG };
13602
+ export { PI$1 as PI, SlickVG, abs$1 as abs, acos$1 as acos, asin$1 as asin, atan$1 as atan, atan2$1 as atan2, ceil$1 as ceil, cos$1 as cos, detectInputType, exp$1 as exp, floor$1 as floor, getElementTransform, getViewBox, hypot, log$1 as log, max$1 as max, min$1 as min, pow$1 as pow, random$1 as random, round$1 as round, sin$1 as sin, sqrt$1 as sqrt, svgPathSimplify, tan$1 as tan, validateSVG };