svg-path-simplify 0.4.5 → 0.4.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/README.md +24 -14
  3. package/dist/svg-path-simplify.esm.js +1620 -1216
  4. package/dist/svg-path-simplify.esm.min.js +2 -2
  5. package/dist/svg-path-simplify.js +1620 -1215
  6. package/dist/svg-path-simplify.min.js +2 -2
  7. package/dist/svg-path-simplify.pathdata.esm.js +298 -665
  8. package/dist/svg-path-simplify.pathdata.esm.min.js +2 -2
  9. package/dist/svg-path-simplify.poly.cjs +8 -9
  10. package/drawing.svg +62 -0
  11. package/index.html +11 -5
  12. package/package.json +1 -1
  13. package/src/index.js +3 -1
  14. package/src/pathData_get_intersections.js +777 -0
  15. package/src/pathData_offset.js +201 -0
  16. package/src/pathData_simplify_cubic.js +14 -34
  17. package/src/pathData_simplify_cubic_extrapolate.js +147 -8
  18. package/src/pathData_simplify_cubicsToArcs.js +65 -21
  19. package/src/pathSimplify-main.js +259 -60
  20. package/src/pathSimplify-presets.js +1 -0
  21. package/src/poly-fit-curve-schneider.js +171 -132
  22. package/src/poly-fit-curve-schneider_check_bulge.js +131 -0
  23. package/src/poly-offset.js +133 -0
  24. package/src/simplify_poly_RC.js +29 -7
  25. package/src/svgii/geometry.js +130 -27
  26. package/src/svgii/geometry_bbox.js +1 -1
  27. package/src/svgii/pathData_analyze.js +6 -28
  28. package/src/svgii/pathData_convert.js +70 -15
  29. package/src/svgii/pathData_remove_collinear.js +32 -36
  30. package/src/svgii/pathData_simplify_refineExtremes.js +1 -3
  31. package/src/svgii/pathData_stringify.js +132 -6
  32. package/src/svgii/pathData_toPolygon.js +33 -23
  33. package/src/svgii/poly_analyze.js +272 -125
  34. package/src/svgii/poly_analyze_cleanup.js +311 -0
  35. package/src/svgii/poly_analyze_getTangents.js +125 -0
  36. package/src/svgii/poly_analyze_get_chunks.js +3 -1
  37. package/src/svgii/poly_normalize.js +12 -0
  38. package/src/svgii/poly_to_pathdata.js +477 -8
  39. package/src/svgii/rounding.js +29 -39
  40. package/src/svgii/svg_cleanup.js +42 -54
  41. package/src/svgii/svg_cleanup_normalize_transforms.js +1 -1
  42. package/src/svgii/visualize.js +33 -2
@@ -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 = []) {
@@ -4367,6 +4321,11 @@
4367
4321
 
4368
4322
  let [rx, ry, largeArc, x, y] = [values[0], values[1], values[3], values[5], values[6]];
4369
4323
  let comPrev = pathData[i - 1];
4324
+
4325
+ // force absolute
4326
+ rx = Math.abs(rx);
4327
+ ry = Math.abs(ry);
4328
+
4370
4329
  let [x0, y0] = [comPrev.values[comPrev.values.length - 2], comPrev.values[comPrev.values.length - 1]];
4371
4330
  let M = { x: x0, y: y0 };
4372
4331
  let p = { x, y };
@@ -4406,7 +4365,7 @@
4406
4365
  if (isHorizontal || isVertical) {
4407
4366
 
4408
4367
  // check if semi circle
4409
- let needsTrueR = isHorizontal ? rx*1.9 > diffX : ry*1.9 > diffY;
4368
+ let needsTrueR = isHorizontal ? rx * 1.9 > diffX : ry * 1.9 > diffY;
4410
4369
 
4411
4370
  // is semicircle we can simplify rx
4412
4371
  if (!needsTrueR) {
@@ -5102,8 +5061,8 @@
5102
5061
  let phi = rotation ? rotation * TAU / 360 : 0;
5103
5062
  let sinphi = phi ? Math.sin(phi) : 0;
5104
5063
  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;
5064
+ let pxp = cosphi * (p0.x - x) *0.5 + sinphi * (p0.y - y) *0.5;
5065
+ let pyp = -sinphi * (p0.x - x) *0.5 + cosphi * (p0.y - y) *0.5;
5107
5066
 
5108
5067
  if (pxp === 0 && pyp === 0) {
5109
5068
  return []
@@ -5139,8 +5098,8 @@
5139
5098
 
5140
5099
  let centerxp = radicant ? radicant * rx / ry * pyp : 0;
5141
5100
  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;
5101
+ let centerx = cosphi * centerxp - sinphi * centeryp + (p0.x + x) * 0.5;
5102
+ let centery = sinphi * centerxp + cosphi * centeryp + (p0.y + y) * 0.5;
5144
5103
 
5145
5104
  let vx1 = (pxp - centerxp) / rx;
5146
5105
  let vy1 = (pyp - centeryp) / ry;
@@ -5162,15 +5121,17 @@
5162
5121
  ang2 = vectorAngle(vx1, vy1, vx2, vy2);
5163
5122
 
5164
5123
  if (sweepFlag === 0 && ang2 > 0) {
5165
- ang2 -= Math.PI * 2;
5124
+
5125
+ ang2 -= TAU;
5166
5126
  }
5167
5127
  else if (sweepFlag === 1 && ang2 < 0) {
5168
- ang2 += Math.PI * 2;
5128
+
5129
+ ang2 += TAU;
5169
5130
  }
5170
5131
 
5171
- let ratio = +(Math.abs(ang2) / (TAU / 4)).toFixed(0) || 1;
5132
+ let ratio = +(Math.abs(ang2) / (TAU * 0.25)).toFixed(0) || 1;
5172
5133
 
5173
- // increase segments for more accureate length calculations
5134
+ // increase segments for more accurate length calculations
5174
5135
  let segments = ratio * splitSegments;
5175
5136
  ang2 /= segments;
5176
5137
  let pathDataArc = [];
@@ -5182,7 +5143,8 @@
5182
5143
  const k = 0.551785;
5183
5144
  let a = ang2 === angle90 ? k :
5184
5145
  (
5185
- ang2 === -angle90 ? -k : 4 / 3 * Math.tan(ang2 / 4)
5146
+
5147
+ ang2 === -angle90 ? -k : 1.33333 * Math.tan(ang2 * 0.25)
5186
5148
  );
5187
5149
 
5188
5150
  let cos2 = ang2 ? Math.cos(ang2) : 1;
@@ -5229,7 +5191,7 @@
5229
5191
  let comA = cubicCommandToArc(p0, cp1, cp2, p, areaThreshold);
5230
5192
  let comAN = cubicCommandToArc(comN.p0, comN.cp1, comN.cp2, comN.p, areaThreshold);
5231
5193
 
5232
- if (comA.isArc && comAN.isArc) {
5194
+ if (comA.isArc && comAN.isArc) {
5233
5195
 
5234
5196
  let dist = getDistManhattan(p0, comN.p);
5235
5197
  let maxDist = dist * 0.01;
@@ -5244,8 +5206,18 @@
5244
5206
  let sweep = area < 0 ? 0 : 1;
5245
5207
 
5246
5208
  if (vertical || horizontal) {
5209
+
5247
5210
  rx = Math.min(rx, comAN.rx);
5248
5211
  ry = Math.min(ry, comAN.ry);
5212
+
5213
+ let diffR = Math.abs(rx - ry) / rx;
5214
+ let isSemiCircle = diffR < 0.025;
5215
+
5216
+ if (isSemiCircle) {
5217
+ rx = rx > 1 ? 1 : Math.min(rx, ry);
5218
+ ry = rx;
5219
+ }
5220
+
5249
5221
  pathData[c] = null;
5250
5222
  pathData[c + 1].type = 'A';
5251
5223
  pathData[c + 1].values = [rx, ry, 0, 0, sweep, comN.p.x, comN.p.y];
@@ -5268,33 +5240,61 @@
5268
5240
  let arcSegArea = 0, isArc = false;
5269
5241
 
5270
5242
  // check angles
5271
- let angle1 = getAngle(p0, cp1, true);
5272
- let angle2 = getAngle(p, cp2, true);
5243
+ let dx1 = (cp1.x - p0.x);
5244
+ let dy1 = (cp1.y - p0.y);
5245
+ let dx2 = (cp2.x - p.x);
5246
+ let dy2 = (cp2.y - p.y);
5247
+
5248
+ let thresh = getDistManhattan(p0, p) * 0.001;
5249
+
5250
+ let isVertical1 = dx1 < thresh;
5251
+ let isVertical2 = dx2 < thresh;
5252
+
5253
+ let isHorizontal1 = dy1 < thresh;
5254
+ let isHorizontal2 = dy2 < thresh;
5255
+
5256
+ let isRightAngle = (isVertical1 || isVertical2) && (isHorizontal1 || isHorizontal2);
5257
+
5258
+ /*
5259
+ let angle1 = getAngleFromDelta(dx1, dy1, true);
5260
+ let angle2 = getAngleFromDelta(dx2, dy2, true);
5273
5261
  let deltaAngle = Math.abs(angle1 - angle2) * 180 / Math.PI;
5274
5262
 
5275
5263
  let angleDiff = Math.abs((deltaAngle % 180) - 90);
5276
5264
  let isRightAngle = angleDiff < 3;
5265
+ */
5277
5266
 
5278
5267
  let rx = 0;
5279
5268
  let ry = 0;
5280
- let ptC;
5269
+ let ptC = p0;
5270
+ let r1 = 0, r2 = 0;
5281
5271
 
5282
5272
  if (isRightAngle) {
5283
- // point between cps
5284
5273
 
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));
5274
+ if (isHorizontal1 && isVertical2) {
5275
+ ptC = { x: p0.x, y: p.y };
5276
+ r1 = Math.abs(p.x-p0.x);
5277
+ r2 = Math.abs(p.y-p0.y);
5278
+ }
5279
+ else if (isHorizontal2 && isVertical1) {
5280
+ ptC = { x: p.x, y: p0.y };
5281
+ r2 = Math.abs(p0.x-p.x);
5282
+ r1 = Math.abs(p0.y-p.y);
5283
+ }
5284
+
5285
+ /*
5286
+ if (r1 && r2) {
5288
5287
 
5289
- // assumed centroid
5290
- ptC = checkLineIntersection(p0, cp1_r, p, cp2_r, false);
5288
+ }
5291
5289
 
5292
- let pI = checkLineIntersection(p0, cp1, p, cp2, false);
5290
+ */
5293
5291
 
5294
- if (pI) {
5292
+ if (r1 && r2) {
5295
5293
 
5294
+ /*
5296
5295
  let r1 = getDistance(p0, pI);
5297
5296
  let r2 = getDistance(p, pI);
5297
+ */
5298
5298
 
5299
5299
  let rMax = +Math.max(r1, r2).toFixed(8);
5300
5300
  let rMin = +Math.min(r1, r2).toFixed(8);
@@ -5312,7 +5312,6 @@
5312
5312
  let circular = (100 / rx * Math.abs(rx - ry)) < 5;
5313
5313
 
5314
5314
  if (circular) {
5315
-
5316
5315
  rx = rMax;
5317
5316
  ry = rx;
5318
5317
  }
@@ -5338,7 +5337,7 @@
5338
5337
  arcSegArea = (Math.PI * (rx * ry)) / 4;
5339
5338
 
5340
5339
  // subtract polygon between start, end and center point
5341
- arcSegArea -= Math.abs(getPolygonArea([p0, p, pI]));
5340
+ arcSegArea -= Math.abs(getPolygonArea([p0, p, ptC]));
5342
5341
 
5343
5342
  let areaDiff = getRelativeAreaDiff(comArea, arcSegArea);
5344
5343
 
@@ -6408,59 +6407,50 @@
6408
6407
  pathData[pathData.length - 1].type.toLowerCase() === 'z';
6409
6408
 
6410
6409
  for (let c = 1, l = pathData.length; c < l; c++) {
6411
-
6412
6410
  let com = pathData[c];
6411
+ let { type, values } = com;
6413
6412
  let comN = pathData[c + 1] || pathData[l - 1];
6414
-
6413
+ let valuesN = comN.values;
6415
6414
  let p1 = comN.type.toLowerCase() === 'z' ? M : { x: comN.values[comN.values.length - 2], y: comN.values[comN.values.length - 1] };
6416
6415
 
6417
- let { type, values } = com;
6418
6416
  let valsL = values.slice(-2);
6419
6417
  p = type !== 'Z' ? { x: valsL[0], y: valsL[1] } : M;
6420
6418
 
6419
+ let nextBezier = type!==comN.type && (comN.type==='C' || comN.type==='Q');
6420
+
6421
6421
  let area = p1 ? getPolygonArea([p0, p, p1], true) : Infinity;
6422
6422
  let distSquare = getSquareDistance(p0, p1);
6423
6423
  let distMax = distSquare ? distSquare / 333 * tolerance : 0;
6424
6424
 
6425
6425
  let isFlat = area < distMax;
6426
-
6427
6426
  let isFlatBez = false;
6427
+ let cpts = [];
6428
6428
 
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);
6429
+ /**
6430
+ * type change
6431
+ * check flatness
6432
+ */
6433
+ if (nextBezier) {
6434
+ cpts = comN.type === 'C' ?
6435
+ [{ x: valuesN[0], y: valuesN[1] }, { x: valuesN[2], y: valuesN[3] }] :
6436
+ (comN.type === 'Q' ? [{ x: valuesN[0], y: valuesN[1] }] : []);
6448
6437
 
6449
- let thresh = (dx0 + dy0) * 0.1
6438
+ isFlat = commandIsFlat([p0, ...cpts, p], { tolerance });
6450
6439
 
6451
- if ( cross0 < thresh) {
6440
+ /*
6452
6441
 
6453
- isFlat = true
6442
+ if(!isFlatBez){
6443
+ pathDataN.push(com)
6444
+ continue
6454
6445
  }
6455
- }
6456
- */
6446
+ */
6457
6447
 
6458
- if (!flatBezierToLinetos && type === 'C') isFlat = false;
6448
+ }
6459
6449
 
6460
6450
  // convert flat beziers to linetos
6461
6451
  if (flatBezierToLinetos && (type === 'C' || type === 'Q')) {
6462
6452
 
6463
- let cpts = type === 'C' ?
6453
+ cpts = type === 'C' ?
6464
6454
  [{ x: values[0], y: values[1] }, { x: values[2], y: values[3] }] :
6465
6455
  (type === 'Q' ? [{ x: values[0], y: values[1] }] : []);
6466
6456
 
@@ -6474,10 +6464,13 @@
6474
6464
  }
6475
6465
  }
6476
6466
 
6477
- // colinear – exclude arcs (as always =) as semicircles won't have an area
6467
+ /**
6468
+ * colinear = simplification success
6469
+ * exclude arcs (as always =)
6470
+ * as semicircles won't have an area
6471
+ */
6478
6472
 
6479
6473
  if (isFlat && c < l - 1 && comN.type !== 'A' && (type === 'L' || (flatBezierToLinetos && isFlatBez))) {
6480
-
6481
6474
  continue;
6482
6475
  }
6483
6476
 
@@ -6961,9 +6954,7 @@
6961
6954
  if (comEx.length === 1) {
6962
6955
 
6963
6956
  comEx = comEx[0];
6964
-
6965
6957
  pathData[i + 1] = null;
6966
-
6967
6958
  pathData[i + 2].values = [comEx.cp1.x, comEx.cp1.y, comEx.cp2.x, comEx.cp2.y, comEx.p.x, comEx.p.y];
6968
6959
  pathData[i + 2].cp1 = comEx.cp1;
6969
6960
  pathData[i + 2].cp2 = comEx.cp2;
@@ -7143,7 +7134,16 @@
7143
7134
  return pts
7144
7135
  }
7145
7136
 
7137
+ if (pts.length && typeof pts[0] === 'string') {
7138
+ pts = pts.map(pt => {
7139
+ return toPointArray(pt.split(/,| /).filter(Boolean).map(Number))
7140
+ });
7141
+ pts = pts.flat(2);
7142
+
7143
+ }
7144
+
7146
7145
  if (flatten) pts = pts.flat(2);
7146
+
7147
7147
  let poly = toArray ? polyPtsToArray(pts) : polyArrayToObject(pts);
7148
7148
  return poly
7149
7149
  }
@@ -8503,7 +8503,7 @@
8503
8503
  styleProps.remove.push('transform');
8504
8504
 
8505
8505
  // scale props like stroke width or dash-array
8506
- styleProps = scaleProps(styleProps, { props: ['stroke-width', 'stroke-dasharray'], scale: scaleX });
8506
+ styleProps = scaleProps(styleProps, { props: ['stroke-width', 'stroke-dasharray', 'stroke-dashoffset'], scale: scaleX });
8507
8507
 
8508
8508
  } else {
8509
8509
  el.setAttribute('transform', transComponents.matrixAtt);
@@ -9050,6 +9050,38 @@
9050
9050
  });
9051
9051
  });
9052
9052
 
9053
+ /**
9054
+ * remove els and attributes
9055
+ */
9056
+
9057
+ // remove meta
9058
+ if (!allowMeta) removeElements.push('meta', 'metadata', 'desc', 'title');
9059
+
9060
+ if (removeClassNames) {
9061
+ removeSVGAttributes.push('class');
9062
+ removeElAttributes.push('class');
9063
+ }
9064
+
9065
+ if (removeIds) {
9066
+ removeSVGAttributes.push('id');
9067
+ removeElAttributes.push('id');
9068
+ }
9069
+
9070
+ // remove hidden elements
9071
+ removeHiddenSvgEls(svg);
9072
+
9073
+ // remove SVG elements
9074
+ removeSvgEls(svg, { removeElements, removeNameSpaced });
9075
+
9076
+ // remove SVG attributes
9077
+ removeSvgAtts(svg, removeSVGAttributes);
9078
+
9079
+ // remove SVG child element attributes
9080
+ removeSvgChildAtts(svg, removeElAttributes);
9081
+
9082
+ // general cleanup
9083
+ if (cleanupSVGAtts) cleanupSVGAttributes(svg, { removeIds, removeClassNames, removeDimensions, stylesToAttributes, allowMeta, allowAriaAtts, allowDataAtts });
9084
+
9053
9085
  // collect all elements' properties
9054
9086
  let svgElProps = [];
9055
9087
  let els = svg.querySelectorAll(`${renderedEls.join(', ')}`);
@@ -9070,14 +9102,15 @@
9070
9102
  let styleProps = parseStylesProperties(el, propOptions);
9071
9103
  let stylePropsFiltered = {};
9072
9104
 
9073
- // convert pathLength before transforming
9074
- if(convertTransforms || attributesToGroup) convertPathLength=true;
9105
+ // reset remove array
9106
+ remove = [];
9075
9107
 
9076
- if (convertPathLength ) {
9108
+ // convert pathLength before transforming
9109
+ if (convertTransforms || attributesToGroup) convertPathLength = true;
9077
9110
 
9111
+ if (convertPathLength) {
9078
9112
  styleProps = convertPathLengthAtt(el, { styleProps });
9079
9113
  remove = [...new Set([...remove, ...styleProps.remove])];
9080
-
9081
9114
  }
9082
9115
 
9083
9116
  // get parent styles
@@ -9127,37 +9160,7 @@
9127
9160
 
9128
9161
  remove = [...new Set([...remove, ...styleProps.remove])];
9129
9162
 
9130
- /**
9131
- * remove els and attributes
9132
- */
9133
-
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 });
9163
+
9161
9164
 
9162
9165
  // all relative units to absolute
9163
9166
  if (toAbsoluteUnits) {
@@ -9202,7 +9205,6 @@
9202
9205
  styleProps = setNormalizedTransformsToEl(el, { styleProps });
9203
9206
 
9204
9207
  remove = [...new Set([...remove, ...styleProps.remove])];
9205
-
9206
9208
  }
9207
9209
 
9208
9210
  /**
@@ -9227,13 +9229,6 @@
9227
9229
  */
9228
9230
  removeAtts(el, remove);
9229
9231
 
9230
- /*
9231
- for (let i = 0; i < remove.length; i++) {
9232
- let att = remove[i];
9233
- el.removeAttribute(att)
9234
- }
9235
- */
9236
-
9237
9232
  } // endof style processing
9238
9233
 
9239
9234
  /**
@@ -10524,71 +10519,6 @@
10524
10519
  return pathData
10525
10520
  }
10526
10521
 
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
10522
  function simplifyPolyRDP(pts, {quality = 0.9, width = 0, height = 0}={}) {
10593
10523
 
10594
10524
  /**
@@ -10783,860 +10713,817 @@
10783
10713
 
10784
10714
  }
10785
10715
 
10786
- function getPolyCentroid(pts) {
10716
+ function refineAdjacentPolyExtremes(pts = []) {
10787
10717
 
10788
10718
  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
-
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
10719
 
10812
- if (diffRel > 0.05) {
10813
- return false;
10814
- }
10720
+ let { x, y, width, height, top, bottom, left, right } = getPolyBBox(pts);
10721
+ let threshShort = (width + height) * 0.05;
10722
+ let thresh = (width + height) * 0.001;
10815
10723
 
10816
- }
10817
- return isRegular;
10818
- }
10724
+ let pt0 = pts[0];
10725
+ let ptLast = pts[l - 1];
10819
10726
 
10820
- function analyzePoly(pts, {
10821
- x = 0,
10822
- y = 0,
10823
- width = 0,
10824
- height = 0,
10825
- debug = false
10826
- } = {}) {
10727
+ /**
10728
+ * cleanup close path - almost vertical or horizontal
10729
+ * average start and end extremes
10730
+ */
10731
+ let dx = Math.abs(ptLast.x - pt0.x);
10732
+ let dy = Math.abs(ptLast.y - pt0.y);
10827
10733
 
10828
- let l = pts.length;
10734
+ if (dy < threshShort || dx < threshShort) {
10829
10735
 
10830
- // bounding box of this sub poly
10831
- let bb0 = getPolyBBox(pts);
10736
+ if (pt0.isExtreme && !pt0.isCorner) {
10832
10737
 
10833
- if (!width || !height) {
10834
- ({ x, y, width, height } = bb0);
10835
- }
10738
+ let xAv = (pt0.x + ptLast.x) * 0.5;
10739
+ let yAv = (pt0.y + ptLast.y) * 0.5;
10836
10740
 
10837
- let thresh = (width + height) * 0.01;
10741
+ pt0.x = xAv;
10742
+ pt0.y = yAv;
10838
10743
 
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];
10744
+ ptLast.x = xAv;
10745
+ ptLast.y = yAv;
10746
+ ptLast.isExtreme = true;
10844
10747
 
10845
- let area = getPolygonArea([p0, p1, p2], false);
10846
- p1.area = area;
10847
- p1.dist = getDistManhattan(p0, p1);
10848
- p1.idx = i;
10748
+ if (dy < thresh) {
10749
+ ptLast.tangentR.y = pt0.y;
10750
+ ptLast.tangentL.y = pt0.y;
10849
10751
 
10752
+ }
10753
+ if (dx < thresh) {
10754
+ ptLast.tangentR.x = pt0.x;
10755
+ ptLast.tangentL.x = pt0.x;
10756
+ }
10757
+ }
10850
10758
  }
