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.
Files changed (42) hide show
  1. package/README.md +28 -1
  2. package/dist/svg-path-simplify.esm.js +4040 -0
  3. package/dist/svg-path-simplify.esm.min.js +1 -0
  4. package/dist/svg-path-simplify.js +4065 -0
  5. package/dist/svg-path-simplify.min.js +1 -0
  6. package/dist/svg-path-simplify.node.js +4062 -0
  7. package/dist/svg-path-simplify.node.min.js +1 -0
  8. package/index.html +222 -0
  9. package/package.json +2 -2
  10. package/src/constants.js +4 -0
  11. package/src/index.js +18 -3
  12. package/src/pathData_simplify_cubic.js +324 -0
  13. package/src/pathData_simplify_cubic_arr.js +50 -0
  14. package/src/pathData_simplify_cubic_extrapolate.js +220 -0
  15. package/src/pathSimplify-main.js +294 -0
  16. package/src/svgii/...parse.js +402 -0
  17. package/src/svgii/geometry.js +1096 -0
  18. package/src/svgii/geometry_area.js +265 -0
  19. package/src/svgii/geometry_bbox.js +223 -0
  20. package/src/svgii/pathData_analyze.js +896 -0
  21. package/src/svgii/pathData_convert.js +1180 -0
  22. package/src/svgii/pathData_parse.js +487 -0
  23. package/src/svgii/pathData_remove_collinear.js +85 -0
  24. package/src/svgii/pathData_remove_zerolength.js +28 -0
  25. package/src/svgii/pathData_reorder.js +204 -0
  26. package/src/svgii/pathData_reverse.js +124 -0
  27. package/src/svgii/pathData_scale.js +42 -0
  28. package/src/svgii/pathData_split.js +449 -0
  29. package/src/svgii/pathData_stringify.js +146 -0
  30. package/src/svgii/pathData_toPolygon.js +92 -0
  31. package/src/svgii/pathdata_cleanup.js +363 -0
  32. package/src/svgii/poly_analyze.js +172 -0
  33. package/src/svgii/poly_to_pathdata.js +185 -0
  34. package/src/svgii/rounding.js +154 -0
  35. package/src/svgii/simplify.js +248 -0
  36. package/src/svgii/simplify_bezier.js +470 -0
  37. package/src/svgii/simplify_linetos.js +93 -0
  38. package/src/svgii/simplify_polygon.js +135 -0
  39. package/src/svgii/stringify.js +103 -0
  40. package/src/svgii/svg_cleanup.js +80 -0
  41. package/src/svgii/visualize.js +317 -0
  42. 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
+ }