svg-path-simplify 0.4.4 → 0.4.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/README.md +9 -1
  3. package/dist/svg-path-simplify.esm.js +1649 -1205
  4. package/dist/svg-path-simplify.esm.min.js +2 -2
  5. package/dist/svg-path-simplify.js +1649 -1204
  6. package/dist/svg-path-simplify.min.js +2 -2
  7. package/dist/svg-path-simplify.pathdata.esm.js +301 -637
  8. package/dist/svg-path-simplify.pathdata.esm.min.js +2 -2
  9. package/dist/svg-path-simplify.poly.cjs +8 -9
  10. package/drawing.svg +62 -0
  11. package/index.html +13 -8
  12. package/package.json +1 -1
  13. package/src/index.js +3 -1
  14. package/src/pathData_get_intersections.js +777 -0
  15. package/src/pathData_offset.js +201 -0
  16. package/src/pathData_simplify_cubic.js +19 -21
  17. package/src/pathData_simplify_cubic_extrapolate.js +147 -8
  18. package/src/pathData_simplify_cubicsToArcs.js +65 -21
  19. package/src/pathSimplify-main.js +267 -64
  20. package/src/pathSimplify-presets.js +1 -1
  21. package/src/poly-fit-curve-schneider.js +171 -132
  22. package/src/poly-fit-curve-schneider_check_bulge.js +131 -0
  23. package/src/poly-offset.js +133 -0
  24. package/src/simplify_poly_RC.js +29 -7
  25. package/src/svg-getAttributes.js +1 -1
  26. package/src/svgii/geometry.js +130 -27
  27. package/src/svgii/geometry_bbox.js +1 -1
  28. package/src/svgii/geometry_length.js +1 -1
  29. package/src/svgii/pathData_analyze.js +6 -28
  30. package/src/svgii/pathData_convert.js +63 -15
  31. package/src/svgii/pathData_remove_collinear.js +32 -36
  32. package/src/svgii/pathData_reorder.js +3 -2
  33. package/src/svgii/pathData_simplify_refineExtremes.js +1 -3
  34. package/src/svgii/pathData_stringify.js +132 -6
  35. package/src/svgii/pathData_toPolygon.js +33 -23
  36. package/src/svgii/poly_analyze.js +272 -125
  37. package/src/svgii/poly_analyze_cleanup.js +311 -0
  38. package/src/svgii/poly_analyze_getTangents.js +125 -0
  39. package/src/svgii/poly_analyze_get_chunks.js +3 -1
  40. package/src/svgii/poly_normalize.js +21 -8
  41. package/src/svgii/poly_to_pathdata.js +477 -8
  42. package/src/svgii/rounding.js +35 -14
  43. package/src/svgii/svg_cleanup.js +44 -51
  44. package/src/svgii/svg_cleanup_convertPathLength.js +5 -0
  45. package/src/svgii/svg_cleanup_normalize_transforms.js +2 -2
  46. package/src/svgii/visualize.js +33 -2
  47. package/v/0.4.5/index.html +1040 -0
  48. package/v/0.4.5/svg-path-simplify.js +13227 -0
@@ -380,7 +380,14 @@
380
380
 
381
381
  // get angle helper
382
382
  function getAngle(p1, p2, normalize = false) {
383
- let angle = atan2(p2.y - p1.y, p2.x - p1.x);
383
+ let angle = Math.atan2(p2.y - p1.y, p2.x - p1.x);
384
+ // normalize negative angles
385
+ if (normalize && angle < 0) angle += Math.PI * 2;
386
+ return angle
387
+ }
388
+
389
+ function getAngleFromDelta(dx, dy, normalize = false) {
390
+ let angle = Math.atan2(dy, dx);
384
391
  // normalize negative angles
385
392
  if (normalize && angle < 0) angle += Math.PI * 2;
386
393
  return angle
@@ -476,7 +483,9 @@
476
483
  // if we cast these lines infinitely in both directions, they intersect here:
477
484
  intersectionPoint = {
478
485
  x: p1.x + (a * (p2.x - p1.x)),
479
- y: p1.y + (a * (p2.y - p1.y))
486
+ y: p1.y + (a * (p2.y - p1.y)),
487
+ t1: a,
488
+ t2: b
480
489
  };
481
490
 
482
491
  let intersection = false;
@@ -578,13 +587,13 @@
578
587
  if (t === 0 && !shortCp1) {
579
588
  pt.x = p0.x;
580
589
  pt.y = p0.y;
581
- pt.angle = getAngle(p0, cp1);
590
+ if (getTangent) pt.angle = getAngle(p0, cp1);
582
591
  }
583
592
 
584
593
  else if (t === 1 && !shortCp2) {
585
594
  pt.x = p.x;
586
595
  pt.y = p.y;
587
- pt.angle = getAngle(cp2, p);
596
+ if (getTangent) pt.angle = getAngle(cp2, p);
588
597
  }
589
598
 
590
599
  else {
@@ -601,18 +610,38 @@
601
610
  pt = interpolate(m3, m4, t);
602
611
 
603
612
  // add angles
604
- pt.angle = getAngle(m3, m4);
613
+ if (getTangent) pt.angle = getAngle(m3, m4);
614
+
615
+ /*
616
+ renderPoint(markers, m0, 'cyan')
617
+ renderPoint(markers, m3, 'magenta')
618
+ renderPoint(markers, pt, 'green')
619
+ renderPoint(markers, m4, 'cyan')
620
+ renderPoint(markers, m2, 'magenta')
621
+ */
605
622
 
606
623
  // add control points
607
- if (getCpts) pt.cpts = [m1, m2, m3, m4];
624
+ if (getCpts) {
625
+ pt.cpts = [m1, m2, m3, m4];
626
+ pt.segments = [
627
+ { p0, cp1: m0, cp2: m3, p: pt },
628
+ { p0: pt, cp1: m4, cp2: m2, p }
629
+ ];
630
+ }
608
631
  } else {
609
632
  m1 = interpolate(p0, cp1, t);
610
633
  m2 = interpolate(cp1, p, t);
611
634
  pt = interpolate(m1, m2, t);
612
- pt.angle = getAngle(m1, m2);
635
+ if (getTangent) pt.angle = getAngle(m1, m2);
613
636
 
614
637
  // add control points
615
- if (getCpts) pt.cpts = [m1, m2];
638
+ if (getCpts) {
639
+ pt.cpts = [m1, m2];
640
+ pt.segments = [
641
+ { p0, cp1: m1, p: pt },
642
+ { p0: p, cp1: m2, p }
643
+ ];
644
+ }
616
645
  }
617
646
  }
618
647
 
@@ -696,7 +725,7 @@
696
725
  * get vertices from path command final on-path points
697
726
  */
698
727
 