10851
10759
 
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];
10760
+ for (let i = 1; i < l; i++) {
10761
+ i > 0 ? pts[i - 1] : pts[l - 1];
10855
10762
  let p1 = pts[i];
10856
10763
  let p2 = i < l - 1 ? pts[i + 1] : pts[l - 1];
10764
+ let dist = getDistManhattan(p1, p2);
10857
10765
 
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;
10766
+ let { isHorizontal, isVertical, isCorner, isLong, isExtreme, isSemiExtreme, isDirChange } = p1;
10874
10767
 
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
- }
10768
+ let extremes = [];
10879
10769
 
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;
10770
+ /*
10771
+ if(isExtreme && p0.isCorner && !isLong && !isCorner){
10772
+ isExtreme= false
10773
+ p1.isExtreme = false
10774
+ p1.isHorizontal = false
10775
+ p1.isVertical = false
10883
10776
 
10884
- if ((isHorizontal || isVertical)) {
10885
- p0.isExtreme = true;
10886
- isExtreme = true;
10777
+ continue;
10887
10778
  }
10779
+ */
10888
10780
 
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;
10781
+ /*
10782
+ if(isExtreme && p2.isCorner && !isLong && !isCorner && dist<threshShort*0.5){
10783
+ isExtreme= false
10784
+ p1.isExtreme = false
10785
+ p1.isHorizontal = false
10786
+ p1.isVertical = false
10896
10787
 
10788
+ if(isVertical){
10789
+ p2.tangentL.x = p2.x
10790
+ }
10791
+ if(isHorizontal){
10792
+ p2.tangentL.y = p2.y
10793
+ }
10794
+ continue;
10897
10795
  }
10796
+ */
10898
10797
 
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);
10798
+ if (isExtreme && !isCorner && p2.isExtreme) {
10799
+ extremes.push(p1);
10914
10800
 
10915
- let isCornerDelta = deltaAngleDeg > 10 && deltaAngleDeg < 160;
10916
- if (isCornerDelta) {
10801
+ for (let j = i + 1; j < l; j++) {
10802
+ let p2 = pts[j];
10803
+ dist = getDistManhattan(p1, p2);
10917
10804
 
10918
- isCorner = true;
10805
+ if (dist * 0.75 >= threshShort || p2.isCorner || p2.isDirChange) {
10806
+ break
10807
+ }
10808
+ if (p2.isExtreme && !p2.isDirChange && !p2.isCorner) {
10809
+ extremes.push(p2);
10810
+ }
10919
10811
  }
10920
10812
 
10921
- }
10813
+ if (extremes.length > 1) {
10922
10814
 
10923
- /*
10924
- let debug = true
10925
- if (debug) {
10815
+ // find best extreme according to angle
10816
+ let angleDiffMin = Infinity;
10926
10817
 
10927
- if ((isHorizontal || isVertical)) {
10928
- renderPoint(markers, p1, 'blue', '2%', '0.5')
10929
- renderPoint(markers, p0, 'blue', '2%', '0.5')
10930
- }
10818
+ let bestMatch = extremes[0];
10931
10819
 
10932
- if (isExtreme) {
10933
- renderPoint(markers, p1, 'cyan', '1.5%', '0.5')
10934
- }
10820
+
10821
+ extremes.forEach(pt => {
10935
10822
 
10936
- if (isDirChange) {
10937
- renderPoint(markers, p1, 'orange', '0.75%', '0.5')
10938
- }
10823
+ let angle = Math.abs(getAngleFromDelta(pt.dx2, pt.dy2, false)) * rad2Deg;
10824
+ let angleDiff = angle > 160 ? Math.abs(180 - angle) : (angle > 60 ? Math.abs(90 - angle) : angle);
10825
+ pt.angle = angle;
10826
+ pt.angleDiff = angleDiff;
10939
10827
 
10940
- if (isCorner) {
10941
- renderPoint(markers, p1, 'magenta', '2.75%', '0.5')
10942
- }
10943
- }
10944
- */
10828
+ if (angleDiff < angleDiffMin) {
10829
+ bestMatch = pt;
10830
+ angleDiffMin = angleDiff;
10945
10831
 
10946
- p1.isCorner = isCorner;
10947
- p1.isExtreme = isExtreme;
10948
- p1.isHorizontal = isHorizontal;
10949
- p1.isVertical = isVertical;
10950
- p1.isDirChange = isDirChange;
10832
+ }
10833
+ });
10951
10834
 
