svg-path-simplify 0.0.9 → 0.1.2

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.
@@ -18,18 +18,68 @@ export function getAngle(p1, p2, normalize = false) {
18
18
  }
19
19
 
20
20
 
21
+ export function getDeltaAngle(centerPoint, startPoint, endPoint, largeArc = false) {
22
+
23
+ const normalizeAngle = (angle) => {
24
+ let normalized = angle % (2 * Math.PI);
25
+
26
+ if (normalized > Math.PI) {
27
+ normalized -= 2 * Math.PI;
28
+ } else if (normalized <= -Math.PI) {
29
+ normalized += 2 * Math.PI;
30
+ }
31
+ return normalized;
32
+ }
33
+
34
+ let startAngle = Math.atan2(
35
+ startPoint.y - centerPoint.y,
36
+ startPoint.x - centerPoint.x
37
+ );
38
+
39
+ let endAngle = Math.atan2(
40
+ endPoint.y - centerPoint.y,
41
+ endPoint.x - centerPoint.x
42
+ );
43
+
44
+ // Calculate raw delta angle (difference)
45
+ let deltaAngle = endAngle - startAngle;
46
+
47
+ // Normalize the delta angle to range (-π, π]
48
+ deltaAngle = normalizeAngle(deltaAngle);
49
+
50
+ if (largeArc) deltaAngle = Math.PI*2 - Math.abs(deltaAngle);
51
+
52
+ let phi = 180 / Math.PI
53
+ let startAngleDeg = startAngle * phi
54
+ let endAngleDeg = endAngle * phi
55
+ let deltaAngleDeg = deltaAngle * phi
56
+
57
+ return {
58
+ startAngle, endAngle, deltaAngle, startAngleDeg,
59
+ endAngleDeg,
60
+ deltaAngleDeg
61
+ };
62
+
63
+ }
64
+
65
+
66
+
67
+
68
+
69
+
70
+
21
71
  /**
22
72
  * based on: Justin C. Round's
23
73
  * http://jsfiddle.net/justin_c_rounds/Gd2S2/light/
24
74
  */
25
75
 
