svg-path-simplify 0.4.5 → 0.4.6

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