10952
- }
10835
+ let extremes2 = [];
10953
10836
 
10954
- // filter adjacent extremes
10955
- let pts1 = [];
10956
- let exclude = [];
10837
+ extremes.forEach((pt, i) => {
10957
10838
 
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;
10839
+ let isBestMatch = pt === bestMatch;
10963
10840
 
10964
- let extremes = [];
10841
+ if (isBestMatch) {
10965
10842
 
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;
10843
+ if (pt.isHorizontal) {
10844
+ pt.tangentL.y = pt.y;
10845
+ pt.tangentR.y = pt.y;
10846
+ }
10847
+ if (pt.isVertical) {
10848
+ pt.tangentL.x = pt.x;
10849
+ pt.tangentR.x = pt.x;
10850
+ }
10969
10851
 
10970
- if (has2nd && !has3rd) {
10971
- extremes.push(p, p1);
10852
+ // renderPoint(markers, pt, 'green', '3%', '0.5')
10972
10853
 
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
- }
10854
+ }
10855
+ else {
10981
10856
 
10982
- if (extremes.length) {
10983
- // average extreme
10857
+ if (bestMatch) {
10984
10858
 
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;
10859
+ if (!isBestMatch && (pt.x === bestMatch.x || pt.y === bestMatch.y)) {
10860
+ extremes2.push(pt);
10861
+ }
10862
+ pt.isExtreme = false;
10863
+ pt.isHorizontal = false;
10864
+ pt.isVertical = false;
10865
+ }
10987
10866
 
10988
- p.x = x;
10989
- p.y = y;
10867
+ }
10990
10868
 
10991
- i += extremes.length - 1;
10992
- }
10993
- }
10869
+ });
10994
10870
 
10995
- if (p.isExtreme || p.isCorner || p.isDirChange) {
10996
- exclude.push(pts1.length);
10997
- }
10871
+ // average coordinates
10872
+ if (extremes2.length) {
10873
+ bestMatch.x = (extremes2[0].x + bestMatch.x) * 0.5;
10874
+ bestMatch.y = (extremes2[0].y + bestMatch.y) * 0.5;
10998
10875
 
10999
- pts1.push(p);
10876
+ }
11000
10877
 
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;
10878
+ i += extremes.length;
10879
+ continue;
11009
10880
  }
11010
10881
  }
11011
10882
 
11012
- pts = pts1;
11013
10883
  }
11014
10884
 
11015
- return pts
11016
10885
  }
11017
10886
 
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];
10887
+ function cleanupPolyKeypoints(pts = []) {
11029
10888
 
11030
10889
  let l = pts.length;
11031
10890
 
11032
- // render
10891
+ getPolyBBox(pts);
10892
+
10893
+ pts[0];
10894
+
11033
10895
  for (let i = 1; i < l; i++) {
11034
- i > 0 ? pts[i] : pts[l - 1];
10896
+ i > 0 ? pts[i - 1] : pts[l - 1];
11035
10897
  let p1 = pts[i];
11036
10898
  i < l - 1 ? pts[i + 1] : pts[l - 1];
11037
10899
 
11038
- // start new chunk
11039
- // keepInflections && p1.isDirChange
11040
- if ((keepExtremes && p1.isExtreme || keepCorners && p1.isCorner )) {
11041
- idx++;
11042
- chunks.push([]);
11043
- }
10900
+ let { isHorizontal, isVertical, isCorner, isLong, isExtreme, isSemiExtreme, isDirChange } = p1;
10901
+ let offset = 0;
11044
10902
 
11045
- lastChunk = chunks[idx];
11046
- lastChunk.push(p1);
11047
- }
10903
+ if (!isSemiExtreme){
11048
10904
 
11049
- // test render
10905
+ continue
10906
+ }
11050
10907
 
11051
- return chunks;
11052
- }
10908
+ if (isSemiExtreme || isExtreme) {
10909
+ let semiExtremes = isSemiExtreme ? [p1] : [];
11053
10910
 
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
- } = {}) {
10911
+ for (let j = i + 1; j < l; j++) {
10912
+ let p2 = pts[j];
11064
10913
 
11065
- if (pts.length === 1) {
11066
- return [];
11067
- }
10914
+ if (!p2.isSemiExtreme || p2.isExtreme || p2.isCorner){
10915
+ break
10916
+ }
10917
+ semiExtremes.push(p2);
10918
+ }
11068
10919
 
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
- }
10920
+ if (semiExtremes.length > 1) {
11076
10921
 
11077
- // prevent bulging
10922
+ let semiExtremeMid = semiExtremes[Math.floor(semiExtremes.length*0.5)];
10923
+ let p1_1 = semiExtremes[0];
10924
+ let p2_1 = semiExtremes[semiExtremes.length - 1];
10925
+ let ptI = checkLineIntersection(p1_1, p1_1.tangentR, p2_1, p2_1.tangentL, false, true);
11078
10926
 
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);
10927
+ semiExtremes.forEach(pt=>{
10928
+ pt.isSemiExtreme=false;
10929
+ });
10930
+ semiExtremeMid.isSemiExtreme=true;
11083
10931
 
11084
- // create pathdata
11085
- let pathData = bezierPtsToPathData(beziers);
11086
- let cp1, cp2;
10932
+ // interpolate mid point
10933
+ if (ptI) {
10934
+ let pI_1 = interpolate(p1_1, ptI, 0.5);
10935
+ let pI_2 = interpolate(p2_1, ptI, 0.5);
10936
+ let pI_3 = interpolate(pI_2, pI_1, 0.5);
11087
10937
 
11088
- adjustCpts = false;
10938
+ semiExtremeMid.x = pI_3.x;
10939
+ semiExtremeMid.y = pI_3.y;
10940
+ semiExtremeMid.tangentL = pI_1;
10941
+ semiExtremeMid.tangentR = pI_2;
11089
10942
 
11090
- adjustCpts = true;
10943
+ i += offset;
10944
+ continue
10945
+ }
11091
10946
 
11092
- if (adjustCpts) {
10947
+ }
10948
+ }
10949
+ // find significant of same type
11093
10950
 
11094
- let len2 = pathData.length;
11095
- let com1 = pathData[0];
10951
+ }
11096
10952
 
11097
- // last cubic segment
11098
- let com2 = pathData[len2 - 1];
10953
+ /*
10954
+ // update index
10955
+ ptsClean.forEach((pt, i) => {
10956
+ pt.idx = i
10957
+ })
10958
+ */
11099
10959
 
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;
10960
+ return pts;
11103
10961
 
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
- }
10962
+ }
11110
10963
 
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;
10964
+ function adjustTangentAngle(cp, p0, p1, p2) {
10965
+ let ang1 = getAngle(p0, p1);
10966
+ let ang2 = getAngle(p0, p2);
10967
+ let angDiff = (ang2 - ang1);
11114
10968
 
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
- }
10969
+ let f = 0.666;
10970
+ f = 1;
11121
10971
 
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
- */
10972
+ cp = rotatePoint(cp, p0.x, p0.y, -angDiff * f);
10973
+ return cp
10974
+ }
11132
10975
 
11133
- }
10976
+ function getTangents(pts = [], {
10977
+ x = 0,
10978
+ y = 0,
10979
+ width = 0,
10980
+ height = 0,
10981
+ debug = false,
10982
+ closed=false,
10983
+ } = {}) {
11134
10984
 
11135
- return pathData
11136
- }
10985
+ let l = pts.length;
11137
10986
 
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) {
10987
+ // bounding box of this sub poly
10988
+ if (!width || !height) {
10989
+ ({ x, y, width, height } = getPolyBBox(pts));
10990
+ }
11144
10991
 
11145
- let MaxIterations = 20;
11146
- let bezCurve;
10992
+ // threshold for horizontal or vertical detection
11147
10993
 
11148
- /*
10994
+ for (let i = 0; i < l; i++) {
10995
+ let p0 = i > 0 ? pts[i - 1] : pts[l - 1];
10996
+ let p1 = pts[i];
10997
+ let p2 = i < l - 1 ? pts[i + 1] : pts[l - 1];
10998
+ let p3 = i < l - 1 ? pts[i + 2] : pts[l - 1];
11149
10999
 
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
- */
11000
+ let { isHorizontal, isVertical, isCorner, isLong, isExtreme, isSemiExtreme, isDirChange } = p1;
11156
11001
 
11157
- let u = chordLengthParameterize(pts);
11158
- let _generateAndReport = generateAndReport(pts, u, u, leftTangent, rightTangent);
11002
+ // default
11003
+ let tangentL = { x: p1.x - p1.dx2 * 0.5, y: p1.y - p1.dy2 * 0.5 };
11004
+ let tangentR = { x: p1.x + p1.dx2 * 0.5, y: p1.y + p1.dy2 * 0.5 };
11159
11005
 
11160
- bezCurve = _generateAndReport[0];
11006
+ // average first tangent
11007
+ if(i===0){
11008
+ tangentR = adjustTangentAngle(p2, p1, p2, p3);
11009
+ }
11161
11010
 
11162
- let maxError = _generateAndReport[1];
11163
- let splitPoint = _generateAndReport[2];
11011
+ /**
11012
+ * add left and right tangents
11013
+ * for later curve fitting
11014
+ */
11164
11015
 
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;
11016
+ if (isHorizontal && !isCorner) {
11017
+ tangentL = { x: p1.x - p1.dx2*0.5, y: p1.y };
11018
+ tangentR = { x: p1.x + p1.dx2*0.5, y: p1.y };
11019
+ }
11020
+ else if (isVertical) {
11021
+ tangentL = { x: p1.x , y: p1.y - p1.dy2*0.5 };
11022
+ tangentR = { x: p1.x , y: p1.y + p1.dy2*0.5 };
11172
11023
  }
11173
- }
11174
11024
 
11175
- if (maxError === 0 || maxError < error) {
11176
- return [bezCurve];
11177
- }
11025
+ if (!isExtreme && p1.isLong) {
11026
+ tangentL = { x: p1.x - p1.dx*0.5, y: p1.y - p1.dy*0.5 };
11027
+ tangentR = { x: p1.x + p1.dx*0.5, y: p1.y + p1.dy*0.5 };
11028
+ }
11178
11029
 