26
- export function checkLineIntersection(p1=null, p2=null, p3=null, p4=null, exact = true, debug=false) {
76
+ export function checkLineIntersection(p1 = null, p2 = null, p3 = null, p4 = null, exact = true, debug = false) {
27
77
  // if the lines intersect, the result contains the x and y of the intersection (treating the lines as infinite) and booleans for whether line segment 1 or line segment 2 contain the point
28
78
  let denominator, a, b, numerator1, numerator2;
29
79
  let intersectionPoint = {}
30
80
 
31
- if(!p1 || !p2 || !p3 || !p4){
32
- if(debug) console.warn('points missing');
81
+ if (!p1 || !p2 || !p3 || !p4) {
82
+ if (debug) console.warn('points missing');
33
83
  return false
34
84
  }
35
85
 
@@ -39,7 +89,7 @@ export function checkLineIntersection(p1=null, p2=null, p3=null, p4=null, exact
39
89
  return false;
40
90
  }
41
91
  } catch {
42
- if(debug) console.warn('!catch', p1, p2, 'p3:', p3, 'p4:', p4);
92
+ if (debug) console.warn('!catch', p1, p2, 'p3:', p3, 'p4:', p4);
43
93
  return false
44
94
  }
45
95
 
@@ -0,0 +1,50 @@
1
+ import { getDeltaAngle, getDistance } from "./geometry";
2
+
3
+ export function getArcFromPoly(pts) {
4
+ if (pts.length < 3) return false
5
+
6
+ // Pick 3 well-spaced points
7
+ let p1 = pts[0];
8
+ let p2 = pts[Math.floor(pts.length / 2)];
9
+ let p3 = pts[pts.length - 1];
10
+
11
+ let x1 = p1.x, y1 = p1.y;
12
+ let x2 = p2.x, y2 = p2.y;
13
+ let x3 = p3.x, y3 = p3.y;
14
+
15
+ let a = x1 - x2;
16
+ let b = y1 - y2;
17
+ let c = x1 - x3;
18
+ let d = y1 - y3;
19
+
20
+ let e = ((x1 * x1 - x2 * x2) + (y1 * y1 - y2 * y2)) / 2;
21
+ let f = ((x1 * x1 - x3 * x3) + (y1 * y1 - y3 * y3)) / 2;
22
+
23
+ let det = a * d - b * c;
24
+
25
+ if (Math.abs(det) < 1e-10) {
26
+ console.warn("Points are collinear or numerically unstable");
27
+ return false;
28
+ }
29
+
30
+ // find center of arc
31
+ let cx = (d * e - b * f) / det;
32
+ let cy = (-c * e + a * f) / det;
33
+ let centroid = { x: cx, y: cy };
34
+
35
+ // Radius (use start point)
36
+ let r = getDistance(centroid, p1);
37
+
38
+ let angleData = getDeltaAngle(centroid, p1, p3)
39
+ let {deltaAngle, startAngle, endAngle} = angleData;
40
+
41
+ return {
42
+ centroid,
43
+ r,
44
+ startAngle,
45
+ endAngle,
46
+ deltaAngle
47
+ };
48
+ }
49
+
50
+
@@ -12,8 +12,9 @@ import { renderPoint, renderPath } from "./visualize";
12
12
  */
13
13
 
14
14
 
15
- import { checkLineIntersection, getAngle, getDistance, getDistAv, getSquareDistance, interpolate, pointAtT, rotatePoint } from './geometry';
15
+ import { checkLineIntersection, getAngle, getDeltaAngle, getDistance, getDistAv, getSquareDistance, interpolate, pointAtT, rotatePoint, toParametricAngle } from './geometry';
16
16
  import { getPathArea, getPolygonArea, getRelativeAreaDiff } from './geometry_area';
17
+ import { pathDataToD } from './pathData_stringify';
17
18
  import { roundPathData } from './rounding';
18
19
  import { renderPoint } from './visualize';
19
20
 
@@ -24,13 +25,13 @@ export function revertCubicQuadratic(p0 = {}, cp1 = {}, cp2 = {}, p = {}) {
24
25
  let cp2X = interpolate(p, cp2, 1.5)
25
26
 
26
27
  let dist0 = getDistAv(p0, p)
27
- let threshold = dist0 * 0.01;
28
+ let threshold = dist0 * 0.03;
28
29
  let dist1 = getDistAv(cp1X, cp2X)
29
30
 
30
31
  let cp1_Q = null;
31
32
  let type = 'C'
32
33
  let values = [cp1.x, cp1.y, cp2.x, cp2.y, p.x, p.y];
33
- let comN = {type, values}
34
+ let comN = { type, values }
34
35
 
35
36
  if (dist1 && threshold && dist1 < threshold) {
36
37
  cp1_Q = checkLineIntersection(p0, cp1, p, cp2, false);
@@ -40,6 +41,7 @@ export function revertCubicQuadratic(p0 = {}, cp1 = {}, cp2 = {}, p = {}) {
40
41
  comN.values = [cp1_Q.x, cp1_Q.y, p.x, p.y];
41
42
  comN.p0 = p0;
42
43
  comN.cp1 = cp1_Q;
44
+ comN.cp2 = null;
43
45
  comN.p = p
44
46
  }
45
47
  }
@@ -62,7 +64,7 @@ export function convertPathData(pathData, {
62
64
  if (toShorthands) pathData = pathDataToShorthands(pathData);
63
65
 
64
66
  // pre round - before relative conversion to minimize distortions
65
- if(decimals>-1 && toRelative) pathData = roundPathData(pathData, decimals);
67
+ if (decimals > -1 && toRelative) pathData = roundPathData(pathData, decimals);
66
68
  if (toRelative) pathData = pathDataToRelative(pathData);
67
69
  if (decimals > -1) pathData = roundPathData(pathData, decimals);
68
70
 
@@ -415,7 +417,7 @@ export function pathDataToShorthands(pathData, decimals = -1, test = false) {
415
417
  let com = pathData[i];
416
418
  let { type, values } = com;
417
419
  let valuesLen = values.length;
418
- let valuesLast = [values[valuesLen-2], values[valuesLen-1]];
420
+ let valuesLast = [values[valuesLen - 2], values[valuesLen - 1]];
419
421
 
420
422
  // previoius command
421
423
  let comPrev = pathData[i - 1];
@@ -602,6 +604,123 @@ export function pathDataToQuadratic(pathData, precision = 0.1) {
602
604
  }
603
605
 
604
606
 
607
+ /**
608
+ * Convert a parametrized SVG arc to cubic Beziers
609
+ * Assumes arc parameters are already resolved
610
+ */
611
+ export function arcToBezierResolved({
612
+
613
+ // start / end points
614
+ p0 = { x: 0, y: 0 },
615
+ p = { x: 0, y: 0 },
616
+
617
+ // center
618
+ centroid = { x: 0, y: 0 },
619
+
620
+ // radii
621
+ rx = 0,
622
+ ry = 0,
623
+
624
+ // SVG-style rotation
625
+ xAxisRotation = 0,
626
+ radToDegree = false,
627
+
628
+ // optional
629
+ startAngle = null,
630
+ endAngle = null,
631
+ deltaAngle = null
632
+
633
+ } = {}) {
634
+
635
+ if (!rx || !ry) return [];
636
+
637
+ // new pathData
638
+ let pathData = [];
639
+
640
+ // maximum delta for cubic approximations: Math.PI / 2 (90deg)
641
+ const maxSegAngle = 1.5707963267948966
642
+
643
+ // Pomax cubic constant
644
+ const k = 0.551785;
645
+
646
+
647
+ // rotation
648
+ let phi = radToDegree
649
+ ? xAxisRotation
650
+ : xAxisRotation * Math.PI / 180;
651
+
652
+ let cosphi = Math.cos(phi);
653
+ let sinphi = Math.sin(phi);
654
+
655
+ // helper: transform point to ellipse local space
656
+ const toLocal = ({ x, y }) => {
657
+ const dx = x - centroid.x;
658
+ const dy = y - centroid.y;
659
+ return {
660
+ x: (cosphi * dx + sinphi * dy) / rx,
661
+ y: (-sinphi * dx + cosphi * dy) / ry
662
+ };
663
+ };
664
+
665
+ // derive angles if not provided
666
+ if (startAngle === null || endAngle === null || deltaAngle === null) {
667
+ ({ startAngle, endAngle, deltaAngle } = getDeltaAngle(centroid, p0, p))
668
+ }
669
+
670
+
671
+
672
+ // parametrize for elliptic arcs
673
+ let startAngleParam = rx !== ry ? toParametricAngle(startAngle, rx, ry) : startAngle;
674
+ //let endAngleParam = rx !== ry ? toParametricAngle(endAngle, rx, ry) : endAngle;
675
+ //let deltaAngleParam = endAngleParam - startAngleParam;
676
+ let deltaAngleParam = rx !== ry ? toParametricAngle(deltaAngle, rx, ry) : deltaAngle;
677
+
678
+ let segments = Math.max(1, Math.ceil(Math.abs(deltaAngleParam) / maxSegAngle));
679
+ let angStep = deltaAngleParam / segments;
680
+
681
+ for (let i = 0; i < segments; i++) {
682
+
683
+ const a = Math.abs(angStep) === maxSegAngle ?
684
+ Math.sign(angStep) * k :
685
+ (4 / 3) * Math.tan(angStep / 4);
686
+
687
+ let cos0 = Math.cos(startAngleParam);
688
+ let sin0 = Math.sin(startAngleParam);
689
+ let cos1 = Math.cos(startAngleParam + angStep);
690
+ let sin1 = Math.sin(startAngleParam + angStep);
691
+
692
+ // unit arc → cubic
693
+ let c1 = { x: cos0 - sin0 * a, y: sin0 + cos0 * a };
694
+ let c2 = { x: cos1 + sin1 * a, y: sin1 - cos1 * a };
695
+ let e = { x: cos1, y: sin1 };
696
+
697
+ let values = [];
698
+
699
+ [c1, c2, e].forEach(pt => {
700
+ let x = pt.x * rx;
701
+ let y = pt.y * ry;
702
+
703
+ values.push(
704
+ cosphi * x - sinphi * y + centroid.x,
705
+ sinphi * x + cosphi * y + centroid.y
706
+ );
707
+ });
708
+
709
+ pathData.push({
710
+ type: 'C',
711
+ values,
712
+ cp1: { x: values[0], y: values[1] },
713
+ cp2: { x: values[2], y: values[3] },
714
+ p: { x: values[4], y: values[5] },
715
+ });
716
+
717
+ startAngleParam += angStep;
718
+ }
719
+
720
+ return pathData;
721
+ }
722
+
723
+
605
724
 
606
725
  /**
607
726
  * convert arctocommands to cubic bezier
@@ -858,6 +977,221 @@ export function convertArrayPathData(pathDataArray) {
858
977
  * cubics to arcs
859
978
  */
860
979
 
980
+
981
+ export function combineCubicsToArcs(pathData = [], {
982
+ threshold = 0,
983
+ } = {}) {
984
+
985
+ let l = pathData.length;
986
+ let pathDataN = [pathData[0]];
987
+
988
+ for (let i = 1; i < l; i++) {
989
+ let com = pathData[i];
990
+ let { type, cp1 = null, cp2 = null, p0, p } = com;
991
+ let comP = pathData[i - 1];
992
+ let comN = pathData[i + 1] ? pathData[i + 1] : null;
993
+ let comN2 = pathData[i + 2] ? pathData[i + 2] : null;
994
+
995
+ if (type === 'C' && comN && comN.type === 'C') {
996
+
997
+ let thresh = getDistAv(p0, p) * 0.02;
998
+ //thresh = getDistAv(p0, p) * 10000;
999
+
1000
+ let dx1 = Math.abs(p0.x - cp1.x)
1001
+ let dy1 = Math.abs(p0.y - cp1.y)
1002
+
1003
+ let isHorizontal1 = dy1 < thresh;
1004
+ let isVertical1 = dx1 < thresh;
1005
+
1006
+
1007
+ let dx2 = Math.abs(comN.p0.x - comN.cp1.x)
1008
+ let dy2 = Math.abs(comN.p0.y - comN.cp1.y)
1009
+
1010
+ let isHorizontal2 = dy2 < thresh;
1011
+ let isVertical2 = dx2 < thresh;
1012
+
1013
+ //console.log(isHorizontal1, isVertical1);
1014
+
1015
+ // check angles
1016
+ let angleDiff1 = (isHorizontal1 || isVertical1) ? 0 : Infinity;
1017
+ let angleDiff2 = (isHorizontal2 || isVertical2) ? 0 : Infinity;
1018
+
1019
+ if (!isHorizontal1 && !isVertical1) {
1020
+ //console.log('get angles', isHorizontal1, isVertical1);
1021
+ let angle1 = getAngle(p0, cp1, true);
1022
+ let angle2 = getAngle(p, cp2, true);
1023
+ let deltaAngle = Math.abs(angle1 - angle2) * 180 / Math.PI;
1024
+ angleDiff1 = Math.abs((deltaAngle % 180) - 90);
1025
+ }
1026
+
1027
+ if (!isHorizontal2 && !isVertical2) {
1028
+ //console.log('get angles', isHorizontal1, isVertical1);
1029
+ let angle1 = getAngle(p0, cp1, true);
1030
+ let angle2 = getAngle(p, cp2, true);
1031
+ let deltaAngle = Math.abs(angle1 - angle2) * 180 / Math.PI;
1032
+ angleDiff2 = Math.abs((deltaAngle % 180) - 90);
1033
+ }
1034
+
1035
+
1036
+ let isRightAngle1 = angleDiff1 < 3;
1037
+ let isRightAngle2 = angleDiff2 < 3;
1038
+
1039
+ let centroids = [];
1040
+ let poly = [];
1041
+ let rArr = []
1042
+ let largeArc = 0;
1043
+
1044
+ // final on path point
1045
+ let p_a = p
1046
+
1047
+ // 2 possible candidates - test radius
1048
+ if (isRightAngle1 && isRightAngle2) {
1049
+ //renderPoint(markers, com.p)
1050
+
1051
+ let pI = checkLineIntersection(p0, cp1, p, cp2, false);
1052
+ let r1 = getDistance(p0, pI);
1053
+ let r2 = getDistance(p, pI);
1054
+ let rDiff1 = Math.abs(r1 - r2)
1055
+ //let r = r1
1056
+
1057
+ rArr.push(r1, r2)
1058
+
1059
+ poly.push(p0, p)
1060
+ p_a = p
1061
+
1062
+
1063
+ // 2 commands can be combined – similar radii
1064
+ if (rDiff1 < thresh) {
1065
+
1066
+ //renderPoint(markers, com.p)
1067
+
1068
+ // add to polygon for sweep
1069
+ poly.push(comN.p)
1070
+
1071
+ // update final point
1072
+ p_a = comN.p
1073
+
1074
+ // approximate/average final center point for final radius
1075
+ let cp1_r = rotatePoint(cp1, p0.x, p0.y, (Math.PI * -0.5))
1076
+ let cp2_r = rotatePoint(cp2, p.x, p.y, (Math.PI * 0.5))
1077
+
1078
+ let cp1_r2 = rotatePoint(comN.cp1, comN.p0.x, comN.p0.y, (Math.PI * -0.5))
1079
+ let cp2_r2 = rotatePoint(comN.cp2, comN.p.x, comN.p.y, (Math.PI * 0.5))
1080
+
1081
+ // assumed centroid
1082
+ let ptC = checkLineIntersection(p0, cp1_r, p, cp2_r, false)
1083
+ let ptC2 = checkLineIntersection(comN.p0, cp1_r2, comN.p, cp2_r2, false)
1084
+ let distC = ptC && ptC2 ? getDistAv(ptC, ptC2) : Infinity
1085
+
1086
+
1087
+ // 2 commands can definitely be combined
1088
+ if (distC < thresh) {
1089
+ //renderPoint(markers, ptC, 'cyan', '1.2%', '0.5')
1090
+ //renderPoint(markers, ptC2, 'magenta', '0.5%', '0.5')
1091
+
1092
+ // add to centroid array
1093
+ centroids.push(ptC, ptC2)
1094
+
1095
+ }
1096
+
1097
+
1098
+ if (comN2 && comN2.type === 'C') {
1099
+
1100
+ let cp1_r3 = rotatePoint(comN2.cp1, comN2.p0.x, comN2.p0.y, (Math.PI * -0.5))
1101
+ let cp2_r3 = rotatePoint(comN2.cp2, comN2.p.x, comN2.p.y, (Math.PI * 0.5))
1102
+ let ptC3 = checkLineIntersection(comN2.p0, cp1_r3, comN2.p, cp2_r3, false)
1103
+
1104
+ let distC2 = ptC && ptC3 ? getDistAv(ptC, ptC3) : Infinity
1105
+
1106
+ // can be combined with 3rd command
1107
+ if (distC2 < thresh) {
1108
+ //renderPoint(markers, ptC3, 'green', '2%', '0.3')
1109
+
1110
+ let r3 = getDistance(ptC3, comN2.p)
1111
+ rArr.push(r3)
1112
+
1113
+ // update final point
1114
+ p_a = comN2.p
1115
+ poly.push(p, comN2.p)
1116
+
1117
+ largeArc = 1;
1118
+
1119
+ }
1120
+ }
1121
+ //console.log(rDiff1, r, r1, r2);
1122
+
1123
+ } else {
1124
+ pathDataN.push(com)
1125
+ continue
1126
+ }
1127
+
1128
+ }
1129
+
1130
+
1131
+ // create new arc command
1132
+ if (poly.length > 1) {
1133
+
1134
+ // get average radius
1135
+ //rArr = rArr.sort()
1136
+ let rA = Math.max(...rArr)
1137
+ rA = rArr[0]
1138
+
1139
+ let centroidA;
1140
+ let xArr = centroids.map(pt => pt.x)
1141
+ let yArr = centroids.map(pt => pt.y)
1142
+
1143
+ centroidA = {
1144
+ x: (xArr.reduce((a, b) => a + b, 0)) / centroids.length,
1145
+ y: (yArr.reduce((a, b) => a + b, 0)) / centroids.length
1146
+ }
1147
+
1148
+ //console.log(xArr, centroidA);
1149
+
1150
+ //rA = getDistance(p0, centroids[0])
1151
+
1152
+ rA = getDistance(p0, centroidA)
1153
+ let rA2 = getDistance(p, centroidA)
1154
+ //rA = (rA+rA2) /2
1155
+ //rA = Math.min(rA,rA2)
1156
+
1157
+ // rA = ((Math.min(...rArr) * 2 + Math.max(...rArr)) ) / 3
1158
+ //console.log(rArr, rA);
1159
+
1160
+ let area = getPolygonArea(poly, false)
1161
+ let sweep = area < 0 ? 0 : 1;
1162
+
1163
+ let comA = { type: 'A', values: [rA, rA, 0, largeArc, sweep, p_a.x, p_a.y], p0, p: p_a }
1164
+
1165
+ console.log('comA', comA);
1166
+
1167
+ pathDataN.push(comA)
1168
+
1169
+ i += rArr.length - 1;
1170
+ //i++
1171
+ continue
1172
+ /*
1173
+ */
1174
+
1175
+ }
1176
+
1177
+
1178
+
1179
+
1180
+ // test angles
1181
+
1182
+ }
1183
+
1184
+ pathDataN.push(com)
1185
+ }
1186
+
1187
+ let d = pathDataToD(pathDataN)
1188
+ console.log(d);
1189
+
1190
+ console.log('pathDataN', pathDataN);
1191
+ return pathDataN
1192
+
1193
+ }
1194
+
861
1195
  export function cubicCommandToArc(p0, cp1, cp2, p, tolerance = 7.5) {
862
1196
 
863
1197
  //console.log(p0, cp1, cp2, p, segArea );