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