11179
- if (maxError < error * error) {
11030
+ /*
11180
11031
 
11181
- let uPrime = u;
11182
- let prevErr = maxError;
11183
- let prevSplit = splitPoint;
11032
+ if (isDirChange && !isCorner && !isExtreme) {
11033
+ p1.tangentL = { x: p1.x-p1.dx2*0.5, y: p1.y-p1.dy2*0.5 }
11034
+ p1.tangentR = { x: p1.x+p1.dx2*0.5, y: p1.y+p1.dy2*0.5 }
11035
+ }
11036
+ */
11184
11037
 
11185
- for (let i = 0; i < MaxIterations; i++) {
11038
+ if (isCorner) {
11186
11039
 
11187
- uPrime = reparameterize(bezCurve, pts, uPrime);
11040
+ tangentL = {x:p0.x, y:p0.y};
11041
+ tangentR = {x:p2.x, y:p2.y};
11188
11042
 
11189
- let _generateAndReport2 = generateAndReport(pts, u, uPrime, leftTangent, rightTangent);
11043
+ let p0_1 = pts[i - 2] ? pts[i - 2] : pts[l - 1];
11190
11044
 
11191
- bezCurve = _generateAndReport2[0];
11192
- maxError = _generateAndReport2[1];
11193
- splitPoint = _generateAndReport2[2];
11045
+ let p2_1 = pts[i + 2] ? pts[i + 2] : pts[1];
11194
11046
 
11195
- if (maxError < error) {
11196
- return [bezCurve];
11047
+ // adjust angle
11048
+ if (!p0.isCorner) {
11049
+ tangentL = adjustTangentAngle(p0, p1, p0, p0_1);
11197
11050
  }
11198
11051
 
11199
- else if (splitPoint === prevSplit) {
11200
- let errChange = maxError / prevErr;
11201
- if (errChange > .9999 && errChange < 1.0001) {
11202
- break;
11203
- }
11052
+ if (!p2.isCorner) {
11053
+ tangentR = adjustTangentAngle(tangentR, p1, p2, p2_1);
11204
11054
  }
11205
11055
 
11206
- prevErr = maxError;
11207
- prevSplit = splitPoint;
11056
+ /*
11057
+ renderPoint(markers, p0, 'darkblue', '0.75%', '0.5')
11058
+ // renderPoint(markers, p0_1, 'blue', '0.5%')
11059
+ renderPoint(markers, tangentL, 'blue', '0.5%', '0.5')
11060
+ renderPoint(markers, tangentR, 'blue', '0.5%', '0.5')
11061
+ */
11062
+
11208
11063
  }
11209
- }
11210
11064
 
11211
- let beziers = [];
11212
- let centerVector = subtract(pts[splitPoint - 1], pts[splitPoint + 1]);
11065
+ p1.tangentL = tangentL;
11066
+ p1.tangentR = tangentR;
11067
+
11068
+ /*
11069
+ debug = true
11070
+ if(debug){
11071
+ if (isCorner || isSemiExtreme || isDirChange || isExtreme) {
11072
+ renderPoint(markers, p1.tangentL, 'darkred', '0.5%')
11073
+ renderPoint(markers, p1.tangentR, 'darkblue', '0.5%')
11074
+ }
11075
+ }
11076
+ */
11213
11077
 
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
11078
  }
11220
11079
 
11221
- let toCenterTangent = normalize(centerVector);
11080
+ }
11222
11081
 
11223
- let fromCenterTangent = mulItems(toCenterTangent, -1);
11082
+ function getPolyCentroid(pts) {
11224
11083
 
11225
- if (pts.length === 3) ;
11084
+ let l = pts.length;
11085
+ let x = 0, y = 0;
11086
+ for (let i = 0; l && i < l; i++) {
11087
+ let pt = pts[i];
11088
+ x += pt.x;
11089
+ y += pt.y;
11090
+ }
11226
11091
 
11227
- beziers.push(
11228
- ...fitCubic(pts.slice(0, splitPoint + 1), leftTangent, toCenterTangent, error, keepCorners),
11229
- ...fitCubic(pts.slice(splitPoint), fromCenterTangent, rightTangent, error, keepCorners)
11230
- );
11092
+ let centroid = { x: x / l, y: y / l };
11093
+ return centroid
11231
11094
 
11232
- return beziers;
11233
11095
  }
11234
11096
 
11235
- /**
11236
- * Use least-squares method to find Bezier control pts for region.
11237
- */
11238
- function generateBezier(pts, parameters, leftTangent, rightTangent) {
11097
+ function detectRegularPolygon(pts, centroid = { x: 0, y: 0 }) {
11098
+ let rSq = getSquareDistance(pts[0], centroid);
11099
+ let isRegular = true;
11239
11100
 
11240
- let a, tmp, u, ux, firstPoint = pts[0], lastPoint = pts[pts.length - 1];
11101
+ for (let i = 1, l = pts.length; i < l; i++) {
11102
+ let pt1 = pts[i];
11103
+ let dist = getSquareDistance(pt1, centroid);
11241
11104
 
11242
- let bezCurve = [firstPoint, null, null, lastPoint];
11243
- let A = zeros_Xx2x2(parameters.length);
11244
- let len = parameters.length;
11105
+ let diff = Math.abs(rSq - dist);
11106
+ let diffRel = diff / rSq;
11245
11107
 
11246
- for (let i = 0; i < len; i++) {
11247
- u = parameters[i];
11248
- ux = 1 - u;
11249
- a = A[i];
11108
+ if (diffRel > 0.05) {
11109
+ return false;
11110
+ }
11250
11111
 
11251
- a[0] = mulItems(leftTangent, 3 * u * (ux * ux));
11252
- a[1] = mulItems(rightTangent, 3 * ux * (u * u));
11253
11112
  }
11113
+ return isRegular;
11114
+ }
11115
+
11116
+ function analyzePoly(pts, {
11117
+ x = 0,
11118
+ y = 0,
11119
+ width = 0,
11120
+ height = 0,
11121
+ debug = false
11122
+ } = {}) {
11254
11123
 
11255
- let C = [[0, 0], [0, 0]];
11256
- let X = [0, 0];
11257
11124
  let l = pts.length;
11125
+ let left = x;
11126
+ let top = y;
11127
+ let right = x + width;
11128
+ let bottom = y + height;
11258
11129
 
11259
- for (let i = 0; i < l; i++) {
11260
- u = parameters[i];
11261
- a = A[i];
11130
+ if (!width || !height) {
11131
+ ({ x, y, width, height, top, bottom, left, right } = getPolyBBox(pts));
11132
+ }
11262
11133
 
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]);
11134
+ // round
11135
+ [x, y, width, height, top, bottom, left, right] = [x, y, width, height, top, bottom, left, right].map(val => +val.toFixed(8));
11267
11136
 
11268
- tmp = subtract(pts[i], pointAtT([firstPoint, firstPoint, lastPoint, lastPoint], u));
11137
+ // bounding box of this sub poly
11138
+ let bb0 = { x, y, top, left, width, height, right, bottom };
11269
11139
 
11270
- X[0] += dot(a[0], tmp);
11271
- X[1] += dot(a[1], tmp);
11272
- }
11140
+ let thresh = (width + height) * 0.01;
11273
11141
 
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];
11142
+ // threshold for horizontal or vertical detection
11143
+ let thresh2 = thresh * 0.75;
11277
11144
 
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;
11145
+ let dims = [];
11282
11146
 
11283
- if (alpha_l < epsilon || alpha_r < epsilon) {
11147
+ /*
11148
+ pts.forEach(pt=>{
11149
+ renderPoint(markers, pt, 'red', '2.5%')
11150
+ })
11151
+ */
11152
+
11153
+ /**
11154
+ * 1st run:
11155
+ * collect more details
11156
+ * area for sign change detection
11157
+ * deltas and distances
11158
+ */
11159
+ for (let i = 0; i < l; i++) {
11160
+ let p0 = i > 0 ? pts[i - 1] : pts[l - 1];
11161
+ let p1 = pts[i];
11162
+ let p2 = i < l - 1 ? pts[i + 1] : pts[l - 1];
11163
+
11164
+ let area = getPolygonArea([p0, p1, p2], false);
11165
+ let dx = i > 0 ? +(p1.x - p0.x).toFixed(7) : 0;
11166
+ let dy = i > 0 ? +(p1.y - p0.y).toFixed(7) : 0;
11167
+
11168
+ let dx2 = +(p2.x - p0.x).toFixed(7);
11169
+ let dy2 = +(p2.y - p0.y).toFixed(7);
11170
+
11171
+ p1.area = area;
11172
+ p1.dist = i > 0 ? getDistManhattan(p0, p1) : 0;
11173
+ // add dist for long/short segment detection
11174
+ dims.push(p1.dist);
11175
+ p1.idx = i;
11176
+ p1.dx = dx;
11177
+ p1.dy = dy;
11178
+ p1.dx2 = dx2;
11179
+ p1.dy2 = dy2;
11284
11180
 
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
11181
  }
11292
11182
 
11293
- return bezCurve;
11294
- }
11183
+ /**
11184
+ * find average segment length
11185
+ * for long/short segment detection
11186
+ */
11187
+ dims = dims.filter(Boolean).sort((a, b) => a - b);
11188
+ let lenD = dims.length;
11189
+ let dimMin = dims[0];
11190
+ dims[lenD - 1];
11295
11191
 
11296
- function generateAndReport(pts, paramsOrig, paramsPrime, leftTangent, rightTangent) {
11297
- let bezCurve, maxError, splitPoint;
11192
+ let dimAv = dims.reduce((a, b) => a + b, 0) / lenD;
11193
+ let dimShort = (dimMin + dimAv) * 0.5;
11194
+ let dimLong = dimAv * 2;
11298
11195
 
11299
- bezCurve = generateBezier(pts, paramsPrime, leftTangent, rightTangent);
11300
- let _computeMaxError = computeMaxError(pts, bezCurve, paramsOrig);
11196
+ /*
11197
+ // round to adjust for minor deviations
11198
+ let idx_q = Math.ceil(lenD * 0.25);
11199
+ let dim_mid = dims[Math.floor(lenD * 0.5)]
11200
+ let dims_min = dims.slice(0, Math.floor(lenD * 0.25));
11201
+ let dim_min = ((dims_min.reduce((a, b) => a + b, 0) / idx_q) + dim_mid) * 0.5;
11301
11202
 
11302
- maxError = _computeMaxError[0];
11303
- splitPoint = _computeMaxError[1];
11203
+ let threshold = 75
11204
+ let decimalsAuto = dim_min > threshold * 1.5 ? 0 : Math.floor(threshold / dim_min).toString().length
11304
11205
 
11305
- return [bezCurve, maxError, splitPoint];
11306
- }
11206
+ // clamp
11207
+ decimalsAuto = Math.min(Math.max(0, decimalsAuto), 8)
11307
11208
 
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
- */
11209
+ pts = roundPoly(pts, 2)
11210
+ console.log(pts);
11211
+ */
11319
11212
 
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)
11213
+ /**
11214
+ * analyze topology:
11215
+ * find significant commands:
11216
+ * extremes, inflections etc.
11217
+ */
11218
+ for (let i = 0; i < l; i++) {
11324
11219
 
11325
- // Calculate q(u) - point (the vector from target to curve point)
11220
+ let p0 = i > 0 ? pts[i - 1] : pts[l - 1];
11221
+ let p1 = pts[i];
11222
+ let p2 = i < l - 1 ? pts[i + 1] : pts[l - 1];
11326
11223
 
11327
- let q = pointAtT(bez, u);
11224
+ // convert area to absolute for flatness checks
11225
+ let area1 = Math.abs(p1.area);
11226
+ let isCorner = false;
11227
+ let isSemiExtreme = false;
11228
+ let isShort = false;
11229
+ let isLong = false;
11328
11230
 
11329
- let dx = q.x - point.x;
11330
- let dy = q.y - point.y;
11231
+ /**
11232
+ * detect short or long
11233
+ */
11234
+ if (p1.dist > dimLong) {
11235
+ isLong = true;
11236
+ }
11331
11237
 
11332
- // First derivative (tangent vector at u)
11333
- let qp = bezierQprime(bez, u);
11238
+ if (p1.dist < dimShort) {
11239
+ isShort = true;
11240
+ }
11334
11241
 
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];
11242
+ let flat = !p1.area || area1 < thresh;
11338
11243
 
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];
11244
+ /**
11245
+ * check extremes
11246
+ */
11247
+ let isExtreme = false;
11342
11248
 
11343
- // Second derivative
11344
- let qpp = bezierQprime(bez, u, true);
11249
+ // 1. total extreme
11250
+ let isTop = p1.y === bb0.top;
11251
+ let isBottom = p1.y === bb0.bottom;
11252
+ let isLeft = p1.x === bb0.left;
11253
+ let isRight = p1.x === bb0.right;
11345
11254
 
11346
- // Second part: 2 * (q(u)-point) · q''(u)
11347
- let secondPart = 2 * (dx * qpp[0] + dy * qpp[1]);
11255
+ if (isTop || isBottom || isLeft || isRight) {
11256
+ isExtreme = true;
11348
11257
 
11349
- let denominator = qpLenSq + secondPart;
11258
+ }
11350
11259
 
11351
- if (Math.abs(denominator) < 1e-10) { // Avoid division by zero
11352
- return u;
11353
- }
11260
+ // 1.2 horizontal or vertical
11261
+ /*
11262
+ let isHorizontal = isTop || isBottom || (p1.y === p0.y && p1.x !== p0.x);
11263
+ let isVertical = isLeft || isRight || (p1.x === p0.x && p1.y !== p0.y)
11354
11264
 
11355
- // Newton-Raphson step: u_new = u - f(u)/f'(u)
11356
- return u - numerator / denominator;
11357
- }
11265
+ if ((isHorizontal || isVertical)) {
11358
11266
 
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;
11267
+ let diffX = Math.abs(p0.x - p1.x)
11268
+ let diffY = Math.abs(p0.y - p1.y)
11369
11269
 
11370
- for (let i = 0; i < l; i++) {
11371
- p = pts[i];
11270
+ if (isLong) {
11271
+ }
11372
11272
 
11373
- currU = prevU + getDistance(p, p0, false);
11374
- u.push(currU);
11375
- prevU = currU;
11273
+ if (isLong && (diffY < thresh2) && diffX > thresh) {
11274
+ p0.isExtreme = true;
11275
+ p0.isHorizontal = true;
11276
+ }
11277
+ else if (isLong && (diffX < thresh2) && diffY > thresh) {
11278
+ p0.isExtreme = true;
11279
+ p0.isVertical = true;
11280
+ }
11376
11281
 
11377
- p0 = p;
11378
- }
11282
+ isExtreme = true
11283
+ }
11284
+ */
11379
11285
 
