svg-path-simplify 0.4.3 → 0.4.5

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 (41) hide show
  1. package/CHANGELOG.md +21 -0
  2. package/README.md +2 -1
  3. package/dist/svg-path-simplify.esm.js +1670 -509
  4. package/dist/svg-path-simplify.esm.min.js +2 -2
  5. package/dist/svg-path-simplify.js +1671 -508
  6. package/dist/svg-path-simplify.min.js +2 -2
  7. package/dist/svg-path-simplify.pathdata.esm.js +936 -463
  8. package/dist/svg-path-simplify.pathdata.esm.min.js +2 -2
  9. package/dist/svg-path-simplify.poly.cjs +9 -8
  10. package/index.html +60 -20
  11. package/package.json +1 -1
  12. package/src/constants.js +4 -0
  13. package/src/detect_input.js +47 -29
  14. package/src/index.js +8 -0
  15. package/src/pathData_simplify_cubic.js +46 -18
  16. package/src/pathData_simplify_revertToquadratics.js +0 -1
  17. package/src/pathSimplify-main.js +81 -20
  18. package/src/pathSimplify-only-pathdata.js +7 -2
  19. package/src/pathSimplify-presets.js +14 -4
  20. package/src/svg-getAttributes.js +5 -3
  21. package/src/svgii/convert_units.js +1 -1
  22. package/src/svgii/geometry.js +140 -2
  23. package/src/svgii/geometry_bbox_element.js +1 -1
  24. package/src/svgii/geometry_deduceRadius.js +116 -27
  25. package/src/svgii/geometry_length.js +18 -2
  26. package/src/svgii/pathData_analyze.js +18 -0
  27. package/src/svgii/pathData_convert.js +188 -88
  28. package/src/svgii/pathData_fix_directions.js +10 -18
  29. package/src/svgii/pathData_reorder.js +123 -16
  30. package/src/svgii/pathData_simplify_refineCorners.js +130 -35
  31. package/src/svgii/pathData_simplify_refine_round.js +420 -0
  32. package/src/svgii/poly_normalize.js +9 -8
  33. package/src/svgii/rounding.js +112 -80
  34. package/src/svgii/svg_cleanup.js +75 -22
  35. package/src/svgii/svg_cleanup_convertPathLength.js +27 -15
  36. package/src/svgii/svg_cleanup_normalize_transforms.js +1 -1
  37. package/src/svgii/svg_cleanup_remove_els_and_atts.js +6 -1
  38. package/src/svgii/svg_el_parse_style_props.js +13 -10
  39. package/src/svgii/svg_validate.js +220 -0
  40. package/tests/testSVG.js +14 -1
  41. package/src/svgii/pathData_refine_round.js +0 -222