699
- function getPathDataVertices(pathData=[], includeCpts = false, decimals = -1) {
728
+ function getPathDataVertices(pathData = [], includeCpts = false, decimals = -1) {
700
729
  let polyPoints = [];
701
730
 
702
731
  pathData.forEach((com) => {
@@ -783,7 +812,8 @@
783
812
 
784
813
  if (rx == 0 || ry == 0) {
785
814
  // invalid arguments
786
- throw Error("rx and ry can not be 0");
815
+ console.warn("rx and ry can not be 0");
816
+ return arcData
787
817
  }
788
818
 
789
819
  /**
@@ -796,10 +826,10 @@
796
826
  let s_phi = !phi ? 0 : Math.sin(phi);
797
827
  let c_phi = !phi ? 1 : Math.cos(phi);
798
828
 
799
- let hd_x = (x1 - x2) / 2;
800
- let hd_y = (y1 - y2) / 2;
801
- let hs_x = (x1 + x2) / 2;
802
- let hs_y = (y1 + y2) / 2;
829
+ let hd_x = (x1 - x2) * 0.5;
830
+ let hd_y = (y1 - y2) * 0.5;
831
+ let hs_x = (x1 + x2) * 0.5;
832
+ let hs_y = (y1 + y2) * 0.5;
803
833
 
804
834
  // F6.5.1
805
835
  let x1_ = !phi ? hd_x : c_phi * hd_x + s_phi * hd_y;
@@ -821,10 +851,11 @@
821
851
  let rxry = rx * ry;
822
852
  let rxy1_ = rx * y1_;
823
853
  let ryx1_ = ry * x1_;
824
- let sum_of_sq = rxy1_ ** 2 + ryx1_ ** 2; // sum of square
854
+ let sum_of_sq = rxy1_ * rxy1_ + ryx1_ * ryx1_; // sum of square
825
855
  if (!sum_of_sq) {
856
+ console.warn("start point can not be same as end point");
857
+ return arcData
826
858
 
827
- throw Error("start point can not be same as end point");
828
859
  }
829
860
  let coe = Math.sqrt(Math.abs((rxry * rxry - sum_of_sq) / sum_of_sq));
830
861
  if (largeArc === sweep) {
@@ -973,7 +1004,11 @@
973
1004
 
974
1005
  function getBezierExtremeT(pts, { addExtremes = true, addSemiExtremes = false } = {}) {
975
1006
  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 });
976
- return tArr;
1007
+ if (tArr.length) {
1008
+ tArr = tArr.map(t => +t.toFixed(9)).sort();
1009
+ }
1010
+
1011
+ return tArr
977
1012
  }
978
1013
 
979
1014
  function getArcExtemes(p0, values) {
@@ -1303,13 +1338,6 @@
1303
1338
  return tArr;
1304
1339
  }
1305
1340
 
1306
- function isMultipleOf45(angleRad, epsilon = 0.001) {
1307
- let rad90 = Math.PI / 2;
1308
- let rad45 = rad90 / 2;
1309
- let isRightAngle = Math.abs(angleRad / rad90 - Math.round(angleRad / rad90)) < epsilon;
1310
- return !isRightAngle ? Math.abs(angleRad / rad45 - Math.round(angleRad / rad45)) < epsilon : false;
1311
- }
1312
-
1313
1341
  function quadraticBezierExtremeT(p0, cp1, p, { addExtremes = true, addSemiExtremes = false } = {}) {
1314
1342
 
1315
1343
  // rotate cpts for semi extremes
@@ -1373,6 +1401,11 @@
1373
1401
  let dx = isArray ? p2[0] - p1[0] : (p2.x - p1.x);
1374
1402
  let dy = isArray ? p2[1] - p1[1] : (p2.y - p1.y);
1375
1403
 
1404
+ /*
1405
+ let sqrt2 = 1.4142135623730951
1406
+ return dx===dy ? Math.abs(dx) * sqrt2 : Math.sqrt(dx * dx + dy * dy);
1407
+ */
1408
+
1376
1409
  return Math.sqrt(dx * dx + dy * dy);
1377
1410
  }
1378
1411
 
@@ -1388,6 +1421,7 @@
1388
1421
  * sloppy but fast
1389
1422
  */
1390
1423
  function getDistManhattan(pt1, pt2) {
1424
+
1391
1425
  let dx = Math.abs(pt2.x - pt1.x);
1392
1426
  let dy = Math.abs(pt2.y - pt1.y);
1393
1427
  return dx + dy;
@@ -1855,7 +1889,7 @@
1855
1889
 
1856
1890
  let len = pathData.length;
1857
1891
  let decimals = decimalsGlobal;
1858
- let decimalsArc = decimals < 3 ? decimals+2 : decimals;
1892
+ let decimalsArc = decimals < 3 ? decimals + 1 : decimals;
1859
1893
 
1860
1894
  for (let c = 0; c < len; c++) {
1861
1895
  let com = pathData[c];
@@ -1890,7 +1924,7 @@
1890
1924
  }
1891
1925
  }
1892
1926
 
1893
- let dim_min = dims.sort();
1927
+ let dim_min = dims.sort((a,b)=>a-b);
1894
1928
  let sliceIdx = Math.ceil(dim_min.length / 8);
1895
1929
  dim_min = dim_min.slice(0, sliceIdx);
1896
1930
  let minVal = dim_min.reduce((a, b) => a + b, 0) / sliceIdx;
@@ -1910,31 +1944,50 @@
1910
1944
  let com = pathData[i];
1911
1945
  let { type, values, p0 = null, p = null, dimA = 0 } = com;
1912
1946
 
1913
- // use existing averave dimension value or calculate
1947
+ // use existing average dimension value or calculate
1914
1948
  if (values.length && p && p0) {
1915
-
1916
1949
  dimA = dimA ? dimA : getDistManhattan(p0, p);
1917
1950
 
1918
- if (dimA) dims.push(dimA);
1951
+ if (type === 'A') dimA *= 0.5;
1919
1952
 
1953
+ if (dimA) dims.push(+dimA.toFixed(8));
1920
1954
  }
1921
1955
 
1922
1956
  }
1923
1957
 
1924
- let dim_min = dims.sort();
1958
+ dims = dims.sort((a,b)=>a-b);
1959
+ let len = dims.length;
1960
+ let dim_mid = dims[Math.floor(len * 0.5)];
1925
1961
 
1926
- let sliceIdx = Math.ceil(dim_min.length / 6);
1927
- dim_min = dim_min.slice(0, sliceIdx);
1928
- let minVal = dim_min.reduce((a, b) => a + b, 0) / sliceIdx;
1962
+ // smallest 25% of values
1963
+ let idx_q = Math.ceil(len * 0.25);
1964
+ let dims_min = dims.slice(0, idx_q);
1965
+
1966
+ // average smallest values with mid value
1967
+ let dim_min = ((dims_min.reduce((a, b) => a + b, 0) / idx_q) + dim_mid) * 0.5;
1929
1968
 
1930
1969
  let threshold = 75;
1931
- let decimalsAuto = minVal > threshold * 1.5 ? 0 : Math.floor(threshold / minVal).toString().length;
1970
+ let decimalsAuto = dim_min > threshold * 1.5 ? 0 : Math.floor(threshold / dim_min).toString().length;
1932
1971
 
1933
1972
  // clamp
1934
1973
  return Math.min(Math.max(0, decimalsAuto), 8)
1935
1974
 
1936
1975
  }
1937
1976
 
1977
+ function roundPoly(poly = [], decimals = 3) {
1978
+ if (!poly.length || decimals < 0) return poly;
1979
+ poly = poly.map(pt => roundPoint(pt, decimals));
1980
+ return poly
1981
+ }
1982
+
1983
+ function roundPoint(pt = {}, decimals = 3) {
1984
+ if (pt.x === undefined || pt.y === undefined || decimals < 0) return pt;
1985
+ pt.x=roundTo(pt.x, decimals);
1986
+ pt.y=roundTo(pt.y, decimals);
1987
+ return pt
1988
+
1989
+ }
1990
+
1938
1991
  /**
1939
1992
  * rounding helper
1940
1993
  * allows for quantized rounding
@@ -2497,7 +2550,7 @@
2497
2550
  attributes.forEach(att=>{
2498
2551
 
2499
2552
  let value = normalizeUnits(el.getAttribute(att), {x, y, width, height});
2500
- atts[att.name] = value;
2553
+ atts[att] = value;
2501
2554
  });
2502
2555
 
2503
2556
  return atts
@@ -2709,7 +2762,7 @@
2709
2762
  }
2710
2763
  }
2711
2764
 
2712
- let bbox = { x: xMin, y: yMin, width: xMax - xMin, height: yMax - yMin };
2765
+ let bbox = { x: xMin, y: yMin, right: xMax, width: xMax - xMin, bottom: yMax, height: yMax - yMin };
2713
2766
  return bbox
2714
2767
  }
2715
2768
 
@@ -2899,7 +2952,7 @@
2899
2952
  * d attribute string
2900
2953
  */
2901
2954
 
2902
- function pathDataToD(pathData, mode = 0) {
2955
+ function pathDataToD(pathData = [], mode = 0) {
2903
2956
 
2904
2957
  mode = parseFloat(mode);
2905
2958
  /*
@@ -2908,80 +2961,99 @@
2908
2961
  1 = verbose
2909
2962
  2 = beautify
2910
2963
  */
2964
+
2911
2965
  let len = pathData.length;
2966
+ let d = '';
2912
2967
 
2913
- let valsString = pathData[0].values.join(" ");
2914
- let separator_command = mode > 1 ? `\n` :
2915
- ((mode < 1) ? '' : ' ');
2916
- let separator_type = mode > 0.5 ? ' ' : '';
2968
+ // group same types
2969
+ let pathDataGrouped = mode >0.5 ? JSON.parse(JSON.stringify(pathData)) : [];
2970
+ let typePrev = 'M';
2917
2971
 
2918
- // 1st command
2919
- let d = `${pathData[0].type}${separator_type}${valsString}${separator_command}`;
2972
+ if (mode < 1) {
2973
+ pathDataGrouped = [pathData[0]];
2920
2974
 
2921
- for (let i = 1; i < len; i++) {
2922
- let com0 = pathData[i - 1];
2923
- let com = pathData[i];
2924
- let { type, values } = com;
2925
- valsString = '';
2926
-
2927
- // Minify Arc commands (A/a) – actually sucks!
2928
- if (!mode && (type === 'A' || type === 'a')) {
2929
- values = [
2930
- values[0], values[1], values[2],
2931
- `${values[3]}${values[4]}${values[5]}`,
2932
- values[6]
2933
- ];
2975
+ let idx = 0;
2976
+
2977
+ for (let i = 1; i < len; i++) {
2978
+ let com = pathData[i];
2979
+ let { type } = com;
2980
+ // decouple from object
2981
+ let values = [...com.values];
2982
+
2983
+ // new type
2984
+ if (type !== typePrev) {
2985
+ pathDataGrouped.push({type, values});
2986
+ idx++;
2987
+ } else {
2988
+ pathDataGrouped[idx].values.push(...values);
2989
+ }
2990
+
2991
+ // update type
2992
+ typePrev = type;
2934
2993
  }
2994
+ }
2995
+
2996
+ // stringify grouped
2997
+ len = pathDataGrouped.length;
2998
+ let separator_type = mode < 1 ? '' : ' ';
2999
+ let separator_command = mode < 1 ? '' : (mode === 1 ? ' ' : `\n`);
2935
3000
 
2936
- // Omit type for repeated commands
2937
- type = ((mode < 1) && com0.type === com.type && com.type.toLowerCase() !== 'm')
2938
- ? " "
2939
- : ((mode < 1) && com0.type === "M" && com.type === "L"
2940
- ? " "
2941
- : com.type);
3001
+ typePrev = 'M';
2942
3002
 
2943
- // concatenate subsequent floating point values
2944
- if (!mode) {
3003
+ for (let i = 0; i < len; i++) {
3004
+ let com = pathDataGrouped[i];
3005
+ let { type, values } = com;
2945
3006
 
2946
- let prevWasFloat = false;
3007
+ // we're always starting a path with absolute M!
3008
+ let omitType = mode < 1 && ((typePrev === 'M' && type === 'L') || (typePrev === 'm' && type === 'l'));
2947
3009
 
2948
- for (let v = 0, l = values.length; v < l; v++) {
2949
- let val = values[v];
2950
- let valStr = val.toString();
2951
- let isFloat = valStr.includes('.');
2952
- let isSmallFloat = isFloat && Math.abs(val) < 1;
3010
+ // add type
3011
+ if (!omitType) d += type + separator_type;
2953
3012
 
2954
- // Remove leading zero from small floats *only* if the previous was also a float
2955
- if (isSmallFloat && prevWasFloat) {
2956
- valStr = valStr.replace(/^0\./, '.');
2957
- }
3013
+ // add values
3014
+ let wasSmallFloat = false;
3015
+ let separatorVal = ' ';
2958
3016
 
2959
- // Add space unless this is the first value OR previous was a small float
2960
- if (v > 0 && !(prevWasFloat && isSmallFloat)) {
2961
- valsString += ' ';
2962
- }
3017
+ for (let v = 0, vlen = values.length; vlen && v < vlen; v++) {
3018
+ let val = values[v];
3019
+ let valAbs = Math.abs(val);
3020
+ let valStr = val.toString();
3021
+ let isNegative = val < 0;
3022
+ let sign = isNegative ? '-' : '';
3023
+ let isSmallFloat = mode > 0.5 ? false : (val && valAbs < 1);
3024
+ let idxSub = isSmallFloat ? (isNegative ? 2 : 1) : 0;
3025
+
3026
+ // we don't need whitespace for first value
3027
+ separatorVal = v === 0 || isNegative ? '' : ' ';
3028
+
3029
+ if (mode < 1) {
3030
+ // omit leading zero
3031
+ if (isSmallFloat) valStr = sign + valStr.substring(idxSub);
3032
+
3033
+ // omit whitespace for subsequent small floats
3034
+ separatorVal = (v === 0 && !omitType) || (wasSmallFloat && isSmallFloat) ?
3035
+ (!mode ? '' : (isNegative ? '' : ' '))
3036
+ : (isNegative ? '' : ' ');
2963
3037
 
2964
- valsString += valStr;
2965
- prevWasFloat = isSmallFloat;
2966
3038
  }
2967
3039
 
2968
- }
2969
- // regular non-minified output
2970
- else {
2971
- valsString = values.join(' ');
3040
+ // omit separator between large Arc sweep and final x in minify mode
3041
+ if (!mode && (type === 'a' || type === 'A')) {
3042
+ let pos = (v % 7);
3043
+ if (pos > 3 && pos < 6) separatorVal = '';
3044
+ }
3045
+
3046
+ d += `${separatorVal}${valStr}`;
3047
+ wasSmallFloat = isSmallFloat;
3048
+
2972
3049
  }
2973
3050
 
2974
- if(i===len-1) separator_command='';
2975
- d += `${type}${separator_type}${valsString}${separator_command}`;
2976
- }
3051
+ // add command separator
3052
+ if (mode) d += separator_command;
3053
+
3054
+ // update previous type
3055
+ typePrev = type;
2977
3056
 
2978
- if (mode < 1) {
2979
- d = d
2980
- .replace(/[A-Za-z]0(?=\.)/g, m => m[0])
2981
- .replace(/ 0\./g, " .") // Space before small decimals
2982
- .replace(/ -/g, "-") // Remove space before negatives
2983
- .replace(/-0\./g, "-.") // Remove leading zero from negative decimals
2984
- .replace(/Z/g, "z"); // Convert uppercase 'Z' to lowercase
2985
3057
  }
2986
3058
 
2987
3059
  return d;
@@ -2989,39 +3061,26 @@
2989
3061
 
2990
3062
  function getCombinedByDominant(com1, com2, maxDist = 0, tolerance = 1, debug = false) {
2991
3063
 
2992
- // cubic Bézier derivative
2993
- const cubicDerivative = (p0, p1, p2, p3, t) => {
2994
- let mt = 1 - t;
2995
-
2996
- return {
2997
- x:
2998
- 3 * mt * mt * (p1.x - p0.x) +
2999
- 6 * mt * t * (p2.x - p1.x) +
3000
- 3 * t * t * (p3.x - p2.x),
3001
- y:
3002
- 3 * mt * mt * (p1.y - p0.y) +
3003
- 6 * mt * t * (p2.y - p1.y) +
3004
- 3 * t * t * (p3.y - p2.y)
3005
- };
3006
- };
3007
-
3008
3064
  // if combining fails return original commands
3009
3065
  let commands = [com1, com2];
3010
3066
 
3011
3067
  // detect dominant
3012
- let dist1 = getDistAv(com1.p0, com1.p);
3013
- let dist2 = getDistAv(com2.p0, com2.p);
3068
+ let dist1 = getDistManhattan(com1.p0, com1.p);
3069
+ let dist2 = getDistManhattan(com2.p0, com2.p);
3070
+ let thresh = (dist1 + dist2) * 0.5 * 0.075 * tolerance;
3014
3071
 
3015
- let reverse = dist1 > dist2;
3072
+ // take longer command
3073
+ let reverse = dist1 < dist2;
3016
3074
 
3017
3075
  // backup original commands
3018
3076
  let com1_o = JSON.parse(JSON.stringify(com1));
3019
3077
  let com2_o = JSON.parse(JSON.stringify(com2));
3020
3078
 
3021
- let ptI = checkLineIntersection(com1_o.p0, com1_o.cp1, com2_o.p, com2_o.cp2, false);
3079
+ // intersection of control tangents
3080
+ let ptI = checkLineIntersection(com1_o.p0, com1_o.cp1, com2_o.p, com2_o.cp2, false, true);
3022
3081
 
3082
+ // no intersection - we can't combine
3023
3083
  if (!ptI) {
3024
-
3025
3084
  return commands
3026
3085
  }
3027
3086
 
@@ -3044,153 +3103,70 @@
3044
3103
  com2 = com2_R;
3045
3104
  }
3046
3105
 
3047
- let add = (a, b) => ({ x: a.x + b.x, y: a.y + b.y });
3048
- let sub = (a, b) => ({ x: a.x - b.x, y: a.y - b.y });
3049
- let mul = (a, s) => ({ x: a.x * s, y: a.y * s });
3050
- let dot = (a, b) => a.x * b.x + a.y * b.y;
3051
-
3052
- // estimate extrapolation parameter t0
3053
-
3054
- let B0 = com2.p0;
3055
- let D0 = cubicDerivative(
3056
- com2.p0,
3057
- com2.cp1,
3058
- com2.cp2,
3059
- com2.p,
3060
- 0
3061
- );
3062
-
3063
- let v = sub(com1.p0, B0);
3064
-
3065
- // first-order projection onto tangent
3066
- let t0 = dot(v, D0) / dot(D0, D0);
3067
-
3068
- // refine with one Newton iteration (optional but cheap)
3069
- let P = pointAtT([com2.p0, com2.cp1, com2.cp2, com2.p], t0);
3070
- let dP = cubicDerivative(com2.p0, com2.cp1, com2.cp2, com2.p, t0);
3071
- let r = sub(P, com1.p0);
3072
-
3073
- t0 -= dot(r, dP) / dot(dP, dP);
3074
-
3075
- // construct merged cubic over [t0, 1]
3076
- let Q0 = pointAtT([com2.p0, com2.cp1, com2.cp2, com2.p], t0);
3077
- let Q3 = com2.p;
3078
-
3079
- let d0 = cubicDerivative(com2.p0, com2.cp1, com2.cp2, com2.p, t0);
3080
- let d1 = cubicDerivative(com2.p0, com2.cp1, com2.cp2, com2.p, 1);
3081
-
3082
- let scale = 1 - t0;
3083
-
3084
- let Q1 = add(Q0, mul(d0, scale / 3));
3085
- let Q2 = sub(Q3, mul(d1, scale / 3));
3086
-
3087
- let result = {
3088
- p0: Q0,
3089
- cp1: Q1,
3090
- cp2: Q2,
3091
- p: Q3,
3092
- t0
3093
- };
3094
-
3095
- if (reverse) {
3096
- result = {
3097
- p0: Q3,
3098
- cp1: Q2,
3099
- cp2: Q1,
3100
- p: Q0,
3101
- t0
3102
- };
3103
- }
3104
-
3105
- let tMid = (1 - t0) * 0.5;
3106
+ // etsimate t for extrapolation
3107
+ let PtI = checkLineIntersection(com1.cp2, com1.p, com2.p, com2.cp2, false, true);
3108
+ let cp1_I = interpolate(com1.p, PtI, 0.666);
3106
3109
 
3107
- let ptM = pointAtT([result.p0, result.cp1, result.cp2, result.p], tMid, false, true);
3108
- let seg1_cp2 = ptM.cpts[2];
3110
+ let dist1_2 = getDistManhattan(com1.cp2, com1.p);
3111
+ let dist2_2 = getDistManhattan(com1.cp2, cp1_I);
3112
+ let t = dist2_2 / dist1_2;
3109
3113
 
3110
- let ptI_1 = checkLineIntersection(ptM, seg1_cp2, result.p0, ptI, false);
3111
- let ptI_2 = checkLineIntersection(ptM, seg1_cp2, result.p, ptI, false);
3114
+ // extrapolate
3115
+ let segs = pointAtT([com1.p0, com1.cp1, com1.cp2, com1.p], t, false, true).segments;
3112
3116
 
3113
- let cp1_2 = interpolate(result.p0, ptI_1, 1.333 );
3114
- let cp2_2 = interpolate(result.p, ptI_2, 1.333 );
3117
+ let seg = segs[0];
3115
3118
 
3116
- // test self intersections and exit
3117
- let cp_intersection = checkLineIntersection(com1_o.p0, cp1_2, com2_o.p, cp2_2, true);
3118
- if (cp_intersection) {
3119
+ // if it worked - points should be nearby
3120
+ let dist = getDistManhattan(seg.p, com2.p);
3119
3121
 
3120
- return commands;
3121
- }
3122
-
3123
- result.cp1 = cp1_2;
3124
- result.cp2 = cp2_2;
3125
-
3126
- // check distances between original starting point and extrapolated
3127
- let dist3 = getDistAv(com1_o.p0, result.p0);
3128
- let dist4 = getDistAv(com2_o.p, result.p);
3129
- let dist5 = (dist3 + dist4);
3130
-
3131
- // use original points
3132
- result.p0 = com1_o.p0;
3133
- result.p = com2_o.p;
3134
- result.extreme = com2_o.extreme;
3135
- result.corner = com2_o.corner;
3136
- result.dimA = com2_o.dimA;
3137
- result.directionChange = com2_o.directionChange;
3138
- result.type = 'C';
3139
- result.values = [result.cp1.x, result.cp1.y, result.cp2.x, result.cp2.y, result.p.x, result.p.y];
3122
+ // if close enough adjust
3123
+ if (dist < thresh) {
3124
+ let angle = getAngle(seg.p, seg.cp2);
3125
+ let angle2 = getAngle(com2.p, com2.cp2);
3140
3126
 
3141
- // extrapolated starting point is not completely off
3142
- if (dist5 < maxDist) {
3143
-
3144
- /*
3145
- let tTotal = 1 + Math.abs(t0);
3146
- let tSplit = reverse ? 1 + t0 : Math.abs(t0);
3147
-
3148
- let pO = pointAtT([com2_o.p0, com2_o.cp1, com2_o.cp2, com2_o.p], t0);
3149
- */
3127
+ let angleDiff = (angle2 - angle);
3150
3128
 
3151
- // split t to meet original mid segment start point
3152
- let tSplit = reverse ? 1 + t0 : Math.abs(t0);
3129
+ // adjust cp angle
3130
+ seg.cp2 = rotatePoint(seg.cp2, seg.p.x, seg.p.y, angleDiff);
3131
+ let dist1 = getDistManhattan(seg.p, seg.cp2);
3153
3132
 
3154
- let tTotal = 1 + Math.abs(t0);
3155
- tSplit = reverse ? 1 + t0 : Math.abs(t0) / tTotal;
3133
+ // copy original final point coordinates
3134
+ seg.p = com2.p;
3156
3135
 
3157
- let ptSplit = pointAtT([result.p0, result.cp1, result.cp2, result.p], tSplit);
3158
- let distSplit = getDistAv(ptSplit, com1.p);
3136
+ // after rotation
3137
+ let dist2 = getDistManhattan(seg.p, seg.cp2);
3138
+ let scale = dist2 / dist1;
3159
3139
 
3160
- // not close enough - exit
3161
- if (distSplit > maxDist * tolerance ) {
3140
+ // adjust tangent length
3141
+ seg.cp2 = interpolate(seg.p, seg.cp2, scale);
3162
3142
 
3163
- return commands;
3143
+ // reverse back
3144
+ if (reverse) {
3145
+ seg = {
3146
+ p0: seg.p,
3147
+ p: seg.p0,
3148
+ cp1: seg.cp2,
3149
+ cp2: seg.cp1,
3150
+ };
3164
3151
  }
3165
3152
 
3166
- // compare combined with original area
3167
- let pathData0 = [
3168
- { type: 'M', values: [com1_o.p0.x, com1_o.p0.y] },
3169
- { 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] },
3170
- { 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] },
3171
- ];
3153
+ commands = [
3154
+ {
3155
+ type: 'C',
3156
+ values: [seg.cp1.x, seg.cp1.y, seg.cp2.x, seg.cp2.y, seg.p.x, seg.p.y],
3157
+ p0: seg.p0,
3158
+ cp1: seg.cp1,
3159
+ cp2: seg.cp2,
3160
+ p: seg.p,
3161
+ extreme: com2_o.extreme,
3162
+ corner: com2_o.corner,
3163
+ directionChange: com2_o.directionChange,
3164
+ dimA: getDistManhattan(seg.p0, seg.p),
3165
+ error: dist
3172
3166
 
3173
- let area0 = getPathArea(pathData0);
3174
- let pathDataN = [
3175
- { type: 'M', values: [result.p0.x, result.p0.y] },
3176
- { type: 'C', values: [result.cp1.x, result.cp1.y, result.cp2.x, result.cp2.y, result.p.x, result.p.y] },
3167
+ }
3177
3168
  ];
3178
3169
 
3179
- let areaN = getPathArea(pathDataN);
3180
- let areaDiff = Math.abs(areaN / area0 - 1);
3181
-
3182
- result.error = areaDiff * 5 * tolerance;
3183
-
3184
- if (debug) {
3185
- pathDataToD(pathDataN);
3186
-
3187
- }
3188
-
3189
- // success!!!
3190
- if (areaDiff < 0.05 * tolerance) {
3191
- commands = [result];
3192
-
3193
- }
3194
3170
  }
3195
3171
 
3196
3172
  return commands
@@ -3242,7 +3218,7 @@
3242
3218
  error += com.error;
3243
3219
 
3244
3220
  // find next candidates
3245
- for (let n = i + 1; error < tolerance && n < l; n++) {
3221
+ for (let n = i + offset; error < tolerance && n < l; n++) {
3246
3222
  let comN = pathData[n];
3247
3223
 
3248
3224
  if (comN.type !== 'C' ||
@@ -3252,6 +3228,7 @@
3252
3228
  (keepExtremes && com.extreme)
3253
3229
  )
3254
3230
  ) {
3231
+
3255
3232
  break
3256
3233
  }
3257
3234
 
@@ -3259,6 +3236,7 @@
3259
3236
 
3260
3237
  // failure - could not be combined - exit loop
3261
3238
  if (combined.length > 1) {
3239
+
3262
3240
  break
3263
3241
  }
3264
3242
 
@@ -3272,6 +3250,7 @@
3272
3250
 
3273
3251
  // return combined
3274
3252
  com = combined[0];
3253
+
3275
3254
  }
3276
3255
 
3277
3256
  pathDataN.push(com);
@@ -3310,11 +3289,12 @@
3310
3289
  // quit if t is start
3311
3290
  if (!t) return commands;
3312
3291
 
3292
+ // get averaged threshold
3313
3293
  let distAv1 = getDistManhattan(com1.p0, com1.p);
3314
3294
  let distAv2 = getDistManhattan(com2.p0, com2.p);
3315
3295
  let distMin = Math.max(0, Math.min(distAv1, distAv2));
3316
3296
 
3317
- let distScale = 0.08;
3297
+ let distScale = 0.075;
3318
3298
  let maxDist = distMin * distScale * tolerance;
3319
3299
 
3320
3300
  // get hypothetical combined command
@@ -3362,16 +3342,6 @@
3362
3342
 
3363
3343
  if (error < maxDist) success = true;
3364
3344
 
3365
- /*
3366
- renderPoint(markers, ptM_seg1, 'cyan')
3367
- renderPoint(markers, pt, 'orange', '1.5%', '1')
3368
- renderPoint(markers, ptM_seg2, 'orange')
3369
-
3370
- renderPoint(markers, com1.p, 'green')
3371
-
3372
- renderPoint(markers, ptI_seg1, 'purple')
3373
- */
3374
-
3375
3345
  }
3376
3346
 
3377
3347
  } // end 1st try
@@ -3385,9 +3355,9 @@
3385
3355
 
3386
3356
  comS.dimA = getDistManhattan(comS.p0, comS.p);
3387
3357
  comS.type = 'C';
3358
+
3388
3359
  comS.extreme = com2.extreme;
3389
3360
  comS.directionChange = com2.directionChange;
3390
-
3391
3361
  comS.corner = com2.corner;
3392
3362
 
3393
3363
  comS.values = [comS.cp1.x, comS.cp1.y, comS.cp2.x, comS.cp2.y, comS.p.x, comS.p.y];
@@ -3448,7 +3418,9 @@
3448
3418
  }
3449
3419
  */
3450
3420
 
3451
- return l1 / l3
3421
+ let t = l1 / l3;
3422
+
3423
+ return t;
3452
3424
  }
3453
3425
 
3454
3426
  function commandIsFlat(points, {
@@ -3501,7 +3473,7 @@
3501
3473
  detectDirection = true,
3502
3474
  detectSemiExtremes = false,
3503
3475
  debug = false,
3504
- addSquareLength = true,
3476
+ addSquareLength = false,
3505
3477
  addArea = true,
3506
3478
 
3507
3479
  } = {}) {
@@ -3550,8 +3522,8 @@
3550
3522
  let commandPts = (type === 'C' || type === 'Q') ?
3551
3523
  (type === 'C' ? [p0, cp1, cp2, p] : [p0, cp1, p]) :
3552
3524
  ([p0, p]);
3553
- let thresholdLength = dimA * 0.1;
3554
- let threshold = thresholdLength * 0.01;
3525
+
3526
+ let threshold = dimA * 0.005;
3555
3527
 
3556
3528
  // bezier types
3557
3529
  let isBezier = type === 'Q' || type === 'C';
@@ -3564,7 +3536,7 @@
3564
3536
  */
3565
3537
  let hasExtremes = false;
3566
3538
 
3567
- if (isBezier) {
3539
+ if (detectExtremes && isBezier) {
3568
3540
 
3569
3541
  let dx = type === 'C' ? Math.abs(com.cp2.x - com.p.x) : Math.abs(com.cp1.x - com.p.x);
3570
3542
  let dy = type === 'C' ? Math.abs(com.cp2.y - com.p.y) : Math.abs(com.cp1.y - com.p.y);
@@ -3603,7 +3575,7 @@
3603
3575
  }
3604
3576
 
3605
3577
  // check extremes introduce by small arcs
3606
- else if(isArc && comN && ((comPrev.type==='C' || comPrev.type==='Q') || (comN.type==='C' || comN.type==='Q')) ){
3578
+ else if(detectExtremes && isArc && comN && ((comPrev.type==='C' || comPrev.type==='Q') || (comN.type==='C' || comN.type==='Q')) ){
3607
3579
  let distN = comN ? comN.dimA : 0;
3608
3580
  let isShort = com.dimA < (comPrev.dimA + distN) * 0.1;
3609
3581
  let smallRadius = com.values[0] === com.values[1] && (com.values[0] < 1);
@@ -3621,28 +3593,7 @@
3621
3593
  if (hasExtremes) com.extreme = true;
3622
3594
 
3623
3595
  // Corners and semi extremes
3624
- if (isBezier && isBezierN) {
3625
-
3626
- // semi extremes
3627
- if (detectSemiExtremes && !com.extreme) {
3628
-
3629
- let dx1 = Math.abs(p.x - cp2.x);
3630
- let dy1 = Math.abs(p.y - cp2.y);
3631
- let hasSemiExtreme = false;
3632
-
3633
- // exclude extremes or small deltas
3634
- if (dx1 && dy1 && dx1 > thresholdLength || dy1 > thresholdLength) {
3635
- let ang1 = getAngle(cp2, p);
3636
- let ang2 = getAngle(p, comN.cp1);
3637
-
3638
- let ang3 = Math.abs(ang1 + ang2) / 2;
3639
- hasSemiExtreme = isMultipleOf45(ang3);
3640
- }
3641
-
3642
- if (hasSemiExtreme) {
3643
- com.semiExtreme = true;
3644
- }
3645
- }
3596
+ if (detectCorners && isBezier && isBezierN) {
3646
3597
 
3647
3598
  /**
3648
3599
  * Detect direction change points
@@ -4222,7 +4173,12 @@
4222
4173
  if (hasShorthands && toLonghands) pathData = pathDataToLonghands(pathData);
4223
4174
 
4224
4175
  // minify semicircle radii
4225
- if (optimizeArcs) pathData = optimizeArcPathData(pathData);
4176
+ if (optimizeArcs) {
4177
+ pathData = optimizeArcPathData(pathData);
4178
+ } else {
4179
+ // get true absolute radii
4180
+ pathData = pathDataToTrueArcValues(pathData);
4181
+ }
4226
4182
 
4227
4183
  if (toShorthands) pathData = pathDataToShorthands(pathData);
4228
4184
 
@@ -4312,9 +4268,42 @@
4312
4268
  }
4313
4269
 
4314
4270
  /**
4315
- *
4316
- * @param {*} pathData
4317
- * @returns
4271
+ * Converts minified arc
4272
+ * values to true rx and ry values
4273
+ */
4274
+ function pathDataToTrueArcValues(pathData = []) {
4275
+ let l = pathData.length;
4276
+ let pathDataN = [pathData[0]];
4277
+
4278
+ for (let i = 1; i < l; i++) {
4279
+ let com = pathData[i];
4280
+ let comPrev = pathData[i-1];
4281
+ let { type, values } = com;
4282
+
4283
+ if (type === 'A') {
4284
+
4285
+ // previous commands final on-path point
4286
+ let [x1, y1] = comPrev.values.slice(-2);
4287
+ let [rx, ry, xAxisRotation, largeArc, sweep, x2, y2] = values;
4288
+ let arcData = svgArcToCenterParam(x1, y1, rx, ry, xAxisRotation, largeArc, sweep, x2, y2);
4289
+
4290
+ // set true arc values
4291
+ com.values[0] = arcData.rx;
4292
+ com.values[1] = arcData.ry;
4293
+
4294
+ }
4295
+
4296
+ pathDataN.push(com);
4297
+
4298
+ }
4299
+ return pathDataN;
4300
+ }
4301
+
4302
+ /**
4303
+ * Minify arc radii
4304
+ * for semi circles
4305
+ * returns smaller rx and ry
4306
+ * values
4318
4307
  */
4319
4308
 
4320
4309
  function optimizeArcPathData(pathData = []) {
@@ -4371,7 +4360,7 @@
4371
4360
  if (isHorizontal || isVertical) {
4372
4361
 
4373
4362
  // check if semi circle
4374
- let needsTrueR = isHorizontal ? rx*1.9 > diffX : ry*1.9 > diffY;
4363
+ let needsTrueR = isHorizontal ? rx * 1.9 > diffX : ry * 1.9 > diffY;
4375
4364
 
4376
4365
  // is semicircle we can simplify rx
4377
4366
  if (!needsTrueR) {
@@ -5067,8 +5056,8 @@
5067
5056
  let phi = rotation ? rotation * TAU / 360 : 0;
5068
5057
  let sinphi = phi ? Math.sin(phi) : 0;
5069
5058
  let cosphi = phi ? Math.cos(phi) : 1;
5070
- let pxp = cosphi * (p0.x - x) / 2 + sinphi * (p0.y - y) / 2;
5071
- let pyp = -sinphi * (p0.x - x) / 2 + cosphi * (p0.y - y) / 2;
5059
+ let pxp = cosphi * (p0.x - x) *0.5 + sinphi * (p0.y - y) *0.5;
5060
+ let pyp = -sinphi * (p0.x - x) *0.5 + cosphi * (p0.y - y) *0.5;
5072
5061
 
5073
5062
  if (pxp === 0 && pyp === 0) {
5074
5063
  return []
@@ -5104,8 +5093,8 @@
5104
5093
 
5105
5094
  let centerxp = radicant ? radicant * rx / ry * pyp : 0;
5106
5095
  let centeryp = radicant ? radicant * -ry / rx * pxp : 0;
5107
- let centerx = cosphi * centerxp - sinphi * centeryp + (p0.x + x) / 2;
5108
- let centery = sinphi * centerxp + cosphi * centeryp + (p0.y + y) / 2;
5096
+ let centerx = cosphi * centerxp - sinphi * centeryp + (p0.x + x) * 0.5;
5097
+ let centery = sinphi * centerxp + cosphi * centeryp + (p0.y + y) * 0.5;
5109
5098
 
5110
5099
  let vx1 = (pxp - centerxp) / rx;
5111
5100
  let vy1 = (pyp - centeryp) / ry;
@@ -5127,15 +5116,17 @@
5127
5116
  ang2 = vectorAngle(vx1, vy1, vx2, vy2);
5128
5117
 
5129
5118
  if (sweepFlag === 0 && ang2 > 0) {
5130
- ang2 -= Math.PI * 2;
5119
+
5120
+ ang2 -= TAU;
5131
5121
  }
5132
5122
  else if (sweepFlag === 1 && ang2 < 0) {
5133
- ang2 += Math.PI * 2;
5123
+
5124
+ ang2 += TAU;
5134
5125
  }
5135
5126
 
5136
- let ratio = +(Math.abs(ang2) / (TAU / 4)).toFixed(0) || 1;
5127
+ let ratio = +(Math.abs(ang2) / (TAU * 0.25)).toFixed(0) || 1;
5137
5128
 
5138
- // increase segments for more accureate length calculations
5129
+ // increase segments for more accurate length calculations
5139
5130
  let segments = ratio * splitSegments;
5140
5131
  ang2 /= segments;
5141
5132
  let pathDataArc = [];
@@ -5147,7 +5138,8 @@
5147
5138
  const k = 0.551785;
5148
5139
  let a = ang2 === angle90 ? k :
5149
5140
  (
5150
- ang2 === -angle90 ? -k : 4 / 3 * Math.tan(ang2 / 4)
5141
+
5142
+ ang2 === -angle90 ? -k : 1.33333 * Math.tan(ang2 * 0.25)
5151
5143
  );
5152
5144
 
5153
5145
  let cos2 = ang2 ? Math.cos(ang2) : 1;
@@ -5194,7 +5186,7 @@
5194
5186
  let comA = cubicCommandToArc(p0, cp1, cp2, p, areaThreshold);
5195
5187
  let comAN = cubicCommandToArc(comN.p0, comN.cp1, comN.cp2, comN.p, areaThreshold);
5196
5188
 
5197
- if (comA.isArc && comAN.isArc) {
5189
+ if (comA.isArc && comAN.isArc) {
5198
5190
 
5199
5191
  let dist = getDistManhattan(p0, comN.p);
5200
5192
  let maxDist = dist * 0.01;
@@ -5209,8 +5201,18 @@
5209
5201
  let sweep = area < 0 ? 0 : 1;
5210
5202
 
5211
5203
  if (vertical || horizontal) {
5204
+
5212
5205
  rx = Math.min(rx, comAN.rx);
5213
5206
  ry = Math.min(ry, comAN.ry);
5207
+
5208
+ let diffR = Math.abs(rx - ry) / rx;
5209
+ let isSemiCircle = diffR < 0.025;
5210
+
5211
+ if (isSemiCircle) {
5212
+ rx = rx > 1 ? 1 : Math.min(rx, ry);
5213
+ ry = rx;
5214
+ }
5215
+
5214
5216
  pathData[c] = null;
5215
5217
  pathData[c + 1].type = 'A';
5216
5218
  pathData[c + 1].values = [rx, ry, 0, 0, sweep, comN.p.x, comN.p.y];
@@ -5233,33 +5235,61 @@
5233
5235
  let arcSegArea = 0, isArc = false;
5234
5236
 
5235
5237
  // check angles
5236
- let angle1 = getAngle(p0, cp1, true);
5237
- let angle2 = getAngle(p, cp2, true);
5238
+ let dx1 = (cp1.x - p0.x);
5239
+ let dy1 = (cp1.y - p0.y);
5240
+ let dx2 = (cp2.x - p.x);
5241
+ let dy2 = (cp2.y - p.y);
5242
+
5243
+ let thresh = getDistManhattan(p0, p) * 0.001;
5244
+
5245
+ let isVertical1 = dx1 < thresh;
5246
+ let isVertical2 = dx2 < thresh;
5247
+
5248
+ let isHorizontal1 = dy1 < thresh;
5249
+ let isHorizontal2 = dy2 < thresh;
5250
+
5251
+ let isRightAngle = (isVertical1 || isVertical2) && (isHorizontal1 || isHorizontal2);
5252
+
5253
+ /*
5254
+ let angle1 = getAngleFromDelta(dx1, dy1, true);
5255
+ let angle2 = getAngleFromDelta(dx2, dy2, true);
5238
5256
  let deltaAngle = Math.abs(angle1 - angle2) * 180 / Math.PI;
5239
5257
 
5240
5258
  let angleDiff = Math.abs((deltaAngle % 180) - 90);
5241
5259
  let isRightAngle = angleDiff < 3;
5260
+ */
5242
5261
 
5243
5262
  let rx = 0;
5244
5263
  let ry = 0;
5245
- let ptC;
5264
+ let ptC = p0;
5265
+ let r1 = 0, r2 = 0;
5246
5266
 
5247
5267
  if (isRightAngle) {
5248
- // point between cps
5249
5268
 
5250
- // center point
5251
- let cp1_r = rotatePoint(cp1, p0.x, p0.y, (Math.PI * -0.5));
5252
- let cp2_r = rotatePoint(cp2, p.x, p.y, (Math.PI * 0.5));
5269
+ if (isHorizontal1 && isVertical2) {
5270
+ ptC = { x: p0.x, y: p.y };
5271
+ r1 = Math.abs(p.x-p0.x);
5272
+ r2 = Math.abs(p.y-p0.y);
5273
+ }
5274
+ else if (isHorizontal2 && isVertical1) {
5275
+ ptC = { x: p.x, y: p0.y };
5276
+ r2 = Math.abs(p0.x-p.x);
5277
+ r1 = Math.abs(p0.y-p.y);
5278
+ }
5279
+
5280
+ /*
5281
+ if (r1 && r2) {
5253
5282
 
5254
- // assumed centroid
5255
- ptC = checkLineIntersection(p0, cp1_r, p, cp2_r, false);
5283
+ }
5256
5284
 
5257
- let pI = checkLineIntersection(p0, cp1, p, cp2, false);
5285
+ */
5258
5286
 
5259
- if (pI) {
5287
+ if (r1 && r2) {
5260
5288
 
5289
+ /*
5261
5290
  let r1 = getDistance(p0, pI);
5262
5291
  let r2 = getDistance(p, pI);
5292
+ */
5263
5293
 
5264
5294
  let rMax = +Math.max(r1, r2).toFixed(8);
5265
5295
  let rMin = +Math.min(r1, r2).toFixed(8);
@@ -5277,7 +5307,6 @@
5277
5307
  let circular = (100 / rx * Math.abs(rx - ry)) < 5;
5278
5308
 
5279
5309
  if (circular) {
5280
-
5281
5310
  rx = rMax;
5282
5311
  ry = rx;
5283
5312
  }
@@ -5303,7 +5332,7 @@
5303
5332
  arcSegArea = (Math.PI * (rx * ry)) / 4;
5304
5333
 
5305
5334
  // subtract polygon between start, end and center point
5306
- arcSegArea -= Math.abs(getPolygonArea([p0, p, pI]));
5335
+ arcSegArea -= Math.abs(getPolygonArea([p0, p, ptC]));
5307
5336
 
5308
5337
  let areaDiff = getRelativeAreaDiff(comArea, arcSegArea);
5309
5338
 
@@ -6373,59 +6402,50 @@
6373
6402
  pathData[pathData.length - 1].type.toLowerCase() === 'z';
6374
6403
 
6375
6404
  for (let c = 1, l = pathData.length; c < l; c++) {
6376
-
6377
6405
  let com = pathData[c];
6406
+ let { type, values } = com;
6378
6407
  let comN = pathData[c + 1] || pathData[l - 1];
6379
-
6408
+ let valuesN = comN.values;
6380
6409
  let p1 = comN.type.toLowerCase() === 'z' ? M : { x: comN.values[comN.values.length - 2], y: comN.values[comN.values.length - 1] };
6381
6410
 
6382
- let { type, values } = com;
6383
6411
  let valsL = values.slice(-2);
6384
6412
  p = type !== 'Z' ? { x: valsL[0], y: valsL[1] } : M;
6385
6413
 
6414
+ let nextBezier = type!==comN.type && (comN.type==='C' || comN.type==='Q');
6415
+
6386
6416
  let area = p1 ? getPolygonArea([p0, p, p1], true) : Infinity;
6387
6417
  let distSquare = getSquareDistance(p0, p1);
6388
6418
  let distMax = distSquare ? distSquare / 333 * tolerance : 0;
6389
6419
 
6390
6420
  let isFlat = area < distMax;
6391
-
6392
6421
  let isFlatBez = false;
6422
+ let cpts = [];
6393
6423
 
6394
- /*
6395
- // flatness by cross product
6396
- let dx0 = Math.abs(p1.x - p0.x)
6397
- let dy0 = Math.abs(p1.y - p0.y)
6398
-
6399
- let dx1 = Math.abs(p.x - p0.x)
6400
- let dy1 = Math.abs(p.y - p0.y)
6401
-
6402
- let dx2 = Math.abs(p1.x - p.x)
6403
- let dy2 = Math.abs(p1.y - p.y)
6404
-
6405
- // zero length segments are flat
6406
- let isZeroLength = (!dy1 && !dx1) || (!dy2 && !dx2)
6407
- if (isZeroLength) isFlat = true;
6408
-
6409
- // check cross products for colinearity
6410
- if (!isFlat) {
6411
-
6412
- let cross0 = Math.abs(dx0 * dy1 - dy0 * dx1);
6424
+ /**
6425
+ * type change
6426
+ * check flatness
6427
+ */
6428
+ if (nextBezier) {
6429
+ cpts = comN.type === 'C' ?
6430
+ [{ x: valuesN[0], y: valuesN[1] }, { x: valuesN[2], y: valuesN[3] }] :
6431
+ (comN.type === 'Q' ? [{ x: valuesN[0], y: valuesN[1] }] : []);
6413
6432
 
6414
- let thresh = (dx0 + dy0) * 0.1
6433
+ isFlat = commandIsFlat([p0, ...cpts, p], { tolerance });
6415
6434
 
6416
- if ( cross0 < thresh) {
6435
+ /*
6417
6436
 
6418
- isFlat = true
6437
+ if(!isFlatBez){
6438
+ pathDataN.push(com)
6439
+ continue
6419
6440
  }
6420
- }
6421
- */
6441
+ */
6422
6442
 
6423
- if (!flatBezierToLinetos && type === 'C') isFlat = false;
6443
+ }
6424
6444
 
6425
6445
  // convert flat beziers to linetos
6426
6446
  if (flatBezierToLinetos && (type === 'C' || type === 'Q')) {
6427
6447
 
6428
- let cpts = type === 'C' ?
6448
+ cpts = type === 'C' ?
6429
6449
  [{ x: values[0], y: values[1] }, { x: values[2], y: values[3] }] :
6430
6450
  (type === 'Q' ? [{ x: values[0], y: values[1] }] : []);
6431
6451
 
@@ -6439,10 +6459,13 @@
6439
6459
  }
6440
6460
  }
6441
6461
 
6442
- // colinear – exclude arcs (as always =) as semicircles won't have an area
6462
+ /**
6463
+ * colinear = simplification success
6464
+ * exclude arcs (as always =)
6465
+ * as semicircles won't have an area
6466
+ */
6443
6467
 
6444
6468
  if (isFlat && c < l - 1 && comN.type !== 'A' && (type === 'L' || (flatBezierToLinetos && isFlatBez))) {
6445
-
6446
6469
  continue;
6447
6470
  }
6448
6471
 
@@ -6558,7 +6581,8 @@
6558
6581
  let { type, values } = com;
6559
6582
  let valsLen = values.length;
6560
6583
  if (valsLen) {
6561
- let p = { type: type, x: values[valsLen - 2], y: values[valsLen - 1], index: 0 };
6584
+ // we need rounding otherwise sorting may crash due to e notation
6585
+ let p = { type: type, x: +values[valsLen - 2].toFixed(8), y: +values[valsLen - 1].toFixed(8), index: 0 };
6562
6586
  p.index = i;
6563
6587
  indices.push(p);
6564
6588
  }
@@ -6566,7 +6590,7 @@
6566
6590
 
6567
6591
  // reorder to top left most
6568
6592
 
6569
- indices = indices.sort((a, b) => +a.y.toFixed(8) - +b.y.toFixed(8) || a.x - b.x);
6593
+ indices = indices.sort((a, b) => a.y - b.y || a.x - b.x);
6570
6594
  newIndex = indices[0].index;
6571
6595
 
6572
6596
  return newIndex ? shiftSvgStartingPoint(pathData, newIndex) : pathData;
@@ -6925,9 +6949,7 @@
6925
6949
  if (comEx.length === 1) {
6926
6950
 
6927
6951
  comEx = comEx[0];
6928
-
6929
6952
  pathData[i + 1] = null;
6930
-
6931
6953
  pathData[i + 2].values = [comEx.cp1.x, comEx.cp1.y, comEx.cp2.x, comEx.cp2.y, comEx.p.x, comEx.p.y];
6932
6954
  pathData[i + 2].cp1 = comEx.cp1;
6933
6955
  pathData[i + 2].cp2 = comEx.cp2;
@@ -7102,18 +7124,28 @@
7102
7124
  } = {}) {
7103
7125
 
7104
7126
  // is stringified flat point attribute
7105
- if(typeof pts === 'string' && !isNaN(pts[0])){
7127
+ if (typeof pts === 'string' && !isNaN(pts[0])) {
7106
7128
  pts = toPointArray(pts.split(/,| /).filter(Boolean).map(Number));
7107
7129
  return pts
7108
7130
  }
7109
7131
 
7132
+ if (pts.length && typeof pts[0] === 'string') {
7133
+ pts = pts.map(pt => {
7134
+ return toPointArray(pt.split(/,| /).filter(Boolean).map(Number))
7135
+ });
7136
+ pts = pts.flat(2);
7137
+
7138
+ }
7139
+
7110
7140
  if (flatten) pts = pts.flat(2);
7141
+
7111
7142
  let poly = toArray ? polyPtsToArray(pts) : polyArrayToObject(pts);
7112
7143
  return poly
7113
7144
  }
7114
7145
 
7115
- function polyArrayToObject(pts) {
7146
+ function polyArrayToObject(pts = []) {
7116
7147
 
7148
+ if (!pts.length) return [];
7117
7149
  // is point object array
7118
7150
  if (pts[0].x !== undefined && pts[0].y !== undefined) return pts
7119
7151
 
@@ -7131,7 +7163,7 @@
7131
7163
  return poly
7132
7164
  }
7133
7165
 
7134
- else if(pts.length>3){
7166
+ else if (pts.length > 3) {
7135
7167
  pts = toPointArray(pts);
7136
7168
  return pts
7137
7169
  }
@@ -7160,13 +7192,13 @@
7160
7192
  function toPointArray(pts) {
7161
7193
  let ptArr = [];
7162
7194
 
7163
- if(pts[0].length===2){
7164
- for (let i = 0, l = pts.length; i < l; i ++) {
7195
+ if (pts[0].length === 2) {
7196
+ for (let i = 0, l = pts.length; i < l; i++) {
7165
7197
  let pt = pts[i];
7166
- ptArr.push({ x: pt[0], y:pt[1] });
7198
+ ptArr.push({ x: pt[0], y: pt[1] });
7167
7199
  }
7168
7200
 
7169
- }else {
7201
+ } else {
7170
7202
  for (let i = 1, l = pts.length; i < l; i += 2) {
7171
7203
  ptArr.push({ x: pts[i - 1], y: pts[i] });
7172
7204
  }
@@ -8008,7 +8040,6 @@
8008
8040
 
8009
8041
  // LG weight/abscissae generator
8010
8042
  function getLegendreGaussValues(n, x1 = -1, x2 = 1) {
8011
- console.log('add new LG', n);
8012
8043
 
8013
8044
  let waArr = [];
8014
8045
  let z1, z, xm, xl, pp, p3, p2, p1;
@@ -8467,7 +8498,7 @@
8467
8498
  styleProps.remove.push('transform');
8468
8499
 
8469
8500
  // scale props like stroke width or dash-array
8470
- styleProps = scaleProps(styleProps, { props: ['stroke-width', 'stroke-dasharray'], scale: scaleX });
8501
+ styleProps = scaleProps(styleProps, { props: ['stroke-width', 'stroke-dasharray', 'stroke-dashoffset'], scale: scaleX });
8471
8502
 
8472
8503
  } else {
8473
8504
  el.setAttribute('transform', transComponents.matrixAtt);
@@ -8485,7 +8516,7 @@
8485
8516
  let prop = props[i];
8486
8517
 
8487
8518
  if (styleProps[prop] !== undefined) {
8488
- styleProps[prop] = styleProps[prop].map(val => round ? roundTo(val * scale, 2) : val * scale);
8519
+ styleProps[prop] = styleProps[prop].map(val => round ? roundTo(val * scale, 3) : val * scale);
8489
8520
  }
8490
8521
  }
8491
8522
  return styleProps
@@ -8506,6 +8537,7 @@
8506
8537
  });
8507
8538
 
8508
8539
  let scale = elLength / pathLength;
8540
+
8509
8541
  styleProps = scaleProps(styleProps, { props: ['stroke-dasharray', 'stroke-dashoffset'], scale });
8510
8542
 
8511
8543
  // set absolute
@@ -9013,6 +9045,38 @@
9013
9045
  });
9014
9046
  });
9015
9047
 
9048
+ /**
9049
+ * remove els and attributes
9050
+ */
9051
+
9052
+ // remove meta
9053
+ if (!allowMeta) removeElements.push('meta', 'metadata', 'desc', 'title');
9054
+
9055
+ if (removeClassNames) {
9056
+ removeSVGAttributes.push('class');
9057
+ removeElAttributes.push('class');
9058
+ }
9059
+
9060
+ if (removeIds) {
9061
+ removeSVGAttributes.push('id');
9062
+ removeElAttributes.push('id');
9063
+ }
9064
+
9065
+ // remove hidden elements
9066
+ removeHiddenSvgEls(svg);
9067
+
9068
+ // remove SVG elements
9069
+ removeSvgEls(svg, { removeElements, removeNameSpaced });
9070
+
9071
+ // remove SVG attributes
9072
+ removeSvgAtts(svg, removeSVGAttributes);
9073
+
9074
+ // remove SVG child element attributes
9075
+ removeSvgChildAtts(svg, removeElAttributes);
9076
+
9077
+ // general cleanup
9078
+ if (cleanupSVGAtts) cleanupSVGAttributes(svg, { removeIds, removeClassNames, removeDimensions, stylesToAttributes, allowMeta, allowAriaAtts, allowDataAtts });
9079
+
9016
9080
  // collect all elements' properties
9017
9081
  let svgElProps = [];
9018
9082
  let els = svg.querySelectorAll(`${renderedEls.join(', ')}`);
@@ -9033,7 +9097,12 @@
9033
9097
  let styleProps = parseStylesProperties(el, propOptions);
9034
9098
  let stylePropsFiltered = {};
9035
9099
 
9100
+ // reset remove array
9101
+ remove = [];
9102
+
9036
9103
  // convert pathLength before transforming
9104
+ if (convertTransforms || attributesToGroup) convertPathLength = true;
9105
+
9037
9106
  if (convertPathLength) {
9038
9107
  styleProps = convertPathLengthAtt(el, { styleProps });
9039
9108
  remove = [...new Set([...remove, ...styleProps.remove])];
@@ -9086,41 +9155,11 @@
9086
9155
 
9087
9156
  remove = [...new Set([...remove, ...styleProps.remove])];
9088
9157
 
9089
- /**
9090
- * remove els and attributes
9091
- */
9092
-
9093
- // remove meta
9094
- if (!allowMeta) removeElements.push('meta', 'metadata', 'desc', 'title');
9158
+
9095
9159
 
9096
- if (removeClassNames) {
9097
- removeSVGAttributes.push('class');
9098
- removeElAttributes.push('class');
9099
- }
9100
-
9101
- if (removeIds) {
9102
- removeSVGAttributes.push('id');
9103
- removeElAttributes.push('id');
9104
- }
9105
-
9106
- // remove hidden elements
9107
- removeHiddenSvgEls(svg);
9108
-
9109
- // remove SVG elements
9110
- removeSvgEls(svg, { removeElements, removeNameSpaced });
9111
-
9112
- // remove SVG attributes
9113
- removeSvgAtts(svg, removeSVGAttributes);
9114
-
9115
- // remove SVG child element attributes
9116
- removeSvgChildAtts(svg, removeElAttributes);
9117
-
9118
- // general cleanup
9119
- if (cleanupSVGAtts) cleanupSVGAttributes(svg, { removeIds, removeClassNames, removeDimensions, stylesToAttributes, allowMeta, allowAriaAtts, allowDataAtts });
9120
-
9121
- // all relative units to absolute
9122
- if (toAbsoluteUnits) {
9123
- normalizeTransforms = true;
9160
+ // all relative units to absolute
9161
+ if (toAbsoluteUnits) {
9162
+ normalizeTransforms = true;
9124
9163
 
9125
9164
  /**
9126
9165
  * apply consolidated
@@ -9161,7 +9200,6 @@
9161
9200
  styleProps = setNormalizedTransformsToEl(el, { styleProps });
9162
9201
 
9163
9202
  remove = [...new Set([...remove, ...styleProps.remove])];
9164
-
9165
9203
  }
9166
9204
 
9167
9205
  /**
@@ -9186,13 +9224,6 @@
9186
9224
  */
9187
9225
  removeAtts(el, remove);
9188
9226
 
9189
- /*
9190
- for (let i = 0; i < remove.length; i++) {
9191
- let att = remove[i];
9192
- el.removeAttribute(att)
9193
- }
9194
- */
9195
-
9196
9227
  } // endof style processing
9197
9228
 
9198
9229
  /**
@@ -9214,7 +9245,7 @@
9214
9245
 
9215
9246
  // scale props like stroke width or dash-array before conversion
9216
9247
  if (matrix && transComponents) {
9217
- ['stroke-width', 'stroke-dasharray'].forEach(att => {
9248
+ ['stroke-width', 'stroke-dasharray', 'stroke-dashoffset'].forEach(att => {
9218
9249
  let attVal = el.getAttribute(att);
9219
9250
  let vals = attVal ? attVal.split(' ').filter(Boolean).map(Number).map(val => val * transComponents.scaleX) : [];
9220
9251
  if (vals.length) el.setAttribute(att, vals.join(' '));
@@ -10483,71 +10514,6 @@
10483
10514
  return pathData
10484
10515
  }
10485
10516
 
10486
- function fixIntersectingCpts(pathData = []) {
10487
-
10488
- let l = pathData.length;
10489
- let p0 = { x: pathData[0].values[0], y: pathData[0].values[1] };
10490
- let p = p0;
10491
-
10492
- for (let i = 1; i < l; i++) {
10493
- let com = pathData[i];
10494
- let comPrev = pathData[i - 1] || null;
10495
- let { type, values } = com;
10496
- pathData[i + 1] ? pathData[i + 1] : null;
10497
-
10498
- if (type === 'C') {
10499
- let tx = 1.2;
10500
- let cp1 = { x: values[0], y: values[1] };
10501
- p = { x: values[4], y: values[5] };
10502
- let cp2 = { x: values[2], y: values[3] };
10503
-
10504
- interpolate(p0, cp1, tx );
10505
- let cp2_ex = interpolate( p, cp2, tx);
10506
-
10507
- comPrev.values.slice(-2);
10508
-
10509
- // extend tangents
10510
-
10511
- let ptI = checkLineIntersection(p0, cp1, p, cp2_ex, true, true);
10512
-
10513
-
10514
- /*
10515
- renderPoly(markers, [p0, cp1], 'orange', '0.75')
10516
- renderPoly(markers, [p, cp2], 'blue', '0.75')
10517
- renderPoly(markers, [p, cp2_ex], 'magenta', '0.75')
10518
-
10519
- renderPoint(markers, p0, 'orange', '0.75')
10520
- renderPoint(markers, cp1, 'magenta', '0.75')
10521
- renderPoint(markers, cp1_ex, 'blue', '0.5')
10522
-
10523
- renderPoint(markers, cp2, 'cyan', '0.75')
10524
- */
10525
-
10526
-
10527
-
10528
- /*
10529
- renderPoint(markers, p0, 'orange')
10530
- renderPoint(markers, p, 'magenta', '0.5')
10531
- */
10532
-
10533
- if (ptI) {
10534
- let t = 0.666;
10535
- cp1 = interpolate(p0, ptI, t);
10536
- cp2 = interpolate(p, ptI, t);
10537
- com.values = [cp1.x, cp1.y, cp2.x, cp2.y, p.x, p.y];
10538
-
10539
- }
10540
-
10541
- }
10542
-
10543
- if (values.length) {
10544
- p0 = p;
10545
- }
10546
- }
10547
-
10548
- return pathData;
10549
- }
10550
-
10551
10517
  function simplifyPolyRDP(pts, {quality = 0.9, width = 0, height = 0}={}) {
10552
10518
 
10553
10519
  /**
@@ -10742,860 +10708,817 @@
10742
10708
 
10743
10709
  }
10744
10710
 
10745
- function getPolyCentroid(pts) {
10711
+ function refineAdjacentPolyExtremes(pts = []) {
10746
10712
 
10747
10713
  let l = pts.length;
10748
- let x = 0, y = 0;
10749
- for (let i = 0; l && i < l; i++) {
10750
- let pt = pts[i];
10751
- x += pt.x;
10752
- y += pt.y;
10753
- }
10754
-
10755
- let centroid = {x: x/l, y:y/l};
10756
- return centroid
10757
-
10758
- }
10759
-
10760
- function detectRegularPolygon(pts, centroid={x:0, y:0}) {
10761
- let rSq = getSquareDistance(pts[0], centroid);
10762
- let isRegular = true;
10763
-
10764
- for (let i = 1, l = pts.length; i < l; i++) {
10765
- let pt1 = pts[i];
10766
- let dist = getSquareDistance(pt1, centroid);
10767
10714
 
10768
- let diff = Math.abs(rSq-dist);
10769
- let diffRel = diff/rSq;
10770
-
10771
- if (diffRel > 0.05) {
10772
- return false;
10773
- }
10715
+ let { x, y, width, height, top, bottom, left, right } = getPolyBBox(pts);
10716
+ let threshShort = (width + height) * 0.05;
10717
+ let thresh = (width + height) * 0.001;
10774
10718
 
10775
- }
10776
- return isRegular;
10777
- }
10719
+ let pt0 = pts[0];
10720
+ let ptLast = pts[l - 1];
10778
10721
 
10779
- function analyzePoly(pts, {
10780
- x = 0,
10781
- y = 0,
10782
- width = 0,
10783
- height = 0,
10784
- debug = false
10785
- } = {}) {
10722
+ /**
10723
+ * cleanup close path - almost vertical or horizontal
10724
+ * average start and end extremes
10725
+ */
10726
+ let dx = Math.abs(ptLast.x - pt0.x);
10727
+ let dy = Math.abs(ptLast.y - pt0.y);
10786
10728
 
10787
- let l = pts.length;
10729
+ if (dy < threshShort || dx < threshShort) {
10788
10730
 
10789
- // bounding box of this sub poly
10790
- let bb0 = getPolyBBox(pts);
10731
+ if (pt0.isExtreme && !pt0.isCorner) {
10791
10732
 
10792
- if (!width || !height) {
10793
- ({ x, y, width, height } = bb0);
10794
- }
10733
+ let xAv = (pt0.x + ptLast.x) * 0.5;
10734
+ let yAv = (pt0.y + ptLast.y) * 0.5;
10795
10735
 
10796
- let thresh = (width + height) * 0.01;
10736
+ pt0.x = xAv;
10737
+ pt0.y = yAv;
10797
10738
 
10798
- // get areas an distances
10799
- for (let i = 0; i < l; i++) {
10800
- let p0 = i > 0 ? pts[i - 1] : pts[l - 1];
10801
- let p1 = pts[i];
10802
- let p2 = i < l - 1 ? pts[i + 1] : pts[l - 1];
10739
+ ptLast.x = xAv;
10740
+ ptLast.y = yAv;
10741
+ ptLast.isExtreme = true;
10803
10742
 
10804
- let area = getPolygonArea([p0, p1, p2], false);
10805
- p1.area = area;
10806
- p1.dist = getDistManhattan(p0, p1);
10807
- p1.idx = i;
10743
+ if (dy < thresh) {
10744
+ ptLast.tangentR.y = pt0.y;
10745
+ ptLast.tangentL.y = pt0.y;
10808
10746
 
10747
+ }
10748
+ if (dx < thresh) {
10749
+ ptLast.tangentR.x = pt0.x;
10750
+ ptLast.tangentL.x = pt0.x;
10751
+ }
10752
+ }
10809
10753
  }
10810
10754
 
10811
- for (let i = 0; i < l; i++) {
10812
- i > 1 ? pts[i - 2] : pts[l - 1];
10813
- let p0 = i > 0 ? pts[i - 1] : pts[l - 1];
10755
+ for (let i = 1; i < l; i++) {
10756
+ i > 0 ? pts[i - 1] : pts[l - 1];
10814
10757
  let p1 = pts[i];
10815
10758
  let p2 = i < l - 1 ? pts[i + 1] : pts[l - 1];
10759
+ let dist = getDistManhattan(p1, p2);
10816
10760
 
10817
- getSquareDistance(p0, p2) * 0.01;
10818
-
10819
- Math.abs(p0.area);
10820
- let area1 = Math.abs(p1.area);
10821
- Math.abs(p2.area);
10822
- let isCorner = false;
10823
-
10824
- let flat = !p1.area || area1 < thresh;
10825
-
10826
- getDistManhattan(p1, p0);
10827
-
10828
- /**
10829
- * check extremes
10830
- */
10831
-
10832
- let isExtreme = false;
10761
+ let { isHorizontal, isVertical, isCorner, isLong, isExtreme, isSemiExtreme, isDirChange } = p1;
10833
10762
 
10834
- // 1. total extreme
10835
- if ((p1.x === bb0.left || p1.x === bb0.right || p1.y === bb0.top || p1.y === bb0.bottom)) {
10836
- isExtreme = true;
10837
- }
10763
+ let extremes = [];
10838
10764
 
10839
- // 1.2 horizontal or vertical
10840
- let isHorizontal = p1.y === p0.y && p1.x !== p0.x;
10841
- let isVertical = p1.x === p0.x && p1.y !== p0.y;
10765
+ /*
10766
+ if(isExtreme && p0.isCorner && !isLong && !isCorner){
10767
+ isExtreme= false
10768
+ p1.isExtreme = false
10769
+ p1.isHorizontal = false
10770
+ p1.isVertical = false
10842
10771
 
10843
- if ((isHorizontal || isVertical)) {
10844
- p0.isExtreme = true;
10845
- isExtreme = true;
10772
+ continue;
10846
10773
  }
10774
+ */
10847
10775
 
10848
- // 1.3 is local or absolute extreme
10849
- let bb = getPolyBBox([p0, p2]); // local bb
10850
- let { left, right, top, bottom } = bb;
10851
-
10852
- let extremeLocal = (p1.x < left || p1.x > right || p1.y < top || p1.y > bottom);
10853
- if (!isExtreme && extremeLocal) {
10854
- isExtreme = true;
10776
+ /*
10777
+ if(isExtreme && p2.isCorner && !isLong && !isCorner && dist<threshShort*0.5){
10778
+ isExtreme= false
10779
+ p1.isExtreme = false
10780
+ p1.isHorizontal = false
10781
+ p1.isVertical = false
10855
10782
 
10783
+ if(isVertical){
10784
+ p2.tangentL.x = p2.x
10785
+ }
10786
+ if(isHorizontal){
10787
+ p2.tangentL.y = p2.y
10788
+ }
10789
+ continue;
10856
10790
  }
10791
+ */
10857
10792
 
10858
- /**
10859
- * 2. sign changes
10860
- */
10861
- let signChange = (p0.area < 0 && p1.area > 0) || (p0.area > 0 && p1.area < 0);
10862
- let isDirChange = signChange && !flat && !p0.isDirChange;
10863
-
10864
- /**
10865
- * 3. corners
10866
- */
10867
-
10868
- if (isExtreme) {
10869
-
10870
- let delta = getDeltaAngle(p1, p2, p0);
10871
- let { deltaAngleDeg } = delta;
10872
- deltaAngleDeg = Math.abs(deltaAngleDeg);
10793
+ if (isExtreme && !isCorner && p2.isExtreme) {
10794
+ extremes.push(p1);
10873
10795
 
10874
- let isCornerDelta = deltaAngleDeg > 10 && deltaAngleDeg < 160;
10875
- if (isCornerDelta) {
10796
+ for (let j = i + 1; j < l; j++) {
10797
+ let p2 = pts[j];
10798
+ dist = getDistManhattan(p1, p2);
10876
10799
 
10877
- isCorner = true;
10800
+ if (dist * 0.75 >= threshShort || p2.isCorner || p2.isDirChange) {
10801
+ break
10802
+ }
10803
+ if (p2.isExtreme && !p2.isDirChange && !p2.isCorner) {
10804
+ extremes.push(p2);
10805
+ }
10878
10806
  }
10879
10807
 
10880
- }
10808
+ if (extremes.length > 1) {
10881
10809
 
10882
- /*
10883
- let debug = true
10884
- if (debug) {
10810
+ // find best extreme according to angle
10811
+ let angleDiffMin = Infinity;
10885
10812
 
10886
- if ((isHorizontal || isVertical)) {
10887
- renderPoint(markers, p1, 'blue', '2%', '0.5')
10888
- renderPoint(markers, p0, 'blue', '2%', '0.5')
10889
- }
10813
+ let bestMatch = extremes[0];
10890
10814
 
10891
- if (isExtreme) {
10892
- renderPoint(markers, p1, 'cyan', '1.5%', '0.5')
10893
- }
10815
+
10816
+ extremes.forEach(pt => {
10894
10817
 
10895
- if (isDirChange) {
10896
- renderPoint(markers, p1, 'orange', '0.75%', '0.5')
10897
- }
10818
+ let angle = Math.abs(getAngleFromDelta(pt.dx2, pt.dy2, false)) * rad2Deg;
10819
+ let angleDiff = angle > 160 ? Math.abs(180 - angle) : (angle > 60 ? Math.abs(90 - angle) : angle);
10820
+ pt.angle = angle;
10821
+ pt.angleDiff = angleDiff;
10898
10822
 
10899
- if (isCorner) {
10900
- renderPoint(markers, p1, 'magenta', '2.75%', '0.5')
10901
- }
10902
- }
10903
- */
10823
+ if (angleDiff < angleDiffMin) {
10824
+ bestMatch = pt;
10825
+ angleDiffMin = angleDiff;
10904
10826
 
10905
- p1.isCorner = isCorner;
10906
- p1.isExtreme = isExtreme;
10907
- p1.isHorizontal = isHorizontal;
10908
- p1.isVertical = isVertical;
10909
- p1.isDirChange = isDirChange;
10827
+ }
10828
+ });
10910
10829
 
10911
- }
10830
+ let extremes2 = [];
10912
10831
 
10913
- // filter adjacent extremes
10914
- let pts1 = [];
10915
- let exclude = [];
10832
+ extremes.forEach((pt, i) => {
10916
10833
 
10917
- {
10918
- for (let i = 0; i < pts.length; i++) {
10919
- let p = pts[i];
10920
- let p1 = pts[i + 1] || null;
10921
- let p2 = pts[i + 2] || null;
10834
+ let isBestMatch = pt === bestMatch;
10922
10835
 
10923
- let extremes = [];
10836
+ if (isBestMatch) {
10924
10837
 
10925
- if (p1 && p1.isExtreme && p.isExtreme && !p.isCorner) {
10926
- let has2nd = p1.dist < thresh * 2 && !p1.isCorner;
10927
- let has3rd = p2 && p2.isExtreme && p2.dist < thresh * 2 && !p2.isCorner;
10838
+ if (pt.isHorizontal) {
10839
+ pt.tangentL.y = pt.y;
10840
+ pt.tangentR.y = pt.y;
10841
+ }
10842
+ if (pt.isVertical) {
10843
+ pt.tangentL.x = pt.x;
10844
+ pt.tangentR.x = pt.x;
10845
+ }
10928
10846
 
10929
- if (has2nd && !has3rd) {
10930
- extremes.push(p, p1);
10847
+ // renderPoint(markers, pt, 'green', '3%', '0.5')
10931
10848
 
10932
- } else if (has3rd) {
10933
- extremes.push(p, p1, p2);
10934
- /*
10935
- renderPoint(markers, p, 'green', '1%', '0.5')
10936
- renderPoint(markers, p1, 'red', '1%', '0.5')
10937
- renderPoint(markers, p2, 'blue', '1%', '0.5')
10938
- */
10939
- }
10849
+ }
10850
+ else {
10940
10851
 
10941
- if (extremes.length) {
10942
- // average extreme
10852
+ if (bestMatch) {
10943
10853
 
10944
- let x = extremes.reduce((a, b) => a + b.x, 0) / extremes.length;
10945
- let y = extremes.reduce((a, b) => a + b.y, 0) / extremes.length;
10854
+ if (!isBestMatch && (pt.x === bestMatch.x || pt.y === bestMatch.y)) {
10855
+ extremes2.push(pt);
10856
+ }
10857
+ pt.isExtreme = false;
10858
+ pt.isHorizontal = false;
10859
+ pt.isVertical = false;
10860
+ }
10946
10861
 
10947
- p.x = x;
10948
- p.y = y;
10862
+ }
10949
10863
 
10950
- i += extremes.length - 1;
10951
- }
10952
- }
10864
+ });
10953
10865
 
10954
- if (p.isExtreme || p.isCorner || p.isDirChange) {
10955
- exclude.push(pts1.length);
10956
- }
10866
+ // average coordinates
10867
+ if (extremes2.length) {
10868
+ bestMatch.x = (extremes2[0].x + bestMatch.x) * 0.5;
10869
+ bestMatch.y = (extremes2[0].y + bestMatch.y) * 0.5;
10957
10870
 
10958
- pts1.push(p);
10871
+ }
10959
10872
 
10960
- // remove last nearby extreme
10961
- let l2 = pts1.length;
10962
- let p0 = pts1[0];
10963
- let pL = pts1[l2 - 1];
10964
- let near0 = getDistManhattan(p0, pL) < thresh * 2;
10965
- if (p0.isExtreme && pL.isExtreme && near0) {
10966
- pL.x = p0.x;
10967
- pL.y = p0.y;
10873
+ i += extremes.length;
10874
+ continue;
10968
10875
  }
10969
10876
  }
10970
10877
 
10971
- pts = pts1;
10972
10878
  }
10973
10879
 
10974
- return pts
10975
10880
  }
10976
10881
 
10977
- function getPolyChunks(pts,
10978
- { closed = true,
10979
- keepCorners = true,
10980
- keepExtremes = true,
10981
- keepInflections = false
10982
- } = {}
10983
- ) {
10984
- let chunks = [[pts[0]]];
10985
-
10986
- let idx = 0;
10987
- let lastChunk = chunks[idx];
10882
+ function cleanupPolyKeypoints(pts = []) {
10988
10883
 
10989
10884
  let l = pts.length;
10990
10885
 
10991
- // render
10886
+ getPolyBBox(pts);
10887
+
10888
+ pts[0];
10889
+
10992
10890
  for (let i = 1; i < l; i++) {
10993
- i > 0 ? pts[i] : pts[l - 1];
10891
+ i > 0 ? pts[i - 1] : pts[l - 1];
10994
10892
  let p1 = pts[i];
10995
10893
  i < l - 1 ? pts[i + 1] : pts[l - 1];
10996
10894
 
10997
- // start new chunk
10998
- // keepInflections && p1.isDirChange
10999
- if ((keepExtremes && p1.isExtreme || keepCorners && p1.isCorner )) {
11000
- idx++;
11001
- chunks.push([]);
11002
- }
10895
+ let { isHorizontal, isVertical, isCorner, isLong, isExtreme, isSemiExtreme, isDirChange } = p1;
10896
+ let offset = 0;
11003
10897
 
11004
- lastChunk = chunks[idx];
11005
- lastChunk.push(p1);
11006
- }
10898
+ if (!isSemiExtreme){
11007
10899
 
11008
- // test render
10900
+ continue
10901
+ }
11009
10902
 
11010
- return chunks;
11011
- }
10903
+ if (isSemiExtreme || isExtreme) {
10904
+ let semiExtremes = isSemiExtreme ? [p1] : [];
11012
10905
 
11013
- /**
11014
- * Fit one or more Bezier curves to a set of pts.
11015
- *
11016
- */
11017
- function fitCurveSchneider(pts, {
11018
- maxError = 0,
11019
- adjustCpts = true,
11020
- harmonize = true,
11021
- keepCorners = true
11022
- } = {}) {
10906
+ for (let j = i + 1; j < l; j++) {
10907
+ let p2 = pts[j];
11023
10908
 
11024
- if (pts.length === 1) {
11025
- return [];
11026
- }
10909
+ if (!p2.isSemiExtreme || p2.isExtreme || p2.isCorner){
10910
+ break
10911
+ }
10912
+ semiExtremes.push(p2);
10913
+ }
11027
10914
 
11028
- // single lineto
11029
- if (pts.length === 2) {
11030
- return [
11031
- { type: 'L', values: [pts[0].x, pts[0].y] },
11032
- { type: 'L', values: [pts[1].x, pts[1].y] }
11033
- ]
11034
- }
10915
+ if (semiExtremes.length > 1) {
11035
10916
 
11036
- // prevent bulging
10917
+ let semiExtremeMid = semiExtremes[Math.floor(semiExtremes.length*0.5)];
10918
+ let p1_1 = semiExtremes[0];
10919
+ let p2_1 = semiExtremes[semiExtremes.length - 1];
10920
+ let ptI = checkLineIntersection(p1_1, p1_1.tangentR, p2_1, p2_1.tangentL, false, true);
11037
10921
 
11038
- let len = pts.length;
11039
- let leftTangent = createTangent(pts[1], pts[0]);
11040
- let rightTangent = createTangent(pts[len - 2], pts[len - 1]);
11041
- let beziers = fitCubic(pts, leftTangent, rightTangent, maxError, keepCorners);
10922
+ semiExtremes.forEach(pt=>{
10923
+ pt.isSemiExtreme=false;
10924
+ });
10925
+ semiExtremeMid.isSemiExtreme=true;
11042
10926
 
11043
- // create pathdata
11044
- let pathData = bezierPtsToPathData(beziers);
11045
- let cp1, cp2;
10927
+ // interpolate mid point
10928
+ if (ptI) {
10929
+ let pI_1 = interpolate(p1_1, ptI, 0.5);
10930
+ let pI_2 = interpolate(p2_1, ptI, 0.5);
10931
+ let pI_3 = interpolate(pI_2, pI_1, 0.5);
11046
10932
 
11047
- adjustCpts = false;
10933
+ semiExtremeMid.x = pI_3.x;
10934
+ semiExtremeMid.y = pI_3.y;
10935
+ semiExtremeMid.tangentL = pI_1;
10936
+ semiExtremeMid.tangentR = pI_2;
11048
10937
 
11049
- adjustCpts = true;
10938
+ i += offset;
10939
+ continue
10940
+ }
11050
10941
 
11051
- if (adjustCpts) {
10942
+ }
10943
+ }
10944
+ // find significant of same type
11052
10945
 
11053
- let len2 = pathData.length;
11054
- let com1 = pathData[0];
10946
+ }
11055
10947
 
11056
- // last cubic segment
11057
- let com2 = pathData[len2 - 1];
10948
+ /*
10949
+ // update index
10950
+ ptsClean.forEach((pt, i) => {
10951
+ pt.idx = i
10952
+ })
10953
+ */
11058
10954
 
11059
- let p0 = { x: pts[0].x, y: pts[0].y };
11060
- let p1 = { x: pts[1].x, y: pts[1].y };
11061
- let p2 = pts[2] ? { x: pts[2].x, y: pts[2].y } : null;
10955
+ return pts;
11062
10956
 
11063
- if (p2) {
11064
- cp1 = { x: com1.values[0], y: com1.values[1] };
11065
- cp1 = adjustTangentAngle(cp1, p0, p1, p2);
11066
- com1.values[0] = cp1.x;
11067
- com1.values[1] = cp1.y;
11068
- }
10957
+ }
11069
10958
 
11070
- let pL = { x: pts[len - 1].x, y: pts[len - 1].y };
11071
- let pL1 = { x: pts[len - 2].x, y: pts[len - 2].y };
11072
- let pL2 = pts[len - 3] ? { x: pts[len - 3][0], y: pts[len - 3][1] } : null;
10959
+ function adjustTangentAngle(cp, p0, p1, p2) {
10960
+ let ang1 = getAngle(p0, p1);
10961
+ let ang2 = getAngle(p0, p2);
10962
+ let angDiff = (ang2 - ang1);
11073
10963
 
11074
- if (pL2) {
11075
- cp2 = { x: com2.values[2], y: com2.values[3] };
11076
- cp2 = adjustTangentAngle(cp2, pL, pL1, pL2);
11077
- com2.values[2] = cp2.x;
11078
- com2.values[3] = cp2.y;
11079
- }
10964
+ let f = 0.666;
10965
+ f = 1;
11080
10966
 
11081
- /*
11082
- // harmonize too tight tangents
11083
- let harmonize = true;
11084
- harmonize = false;
11085
- if (harmonize) {
11086
- pathData = harmonizeCubicCptsThird([{ type: 'M', values: [pts[0].x, pts[0].y] },
11087
- ...pathData])
11088
- pathData.shift()
11089
- }
11090
- */
10967
+ cp = rotatePoint(cp, p0.x, p0.y, -angDiff * f);
10968
+ return cp
10969
+ }
11091
10970
 
11092
- }
10971
+ function getTangents(pts = [], {
10972
+ x = 0,
10973
+ y = 0,
10974
+ width = 0,
10975
+ height = 0,
10976
+ debug = false,
10977
+ closed=false,
10978
+ } = {}) {
11093
10979
 
11094
- return pathData
11095
- }
10980
+ let l = pts.length;
11096
10981
 
11097
- /**
11098
- * Fit a Bezier curve to a (sub)set of digitized pts.
11099
- * Your code should not call this function directly. Use fitCurve instead.
11100
- *control-point-1, control-point-2, second-point] and pts are [x, y]
11101
- */
11102
- function fitCubic(pts, leftTangent, rightTangent, error, keepCorners = false) {
10982
+ // bounding box of this sub poly
10983
+ if (!width || !height) {
10984
+ ({ x, y, width, height } = getPolyBBox(pts));
10985
+ }
11103
10986
 
11104
- let MaxIterations = 20;
11105
- let bezCurve;
10987
+ // threshold for horizontal or vertical detection
11106
10988
 
11107
- /*
10989
+ for (let i = 0; i < l; i++) {
10990
+ let p0 = i > 0 ? pts[i - 1] : pts[l - 1];
10991
+ let p1 = pts[i];
10992
+ let p2 = i < l - 1 ? pts[i + 1] : pts[l - 1];
10993
+ let p3 = i < l - 1 ? pts[i + 2] : pts[l - 1];
11108
10994
 
11109
- if (pts.length === 2) {
11110
- let dist = getDistance(pts[0], pts[1], false) * 0.333;
11111
- bezCurve = [pts[0], addArrays(pts[0], mulItems(leftTangent, dist)), addArrays(pts[1], mulItems(rightTangent, dist)), pts[1]];
11112
- return [bezCurve];
11113
- }
11114
- */
10995
+ let { isHorizontal, isVertical, isCorner, isLong, isExtreme, isSemiExtreme, isDirChange } = p1;
11115
10996
 
11116
- let u = chordLengthParameterize(pts);
11117
- let _generateAndReport = generateAndReport(pts, u, u, leftTangent, rightTangent);
10997
+ // default
10998
+ let tangentL = { x: p1.x - p1.dx2 * 0.5, y: p1.y - p1.dy2 * 0.5 };
10999
+ let tangentR = { x: p1.x + p1.dx2 * 0.5, y: p1.y + p1.dy2 * 0.5 };
11118
11000
 
11119
- bezCurve = _generateAndReport[0];
11001
+ // average first tangent
11002
+ if(i===0){
11003
+ tangentR = adjustTangentAngle(p2, p1, p2, p3);
11004
+ }
11120
11005
 
11121
- let maxError = _generateAndReport[1];
11122
- let splitPoint = _generateAndReport[2];
11006
+ /**
11007
+ * add left and right tangents
11008
+ * for later curve fitting
11009
+ */
11123
11010
 
11124
- // check if curve is bulged
11125
- if (keepCorners) {
11126
- let checkBulge = areaDeviationTooLarge(pts, bezCurve);
11127
- let { isBulged, bezierNew } = checkBulge;
11128
- if (isBulged) {
11129
- // return cubic corner segment
11130
- bezCurve = bezierNew;
11011
+ if (isHorizontal && !isCorner) {
11012
+ tangentL = { x: p1.x - p1.dx2*0.5, y: p1.y };
11013
+ tangentR = { x: p1.x + p1.dx2*0.5, y: p1.y };
11014
+ }
11015
+ else if (isVertical) {
11016
+ tangentL = { x: p1.x , y: p1.y - p1.dy2*0.5 };
11017
+ tangentR = { x: p1.x , y: p1.y + p1.dy2*0.5 };
11131
11018
  }
11132
- }
11133
11019
 
11134
- if (maxError === 0 || maxError < error) {
11135
- return [bezCurve];
11136
- }
11020
+ if (!isExtreme && p1.isLong) {
11021
+ tangentL = { x: p1.x - p1.dx*0.5, y: p1.y - p1.dy*0.5 };
11022
+ tangentR = { x: p1.x + p1.dx*0.5, y: p1.y + p1.dy*0.5 };
11023
+ }
11137
11024
 
11138
- if (maxError < error * error) {
11025
+ /*
11139
11026
 
11140
- let uPrime = u;
11141
- let prevErr = maxError;
11142
- let prevSplit = splitPoint;
11027
+ if (isDirChange && !isCorner && !isExtreme) {
11028
+ p1.tangentL = { x: p1.x-p1.dx2*0.5, y: p1.y-p1.dy2*0.5 }
11029
+ p1.tangentR = { x: p1.x+p1.dx2*0.5, y: p1.y+p1.dy2*0.5 }
11030
+ }
11031
+ */
11143
11032
 
11144
- for (let i = 0; i < MaxIterations; i++) {
11033
+ if (isCorner) {
11145
11034
 
11146
- uPrime = reparameterize(bezCurve, pts, uPrime);
11035
+ tangentL = {x:p0.x, y:p0.y};
11036
+ tangentR = {x:p2.x, y:p2.y};
11147
11037
 
11148
- let _generateAndReport2 = generateAndReport(pts, u, uPrime, leftTangent, rightTangent);
11038
+ let p0_1 = pts[i - 2] ? pts[i - 2] : pts[l - 1];
11149
11039
 
11150
- bezCurve = _generateAndReport2[0];
11151
- maxError = _generateAndReport2[1];
11152
- splitPoint = _generateAndReport2[2];
11040
+ let p2_1 = pts[i + 2] ? pts[i + 2] : pts[1];
11153
11041
 
11154
- if (maxError < error) {
11155
- return [bezCurve];
11042
+ // adjust angle
11043
+ if (!p0.isCorner) {
11044
+ tangentL = adjustTangentAngle(p0, p1, p0, p0_1);
11156
11045
  }
11157
11046
 
11158
- else if (splitPoint === prevSplit) {
11159
- let errChange = maxError / prevErr;
11160
- if (errChange > .9999 && errChange < 1.0001) {
11161
- break;
11162
- }
11047
+ if (!p2.isCorner) {
11048
+ tangentR = adjustTangentAngle(tangentR, p1, p2, p2_1);
11163
11049
  }
11164
11050
 
11165
- prevErr = maxError;
11166
- prevSplit = splitPoint;
11051
+ /*
11052
+ renderPoint(markers, p0, 'darkblue', '0.75%', '0.5')
11053
+ // renderPoint(markers, p0_1, 'blue', '0.5%')
11054
+ renderPoint(markers, tangentL, 'blue', '0.5%', '0.5')
11055
+ renderPoint(markers, tangentR, 'blue', '0.5%', '0.5')
11056
+ */
11057
+
11167
11058
  }
11168
- }
11169
11059
 
11170
- let beziers = [];
11171
- let centerVector = subtract(pts[splitPoint - 1], pts[splitPoint + 1]);
11060
+ p1.tangentL = tangentL;
11061
+ p1.tangentR = tangentR;
11062
+
11063
+ /*
11064
+ debug = true
11065
+ if(debug){
11066
+ if (isCorner || isSemiExtreme || isDirChange || isExtreme) {
11067
+ renderPoint(markers, p1.tangentL, 'darkred', '0.5%')
11068
+ renderPoint(markers, p1.tangentR, 'darkblue', '0.5%')
11069
+ }
11070
+ }
11071
+ */
11172
11072
 
11173
- if (centerVector.x === 0 && centerVector.y === 0) {
11174
- centerVector = subtract(pts[splitPoint - 1], pts[splitPoint]);
11175
- let _ref = { x: -centerVector.y, y: centerVector.x };
11176
- centerVector.x = _ref.x;
11177
- centerVector.y = _ref.y;
11178
11073
  }
11179
11074
 
11180
- let toCenterTangent = normalize(centerVector);
11075
+ }
11181
11076
 
11182
- let fromCenterTangent = mulItems(toCenterTangent, -1);
11077
+ function getPolyCentroid(pts) {
11183
11078
 
11184
- if (pts.length === 3) ;
11079
+ let l = pts.length;
11080
+ let x = 0, y = 0;
11081
+ for (let i = 0; l && i < l; i++) {
11082
+ let pt = pts[i];
11083
+ x += pt.x;
11084
+ y += pt.y;
11085
+ }
11185
11086
 
11186
- beziers.push(
11187
- ...fitCubic(pts.slice(0, splitPoint + 1), leftTangent, toCenterTangent, error, keepCorners),
11188
- ...fitCubic(pts.slice(splitPoint), fromCenterTangent, rightTangent, error, keepCorners)
11189
- );
11087
+ let centroid = { x: x / l, y: y / l };
11088
+ return centroid
11190
11089
 
11191
- return beziers;
11192
11090
  }
11193
11091
 
11194
- /**
11195
- * Use least-squares method to find Bezier control pts for region.
11196
- */
11197
- function generateBezier(pts, parameters, leftTangent, rightTangent) {
11092
+ function detectRegularPolygon(pts, centroid = { x: 0, y: 0 }) {
11093
+ let rSq = getSquareDistance(pts[0], centroid);
11094
+ let isRegular = true;
11198
11095
 
11199
- let a, tmp, u, ux, firstPoint = pts[0], lastPoint = pts[pts.length - 1];
11096
+ for (let i = 1, l = pts.length; i < l; i++) {
11097
+ let pt1 = pts[i];
11098
+ let dist = getSquareDistance(pt1, centroid);
11200
11099
 
11201
- let bezCurve = [firstPoint, null, null, lastPoint];
11202
- let A = zeros_Xx2x2(parameters.length);
11203
- let len = parameters.length;
11100
+ let diff = Math.abs(rSq - dist);
11101
+ let diffRel = diff / rSq;
11204
11102
 
11205
- for (let i = 0; i < len; i++) {
11206
- u = parameters[i];
11207
- ux = 1 - u;
11208
- a = A[i];
11103
+ if (diffRel > 0.05) {
11104
+ return false;
11105
+ }
11209
11106
 
11210
- a[0] = mulItems(leftTangent, 3 * u * (ux * ux));
11211
- a[1] = mulItems(rightTangent, 3 * ux * (u * u));
11212
11107
  }
11108
+ return isRegular;
11109
+ }
11110
+
11111
+ function analyzePoly(pts, {
11112
+ x = 0,
11113
+ y = 0,
11114
+ width = 0,
11115
+ height = 0,
11116
+ debug = false
11117
+ } = {}) {
11213
11118
 
11214
- let C = [[0, 0], [0, 0]];
11215
- let X = [0, 0];
11216
11119
  let l = pts.length;
11120
+ let left = x;
11121
+ let top = y;
11122
+ let right = x + width;
11123
+ let bottom = y + height;
11217
11124
 
11218
- for (let i = 0; i < l; i++) {
11219
- u = parameters[i];
11220
- a = A[i];
11125
+ if (!width || !height) {
11126
+ ({ x, y, width, height, top, bottom, left, right } = getPolyBBox(pts));
11127
+ }
11128
+
11129
+ // round
11130
+ [x, y, width, height, top, bottom, left, right] = [x, y, width, height, top, bottom, left, right].map(val => +val.toFixed(8));
11221
11131
 
11222
- C[0][0] += dot(a[0], a[0]);
11223
- C[0][1] += dot(a[0], a[1]);
11224
- C[1][0] += dot(a[0], a[1]);
11225
- C[1][1] += dot(a[1], a[1]);
11132
+ // bounding box of this sub poly
11133
+ let bb0 = { x, y, top, left, width, height, right, bottom };
11226
11134
 
11227
- tmp = subtract(pts[i], pointAtT([firstPoint, firstPoint, lastPoint, lastPoint], u));
11135
+ let thresh = (width + height) * 0.01;
11228
11136
 
11229
- X[0] += dot(a[0], tmp);
11230
- X[1] += dot(a[1], tmp);
11231
- }
11137
+ // threshold for horizontal or vertical detection
11138
+ let thresh2 = thresh * 0.75;
11232
11139
 
11233
- let det_C0_C1 = C[0][0] * C[1][1] - C[1][0] * C[0][1];
11234
- let det_C0_X = C[0][0] * X[1] - C[1][0] * X[0];
11235
- let det_X_C1 = X[0] * C[1][1] - X[1] * C[0][1];
11140
+ let dims = [];
11236
11141
 
11237
- let alpha_l = det_C0_C1 === 0 ? 0 : det_X_C1 / det_C0_C1;
11238
- let alpha_r = det_C0_C1 === 0 ? 0 : det_C0_X / det_C0_C1;
11239
- let segLength = getDistance(firstPoint, lastPoint, false);
11240
- let epsilon = 1.0e-6 * segLength;
11142
+ /*
11143
+ pts.forEach(pt=>{
11144
+ renderPoint(markers, pt, 'red', '2.5%')
11145
+ })
11146
+ */
11241
11147
 
11242
- if (alpha_l < epsilon || alpha_r < epsilon) {
11148
+ /**
11149
+ * 1st run:
11150
+ * collect more details
11151
+ * area for sign change detection
11152
+ * deltas and distances
11153
+ */
11154
+ for (let i = 0; i < l; i++) {
11155
+ let p0 = i > 0 ? pts[i - 1] : pts[l - 1];
11156
+ let p1 = pts[i];
11157
+ let p2 = i < l - 1 ? pts[i + 1] : pts[l - 1];
11158
+
11159
+ let area = getPolygonArea([p0, p1, p2], false);
11160
+ let dx = i > 0 ? +(p1.x - p0.x).toFixed(7) : 0;
11161
+ let dy = i > 0 ? +(p1.y - p0.y).toFixed(7) : 0;
11162
+
11163
+ let dx2 = +(p2.x - p0.x).toFixed(7);
11164
+ let dy2 = +(p2.y - p0.y).toFixed(7);
11165
+
11166
+ p1.area = area;
11167
+ p1.dist = i > 0 ? getDistManhattan(p0, p1) : 0;
11168
+ // add dist for long/short segment detection
11169
+ dims.push(p1.dist);
11170
+ p1.idx = i;
11171
+ p1.dx = dx;
11172
+ p1.dy = dy;
11173
+ p1.dx2 = dx2;
11174
+ p1.dy2 = dy2;
11243
11175
 
11244
- bezCurve[1] = addArrays(firstPoint, mulItems(leftTangent, segLength * 0.333));
11245
- bezCurve[2] = addArrays(lastPoint, mulItems(rightTangent, segLength * 0.333));
11246
- } else {
11247
- // First and last control pts of the Bezier curve
11248
- bezCurve[1] = addArrays(firstPoint, mulItems(leftTangent, alpha_l));
11249
- bezCurve[2] = addArrays(lastPoint, mulItems(rightTangent, alpha_r));
11250
11176
  }
11251
11177
 
11252
- return bezCurve;
11253
- }
11178
+ /**
11179
+ * find average segment length
11180
+ * for long/short segment detection
11181
+ */
11182
+ dims = dims.filter(Boolean).sort((a, b) => a - b);
11183
+ let lenD = dims.length;
11184
+ let dimMin = dims[0];
11185
+ dims[lenD - 1];
11254
11186
 
11255
- function generateAndReport(pts, paramsOrig, paramsPrime, leftTangent, rightTangent) {
11256
- let bezCurve, maxError, splitPoint;
11187
+ let dimAv = dims.reduce((a, b) => a + b, 0) / lenD;
11188
+ let dimShort = (dimMin + dimAv) * 0.5;
11189
+ let dimLong = dimAv * 2;
11257
11190
 
11258
- bezCurve = generateBezier(pts, paramsPrime, leftTangent, rightTangent);
11259
- let _computeMaxError = computeMaxError(pts, bezCurve, paramsOrig);
11191
+ /*
11192
+ // round to adjust for minor deviations
11193
+ let idx_q = Math.ceil(lenD * 0.25);
11194
+ let dim_mid = dims[Math.floor(lenD * 0.5)]
11195
+ let dims_min = dims.slice(0, Math.floor(lenD * 0.25));
11196
+ let dim_min = ((dims_min.reduce((a, b) => a + b, 0) / idx_q) + dim_mid) * 0.5;
11260
11197
 
11261
- maxError = _computeMaxError[0];
11262
- splitPoint = _computeMaxError[1];
11198
+ let threshold = 75
11199
+ let decimalsAuto = dim_min > threshold * 1.5 ? 0 : Math.floor(threshold / dim_min).toString().length
11263
11200
 
11264
- return [bezCurve, maxError, splitPoint];
11265
- }
11201
+ // clamp
11202
+ decimalsAuto = Math.min(Math.max(0, decimalsAuto), 8)
11266
11203
 
11267
- /**
11268
- * Given set of pts and their parameterization, try to find a better parameterization.
11269
- */
11270
- function reparameterize(bezier, pts, parameters) {
11271
- return parameters.map((p, i) => {
11272
- return newtonRaphsonRootFind(bezier, pts[i], p);
11273
- });
11274
- }
11275
- /**
11276
- * Use Newton-Raphson iteration to find better root.
11277
- */
11204
+ pts = roundPoly(pts, 2)
11205
+ console.log(pts);
11206
+ */
11278
11207
 
11279
- function newtonRaphsonRootFind(bez, point, u) {
11280
- // bez is [p0, cp1, cp2, p1] where each is [x, y]
11281
- // point is the target point [x, y] we're trying to get close to
11282
- // u is our current parameter value (0-1)
11208
+ /**
11209
+ * analyze topology:
11210
+ * find significant commands:
11211
+ * extremes, inflections etc.
11212
+ */
11213
+ for (let i = 0; i < l; i++) {
11283
11214
 
11284
- // Calculate q(u) - point (the vector from target to curve point)
11215
+ let p0 = i > 0 ? pts[i - 1] : pts[l - 1];
11216
+ let p1 = pts[i];
11217
+ let p2 = i < l - 1 ? pts[i + 1] : pts[l - 1];
11285
11218
 
11286
- let q = pointAtT(bez, u);
11219
+ // convert area to absolute for flatness checks
11220
+ let area1 = Math.abs(p1.area);
11221
+ let isCorner = false;
11222
+ let isSemiExtreme = false;
11223
+ let isShort = false;
11224
+ let isLong = false;
11287
11225
 
11288
- let dx = q.x - point.x;
11289
- let dy = q.y - point.y;
11226
+ /**
11227
+ * detect short or long
11228
+ */
11229
+ if (p1.dist > dimLong) {
11230
+ isLong = true;
11231
+ }
11290
11232
 
11291
- // First derivative (tangent vector at u)
11292
- let qp = bezierQprime(bez, u);
11233
+ if (p1.dist < dimShort) {
11234
+ isShort = true;
11235
+ }
11293
11236
 
11294
- // Numerator: dot product of (q(u) - point) and q'(u)
11295
- // This represents how much the error aligns with the tangent
11296
- let numerator = dx * qp[0] + dy * qp[1];
11237
+ let flat = !p1.area || area1 < thresh;
11297
11238
 
11298
- // Denominator: |q'(u)|² + 2 * (q(u)-point) · q''(u)
11299
- // First part: squared length of tangent vector
11300
- let qpLenSq = qp[0] * qp[0] + qp[1] * qp[1];
11239
+ /**
11240
+ * check extremes
11241
+ */
11242
+ let isExtreme = false;
11301
11243
 
11302
- // Second derivative
11303
- let qpp = bezierQprime(bez, u, true);
11244
+ // 1. total extreme
11245
+ let isTop = p1.y === bb0.top;
11246
+ let isBottom = p1.y === bb0.bottom;
11247
+ let isLeft = p1.x === bb0.left;
11248
+ let isRight = p1.x === bb0.right;
11304
11249
 
11305
- // Second part: 2 * (q(u)-point) · q''(u)
11306
- let secondPart = 2 * (dx * qpp[0] + dy * qpp[1]);
11250
+ if (isTop || isBottom || isLeft || isRight) {
11251
+ isExtreme = true;
11307
11252
 
11308
- let denominator = qpLenSq + secondPart;
11253
+ }
11309
11254
 
11310
- if (Math.abs(denominator) < 1e-10) { // Avoid division by zero
11311
- return u;
11312
- }
11255
+ // 1.2 horizontal or vertical
11256
+ /*
11257
+ let isHorizontal = isTop || isBottom || (p1.y === p0.y && p1.x !== p0.x);
11258
+ let isVertical = isLeft || isRight || (p1.x === p0.x && p1.y !== p0.y)
11313
11259
 
11314
- // Newton-Raphson step: u_new = u - f(u)/f'(u)
11315
- return u - numerator / denominator;
11316
- }
11260
+ if ((isHorizontal || isVertical)) {
11317
11261
 
11318
- /**
11319
- * Assign parameter values to digitized pts using relative distances between pts.
11320
- */
11321
- function chordLengthParameterize(pts) {
11322
- let u = [];
11323
- let l = pts.length;
11324
- let p0 = pts[0];
11325
- let p = pts[1];
11326
- let currU = 0;
11327
- let prevU = 0;
11262
+ let diffX = Math.abs(p0.x - p1.x)
11263
+ let diffY = Math.abs(p0.y - p1.y)
11328
11264
 
11329
- for (let i = 0; i < l; i++) {
11330
- p = pts[i];
11265
+ if (isLong) {
11266
+ }
11331
11267
 
11332
- currU = prevU + getDistance(p, p0, false);
11333
- u.push(currU);
11334
- prevU = currU;
11268
+ if (isLong && (diffY < thresh2) && diffX > thresh) {
11269
+ p0.isExtreme = true;
11270
+ p0.isHorizontal = true;
11271
+ }
11272
+ else if (isLong && (diffX < thresh2) && diffY > thresh) {
11273
+ p0.isExtreme = true;
11274
+ p0.isVertical = true;
11275
+ }
11335
11276
 
11336
- p0 = p;
11337
- }
11277
+ isExtreme = true
11278
+ }
11279
+ */
11338
11280
 
11339
- u = u.map(function (x) {
11340
- return x / prevU;
11341
- });
11281
+ let dx = Math.abs(p0.x - p1.x);
11282
+ let dy = Math.abs(p0.y - p1.y);
11342
11283
 
11343
- return u;
11344
- }
11345
- /**
11346
- * Find the maximum squared distance of digitized pts to fitted curve.
11347
- */
11348
- function computeMaxError(pts, bez, parameters) {
11349
- let dist,
11350
- maxDist,
11351
- splitPoint,
11352
- i, point, t;
11284
+ let vh_thresh = thresh * 0.05;
11285
+ // vh_thresh = thresh * 0.25
11286
+ let isHorizontal = isTop || isBottom || (p1.y === p0.y && p1.x !== p0.x) || (dy <= vh_thresh);
11287
+ let isVertical = (isLeft || isRight || (p1.x === p0.x && p1.y !== p0.y) || (dx <= vh_thresh));
11353
11288
 
11354
- maxDist = 0;
11355
- splitPoint = Math.floor(pts.length * 0.5);
11289
+ // renderPoint(markers, p1, 'red', '0.5%')
11356
11290
 
11357
- let t_distMap = mapTtoRelativeDistances(bez, 10);
11358
- let l = pts.length;
11359
- let ptOnPath = null;
11291
+ if (p1.y === p0.y) ;
11360
11292
 
11361
- for (i = 0; i < l; i++) {
11362
- point = pts[i];
11293
+ if ((isHorizontal || isVertical)) {
11363
11294
 
11364
- t = find_t(parameters[i], t_distMap, 10);
11295
+ if (isLong && isHorizontal) {
11296
+ p0.isExtreme = true;
11297
+ p0.isHorizontal = true;
11365
11298
 
11366
- ptOnPath = pointAtT(bez, t);
11367
- dist = getSquareDistance(ptOnPath, point);
11299
+ }
11300
+ else if (isLong && isVertical) {
11301
+ p0.isExtreme = true;
11302
+ p0.isVertical = true;
11303
+ }
11368
11304
 
11369
- /*
11370
- console.log('v', v);
11371
- v = subtract(pointAtT(bez, t), point);
11372
- dist = v.x * v.x + v.y * v.y;
11373
- */
11305
+ isExtreme = true;
11306
+ }
11374
11307
 
11375
- if (dist > maxDist) {
11308
+ // 1.3 is local or absolute extreme
11309
+ let bb = getPolyBBox([p0, p2]); // local bb
11310
+ let { left, right, top, bottom } = bb;
11311
+
11312
+ let extremeLocal = (p1.x < left || p1.x > right || p1.y < top || p1.y > bottom);
11313
+ if (!isExtreme && extremeLocal) {
11314
+ isExtreme = true;
11376
11315
 
11377
- maxDist = dist;
11378
- splitPoint = i;
11379
11316
  }
11380
- }
11381
11317
 
11382
- return [maxDist, splitPoint];
11383
- }
11318
+ /**
11319
+ * 2. sign changes
11320
+ */
11321
+ let signChange = (p0.area < 0 && p1.area > 0) || (p0.area > 0 && p1.area < 0);
11322
+ let isDirChange = signChange && !flat && !p0.isDirChange && isLong;
11384
11323
 
11385
- function mapTtoRelativeDistances(bez, B_parts) {
11386
- let B_t_curr;
11387
- let B_t_dist = [0];
11388
- let B_t_prev = bez[0];
11389
- let sumLen = 0;
11324
+ /**
11325
+ * 3. corners
11326
+ */
11390
11327
 
11391
- for (let i = 1; i <= B_parts; i++) {
11392
- B_t_curr = pointAtT(bez, i / B_parts);
11393
- sumLen += getDistance(B_t_curr, B_t_prev);
11394
- B_t_dist.push(sumLen);
11395
- B_t_prev = B_t_curr;
11396
- }
11328
+ if (isExtreme) {
11397
11329
 
11398
- B_t_dist = B_t_dist.map((x) => {
11399
- return x / sumLen;
11400
- });
11401
- return B_t_dist;
11402
- }
11403
- function find_t(param, t_distMap, B_parts) {
11330
+ let delta = getDeltaAngle(p1, p2, p0);
11331
+ let { deltaAngleDeg } = delta;
11332
+ deltaAngleDeg = Math.abs(deltaAngleDeg);
11404
11333
 
11405
- if (param < 0) {
11406
- return 0;
11407
- }
11408
- if (param > 1) {
11409
- return 1;
11410
- }
11334
+ let isCornerDelta = deltaAngleDeg > 10 && deltaAngleDeg < 160;
11335
+ if (isCornerDelta) {
11411
11336
 
11412
- let lenMax, lenMin, tMax, tMin, t;
11337
+ isCorner = true;
11413
11338
 
11414
- for (let i = 1; i <= B_parts; i++) {
11339
+ }
11415
11340
 
11416
- if (param <= t_distMap[i]) {
11417
- tMin = (i - 1) / B_parts;
11418
- tMax = i / B_parts;
11419
- lenMin = t_distMap[i - 1];
11420
- lenMax = t_distMap[i];
11421
- t = (param - lenMin) / (lenMax - lenMin) * (tMax - tMin) + tMin;
11422
- break;
11423
11341
  }
11424
- }
11425
- return t;
11426
- }
11427
11342
 
11428
- function areaDeviationTooLarge(pts, bezCurve) {
11343
+ if (isExtreme && !isCorner) {
11429
11344
 
11430
- let split = 4;
11431
- let step = 1 / split;
11432
- let l = pts.length;
11345
+ if ((Math.abs(p1.dy2) < thresh2) && Math.abs(p1.dx2) > thresh) {
11346
+ isHorizontal = true;
11347
+ }
11348
+ else if (Math.abs(p1.dx2) < thresh2 && Math.abs(p1.dy2) > thresh) {
11349
+ isVertical = true;
11350
+ }
11351
+ }
11433
11352
 
11434
- // create polygon from curve candidate
11435
- let poly = [bezCurve[0]];
11436
- let pt;
11353
+ /**
11354
+ * semi extremes
11355
+ * ~ 45deg tangent
11356
+ */
11357
+ let diffX = Math.abs(p1.dx2);
11358
+ let diffY = Math.abs(p1.dy2);
11437
11359
 
11438
- for (let i = 1; i < split; i++) {
11439
- let t = step * i;
11440
- pt = pointAtT(bezCurve, t);
11441
- poly.push(pt);
11442
- }
11360
+ let ratDelta = (diffX / diffY);
11443
11361
 
11444
- poly.push(bezCurve[bezCurve.length - 1]);
11362
+ if (ratDelta > 0.8 && ratDelta <= 1.2) {
11363
+ isSemiExtreme = true;
11364
+ }
11445
11365
 
11446
- // Original area
11447
- let polyArea = getPolygonArea(pts, false);
11366
+ p1.isCorner = isCorner;
11367
+ p1.isExtreme = isExtreme;
11368
+ p1.isSemiExtreme = isSemiExtreme;
11369
+ p1.isLong = isLong;
11370
+ p1.isShort = isShort;
11371
+
11372
+ p1.isHorizontal = isHorizontal;
11373
+ p1.isVertical = isVertical;
11374
+ p1.isDirChange = isDirChange;
11448
11375
 
11449
- // flat line
11450
- if (!polyArea && pts.length === 2) {
11451
- polyArea = getSquareDistance(pts[0], pts[1]) * 0.01;
11452
11376
  }
11453
11377
 
11454
- let curveArea = getPolygonArea(poly, false);
11455
- let rat = curveArea / polyArea;
11378
+ // add tangents
11379
+ getTangents(pts, { x, y, width, height });
11456
11380
 
11457
- let isBulged = rat > 1.1;
11381
+ refineAdjacentPolyExtremes(pts);
11458
11382
 
11459
- // isBulged = rat > 2
11460
- let bezierNew = bezCurve;
11461
- if (isBulged) {
11462
- let ptMid = pts[Math.floor(l * 0.5)];
11463
- let p = pts[l - 1];
11464
- /*
11465
- let cp1 = pointAtT([pts[0], ptMid], 0.666);
11466
- let cp2 = pointAtT([p, ptMid], 0.666);
11383
+ // filter adjacent significant points
11384
+ cleanupPolyKeypoints(pts);
11467
11385
 
11468
- bezierNew = [pts[0], cp1, cp2, p]
11386
+ renderPolyTopology(pts);
11469
11387
 
11470
-
11388
+ return pts
11389
+ }
11471
11390
 
11472
-
11473
- cp1 = pointAtT([bezCurve[0], bezCurve[3]], 0.333);
11474
- cp2 = pointAtT([bezCurve[0], bezCurve[3]], 0.666);
11475
- bezCurve = [bezCurve[0], cp1, cp2, bezCurve[3]]
11476
- */
11391
+ /*
11477
11392
 
11478
- bezCurve = [pts[0], ptMid, ptMid, p];
11479
- bezierNew = bezCurve;
11480
- }
11393
+ */
11481
11394
 
11482
- return { isBulged, bezierNew };
11483
- }
11395
+ // just for visualization
11396
+ function renderPolyTopology(pts, showTangents = true) {
11484
11397
 
11485
- /**
11486
- * Creates a vector of length 1 which shows the direction from B to A
11487
- */
11488
- function createTangent(p1, p2) {
11489
- // Returns unit vector pointing from B to A
11490
- let dx = p1.x - p2.x;
11491
- let dy = p1.y - p2.y;
11492
- let length = Math.sqrt(dx * dx + dy * dy);
11398
+ let l = pts.length;
11493
11399
 
11494
- if (length === 0) return { x: 0, y: 0 };
11495
- return { x: dx / length, y: dy / length };
11496
- }
11400
+ // render
11401
+ for (let i = 0; i < l; i++) {
11402
+ i > 0 ? pts[i - 1] : pts[l - 1];
11403
+ let p1 = pts[i];
11404
+ i < l - 1 ? pts[i + 1] : pts[l - 1];
11497
11405
 
11498
- /**
11499
- * Math helpers
11500
- */
11406
+ if (p1.isDirChange) {
11407
+ renderPoint(markers, p1, 'orange', '1%', '0.75');
11408
+ }
11501
11409
 
11502
- // Basic vector utilities (only what's absolutely necessary)
11503
- function subtract(a, b) {
11504
- return { x: a.x - b.x, y: a.y - b.y };
11505
- }
11410
+ if (p1.isSemiExtreme) {
11411
+ renderPoint(markers, p1, 'red', '1%', '0.5');
11412
+ }
11506
11413
 
11507
- function addArrays(a, b) {
11508
- return { x: a.x + b.x, y: a.y + b.y };
11509
- }
11414
+ /*
11415
+ if (p1.isLong && (p1.isDirChange || p1.isExtreme || p1.isCorner || p1.isSemiExtreme)) {
11416
+ renderPoint(markers, p1, 'green', '1.5%', '0.5')
11417
+ }
11418
+ */
11510
11419
 
11511
- function mulItems(v, s) {
11512
- return { x: v.x * s, y: v.y * s };
11513
- }
11420
+ if (p1.isDirChange) {
11421
+ renderPoint(markers, p1, 'green', '1.5%', '0.5');
11422
+ }
11514
11423
 
11515
- function normalize(v) {
11516
- let len = Math.sqrt(v.x * v.x + v.y * v.y);
11517
- return len === 0 ? { x: 0, y: 0 } : { x: v.x / len, y: v.y / len };
11518
- }
11424
+ if (p1.isExtreme) {
11425
+ renderPoint(markers, p1, 'cyan', '1%', '0.5');
11426
+ }
11519
11427
 
11520
- function dot(a, b) {
11521
- return a.x * b.x + a.y * b.y;
11522
- }
11428
+ if (p1.isHorizontal) {
11429
+ renderPoint(markers, p1, 'blue', '1.5%', '0.25');
11430
+ }
11523
11431
 
11524
- function zeros_Xx2x2(x) {
11525
- let zs = [];
11526
- while (x--) {
11527
- zs.push([0, 0]);
11528
- }
11529
- return zs;
11530
- }
11432
+ if (p1.isVertical) {
11433
+ renderPoint(markers, p1, 'purple', '1.5%', '0.25');
11434
+ }
11531
11435
 
11532
- // First derivative (tangent vector)
11533
- function bezierQprime(bez, u, second = false) {
11534
- let p0 = bez[0], cp1 = bez[1], cp2 = bez[2], p1 = bez[3];
11535
- let t = u;
11536
- let mt = 1 - t;
11537
- let mt2 = mt * mt;
11538
- let t2 = t * t;
11539
- let dx, dy;
11436
+ if (p1.isCorner) {
11437
+ renderPoint(markers, p1, 'magenta', '1%', '1');
11438
+ }
11540
11439
 
11541
- if (second) {
11542
- dx = 6 * mt * (cp2.x - 2 * cp1.x + p0.x) +
11543
- 6 * t * (p1.x - 2 * cp2.x + cp1.x);
11440
+ if (showTangents && (p1.isCorner || p1.isSemiExtreme || p1.isDirChange || p1.isExtreme)) {
11441
+ renderPoint(markers, p1.tangentL, 'darkred', '0.5%');
11442
+ renderPoint(markers, p1.tangentR, 'darkblue', '0.5%');
11544
11443
 
11545
- dy = 6 * mt * (cp2.y - 2 * cp1.y + p0.y) +
11546
- 6 * t * (p1.y - 2 * cp2.y + cp1.y);
11444
+ /*
11445
+ if (p1.isDirChange) {
11446
+ renderPoint(markers, p1.tangentL, 'darkred', '1.5%')
11447
+ renderPoint(markers, p1.tangentR, 'darkblue', '1.5%')
11448
+ }
11449
+ */
11547
11450
 
11548
- } else {
11549
- dx = 3 * mt2 * (cp1.x - p0.x) +
11550
- 6 * mt * t * (cp2.x - cp1.x) +
11551
- 3 * t2 * (p1.x - cp2.x);
11451
+ }
11552
11452
 
11553
- dy = 3 * mt2 * (cp1.y - p0.y) +
11554
- 6 * mt * t * (cp2.y - cp1.y) +
11555
- 3 * t2 * (p1.y - cp2.y);
11556
11453
  }
11557
11454
 
11558
- return [dx, dy];
11559
11455
  }
11560
11456
 
11561
- function adjustTangentAngle(cp, p0, p1, p2) {
11562
- let ang1 = getAngle(p0, p1);
11563
- let ang2 = getAngle(p0, p2);
11564
- let angDiff = (ang2 - ang1);
11565
- cp = rotatePoint(cp, p0.x, p0.y, -angDiff);
11566
- return cp
11567
- }
11457
+ function getPolyChunks(pts,
11458
+ { closed = true,
11459
+ keepCorners = true,
11460
+ keepExtremes = true,
11461
+ keepInflections = false
11462
+ } = {}
11463
+ ) {
11464
+ let chunks = [[pts[0]]];
11568
11465
 
11569
- // convert to pathdata
11570
- function bezierPtsToPathData(beziers = []) {
11571
- let pathData = [];
11572
- beziers.forEach(bez => {
11466
+ let idx = 0;
11467
+ let lastChunk = chunks[idx];
11573
11468
 
11574
- let type = bez.length === 4 ? 'C' : (bez.length === 3 ? 'Q' : 'L');
11469
+ let l = pts.length;
11575
11470
 
11576
- let cp1 = type === 'C' || type === 'Q' ? bez[1] : null;
11577
- let cp2 = type === 'C' ? bez[2] : null;
11578
- let p = bez[bez.length - 1];
11471
+ // render
11472
+ for (let i = 1; i < l; i++) {
11473
+ i > 0 ? pts[i] : pts[l - 1];
11474
+ let p1 = pts[i];
11475
+ i < l - 1 ? pts[i + 1] : pts[l - 1];
11579
11476
 
11580
- let values = type === 'C' ?
11581
- [cp1.x, cp1.y, cp2.x, cp2.y, p.x, p.y] :
11582
- (type === 'Q' ?
11583
- [cp1.x, cp1.y, p.x, p.y] :
11584
- [p.x, p.y]
11585
- );
11477
+ // start new chunk
11478
+ // keepInflections && p1.isDirChange
11479
+ if ((keepExtremes && p1.isExtreme || keepCorners && p1.isCorner ||
11480
+ (keepInflections && p1.isDirChange && !p1.isExtreme && !p1.isCorner )
11481
+ )) {
11482
+ idx++;
11483
+ chunks.push([]);
11484
+ }
11586
11485
 
11587
- let com = { type, values };
11588
- pathData.push(com);
11589
- });
11486
+ lastChunk = chunks[idx];
11487
+ lastChunk.push(p1);
11488
+ }
11590
11489
 
11591
- return pathData
11490
+ // test render
11491
+
11492
+ return chunks;
11592
11493
  }
11593
11494
 
11594
- function simplifyRC(pts, quality = 1, shiftStart = true) {
11495
+ function removeCoincidingVertices(pts = []) {
11496
+ let l = pts.length;
11497
+ if (!l) return pts;
11498
+
11499
+ let ptsN = [pts[0]];
11500
+ let pt1, pt2;
11501
+
11502
+ for (let i = 1; i < l; i++) {
11503
+ pt1 = pts[i - 1];
11504
+ pt2 = pts[i];
11505
+
11506
+ /**
11507
+ * 1. Skip zero-length segments
11508
+ */
11509
+ if (pt1.x === pt2.x && pt1.y === pt2.y) {
11510
+ continue;
11511
+ }
11512
+ ptsN.push(pt2);
11513
+ }
11514
+ return ptsN
11515
+
11516
+ }
11595
11517
 
11596
- if (pts.length < 4) return pts;
11518
+ function simplifyRC(pts = [], quality = 1, shiftStart = true) {
11597
11519
 
11598
11520
  let l = pts.length;
11521
+ if (l < 4) return pts;
11599
11522
 
11600
11523
  // starting point
11601
11524
  let M = pts[0];
@@ -11660,7 +11583,7 @@
11660
11583
  let thresh = getSquareDistance(pt0, pt2) * 0.005;
11661
11584
 
11662
11585
  // flat
11663
- if ( area <= thresh && i<l-1) {
11586
+ if (area <= thresh && i < l - 1) {
11664
11587
 
11665
11588
  pt0 = pt1;
11666
11589
  continue
@@ -11681,10 +11604,10 @@
11681
11604
  }
11682
11605
 
11683
11606
  // 1st and last are colinear
11684
- let area0 = getPolygonArea([ptsSmp[1], M, ptsSmp[ptsSmp.length-1]], true);
11685
- let thresh0 = getSquareDistance (ptsSmp[1], ptsSmp[ptsSmp.length-1]) * 0.005;
11607
+ let area0 = getPolygonArea([ptsSmp[1], M, ptsSmp[ptsSmp.length - 1]], true);
11608
+ let thresh0 = getSquareDistance(ptsSmp[1], ptsSmp[ptsSmp.length - 1]) * 0.005;
11686
11609
  // remove first point
11687
- if(area0 < thresh0) ptsSmp.shift();
11610
+ if (area0 < thresh0) ptsSmp.shift();
11688
11611
 
11689
11612
  return ptsSmp;
11690
11613
  }
@@ -11703,6 +11626,7 @@
11703
11626
  tolerance = 1,
11704
11627
  simplifyRD = 1,
11705
11628
  simplifyRDP = 1,
11629
+ isClosed = true,
11706
11630
  } = {}) {
11707
11631
 
11708
11632
  let polyPath = [];
@@ -11780,6 +11704,25 @@
11780
11704
 
11781
11705
  // remove colinear
11782
11706
 
11707
+ keepExtremes = false;
11708
+ keepCorners = false;
11709
+
11710
+ keepExtremes = true;
11711
+ keepCorners = true;
11712
+
11713
+ // check if closed
11714
+ /*
11715
+ let bb = getPolyBBox(pts)
11716
+ let thresh = (bb.width+bb.height)*0.25
11717
+ let dist0 = getDistManhattan(pts[0], pts[pts.length-1])
11718
+
11719
+ */
11720
+
11721
+ // copy 1st first to end
11722
+ if (isClosed) {
11723
+ pts.push(pts[0]);
11724
+ }
11725
+
11783
11726
  // get topology of poly
11784
11727
  let polyAnalyzed = !keepExtremes && !keepCorners ? pts : analyzePoly(pts, {
11785
11728
  debug: false
@@ -11787,69 +11730,369 @@
11787
11730
  });
11788
11731
 
11789
11732
  // split into segment chunks
11790
- let chunks = !keepExtremes && !keepCorners ? [polyAnalyzed] : getPolyChunks(polyAnalyzed, { keepCorners, keepExtremes, keepInflections });
11733
+
11734
+ let chunks = getPolyChunks(polyAnalyzed, { keepCorners, keepExtremes, keepInflections: true });
11791
11735
 
11792
11736
  // Schneider curve fit
11793
11737
  let threshold = width && height ? (width + height) / 2 * 0.004 * tolerance : 2.5;
11738
+ threshold = width && height ? (width + height) / 2 * 0.004 * tolerance : 2.5;
11794
11739
 
11795
- polyPath = simplifyPolyChunks(chunks, {
11796
- closed,
11797
- tolerance: threshold,
11798
- keepCorners,
11799
- keepExtremes: true,
11800
- });
11740
+ {
11801
11741
 
11802
- polyPath = fixIntersectingCpts(polyPath);
11742
+ polyPath = simplifyPolyChunksTopology(chunks, {
11743
+ closed,
11744
+ tolerance: threshold,
11745
+ keepCorners,
11746
+ keepExtremes: true,
11747
+ });
11748
+ }
11803
11749
 
11804
11750
  return polyPath;
11805
11751
  }
11806
11752
 
11807
11753
  /**
11808
- * convert polygon chunks
11809
- * to cubic beziers
11754
+ * topology based curve fit
11810
11755
  */
11811
-
11812
- function simplifyPolyChunks(chunks = [], {
11756
+ function simplifyPolyChunksTopology(chunks = [], {
11813
11757
  closed = true,
11814
11758
  keepCorners = true,
11815
11759
  tolerance = 1,
11816
11760
  } = {}) {
11817
11761
 
11762
+ console.log(chunks);
11763
+
11818
11764
  let l = chunks.length;
11819
11765
 
11820
11766
  // new pathData
11767
+
11821
11768
  let pathData = [{ type: 'M', values: [chunks[0][0].x, chunks[0][0].y] }];
11822
11769
 
11770
+ // loop chunks
11823
11771
  for (let i = 0; i < l; i++) {
11824
11772
 
11773
+ let chunkPrev = i > 0 ? chunks[i - 1] : (closed ? chunks[l - 1] : null);
11825
11774
  let chunk = chunks[i];
11826
- let chunkN = chunks[i + 1] ? chunks[i + 1] : null;
11775
+ let chunkN = chunks[i + 1] ? chunks[i + 1] : (closed ? chunks[0] : null);
11827
11776
  let segments = [];
11828
- let chunklen = chunk.length;
11829
- chunk[chunk.length - 1];
11830
11777
 
11831
11778
  // add from next command
11832
11779
  if (chunkN) {
11833
11780
  chunk.push(chunkN[0]);
11834
11781
  }
11835
11782
 
11836
- // nothing to simplify
11837
- if (chunklen < 2 || (chunklen === 2 && chunk[1].isExtreme)) {
11838
- chunk[chunk.length - 1];
11839
- segments = chunk.map(com => { return { type: 'L', values: [com.x, com.y] } });
11783
+ let chunklen = chunk.length;
11784
+ let hasInflection = false;
11785
+ let segments_1 = [], segments_2 = [], segments_3 = [];
11786
+ let segsRequired = 3;
11787
+
11788
+ // 1st point
11789
+ let p1 = chunk[0];
11790
+ // last point in chunk
11791
+ let p2 = chunk[chunklen - 1];
11792
+
11793
+ // nothing to simplify - lineto
11794
+
11795
+ // if (chunklen < 2 || (chunklen === 2 && (chunk[1].isExtreme || i===l-1 && !closed) )) {
11796
+
11797
+ if (chunklen < 2 || (chunklen === 2 && (chunk[1].isExtreme))) {
11798
+
11799
+ if (chunklen === 2) {
11800
+ segsRequired = 2;
11801
+ segments_2 = [
11802
+ {
11803
+ type: 'L',
11804
+ values: [p1.x, p1.y],
11805
+ p0: chunkPrev,
11806
+ p: p1,
11807
+ },
11808
+ {
11809
+ type: 'L',
11810
+ values: [chunk[1].x, chunk[1].y],
11811
+ p0: p1,
11812
+ p: p2,
11813
+ }
11814
+ ];
11815
+ } else {
11816
+ segsRequired = 1;
11817
+ segments_1 = [
11818
+ {
11819
+ type: 'L',
11820
+ values: [p1.x, p1.y],
11821
+ p0: chunkPrev,
11822
+ p: p2,
11823
+ },
11824
+ ];
11825
+ }
11840
11826
 
11841
11827
  } else {
11842
- segments = fitCurveSchneider(chunk, {
11843
- maxError: tolerance, keepCorners
11844
- });
11828
+
11829
+ // point before inflection
11830
+ let p3 = chunk[chunklen - 2];
11831
+
11832
+ chunk.filter(pt => pt.isExtreme);
11833
+ let semiExtremes = chunk.filter(pt => pt.isSemiExtreme);
11834
+ chunk.filter(pt => pt.isCorner);
11835
+ let inflections = chunk.filter(pt => pt.isDirChange && !pt.isCorner && !pt.isExtreme);
11836
+ hasInflection = inflections.length && inflections[0] !== p1;
11837
+
11838
+ let idxMid = Math.floor(chunklen * 0.5);
11839
+ let pMid = semiExtremes.length ? semiExtremes[Math.floor(semiExtremes.length * 0.5)] : chunk[idxMid];
11840
+
11841
+ let dist0 = getDistManhattan(p1, p3);
11842
+ let dist1 = getDistManhattan(pMid, p3);
11843
+ let dist2 = getDistManhattan(pMid, p1);
11844
+ let thresh = dist0 * 0.25;
11845
+ let shortMidSegment = dist1 < thresh || dist2 < thresh;
11846
+
11847
+ /**
11848
+ * we have 3 modes
11849
+ * 1 segment: only 1 segment between extremes/corners
11850
+ * 2 segments: semiextreme/mid in between
11851
+ * 3 segments: inflection
11852
+ */
11853
+
11854
+ segsRequired = shortMidSegment ? (!hasInflection ? 1 : 2) : (hasInflection ? 3 : 2);
11855
+
11856
+ let cp1_1 = p1.tangentR;
11857
+ let cp2_1 = pMid.tangentL;
11858
+ let p_1 = pMid;
11859
+
11860
+ // renderPoint(markers, pMid, 'orange', '2%')
11861
+
11862
+ let cp2_2 = p2.tangentL;
11863
+ let cp1_2 = pMid.tangentR;
11864
+ let p_2 = p2;
11865
+
11866
+ let cp1_3 = null;
11867
+ let cp2_3 = null;
11868
+ let p_3 = null;
11869
+
11870
+ // general extrapolation
11871
+ let t = 0.666;
11872
+ let ptI_1 = null, ptI_2 = null, ptI_3 = null;
11873
+
11874
+ // 1 segment
11875
+ ptI_1 = checkLineIntersection(p1, p1.tangentR, p2, p2.tangentL, false, true);
11876
+ if (ptI_1) {
11877
+ cp1_1 = interpolate(p1, ptI_1, t);
11878
+ cp2_1 = interpolate(p2, ptI_1, t);
11879
+ p_1 = p2;
11880
+
11881
+ segments_1 = [
11882
+
11883
+ {
11884
+ type: 'C',
11885
+ values: [cp1_1.x, cp1_1.y, cp2_1.x, cp2_1.y, p_1.x, p_1.y],
11886
+ p0: p1,
11887
+ cp1: cp1_1,
11888
+ cp2: cp2_1,
11889
+ p: p_1,
11890
+ }
11891
+ ];
11892
+
11893
+ }
11894
+
11895
+ // 2 segments
11896
+ ptI_1 = checkLineIntersection(p1, p1.tangentR, pMid, pMid.tangentL, false, true);
11897
+ ptI_2 = checkLineIntersection(p2, p2.tangentL, pMid, pMid.tangentR, false, true);
11898
+
11899
+ if (ptI_1 && ptI_2) {
11900
+ cp1_1 = interpolate(p1, ptI_1, t);
11901
+ cp2_1 = interpolate(pMid, ptI_1, t);
11902
+ p_1 = pMid;
11903
+
11904
+ cp1_2 = interpolate(pMid, ptI_2, t);
11905
+ cp2_2 = interpolate(p2, ptI_2, t);
11906
+ p_2 = p2;
11907
+
11908
+ segments_2 = [
11909
+
11910
+ {
11911
+ type: 'C',
11912
+ values: [cp1_1.x, cp1_1.y, cp2_1.x, cp2_1.y, p_1.x, p_1.y],
11913
+ p0: p1,
11914
+ cp1: cp1_1,
11915
+ cp2: cp2_1,
11916
+ p: p_1,
11917
+ isExtreme: p_1.isExtreme
11918
+ },
11919
+ {
11920
+ type: 'C',
11921
+ values: [cp1_2.x, cp1_2.y, cp2_2.x, cp2_2.y, p_2.x, p_2.y],
11922
+ p0: p_1,
11923
+ cp1: cp1_2,
11924
+ cp2: cp2_2,
11925
+ p: p_2,
11926
+ isExtreme: p_2.isExtreme
11927
+
11928
+ },
11929
+ ];
11930
+
11931
+ }
11932
+
11933
+ // 3 segments
11934
+
11935
+ if (hasInflection) {
11936
+
11937
+ // get pt between dir change and mid
11938
+ let idx_3_4 = Math.floor(chunklen * 0.75);
11939
+ p3 = chunk[idx_3_4];
11940
+ ptI_3 = checkLineIntersection(p3, p3.tangentR, p2, p2.tangentL, false, false);
11941
+
11942
+ if (ptI_3) {
11943
+ let tangentR_beforeDirChange = interpolate(p3, ptI_3, t);
11944
+
11945
+ // extend right tangent
11946
+ p3.tangentR.x = tangentR_beforeDirChange.x;
11947
+ p3.tangentR.y = tangentR_beforeDirChange.y;
11948
+
11949
+ // extend dir change tangent
11950
+ let tangentL_dirChange = interpolate(p2, ptI_3, t);
11951
+ p2.tangentL.x = tangentL_dirChange.x;
11952
+ p2.tangentL.y = tangentL_dirChange.y;
11953
+ } else {
11954
+
11955
+ if (p3 === p2) {
11956
+
11957
+ idx_3_4 = Math.floor(chunklen * 0.3);
11958
+ p3 = chunk[idx_3_4];
11959
+
11960
+ }
11961
+ checkLineIntersection(p3, p3.tangentR, p2, p2.tangentL, false, false);
11962
+
11963
+ cp1_1 = interpolate(p1, p1.tangentR, 1.333);
11964
+ cp2_1 = interpolate(p2, p2.tangentL, 1.333);
11965
+
11966
+ segments_3 = [
11967
+ {
11968
+ type: 'C',
11969
+ values: [cp1_1.x, cp1_1.y, cp2_1.x, cp2_1.y, p2.x, p2.y],
11970
+ p0: p1,
11971
+ cp1: cp1_1,
11972
+ cp2: cp2_1,
11973
+ p: p2,
11974
+ isExtreme: p2.isExtreme
11975
+
11976
+ },
11977
+ ];
11978
+
11979
+ /*
11980
+ let tangentR_beforeDirChange = interpolate(p3, ptI_3, t)
11981
+
11982
+ // extend right tangent
11983
+ p3.tangentR.x = tangentR_beforeDirChange.x
11984
+ p3.tangentR.y = tangentR_beforeDirChange.y
11985
+
11986
+ let tangentL_dirChange = interpolate(p2, ptI_3, t)
11987
+ p2.tangentL.x = tangentL_dirChange.x
11988
+ p2.tangentL.y = tangentL_dirChange.y
11989
+ */
11990
+
11991
+ pathDataToD([{ type: 'M', values: [p1.x, p1.y] }, ...segments_3]);
11992
+
11993
+ }
11994
+
11995
+ cp1_3 = p3.tangentR;
11996
+ cp2_3 = p2.tangentL;
11997
+ p_3 = p2;
11998
+
11999
+ ptI_1 = checkLineIntersection(p1, p1.tangentR, pMid, pMid.tangentL, false, true);
12000
+ ptI_2 = checkLineIntersection(pMid, pMid.tangentR, p3, p3.tangentL, false, true);
12001
+
12002
+ if (ptI_1 && ptI_2 && ptI_3) {
12003
+
12004
+ cp1_1 = interpolate(p1, ptI_1, t);
12005
+ cp2_1 = interpolate(pMid, ptI_1, t);
12006
+ p_1 = pMid;
12007
+
12008
+ cp1_2 = interpolate(pMid, ptI_2, t);
12009
+ cp2_2 = interpolate(p3, ptI_2, t);
12010
+ p_2 = p3;
12011
+
12012
+ segments_3 = [
12013
+ {
12014
+ type: 'C',
12015
+ values: [cp1_1.x, cp1_1.y, cp2_1.x, cp2_1.y, p_1.x, p_1.y],
12016
+ p0: p1,
12017
+ cp1: cp1_1,
12018
+ cp2: cp2_1,
12019
+ p: p_1,
12020
+ isExtreme: p_1.isExtreme
12021
+
12022
+ },
12023
+ {
12024
+ type: 'C',
12025
+ values: [cp1_2.x, cp1_2.y, cp2_2.x, cp2_2.y, p_2.x, p_2.y],
12026
+ p0: p_1,
12027
+ cp1: cp1_2,
12028
+ cp2: cp2_2,
12029
+ p: p_2,
12030
+ },
12031
+ {
12032
+ type: 'C',
12033
+ values: [cp1_3.x, cp1_3.y, cp2_3.x, cp2_3.y, p_3.x, p_3.y],
12034
+ p0: p_2,
12035
+ cp1: cp1_3,
12036
+ cp2: cp2_3,
12037
+ p: p_3,
12038
+ isExtreme: p_3.isExtreme
12039
+
12040
+ }
12041
+ ];
12042
+
12043
+ pathDataToD([{ type: 'M', values: [p1.x, p1.y] }, ...segments_3]);
12044
+
12045
+ }
12046
+
12047
+ }
12048
+
12049
+ }
12050
+
12051
+ if (segsRequired === 1) {
12052
+ segments = segments_1;
12053
+ } else if (segsRequired === 2 && segments_2.length) {
12054
+ segments = segments_2;
12055
+ }
12056
+ else ;
12057
+
12058
+ segments = segments_3.length ? segments_3 : segments_2;
12059
+ /*
12060
+ if (simplify && !isLinetoSeg && segments.length > 1) {
12061
+
12062
+ let com1 = segments[0]
12063
+ let com2 = segments[1]
12064
+
12065
+ tolerance = 1.1
12066
+ let combined = combineCubicPairs(com1, com2, { tolerance })
12067
+
12068
+ let error = 0;
12069
+ let comsSimp =[]
12070
+
12071
+ console.log('!!!combined', segments.length, combined);
12072
+
12073
+ // success
12074
+ if (combined.length === 1) {
12075
+
12076
+ if(segments.length === 2){
12077
+ segments = combined
12078
+ }
12079
+
12080
+ let com = combined[0]
12081
+ }
12082
+
11845
12083
  }
12084
+ */
11846
12085
 
11847
12086
  // remove first segment to connect to last segment
11848
12087
  pathData.push(...segments);
11849
12088
 
11850
12089
  }
11851
12090
 
11852
- if (closed) pathData.push({ type: 'Z', values: [] });
12091
+ if (closed) {
12092
+ pathData.push({ type: 'Z', values: [] });
12093
+ }
12094
+
12095
+ // refine extremes
11853
12096
  return pathData
11854
12097
 
11855
12098
  }
@@ -11860,13 +12103,18 @@
11860
12103
  */
11861
12104
  function pathDataToPolygonOpt(pathData, {
11862
12105
  precisionPoly = 1,
11863
- autoAccuracy=false,
11864
- polyFormat='points',
11865
- decimals=-1,
11866
- simplifyRD=1,
11867
- simplifyRDP=1,
12106
+ autoAccuracy = false,
12107
+ polyFormat = 'object',
12108
+ decimals = -1,
12109
+ simplifyRD = 1,
12110
+ simplifyRDP = 1,
11868
12111
  } = {}) {
11869
12112
 
12113
+ pathData = convertPathData(pathData, {toAbsolute:true, toLonghands:true, arcToCubic:true});
12114
+ pathData = addExtremePoints(pathData);
12115
+
12116
+ pathData = getPathDataVerbose(pathData);
12117
+
11870
12118
  let l = pathData.length;
11871
12119
  let M = { x: pathData[0].values[0], y: pathData[0].values[1] };
11872
12120
  let p0 = M;
@@ -11893,7 +12141,7 @@
11893
12141
  let pts2 = [pts[0]];
11894
12142
 
11895
12143
  // adjustments for very small or large paths
11896
- dims = dims.filter(Boolean).sort();
12144
+ dims = dims.filter(Boolean).sort((a,b)=>a-b);
11897
12145
  let dimMax = dims[dims.length - 1];
11898
12146
 
11899
12147
  let scale = dimMax > 2 && dimMax < 25 ? 1 : (20 / dimMax);
@@ -11932,31 +12180,33 @@
11932
12180
  }
11933
12181
 
11934
12182
  // simplify polygon
11935
- if(simplifyRD>0){
11936
- pts2 = simplifyPolyRD(pts2, {quality:simplifyRD});
12183
+ if (simplifyRD > 0) {
12184
+ pts2 = simplifyPolyRD(pts2, { quality: simplifyRD });
11937
12185
  }
11938
12186
 
11939
- if(simplifyRDP>0){
11940
- pts2 = simplifyPolyRDP(pts2, {quality:simplifyRDP});
12187
+ if (simplifyRDP > 0) {
12188
+ pts2 = simplifyPolyRDP(pts2, { quality: simplifyRDP });
11941
12189
  }
11942
12190
 
11943
- pathDataPoly = pathDataFromPoly(pts2);
11944
- pathData = pathDataPoly;
11945
-
11946
- if(autoAccuracy){
12191
+ if (autoAccuracy) {
11947
12192
  decimals = detectAccuracyPoly(pts);
11948
12193
  }
11949
12194
 
11950
- 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 } });
12195
+ 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 } });
11951
12196
 
11952
- if(polyFormat==='array'){
12197
+ pathDataPoly = pathDataFromPoly(poly);
12198
+ pathData = pathDataPoly;
12199
+
12200
+ if (polyFormat === 'array') {
11953
12201
  poly = poly.map(pt => { return [pt.x, pt.y] });
11954
12202
  }
11955
- else if(polyFormat==='string'){
12203
+ else if (polyFormat === 'string') {
11956
12204
  poly = poly.map(pt => { return [pt.x, pt.y].join(',') }).flat().join(' ');
11957
12205
  }
11958
12206
 
11959
- return { pathData, poly }
12207
+ let d= pathDataToD(pathData);
12208
+
12209
+ return { pathData, poly, d }
11960
12210
 
11961
12211
  }
11962
12212
 
@@ -12197,6 +12447,7 @@
12197
12447
  // polygon
12198
12448
  toPolygon: false,
12199
12449
  smoothPoly: false,
12450
+ isClosed:true,
12200
12451
  polyFormat: 'object',
12201
12452
  precisionPoly: 1,
12202
12453
  simplifyRD: 0,
@@ -12345,7 +12596,6 @@
12345
12596
  addViewBox: true,
12346
12597
  removeDimensions: true,
12347
12598
  removeOffCanvas: true,
12348
-
12349
12599
  /*
12350
12600
  */
12351
12601
  }
@@ -12494,6 +12744,122 @@
12494
12744
  }
12495
12745
  */
12496
12746
 
12747
+ function SlickVGObj(props = {}) {
12748
+
12749
+ Object.assign(this, props);
12750
+ }
12751
+
12752
+ SlickVGObj.prototype.getD = function () {
12753
+ let d = this.d;
12754
+ return d;
12755
+ };
12756
+
12757
+ SlickVGObj.prototype.getSvg = function () {
12758
+ let svg = this.svg;
12759
+
12760
+ if (!svg) {
12761
+ let xArr = [];
12762
+ let yArr = [];
12763
+ let d = this.d;
12764
+ let pathDataPlusArr = this.pathDataPlusArr || [];
12765
+ pathDataPlusArr.forEach(path => {
12766
+ path.forEach(sub => {
12767
+ let { pathData } = sub;
12768
+ let bb = getPathDataBBox(pathData);
12769
+ let { x, y, right, bottom } = bb;
12770
+ xArr.push(x, right);
12771
+ yArr.push(y, bottom);
12772
+ });
12773
+ });
12774
+
12775
+ let x = Math.min(...xArr);
12776
+ let right = Math.max(...xArr);
12777
+ let y = Math.min(...yArr);
12778
+ let bottom = Math.max(...yArr);
12779
+ let width = right - x;
12780
+ let height = bottom - y;
12781
+
12782
+ svg = `<svg xmlns="${svgNs}" viewBox="${[x, y, width, height].join(' ')}"><path d="${d}"/></svg>`;
12783
+
12784
+ }
12785
+ return svg;
12786
+ };
12787
+
12788
+ /**
12789
+ * retrieve poly
12790
+ * formats: points, array, string, pathData, d,
12791
+ */
12792
+ SlickVGObj.prototype.getPoly = function (options = {}
12793
+ ) {
12794
+
12795
+ options = {
12796
+ ...{
12797
+ precisionPoly: 1,
12798
+ simplifyRDP: 0,
12799
+ simplifyRD: 0,
12800
+ autoAccuracy: true,
12801
+ decimals: 3,
12802
+ format: 'object'
12803
+ },
12804
+ ...options
12805
+ };
12806
+
12807
+ let { precisionPoly, simplifyRDP, simplifyRD, autoAccuracy, decimals, format } = options;
12808
+
12809
+ let polyFormat = format;
12810
+
12811
+ let polys = this.polys;
12812
+ if (!polys.length) {
12813
+ let pathDataPlusArr = this.pathDataPlusArr || [];
12814
+ let poly = [];
12815
+ let polyPaths = [];
12816
+ let dPoly = '';
12817
+ pathDataPlusArr.forEach(path => {
12818
+ path.forEach(sub => {
12819
+ let { pathData } = sub;
12820
+
12821
+ let polyData = pathDataToPolygonOpt(pathData, {
12822
+ precisionPoly,
12823
+ autoAccuracy,
12824
+ decimals,
12825
+ simplifyRD,
12826
+ simplifyRDP,
12827
+ polyFormat
12828
+ });
12829
+
12830
+ dPoly += polyData.d;
12831
+ poly.push(polyData.poly);
12832
+ polyPaths.push(polyData.pathData);
12833
+
12834
+ });
12835
+ });
12836
+
12837
+ if (polyFormat === 'object' || polyFormat === 'array' || polyFormat === 'string') {
12838
+ polys = poly;
12839
+ }
12840
+ else if (polyFormat === 'pathData') {
12841
+ polys = polyPaths.flat();
12842
+ }
12843
+
12844
+ else if (polyFormat === 'd') {
12845
+ polys = dPoly;
12846
+ }
12847
+
12848
+ }
12849
+ return polys;
12850
+ };
12851
+
12852
+ /*
12853
+ export function PathLengthObject(props = {}) {
12854
+ Object.assign(this, props);
12855
+ }
12856
+ */
12857
+
12858
+ function SlickVG(input = '', settings = {}) {
12859
+ settings.getObject = true;
12860
+ return svgPathSimplify(input, settings)
12861
+ }
12862
+
12497
12863
  function svgPathSimplify(input = '', settings = {}) {
12498
12864
 
12499
12865
  let preset = settings['preset'] !== undefined && settings['preset'] ? settings['preset'] : null;
@@ -12505,7 +12871,7 @@
12505
12871
  ...settings
12506
12872
  };
12507
12873
 
12508
- 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;
12874
+ 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;
12509
12875
 
12510
12876
  // clamp tolerance and scale
12511
12877
  tolerance = Math.max(0.1, tolerance);
@@ -12516,6 +12882,10 @@
12516
12882
  settings.convertTransforms = true;
12517
12883
  }
12518
12884
 
12885
+ if (shapeConvert === 'toShapes' || shapeConvert === 'shapesToPaths') {
12886
+ keepSmaller = false;
12887
+ }
12888
+
12519
12889
  /**
12520
12890
  * intercept
12521
12891
  * invalid inputs
@@ -12532,11 +12902,11 @@
12532
12902
  original: 0,
12533
12903
  new: 0,
12534
12904
  saved: 0,
12535
- svgSize:0,
12536
- svgSizeOpt:0,
12537
- compression:0,
12538
- decimals:0,
12539
- invalid:true
12905
+ svgSize: 0,
12906
+ svgSizeOpt: 0,
12907
+ compression: 0,
12908
+ decimals: 0,
12909
+ invalid: true
12540
12910
  };
12541
12911
 
12542
12912
  return { svg: dummySVG, d: '', polys: [], report, pathDataPlusArr: [], pathDataPlusArr_global: [], inputType: 'invalid', dOriginal: '' };
@@ -12693,6 +13063,8 @@
12693
13063
  toRelative,
12694
13064
  toMixed,
12695
13065
  toShorthands,
13066
+ // return true arc radii or minified/parametrized
13067
+ optimizeArcs: minifyD < 1,
12696
13068
  decimals,
12697
13069
  };
12698
13070
 
@@ -12704,7 +13076,12 @@
12704
13076
  let pathDataPlusArr = [];
12705
13077
  let path = paths[i];
12706
13078
  let { d, el } = path;
12707
- let isPoly = false;
13079
+
13080
+ // disable reordering for elements with stroke dash-array
13081
+ if (el && (el.hasAttribute('stroke-dasharray') || el.hasAttribute('stroke-dashoffset'))) {
13082
+ optimizeOrder = false;
13083
+
13084
+ }
12708
13085
 
12709
13086
  // if polygon we already heave absolute coordinates
12710
13087
 
@@ -12745,7 +13122,7 @@
12745
13122
  // count commands for evaluation
12746
13123
  comCount += pathData.length;
12747
13124
 
12748
- if (!isPoly && removeOrphanSubpaths) pathData = removeOrphanedM(pathData);
13125
+ if (removeOrphanSubpaths) pathData = removeOrphanedM(pathData);
12749
13126
 
12750
13127
  /**
12751
13128
  * get sub paths
@@ -12759,8 +13136,9 @@
12759
13136
  let pathDataSub = subPathArr[i];
12760
13137
  let poly = [];
12761
13138
  let coms = Array.from(new Set(pathDataSub.map(com => com.type))).join('');
12762
- isPoly = !(/[acqts]/gi).test(coms);
12763
- let closed = isPoly ? true : false;
13139
+ let isPoly = !(/[acqts]/gi).test(coms);
13140
+
13141
+ let closed = (/z/gi).test(coms);
12764
13142
 
12765
13143
  if (isPoly && !mode) {
12766
13144
 
@@ -12777,7 +13155,6 @@
12777
13155
 
12778
13156
  }
12779
13157
 
12780
- toPolygon = false;
12781
13158
  pathDataSub = pathDataFromPoly(poly, closed);
12782
13159
 
12783
13160
  }
@@ -12786,26 +13163,56 @@
12786
13163
  * convert curves to polygon
12787
13164
  * flattening
12788
13165
  */
12789
- else if (toPolygon) {
13166
+
13167
+ if (toPolygon) {
12790
13168
  simplifyBezier = false;
12791
13169
  smoothPoly = false;
12792
13170
  harmonizeCpts = false;
12793
13171
 
12794
- pathDataSub = getPathDataVerbose(pathDataSub);
13172
+ /**
13173
+ * if pathdata is already polygon- pass through
13174
+ * otherwise create precise polygon by curve splitting
13175
+ * */
12795
13176
 
12796
- let polyData = pathDataToPolygonOpt(pathDataSub, {
12797
- precisionPoly,
12798
- autoAccuracy,
13177
+ if (!isPoly) {
13178
+ pathDataSub = getPathDataVerbose(pathDataSub);
13179
+ let polyData = pathDataToPolygonOpt(pathDataSub, {
13180
+ precisionPoly,
13181
+ autoAccuracy,
13182
+ polyFormat,
12799
13183
 
12800
- simplifyRD,
12801
- simplifyRDP
12802
- });
13184
+ simplifyRD,
13185
+ simplifyRDP
13186
+ });
13187
+
13188
+ poly = polyData.poly;
13189
+ pathDataSub = polyData.pathData;
13190
+ isPoly = true;
13191
+
13192
+ }
13193
+
13194
+ polys.push(poly);
13195
+
13196
+ }
13197
+
13198
+ // harmonize cpts
13199
+ // if (harmonizeCpts) pathDataSub = harmonizeCubicCpts(pathDataSub)
12803
13200
 
12804
- pathDataSub = polyData.pathData;
12805
- isPoly = true;
13201
+ if (smoothPoly) {
12806
13202
 
13203
+ removeZeroLength=true;
13204
+ optimizeOrder=true;
12807
13205
  }
12808
13206
 
13207
+ // remove zero length linetos
13208
+ if (removeColinear || removeZeroLength) pathDataSub = removeZeroLengthLinetos(pathDataSub);
13209
+
13210
+ // sort to top left
13211
+ if (optimizeOrder) pathDataSub = pathDataToTopLeft(pathDataSub);
13212
+
13213
+ // Preprocessing: remove colinear - ignore flat beziers (removed later)
13214
+ if (removeColinear) pathDataSub = pathDataRemoveColinear(pathDataSub, { tolerance, flatBezierToLinetos: false });
13215
+
12809
13216
  /**
12810
13217
  * poly to beziers via
12811
13218
  * Philip J. Schneider's
@@ -12814,12 +13221,18 @@
12814
13221
  if (smoothPoly) {
12815
13222
 
12816
13223
  if (isPoly) {
12817
- pathDataSub = removeZeroLengthLinetos(pathDataSub);
13224
+ /*
13225
+ pathDataSub = pathDataToTopLeft(pathDataSub)
13226
+ pathDataSub = removeZeroLengthLinetos(pathDataSub)
13227
+ pathDataSub = pathDataRemoveColinear(pathDataSub, { tolerance, flatBezierToLinetos: true });
13228
+ */
13229
+
12818
13230
  let poly = getPathDataVertices(pathDataSub);
12819
13231
 
12820
13232
  // options for poly simplification
12821
13233
  let optionsPoly = {
12822
- denoise: 0.8,
13234
+
13235
+ denoise: 0,
12823
13236
  tolerance,
12824
13237
  width: bb_poly.width,
12825
13238
  height: bb_poly.height,
@@ -12831,26 +13244,15 @@
12831
13244
  closed,
12832
13245
  simplifyRD,
12833
13246
  simplifyRDP,
13247
+ isClosed,
12834
13248
  };
12835
13249
 
12836
13250
  pathDataSub = simplifyPolygonToPathData(poly, optionsPoly);
12837
13251
  // flag as non poly as we're smoothing to curves
12838
-
13252
+ isPoly = false;
12839
13253
  }
12840
13254
  }
12841
13255
 
12842
- // harmonize cpts
12843
- // if (harmonizeCpts) pathDataSub = harmonizeCubicCpts(pathDataSub)
12844
-
12845
- // remove zero length linetos
12846
- if (removeColinear || removeZeroLength) pathDataSub = removeZeroLengthLinetos(pathDataSub);
12847
-
12848
- // sort to top left
12849
- if (optimizeOrder) pathDataSub = pathDataToTopLeft(pathDataSub);
12850
-
12851
- // Preprocessing: remove colinear - ignore flat beziers (removed later)
12852
- if (removeColinear) pathDataSub = pathDataRemoveColinear(pathDataSub, { tolerance, flatBezierToLinetos: false });
12853
-
12854
13256
  let tMin = 0, tMax = 1;
12855
13257
  if (addExtremes) pathDataSub = addExtremePoints(pathDataSub,
12856
13258
  { tMin, tMax, addExtremes, angles: [30] });
@@ -12873,11 +13275,13 @@
12873
13275
  pathDataPlus.bb = getPolyBBox(getPathDataVertices(pathDataCubic));
12874
13276
  }
12875
13277
  pathDataPlus.dimA = pathDataPlus.bb.width + pathDataPlus.bb.height;
13278
+
12876
13279
  pathDataPlus.pathData = getPathDataVerbose(pathDataSub, {
12877
13280
  addSquareLength: false,
12878
13281
  addArea: false,
12879
13282
  addAverageDim: false
12880
13283
  });
13284
+
12881
13285
  }
12882
13286
 
12883
13287
  // simplify beziers
@@ -12888,6 +13292,12 @@
12888
13292
 
12889
13293
  if (refineClosing) pathData = refineClosingCommand(pathData, { threshold: dimA * 0.001 });
12890
13294
 
13295
+ // refine round segment sequences
13296
+ if (simplifyRound) {
13297
+ pathData = refineRoundSegments(pathData);
13298
+ pathData = simplifyAdjacentRound(pathData);
13299
+ }
13300
+
12891
13301
  pathData = simplifyBezier ? simplifyPathDataCubic(pathData, { simplifyBezier, keepInflections, keepExtremes, keepCorners, revertToQuadratics, tolerance }) : pathData;
12892
13302
 
12893
13303
  // refine extremes
@@ -12911,12 +13321,6 @@
12911
13321
  pathData = refineRoundedCorners(pathData, { threshold, tolerance, simplifyQuadraticCorners });
12912
13322
  }
12913
13323
 
12914
- // refine round segment sequences
12915
- if (simplifyRound) {
12916
- pathData = refineRoundSegments(pathData);
12917
- pathData = simplifyAdjacentRound(pathData);
12918
- }
12919
-
12920
13324
  // simplify to quadratics
12921
13325
  if (revertToQuadratics) pathData = pathDataRevertCubicToQuadratic(pathData, tolerance);
12922
13326
 
@@ -12933,6 +13337,8 @@
12933
13337
 
12934
13338
  }
12935
13339
 
13340
+ // offset path
13341
+
12936
13342
  // update
12937
13343
  pathDataPlusArr.push({ pathData, bb });
12938
13344
 
@@ -12992,17 +13398,18 @@
12992
13398
  }
12993
13399
 
12994
13400
  // add simplified poly - if not populated by toPoly conversion
13401
+ /*
12995
13402
  if (isPoly) {
12996
13403
 
12997
13404
  pathDataPlusArr.forEach(sub => {
12998
- let poly = getPathDataVertices(sub.pathData, false, decimals);
13405
+ let poly = getPathDataVertices(sub.pathData, false, decimals)
12999
13406
  if (polyFormat === 'array') {
13000
- poly = polyPtsToArray(poly);
13407
+ poly = polyPtsToArray(poly)
13001
13408
  }
13002
- polys.push(poly);
13003
- });
13004
-
13409
+ polys.push(poly)
13410
+ })
13005
13411
  }
13412
+ */
13006
13413
 
13007
13414
  // split into sub paths - returns svg with multiple paths
13008
13415
  if (splitCompound && !mode && pathDataPlusArr.length > 1) {
@@ -13101,7 +13508,6 @@
13101
13508
  svg = stringifySVG(svg, { omitNamespace, removeComments, format: minifyD });
13102
13509
 
13103
13510
  svgSizeOpt = svg.length;
13104
-
13105
13511
  compression = +(100 / svgSize * (svgSizeOpt)).toFixed(2);
13106
13512
 
13107
13513
  svgSize = +(svgSize / 1024).toFixed(3);
@@ -13127,15 +13533,52 @@
13127
13533
  ({ d, report } = paths[0]);
13128
13534
  }
13129
13535
 
13130
- if (polys.length && polys.length === 1) {
13131
- polys = polys[0];
13132
- }
13536
+ // sanitize poly output
13537
+ if (polys.length) {
13538
+
13539
+ // round point data
13540
+ polys.forEach((poly, i) => {
13541
+
13542
+ if (polyFormat === 'string') poly = normalizePoly(poly);
13543
+
13544
+ poly = roundPoly(poly, decimals);
13545
+ // remove coinciding points
13546
+ polys[i] = removeCoincidingVertices(poly);
13547
+ });
13548
+
13549
+ if (polys.length === 1) {
13550
+ polys = polys[0];
13551
+ }
13552
+
13553
+ if (polyFormat === 'string') {
13554
+
13555
+ polys = normalizePoly(polys, { toArray: true, flatten: true });
13556
+
13557
+ polys = polys.flat().join(' ');
13558
+ }
13133
13559
 
13134
- if (polyFormat === 'string' && polys.length) {
13135
- polys = polys.flat().map(pt => `${pt.x},${pt.y}`).join(' ');
13136
13560
  }
13137
13561
 
13138
- return !getObject ? (d ? d : svg) : { svg, d, polys, report, pathDataPlusArr: pathDataPlusArr_global, inputType, dOriginal };
13562
+ // create object
13563
+ let svgObj = new SlickVGObj({ svg, d, polys, report, pathDataPlusArr: pathDataPlusArr_global, inputType, dOriginal });
13564
+
13565
+ /*
13566
+ let d2 = svgObj.getD()
13567
+ console.log('d2', d2);
13568
+
13569
+ let svg2 = svgObj.getSvg()
13570
+ console.log('svg2', svg2);
13571
+
13572
+ let format = 'd'
13573
+ format = 'pathData'
13574
+ format = 'd'
13575
+ format = 'points'
13576
+ format = 'array'
13577
+ let poly2 = svgObj.getPoly({ format })
13578
+ console.log('poly2', 'format', format, poly2);
13579
+ */
13580
+
13581
+ return !getObject ? (d ? d : svg) : svgObj;
13139
13582
 
13140
13583
  }
13141
13584
 
@@ -13144,6 +13587,7 @@
13144
13587
  // IIFE
13145
13588
  if (typeof window !== 'undefined') {
13146
13589
  window.svgPathSimplify = svgPathSimplify;
13590
+ window.SlickVG = SlickVG;
13147
13591
  window.getElementTransform = getElementTransform;
13148
13592
  window.validateSVG = validateSVG;
13149
13593
  window.detectInputType = detectInputType;
@@ -13153,6 +13597,7 @@
13153
13597
  }
13154
13598
 
13155
13599
  exports.PI = PI$1;
13600
+ exports.SlickVG = SlickVG;
13156
13601
  exports.abs = abs$1;
13157
13602
  exports.acos = acos$1;
13158
13603
  exports.asin = asin$1;