11380
- u = u.map(function (x) {
11381
- return x / prevU;
11382
- });
11286
+ let dx = Math.abs(p0.x - p1.x);
11287
+ let dy = Math.abs(p0.y - p1.y);
11383
11288
 
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;
11289
+ let vh_thresh = thresh * 0.05;
11290
+ // vh_thresh = thresh * 0.25
11291
+ let isHorizontal = isTop || isBottom || (p1.y === p0.y && p1.x !== p0.x) || (dy <= vh_thresh);
11292
+ let isVertical = (isLeft || isRight || (p1.x === p0.x && p1.y !== p0.y) || (dx <= vh_thresh));
11394
11293
 
11395
- maxDist = 0;
11396
- splitPoint = Math.floor(pts.length * 0.5);
11294
+ // renderPoint(markers, p1, 'red', '0.5%')
11397
11295
 
11398
- let t_distMap = mapTtoRelativeDistances(bez, 10);
11399
- let l = pts.length;
11400
- let ptOnPath = null;
11296
+ if (p1.y === p0.y) ;
11401
11297
 
11402
- for (i = 0; i < l; i++) {
11403
- point = pts[i];
11298
+ if ((isHorizontal || isVertical)) {
11404
11299
 
11405
- t = find_t(parameters[i], t_distMap, 10);
11300
+ if (isLong && isHorizontal) {
11301
+ p0.isExtreme = true;
11302
+ p0.isHorizontal = true;
11406
11303
 
11407
- ptOnPath = pointAtT(bez, t);
11408
- dist = getSquareDistance(ptOnPath, point);
11304
+ }
11305
+ else if (isLong && isVertical) {
11306
+ p0.isExtreme = true;
11307
+ p0.isVertical = true;
11308
+ }
11409
11309
 
11410
- /*
11411
- console.log('v', v);
11412
- v = subtract(pointAtT(bez, t), point);
11413
- dist = v.x * v.x + v.y * v.y;
11414
- */
11310
+ isExtreme = true;
11311
+ }
11415
11312
 
11416
- if (dist > maxDist) {
11313
+ // 1.3 is local or absolute extreme
11314
+ let bb = getPolyBBox([p0, p2]); // local bb
11315
+ let { left, right, top, bottom } = bb;
11316
+
11317
+ let extremeLocal = (p1.x < left || p1.x > right || p1.y < top || p1.y > bottom);
11318
+ if (!isExtreme && extremeLocal) {
11319
+ isExtreme = true;
11417
11320
 
11418
- maxDist = dist;
11419
- splitPoint = i;
11420
11321
  }
11421
- }
11422
11322
 
11423
- return [maxDist, splitPoint];
11424
- }
11323
+ /**
11324
+ * 2. sign changes
11325
+ */
11326
+ let signChange = (p0.area < 0 && p1.area > 0) || (p0.area > 0 && p1.area < 0);
11327
+ let isDirChange = signChange && !flat && !p0.isDirChange && isLong;
11425
11328
 
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;
11329
+ /**
11330
+ * 3. corners
11331
+ */
11431
11332
 
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
- }
11333
+ if (isExtreme) {
11438
11334
 
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) {
11335
+ let delta = getDeltaAngle(p1, p2, p0);
11336
+ let { deltaAngleDeg } = delta;
11337
+ deltaAngleDeg = Math.abs(deltaAngleDeg);
11445
11338
 
11446
- if (param < 0) {
11447
- return 0;
11448
- }
11449
- if (param > 1) {
11450
- return 1;
11451
- }
11339
+ let isCornerDelta = deltaAngleDeg > 10 && deltaAngleDeg < 160;
11340
+ if (isCornerDelta) {
11452
11341
 
11453
- let lenMax, lenMin, tMax, tMin, t;
11342
+ isCorner = true;
11454
11343
 
11455
- for (let i = 1; i <= B_parts; i++) {
11344
+ }
11456
11345
 
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
11346
  }
11465
- }
11466
- return t;
11467
- }
11468
11347
 
11469
- function areaDeviationTooLarge(pts, bezCurve) {
11348
+ if (isExtreme && !isCorner) {
11470
11349
 
11471
- let split = 4;
11472
- let step = 1 / split;
11473
- let l = pts.length;
11350
+ if ((Math.abs(p1.dy2) < thresh2) && Math.abs(p1.dx2) > thresh) {
11351
+ isHorizontal = true;
11352
+ }
11353
+ else if (Math.abs(p1.dx2) < thresh2 && Math.abs(p1.dy2) > thresh) {
11354
+ isVertical = true;
11355
+ }
11356
+ }
11474
11357
 
11475
- // create polygon from curve candidate
11476
- let poly = [bezCurve[0]];
11477
- let pt;
11358
+ /**
11359
+ * semi extremes
11360
+ * ~ 45deg tangent
11361
+ */
11362
+ let diffX = Math.abs(p1.dx2);
11363
+ let diffY = Math.abs(p1.dy2);
11478
11364
 
11479
- for (let i = 1; i < split; i++) {
11480
- let t = step * i;
11481
- pt = pointAtT(bezCurve, t);
11482
- poly.push(pt);
11483
- }
11365
+ let ratDelta = (diffX / diffY);
11366
+
11367
+ if (ratDelta > 0.8 && ratDelta <= 1.2) {
11368
+ isSemiExtreme = true;
11369
+ }
11484
11370
 
11485
- poly.push(bezCurve[bezCurve.length - 1]);
11371
+ p1.isCorner = isCorner;
11372
+ p1.isExtreme = isExtreme;
11373
+ p1.isSemiExtreme = isSemiExtreme;
11374
+ p1.isLong = isLong;
11375
+ p1.isShort = isShort;
11486
11376
 
11487
- // Original area
11488
- let polyArea = getPolygonArea(pts, false);
11377
+ p1.isHorizontal = isHorizontal;
11378
+ p1.isVertical = isVertical;
11379
+ p1.isDirChange = isDirChange;
11489
11380
 
11490
- // flat line
11491
- if (!polyArea && pts.length === 2) {
11492
- polyArea = getSquareDistance(pts[0], pts[1]) * 0.01;
11493
11381
  }
11494
11382
 
11495
- let curveArea = getPolygonArea(poly, false);
11496
- let rat = curveArea / polyArea;
11383
+ // add tangents
11384
+ getTangents(pts, { x, y, width, height });
11497
11385
 
11498
- let isBulged = rat > 1.1;
11386
+ refineAdjacentPolyExtremes(pts);
11499
11387
 
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);
11388
+ // filter adjacent significant points
11389
+ cleanupPolyKeypoints(pts);
11508
11390
 
11509
- bezierNew = [pts[0], cp1, cp2, p]
11391
+ renderPolyTopology(pts);
11510
11392
 
11511
-
11393
+ return pts
11394
+ }
11512
11395
 
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
- */
11396
+ /*
11518
11397
 
11519
- bezCurve = [pts[0], ptMid, ptMid, p];
11520
- bezierNew = bezCurve;
11521
- }
11398
+ */
11522
11399
 
11523
- return { isBulged, bezierNew };
11524
- }
11400
+ // just for visualization
11401
+ function renderPolyTopology(pts, showTangents = true) {
11525
11402
 
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);
11403
+ let l = pts.length;
11534
11404
 
11535
- if (length === 0) return { x: 0, y: 0 };
11536
- return { x: dx / length, y: dy / length };
11537
- }
11405
+ // render
11406
+ for (let i = 0; i < l; i++) {
11407
+ i > 0 ? pts[i - 1] : pts[l - 1];
11408
+ let p1 = pts[i];
11409
+ i < l - 1 ? pts[i + 1] : pts[l - 1];
11538
11410
 
11539
- /**
11540
- * Math helpers
11541
- */
11411
+ if (p1.isDirChange) {
11412
+ renderPoint(markers, p1, 'orange', '1%', '0.75');
11413
+ }
11542
11414
 
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
- }
11415
+ if (p1.isSemiExtreme) {
11416
+ renderPoint(markers, p1, 'red', '1%', '0.5');
11417
+ }
11547
11418
 
11548
- function addArrays(a, b) {
11549
- return { x: a.x + b.x, y: a.y + b.y };
11550
- }
11419
+ /*
11420
+ if (p1.isLong && (p1.isDirChange || p1.isExtreme || p1.isCorner || p1.isSemiExtreme)) {
11421
+ renderPoint(markers, p1, 'green', '1.5%', '0.5')
11422
+ }
11423
+ */
11551
11424
 
11552
- function mulItems(v, s) {
11553
- return { x: v.x * s, y: v.y * s };
11554
- }
11425
+ if (p1.isDirChange) {
11426
+ renderPoint(markers, p1, 'green', '1.5%', '0.5');
11427
+ }
11555
11428
 
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
- }
11429
+ if (p1.isExtreme) {
11430
+ renderPoint(markers, p1, 'cyan', '1%', '0.5');
11431
+ }
11560
11432
 
11561
- function dot(a, b) {
11562
- return a.x * b.x + a.y * b.y;
11563
- }
11433
+ if (p1.isHorizontal) {
11434
+ renderPoint(markers, p1, 'blue', '1.5%', '0.25');
11435
+ }
11436
+
11437
+ if (p1.isVertical) {
11438
+ renderPoint(markers, p1, 'purple', '1.5%', '0.25');
11439
+ }
11440
+
11441
+ if (p1.isCorner) {
11442
+ renderPoint(markers, p1, 'magenta', '1%', '1');
11443
+ }
11444
+
11445
+ if (showTangents && (p1.isCorner || p1.isSemiExtreme || p1.isDirChange || p1.isExtreme)) {
11446
+ renderPoint(markers, p1.tangentL, 'darkred', '0.5%');
11447
+ renderPoint(markers, p1.tangentR, 'darkblue', '0.5%');
11448
+
11449
+ /*
11450
+ if (p1.isDirChange) {
11451
+ renderPoint(markers, p1.tangentL, 'darkred', '1.5%')
11452
+ renderPoint(markers, p1.tangentR, 'darkblue', '1.5%')
11453
+ }
11454
+ */
11455
+
11456
+ }
11564
11457
 
11565
- function zeros_Xx2x2(x) {
11566
- let zs = [];
11567
- while (x--) {
11568
- zs.push([0, 0]);
11569
11458
  }
11570
- return zs;
11459
+
11571
11460
  }
11572
11461
 
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;
11462
+ function getPolyChunks(pts,
11463
+ { closed = true,
11464
+ keepCorners = true,
11465
+ keepExtremes = true,
11466
+ keepInflections = false
11467
+ } = {}
11468
+ ) {
11469
+ let chunks = [[pts[0]]];
11470
+
11471
+ let idx = 0;
11472
+ let lastChunk = chunks[idx];
11581
11473
 
11582
- if (second) {
11583
- dx = 6 * mt * (cp2.x - 2 * cp1.x + p0.x) +
11584
- 6 * t * (p1.x - 2 * cp2.x + cp1.x);
11474
+ let l = pts.length;
11585
11475
 
11586
- dy = 6 * mt * (cp2.y - 2 * cp1.y + p0.y) +
11587
- 6 * t * (p1.y - 2 * cp2.y + cp1.y);
11476
+ // render
11477
+ for (let i = 1; i < l; i++) {
11478
+ i > 0 ? pts[i] : pts[l - 1];
11479
+ let p1 = pts[i];
11480
+ i < l - 1 ? pts[i + 1] : pts[l - 1];
11588
11481
 
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);
11482
+ // start new chunk
11483
+ // keepInflections && p1.isDirChange
11484
+ if ((keepExtremes && p1.isExtreme || keepCorners && p1.isCorner ||
11485
+ (keepInflections && p1.isDirChange && !p1.isExtreme && !p1.isCorner )
11486
+ )) {
11487
+ idx++;
11488
+ chunks.push([]);
11489
+ }
11593
11490
 
11594
- dy = 3 * mt2 * (cp1.y - p0.y) +
11595
- 6 * mt * t * (cp2.y - cp1.y) +
11596
- 3 * t2 * (p1.y - cp2.y);
11491
+ lastChunk = chunks[idx];
11492
+ lastChunk.push(p1);
11597
11493
  }
11598
11494
 
11599
- return [dx, dy];
11600
- }
11495
+ // test render
11601
11496
 
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
11497
+ return chunks;
11608
11498
  }
11609
11499
 
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');
11500
+ function removeCoincidingVertices(pts = []) {
11501
+ let l = pts.length;
11502
+ if (!l) return pts;
11616
11503
 
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];
11504
+ let ptsN = [pts[0]];
11505
+ let pt1, pt2;
11620
11506
 
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
- );
11507
+ for (let i = 1; i < l; i++) {
11508
+ pt1 = pts[i - 1];
11509
+ pt2 = pts[i];
11627
11510
 
11628
- let com = { type, values };
11629
- pathData.push(com);
11630
- });
11511
+ /**
11512
+ * 1. Skip zero-length segments
11513
+ */
11514
+ if (pt1.x === pt2.x && pt1.y === pt2.y) {
11515
+ continue;
11516
+ }
11517
+ ptsN.push(pt2);
11518
+ }
11519
+ return ptsN
11631
11520
 
11632
- return pathData
11633
11521
  }
11634
11522
 