@@ -321,6 +321,7 @@ export function pointAtT(pts, t = 0.5, getTangent = false, getCpts = false, retu
321
321
  let t1 = 1 - t;
322
322
 
323
323
  // cubic beziers
324
+ /*
324
325
  if (isCubic) {
325
326
  pt = {
326
327
  x:
@@ -336,11 +337,30 @@ export function pointAtT(pts, t = 0.5, getTangent = false, getCpts = false, retu
336
337
  };
337
338
 
338
339
  }
340
+ */
341
+
342
+ if (isCubic) {
343
+ pt = {
344
+ x:
345
+ t1 * t1 * t1 * p0.x +
346
+ 3 * t1 * t1 * t * cp1.x +
347
+ 3 * t1 * t * t * cp2.x +
348
+ t * t * t * p.x,
349
+ y:
350
+ t1 * t1 * t1 * p0.y +
351
+ 3 * t1 * t1 * t * cp1.y +
352
+ 3 * t1 * t * t * cp2.y +
353
+ t * t * t * p.y,
354
+ };
355
+
356
+ }
357
+
358
+
339
359
  // quadratic beziers
340
360
  else {
341
361
  pt = {
342
- x: t1 * t1 * p0.x + 2 * t1 * t * cp1.x + t ** 2 * p.x,
343
- y: t1 * t1 * p0.y + 2 * t1 * t * cp1.y + t ** 2 * p.y,
362
+ x: t1 * t1 * p0.x + 2 * t1 * t * cp1.x + t * t * p.x,
363
+ y: t1 * t1 * p0.y + 2 * t1 * t * cp1.y + t * t * p.y,
344
364
  };
345
365
  }
346
366
 
@@ -963,6 +983,124 @@ export function getBezierExtremeT(pts, { addExtremes = true, addSemiExtremes = f
963
983
  * https://stackoverflow.com/questions/87734/#75031511
964
984
  * See also: https://github.com/foo123/Geometrize
965
985
  */
986
+
987
+ export function getArcExtemesParam({
988
+ cx=0, cy=0, rx=0, ry=0,
989
+ p=null,
990
+ p0=null,
991
+ endAngle=0,
992
+ deltaAngle=0,
993
+
994
+
995
+ }={}) {
996
+ // compute point on ellipse from angle around ellipse (theta)
997
+ const arc = (theta, cx, cy, rx, ry, alpha) => {
998
+ // theta is angle in radians around arc
999
+ // alpha is angle of rotation of ellipse in radians
1000
+ var cos = Math.cos(alpha),
1001
+ sin = Math.sin(alpha),
1002
+ x = rx * Math.cos(theta),
1003
+ y = ry * Math.sin(theta);
1004
+
1005
+ return {
1006
+ x: cx + cos * x - sin * y,
1007
+ y: cy + sin * x + cos * y
1008
+ };
1009
+ }
1010
+
1011
+ //parametrize arcto data
1012
+ //let arcData = svgArcToCenterParam(p0.x, p0.y, values[0], values[1], values[2], values[3], values[4], values[5], values[6]);
1013
+ //let { rx, ry, cx, cy, endAngle, deltaAngle } = arcData;
1014
+
1015
+ // arc rotation
1016
+ let deg = values[2];
1017
+
1018
+ // final on path point
1019
+ //let p = { x: values[5], y: values[6] }
1020
+
1021
+ // collect extreme points – add end point
1022
+ let extremes = [p]
1023
+
1024
+ // rotation to radians
1025
+ let alpha = deg * Math.PI / 180;
1026
+ let tan = Math.tan(alpha),
1027
+ p1, p2, p3, p4, theta;
1028
+
1029
+ /**
1030
+ * find min/max from zeroes of directional derivative along x and y
1031
+ * along x axis
1032
+ */
1033
+ theta = Math.atan2(-ry * tan, rx);
1034
+
1035
+ let angle1 = theta;
1036
+ let angle2 = theta + Math.PI;
1037
+ let angle3 = Math.atan2(ry, rx * tan);
1038
+ let angle4 = angle3 + Math.PI;
1039
+
1040
+
1041
+ // inner bounding box
1042
+ let xArr = [p0.x, p.x]
1043
+ let yArr = [p0.y, p.y]
1044
+ let xMin = Math.min(...xArr)
1045
+ let xMax = Math.max(...xArr)
1046
+ let yMin = Math.min(...yArr)
1047
+ let yMax = Math.max(...yArr)
1048
+
1049
+
1050
+ // on path point close after start
1051
+ let angleAfterStart = endAngle - deltaAngle * 0.001
1052
+ let pP2 = arc(angleAfterStart, cx, cy, rx, ry, alpha);
1053
+
1054
+ // on path point close before end
1055
+ let angleBeforeEnd = endAngle - deltaAngle * 0.999
1056
+ let pP3 = arc(angleBeforeEnd, cx, cy, rx, ry, alpha);
1057
+
1058
+
1059
+ /**
1060
+ * expected extremes
1061
+ * if leaving inner bounding box
1062
+ * (between segment start and end point)
1063
+ * otherwise exclude elliptic extreme points
1064
+ */
1065
+
1066
+ // right
1067
+ if (pP2.x > xMax || pP3.x > xMax) {
1068
+ // get point for this theta
1069
+ p1 = arc(angle1, cx, cy, rx, ry, alpha);
1070
+ extremes.push(p1)
1071
+ }
1072
+
1073
+ // left
1074
+ if (pP2.x < xMin || pP3.x < xMin) {
1075
+ // get anti-symmetric point
1076
+ p2 = arc(angle2, cx, cy, rx, ry, alpha);
1077
+ extremes.push(p2)
1078
+ }
1079
+
1080
+ // top
1081
+ if (pP2.y < yMin || pP3.y < yMin) {
1082
+ // get anti-symmetric point
1083
+ p4 = arc(angle4, cx, cy, rx, ry, alpha);
1084
+ extremes.push(p4)
1085
+ }
1086
+
1087
+ // bottom
1088
+ if (pP2.y > yMax || pP3.y > yMax) {
1089
+ // get point for this theta
1090
+ p3 = arc(angle3, cx, cy, rx, ry, alpha);
1091
+ extremes.push(p3)
1092
+ }
1093
+
1094
+ return extremes;
1095
+ }
1096
+
1097
+
1098
+
1099
+
1100
+
1101
+
1102
+
1103
+
966
1104
  export function getArcExtemes(p0, values) {
967
1105
  // compute point on ellipse from angle around ellipse (theta)
968
1106
  const arc = (theta, cx, cy, rx, ry, alpha) => {
@@ -12,7 +12,7 @@ export function getElBBox(el){
12
12
 
13
13
  switch(type){
14
14
  case 'path':
15
- let pathData = parsePathDataNormalized(atts.d)
15
+ let pathData = parsePathDataNormalized(el.getAttribute('d'))
16
16
  bb=getPolyBBox(getPathDataPoly(pathData))
17
17
 
18
18
  break;
@@ -1,42 +1,92 @@
1
- import { getDeltaAngle, getDistance } from "./geometry";
1
+ import { checkLineIntersection, getDeltaAngle, getDistance, getDistManhattan, getSquareDistance } from "./geometry";
2
+ import { renderPoint } from "./visualize";
3
+ //import { arcToBezierResolved } from "./pathData_convert";
4
+ //import { renderPoint, renderPoly } from "./visualize";
2
5
 
3
- export function getArcFromPoly(pts) {
6
+ export function getArcFromPoly(pts, precise = false) {
4
7
  if (pts.length < 3) return false
5
8
 
6
9
  // 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
+ let len = pts.length
11
+ let idx1 = Math.floor(len * 0.333)
12
+ let idx2 = Math.floor(len * 0.666)
13
+ let idx3 = Math.floor(len * 0.5)
10
14
 
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
 
15
- let a = x1 - x2;
16
- let b = y1 - y2;
17
- let c = x1 - x3;
18
- let d = y1 - y3;
16
+ let p1 = pts[0];
17
+ let p2 = pts[idx3];
18
+ let p3 = pts[len - 1];
19
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;
20
+ // Radius (use start point)
21
+ let pts1 = [p1, p2, p3];
22
+ let centroid = getPolyArcCentroid(pts1);
22
23
 
23
- let det = a * d - b * c;
24
+ let r = 0, deltaAngle = 0, startAngle = 0, endAngle = 0, angleData = {};
24
25
 
25
- if (Math.abs(det) < 1e-10) {
26
- console.warn("Points are collinear or numerically unstable");
27
- return false;
28
- }
29
26
 
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 };
27
+ // check if radii are consistent
28
+ if (precise) {
29
+
30
+
31
+ /**
32
+ * check multiple centroids
33
+ * if the polyline can be expressed as
34
+ * an arc - all centroids should be close
35
+ */
36
+
37
+ if (len > 3) {
38
+ let centroid1 = getPolyArcCentroid([p1, pts[idx1], p3]);
39
+ let centroid2 = getPolyArcCentroid([p1, pts[idx2], p3]);
40
+
41
+ if (!centroid1 || !centroid2) return false;
42
+
43
+ //let dist0 = getDistManhattan(p1, p3)
44
+ let dist0 = getDistManhattan(centroid, p2)
45
+ let dist1 = getDistManhattan(centroid, centroid1)
46
+ let dist2 = getDistManhattan(centroid, centroid2)
47
+ let errorCentroid = (dist1 + dist2)
48
+
49
+ // centroids diverging too much
50
+ if (errorCentroid > dist0 * 0.05) {
51
+ //renderPoint(markers, centroid, 'magenta')
52
+ return false
53
+ }
54
+
55
+ }
56
+
57
+ let rSqMid = getSquareDistance(centroid, p2);
58
+
59
+ //check if radii are close enough
60
+ for (let i = 0; i < len; i++) {
61
+ let pt = pts[i]
62
+ let rSq = getSquareDistance(centroid, pt);
63
+ let error = Math.abs(rSqMid - rSq) / rSqMid
64
+
65
+ if (error > 0.0025) {
66
+ /*
67
+ console.log('error', error, len, idx1, idx2, idx3);
68
+ renderPoint(markers, centroid, 'orange')
69
+ renderPoint(markers, p1, 'green')
70
+ renderPoint(markers, p2)
71
+ renderPoint(markers, p3, 'purple')
72
+ */
73
+ return false;
74
+ }
75
+ }
76
+
77
+ // calculate proper radius
78
+ r = Math.sqrt(rSqMid);
79
+ angleData = getDeltaAngle(centroid, p1, p3);
80
+ ({ deltaAngle, startAngle, endAngle } = angleData);
81
+
82
+
83
+ } else {
84
+ r = getDistance(centroid, p1);
85
+ angleData = getDeltaAngle(centroid, p1, p3);
86
+ ({ deltaAngle, startAngle, endAngle } = angleData);
87
+ }
34
88
 
35
- // Radius (use start point)
36
- let r = getDistance(centroid, p1);
37
89
 
38
- let angleData = getDeltaAngle(centroid, p1, p3)
39
- let {deltaAngle, startAngle, endAngle} = angleData;
40
90
 
41
91
  return {
42
92
  centroid,
@@ -48,3 +98,42 @@ export function getArcFromPoly(pts) {
48
98
  }
49
99
 
50
100
 
101
+
102
+ export function getPolyArcCentroid(pts = []) {
103
+
104
+ pts = pts.filter(pt => pt !== undefined);
105
+ if (pts.length < 3) return false
106
+ //console.log(pts);
107
+
108
+ let p1 = pts[0];
109
+ let p2 = pts[Math.floor(pts.length / 2)];
110
+ let p3 = pts[pts.length - 1];
111
+
112
+ let x1 = p1.x, y1 = p1.y;
113
+ let x2 = p2.x, y2 = p2.y;
114
+ let x3 = p3.x, y3 = p3.y;
115
+
116
+ let a = x1 - x2;
117
+ let b = y1 - y2;
118
+ let c = x1 - x3;
119
+ let d = y1 - y3;
120
+
121
+ let e = ((x1 * x1 - x2 * x2) + (y1 * y1 - y2 * y2)) / 2;
122
+ let f = ((x1 * x1 - x3 * x3) + (y1 * y1 - y3 * y3)) / 2;
123
+
124
+ let det = a * d - b * c;
125
+
126
+ // colinear points
127
+ if (Math.abs(det) < 1e-10) {
128
+ return false;
129
+ }
130
+
131
+ // find center of arc
132
+ let cx = (d * e - b * f) / det;
133
+ let cy = (-c * e + a * f) / det;
134
+ let centroid = { x: cx, y: cy };
135
+ return centroid
136
+ }
137
+
138
+
139
+
@@ -1,4 +1,4 @@
1
- import { getDistance } from "./geometry";
1
+ import { getDistance, getDistManhattan } from "./geometry";
2
2
 
3
3
  // Legendre Gauss weight and abscissa values
4
4
  export const waArr_global = [];
@@ -111,7 +111,7 @@ export function getLength(pts, {
111
111
 
112
112
  // LG weight/abscissae generator
113
113
  export function getLegendreGaussValues(n, x1 = -1, x2 = 1) {
114
- console.log('add new LG', n);
114
+ //console.log('add new LG', n);
115
115
 
116
116
  let waArr = []
117
117
  let z1, z, xm, xl, pp, p3, p2, p1;
@@ -176,6 +176,22 @@ export function getPolygonLength(pts=[], isPoly=false){
176
176
  return len
177
177
  }
178
178
 
179
+ export function getPolygonLengthManhattan(pts=[], isPoly=false){
180
+
181
+ let len = 0;
182
+ let l=pts.length;
183
+
184
+ for(let i=1; i<l; i++){
185
+ let p1 = pts[i-1]
186
+ let p2 = pts[i]
187
+ len += getDistManhattan(p1, p2)
188
+ }
189
+ if(isPoly){
190
+ len += getDistManhattan(pts[l-1], pts[0])
191
+ }
192
+ return len
193
+ }
194
+
179
195
 
180
196
  /**
181
197
  * Ramanujan approximation
@@ -67,6 +67,7 @@ export function analyzePathData(pathData = [], {
67
67
  let { type, values, p0, p, cp1 = null, cp2 = null, squareDist = 0, cptArea = 0, dimA = 0 } = com;
68
68
 
69
69
  //next command
70
+ let comPrev = pathData[c-2];
70
71
  let comN = pathData[c] || null;
71
72
 
72
73
 
@@ -93,6 +94,7 @@ export function analyzePathData(pathData = [], {
93
94
 
94
95
  // bezier types
95
96
  let isBezier = type === 'Q' || type === 'C';
97
+ let isArc = type === 'A';
96
98
  let isBezierN = comN && (comN.type === 'Q' || comN.type === 'C');
97
99
 
98
100
 
@@ -149,6 +151,22 @@ export function analyzePathData(pathData = [], {
149
151
  }
150
152
  }
151
153
 
154
+ // check extremes introduce by small arcs
155
+ else if(isArc && comN && ((comPrev.type==='C' || comPrev.type==='Q') || (comN.type==='C' || comN.type==='Q')) ){
156
+ let distN = comN ? comN.dimA : 0
157
+ let isShort = com.dimA < (comPrev.dimA + distN) * 0.1;
158
+ let smallRadius = com.values[0] === com.values[1] && (com.values[0] < 1)
159
+
160
+ if(isShort && smallRadius){
161
+ let bb = getPolyBBox([comPrev.p0, comN.p])
162
+ if(p.x>bb.right || p.x<bb.x || p.y<bb.y || p.y>bb.bottom){
163
+ hasExtremes = true;
164
+ //renderPoint(markers, p)
165
+ }
166
+ }
167
+
168
+ }
169
+
152
170
  if (hasExtremes) com.extreme = true
153
171
 
154
172
  // Corners and semi extremes