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