11635
- function simplifyRC(pts, quality = 1, shiftStart = true) {
11636
-
11637
- if (pts.length < 4) return pts;
11523
+ function simplifyRC(pts = [], quality = 1, shiftStart = true) {
11638
11524
 
11639
11525
  let l = pts.length;
11526
+ if (l < 4) return pts;
11640
11527
 
11641
11528
  // starting point
11642
11529
  let M = pts[0];
@@ -11701,7 +11588,7 @@
11701
11588
  let thresh = getSquareDistance(pt0, pt2) * 0.005;
11702
11589
 
11703
11590
  // flat
11704
- if ( area <= thresh && i<l-1) {
11591
+ if (area <= thresh && i < l - 1) {
11705
11592
 
11706
11593
  pt0 = pt1;
11707
11594
  continue
@@ -11722,10 +11609,10 @@
11722
11609
  }
11723
11610
 
11724
11611
  // 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;
11612
+ let area0 = getPolygonArea([ptsSmp[1], M, ptsSmp[ptsSmp.length - 1]], true);
11613
+ let thresh0 = getSquareDistance(ptsSmp[1], ptsSmp[ptsSmp.length - 1]) * 0.005;
11727
11614
  // remove first point
11728
- if(area0 < thresh0) ptsSmp.shift();
11615
+ if (area0 < thresh0) ptsSmp.shift();
11729
11616
 
11730
11617
  return ptsSmp;
11731
11618
  }
@@ -11744,6 +11631,7 @@
11744
11631
  tolerance = 1,
11745
11632
  simplifyRD = 1,
11746
11633
  simplifyRDP = 1,
11634
+ isClosed = true,
11747
11635
  } = {}) {
11748
11636
 
11749
11637
  let polyPath = [];
@@ -11821,6 +11709,25 @@
11821
11709
 
11822
11710
  // remove colinear
11823
11711
 
11712
+ keepExtremes = false;
11713
+ keepCorners = false;
11714
+
11715
+ keepExtremes = true;
11716
+ keepCorners = true;
11717
+
11718
+ // check if closed
11719
+ /*
11720
+ let bb = getPolyBBox(pts)
11721
+ let thresh = (bb.width+bb.height)*0.25
11722
+ let dist0 = getDistManhattan(pts[0], pts[pts.length-1])
11723
+
11724
+ */
11725
+
11726
+ // copy 1st first to end
11727
+ if (isClosed) {
11728
+ pts.push(pts[0]);
11729
+ }
11730
+
11824
11731
  // get topology of poly
11825
11732
  let polyAnalyzed = !keepExtremes && !keepCorners ? pts : analyzePoly(pts, {
11826
11733
  debug: false
@@ -11828,69 +11735,369 @@
11828
11735
  });
11829
11736
 
11830
11737
  // split into segment chunks
11831
- let chunks = !keepExtremes && !keepCorners ? [polyAnalyzed] : getPolyChunks(polyAnalyzed, { keepCorners, keepExtremes, keepInflections });
11738
+
11739
+ let chunks = getPolyChunks(polyAnalyzed, { keepCorners, keepExtremes, keepInflections: true });
11832
11740
 
11833
11741
  // Schneider curve fit
11834
11742
  let threshold = width && height ? (width + height) / 2 * 0.004 * tolerance : 2.5;
11743
+ threshold = width && height ? (width + height) / 2 * 0.004 * tolerance : 2.5;
11835
11744
 
11836
- polyPath = simplifyPolyChunks(chunks, {
11837
- closed,
11838
- tolerance: threshold,
11839
- keepCorners,
11840
- keepExtremes: true,
11841
- });
11745
+ {
11842
11746
 
11843
- polyPath = fixIntersectingCpts(polyPath);
11747
+ polyPath = simplifyPolyChunksTopology(chunks, {
11748
+ closed,
11749
+ tolerance: threshold,
11750
+ keepCorners,
11751
+ keepExtremes: true,
11752
+ });
11753
+ }
11844
11754
 
11845
11755
  return polyPath;
11846
11756
  }
11847
11757
 
11848
11758
  /**
11849
- * convert polygon chunks
11850
- * to cubic beziers
11759
+ * topology based curve fit
11851
11760
  */
11852
-
11853
- function simplifyPolyChunks(chunks = [], {
11761
+ function simplifyPolyChunksTopology(chunks = [], {
11854
11762
  closed = true,
11855
11763
  keepCorners = true,
11856
11764
  tolerance = 1,
11857
11765
  } = {}) {
11858
11766
 
11767
+ console.log(chunks);
11768
+
11859
11769
  let l = chunks.length;
11860
11770
 
11861
11771
  // new pathData
11772
+
11862
11773
  let pathData = [{ type: 'M', values: [chunks[0][0].x, chunks[0][0].y] }];
11863
11774
 
11775
+ // loop chunks
11864
11776
  for (let i = 0; i < l; i++) {
11865
11777
 
11778
+ let chunkPrev = i > 0 ? chunks[i - 1] : (closed ? chunks[l - 1] : null);
11866
11779
  let chunk = chunks[i];
11867
- let chunkN = chunks[i + 1] ? chunks[i + 1] : null;
11780
+ let chunkN = chunks[i + 1] ? chunks[i + 1] : (closed ? chunks[0] : null);
11868
11781
  let segments = [];
11869
- let chunklen = chunk.length;
11870
- chunk[chunk.length - 1];
11871
11782
 
11872
11783
  // add from next command
11873
11784
  if (chunkN) {
11874
11785
  chunk.push(chunkN[0]);
11875
11786
  }
11876
11787
 
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] } });
11788
+ let chunklen = chunk.length;
11789
+ let hasInflection = false;
11790
+ let segments_1 = [], segments_2 = [], segments_3 = [];
11791
+ let segsRequired = 3;
11792
+
11793
+ // 1st point
11794
+ let p1 = chunk[0];
11795
+ // last point in chunk
11796
+ let p2 = chunk[chunklen - 1];
11797
+
11798
+ // nothing to simplify - lineto
11799
+
11800
+ // if (chunklen < 2 || (chunklen === 2 && (chunk[1].isExtreme || i===l-1 && !closed) )) {
11801
+
11802
+ if (chunklen < 2 || (chunklen === 2 && (chunk[1].isExtreme))) {
11803
+
11804
+ if (chunklen === 2) {
11805
+ segsRequired = 2;
11806
+ segments_2 = [
11807
+ {
11808
+ type: 'L',
11809
+ values: [p1.x, p1.y],
11810
+ p0: chunkPrev,
11811
+ p: p1,
11812
+ },
11813
+ {
11814
+ type: 'L',
11815
+ values: [chunk[1].x, chunk[1].y],
11816
+ p0: p1,
11817
+ p: p2,
11818
+ }
11819
+ ];
11820
+ } else {
11821
+ segsRequired = 1;
11822
+ segments_1 = [
11823
+ {
11824
+ type: 'L',
11825
+ values: [p1.x, p1.y],
11826
+ p0: chunkPrev,
11827
+ p: p2,
11828
+ },
11829
+ ];
11830
+ }
11881
11831
 
11882
11832
  } else {
11883
- segments = fitCurveSchneider(chunk, {
11884
- maxError: tolerance, keepCorners
11885
- });
11833
+
11834
+ // point before inflection
11835
+ let p3 = chunk[chunklen - 2];
11836
+
11837
+ chunk.filter(pt => pt.isExtreme);
11838
+ let semiExtremes = chunk.filter(pt => pt.isSemiExtreme);
11839
+ chunk.filter(pt => pt.isCorner);
11840
+ let inflections = chunk.filter(pt => pt.isDirChange && !pt.isCorner && !pt.isExtreme);
11841
+ hasInflection = inflections.length && inflections[0] !== p1;
11842
+
11843
+ let idxMid = Math.floor(chunklen * 0.5);
11844
+ let pMid = semiExtremes.length ? semiExtremes[Math.floor(semiExtremes.length * 0.5)] : chunk[idxMid];
11845
+
11846
+ let dist0 = getDistManhattan(p1, p3);
11847
+ let dist1 = getDistManhattan(pMid, p3);
11848
+ let dist2 = getDistManhattan(pMid, p1);
11849
+ let thresh = dist0 * 0.25;
11850
+ let shortMidSegment = dist1 < thresh || dist2 < thresh;
11851
+
11852
+ /**
11853
+ * we have 3 modes
11854
+ * 1 segment: only 1 segment between extremes/corners
11855
+ * 2 segments: semiextreme/mid in between
11856
+ * 3 segments: inflection
11857
+ */
11858
+
11859
+ segsRequired = shortMidSegment ? (!hasInflection ? 1 : 2) : (hasInflection ? 3 : 2);
11860
+
11861
+ let cp1_1 = p1.tangentR;
11862
+ let cp2_1 = pMid.tangentL;
11863
+ let p_1 = pMid;
11864
+
11865
+ // renderPoint(markers, pMid, 'orange', '2%')
11866
+
11867
+ let cp2_2 = p2.tangentL;
11868
+ let cp1_2 = pMid.tangentR;
11869
+ let p_2 = p2;
11870
+
11871
+ let cp1_3 = null;
11872
+ let cp2_3 = null;
11873
+ let p_3 = null;
11874
+
11875
+ // general extrapolation
11876
+ let t = 0.666;
11877
+ let ptI_1 = null, ptI_2 = null, ptI_3 = null;
11878
+
11879
+ // 1 segment
11880
+ ptI_1 = checkLineIntersection(p1, p1.tangentR, p2, p2.tangentL, false, true);
11881
+ if (ptI_1) {
11882
+ cp1_1 = interpolate(p1, ptI_1, t);
11883
+ cp2_1 = interpolate(p2, ptI_1, t);
11884
+ p_1 = p2;
11885
+
11886
+ segments_1 = [
11887
+
11888
+ {
11889
+ type: 'C',
11890
+ values: [cp1_1.x, cp1_1.y, cp2_1.x, cp2_1.y, p_1.x, p_1.y],
11891
+ p0: p1,
11892
+ cp1: cp1_1,
11893
+ cp2: cp2_1,
11894
+ p: p_1,
11895
+ }
11896
+ ];
11897
+
11898
+ }
11899
+
11900
+ // 2 segments
11901
+ ptI_1 = checkLineIntersection(p1, p1.tangentR, pMid, pMid.tangentL, false, true);
11902
+ ptI_2 = checkLineIntersection(p2, p2.tangentL, pMid, pMid.tangentR, false, true);
11903
+
11904
+ if (ptI_1 && ptI_2) {
11905
+ cp1_1 = interpolate(p1, ptI_1, t);
11906
+ cp2_1 = interpolate(pMid, ptI_1, t);
11907
+ p_1 = pMid;
11908
+
11909
+ cp1_2 = interpolate(pMid, ptI_2, t);
11910
+ cp2_2 = interpolate(p2, ptI_2, t);
11911
+ p_2 = p2;
11912
+
11913
+ segments_2 = [
11914
+
11915
+ {
11916
+ type: 'C',
11917
+ values: [cp1_1.x, cp1_1.y, cp2_1.x, cp2_1.y, p_1.x, p_1.y],
11918
+ p0: p1,
11919
+ cp1: cp1_1,
11920
+ cp2: cp2_1,
11921
+ p: p_1,
11922
+ isExtreme: p_1.isExtreme
11923
+ },
11924
+ {
11925
+ type: 'C',
11926
+ values: [cp1_2.x, cp1_2.y, cp2_2.x, cp2_2.y, p_2.x, p_2.y],
11927
+ p0: p_1,
11928
+ cp1: cp1_2,
11929
+ cp2: cp2_2,
11930
+ p: p_2,
11931
+ isExtreme: p_2.isExtreme
11932
+
11933
+ },
11934
+ ];
11935
+
11936
+ }
11937
+
11938
+ // 3 segments
11939
+
11940
+ if (hasInflection) {
11941
+
11942
+ // get pt between dir change and mid
11943
+ let idx_3_4 = Math.floor(chunklen * 0.75);
11944
+ p3 = chunk[idx_3_4];
11945
+ ptI_3 = checkLineIntersection(p3, p3.tangentR, p2, p2.tangentL, false, false);
11946
+
11947
+ if (ptI_3) {
11948
+ let tangentR_beforeDirChange = interpolate(p3, ptI_3, t);
11949
+
11950
+ // extend right tangent
11951
+ p3.tangentR.x = tangentR_beforeDirChange.x;
11952
+ p3.tangentR.y = tangentR_beforeDirChange.y;
11953
+
11954
+ // extend dir change tangent
11955
+ let tangentL_dirChange = interpolate(p2, ptI_3, t);
11956
+ p2.tangentL.x = tangentL_dirChange.x;
11957
+ p2.tangentL.y = tangentL_dirChange.y;
11958
+ } else {
11959
+
11960
+ if (p3 === p2) {
11961
+
11962
+ idx_3_4 = Math.floor(chunklen * 0.3);
11963
+ p3 = chunk[idx_3_4];
11964
+
11965
+ }
11966
+ checkLineIntersection(p3, p3.tangentR, p2, p2.tangentL, false, false);
11967
+
11968
+ cp1_1 = interpolate(p1, p1.tangentR, 1.333);
11969
+ cp2_1 = interpolate(p2, p2.tangentL, 1.333);
11970
+
11971
+ segments_3 = [
11972
+ {
11973
+ type: 'C',
11974
+ values: [cp1_1.x, cp1_1.y, cp2_1.x, cp2_1.y, p2.x, p2.y],
11975
+ p0: p1,
11976
+ cp1: cp1_1,
11977
+ cp2: cp2_1,
11978
+ p: p2,
11979
+ isExtreme: p2.isExtreme
11980
+
11981
+ },
11982
+ ];
11983
+
11984
+ /*
11985
+ let tangentR_beforeDirChange = interpolate(p3, ptI_3, t)
11986
+
11987
+ // extend right tangent
11988
+ p3.tangentR.x = tangentR_beforeDirChange.x
11989
+ p3.tangentR.y = tangentR_beforeDirChange.y
11990
+
11991
+ let tangentL_dirChange = interpolate(p2, ptI_3, t)
11992
+ p2.tangentL.x = tangentL_dirChange.x
11993
+ p2.tangentL.y = tangentL_dirChange.y
11994
+ */
11995
+
11996
+ pathDataToD([{ type: 'M', values: [p1.x, p1.y] }, ...segments_3]);
11997
+
11998
+ }
11999
+
12000
+ cp1_3 = p3.tangentR;
12001
+ cp2_3 = p2.tangentL;
12002
+ p_3 = p2;
12003
+
12004
+ ptI_1 = checkLineIntersection(p1, p1.tangentR, pMid, pMid.tangentL, false, true);
12005
+ ptI_2 = checkLineIntersection(pMid, pMid.tangentR, p3, p3.tangentL, false, true);
12006
+
12007
+ if (ptI_1 && ptI_2 && ptI_3) {
12008
+
12009
+ cp1_1 = interpolate(p1, ptI_1, t);
12010
+ cp2_1 = interpolate(pMid, ptI_1, t);
12011
+ p_1 = pMid;
12012
+
12013
+ cp1_2 = interpolate(pMid, ptI_2, t);
12014
+ cp2_2 = interpolate(p3, ptI_2, t);
12015
+ p_2 = p3;
12016
+
12017
+ segments_3 = [
12018
+ {
12019
+ type: 'C',
12020
+ values: [cp1_1.x, cp1_1.y, cp2_1.x, cp2_1.y, p_1.x, p_1.y],
12021
+ p0: p1,
12022
+ cp1: cp1_1,
12023
+ cp2: cp2_1,
12024
+ p: p_1,
12025
+ isExtreme: p_1.isExtreme
12026
+
12027
+ },
12028
+ {
12029
+ type: 'C',
12030
+ values: [cp1_2.x, cp1_2.y, cp2_2.x, cp2_2.y, p_2.x, p_2.y],
12031
+ p0: p_1,
12032
+ cp1: cp1_2,
12033
+ cp2: cp2_2,
12034
+ p: p_2,
12035
+ },
12036
+ {
12037
+ type: 'C',
12038
+ values: [cp1_3.x, cp1_3.y, cp2_3.x, cp2_3.y, p_3.x, p_3.y],
12039
+ p0: p_2,
12040
+ cp1: cp1_3,
12041
+ cp2: cp2_3,
12042
+ p: p_3,
12043
+ isExtreme: p_3.isExtreme
12044
+
12045
+ }
12046
+ ];
12047
+
12048
+ pathDataToD([{ type: 'M', values: [p1.x, p1.y] }, ...segments_3]);
12049
+
12050
+ }
12051
+
12052
+ }
12053
+
12054
+ }
12055
+
12056
+ if (segsRequired === 1) {
12057
+ segments = segments_1;
12058
+ } else if (segsRequired === 2 && segments_2.length) {
12059
+ segments = segments_2;
12060
+ }
12061
+ else ;
12062
+
12063
+ segments = segments_3.length ? segments_3 : segments_2;
12064
+ /*
12065
+ if (simplify && !isLinetoSeg && segments.length > 1) {
12066
+
12067
+ let com1 = segments[0]
12068
+ let com2 = segments[1]
12069
+
12070
+ tolerance = 1.1
12071
+ let combined = combineCubicPairs(com1, com2, { tolerance })
12072
+
12073
+ let error = 0;
12074
+ let comsSimp =[]
12075
+
12076
+ console.log('!!!combined', segments.length, combined);
12077
+
12078
+ // success
12079
+ if (combined.length === 1) {
12080
+
12081
+ if(segments.length === 2){
12082
+ segments = combined
12083
+ }
12084
+
12085
+ let com = combined[0]
12086
+ }
12087
+
11886
12088
  }
12089
+ */
11887
12090
 
