svg-path-simplify 0.0.1 → 0.0.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.
- package/README.md +28 -1
- package/dist/svg-path-simplify.esm.js +4040 -0
- package/dist/svg-path-simplify.esm.min.js +1 -0
- package/dist/svg-path-simplify.js +4065 -0
- package/dist/svg-path-simplify.min.js +1 -0
- package/dist/svg-path-simplify.node.js +4062 -0
- package/dist/svg-path-simplify.node.min.js +1 -0
- package/index.html +222 -0
- package/package.json +2 -2
- package/src/constants.js +4 -0
- package/src/index.js +18 -3
- package/src/pathData_simplify_cubic.js +324 -0
- package/src/pathData_simplify_cubic_arr.js +50 -0
- package/src/pathData_simplify_cubic_extrapolate.js +220 -0
- package/src/pathSimplify-main.js +294 -0
- package/src/svgii/...parse.js +402 -0
- package/src/svgii/geometry.js +1096 -0
- package/src/svgii/geometry_area.js +265 -0
- package/src/svgii/geometry_bbox.js +223 -0
- package/src/svgii/pathData_analyze.js +896 -0
- package/src/svgii/pathData_convert.js +1180 -0
- package/src/svgii/pathData_parse.js +487 -0
- package/src/svgii/pathData_remove_collinear.js +85 -0
- package/src/svgii/pathData_remove_zerolength.js +28 -0
- package/src/svgii/pathData_reorder.js +204 -0
- package/src/svgii/pathData_reverse.js +124 -0
- package/src/svgii/pathData_scale.js +42 -0
- package/src/svgii/pathData_split.js +449 -0
- package/src/svgii/pathData_stringify.js +146 -0
- package/src/svgii/pathData_toPolygon.js +92 -0
- package/src/svgii/pathdata_cleanup.js +363 -0
- package/src/svgii/poly_analyze.js +172 -0
- package/src/svgii/poly_to_pathdata.js +185 -0
- package/src/svgii/rounding.js +154 -0
- package/src/svgii/simplify.js +248 -0
- package/src/svgii/simplify_bezier.js +470 -0
- package/src/svgii/simplify_linetos.js +93 -0
- package/src/svgii/simplify_polygon.js +135 -0
- package/src/svgii/stringify.js +103 -0
- package/src/svgii/svg_cleanup.js +80 -0
- package/src/svgii/visualize.js +317 -0
- package/LICENSE +0 -21
|
@@ -0,0 +1,1096 @@
|
|
|
1
|
+
/*
|
|
2
|
+
import {abs, acos, asin, atan, atan2, ceil, cos, exp, floor,
|
|
3
|
+
log, max, min, pow, random, round, sin, sqrt, tan, PI} from '/.constants.js';
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export const {
|
|
7
|
+
abs, acos, asin, atan, atan2, ceil, cos, exp, floor,
|
|
8
|
+
log, max, min, pow, random, round, sin, sqrt, tan, PI
|
|
9
|
+
} = Math;
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
// get angle helper
|
|
13
|
+
export function getAngle(p1, p2, normalize = false) {
|
|
14
|
+
let angle = atan2(p2.y - p1.y, p2.x - p1.x);
|
|
15
|
+
// normalize negative angles
|
|
16
|
+
if (normalize && angle < 0) angle += Math.PI * 2
|
|
17
|
+
return angle
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* based on: Justin C. Round's
|
|
23
|
+
* http://jsfiddle.net/justin_c_rounds/Gd2S2/light/
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
export function checkLineIntersection(p1, p2, p3, p4, exact = true) {
|
|
27
|
+
// 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
|
+
let denominator, a, b, numerator1, numerator2;
|
|
29
|
+
let intersectionPoint = {}
|
|
30
|
+
|
|
31
|
+
try {
|
|
32
|
+
denominator = ((p4.y - p3.y) * (p2.x - p1.x)) - ((p4.x - p3.x) * (p2.y - p1.y));
|
|
33
|
+
if (denominator == 0) {
|
|
34
|
+
return false;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
} catch {
|
|
38
|
+
console.log('!catch', p1, p2, 'p3:', p3, p4);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
a = p1.y - p3.y;
|
|
42
|
+
b = p1.x - p3.x;
|
|
43
|
+
numerator1 = ((p4.x - p3.x) * a) - ((p4.y - p3.y) * b);
|
|
44
|
+
numerator2 = ((p2.x - p1.x) * a) - ((p2.y - p1.y) * b);
|
|
45
|
+
|
|
46
|
+
a = numerator1 / denominator;
|
|
47
|
+
b = numerator2 / denominator;
|
|
48
|
+
|
|
49
|
+
// if we cast these lines infinitely in both directions, they intersect here:
|
|
50
|
+
intersectionPoint = {
|
|
51
|
+
x: p1.x + (a * (p2.x - p1.x)),
|
|
52
|
+
y: p1.y + (a * (p2.y - p1.y))
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// console.log('intersectionPoint', intersectionPoint, p1, p2);
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
let intersection = false;
|
|
60
|
+
// if line1 is a segment and line2 is infinite, they intersect if:
|
|
61
|
+
if ((a > 0 && a < 1) && (b > 0 && b < 1)) {
|
|
62
|
+
intersection = true;
|
|
63
|
+
//console.log('line inters');
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (exact && !intersection) {
|
|
67
|
+
//console.log('no line inters');
|
|
68
|
+
return false;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// if line1 and line2 are segments, they intersect if both of the above are true
|
|
72
|
+
//console.log('inter', intersectionPoint)
|
|
73
|
+
return intersectionPoint;
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* get distance between 2 points
|
|
80
|
+
* pythagorean theorem
|
|
81
|
+
*/
|
|
82
|
+
export function getDistance(p1, p2) {
|
|
83
|
+
return sqrt(
|
|
84
|
+
(p2.x - p1.x) * (p2.x - p1.x) + (p2.y - p1.y) * (p2.y - p1.y)
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function getSquareDistance(p1, p2) {
|
|
89
|
+
return (p2.x - p1.x) ** 2 + (p2.y - p1.y) ** 2
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function lineLength(p1, p2) {
|
|
93
|
+
return sqrt(
|
|
94
|
+
(p2.x - p1.x) * (p2.x - p1.x) + (p2.y - p1.y) * (p2.y - p1.y)
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Linear interpolation (LERP) helper
|
|
101
|
+
*/
|
|
102
|
+
export function interpolate(p1, p2, t, getTangent = false) {
|
|
103
|
+
|
|
104
|
+
let pt = {
|
|
105
|
+
x: (p2.x - p1.x) * t + p1.x,
|
|
106
|
+
y: (p2.y - p1.y) * t + p1.y,
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
if (getTangent) {
|
|
110
|
+
pt.angle = getAngle(p1, p2)
|
|
111
|
+
|
|
112
|
+
// normalize negative angles
|
|
113
|
+
if (pt.angle < 0) pt.angle += PI * 2
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return pt
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
export function pointAtT(pts, t = 0.5, getTangent = false, getCpts = false) {
|
|
121
|
+
|
|
122
|
+
const getPointAtBezierT = (pts, t, getTangent = false) => {
|
|
123
|
+
|
|
124
|
+
let isCubic = pts.length === 4;
|
|
125
|
+
let p0 = pts[0];
|
|
126
|
+
let cp1 = pts[1];
|
|
127
|
+
let cp2 = isCubic ? pts[2] : pts[1];
|
|
128
|
+
let p = pts[pts.length - 1];
|
|
129
|
+
let pt = { x: 0, y: 0 };
|
|
130
|
+
|
|
131
|
+
if (getTangent || getCpts) {
|
|
132
|
+
let m0, m1, m2, m3, m4;
|
|
133
|
+
let shortCp1 = p0.x === cp1.x && p0.y === cp1.y;
|
|
134
|
+
let shortCp2 = p.x === cp2.x && p.y === cp2.y;
|
|
135
|
+
|
|
136
|
+
if (t === 0 && !shortCp1) {
|
|
137
|
+
pt.x = p0.x;
|
|
138
|
+
pt.y = p0.y;
|
|
139
|
+
pt.angle = getAngle(p0, cp1)
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
else if (t === 1 && !shortCp2) {
|
|
143
|
+
pt.x = p.x;
|
|
144
|
+
pt.y = p.y;
|
|
145
|
+
pt.angle = getAngle(cp2, p)
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
else {
|
|
149
|
+
// adjust if cps are on start or end point
|
|
150
|
+
if (shortCp1) t += 0.0000001;
|
|
151
|
+
if (shortCp2) t -= 0.0000001;
|
|
152
|
+
|
|
153
|
+
m0 = interpolate(p0, cp1, t);
|
|
154
|
+
if (isCubic) {
|
|
155
|
+
m1 = interpolate(cp1, cp2, t);
|
|
156
|
+
m2 = interpolate(cp2, p, t);
|
|
157
|
+
m3 = interpolate(m0, m1, t);
|
|
158
|
+
m4 = interpolate(m1, m2, t);
|
|
159
|
+
pt = interpolate(m3, m4, t);
|
|
160
|
+
|
|
161
|
+
// add angles
|
|
162
|
+
pt.angle = getAngle(m3, m4)
|
|
163
|
+
|
|
164
|
+
// add control points
|
|
165
|
+
if (getCpts) pt.cpts = [m1, m2, m3, m4];
|
|
166
|
+
} else {
|
|
167
|
+
m1 = interpolate(p0, cp1, t);
|
|
168
|
+
m2 = interpolate(cp1, p, t);
|
|
169
|
+
pt = interpolate(m1, m2, t);
|
|
170
|
+
pt.angle = getAngle(m1, m2);
|
|
171
|
+
|
|
172
|
+
// add control points
|
|
173
|
+
if (getCpts) pt.cpts = [m1, m2];
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
}
|
|
178
|
+
// take simplified calculations without tangent angles
|
|
179
|
+
else {
|
|
180
|
+
let t1 = 1 - t;
|
|
181
|
+
|
|
182
|
+
// cubic beziers
|
|
183
|
+
if (isCubic) {
|
|
184
|
+
pt = {
|
|
185
|
+
x:
|
|
186
|
+
t1 ** 3 * p0.x +
|
|
187
|
+
3 * t1 ** 2 * t * cp1.x +
|
|
188
|
+
3 * t1 * t ** 2 * cp2.x +
|
|
189
|
+
t ** 3 * p.x,
|
|
190
|
+
y:
|
|
191
|
+
t1 ** 3 * p0.y +
|
|
192
|
+
3 * t1 ** 2 * t * cp1.y +
|
|
193
|
+
3 * t1 * t ** 2 * cp2.y +
|
|
194
|
+
t ** 3 * p.y,
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
}
|
|
198
|
+
// quadratic beziers
|
|
199
|
+
else {
|
|
200
|
+
pt = {
|
|
201
|
+
x: t1 * t1 * p0.x + 2 * t1 * t * cp1.x + t ** 2 * p.x,
|
|
202
|
+
y: t1 * t1 * p0.y + 2 * t1 * t * cp1.y + t ** 2 * p.y,
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
return pt
|
|
209
|
+
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
let pt;
|
|
213
|
+
if (pts.length > 2) {
|
|
214
|
+
pt = getPointAtBezierT(pts, t, getTangent);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
else {
|
|
218
|
+
pt = interpolate(pts[0], pts[1], t, getTangent)
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// normalize negative angles
|
|
222
|
+
if (getTangent && pt.angle < 0) pt.angle += PI * 2
|
|
223
|
+
|
|
224
|
+
return pt
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* get vertices from path command final on-path points
|
|
231
|
+
*/
|
|
232
|
+
export function getPathDataVertices(pathData) {
|
|
233
|
+
let polyPoints = [];
|
|
234
|
+
let p0 = { x: pathData[0].values[0], y: pathData[0].values[1] };
|
|
235
|
+
|
|
236
|
+
pathData.forEach((com) => {
|
|
237
|
+
let { type, values } = com;
|
|
238
|
+
// get final on path point from last 2 values
|
|
239
|
+
if (values.length) {
|
|
240
|
+
let pt = values.length > 1 ? { x: values[values.length - 2], y: values[values.length - 1] }
|
|
241
|
+
: (type === 'V' ? { x: p0.x, y: values[0] } : { x: values[0], y: p0.y });
|
|
242
|
+
polyPoints.push(pt);
|
|
243
|
+
p0 = pt;
|
|
244
|
+
}
|
|
245
|
+
});
|
|
246
|
+
return polyPoints;
|
|
247
|
+
};
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* based on @cuixiping;
|
|
253
|
+
* https://stackoverflow.com/questions/9017100/calculate-center-of-svg-arc/12329083#12329083
|
|
254
|
+
*/
|
|
255
|
+
export function svgArcToCenterParam(x1, y1, rx, ry, xAxisRotation, largeArc, sweep, x2, y2) {
|
|
256
|
+
|
|
257
|
+
// helper for angle calculation
|
|
258
|
+
const getAngle = (cx, cy, x, y) => {
|
|
259
|
+
return atan2(y - cy, x - cx);
|
|
260
|
+
};
|
|
261
|
+
|
|
262
|
+
// make sure rx, ry are positive
|
|
263
|
+
rx = abs(rx);
|
|
264
|
+
ry = abs(ry);
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
// create data object
|
|
268
|
+
let arcData = {
|
|
269
|
+
cx: 0,
|
|
270
|
+
cy: 0,
|
|
271
|
+
// rx/ry values may be deceptive in arc commands
|
|
272
|
+
rx: rx,
|
|
273
|
+
ry: ry,
|
|
274
|
+
startAngle: 0,
|
|
275
|
+
endAngle: 0,
|
|
276
|
+
deltaAngle: 0,
|
|
277
|
+
clockwise: sweep,
|
|
278
|
+
// copy explicit arc properties
|
|
279
|
+
xAxisRotation,
|
|
280
|
+
largeArc,
|
|
281
|
+
sweep
|
|
282
|
+
};
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
if (rx == 0 || ry == 0) {
|
|
286
|
+
// invalid arguments
|
|
287
|
+
throw Error("rx and ry can not be 0");
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
let shortcut = true
|
|
291
|
+
//console.log('short');
|
|
292
|
+
|
|
293
|
+
if (rx === ry && shortcut) {
|
|
294
|
+
|
|
295
|
+
// test semicircles
|
|
296
|
+
let diffX = Math.abs(x2 - x1)
|
|
297
|
+
let diffY = Math.abs(y2 - y1)
|
|
298
|
+
let r = diffX;
|
|
299
|
+
|
|
300
|
+
let xMin = Math.min(x1, x2),
|
|
301
|
+
yMin = Math.min(y1, y2),
|
|
302
|
+
PIHalf = Math.PI * 0.5
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
// semi circles
|
|
306
|
+
if (diffX === 0 && diffY || diffY === 0 && diffX) {
|
|
307
|
+
//console.log('semi');
|
|
308
|
+
|
|
309
|
+
r = diffX === 0 && diffY ? diffY / 2 : diffX / 2;
|
|
310
|
+
arcData.rx = r
|
|
311
|
+
arcData.ry = r
|
|
312
|
+
|
|
313
|
+
// verical
|
|
314
|
+
if (diffX === 0 && diffY) {
|
|
315
|
+
arcData.cx = x1;
|
|
316
|
+
arcData.cy = yMin + diffY / 2;
|
|
317
|
+
arcData.startAngle = y1 > y2 ? PIHalf : -PIHalf
|
|
318
|
+
arcData.endAngle = y1 > y2 ? -PIHalf : PIHalf
|
|
319
|
+
arcData.deltaAngle = sweep ? Math.PI : -Math.PI
|
|
320
|
+
|
|
321
|
+
}
|
|
322
|
+
// horizontal
|
|
323
|
+
else if (diffY === 0 && diffX) {
|
|
324
|
+
arcData.cx = xMin + diffX / 2;
|
|
325
|
+
arcData.cy = y1
|
|
326
|
+
arcData.startAngle = x1 > x2 ? Math.PI : 0
|
|
327
|
+
arcData.endAngle = x1 > x2 ? -Math.PI : Math.PI
|
|
328
|
+
arcData.deltaAngle = sweep ? Math.PI : -Math.PI
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
//console.log(arcData);
|
|
332
|
+
return arcData;
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/**
|
|
337
|
+
* if rx===ry x-axis rotation is ignored
|
|
338
|
+
* otherwise convert degrees to radians
|
|
339
|
+
*/
|
|
340
|
+
let phi = rx === ry ? 0 : (xAxisRotation * PI) / 180;
|
|
341
|
+
let cx, cy
|
|
342
|
+
|
|
343
|
+
let s_phi = !phi ? 0 : sin(phi);
|
|
344
|
+
let c_phi = !phi ? 1 : cos(phi);
|
|
345
|
+
|
|
346
|
+
let hd_x = (x1 - x2) / 2;
|
|
347
|
+
let hd_y = (y1 - y2) / 2;
|
|
348
|
+
let hs_x = (x1 + x2) / 2;
|
|
349
|
+
let hs_y = (y1 + y2) / 2;
|
|
350
|
+
|
|
351
|
+
// F6.5.1
|
|
352
|
+
let x1_ = !phi ? hd_x : c_phi * hd_x + s_phi * hd_y;
|
|
353
|
+
let y1_ = !phi ? hd_y : c_phi * hd_y - s_phi * hd_x;
|
|
354
|
+
|
|
355
|
+
// F.6.6 Correction of out-of-range radii
|
|
356
|
+
// Step 3: Ensure radii are large enough
|
|
357
|
+
let lambda = (x1_ * x1_) / (rx * rx) + (y1_ * y1_) / (ry * ry);
|
|
358
|
+
if (lambda > 1) {
|
|
359
|
+
rx = rx * sqrt(lambda);
|
|
360
|
+
ry = ry * sqrt(lambda);
|
|
361
|
+
|
|
362
|
+
// save real rx/ry
|
|
363
|
+
arcData.rx = rx;
|
|
364
|
+
arcData.ry = ry;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
let rxry = rx * ry;
|
|
368
|
+
let rxy1_ = rx * y1_;
|
|
369
|
+
let ryx1_ = ry * x1_;
|
|
370
|
+
let sum_of_sq = rxy1_ ** 2 + ryx1_ ** 2; // sum of square
|
|
371
|
+
if (!sum_of_sq) {
|
|
372
|
+
//console.log('error:', rx, ry, rxy1_, ryx1_);
|
|
373
|
+
throw Error("start point can not be same as end point");
|
|
374
|
+
}
|
|
375
|
+
let coe = sqrt(abs((rxry * rxry - sum_of_sq) / sum_of_sq));
|
|
376
|
+
if (largeArc == sweep) {
|
|
377
|
+
coe = -coe;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
// F6.5.2
|
|
381
|
+
let cx_ = (coe * rxy1_) / ry;
|
|
382
|
+
let cy_ = (-coe * ryx1_) / rx;
|
|
383
|
+
|
|
384
|
+
/** F6.5.3
|
|
385
|
+
* center point of ellipse
|
|
386
|
+
*/
|
|
387
|
+
cx = !phi ? hs_x + cx_ : c_phi * cx_ - s_phi * cy_ + hs_x;
|
|
388
|
+
cy = !phi ? hs_y + cy_ : s_phi * cx_ + c_phi * cy_ + hs_y;
|
|
389
|
+
arcData.cy = cy;
|
|
390
|
+
arcData.cx = cx;
|
|
391
|
+
|
|
392
|
+
/** F6.5.5
|
|
393
|
+
* calculate angles between center point and
|
|
394
|
+
* commands starting and final on path point
|
|
395
|
+
*/
|
|
396
|
+
let startAngle = getAngle(cx, cy, x1, y1);
|
|
397
|
+
let endAngle = getAngle(cx, cy, x2, y2);
|
|
398
|
+
|
|
399
|
+
// adjust end angle
|
|
400
|
+
if (!sweep && endAngle > startAngle) {
|
|
401
|
+
//console.log('adj neg');
|
|
402
|
+
endAngle -= Math.PI * 2
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
if (sweep && startAngle > endAngle) {
|
|
406
|
+
//console.log('adj pos');
|
|
407
|
+
endAngle = endAngle <= 0 ? endAngle + Math.PI * 2 : endAngle
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
let deltaAngle = endAngle - startAngle
|
|
411
|
+
arcData.startAngle = startAngle;
|
|
412
|
+
arcData.endAngle = endAngle;
|
|
413
|
+
arcData.deltaAngle = deltaAngle;
|
|
414
|
+
|
|
415
|
+
//console.log('arc', arcData);
|
|
416
|
+
return arcData;
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
|
|
420
|
+
|
|
421
|
+
export function rotatePoint(pt, cx, cy, rotation = 0, convertToRadians = false) {
|
|
422
|
+
if (!rotation) return pt;
|
|
423
|
+
|
|
424
|
+
rotation = convertToRadians ? (rotation / 180) * Math.PI : rotation;
|
|
425
|
+
|
|
426
|
+
return {
|
|
427
|
+
x: cx + (pt.x - cx) * Math.cos(rotation) - (pt.y - cy) * Math.sin(rotation),
|
|
428
|
+
y: cy + (pt.x - cx) * Math.sin(rotation) + (pt.y - cy) * Math.cos(rotation)
|
|
429
|
+
};
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
|
|
433
|
+
|
|
434
|
+
|
|
435
|
+
export function reducepts(pts, max = 48) {
|
|
436
|
+
if (!Array.isArray(pts) || pts.length <= max) return pts;
|
|
437
|
+
|
|
438
|
+
// Calculate how many pts to skip between kept pts
|
|
439
|
+
let len = pts.length;
|
|
440
|
+
let step = len / max;
|
|
441
|
+
let reduced = [];
|
|
442
|
+
|
|
443
|
+
for (let i = 0; i < max; i++) {
|
|
444
|
+
reduced.push(pts[Math.floor(i * step)]);
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
let lenR = reduced.length;
|
|
448
|
+
// Always include the last point to maintain path integrity
|
|
449
|
+
if (reduced[lenR - 1] !== pts[len - 1]) {
|
|
450
|
+
reduced[lenR - 1] = pts[len - 1];
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
return reduced;
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
|
|
457
|
+
export function sortPolygonLeftTopFirst(pts) {
|
|
458
|
+
if (pts.length === 0) return pts.slice();
|
|
459
|
+
|
|
460
|
+
let firstIndex = 0;
|
|
461
|
+
for (let i = 1; i < pts.length; i++) {
|
|
462
|
+
const current = pts[i];
|
|
463
|
+
const first = pts[firstIndex];
|
|
464
|
+
if (current.x < first.x || (current.x === first.x && current.y < first.y)) {
|
|
465
|
+
firstIndex = i;
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
return pts.slice(firstIndex).concat(pts.slice(0, firstIndex));
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
|
|
473
|
+
export function getPointOnEllipse(cx, cy, rx, ry, angle, ellipseRotation = 0, parametricAngle = true, degrees = false) {
|
|
474
|
+
|
|
475
|
+
|
|
476
|
+
//console.log(cx, cy, rx, ry, angle, ellipseRotation, parametricAngle);
|
|
477
|
+
|
|
478
|
+
// Convert degrees to radians
|
|
479
|
+
angle = degrees ? (angle * PI) / 180 : angle;
|
|
480
|
+
ellipseRotation = degrees ? (ellipseRotation * PI) / 180 : ellipseRotation;
|
|
481
|
+
// reset rotation for circles or 360 degree
|
|
482
|
+
ellipseRotation = rx !== ry ? (ellipseRotation !== PI * 2 ? ellipseRotation : 0) : 0;
|
|
483
|
+
|
|
484
|
+
// is ellipse
|
|
485
|
+
if (parametricAngle && rx !== ry) {
|
|
486
|
+
// adjust angle for ellipse rotation
|
|
487
|
+
angle = ellipseRotation ? angle - ellipseRotation : angle;
|
|
488
|
+
// Get the parametric angle for the ellipse
|
|
489
|
+
let angleParametric = atan(tan(angle) * (rx / ry));
|
|
490
|
+
// Ensure the parametric angle is in the correct quadrant
|
|
491
|
+
angle = cos(angle) < 0 ? angleParametric + PI : angleParametric;
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
// Calculate the point on the ellipse without rotation
|
|
495
|
+
let x = cx + rx * cos(angle),
|
|
496
|
+
y = cy + ry * sin(angle);
|
|
497
|
+
let pt = {
|
|
498
|
+
x: x,
|
|
499
|
+
y: y
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
if (ellipseRotation) {
|
|
503
|
+
pt.x = cx + (x - cx) * cos(ellipseRotation) - (y - cy) * sin(ellipseRotation)
|
|
504
|
+
pt.y = cy + (x - cx) * sin(ellipseRotation) + (y - cy) * cos(ellipseRotation)
|
|
505
|
+
}
|
|
506
|
+
return pt
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
|
|
510
|
+
// to parametric angle helper
|
|
511
|
+
export function toParametricAngle(angle, rx, ry) {
|
|
512
|
+
|
|
513
|
+
if (rx === ry || (angle % PI * 0.5 === 0)) return angle;
|
|
514
|
+
let angleP = atan(tan(angle) * (rx / ry));
|
|
515
|
+
|
|
516
|
+
// Ensure the parametric angle is in the correct quadrant
|
|
517
|
+
angleP = cos(angle) < 0 ? angleP + PI : angleP;
|
|
518
|
+
|
|
519
|
+
return angleP
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
// From parametric angle to non-parametric angle
|
|
523
|
+
export function toNonParametricAngle(angleP, rx, ry) {
|
|
524
|
+
|
|
525
|
+
if (rx === ry || (angleP % PI * 0.5 === 0)) return angleP;
|
|
526
|
+
|
|
527
|
+
let angle = atan(tan(angleP) * (ry / rx));
|
|
528
|
+
// Ensure the non-parametric angle is in the correct quadrant
|
|
529
|
+
return cos(angleP) < 0 ? angle + PI : angle;
|
|
530
|
+
};
|
|
531
|
+
|
|
532
|
+
|
|
533
|
+
/**
|
|
534
|
+
* get tangent angle on ellipse
|
|
535
|
+
* at angle
|
|
536
|
+
*/
|
|
537
|
+
export function getTangentAngle(rx, ry, parametricAngle) {
|
|
538
|
+
|
|
539
|
+
// Derivative components
|
|
540
|
+
let dx = -rx * sin(parametricAngle);
|
|
541
|
+
let dy = ry * cos(parametricAngle);
|
|
542
|
+
let tangentAngle = atan2(dy, dx);
|
|
543
|
+
|
|
544
|
+
return tangentAngle;
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
export function bezierhasExtreme(p0, cpts = [], angleThreshold = 0.05) {
|
|
548
|
+
let isCubic = cpts.length === 3 ? true : false;
|
|
549
|
+
let cp1 = cpts[0] || null
|
|
550
|
+
let cp2 = isCubic ? cpts[1] : null;
|
|
551
|
+
let p = isCubic ? cpts[2] : cpts[1];
|
|
552
|
+
let PIquarter = Math.PI * 0.5;
|
|
553
|
+
|
|
554
|
+
let extCp1 = false,
|
|
555
|
+
extCp2 = false;
|
|
556
|
+
|
|
557
|
+
//console.log('ang', cp1);
|
|
558
|
+
let ang1 = cp1 ? getAngle(p, cp1, true) : null;
|
|
559
|
+
|
|
560
|
+
extCp1 = Math.abs((ang1 % PIquarter)) < angleThreshold || Math.abs((ang1 % PIquarter) - PIquarter) < angleThreshold;
|
|
561
|
+
|
|
562
|
+
if (isCubic) {
|
|
563
|
+
let ang2 = cp2 ? getAngle(cp2, p, true) : 0;
|
|
564
|
+
extCp2 = Math.abs((ang2 % PIquarter)) <= angleThreshold ||
|
|
565
|
+
Math.abs((ang2 % PIquarter) - PIquarter) <= angleThreshold;
|
|
566
|
+
}
|
|
567
|
+
return (extCp1 || extCp2)
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
|
|
571
|
+
|
|
572
|
+
export function getBezierExtremeT(pts) {
|
|
573
|
+
let tArr = pts.length === 4 ? cubicBezierExtremeT(pts[0], pts[1], pts[2], pts[3]) : quadraticBezierExtremeT(pts[0], pts[1], pts[2]);
|
|
574
|
+
return tArr;
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
|
|
578
|
+
/**
|
|
579
|
+
* based on Nikos M.'s answer
|
|
580
|
+
* how-do-you-calculate-the-axis-aligned-bounding-box-of-an-ellipse
|
|
581
|
+
* https://stackoverflow.com/questions/87734/#75031511
|
|
582
|
+
* See also: https://github.com/foo123/Geometrize
|
|
583
|
+
*/
|
|
584
|
+
export function getArcExtemes(p0, values) {
|
|
585
|
+
// compute point on ellipse from angle around ellipse (theta)
|
|
586
|
+
const arc = (theta, cx, cy, rx, ry, alpha) => {
|
|
587
|
+
// theta is angle in radians around arc
|
|
588
|
+
// alpha is angle of rotation of ellipse in radians
|
|
589
|
+
var cos = Math.cos(alpha),
|
|
590
|
+
sin = Math.sin(alpha),
|
|
591
|
+
x = rx * Math.cos(theta),
|
|
592
|
+
y = ry * Math.sin(theta);
|
|
593
|
+
|
|
594
|
+
return {
|
|
595
|
+
x: cx + cos * x - sin * y,
|
|
596
|
+
y: cy + sin * x + cos * y
|
|
597
|
+
};
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
//parametrize arcto data
|
|
601
|
+
let arcData = svgArcToCenterParam(p0.x, p0.y, values[0], values[1], values[2], values[3], values[4], values[5], values[6]);
|
|
602
|
+
let { rx, ry, cx, cy, endAngle, deltaAngle } = arcData;
|
|
603
|
+
|
|
604
|
+
// arc rotation
|
|
605
|
+
let deg = values[2];
|
|
606
|
+
|
|
607
|
+
// final on path point
|
|
608
|
+
let p = { x: values[5], y: values[6] }
|
|
609
|
+
|
|
610
|
+
// collect extreme points – add end point
|
|
611
|
+
let extremes = [p]
|
|
612
|
+
|
|
613
|
+
// rotation to radians
|
|
614
|
+
let alpha = deg * Math.PI / 180;
|
|
615
|
+
let tan = Math.tan(alpha),
|
|
616
|
+
p1, p2, p3, p4, theta;
|
|
617
|
+
|
|
618
|
+
/**
|
|
619
|
+
* find min/max from zeroes of directional derivative along x and y
|
|
620
|
+
* along x axis
|
|
621
|
+
*/
|
|
622
|
+
theta = Math.atan2(-ry * tan, rx);
|
|
623
|
+
|
|
624
|
+
let angle1 = theta;
|
|
625
|
+
let angle2 = theta + Math.PI;
|
|
626
|
+
let angle3 = Math.atan2(ry, rx * tan);
|
|
627
|
+
let angle4 = angle3 + Math.PI;
|
|
628
|
+
|
|
629
|
+
|
|
630
|
+
// inner bounding box
|
|
631
|
+
let xArr = [p0.x, p.x]
|
|
632
|
+
let yArr = [p0.y, p.y]
|
|
633
|
+
let xMin = Math.min(...xArr)
|
|
634
|
+
let xMax = Math.max(...xArr)
|
|
635
|
+
let yMin = Math.min(...yArr)
|
|
636
|
+
let yMax = Math.max(...yArr)
|
|
637
|
+
|
|
638
|
+
|
|
639
|
+
// on path point close after start
|
|
640
|
+
let angleAfterStart = endAngle - deltaAngle * 0.001
|
|
641
|
+
let pP2 = arc(angleAfterStart, cx, cy, rx, ry, alpha);
|
|
642
|
+
|
|
643
|
+
// on path point close before end
|
|
644
|
+
let angleBeforeEnd = endAngle - deltaAngle * 0.999
|
|
645
|
+
let pP3 = arc(angleBeforeEnd, cx, cy, rx, ry, alpha);
|
|
646
|
+
|
|
647
|
+
|
|
648
|
+
/**
|
|
649
|
+
* expected extremes
|
|
650
|
+
* if leaving inner bounding box
|
|
651
|
+
* (between segment start and end point)
|
|
652
|
+
* otherwise exclude elliptic extreme points
|
|
653
|
+
*/
|
|
654
|
+
|
|
655
|
+
// right
|
|
656
|
+
if (pP2.x > xMax || pP3.x > xMax) {
|
|
657
|
+
// get point for this theta
|
|
658
|
+
p1 = arc(angle1, cx, cy, rx, ry, alpha);
|
|
659
|
+
extremes.push(p1)
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
// left
|
|
663
|
+
if (pP2.x < xMin || pP3.x < xMin) {
|
|
664
|
+
// get anti-symmetric point
|
|
665
|
+
p2 = arc(angle2, cx, cy, rx, ry, alpha);
|
|
666
|
+
extremes.push(p2)
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
// top
|
|
670
|
+
if (pP2.y < yMin || pP3.y < yMin) {
|
|
671
|
+
// get anti-symmetric point
|
|
672
|
+
p4 = arc(angle4, cx, cy, rx, ry, alpha);
|
|
673
|
+
extremes.push(p4)
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
// bottom
|
|
677
|
+
if (pP2.y > yMax || pP3.y > yMax) {
|
|
678
|
+
// get point for this theta
|
|
679
|
+
p3 = arc(angle3, cx, cy, rx, ry, alpha);
|
|
680
|
+
extremes.push(p3)
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
return extremes;
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
|
|
687
|
+
|
|
688
|
+
// cubic bezier.
|
|
689
|
+
export function cubicBezierExtremeT(p0, cp1, cp2, p) {
|
|
690
|
+
let [x0, y0, x1, y1, x2, y2, x3, y3] = [p0.x, p0.y, cp1.x, cp1.y, cp2.x, cp2.y, p.x, p.y];
|
|
691
|
+
|
|
692
|
+
/**
|
|
693
|
+
* if control points are within
|
|
694
|
+
* bounding box of start and end point
|
|
695
|
+
* we cant't have extremes
|
|
696
|
+
*/
|
|
697
|
+
let top = Math.min(p0.y, p.y)
|
|
698
|
+
let left = Math.min(p0.x, p.x)
|
|
699
|
+
let right = Math.max(p0.x, p.x)
|
|
700
|
+
let bottom = Math.max(p0.y, p.y)
|
|
701
|
+
|
|
702
|
+
if (
|
|
703
|
+
cp1.y >= top && cp1.y <= bottom &&
|
|
704
|
+
cp2.y >= top && cp2.y <= bottom &&
|
|
705
|
+
cp1.x >= left && cp1.x <= right &&
|
|
706
|
+
cp2.x >= left && cp2.x <= right
|
|
707
|
+
) {
|
|
708
|
+
return []
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
let tArr = [],
|
|
712
|
+
a, b, c, t, t1, t2, b2ac, sqrt_b2ac;
|
|
713
|
+
for (let i = 0; i < 2; ++i) {
|
|
714
|
+
if (i == 0) {
|
|
715
|
+
b = 6 * x0 - 12 * x1 + 6 * x2;
|
|
716
|
+
a = -3 * x0 + 9 * x1 - 9 * x2 + 3 * x3;
|
|
717
|
+
c = 3 * x1 - 3 * x0;
|
|
718
|
+
} else {
|
|
719
|
+
b = 6 * y0 - 12 * y1 + 6 * y2;
|
|
720
|
+
a = -3 * y0 + 9 * y1 - 9 * y2 + 3 * y3;
|
|
721
|
+
c = 3 * y1 - 3 * y0;
|
|
722
|
+
}
|
|
723
|
+
if (Math.abs(a) < 1e-12) {
|
|
724
|
+
if (Math.abs(b) < 1e-12) {
|
|
725
|
+
continue;
|
|
726
|
+
}
|
|
727
|
+
t = -c / b;
|
|
728
|
+
if (0 < t && t < 1) {
|
|
729
|
+
tArr.push(t);
|
|
730
|
+
}
|
|
731
|
+
continue;
|
|
732
|
+
}
|
|
733
|
+
b2ac = b * b - 4 * c * a;
|
|
734
|
+
if (b2ac < 0) {
|
|
735
|
+
if (Math.abs(b2ac) < 1e-12) {
|
|
736
|
+
t = -b / (2 * a);
|
|
737
|
+
if (0 < t && t < 1) {
|
|
738
|
+
tArr.push(t);
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
continue;
|
|
742
|
+
}
|
|
743
|
+
sqrt_b2ac = Math.sqrt(b2ac);
|
|
744
|
+
t1 = (-b + sqrt_b2ac) / (2 * a);
|
|
745
|
+
if (0 < t1 && t1 < 1) {
|
|
746
|
+
tArr.push(t1);
|
|
747
|
+
}
|
|
748
|
+
t2 = (-b - sqrt_b2ac) / (2 * a);
|
|
749
|
+
if (0 < t2 && t2 < 1) {
|
|
750
|
+
tArr.push(t2);
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
let j = tArr.length;
|
|
755
|
+
while (j--) {
|
|
756
|
+
t = tArr[j];
|
|
757
|
+
}
|
|
758
|
+
return tArr;
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
|
|
762
|
+
|
|
763
|
+
//For quadratic bezier.
|
|
764
|
+
export function quadraticBezierExtremeT(p0, cp1, p) {
|
|
765
|
+
/**
|
|
766
|
+
* if control points are within
|
|
767
|
+
* bounding box of start and end point
|
|
768
|
+
* we cant't have extremes
|
|
769
|
+
*/
|
|
770
|
+
let top = Math.min(p0.y, p.y)
|
|
771
|
+
let left = Math.min(p0.x, p.x)
|
|
772
|
+
let right = Math.max(p0.x, p.x)
|
|
773
|
+
let bottom = Math.max(p0.y, p.y)
|
|
774
|
+
let a, b, c, t;
|
|
775
|
+
|
|
776
|
+
if (
|
|
777
|
+
cp1.y >= top && cp1.y <= bottom &&
|
|
778
|
+
cp1.x >= left && cp1.x <= right
|
|
779
|
+
) {
|
|
780
|
+
return []
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
|
|
784
|
+
let [x0, y0, x1, y1, x2, y2] = [p0.x, p0.y, cp1.x, cp1.y, p.x, p.y];
|
|
785
|
+
let extemeT = [];
|
|
786
|
+
|
|
787
|
+
for (let i = 0; i < 2; ++i) {
|
|
788
|
+
a = i == 0 ? x0 - 2 * x1 + x2 : y0 - 2 * y1 + y2;
|
|
789
|
+
b = i == 0 ? -2 * x0 + 2 * x1 : -2 * y0 + 2 * y1;
|
|
790
|
+
c = i == 0 ? x0 : y0;
|
|
791
|
+
if (Math.abs(a) > 1e-12) {
|
|
792
|
+
t = -b / (2 * a);
|
|
793
|
+
if (t > 0 && t < 1) {
|
|
794
|
+
extemeT.push(t);
|
|
795
|
+
}
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
return extemeT
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
|
|
802
|
+
|
|
803
|
+
/**
|
|
804
|
+
* check if lines are intersecting
|
|
805
|
+
* returns point and t value (where lines are intersecting)
|
|
806
|
+
*/
|
|
807
|
+
export function intersectLines(p1, p2, p3, p4) {
|
|
808
|
+
|
|
809
|
+
const isOnLine = (x1, y1, x2, y2, px, py, tolerance = 0.001) => {
|
|
810
|
+
var f = function (somex) { return (y2 - y1) / (x2 - x1) * (somex - x1) + y1; };
|
|
811
|
+
return Math.abs(f(px) - py) < tolerance
|
|
812
|
+
&& px >= x1 && px <= x2;
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
|
|
816
|
+
/*
|
|
817
|
+
// flat lines?
|
|
818
|
+
let is_flat1 = p1.y === p2.y || p1.x === p2.x
|
|
819
|
+
let is_flat2 = p3.y === p4.y || p1.y === p2.y
|
|
820
|
+
console.log('flat', is_flat1, is_flat2);
|
|
821
|
+
*/
|
|
822
|
+
|
|
823
|
+
|
|
824
|
+
if (
|
|
825
|
+
Math.max(p1.x, p2.x) < Math.min(p3.x, p4.x) ||
|
|
826
|
+
Math.min(p1.x, p2.x) > Math.max(p3.x, p4.x) ||
|
|
827
|
+
Math.max(p1.y, p2.y) < Math.min(p3.y, p4.y) ||
|
|
828
|
+
Math.min(p1.y, p2.y) > Math.max(p3.y, p4.y)
|
|
829
|
+
) {
|
|
830
|
+
return false;
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
let denominator = (p1.x - p2.x) * (p3.y - p4.y) - (p1.y - p2.y) * (p3.x - p4.x);
|
|
834
|
+
if (denominator == 0) {
|
|
835
|
+
return false;
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
let a = p1.y - p3.y;
|
|
839
|
+
let b = p1.x - p3.x;
|
|
840
|
+
let numerator1 = ((p4.x - p3.x) * a) - ((p4.y - p3.y) * b);
|
|
841
|
+
let numerator2 = ((p2.x - p1.x) * a) - ((p2.y - p1.y) * b);
|
|
842
|
+
a = numerator1 / denominator;
|
|
843
|
+
b = numerator2 / denominator;
|
|
844
|
+
|
|
845
|
+
|
|
846
|
+
let px = p1.x + (a * (p2.x - p1.x)),
|
|
847
|
+
py = p1.y + (a * (p2.y - p1.y));
|
|
848
|
+
|
|
849
|
+
let px2 = +px.toFixed(2),
|
|
850
|
+
py2 = +py.toFixed(2);
|
|
851
|
+
|
|
852
|
+
|
|
853
|
+
// is point in boundaries/actually on line?
|
|
854
|
+
if (
|
|
855
|
+
px2 < +Math.min(p1.x, p2.x).toFixed(2) ||
|
|
856
|
+
px2 > +Math.max(p1.x, p2.x).toFixed(2) ||
|
|
857
|
+
px2 < +Math.min(p3.x, p4.x).toFixed(2) ||
|
|
858
|
+
px2 > +Math.max(p3.x, p4.x).toFixed(2) ||
|
|
859
|
+
py2 < +Math.min(p1.y, p2.y).toFixed(2) ||
|
|
860
|
+
py2 > +Math.max(p1.y, p2.y).toFixed(2) ||
|
|
861
|
+
py2 < +Math.min(p3.y, p4.y).toFixed(2) ||
|
|
862
|
+
py2 > +Math.max(p3.y, p4.y).toFixed(2)
|
|
863
|
+
) {
|
|
864
|
+
|
|
865
|
+
// if final point is on line
|
|
866
|
+
if (isOnLine(p3.x, p3.y, p4.x, p4.y, p2.x, p2.y, 0.1)) {
|
|
867
|
+
return { x: p2.x, y: p2.y };
|
|
868
|
+
}
|
|
869
|
+
return false;
|
|
870
|
+
}
|
|
871
|
+
return { x: px, y: py, t: b };
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
|
|
875
|
+
|
|
876
|
+
|
|
877
|
+
/**
|
|
878
|
+
* check polygon flatness helper
|
|
879
|
+
* basically a reduced shoelace algorithm
|
|
880
|
+
*/
|
|
881
|
+
export function commandIsFlat0(points, tolerance = 0.025) {
|
|
882
|
+
|
|
883
|
+
|
|
884
|
+
let xArr = points.map(pt => { return pt.x })
|
|
885
|
+
let yArr = points.map(pt => { return pt.y })
|
|
886
|
+
|
|
887
|
+
let xMin = Math.min(...xArr)
|
|
888
|
+
let xMax = Math.max(...xArr)
|
|
889
|
+
let yMin = Math.min(...yArr)
|
|
890
|
+
let yMax = Math.max(...yArr)
|
|
891
|
+
let w = xMax - xMin
|
|
892
|
+
let h = yMax - yMin
|
|
893
|
+
|
|
894
|
+
|
|
895
|
+
if (points.length < 3 || (w === 0 || h === 0)) {
|
|
896
|
+
return { area: 0, flat: true, thresh: 0.0001, ratio: 0 };
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
tolerance = 0.5;
|
|
900
|
+
let thresh = (w + h) * 0.5 * tolerance;
|
|
901
|
+
|
|
902
|
+
|
|
903
|
+
//let thresh = tolerance;
|
|
904
|
+
//console.log('w,h', w, h, thresh);
|
|
905
|
+
|
|
906
|
+
let area = 0;
|
|
907
|
+
for (let i = 0; i < points.length; i++) {
|
|
908
|
+
let addX = points[i].x;
|
|
909
|
+
let addY = points[i === points.length - 1 ? 0 : i + 1].y;
|
|
910
|
+
let subX = points[i === points.length - 1 ? 0 : i + 1].x;
|
|
911
|
+
let subY = points[i].y;
|
|
912
|
+
area += addX * addY * 0.5 - subX * subY * 0.5;
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
//console.log('flatArea', area, points);
|
|
916
|
+
area = +Math.abs(area).toFixed(9);
|
|
917
|
+
|
|
918
|
+
let ratio = area / thresh;
|
|
919
|
+
let isFlat = area === 0 ? true : (ratio < 0.15 ? true : false);
|
|
920
|
+
//isFlat= false
|
|
921
|
+
|
|
922
|
+
return { area: area, flat: isFlat, thresh: thresh, ratio: ratio };
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
|
|
926
|
+
export function commandIsFlat(points, tolerance = 0.025) {
|
|
927
|
+
|
|
928
|
+
let p0 = points[0];
|
|
929
|
+
let p = points[points.length - 1];
|
|
930
|
+
|
|
931
|
+
let xArr = points.map(pt => { return pt.x })
|
|
932
|
+
let yArr = points.map(pt => { return pt.y })
|
|
933
|
+
|
|
934
|
+
let xMin = Math.min(...xArr)
|
|
935
|
+
let xMax = Math.max(...xArr)
|
|
936
|
+
let yMin = Math.min(...yArr)
|
|
937
|
+
let yMax = Math.max(...yArr)
|
|
938
|
+
let w = xMax - xMin
|
|
939
|
+
let h = yMax - yMin
|
|
940
|
+
|
|
941
|
+
|
|
942
|
+
if (points.length < 3 || (w === 0 || h === 0)) {
|
|
943
|
+
return { area: 0, flat: true, thresh: 0.0001, ratio: 0 };
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
let squareDist = getSquareDistance(p0, p)
|
|
947
|
+
let squareDist1 = getSquareDistance(p0, points[0])
|
|
948
|
+
let squareDist2 = points.length > 3 ? getSquareDistance(p, points[1]) : squareDist1;
|
|
949
|
+
let squareDistAvg = (squareDist1 + squareDist2) / 2
|
|
950
|
+
|
|
951
|
+
tolerance = 0.5;
|
|
952
|
+
let thresh = (w + h) * 0.5 * tolerance;
|
|
953
|
+
|
|
954
|
+
|
|
955
|
+
//let thresh = tolerance;
|
|
956
|
+
//console.log('w,h', w, h, thresh);
|
|
957
|
+
|
|
958
|
+
let area = 0;
|
|
959
|
+
for (let i = 0, l = points.length; i < l; i++) {
|
|
960
|
+
let addX = points[i].x;
|
|
961
|
+
let addY = points[i === points.length - 1 ? 0 : i + 1].y;
|
|
962
|
+
let subX = points[i === points.length - 1 ? 0 : i + 1].x;
|
|
963
|
+
let subY = points[i].y;
|
|
964
|
+
area += addX * addY * 0.5 - subX * subY * 0.5;
|
|
965
|
+
}
|
|
966
|
+
|
|
967
|
+
//console.log('flatArea', area, points);
|
|
968
|
+
area = +Math.abs(area).toFixed(9);
|
|
969
|
+
|
|
970
|
+
let diff = Math.abs(area - squareDist);
|
|
971
|
+
let areaDiff = Math.abs(100 - (100 / area * (area + diff)))
|
|
972
|
+
let areaThresh = 1000
|
|
973
|
+
|
|
974
|
+
//let ratio = area / (squareDistAvg/areaThresh);
|
|
975
|
+
let ratio = area / (squareDistAvg);
|
|
976
|
+
|
|
977
|
+
|
|
978
|
+
//let isFlat = area === 0 ? true : (ratio < 0.15 ? true : false);
|
|
979
|
+
//let isFlat = area === 0 ? true : (area < squareDist/areaThresh ? true : false);
|
|
980
|
+
|
|
981
|
+
let isFlat = area === 0 ? true : area < squareDistAvg / areaThresh;
|
|
982
|
+
|
|
983
|
+
|
|
984
|
+
return { area: area, flat: isFlat, thresh: thresh, ratio: ratio, squareDist: squareDist, areaThresh: squareDist / areaThresh };
|
|
985
|
+
}
|
|
986
|
+
|
|
987
|
+
|
|
988
|
+
|
|
989
|
+
|
|
990
|
+
/**
|
|
991
|
+
* sloppy distance calculation
|
|
992
|
+
* based on x/y differences
|
|
993
|
+
*/
|
|
994
|
+
export function getDistAv(pt1, pt2) {
|
|
995
|
+
let diffX = Math.abs(pt1.x - pt2.x);
|
|
996
|
+
let diffY = Math.abs(pt1.y - pt2.y);
|
|
997
|
+
let diff = (diffX + diffY) / 2;
|
|
998
|
+
return diff;
|
|
999
|
+
}
|
|
1000
|
+
|
|
1001
|
+
/**
|
|
1002
|
+
* get command dimensions
|
|
1003
|
+
* for threshold value
|
|
1004
|
+
*/
|
|
1005
|
+
|
|
1006
|
+
export function getComThresh(pts, tolerance = 0.01) {
|
|
1007
|
+
let xArr = pts.map(pt => { return pt.x })
|
|
1008
|
+
let yArr = pts.map(pt => { return pt.y })
|
|
1009
|
+
let xMin = Math.min(...xArr)
|
|
1010
|
+
let xMax = Math.max(...xArr)
|
|
1011
|
+
let yMin = Math.min(...yArr)
|
|
1012
|
+
let yMax = Math.max(...yArr)
|
|
1013
|
+
|
|
1014
|
+
let w = xMax - xMin
|
|
1015
|
+
let h = yMax - yMin
|
|
1016
|
+
|
|
1017
|
+
let dimA = (w + h) / 2
|
|
1018
|
+
|
|
1019
|
+
let thresh = dimA * tolerance
|
|
1020
|
+
return thresh
|
|
1021
|
+
}
|
|
1022
|
+
|
|
1023
|
+
export function getComBBTolerance(p1, p2, tolerance = 0.5) {
|
|
1024
|
+
let xMin = Math.min(p1.x, p2.x)
|
|
1025
|
+
let xMax = Math.max(p1.x, p2.x)
|
|
1026
|
+
let yMin = Math.min(p1.y, p2.y)
|
|
1027
|
+
let yMax = Math.max(p1.y, p2.y)
|
|
1028
|
+
|
|
1029
|
+
let w = xMax - xMin
|
|
1030
|
+
let h = yMax - yMin
|
|
1031
|
+
|
|
1032
|
+
let thresh = (w + h) * 0.5 * tolerance
|
|
1033
|
+
if (thresh === 0) {
|
|
1034
|
+
//console.log('is zero', w,h, p1, p2);
|
|
1035
|
+
}
|
|
1036
|
+
return thresh
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1039
|
+
|
|
1040
|
+
|
|
1041
|
+
|
|
1042
|
+
|
|
1043
|
+
|
|
1044
|
+
/**
|
|
1045
|
+
* reduce polypoints
|
|
1046
|
+
* for sloppy dimension approximations
|
|
1047
|
+
*/
|
|
1048
|
+
export function reducePoints(points, maxPoints = 48) {
|
|
1049
|
+
if (!Array.isArray(points) || points.length <= maxPoints) return points;
|
|
1050
|
+
|
|
1051
|
+
// Calculate how many points to skip between kept points
|
|
1052
|
+
let len = points.length;
|
|
1053
|
+
let step = len / maxPoints;
|
|
1054
|
+
let reduced = [];
|
|
1055
|
+
|
|
1056
|
+
for (let i = 0; i < maxPoints; i++) {
|
|
1057
|
+
reduced.push(points[Math.floor(i * step)]);
|
|
1058
|
+
}
|
|
1059
|
+
|
|
1060
|
+
let lenR = reduced.length;
|
|
1061
|
+
// Always include the last point to maintain path integrity
|
|
1062
|
+
if (reduced[lenR - 1] !== points[len - 1]) {
|
|
1063
|
+
reduced[lenR - 1] = points[len - 1];
|
|
1064
|
+
}
|
|
1065
|
+
|
|
1066
|
+
return reduced;
|
|
1067
|
+
}
|
|
1068
|
+
|
|
1069
|
+
|
|
1070
|
+
export function mirrorCpts(cpt2_0, pt0, cpt2, pt1, outgoing = true, t=0.666) {
|
|
1071
|
+
|
|
1072
|
+
// hypotenuse angle
|
|
1073
|
+
let ang0 = getAngle(pt0, pt1, true);
|
|
1074
|
+
let ang1 = outgoing ? getAngle(pt1, cpt2, true) : getAngle(pt0, cpt2_0, true);
|
|
1075
|
+
|
|
1076
|
+
|
|
1077
|
+
let delta = ang0 - ang1
|
|
1078
|
+
let ang = ang0 + delta
|
|
1079
|
+
|
|
1080
|
+
// calculate rotated cp
|
|
1081
|
+
let r = 2;
|
|
1082
|
+
|
|
1083
|
+
// mirror control point
|
|
1084
|
+
let cp2_r = outgoing ? getPointOnEllipse(pt0.x, pt0.y, r, r, ang, 0, false) : getPointOnEllipse(pt1.x, pt1.y, r, r, ang, 0, false);
|
|
1085
|
+
|
|
1086
|
+
// intersection control point
|
|
1087
|
+
let cpI = outgoing ? checkLineIntersection(pt1, cpt2, pt0, cp2_r, false) : checkLineIntersection(pt0, cpt2_0, pt1, cp2_r, false);
|
|
1088
|
+
|
|
1089
|
+
//console.log('cpI', cpI);
|
|
1090
|
+
let cp1 = cpI ? pointAtT([pt0, cpI], t) : pt1;
|
|
1091
|
+
let cp2 = cpI ? pointAtT([pt1, cpI], t) : pt1;
|
|
1092
|
+
|
|
1093
|
+
return { cp1, cp2 }
|
|
1094
|
+
|
|
1095
|
+
|
|
1096
|
+
}
|