11888
12091
  // remove first segment to connect to last segment
11889
12092
  pathData.push(...segments);
11890
12093
 
11891
12094
  }
11892
12095
 
11893
- if (closed) pathData.push({ type: 'Z', values: [] });
12096
+ if (closed) {
12097
+ pathData.push({ type: 'Z', values: [] });
12098
+ }
12099
+
12100
+ // refine extremes
11894
12101
  return pathData
11895
12102
 
11896
12103
  }
@@ -11901,13 +12108,18 @@
11901
12108
  */
11902
12109
  function pathDataToPolygonOpt(pathData, {
11903
12110
  precisionPoly = 1,
11904
- autoAccuracy=false,
11905
- polyFormat='points',
11906
- decimals=-1,
11907
- simplifyRD=1,
11908
- simplifyRDP=1,
12111
+ autoAccuracy = false,
12112
+ polyFormat = 'object',
12113
+ decimals = -1,
12114
+ simplifyRD = 1,
12115
+ simplifyRDP = 1,
11909
12116
  } = {}) {
11910
12117
 
12118
+ pathData = convertPathData(pathData, {toAbsolute:true, toLonghands:true, arcToCubic:true});
12119
+ pathData = addExtremePoints(pathData);
12120
+
12121
+ pathData = getPathDataVerbose(pathData);
12122
+
11911
12123
  let l = pathData.length;
11912
12124
  let M = { x: pathData[0].values[0], y: pathData[0].values[1] };
11913
12125
  let p0 = M;
@@ -11934,7 +12146,7 @@
11934
12146
  let pts2 = [pts[0]];
11935
12147
 
11936
12148
  // adjustments for very small or large paths
11937
- dims = dims.filter(Boolean).sort();
12149
+ dims = dims.filter(Boolean).sort((a,b)=>a-b);
11938
12150
  let dimMax = dims[dims.length - 1];
11939
12151
 
11940
12152
  let scale = dimMax > 2 && dimMax < 25 ? 1 : (20 / dimMax);
@@ -11973,31 +12185,33 @@
11973
12185
  }
11974
12186
 
11975
12187
  // simplify polygon
11976
- if(simplifyRD>0){
11977
- pts2 = simplifyPolyRD(pts2, {quality:simplifyRD});
12188
+ if (simplifyRD > 0) {
12189
+ pts2 = simplifyPolyRD(pts2, { quality: simplifyRD });
11978
12190
  }
11979
12191
 
11980
- if(simplifyRDP>0){
11981
- pts2 = simplifyPolyRDP(pts2, {quality:simplifyRDP});
12192
+ if (simplifyRDP > 0) {
12193
+ pts2 = simplifyPolyRDP(pts2, { quality: simplifyRDP });
11982
12194
  }
11983
12195
 
11984
- pathDataPoly = pathDataFromPoly(pts2);
11985
- pathData = pathDataPoly;
11986
-
11987
- if(autoAccuracy){
12196
+ if (autoAccuracy) {
11988
12197
  decimals = detectAccuracyPoly(pts);
11989
12198
  }
11990
12199
 
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 } });
12200
+ 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
12201
 
11993
- if(polyFormat==='array'){
12202
+ pathDataPoly = pathDataFromPoly(poly);
12203
+ pathData = pathDataPoly;
12204
+
12205
+ if (polyFormat === 'array') {
11994
12206
  poly = poly.map(pt => { return [pt.x, pt.y] });
11995
12207
  }
11996
- else if(polyFormat==='string'){
12208
+ else if (polyFormat === 'string') {
11997
12209
  poly = poly.map(pt => { return [pt.x, pt.y].join(',') }).flat().join(' ');
11998
12210
  }
11999
12211
 
12000
- return { pathData, poly }
12212
+ let d= pathDataToD(pathData);
12213
+
12214
+ return { pathData, poly, d }
12001
12215
 
12002
12216
  }
12003
12217
 
@@ -12238,6 +12452,7 @@
12238
12452
  // polygon
12239
12453
  toPolygon: false,
12240
12454
  smoothPoly: false,
12455
+ isClosed:true,
12241
12456
  polyFormat: 'object',
12242
12457
  precisionPoly: 1,
12243
12458
  simplifyRD: 0,
@@ -12534,6 +12749,122 @@
12534
12749
  }
12535
12750
  */
12536
12751
 
12752
+ function SlickVGObj(props = {}) {
12753
+
12754
+ Object.assign(this, props);
12755
+ }
12756
+
12757
+ SlickVGObj.prototype.getD = function () {
12758
+ let d = this.d;
12759
+ return d;
12760
+ };
12761
+
12762
+ SlickVGObj.prototype.getSvg = function () {
12763
+ let svg = this.svg;
12764
+
12765
+ if (!svg) {
12766
+ let xArr = [];
12767
+ let yArr = [];
12768
+ let d = this.d;
12769
+ let pathDataPlusArr = this.pathDataPlusArr || [];
12770
+ pathDataPlusArr.forEach(path => {
12771
+ path.forEach(sub => {
12772
+ let { pathData } = sub;
12773
+ let bb = getPathDataBBox(pathData);
12774
+ let { x, y, right, bottom } = bb;
12775
+ xArr.push(x, right);
12776
+ yArr.push(y, bottom);
12777
+ });
12778
+ });
12779
+
12780
+ let x = Math.min(...xArr);
12781
+ let right = Math.max(...xArr);
12782
+ let y = Math.min(...yArr);
12783
+ let bottom = Math.max(...yArr);
12784
+ let width = right - x;
12785
+ let height = bottom - y;
12786
+
12787
+ svg = `<svg xmlns="${svgNs}" viewBox="${[x, y, width, height].join(' ')}"><path d="${d}"/></svg>`;
12788
+
12789
+ }
12790
+ return svg;
12791
+ };
12792
+
12793
+ /**
12794
+ * retrieve poly
12795
+ * formats: points, array, string, pathData, d,
12796
+ */
12797
+ SlickVGObj.prototype.getPoly = function (options = {}
12798
+ ) {
12799
+
12800
+ options = {
12801
+ ...{
12802
+ precisionPoly: 1,
12803
+ simplifyRDP: 0,
12804
+ simplifyRD: 0,
12805
+ autoAccuracy: true,
12806
+ decimals: 3,
12807
+ format: 'object'
12808
+ },
12809
+ ...options
12810
+ };
12811
+
12812
+ let { precisionPoly, simplifyRDP, simplifyRD, autoAccuracy, decimals, format } = options;
12813
+
12814
+ let polyFormat = format;
12815
+
12816
+ let polys = this.polys;
12817
+ if (!polys.length) {
12818
+ let pathDataPlusArr = this.pathDataPlusArr || [];
12819
+ let poly = [];
12820
+ let polyPaths = [];
12821
+ let dPoly = '';
12822
+ pathDataPlusArr.forEach(path => {
12823
+ path.forEach(sub => {
12824
+ let { pathData } = sub;
12825
+
12826
+ let polyData = pathDataToPolygonOpt(pathData, {
12827
+ precisionPoly,
12828
+ autoAccuracy,
12829
+ decimals,
12830
+ simplifyRD,
12831
+ simplifyRDP,
12832
+ polyFormat
12833
+ });
12834
+
12835
+ dPoly += polyData.d;
12836
+ poly.push(polyData.poly);
12837
+ polyPaths.push(polyData.pathData);
12838
+
12839
+ });
12840
+ });
12841
+
12842
+ if (polyFormat === 'object' || polyFormat === 'array' || polyFormat === 'string') {
12843
+ polys = poly;
12844
+ }
12845
+ else if (polyFormat === 'pathData') {
12846
+ polys = polyPaths.flat();
12847
+ }
12848
+
12849
+ else if (polyFormat === 'd') {
12850
+ polys = dPoly;
12851
+ }
12852
+
12853
+ }
12854
+ return polys;
12855
+ };
12856
+
12857
+ /*
12858
+ export function PathLengthObject(props = {}) {
12859
+ Object.assign(this, props);
12860
+ }
12861
+ */
12862
+
12863
+ function SlickVG(input = '', settings = {}) {
12864
+ settings.getObject = true;
12865
+ return svgPathSimplify(input, settings)
12866
+ }
12867
+
12537
12868
  function svgPathSimplify(input = '', settings = {}) {
12538
12869
 
12539
12870
  let preset = settings['preset'] !== undefined && settings['preset'] ? settings['preset'] : null;
@@ -12545,7 +12876,7 @@
12545
12876
  ...settings
12546
12877
  };
12547
12878
 
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;
12879
+ 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
12880
 
12550
12881
  // clamp tolerance and scale
12551
12882
  tolerance = Math.max(0.1, tolerance);
@@ -12556,6 +12887,10 @@
12556
12887
  settings.convertTransforms = true;
12557
12888
  }
12558
12889
 
12890
+ if (shapeConvert === 'toShapes' || shapeConvert === 'shapesToPaths') {
12891
+ keepSmaller = false;
12892
+ }
12893
+
12559
12894
  /**
12560
12895
  * intercept
12561
12896
  * invalid inputs
@@ -12733,6 +13068,8 @@
12733
13068
  toRelative,
12734
13069
  toMixed,
12735
13070
  toShorthands,
13071
+ // return true arc radii or minified/parametrized
13072
+ optimizeArcs: minifyD < 1,
12736
13073
  decimals,
12737
13074
  };
12738
13075
 
@@ -12744,7 +13081,6 @@
12744
13081
  let pathDataPlusArr = [];
12745
13082
  let path = paths[i];
12746
13083
  let { d, el } = path;
12747
- let isPoly = false;
12748
13084
 
12749
13085
  // disable reordering for elements with stroke dash-array
12750
13086
  if (el && (el.hasAttribute('stroke-dasharray') || el.hasAttribute('stroke-dashoffset'))) {
@@ -12755,6 +13091,7 @@
12755
13091
  // if polygon we already heave absolute coordinates
12756
13092
 
12757
13093
  let pathData = parsePathDataNormalized(d, { quadraticToCubic, arcToCubic });
13094
+ console.log('!!!pathData', pathData, arcToCubic);
12758
13095
 
12759
13096
  // get polygon bbox
12760
13097
  let bb_poly = smoothPoly || toPolygon ? getPolyBBox(getPathDataVertices(pathData)) : null;
@@ -12791,7 +13128,7 @@
12791
13128
  // count commands for evaluation
12792
13129
  comCount += pathData.length;
12793
13130
 
12794
- if (!isPoly && removeOrphanSubpaths) pathData = removeOrphanedM(pathData);
13131
+ if (removeOrphanSubpaths) pathData = removeOrphanedM(pathData);
12795
13132
 
12796
13133
  /**
12797
13134
  * get sub paths
@@ -12805,8 +13142,9 @@
12805
13142
  let pathDataSub = subPathArr[i];
12806
13143
  let poly = [];
12807
13144
  let coms = Array.from(new Set(pathDataSub.map(com => com.type))).join('');
12808
- isPoly = !(/[acqts]/gi).test(coms);
12809
- let closed = isPoly ? true : false;
13145
+ let isPoly = !(/[acqts]/gi).test(coms);
13146
+
13147
+ let closed = (/z/gi).test(coms);
12810
13148
 
12811
13149
  if (isPoly && !mode) {
12812
13150
 
@@ -12823,7 +13161,6 @@
12823
13161
 
12824
13162
  }
12825
13163
 
12826
- toPolygon = false;
12827
13164
  pathDataSub = pathDataFromPoly(poly, closed);
12828
13165
 
12829
13166
  }
@@ -12832,26 +13169,56 @@
12832
13169
  * convert curves to polygon
12833
13170
  * flattening
12834
13171
  */
12835
- else if (toPolygon) {
13172
+
13173
+ if (toPolygon) {
12836
13174
  simplifyBezier = false;
12837
13175
  smoothPoly = false;
12838
13176
  harmonizeCpts = false;
12839
13177
 
12840
- pathDataSub = getPathDataVerbose(pathDataSub);
13178
+ /**
13179
+ * if pathdata is already polygon- pass through
13180
+ * otherwise create precise polygon by curve splitting
13181
+ * */
12841
13182
 
12842
- let polyData = pathDataToPolygonOpt(pathDataSub, {
12843
- precisionPoly,
12844
- autoAccuracy,
13183
+ if (!isPoly) {
13184
+ pathDataSub = getPathDataVerbose(pathDataSub);
13185
+ let polyData = pathDataToPolygonOpt(pathDataSub, {
13186
+ precisionPoly,
13187
+ autoAccuracy,
13188
+ polyFormat,
12845
13189
 
12846
- simplifyRD,
12847
- simplifyRDP
12848
- });
13190
+ simplifyRD,
13191
+ simplifyRDP
13192
+ });
13193
+
13194
+ poly = polyData.poly;
13195
+ pathDataSub = polyData.pathData;
13196
+ isPoly = true;
13197
+
13198
+ }
13199
+
13200
+ polys.push(poly);
13201
+
13202
+ }
13203
+
13204
+ // harmonize cpts
13205
+ // if (harmonizeCpts) pathDataSub = harmonizeCubicCpts(pathDataSub)
12849
13206
 
12850
- pathDataSub = polyData.pathData;
12851
- isPoly = true;
13207
+ if (smoothPoly) {
12852
13208
 
13209
+ removeZeroLength=true;
13210
+ optimizeOrder=true;
12853
13211
  }
12854
13212
 
13213
+ // remove zero length linetos
13214
+ if (removeColinear || removeZeroLength) pathDataSub = removeZeroLengthLinetos(pathDataSub);
13215
+
13216
+ // sort to top left
13217
+ if (optimizeOrder) pathDataSub = pathDataToTopLeft(pathDataSub);
13218
+
13219
+ // Preprocessing: remove colinear - ignore flat beziers (removed later)
13220
+ if (removeColinear) pathDataSub = pathDataRemoveColinear(pathDataSub, { tolerance, flatBezierToLinetos: false });
13221
+
12855
13222
  /**
12856
13223
  * poly to beziers via
12857
13224
  * Philip J. Schneider's
@@ -12860,12 +13227,18 @@
12860
13227
  if (smoothPoly) {
12861
13228
 
12862
13229
  if (isPoly) {
12863
- pathDataSub = removeZeroLengthLinetos(pathDataSub);
13230
+ /*
13231
+ pathDataSub = pathDataToTopLeft(pathDataSub)
13232
+ pathDataSub = removeZeroLengthLinetos(pathDataSub)
13233
+ pathDataSub = pathDataRemoveColinear(pathDataSub, { tolerance, flatBezierToLinetos: true });
13234
+ */
13235
+
12864
13236
  let poly = getPathDataVertices(pathDataSub);
12865
13237
 
12866
13238
  // options for poly simplification
12867
13239
  let optionsPoly = {
12868
- denoise: 0.8,
13240
+
13241
+ denoise: 0,
12869
13242
  tolerance,
12870
13243
  width: bb_poly.width,
12871
13244
  height: bb_poly.height,
@@ -12877,26 +13250,15 @@
12877
13250
  closed,
12878
13251
  simplifyRD,
12879
13252
  simplifyRDP,
13253
+ isClosed,
12880
13254
  };
12881
13255
 
12882
13256
  pathDataSub = simplifyPolygonToPathData(poly, optionsPoly);
12883
13257
  // flag as non poly as we're smoothing to curves
12884
-
13258
+ isPoly = false;
12885
13259
  }
12886
13260
  }
12887
13261
 
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
13262
  let tMin = 0, tMax = 1;
12901
13263
  if (addExtremes) pathDataSub = addExtremePoints(pathDataSub,
12902
13264
  { tMin, tMax, addExtremes, angles: [30] });
@@ -12919,11 +13281,13 @@
12919
13281
  pathDataPlus.bb = getPolyBBox(getPathDataVertices(pathDataCubic));
12920
13282
  }
12921
13283
  pathDataPlus.dimA = pathDataPlus.bb.width + pathDataPlus.bb.height;
13284
+
12922
13285
  pathDataPlus.pathData = getPathDataVerbose(pathDataSub, {
12923
13286
  addSquareLength: false,
12924
13287
  addArea: false,
12925
13288
  addAverageDim: false
12926
13289
  });
13290
+
12927
13291
  }
12928
13292
 
12929
13293
  // simplify beziers
@@ -12934,6 +13298,12 @@
12934
13298
 
12935
13299
  if (refineClosing) pathData = refineClosingCommand(pathData, { threshold: dimA * 0.001 });
12936
13300
 
13301
+ // refine round segment sequences
13302
+ if (simplifyRound) {
13303
+ pathData = refineRoundSegments(pathData);
13304
+ pathData = simplifyAdjacentRound(pathData);
13305
+ }
13306
+
12937
13307
  pathData = simplifyBezier ? simplifyPathDataCubic(pathData, { simplifyBezier, keepInflections, keepExtremes, keepCorners, revertToQuadratics, tolerance }) : pathData;
12938
13308
 
12939
13309
  // refine extremes
@@ -12957,12 +13327,6 @@
12957
13327
  pathData = refineRoundedCorners(pathData, { threshold, tolerance, simplifyQuadraticCorners });
12958
13328
  }
12959
13329
 
12960
- // refine round segment sequences
12961
- if (simplifyRound) {
12962
- pathData = refineRoundSegments(pathData);
12963
- pathData = simplifyAdjacentRound(pathData);
12964
- }
12965
-
12966
13330
  // simplify to quadratics
12967
13331
  if (revertToQuadratics) pathData = pathDataRevertCubicToQuadratic(pathData, tolerance);
12968
13332
 
@@ -12979,6 +13343,8 @@
12979
13343
 
12980
13344
  }
12981
13345
 
13346
+ // offset path
13347
+
12982
13348
  // update
12983
13349
  pathDataPlusArr.push({ pathData, bb });
12984
13350
 
@@ -13038,17 +13404,18 @@
13038
13404
  }
13039
13405
 
13040
13406
  // add simplified poly - if not populated by toPoly conversion
13407
+ /*
13041
13408
  if (isPoly) {
13042
13409
 
13043
13410
  pathDataPlusArr.forEach(sub => {
13044
- let poly = getPathDataVertices(sub.pathData, false, decimals);
13411
+ let poly = getPathDataVertices(sub.pathData, false, decimals)
13045
13412
  if (polyFormat === 'array') {
13046
- poly = polyPtsToArray(poly);
13413
+ poly = polyPtsToArray(poly)
13047
13414
  }
13048
- polys.push(poly);
13049
- });
13050
-
13415
+ polys.push(poly)
13416
+ })
13051
13417
  }
13418
+ */
13052
13419
 
13053
13420
  // split into sub paths - returns svg with multiple paths
13054
13421
  if (splitCompound && !mode && pathDataPlusArr.length > 1) {
@@ -13147,7 +13514,6 @@
13147
13514
  svg = stringifySVG(svg, { omitNamespace, removeComments, format: minifyD });
13148
13515
 
13149
13516
  svgSizeOpt = svg.length;
13150
-
13151
13517
  compression = +(100 / svgSize * (svgSizeOpt)).toFixed(2);
13152
13518
 
13153
13519
  svgSize = +(svgSize / 1024).toFixed(3);
@@ -13173,15 +13539,52 @@
13173
13539
  ({ d, report } = paths[0]);
13174
13540
  }
13175
13541
 
13176
- if (polys.length && polys.length === 1) {
13177
- polys = polys[0];
13178
- }
13542
+ // sanitize poly output
13543
+ if (polys.length) {
13544
+
13545
+ // round point data
13546
+ polys.forEach((poly, i) => {
13547
+
13548
+ if (polyFormat === 'string') poly = normalizePoly(poly);
13549
+
13550
+ poly = roundPoly(poly, decimals);
13551
+ // remove coinciding points
13552
+ polys[i] = removeCoincidingVertices(poly);
13553
+ });
13554
+
13555
+ if (polys.length === 1) {
13556
+ polys = polys[0];
13557
+ }
13558
+
13559
+ if (polyFormat === 'string') {
13560
+
13561
+ polys = normalizePoly(polys, { toArray: true, flatten: true });
13562
+
13563
+ polys = polys.flat().join(' ');
13564
+ }
13179
13565
 
13180
- if (polyFormat === 'string' && polys.length) {
13181
- polys = polys.flat().map(pt => `${pt.x},${pt.y}`).join(' ');
13182
13566
  }
13183
13567
 
13184
- return !getObject ? (d ? d : svg) : { svg, d, polys, report, pathDataPlusArr: pathDataPlusArr_global, inputType, dOriginal };
13568
+ // create object
13569
+ let svgObj = new SlickVGObj({ svg, d, polys, report, pathDataPlusArr: pathDataPlusArr_global, inputType, dOriginal });
13570
+
13571
+ /*
13572
+ let d2 = svgObj.getD()
13573
+ console.log('d2', d2);
13574
+
13575
+ let svg2 = svgObj.getSvg()
13576
+ console.log('svg2', svg2);
13577
+
13578
+ let format = 'd'
13579
+ format = 'pathData'
13580
+ format = 'd'
13581
+ format = 'points'
13582
+ format = 'array'
13583
+ let poly2 = svgObj.getPoly({ format })
13584
+ console.log('poly2', 'format', format, poly2);
13585
+ */
13586
+
13587
+ return !getObject ? (d ? d : svg) : svgObj;
13185
13588
 
13186
13589
  }
13187
13590
 
@@ -13190,6 +13593,7 @@
13190
13593
  // IIFE
13191
13594
  if (typeof window !== 'undefined') {
13192
13595
  window.svgPathSimplify = svgPathSimplify;
13596
+ window.SlickVG = SlickVG;
13193
13597
  window.getElementTransform = getElementTransform;
13194
13598
  window.validateSVG = validateSVG;
13195
13599
  window.detectInputType = detectInputType;
@@ -13199,6 +13603,7 @@
13199
13603
  }
13200
13604
 
13201
13605
  exports.PI = PI$1;
13606
+ exports.SlickVG = SlickVG;
13202
13607
  exports.abs = abs$1;
13203
13608
  exports.acos = acos$1;
13204
13609
  exports.asin = asin$1;