pts 0.12.9 → 1.0.1

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/src/Op.ts ADDED
@@ -0,0 +1,2442 @@
1
+ /*! Pts.js is licensed under Apache License 2.0. Copyright © 2017-current William Ngan and contributors. (https://github.com/williamngan/pts) */
2
+
3
+ import { Util } from "./Util";
4
+ import { Geom, Num } from "./Num";
5
+ import { Pt, Group } from "./Pt";
6
+ import { Mat } from "./LinearAlgebra";
7
+ import { overlay } from "./_path";
8
+ import {
9
+ type PtLike,
10
+ type GroupLike,
11
+ type PtLikeIterable,
12
+ type IntersectContext,
13
+ type PtIterable,
14
+ type PolygonLike,
15
+ } from "./Types";
16
+
17
+ let _errorLength = (obj: any, param: number | string = "expected") =>
18
+ Util.warn("Group's length is less than " + param, obj);
19
+ let _errorOutofBound = (obj: any, param: number | string = "") =>
20
+ Util.warn(`Index ${param} is out of bound in Group`, obj);
21
+
22
+ /**
23
+ * Line class provides static functions to create and operate on lines. A line is usually represented as a Group of 2 Pts.
24
+ * You can use the static functions as-is, or apply the [`Group.op`](#link) or [`Pt.op`](#link) to enable functional programming.
25
+ * See [Op guide](../guide/Op-0400.html) for details.
26
+ */
27
+ export class Line {
28
+ /**
29
+ * Create a line that originates from an anchor point, given an angle and a magnitude.
30
+ * @param anchor an anchor Pt
31
+ * @param angle an angle in radian
32
+ * @param magnitude magnitude of the line
33
+ * @return a Group of 2 Pts representing a line segement
34
+ */
35
+ static fromAngle(anchor: PtLike, angle: number, magnitude: number): Group {
36
+ let g = new Group(new Pt(anchor), new Pt(anchor));
37
+ g[1].toAngle(angle, magnitude, true);
38
+ return g;
39
+ }
40
+
41
+ /**
42
+ * Calculate the slope of a line.
43
+ * @param p1 line's first end point
44
+ * @param p2 line's second end point
45
+ */
46
+ static slope(p1: PtLike, p2: PtLike): number | undefined {
47
+ return p2[0] - p1[0] === 0 ? undefined : (p2[1] - p1[1]) / (p2[0] - p1[0]);
48
+ }
49
+
50
+ /**
51
+ * Calculate the slope and xy intercepts of a line.
52
+ * @param p1 line's first end point
53
+ * @param p2 line's second end point
54
+ * @returns an object with `slope`, `xi`, `yi` properties
55
+ */
56
+ static intercept(
57
+ p1: PtLike,
58
+ p2: PtLike,
59
+ ): { slope: number; xi: number | undefined; yi: number } | undefined {
60
+ if (p2[0] - p1[0] === 0) {
61
+ return undefined;
62
+ } else {
63
+ let m = (p2[1] - p1[1]) / (p2[0] - p1[0]);
64
+ let c = p1[1] - m * p1[0];
65
+ return { slope: m, yi: c, xi: m === 0 ? undefined : -c / m };
66
+ }
67
+ }
68
+
69
+ /**
70
+ * Given a 2D path and a point, find whether the point is on left or right side of the line.
71
+ * @param line a Group or an Iterable<PtLike> representing a line
72
+ * @param pt a Pt or numeric array
73
+ * @returns a negative value if on left and a positive value if on right. If collinear, then the return value is 0.
74
+ */
75
+ static sideOfPt2D(line: PtLikeIterable, pt: PtLike): number {
76
+ let _line = Util.iterToArray(line);
77
+ return (
78
+ (_line[1][0] - _line[0][0]) * (pt[1] - _line[0][1]) -
79
+ (pt[0] - _line[0][0]) * (_line[1][1] - _line[0][1])
80
+ );
81
+ }
82
+
83
+ /**
84
+ * Check if three Pts are collinear, ie, on the same straight path.
85
+ * @param p1 first Pt
86
+ * @param p2 second Pt
87
+ * @param p3 third Pt
88
+ * @param threshold a threshold where a smaller value means higher precision threshold for the straight line. Default is 0.01.
89
+ */
90
+ static collinear(
91
+ p1: PtLike,
92
+ p2: PtLike,
93
+ p3: PtLike,
94
+ threshold: number = 0.01,
95
+ ): boolean {
96
+ // Compare the cross product normalized by the segment magnitudes (the
97
+ // sine of the angle between them), so the threshold is scale-free.
98
+ const a = new Pt(0, 0, 0).to(p1).$subtract(p2);
99
+ const b = new Pt(0, 0, 0).to(p1).$subtract(p3);
100
+ const magSq = a.magnitudeSq() * b.magnitudeSq();
101
+ if (magSq === 0) return true; // coincident points are collinear
102
+ return a.$cross(b).magnitudeSq() / magSq <= threshold * threshold;
103
+ }
104
+
105
+ /**
106
+ * Get magnitude of a line segment.
107
+ * @param line a Group or an Iterable<Pt> with at least 2 Pt
108
+ */
109
+ static magnitude(line: PtIterable): number {
110
+ let _line = Util.iterToArray(line);
111
+ return _line.length >= 2 ? _line[1].$subtract(_line[0]).magnitude() : 0;
112
+ }
113
+
114
+ /**
115
+ * Get squared magnitude of a line segment.
116
+ * @param line a Group or an Iterable<Pt> with at least 2 Pt
117
+ */
118
+ static magnitudeSq(line: PtIterable): number {
119
+ let _line = Util.iterToArray(line);
120
+ return _line.length >= 2 ? _line[1].$subtract(_line[0]).magnitudeSq() : 0;
121
+ }
122
+
123
+ /**
124
+ * Find a point on a line that is perpendicular (shortest distance) to a target point.
125
+ * @param line a Group or an Iterable<Pt> that defines a line
126
+ * @param pt a target Pt
127
+ * @param asProjection if true, this returns the projection vector instead. Default is false.
128
+ * @returns a Pt on the line that is perpendicular to the target Pt, or a projection vector if `asProjection` is true.
129
+ */
130
+ static perpendicularFromPt(
131
+ line: PtIterable,
132
+ pt: PtLike,
133
+ asProjection: boolean = false,
134
+ ): Pt | undefined {
135
+ let _line = Util.iterToArray(line);
136
+ if (_line[0].equals(_line[1])) return undefined;
137
+ let a = _line[0].$subtract(_line[1]);
138
+ let b = _line[1].$subtract(pt);
139
+ let proj = b.$subtract(a.$project(b));
140
+
141
+ return asProjection ? proj : proj.$add(pt);
142
+ }
143
+
144
+ /**
145
+ * Given a line and a point, find the shortest distance from the point to the line.
146
+ * @param line a Group of 2 Pts
147
+ * @param pt a Pt
148
+ * @see `Line.perpendicularFromPt`
149
+ */
150
+ static distanceFromPt(line: GroupLike, pt: PtLike | number[]): number {
151
+ let _line = Util.iterToArray(line);
152
+ let projectionVector = Line.perpendicularFromPt(_line, pt, true);
153
+ if (projectionVector) {
154
+ return projectionVector.magnitude();
155
+ } else {
156
+ // line is made of 2 identical points, return distance between this point and pt
157
+ return _line[0].$subtract(pt).magnitude();
158
+ }
159
+ }
160
+
161
+ /**
162
+ * Given two lines as rays (infinite lines), find their intersection point if any.
163
+ * @param la a Group or an Iterable<Pt> with 2 Pt representing a ray
164
+ * @param lb a Group or an Iterable<Pt> with 2 Pts representing another ray
165
+ * @returns an intersection Pt or undefined if no intersection
166
+ */
167
+ static intersectRay2D(la: PtIterable, lb: PtIterable): Pt | undefined {
168
+ const _la = Util.iterToArray(la);
169
+ const _lb = Util.iterToArray(lb);
170
+
171
+ const pa = _la[0];
172
+ const pb = _lb[0];
173
+
174
+ // parametric form: no slope blow-up near vertical lines and uniform
175
+ // handling of vertical/horizontal cases
176
+ const rx = _la[1][0] - pa[0];
177
+ const ry = _la[1][1] - pa[1];
178
+ const sx = _lb[1][0] - pb[0];
179
+ const sy = _lb[1][1] - pb[1];
180
+
181
+ // a zero-length input does not define a ray
182
+ if ((rx === 0 && ry === 0) || (sx === 0 && sy === 0)) return undefined;
183
+
184
+ const det = rx * sy - ry * sx;
185
+ if (det === 0) {
186
+ // parallel; if collinear, keep legacy behavior of returning la's start
187
+ const qpx = pb[0] - pa[0];
188
+ const qpy = pb[1] - pa[1];
189
+ return qpx * ry - qpy * rx === 0 ? new Pt(pa[0], pa[1]) : undefined;
190
+ }
191
+
192
+ const t = ((pb[0] - pa[0]) * sy - (pb[1] - pa[1]) * sx) / det;
193
+ return new Pt(pa[0] + t * rx, pa[1] + t * ry);
194
+ }
195
+
196
+ /**
197
+ * Given two line segemnts, find their intersection point if any.
198
+ * @param la a Group or an Iterable<Pt> with 2 Pt representing a line segment
199
+ * @param lb a Group or an Iterable<Pt> with 2 Pt representing a line segment
200
+ * @returns an intersection Pt or undefined if no intersection
201
+ */
202
+ static intersectLine2D(la: PtIterable, lb: PtIterable): Pt | undefined {
203
+ let _la = Util.iterToArray(la);
204
+ let _lb = Util.iterToArray(lb);
205
+
206
+ let pt = Line.intersectRay2D(_la, _lb);
207
+ return pt &&
208
+ Geom.withinBound(pt, _la[0], _la[1]) &&
209
+ Geom.withinBound(pt, _lb[0], _lb[1])
210
+ ? pt
211
+ : undefined;
212
+ }
213
+
214
+ /**
215
+ * Given a line segemnt and a ray (infinite line), find their intersection point if any.
216
+ * @param line a Group of 2 Pts representing a line segment
217
+ * @param ray a Group of 2 Pts representing a ray
218
+ * @returns an intersection Pt or undefined if no intersection
219
+ */
220
+ static intersectLineWithRay2D(
221
+ line: PtIterable,
222
+ ray: PtIterable,
223
+ ): Pt | undefined {
224
+ let _line = Util.iterToArray(line);
225
+ let _ray = Util.iterToArray(ray);
226
+ let pt = Line.intersectRay2D(_line, _ray);
227
+ return pt && Geom.withinBound(pt, _line[0], _line[1]) ? pt : undefined;
228
+ }
229
+
230
+ /**
231
+ * Given a line segemnt or a ray (infinite line), find its intersection point(s) with a polygon.
232
+ * @param lineOrRay a Group or an Iterable<Pt> with 2 Pt representing a line or ray
233
+ * @param poly a Group or an Iterable<Pt> representing a polygon
234
+ * @param sourceIsRay a boolean value to treat the line as a ray (infinite line). Default is `false`.
235
+ */
236
+ static intersectPolygon2D(
237
+ lineOrRay: PtIterable,
238
+ poly: PtIterable,
239
+ sourceIsRay: boolean = false,
240
+ ): Group | undefined {
241
+ let _lineOrRay = Util.iterToArray(lineOrRay);
242
+ let _poly = Util.iterToArray(poly);
243
+
244
+ let fn = sourceIsRay ? Line.intersectLineWithRay2D : Line.intersectLine2D;
245
+ let pts = new Group();
246
+ for (let i = 0, len = _poly.length; i < len; i++) {
247
+ let next = i === len - 1 ? 0 : i + 1;
248
+ let d = fn([_poly[i], _poly[next]], _lineOrRay);
249
+ if (d) pts.push(d);
250
+ }
251
+ return pts.length > 0 ? pts : undefined;
252
+ }
253
+
254
+ /**
255
+ * Find intersection points of 2 sets of lines. This checks all line segments in the two lists. Consider using a bounding-box check before calling this. If you are checking convex polygon intersections, using [`Polygon.intersectPolygon2D`](#link) will be more efficient.
256
+ * @param lines1 an Array/Iterable of (Groups or Iterables<Pt>)
257
+ * @param lines2 an Array/Iterable of (Groups or Iterables<Pt>)
258
+ * @param isRay a boolean value to treat the line as a ray (infinite line). Default is `false`.
259
+ */
260
+ static intersectLines2D(
261
+ lines1: Iterable<PtIterable>,
262
+ lines2: Iterable<PtIterable>,
263
+ isRay: boolean = false,
264
+ ): Group {
265
+ let group = new Group();
266
+ let fn = isRay ? Line.intersectLineWithRay2D : Line.intersectLine2D;
267
+ for (let l1 of lines1) {
268
+ for (let l2 of lines2) {
269
+ let _ip = fn(l1, l2);
270
+ if (_ip) group.push(_ip);
271
+ }
272
+ }
273
+ return group;
274
+ }
275
+
276
+ /**
277
+ * Get two points of a ray that intersects with a point on a 2D grid.
278
+ * @param ray a Group or an Iterable<Pt> representing a ray
279
+ * @param gridPt a Pt on the grid
280
+ * @returns a group of two intersecting Pts. The first one is horizontal intersection and the second one is vertical intersection.
281
+ */
282
+ static intersectGridWithRay2D(ray: PtIterable, gridPt: PtLike): Group {
283
+ let _ray = Util.iterToArray(ray);
284
+ let t = Line.intercept(
285
+ new Pt(_ray[0]).subtract(gridPt),
286
+ new Pt(_ray[1]).subtract(gridPt),
287
+ );
288
+ let g = new Group();
289
+ if (t && t.xi !== undefined) g.push(new Pt(gridPt[0] + t.xi, gridPt[1]));
290
+ if (t && t.yi !== undefined) g.push(new Pt(gridPt[0], gridPt[1] + t.yi));
291
+ return g;
292
+ }
293
+
294
+ /**
295
+ * Get two intersection Pts of a line segment with a 2D grid point.
296
+ * @param line a ray specified by 2 Pts
297
+ * @param gridPt a Pt on the grid
298
+ * @returns a group of two intersecting Pts. The first one is horizontal intersection and the second one is vertical intersection.
299
+ */
300
+ static intersectGridWithLine2D(
301
+ line: GroupLike,
302
+ gridPt: PtLike | number[],
303
+ ): Group {
304
+ let _line = Util.iterToArray(line);
305
+ let g = Line.intersectGridWithRay2D(_line, gridPt);
306
+ let gg = new Group();
307
+ for (let i = 0, len = g.length; i < len; i++) {
308
+ if (Geom.withinBound(g[i], _line[0], _line[1])) gg.push(g[i]);
309
+ }
310
+ return gg;
311
+ }
312
+
313
+ /**
314
+ * An easy way to get rectangle-line intersection points. For more optimized implementation, store the rectangle's sides separately (eg, `Rectangle.sides()`) and use `Polygon.intersectPolygon2D()`.
315
+ * @param line a Group representing a line
316
+ * @param rect a Group representing a rectangle
317
+ * @returns a Group of intersecting Pts
318
+ */
319
+ static intersectRect2D(line: GroupLike, rect: GroupLike): Group {
320
+ let _line = Util.iterToArray(line);
321
+ let _rect = Util.iterToArray(rect);
322
+ let box = Geom.boundingBox(Group.fromPtArray(_line));
323
+ if (!Rectangle.hasIntersectRect2D(box, _rect)) return new Group();
324
+ return Line.intersectLines2D([_line], Rectangle.sides(_rect));
325
+ }
326
+
327
+ /**
328
+ * Get evenly distributed points on a line. Similar to [`Create.distributeLinear`](#link) but excluding end points.
329
+ * @param line a Group or an Iterable<PtLike> representing a line
330
+ * @param num number of points to get
331
+ */
332
+ static subpoints(line: PtLikeIterable, num: number) {
333
+ let _line = Util.iterToArray(line);
334
+ let pts = new Group();
335
+ for (let i = 1; i <= num; i++) {
336
+ pts.push(Geom.interpolate(_line[0], _line[1], i / (num + 1)));
337
+ }
338
+ return pts;
339
+ }
340
+
341
+ /**
342
+ * Crop this line by a circle or rectangle at end points. This can be useful for creating arrows that connect to an object's edge.
343
+ * @param line a Group or an Iterable<Pt> representing a line to crop
344
+ * @param size size of circle or rectangle as Pt
345
+ * @param index line's end point index, ie, 0 = start and 1 = end.
346
+ * @param cropAsCircle a boolean to specify whether the `size` parameter should be treated as circle. Default is `true`.
347
+ * @return an intersecting point on the line that can be used for cropping.
348
+ */
349
+ static crop(
350
+ line: PtIterable,
351
+ size: PtLike,
352
+ index: number = 0,
353
+ cropAsCircle: boolean = true,
354
+ ): Pt | undefined {
355
+ let _line = Util.iterToArray(line);
356
+ let tdx = index === 0 ? 1 : 0;
357
+ let ls = _line[tdx].$subtract(_line[index]);
358
+
359
+ if (ls.magnitudeSq() === 0) return _line[index]; // zero-length line
360
+
361
+ if (cropAsCircle) {
362
+ let d = ls.unit().multiply(size[1]);
363
+ return _line[index].$add(d);
364
+ } else {
365
+ if (size[0] === 0) return _line[index]; // degenerate rectangle
366
+ let rect = Rectangle.fromCenter(_line[index], size);
367
+ let sides = Rectangle.sides(rect);
368
+ let sideIdx = 0;
369
+
370
+ if (Math.abs(ls[1] / ls[0]) > Math.abs(size[1] / size[0])) {
371
+ sideIdx = ls[1] < 0 ? 0 : 2;
372
+ } else {
373
+ sideIdx = ls[0] < 0 ? 3 : 1;
374
+ }
375
+ return Line.intersectRay2D(sides[sideIdx], _line);
376
+ }
377
+ }
378
+
379
+ /**
380
+ * Create an marker arrow or line, placed at an end point of this line.
381
+ * @param line a Group or an Iterable<Pt> representing a line to place marker
382
+ * @param size size of the marker as Pt
383
+ * @param graphic either "arrow" or "line"
384
+ * @param atTail a boolean, if `true`, the marker will be positioned at tail of the line (ie, index = 1). Default is `true`.
385
+ * @returns a Group that defines the marker's shape
386
+ */
387
+ static marker(
388
+ line: PtIterable,
389
+ size: PtLike,
390
+ graphic: string = "arrow",
391
+ atTail: boolean = true,
392
+ ): Group {
393
+ let _line = Util.iterToArray(line);
394
+ let h = atTail ? 0 : 1;
395
+ let t = atTail ? 1 : 0;
396
+ let unit = _line[h].$subtract(_line[t]);
397
+
398
+ if (unit.magnitudeSq() === 0) return new Group();
399
+ unit.unit();
400
+
401
+ let ps = Geom.perpendicular(unit).multiply(size[0]).add(_line[t]);
402
+ if (graphic == "arrow") {
403
+ ps.add(unit.$multiply(size[1]));
404
+ return new Group(_line[t], ps[0], ps[1]);
405
+ } else {
406
+ return new Group(ps[0], ps[1]);
407
+ }
408
+ }
409
+
410
+ /**
411
+ * Convert this line to a new rectangle representation.
412
+ * @param line a Group representing a line
413
+ */
414
+ static toRect(line: GroupLike) {
415
+ let _line = Util.iterToArray(line);
416
+ return new Group(_line[0].$min(_line[1]), _line[0].$max(_line[1]));
417
+ }
418
+ }
419
+
420
+ /**
421
+ * Rectangle class provides static functions to create and operate on rectangles. A rectangle is usually represented as a Group of 2 Pts, marking the top-left and bottom-right corners of the rectangle.
422
+ * You can use the static functions as-is, or apply the [`Group.op`](#link) or [`Pt.op`](#link) to enable functional programming.
423
+ * See [Op guide](../guide/Op-0400.html) for details.
424
+ */
425
+ export class Rectangle {
426
+ /**
427
+ * Create a rectangle from top-left anchor point. Same as [`Rectangle.fromTopLeft`](#link).
428
+ * @param topLeft top-left point
429
+ * @param widthOrSize width as a number, or a Pt that defines its size
430
+ * @param height optional height as a number
431
+ * @returns a Group of 2 Pts representing a rectangle
432
+ */
433
+ static from(
434
+ topLeft: PtLike,
435
+ widthOrSize: number | PtLike,
436
+ height?: number,
437
+ ): Group {
438
+ return Rectangle.fromTopLeft(topLeft, widthOrSize, height);
439
+ }
440
+
441
+ /**
442
+ * Create a rectangle given a top-left position and a size.
443
+ * @param topLeft top-left point
444
+ * @param widthOrSize width as a number, or a Pt that defines its size
445
+ * @param height optional height as a number
446
+ * @returns a Group of 2 Pts representing a rectangle
447
+ */
448
+ static fromTopLeft(
449
+ topLeft: PtLike,
450
+ widthOrSize: number | PtLike,
451
+ height?: number,
452
+ ): Group {
453
+ let size =
454
+ typeof widthOrSize == "number"
455
+ ? [widthOrSize, height ?? widthOrSize]
456
+ : widthOrSize;
457
+ return new Group(new Pt(topLeft), new Pt(topLeft).add(size));
458
+ }
459
+
460
+ /**
461
+ * Create a rectangle given a center position and a size.
462
+ * @param center center point
463
+ * @param widthOrSize width as a number, or a Pt that defines its size
464
+ * @param height optional height as a number
465
+ * @returns a Group of 2 Pts representing a rectangle
466
+ */
467
+ static fromCenter(
468
+ center: PtLike,
469
+ widthOrSize: number | PtLike,
470
+ height?: number,
471
+ ): Group {
472
+ let half =
473
+ typeof widthOrSize == "number"
474
+ ? [widthOrSize / 2, (height ?? widthOrSize) / 2]
475
+ : new Pt(widthOrSize).divide(2);
476
+ return new Group(new Pt(center).subtract(half), new Pt(center).add(half));
477
+ }
478
+
479
+ /**
480
+ * Create a new circle that either fits within or encloses the rectangle. Same as [`Circle.fromRect`](#link).
481
+ * @param pts a Group or an Iterable<Pt> with 2 Pt representing a rectangle
482
+ * @param enclose if `true`, the circle will enclose the rectangle. If `false`, the circle will fit inside the rectangle.
483
+ * @returns a Group that represents a circle
484
+ */
485
+ static toCircle(pts: PtIterable, enclose: boolean = true): Group {
486
+ return Circle.fromRect(pts, enclose);
487
+ }
488
+
489
+ /**
490
+ * Create a square that either fits within or encloses a rectangle.
491
+ * @param pts a Group or an Iterable<Pt> with 2 Pt representing a rectangle
492
+ * @param enclose if `true`, the square will enclose the rectangle. Default is `false`, which will fit the square inside the rectangle.
493
+ * @returns a Group of 2 Pts representing a rectangle
494
+ */
495
+ static toSquare(pts: PtIterable, enclose = false): Group {
496
+ let _pts = Util.iterToArray(pts);
497
+ let s = Rectangle.size(_pts);
498
+ let m = enclose ? s.maxValue().value : s.minValue().value;
499
+ return Rectangle.fromCenter(Rectangle.center(_pts), m, m);
500
+ }
501
+
502
+ /**
503
+ * Get the size of this rectangle as a Pt.
504
+ * @param pts a Group or an Iterable<Pt> with 2 Pt representing a Rectangle
505
+ */
506
+ static size(pts: PtIterable): Pt {
507
+ let p = Util.iterToArray(pts);
508
+ return p[0].$max(p[1]).subtract(p[0].$min(p[1]));
509
+ }
510
+
511
+ /**
512
+ * Get the center of this rectangle.
513
+ * @param pts a Group or an Iterable<Pt> with 2 Pt representing a Rectangle
514
+ */
515
+ static center(pts: PtIterable): Pt {
516
+ let p = Util.iterToArray(pts);
517
+ let min = p[0].$min(p[1]);
518
+ let max = p[0].$max(p[1]);
519
+ return min.add(max.$subtract(min).divide(2));
520
+ }
521
+
522
+ /**
523
+ * Get the 4 corners of this rectangle as a Group.
524
+ * @param rect a Group or an Iterable<Pt> with 2 Pt representing a Rectangle
525
+ */
526
+ static corners(rect: PtIterable): Group {
527
+ let _rect = Util.iterToArray(rect);
528
+ let p0 = _rect[0].$min(_rect[1]);
529
+ let p2 = _rect[0].$max(_rect[1]);
530
+ return new Group(p0, new Pt(p2.x, p0.y), p2, new Pt(p0.x, p2.y));
531
+ }
532
+
533
+ /**
534
+ * Get the 4 sides of this rectangle as an array of 4 Groups.
535
+ * @param rect a Group or an Iterable<Pt> with 2 Pt representing a Rectangle
536
+ * @returns an array of 4 Groups, each of which represents a line segment
537
+ */
538
+ static sides(rect: PtIterable): Group[] {
539
+ let [p0, p1, p2, p3] = Rectangle.corners(rect);
540
+ return [
541
+ new Group(p0, p1),
542
+ new Group(p1, p2),
543
+ new Group(p2, p3),
544
+ new Group(p3, p0),
545
+ ];
546
+ }
547
+
548
+ /**
549
+ * Given an array of rectangles, get a rectangle that bounds all of them.
550
+ * @param rects an array of (Groups or Iterables<PtLike>) that represents a set of rectangles
551
+ * @returns the bounding rectangle as a Group
552
+ */
553
+ static boundingBox(rects: Iterable<PtLikeIterable>): Group {
554
+ let _rects = Util.iterToArray(rects);
555
+ let merged = Util.flatten(_rects, false);
556
+ if (merged.length === 0) return new Group();
557
+
558
+ // Infinity, not Number.MAX_VALUE/MIN_VALUE. Pt is a Float32Array, in which
559
+ // MAX_VALUE overflows to Infinity and MIN_VALUE flushes to 0, which would
560
+ // leave the running maximum starting above every negative coordinate.
561
+ let min = Pt.make(2, Infinity);
562
+ let max = Pt.make(2, -Infinity);
563
+
564
+ // calculate min max in a single pass
565
+ for (let i = 0, len = merged.length; i < len; i++) {
566
+ let p = merged[i];
567
+ let dim = Math.min(2, p.length);
568
+ for (let k = 0; k < dim; k++) {
569
+ min[k] = Math.min(min[k], p[k]);
570
+ max[k] = Math.max(max[k], p[k]);
571
+ }
572
+ }
573
+ return new Group(min, max);
574
+ }
575
+
576
+ /**
577
+ * Convert this rectangle into a Group representing a polygon. An alias for [`Rectangle.corners`](#link)
578
+ * @param rect a Group or an Iterable<Pt> with 2 Pt representing a Rectangle
579
+ */
580
+ static polygon(rect: PtIterable): Group {
581
+ return Rectangle.corners(rect);
582
+ }
583
+
584
+ /**
585
+ * Subdivide a rectangle into 4 rectangles, one for each quadrant.
586
+ * @param rect a Group or an Iterable<Pt> with 2 Pt representing a Rectangle
587
+ * @returns an array of 4 Groups of rectangles
588
+ */
589
+ static quadrants(rect: PtIterable, center?: PtLike): Group[] {
590
+ let _rect = Util.iterToArray(rect);
591
+ let corners = Rectangle.corners(_rect);
592
+ let _center =
593
+ center != undefined ? new Pt(center) : Rectangle.center(_rect);
594
+ return corners.map((c) => new Group(c, _center).boundingBox());
595
+ }
596
+
597
+ /**
598
+ * Subdivde a rectangle into 2 rectangles, by row or by column.
599
+ * @param rect a Group or an Iterable<Pt> with 2 Pt representing a Rectangle
600
+ * @param ratio a value between 0 to 1 to indicate the split ratio
601
+ * @param asRows if `true`, split into 2 rows. Default is `false` which splits into 2 columns.
602
+ * @returns an array of 2 Groups of rectangles
603
+ */
604
+ static halves(
605
+ rect: PtIterable,
606
+ ratio: number = 0.5,
607
+ asRows: boolean = false,
608
+ ): Group[] {
609
+ let _rect = Util.iterToArray(rect);
610
+ let min = _rect[0].$min(_rect[1]);
611
+ let max = _rect[0].$max(_rect[1]);
612
+ let mid = asRows
613
+ ? Num.lerp(min[1], max[1], ratio)
614
+ : Num.lerp(min[0], max[0], ratio);
615
+ return asRows
616
+ ? [
617
+ new Group(min, new Pt(max[0], mid)),
618
+ new Group(new Pt(min[0], mid), max),
619
+ ]
620
+ : [
621
+ new Group(min, new Pt(mid, max[1])),
622
+ new Group(new Pt(mid, min[1]), max),
623
+ ];
624
+ }
625
+
626
+ /**
627
+ * Check if a point is within a rectangle.
628
+ * @param rect a Group of 2 Pts representing a Rectangle
629
+ * @param pt the point to check
630
+ */
631
+ static withinBound(rect: GroupLike, pt: PtLike): boolean {
632
+ let _rect = Util.iterToArray(rect);
633
+ return Geom.withinBound(pt, _rect[0], _rect[1]);
634
+ }
635
+
636
+ /**
637
+ * Check if a rectangle is within the bounds of another rectangle.
638
+ * @param rect1 a Group of 2 Pts representing a rectangle
639
+ * @param rect2 a Group of 2 Pts representing a rectangle
640
+ * @param resetBoundingBox if `true`, reset the bounding box. Default is `false` which assumes the rect's first Pt at is its top-left corner.
641
+ */
642
+ static hasIntersectRect2D(
643
+ rect1: GroupLike,
644
+ rect2: GroupLike,
645
+ resetBoundingBox: boolean = false,
646
+ ): boolean {
647
+ let _rect1 = Util.iterToArray(rect1);
648
+ let _rect2 = Util.iterToArray(rect2);
649
+
650
+ if (resetBoundingBox) {
651
+ _rect1 = Geom.boundingBox(_rect1);
652
+ _rect2 = Geom.boundingBox(_rect2);
653
+ }
654
+
655
+ if (_rect1[0][0] > _rect2[1][0] || _rect2[0][0] > _rect1[1][0])
656
+ return false;
657
+ if (_rect1[0][1] > _rect2[1][1] || _rect2[0][1] > _rect1[1][1])
658
+ return false;
659
+ return true;
660
+ }
661
+
662
+ /**
663
+ * An easy way to get rectangle-rectangle intersection points. For more optimized implementation, store the rectangle's sides separately (eg, `Rectangle.sides()`) and use `Polygon.intersectPolygon2D()`.
664
+ * @param rect1 a Group of 2 Pts representing a rectangle
665
+ * @param rect2 a Group of 2 Pts representing a rectangle
666
+ */
667
+ static intersectRect2D(rect1: GroupLike, rect2: GroupLike): Group {
668
+ let _rect1 = Util.iterToArray(rect1);
669
+ let _rect2 = Util.iterToArray(rect2);
670
+ if (!Rectangle.hasIntersectRect2D(_rect1, _rect2)) return new Group();
671
+ return Line.intersectLines2D(
672
+ Rectangle.sides(_rect1),
673
+ Rectangle.sides(_rect2),
674
+ );
675
+ }
676
+ }
677
+
678
+ /**
679
+ * Circle class provides static functions to create and operate on circles. A circle is usually represented as a Group of 2 Pts, where the first Pt specifies the center, and the second Pt specifies the radius.
680
+ * To move a circle without changing its radius, move only its center, eg `circle[0].to(20, 20)`. Group transforms such as `circle.moveTo(20, 20)` affect both Pts, including the radius.
681
+ * You can use the static functions as-is, or apply the [`Group.op`](#link) or [`Pt.op`](#link) to enable functional programming.
682
+ * See [Op guide](../guide/Op-0400.html) for details.
683
+ */
684
+ export class Circle {
685
+ /**
686
+ * Create a circle that either fits within, or encloses, a rectangle.
687
+ * @param pts a Group or an Iterable<PtLike> with 2 Pt representing a rectangle
688
+ * @param enclose if `true`, the circle will enclose the rectangle. Default is `false`, which will fit the circle inside the rectangle.
689
+ * @returns a Group that represents a circle
690
+ */
691
+ static fromRect(pts: PtLikeIterable, enclose = false): Group {
692
+ let _pts = Util.iterToArray(pts);
693
+ let r = 0;
694
+ let min = (r = Rectangle.size(_pts).minValue().value / 2);
695
+ if (enclose) {
696
+ let max = Rectangle.size(_pts).maxValue().value / 2;
697
+ r = Math.sqrt(min * min + max * max);
698
+ } else {
699
+ r = min;
700
+ }
701
+ return new Group(Rectangle.center(_pts), new Pt(r, r));
702
+ }
703
+
704
+ /**
705
+ * Create a circle that either fits within, or encloses, a triangle. Same as [`Triangle.circumcircle`](#link) or [`Triangle.incircle`](#link).
706
+ * @param pts a Group or an Iterable<Pt> with 3 Pt representing a rectangle
707
+ * @param enclose if `true`, the circle will enclose the triangle. Default is `false`, which will fit the circle inside the triangle.
708
+ * @returns a Group that represents a circle
709
+ */
710
+ static fromTriangle(
711
+ pts: PtIterable,
712
+ enclose: boolean = false,
713
+ ): Group | undefined {
714
+ if (enclose) {
715
+ return Triangle.circumcircle(pts);
716
+ } else {
717
+ return Triangle.incircle(pts);
718
+ }
719
+ }
720
+
721
+ /**
722
+ * Create a circle based on a center point and a radius.
723
+ * @param pt center point of circle
724
+ * @param radius radius of circle
725
+ * @returns a Group that represents a circle
726
+ */
727
+ static fromCenter(pt: PtLike, radius: number): Group {
728
+ return new Group(new Pt(pt), new Pt(radius, radius));
729
+ }
730
+
731
+ /**
732
+ * Check if a point is within a circle.
733
+ * @param pts a Group or an Iterable<Pt> with 2 Pt representing a circle
734
+ * @param pt the point to checks
735
+ * @param threshold an optional small number to set threshold. Default is 0.
736
+ */
737
+ static withinBound(
738
+ pts: PtIterable,
739
+ pt: PtLike,
740
+ threshold: number = 0,
741
+ ): boolean {
742
+ let _pts = Util.iterToArray(pts);
743
+ let d = _pts[0].$subtract(pt);
744
+ return d.dot(d) + threshold < _pts[1].x * _pts[1].x;
745
+ }
746
+
747
+ /**
748
+ * Get the intersection points between a circle and a ray (infinite line).
749
+ * @param circle a Group or an Iterable<Pt> with 2 Pt representing a circle
750
+ * @param ray a Group or an Iterable<Pt> with 2 Pt representing a ray
751
+ * @returns a Group of intersection points, or an empty Group if no intersection is found
752
+ */
753
+ static intersectRay2D(circle: PtIterable, ray: PtIterable): Group {
754
+ let _pts = Util.iterToArray(circle);
755
+ let _ray = Util.iterToArray(ray);
756
+
757
+ let d = _ray[0].$subtract(_ray[1]);
758
+ let f = _pts[0].$subtract(_ray[0]);
759
+
760
+ let a = d.dot(d);
761
+ if (a === 0) return new Group(); // degenerate ray (two identical points)
762
+ let b = f.dot(d);
763
+ let c = f.dot(f) - _pts[1].x * _pts[1].x;
764
+ let p = b / a;
765
+ let q = c / a;
766
+ let disc = p * p - q; // discriminant
767
+
768
+ if (disc < 0) {
769
+ return new Group();
770
+ } else {
771
+ let discSqrt = Math.sqrt(disc);
772
+
773
+ let t1 = -p + discSqrt;
774
+ let p1 = _ray[0].$subtract(d.$multiply(t1));
775
+ if (disc === 0) return new Group(p1);
776
+
777
+ let t2 = -p - discSqrt;
778
+ let p2 = _ray[0].$subtract(d.$multiply(t2));
779
+ return new Group(p1, p2);
780
+ }
781
+ }
782
+
783
+ /**
784
+ * Get the intersection points between a circle and a line segment.
785
+ * @param circle a Group or an Iterable<Pt> with Pt representing a circle
786
+ * @param line a Group or an Iterable<Pt> with 2 Pt representing a line
787
+ * @returns a Group of intersection points, or an empty Group if no intersection is found
788
+ */
789
+ static intersectLine2D(circle: PtIterable, line: PtIterable): Group {
790
+ let _pts = Util.iterToArray(circle);
791
+ let _line = Util.iterToArray(line);
792
+
793
+ let ps = Circle.intersectRay2D(_pts, _line);
794
+ let g = new Group();
795
+ if (ps.length > 0) {
796
+ for (let i = 0, len = ps.length; i < len; i++) {
797
+ if (Rectangle.withinBound(_line, ps[i])) g.push(ps[i]);
798
+ }
799
+ }
800
+ return g;
801
+ }
802
+
803
+ /**
804
+ * Get the intersection points between two circles.
805
+ * @param circle1 a Group or an Iterable<Pt> with 2 Pt representing a circle
806
+ * @param circle2 a Group or an Iterable<Pt> with 2 Pt representing a circle
807
+ * @returns a Group of intersection points, or an empty Group if no intersection is found
808
+ */
809
+ static intersectCircle2D(circle1: PtIterable, circle2: PtIterable): Group {
810
+ let _pts = Util.iterToArray(circle1);
811
+ let _circle = Util.iterToArray(circle2);
812
+
813
+ let dv = _circle[0].$subtract(_pts[0]);
814
+ let dr2 = dv.magnitudeSq();
815
+ let dr = Math.sqrt(dr2);
816
+
817
+ let ar = _pts[1].x;
818
+ let br = _circle[1].x;
819
+ let ar2 = ar * ar;
820
+ let br2 = br * br;
821
+
822
+ if (dr > ar + br) {
823
+ // not intersected
824
+ return new Group();
825
+ } else if (dr < Math.abs(ar - br)) {
826
+ // completely enclosed
827
+ return new Group(_pts[0].clone());
828
+ } else if (dr === 0) {
829
+ // coincident circles: no discrete intersection points
830
+ return new Group();
831
+ } else {
832
+ let a = (ar2 - br2 + dr2) / (2 * dr);
833
+ let h = Math.sqrt(ar2 - a * a);
834
+ let p = dv.$multiply(a / dr).add(_pts[0]);
835
+ return new Group(
836
+ new Pt(p.x + (h * dv.y) / dr, p.y - (h * dv.x) / dr),
837
+ new Pt(p.x - (h * dv.y) / dr, p.y + (h * dv.x) / dr),
838
+ );
839
+ }
840
+ }
841
+
842
+ /**
843
+ * Quick way to check rectangle intersection with a circle.
844
+ * For more optimized implementation, store the rectangle's sides separately (eg, [`Rectangle.sides`](#link)) and use [`Polygon.intersectPolygon2D()`](#link).
845
+ * @param circle a Group or an Iterable<Pt> with 2 Pt representing a circle
846
+ * @param rect a Group or an Iterable<Pt> with 2 Pt representing a rectangle
847
+ * @returns a Group of intersection points, or an empty Group if no intersection is found
848
+ */
849
+ static intersectRect2D(circle: PtIterable, rect: PtIterable): Group {
850
+ let _pts = Util.iterToArray(circle);
851
+ let _rect = Util.iterToArray(rect);
852
+
853
+ let sides = Rectangle.sides(_rect);
854
+ let g = [];
855
+ for (let i = 0, len = sides.length; i < len; i++) {
856
+ let ps = Circle.intersectLine2D(_pts, sides[i]);
857
+ if (ps.length > 0) g.push(ps);
858
+ }
859
+ return Util.flatten(g);
860
+ }
861
+
862
+ /**
863
+ * Get a rectangle that either fits within or encloses this circle. See also [`Rectangle.toCircle`](#link)
864
+ * @param circle a Group or an Iterable<Pt> with 2 Pt representing a circle
865
+ * @param within if `true`, the rectangle will be within the circle. If `false`, the rectangle will enclose the circle.
866
+ * @returns a Group representing a rectangle
867
+ */
868
+ static toRect(circle: PtIterable, within: boolean = false): Group {
869
+ let _pts = Util.iterToArray(circle);
870
+ let r = _pts[1][0];
871
+ if (within) {
872
+ // half-side of the maximal inscribed square (its corners on the circle)
873
+ let half = r / Math.SQRT2;
874
+ return new Group(_pts[0].$subtract(half), _pts[0].$add(half));
875
+ } else {
876
+ return new Group(_pts[0].$subtract(r), _pts[0].$add(r));
877
+ }
878
+ }
879
+
880
+ /**
881
+ * Get a triangle that fits within this circle.
882
+ * @param circle a Group or an Iterable<Pt> with 2 Pt representing a circle
883
+ * @param within if `true`, the triangle will be within the circle. If `false`, the triangle will enclose the circle.
884
+ */
885
+ static toTriangle(circle: PtIterable, within: boolean = true): Group {
886
+ let _pts = Util.iterToArray(circle);
887
+ if (within) {
888
+ let ang = -Math.PI / 2;
889
+ let inc = (Math.PI * 2) / 3;
890
+ let g = new Group();
891
+ for (let i = 0; i < 3; i++) {
892
+ g.push(_pts[0].clone().toAngle(ang, _pts[1][0], true));
893
+ ang += inc;
894
+ }
895
+ return g;
896
+ } else {
897
+ return Triangle.fromCenter(_pts[0], _pts[1][0]);
898
+ }
899
+ }
900
+ }
901
+
902
+ /**
903
+ * Triangle class provides static functions to create and operate on trianges. A triange is a polygon represented as a Group of 3 Pts.
904
+ * You can use the static functions as-is, or apply the [`Group.op`](#link) or [`Pt.op`](#link) to enable functional programming.
905
+ * See [Op guide](../guide/Op-0400.html) for details.
906
+ */
907
+ export class Triangle {
908
+ /**
909
+ * Create a triangle from a rectangle. The triangle will be isosceles, with the bottom of the rectangle as its base.
910
+ * @param rect a Group or an Iterable<Pt> with 2 Pt representing a rectangle
911
+ */
912
+ static fromRect(rect: PtIterable): Group {
913
+ let _rect = Util.iterToArray(rect);
914
+ let top = _rect[0].$add(_rect[1]).divide(2);
915
+ top.y = _rect[0][1];
916
+ let left = _rect[1].clone();
917
+ left.x = _rect[0][0];
918
+ return new Group(top, _rect[1].clone(), left);
919
+ }
920
+
921
+ /**
922
+ * Create a triangle that fits within a circle.
923
+ * @param circle a Group or an Iterable<Pt> with 2 Pt representing a circle
924
+ */
925
+ static fromCircle(circle: PtIterable): Group {
926
+ return Circle.toTriangle(circle, true);
927
+ }
928
+
929
+ /**
930
+ * Create an equilateral triangle based on a center point and a size.
931
+ * @param pt the center point
932
+ * @param size size is the magnitude of lines from center to the triangle's vertices, like a "radius".
933
+ */
934
+ static fromCenter(pt: PtLike, size: number): Group {
935
+ return Triangle.fromCircle(Circle.fromCenter(pt, size));
936
+ }
937
+
938
+ /**
939
+ * Get the medial, which is an inner triangle formed by connecting the midpoints of this triangle's sides.
940
+ * @param tri a Group or an Iterable<Pt> representing a triangle
941
+ * @returns a Group representing a medial triangle
942
+ */
943
+ static medial(tri: PtIterable): Group {
944
+ let _pts = Util.iterToArray(tri);
945
+ if (_pts.length < 3) return _errorLength(new Group(), 3);
946
+ return Polygon.midpoints(_pts, true);
947
+ }
948
+
949
+ /**
950
+ * Given a point of the triangle, the opposite side is the side which the point doesn't touch.
951
+ * @param tri a Group or an Iterable<Pt> representing a triangle
952
+ * @param index a Pt on the triangle group
953
+ * @returns a Group that represents a line of the opposite side
954
+ */
955
+ static oppositeSide(tri: PtIterable, index: number): Group {
956
+ let _pts = Util.iterToArray(tri);
957
+ if (_pts.length < 3) return _errorLength(new Group(), 3);
958
+ if (index === 0) {
959
+ return Group.fromPtArray([_pts[1], _pts[2]]);
960
+ } else if (index === 1) {
961
+ return Group.fromPtArray([_pts[0], _pts[2]]);
962
+ } else {
963
+ return Group.fromPtArray([_pts[0], _pts[1]]);
964
+ }
965
+ }
966
+
967
+ /**
968
+ * Get a triangle's altitude, which is a line from a triangle's point to its opposite side, and perpendicular to its opposite side.
969
+ * @param tri a Group or an Iterable<Pt> representing a triangle
970
+ * @param index a Pt on the triangle group
971
+ * @returns a Group that represents the altitude line
972
+ */
973
+ static altitude(tri: PtIterable, index: number): Group {
974
+ let _pts = Util.iterToArray(tri);
975
+ let opp = Triangle.oppositeSide(_pts, index);
976
+ if (opp.length > 1) {
977
+ return new Group(
978
+ _pts[index],
979
+ Line.perpendicularFromPt(opp, _pts[index])!,
980
+ );
981
+ } else {
982
+ return new Group();
983
+ }
984
+ }
985
+
986
+ /**
987
+ * Get orthocenter, which is the intersection point of a triangle's 3 altitudes (the 3 lines that are perpendicular to its 3 opposite sides).
988
+ * @param tri a Group or an Iterable<Pt> representing a triangle
989
+ * @returns the orthocenter as a Pt
990
+ */
991
+ static orthocenter(tri: PtIterable): Pt | undefined {
992
+ let _pts = Util.iterToArray(tri);
993
+ if (_pts.length < 3) return _errorLength(undefined, 3);
994
+ let a = Triangle.altitude(_pts, 0);
995
+ let b = Triangle.altitude(_pts, 1);
996
+ return Line.intersectRay2D(a, b);
997
+ }
998
+
999
+ /**
1000
+ * Get incenter, which is the center point of its inner circle, and also the intersection point of its 3 angle bisector lines (each of which cuts one of the 3 angles in half).
1001
+ * @param tri a Group or an Iterable<Pt> representing a triangle
1002
+ * @returns the incenter as a Pt
1003
+ */
1004
+ static incenter(tri: PtIterable): Pt | undefined {
1005
+ let _pts = Util.iterToArray(tri);
1006
+ if (_pts.length < 3) return _errorLength(undefined, 3);
1007
+ let a = Polygon.bisector(_pts, 0)!.add(_pts[0]);
1008
+ let b = Polygon.bisector(_pts, 1)!.add(_pts[1]);
1009
+ return Line.intersectRay2D(new Group(_pts[0], a), new Group(_pts[1], b));
1010
+ }
1011
+
1012
+ /**
1013
+ * Get an interior circle, which is the largest circle completed enclosed by this triangle.
1014
+ * @param tri a Group or an Iterable<Pt> representing a triangle
1015
+ * @param center Optional parameter if the incenter is already known. Otherwise, leave it empty and the incenter will be calculated
1016
+ */
1017
+ static incircle(tri: PtIterable, center?: Pt): Group | undefined {
1018
+ let _pts = Util.iterToArray(tri);
1019
+ let c = center ? center : Triangle.incenter(_pts);
1020
+ if (!c) return undefined; // degenerate (collinear) triangle
1021
+ let area = Polygon.area(_pts);
1022
+ let perim = Polygon.perimeter(_pts, true);
1023
+ let r = (2 * area) / perim.total;
1024
+ // incenter can still resolve for some collinear orderings; a zero-area
1025
+ // triangle has no incircle regardless of point order
1026
+ if (!(r > 0)) return undefined;
1027
+ return Circle.fromCenter(c, r);
1028
+ }
1029
+
1030
+ /**
1031
+ * Get circumcenter, which is the intersection point of its 3 perpendicular bisectors lines ( each of which divides a side in half and is perpendicular to the side).
1032
+ * @param tri a Group or an Iterable<Pt> representing a triangle
1033
+ * @returns the circumcenter as a Pt
1034
+ */
1035
+ static circumcenter(tri: PtIterable): Pt | undefined {
1036
+ let _pts = Util.iterToArray(tri);
1037
+ let md = Triangle.medial(_pts);
1038
+ let a = [
1039
+ md[0],
1040
+ Geom.perpendicular(_pts[0].$subtract(md[0])).p1.$add(md[0]),
1041
+ ];
1042
+ let b = [
1043
+ md[1],
1044
+ Geom.perpendicular(_pts[1].$subtract(md[1])).p1.$add(md[1]),
1045
+ ];
1046
+ return Line.intersectRay2D(a, b);
1047
+ }
1048
+
1049
+ /**
1050
+ * Get circumcenter, which is the intersection point of its 3 perpendicular bisectors lines ( each of which divides a side in half and is perpendicular to the side).
1051
+ * @param tri a Group or an Iterable<Pt> representing a triangle
1052
+ * @param center Optional parameter if the circumcenter is already known. Otherwise, leave it empty and the circumcenter will be calculated
1053
+ */
1054
+ static circumcircle(tri: PtIterable, center?: Pt): Group | undefined {
1055
+ let _pts = Util.iterToArray(tri);
1056
+ let c = center ? center : Triangle.circumcenter(_pts);
1057
+ if (!c) return undefined; // degenerate (collinear) triangle
1058
+ let r = _pts[0].$subtract(c).magnitude();
1059
+ return Circle.fromCenter(c, r);
1060
+ }
1061
+ }
1062
+
1063
+ /**
1064
+ * Polygon class provides static functions to create and operate on polygons. A polygon is usually represented as a Group of 3 or more Pts.
1065
+ * You can use the static functions as-is, or apply the [`Group.op`](#link) or [`Pt.op`](#link) to enable functional programming.
1066
+ * See [Op guide](../guide/Op-0400.html) for details.
1067
+ */
1068
+ export class Polygon {
1069
+ /**
1070
+ * Get the centroid of a polygon, which is the average of all its points.
1071
+ * @param pts a Group or an Iterable<PtLike> representing a polygon
1072
+ */
1073
+ static centroid(pts: PtLikeIterable): Pt {
1074
+ return Geom.centroid(pts);
1075
+ }
1076
+
1077
+ /**
1078
+ * Create a rectangular polygon. Same as creating a Rectangle and then getting its corners via [`Rectangle.corners`](#link).
1079
+ * @param center center point of the rectangle
1080
+ * @param widthOrSize width as number, or a Pt representing the size of the rectangle
1081
+ * @param height optional height
1082
+ */
1083
+ static rectangle(
1084
+ center: PtLike,
1085
+ widthOrSize: number | PtLike,
1086
+ height?: number,
1087
+ ): Group {
1088
+ return Rectangle.corners(Rectangle.fromCenter(center, widthOrSize, height));
1089
+ }
1090
+
1091
+ /**
1092
+ * Create a regular polygon.
1093
+ * @param center The center position of the polygon
1094
+ * @param radius The radius, ie, a length from the center position to one of the polygon's corners.
1095
+ * @param sides Number of sides
1096
+ */
1097
+ static fromCenter(center: PtLike, radius: number, sides: number) {
1098
+ let g = new Group();
1099
+ for (let i = 0; i < sides; i++) {
1100
+ let ang = (Math.PI * 2 * i) / sides;
1101
+ g.push(
1102
+ new Pt(Math.cos(ang) * radius, Math.sin(ang) * radius).add(center),
1103
+ );
1104
+ }
1105
+ return g;
1106
+ }
1107
+
1108
+ /**
1109
+ * Given a polygon, get one edge using an index.
1110
+ * @param pts a Group or an Iterable<PtLike> representing a polygon
1111
+ * @param index index of a Pt in the Group
1112
+ */
1113
+ static lineAt(pts: PtLikeIterable, index: number) {
1114
+ let _pts = Util.iterToArray(pts);
1115
+ if (index < 0 || index >= _pts.length)
1116
+ throw new Error("index out of the Polygon's range");
1117
+ return new Group(
1118
+ _pts[index],
1119
+ index === _pts.length - 1 ? _pts[0] : _pts[index + 1],
1120
+ );
1121
+ }
1122
+
1123
+ /**
1124
+ * Get the line segments in this polygon.
1125
+ * @param poly a Group or an Iterable<Pt>
1126
+ * @param closePath a boolean to specify whether the polygon should be closed (ie, whether the final segment should be counted).
1127
+ * @returns an array of Groups which has 2 Pts in each group
1128
+ */
1129
+ static lines(poly: PtIterable, closePath: boolean = true): Group[] {
1130
+ let _pts = Util.iterToArray(poly);
1131
+ if (_pts.length < 2) return _errorLength(new Group(), 2);
1132
+ // build real Groups; indexed assignment avoids the slow spread through
1133
+ // the Array subclass constructor
1134
+ const count = closePath ? _pts.length : _pts.length - 1;
1135
+ const sp: Group[] = new Array(count);
1136
+ for (let i = 0; i < count; i++) {
1137
+ const seg = new Group();
1138
+ seg[0] = _pts[i];
1139
+ seg[1] = _pts[i === _pts.length - 1 ? 0 : i + 1];
1140
+ sp[i] = seg;
1141
+ }
1142
+ return sp;
1143
+ }
1144
+
1145
+ /**
1146
+ * Get a new polygon group that is derived from midpoints in this polygon.
1147
+ * @param poly a Group or an Iterable<Pt>
1148
+ * @param closePath a boolean to specify whether the polygon should be closed (ie, whether the final segment should be counted).
1149
+ * @param t a value between 0 to 1 for interpolation. Default to 0.5 which will get the middle point.
1150
+ */
1151
+ static midpoints(
1152
+ poly: PtIterable,
1153
+ closePath: boolean = false,
1154
+ t: number = 0.5,
1155
+ ): Group {
1156
+ const sides = Polygon.lines(poly, closePath);
1157
+ const mids = new Group();
1158
+ for (let i = 0, len = sides.length; i < len; i++) {
1159
+ mids[i] = Geom.interpolate(sides[i][0], sides[i][1], t);
1160
+ }
1161
+ return mids;
1162
+ }
1163
+
1164
+ /**
1165
+ * Given a Pt in the polygon group, the adjacent sides are the two sides which the Pt touches.
1166
+ * @param poly a Group or an Iterable<Pt>
1167
+ * @param index the target Pt
1168
+ * @param closePath a boolean to specify whether the polygon should be closed (ie, whether the final segment should be counted).
1169
+ */
1170
+ static adjacentSides(
1171
+ poly: PtIterable,
1172
+ index: number,
1173
+ closePath: boolean = false,
1174
+ ): Group[] {
1175
+ let _pts = Util.iterToArray(poly);
1176
+ if (_pts.length < 2) return _errorLength(new Group(), 2);
1177
+ if (index < 0 || index >= _pts.length)
1178
+ return _errorOutofBound(new Group(), index);
1179
+
1180
+ let gs = [];
1181
+ let left = index - 1;
1182
+ if (closePath && left < 0) left = _pts.length - 1;
1183
+ if (left >= 0) gs.push(new Group(_pts[index], _pts[left]));
1184
+
1185
+ let right = index + 1;
1186
+ if (closePath && right > _pts.length - 1) right = 0;
1187
+ if (right <= _pts.length - 1) gs.push(new Group(_pts[index], _pts[right]));
1188
+
1189
+ return gs;
1190
+ }
1191
+
1192
+ /**
1193
+ * Get a bisector which is a line that split between two sides of a polygon equally.
1194
+ * @param poly a Group or an Iterable<Pt>
1195
+ * @param index the Pt in the polygon to bisect from
1196
+ * @returns a bisector direction Pt, the average of the two adjacent sides' unit vectors (not itself normalized)
1197
+ */
1198
+ static bisector(poly: PtIterable, index: number): Pt | undefined {
1199
+ let sides = Polygon.adjacentSides(poly, index, true);
1200
+ if (sides.length >= 2) {
1201
+ let a = sides[0][1].$subtract(sides[0][0]).unit();
1202
+ let b = sides[1][1].$subtract(sides[1][0]).unit();
1203
+ return a.add(b).divide(2);
1204
+ } else {
1205
+ return undefined;
1206
+ }
1207
+ }
1208
+
1209
+ /**
1210
+ * Find the perimeter of this polygon, ie, the lengths of its sides.
1211
+ * @param poly a Group or an Iterable<Pt>
1212
+ * @param closePath a boolean to specify whether the polygon should be closed (ie, whether the final segment should be counted).
1213
+ * @returns an object with `total` length, and `segments` which is a Pt that stores each segment's length
1214
+ */
1215
+ static perimeter(
1216
+ poly: PtIterable,
1217
+ closePath: boolean = false,
1218
+ ): { total: number; segments: Pt } {
1219
+ const _pts = Util.iterToArray(poly);
1220
+ if (_pts.length < 2) {
1221
+ _errorLength(new Group(), 2);
1222
+ return { total: 0, segments: Pt.make(0, 0) };
1223
+ }
1224
+
1225
+ const count = closePath ? _pts.length : _pts.length - 1;
1226
+ const p = Pt.make(count, 0);
1227
+ let mag = 0;
1228
+
1229
+ for (let i = 0; i < count; i++) {
1230
+ const a = _pts[i];
1231
+ const b = _pts[i === _pts.length - 1 ? 0 : i + 1];
1232
+ const dx = b[0] - a[0];
1233
+ const dy = b[1] - a[1];
1234
+ const m = Math.sqrt(dx * dx + dy * dy);
1235
+ mag += m;
1236
+ p[i] = m;
1237
+ }
1238
+
1239
+ return {
1240
+ total: mag,
1241
+ segments: p,
1242
+ };
1243
+ }
1244
+
1245
+ /**
1246
+ * Find the area of a simple (non-self-intersecting) polygon using the shoelace formula.
1247
+ * @param pts a Group or an Iterable<PtLike> representing a polygon
1248
+ */
1249
+ static area(pts: PtLikeIterable): number {
1250
+ let _pts = Util.iterToArray(pts);
1251
+ if (_pts.length < 3) return _errorLength(0, 3);
1252
+ // determinant
1253
+ let det = (a: PtLike, b: PtLike) => a[0] * b[1] - a[1] * b[0];
1254
+
1255
+ let area = 0;
1256
+ for (let i = 0, len = _pts.length; i < len; i++) {
1257
+ if (i < _pts.length - 1) {
1258
+ area += det(_pts[i], _pts[i + 1]);
1259
+ } else {
1260
+ area += det(_pts[i], _pts[0]);
1261
+ }
1262
+ }
1263
+ return Math.abs(area / 2);
1264
+ }
1265
+
1266
+ /**
1267
+ * Get a convex hull of a set of points, using Melkman's algorithm. ([Reference](http://geomalgorithms.com/a12-_hull-3.html)).
1268
+ * @param pts a Group or an Iterable<PtLike>
1269
+ * @param sorted a boolean value to indicate if the group is pre-sorted by x position. Default is false.
1270
+ * @returns a group of Pt that defines the convex hull polygon
1271
+ */
1272
+ static convexHull(pts: PtLikeIterable, sorted: boolean = false): Group {
1273
+ let _pts = Util.iterToArray(pts);
1274
+ if (_pts.length < 3) return _errorLength(new Group(), 3);
1275
+
1276
+ if (!sorted) {
1277
+ _pts = _pts.slice();
1278
+ _pts.sort((a, b) => a[0] - b[0]);
1279
+ }
1280
+
1281
+ // check if is on left of ray a-b
1282
+ let left = (a: PtLike, b: PtLike, c: PtLike) => {
1283
+ return (b[0] - a[0]) * (c[1] - a[1]) - (c[0] - a[0]) * (b[1] - a[1]) > 0;
1284
+ };
1285
+
1286
+ // double end queue
1287
+ let dq = [];
1288
+ let bot = _pts.length - 2;
1289
+ let top = bot + 3;
1290
+ dq[bot] = _pts[2];
1291
+ dq[top] = _pts[2];
1292
+
1293
+ // first 3 pt as counter-clockwise triangle
1294
+ if (left(_pts[0], _pts[1], _pts[2])) {
1295
+ dq[bot + 1] = _pts[0];
1296
+ dq[bot + 2] = _pts[1];
1297
+ } else {
1298
+ dq[bot + 1] = _pts[1];
1299
+ dq[bot + 2] = _pts[0];
1300
+ }
1301
+
1302
+ // remaining pts
1303
+ for (let i = 3, len = _pts.length; i < len; i++) {
1304
+ let pt = _pts[i];
1305
+
1306
+ // if inside the hull
1307
+ if (left(dq[bot], dq[bot + 1], pt) && left(dq[top - 1], dq[top], pt)) {
1308
+ continue;
1309
+ }
1310
+
1311
+ // rightmost tangent
1312
+ while (!left(dq[bot], dq[bot + 1], pt)) {
1313
+ bot += 1;
1314
+ }
1315
+ bot -= 1;
1316
+ dq[bot] = pt;
1317
+
1318
+ // leftmost tangent
1319
+ while (!left(dq[top - 1], dq[top], pt)) {
1320
+ top -= 1;
1321
+ }
1322
+ top += 1;
1323
+ dq[top] = pt;
1324
+ }
1325
+
1326
+ let hull = new Group();
1327
+ for (let h = 0; h < top - bot; h++) {
1328
+ hull.push(dq[bot + h]);
1329
+ }
1330
+
1331
+ return hull;
1332
+ }
1333
+
1334
+ /**
1335
+ * Given a point in the polygon as an origin, get an array of lines that connect all the remaining points to the origin point.
1336
+ * @param poly a Group or an Iterable<Pt> representing a polygon
1337
+ * @param originIndex the origin point's index in the polygon
1338
+ * @returns an array of Groups of line segments
1339
+ */
1340
+ static network(poly: PtIterable, originIndex: number = 0): Group[] {
1341
+ let _pts = Util.iterToArray(poly);
1342
+ let g = [];
1343
+ for (let i = 0, len = _pts.length; i < len; i++) {
1344
+ if (i != originIndex) g.push(new Group(_pts[originIndex], _pts[i]));
1345
+ }
1346
+ return g;
1347
+ }
1348
+
1349
+ /**
1350
+ * Given a target Pt, find a Pt in the polygon's corners that's nearest to it.
1351
+ * @param poly a Group or an Iterable<Pt>
1352
+ * @param pt Pt to check
1353
+ * @returns an index in the pts indicating the nearest Pt, or -1 if none found
1354
+ */
1355
+ static nearestPt(poly: PtIterable, pt: PtLike): number {
1356
+ const _poly = Util.iterToArray(poly);
1357
+ const px = pt[0];
1358
+ const py = pt[1];
1359
+ let _near = Number.MAX_VALUE;
1360
+ let _item = -1;
1361
+ for (let i = 0, len = _poly.length; i < len; i++) {
1362
+ const dx = _poly[i][0] - px;
1363
+ const dy = _poly[i][1] - py;
1364
+ const d = dx * dx + dy * dy;
1365
+ if (d < _near) {
1366
+ _near = d;
1367
+ _item = i;
1368
+ }
1369
+ }
1370
+ return _item;
1371
+ }
1372
+
1373
+ /**
1374
+ * Project axis (eg, for use in Separation Axis Theorem).
1375
+ * @param poly a Group or an Iterable<Pt>
1376
+ * @param unitAxis unit axis for calculating dot product
1377
+ */
1378
+ static projectAxis(poly: PtIterable, unitAxis: Pt): Pt {
1379
+ let _poly = Util.iterToArray(poly);
1380
+ let min = unitAxis.dot(_poly[0]);
1381
+ let max = min;
1382
+ for (let n = 1, len = _poly.length; n < len; n++) {
1383
+ const dot = unitAxis.dot(_poly[n]);
1384
+ if (dot < min) min = dot;
1385
+ else if (dot > max) max = dot;
1386
+ }
1387
+ return new Pt(min, max);
1388
+ }
1389
+
1390
+ /**
1391
+ * Scalar core of the axis-overlap test used by the SAT functions: project both polygons on
1392
+ * the unit axis (ax, ay) and return the gap between the intervals (negative means overlap).
1393
+ */
1394
+ private static _axisOverlap2D(
1395
+ poly1: Pt[],
1396
+ poly2: Pt[],
1397
+ ax: number,
1398
+ ay: number,
1399
+ ): number {
1400
+ let min1 = ax * poly1[0][0] + ay * poly1[0][1];
1401
+ let max1 = min1;
1402
+ for (let n = 1, len = poly1.length; n < len; n++) {
1403
+ const d = ax * poly1[n][0] + ay * poly1[n][1];
1404
+ if (d < min1) min1 = d;
1405
+ else if (d > max1) max1 = d;
1406
+ }
1407
+ let min2 = ax * poly2[0][0] + ay * poly2[0][1];
1408
+ let max2 = min2;
1409
+ for (let n = 1, len = poly2.length; n < len; n++) {
1410
+ const d = ax * poly2[n][0] + ay * poly2[n][1];
1411
+ if (d < min2) min2 = d;
1412
+ else if (d > max2) max2 = d;
1413
+ }
1414
+ return min1 < min2 ? min2 - max1 : min1 - max2;
1415
+ }
1416
+
1417
+ /**
1418
+ * Check overlap distance from projected axis.
1419
+ * @param poly1 a Group or an Iterable<Pt> representing the first polygon
1420
+ * @param poly2 a Group or an Iterable<Pt> representing the second polygon
1421
+ * @param unitAxis unit axis
1422
+ */
1423
+ protected static _axisOverlap(
1424
+ poly1: PtIterable,
1425
+ poly2: PtIterable,
1426
+ unitAxis: Pt,
1427
+ ) {
1428
+ let pa = Polygon.projectAxis(poly1, unitAxis);
1429
+ let pb = Polygon.projectAxis(poly2, unitAxis);
1430
+ return pa[0] < pb[0] ? pb[0] - pa[1] : pa[0] - pb[1];
1431
+ }
1432
+
1433
+ /**
1434
+ * Check if a Pt is inside a convex polygon.
1435
+ * @param poly a Group or an Iterable<PtLike> representing a convex polygon
1436
+ * @param pt the Pt to check
1437
+ */
1438
+ static hasIntersectPoint(poly: PtLikeIterable, pt: PtLike): boolean {
1439
+ const _poly = Util.iterToArray(poly);
1440
+ const px = pt[0];
1441
+ const py = pt[1];
1442
+ let c = false;
1443
+ // same ray-cast as before, without a Group allocation per edge
1444
+ for (let i = 0, len = _poly.length; i < len; i++) {
1445
+ const a = _poly[i];
1446
+ const b = _poly[i === len - 1 ? 0 : i + 1];
1447
+ if (
1448
+ a[1] > py != b[1] > py &&
1449
+ px < ((b[0] - a[0]) * (py - a[1])) / (b[1] - a[1]) + a[0]
1450
+ ) {
1451
+ c = !c;
1452
+ }
1453
+ }
1454
+ return c;
1455
+ }
1456
+
1457
+ /**
1458
+ * Check if a convex polygon and a circle has intersections using Separating Axis Theorem.
1459
+ * @param poly a Group or an Iterable<Pt> representing a convex polygon
1460
+ * @param circle a Group or an Iterable<Pt> representing a circle
1461
+ * @returns an `IntersectContext` object that stores the intersection info, or undefined if there's no intersection
1462
+ */
1463
+ static hasIntersectCircle(
1464
+ poly: PtIterable,
1465
+ circle: PtIterable,
1466
+ ): IntersectContext | null {
1467
+ let _poly = Util.iterToArray(poly);
1468
+ let _circle = Util.iterToArray(circle);
1469
+
1470
+ const c = _circle[0];
1471
+ const r = _circle[1][0];
1472
+
1473
+ // AABB pre-reject against the circle's bounding box. The corner cases this
1474
+ // skips are exactly those the perpendicular-foot check below would reject.
1475
+ let bx0 = Infinity;
1476
+ let by0 = Infinity;
1477
+ let bx1 = -Infinity;
1478
+ let by1 = -Infinity;
1479
+ for (let i = 0, len = _poly.length; i < len; i++) {
1480
+ const p = _poly[i];
1481
+ if (p[0] < bx0) bx0 = p[0];
1482
+ if (p[0] > bx1) bx1 = p[0];
1483
+ if (p[1] < by0) by0 = p[1];
1484
+ if (p[1] > by1) by1 = p[1];
1485
+ }
1486
+ if (c[0] + r < bx0 || c[0] - r > bx1 || c[1] + r < by0 || c[1] - r > by1) {
1487
+ return null;
1488
+ }
1489
+
1490
+ let minDist = Number.MAX_SAFE_INTEGER;
1491
+ let minEdge: Group | null = null;
1492
+ let minAx = 0;
1493
+ let minAy = 0;
1494
+ let which = -1;
1495
+
1496
+ for (let i = 0, len = _poly.length; i < len; i++) {
1497
+ const ea = _poly[i];
1498
+ const eb = _poly[i === len - 1 ? 0 : i + 1];
1499
+ let ax = ea[1] - eb[1]; // unit perpendicular of the edge
1500
+ let ay = eb[0] - ea[0];
1501
+ const alen = Math.sqrt(ax * ax + ay * ay);
1502
+ if (alen === 0) continue; // degenerate edge (duplicate points)
1503
+ ax /= alen;
1504
+ ay /= alen;
1505
+
1506
+ // the circle projects onto the axis as [center·axis − r, center·axis + r]
1507
+ let minP = ax * _poly[0][0] + ay * _poly[0][1];
1508
+ let maxP = minP;
1509
+ for (let n = 1; n < len; n++) {
1510
+ const d = ax * _poly[n][0] + ay * _poly[n][1];
1511
+ if (d < minP) minP = d;
1512
+ else if (d > maxP) maxP = d;
1513
+ }
1514
+ const dotC = ax * c[0] + ay * c[1];
1515
+ const dist = minP < dotC - r ? dotC - r - maxP : minP - (dotC + r);
1516
+
1517
+ if (dist > 0) {
1518
+ return null;
1519
+ } else if (Math.abs(dist) < minDist) {
1520
+ // Fix edge case and make sure the circle is intersecting. To be improved.
1521
+ const edge = Polygon.lineAt(_poly, i);
1522
+ const check =
1523
+ Rectangle.withinBound(edge, Line.perpendicularFromPt(edge, c)!) ||
1524
+ Circle.intersectLine2D(_circle, edge).length > 0;
1525
+
1526
+ if (check) {
1527
+ minEdge = edge;
1528
+ minAx = ax;
1529
+ minAy = ay;
1530
+ minDist = Math.abs(dist);
1531
+ which = i;
1532
+ }
1533
+ }
1534
+ }
1535
+
1536
+ if (!minEdge) return null;
1537
+
1538
+ // Edge normals alone miss the case where the circle sits past a vertex:
1539
+ // the separating axis there runs from the nearest vertex to the center.
1540
+ let vx = 0;
1541
+ let vy = 0;
1542
+ let vd = Infinity;
1543
+ for (let i = 0, len = _poly.length; i < len; i++) {
1544
+ const dx = c[0] - _poly[i][0];
1545
+ const dy = c[1] - _poly[i][1];
1546
+ const d = dx * dx + dy * dy;
1547
+ if (d < vd) {
1548
+ vd = d;
1549
+ vx = dx;
1550
+ vy = dy;
1551
+ }
1552
+ }
1553
+ if (vd > 0) {
1554
+ const vlen = Math.sqrt(vd);
1555
+ vx /= vlen;
1556
+ vy /= vlen;
1557
+ let minP = Infinity;
1558
+ let maxP = -Infinity;
1559
+ for (let i = 0, len = _poly.length; i < len; i++) {
1560
+ const d = vx * _poly[i][0] + vy * _poly[i][1];
1561
+ if (d < minP) minP = d;
1562
+ if (d > maxP) maxP = d;
1563
+ }
1564
+ const dotC = vx * c[0] + vy * c[1];
1565
+ if (dotC - r > maxP || dotC + r < minP) return null;
1566
+ }
1567
+
1568
+ // direction
1569
+ const centroid = Polygon.centroid(_poly);
1570
+ if (minAx * (c[0] - centroid[0]) + minAy * (c[1] - centroid[1]) < 0) {
1571
+ minAx = -minAx;
1572
+ minAy = -minAy;
1573
+ }
1574
+
1575
+ return {
1576
+ which,
1577
+ dist: minDist,
1578
+ normal: new Pt(minAx, minAy),
1579
+ edge: minEdge,
1580
+ vertex: c,
1581
+ };
1582
+ }
1583
+
1584
+ /**
1585
+ * Check if two convex polygons have intersections using Separating Axis Theorem.
1586
+ * @param poly1 a Group or an Iterable<Pt> representing a convex polygon
1587
+ * @param poly2 a Group or an Iterable<Pt> representing another convex polygon
1588
+ * @return an `IntersectContext` object that stores the intersection info, or undefined if there's no intersection
1589
+ */
1590
+ static hasIntersectPolygon(
1591
+ poly1: PtIterable,
1592
+ poly2: PtIterable,
1593
+ ): IntersectContext | null {
1594
+ // Reference: https://www.gamedev.net/articles/programming/math-and-physics/a-verlet-based-approach-for-2d-game-physics-r2714/
1595
+ let _poly1 = Util.iterToArray(poly1);
1596
+ let _poly2 = Util.iterToArray(poly2);
1597
+ const len1 = _poly1.length;
1598
+ const len2 = _poly2.length;
1599
+
1600
+ // AABB pre-reject: for convex polygons, disjoint bounding boxes guarantee
1601
+ // that a separating edge normal exists, so the axis scan can be skipped
1602
+ let ax0 = Infinity;
1603
+ let ay0 = Infinity;
1604
+ let ax1 = -Infinity;
1605
+ let ay1 = -Infinity;
1606
+ for (let i = 0; i < len1; i++) {
1607
+ const p = _poly1[i];
1608
+ if (p[0] < ax0) ax0 = p[0];
1609
+ if (p[0] > ax1) ax1 = p[0];
1610
+ if (p[1] < ay0) ay0 = p[1];
1611
+ if (p[1] > ay1) ay1 = p[1];
1612
+ }
1613
+ let bx0 = Infinity;
1614
+ let by0 = Infinity;
1615
+ let bx1 = -Infinity;
1616
+ let by1 = -Infinity;
1617
+ for (let i = 0; i < len2; i++) {
1618
+ const p = _poly2[i];
1619
+ if (p[0] < bx0) bx0 = p[0];
1620
+ if (p[0] > bx1) bx1 = p[0];
1621
+ if (p[1] < by0) by0 = p[1];
1622
+ if (p[1] > by1) by1 = p[1];
1623
+ }
1624
+ if (ax0 > bx1 || bx0 > ax1 || ay0 > by1 || by0 > ay1) return null;
1625
+
1626
+ // scan all edge normals as separating axes, tracking the smallest overlap
1627
+ let minDist = Number.MAX_SAFE_INTEGER;
1628
+ let minIndex = -1;
1629
+ let minAx = 0;
1630
+ let minAy = 0;
1631
+
1632
+ for (let i = 0, plen = len1 + len2; i < plen; i++) {
1633
+ const src = i < len1 ? _poly1 : _poly2;
1634
+ const ei = i < len1 ? i : i - len1;
1635
+ const ea = src[ei];
1636
+ const eb = src[ei === src.length - 1 ? 0 : ei + 1];
1637
+ let ax = ea[1] - eb[1]; // unit perpendicular of the edge
1638
+ let ay = eb[0] - ea[0];
1639
+ const alen = Math.sqrt(ax * ax + ay * ay);
1640
+ if (alen === 0) continue; // degenerate edge (duplicate points)
1641
+ ax /= alen;
1642
+ ay /= alen;
1643
+
1644
+ const dist = Polygon._axisOverlap2D(_poly1, _poly2, ax, ay);
1645
+ if (dist > 0) {
1646
+ return null;
1647
+ } else if (Math.abs(dist) < minDist) {
1648
+ minDist = Math.abs(dist);
1649
+ minIndex = i;
1650
+ minAx = ax;
1651
+ minAy = ay;
1652
+ }
1653
+ }
1654
+
1655
+ if (minIndex < 0) return null;
1656
+
1657
+ const which = minIndex < len1 ? 0 : 1;
1658
+ const edge =
1659
+ which === 0
1660
+ ? Polygon.lineAt(_poly1, minIndex)
1661
+ : Polygon.lineAt(_poly2, minIndex - len1);
1662
+
1663
+ // flip if needed to make sure vertex and edge are in corresponding polygons
1664
+ const b1 = which === 0 ? _poly2 : _poly1;
1665
+ const b2 = which === 0 ? _poly1 : _poly2;
1666
+
1667
+ const c1 = Polygon.centroid(b1);
1668
+ const c2 = Polygon.centroid(b2);
1669
+
1670
+ // direction
1671
+ if (minAx * (c1[0] - c2[0]) + minAy * (c1[1] - c2[1]) < 0) {
1672
+ minAx = -minAx;
1673
+ minAy = -minAy;
1674
+ }
1675
+
1676
+ // find vertex at smallest distance
1677
+ let smallest = Number.MAX_SAFE_INTEGER;
1678
+ let vertex: Pt = null!;
1679
+ for (let i = 0, len = b1.length; i < len; i++) {
1680
+ const d = minAx * (b1[i][0] - c2[0]) + minAy * (b1[i][1] - c2[1]);
1681
+ if (d < smallest) {
1682
+ smallest = d;
1683
+ vertex = b1[i];
1684
+ }
1685
+ }
1686
+
1687
+ return {
1688
+ which,
1689
+ dist: minDist,
1690
+ normal: new Pt(minAx, minAy),
1691
+ edge,
1692
+ vertex,
1693
+ };
1694
+ }
1695
+
1696
+ /**
1697
+ * Find intersection points of 2 polygons by checking every side of both polygons. Performance may be slow for complex polygons.
1698
+ * @param poly1 a Group or an Iterable<Pt> representing a polygon
1699
+ * @param poly2 a Group or an Iterable<Pt> representing another polygon
1700
+ */
1701
+ static intersectPolygon2D(poly1: PtIterable, poly2: PtIterable): Group {
1702
+ let _poly1 = Util.iterToArray(poly1);
1703
+ let _poly2 = Util.iterToArray(poly2);
1704
+
1705
+ let lp = Polygon.lines(_poly1);
1706
+ let g = [];
1707
+ for (let i = 0, len = lp.length; i < len; i++) {
1708
+ let ins = Line.intersectPolygon2D(lp[i], _poly2, false);
1709
+ if (ins) g.push(ins);
1710
+ }
1711
+ return Util.flatten(g, true) as Group;
1712
+ }
1713
+
1714
+ /**
1715
+ * Get a bounding box for each polygon group, as well as a union bounding-box for all groups.
1716
+ * @param polys an Array/Iterable of (Groups or Iterables<Pt>)
1717
+ */
1718
+ static toRects(polys: Iterable<PtIterable>): Group[] {
1719
+ let boxes = [];
1720
+ for (let g of polys) {
1721
+ boxes.push(Geom.boundingBox(g));
1722
+ }
1723
+
1724
+ let merged = Util.flatten(boxes, false);
1725
+ boxes.unshift(Geom.boundingBox(merged));
1726
+ return boxes;
1727
+ }
1728
+ }
1729
+
1730
+ /**
1731
+ * Path class provides static functions to combine polygons with boolean operations:
1732
+ * unite, intersect, exclude, subtract the shapes in front or behind, divide into faces, or crop by the top shape.
1733
+ * Shapes are listed in stacking order, the first at the back and the last in front, like the order you would draw them in.
1734
+ * A shape is a polygon (a Group, or any iterable of points), or a list of rings that together form a polygon with holes,
1735
+ * such as the result of another Path function. Every result is such a list of rings: an outer ring followed by its
1736
+ * holes, in opposite orientations, which [`CanvasForm.compound`](#link) draws as one path. Rings are open (the first point
1737
+ * is not repeated), 2D, and never share Pts with the input. Clockwise and counterclockwise rings are the same shape, and
1738
+ * a self-intersecting ring covers what `form.polygon` would fill (the nonzero rule). Input vertices closer together than a
1739
+ * millionth of the largest absolute coordinate are merged before finding intersections. For small shapes at large offsets,
1740
+ * work in local coordinates to avoid losing detail to this tolerance or the Float32 output.
1741
+ * See [Op guide](../guide/Op-0400.html) for details.
1742
+ */
1743
+ export class Path {
1744
+ /**
1745
+ * Unite: merge all shapes into one polygon (the area inside any shape).
1746
+ * @param shapes an Array/Iterable of polygons in stacking order, back to front. Each is a Group or an Iterable<PtLike>, or a list of rings for a polygon with holes. A single ring is taken as one shape.
1747
+ * @returns the rings of the merged polygon: each outer ring followed by its holes; empty if the shapes have no area
1748
+ * @example `form.fillOnly("#f03").compound( Path.unite( [star, disc] ) )`
1749
+ */
1750
+ static unite(shapes: Iterable<PolygonLike> | PtLikeIterable): Group[] {
1751
+ return overlay(shapes, "unite");
1752
+ }
1753
+
1754
+ /**
1755
+ * Intersect: keep only the area inside every shape.
1756
+ * @param shapes an Array/Iterable of polygons in stacking order, back to front. Each is a Group or an Iterable<PtLike>, or a list of rings for a polygon with holes. A single ring is taken as one shape.
1757
+ * @returns the rings of the common polygon: each outer ring followed by its holes; empty if the shapes do not all overlap
1758
+ * @example `Path.intersect( [a, b, c] )`
1759
+ */
1760
+ static intersect(shapes: Iterable<PolygonLike> | PtLikeIterable): Group[] {
1761
+ return overlay(shapes, "intersect");
1762
+ }
1763
+
1764
+ /**
1765
+ * Exclude: keep the area inside an odd number of shapes, so where two shapes overlap becomes a hole.
1766
+ * @param shapes an Array/Iterable of polygons in stacking order, back to front. Each is a Group or an Iterable<PtLike>, or a list of rings for a polygon with holes. A single ring is taken as one shape.
1767
+ * @returns the rings of the result: each outer ring followed by its holes; empty if the shapes cancel out
1768
+ * @example `Path.exclude( [a, b] )`
1769
+ */
1770
+ static exclude(shapes: Iterable<PolygonLike> | PtLikeIterable): Group[] {
1771
+ return overlay(shapes, "exclude");
1772
+ }
1773
+
1774
+ /**
1775
+ * Minus Front: subtract every shape in front from the backmost (first) shape.
1776
+ * @param shapes an Array/Iterable of polygons in stacking order, back to front. Each is a Group or an Iterable<PtLike>, or a list of rings for a polygon with holes. A single ring is taken as one shape.
1777
+ * @returns the rings of what remains of the first shape: each outer ring followed by its holes; empty if nothing remains
1778
+ * @example `Path.minusFront( [disc, hole] )` cuts `hole` out of `disc`
1779
+ */
1780
+ static minusFront(shapes: Iterable<PolygonLike> | PtLikeIterable): Group[] {
1781
+ return overlay(shapes, "minusFront");
1782
+ }
1783
+
1784
+ /**
1785
+ * Minus Back: subtract every shape behind from the frontmost (last) shape.
1786
+ * @param shapes an Array/Iterable of polygons in stacking order, back to front. Each is a Group or an Iterable<PtLike>, or a list of rings for a polygon with holes. A single ring is taken as one shape.
1787
+ * @returns the rings of what remains of the last shape: each outer ring followed by its holes; empty if nothing remains
1788
+ * @example `Path.minusBack( [wall, window] )` keeps the part of `window` not covered by `wall`
1789
+ */
1790
+ static minusBack(shapes: Iterable<PolygonLike> | PtLikeIterable): Group[] {
1791
+ return overlay(shapes, "minusBack");
1792
+ }
1793
+
1794
+ /**
1795
+ * Divide: split the shapes at every crossing into separate faces. Each face is the largest area not cut by any edge, so
1796
+ * a region inside two shapes is its own face, and a self-overlapping region of one shape is too.
1797
+ * @param shapes an Array/Iterable of polygons in stacking order, back to front. Each is a Group or an Iterable<PtLike>, or a list of rings for a polygon with holes. A single ring is taken as one shape.
1798
+ * @returns an array of polygons, one per face, each an outer ring followed by its holes
1799
+ * @example `Path.divide( [a, b] ).forEach( (face, i) => form.fillOnly( colors[i] ).compound( face ) )`
1800
+ */
1801
+ static divide(shapes: Iterable<PolygonLike> | PtLikeIterable): Group[][] {
1802
+ return overlay(shapes, "divide");
1803
+ }
1804
+
1805
+ /**
1806
+ * Crop: use the frontmost (last) shape as a mask, keeping the faces of the other shapes inside it and deleting the mask itself.
1807
+ * Like [`Path.divide`](#link), the shapes under the mask stay divided where they overlap.
1808
+ * See a [demo here](https://ptsjs.org/demo/?name=path.crop).
1809
+ * @param shapes an Array/Iterable of polygons in stacking order, back to front. Each is a Group or an Iterable<PtLike>, or a list of rings for a polygon with holes. A single ring is taken as one shape.
1810
+ * @returns an array of polygons, one per face inside the mask, each an outer ring followed by its holes
1811
+ * @example `Path.crop( [photo, frame] )`
1812
+ */
1813
+ static crop(shapes: Iterable<PolygonLike> | PtLikeIterable): Group[][] {
1814
+ return overlay(shapes, "crop");
1815
+ }
1816
+ }
1817
+
1818
+ /**
1819
+ * Curve class provides static functions to interpolate curves. A curve is usually represented as a Group of 3 or more control points.
1820
+ * You can use the static functions as-is, or apply the [`Group.op`](#link) or [`Pt.op`](#link) to enable functional programming.
1821
+ * See [Op guide](../guide/Op-0400.html) for details.
1822
+ */
1823
+ export class Curve {
1824
+ /**
1825
+ * Get a precalculated coefficients per step.
1826
+ * @param steps number of steps
1827
+ */
1828
+ static getSteps(steps: number): Group {
1829
+ let ts = new Group();
1830
+ for (let i = 0; i <= steps; i++) {
1831
+ let t = i / steps;
1832
+ ts.push(new Pt(t * t * t, t * t, t, 1));
1833
+ }
1834
+ return ts;
1835
+ }
1836
+
1837
+ /**
1838
+ * Given an index for the starting position in a Pt group, get the control and/or end points of a curve segment.
1839
+ * @param pts a Group or an Iterable<PtLike>
1840
+ * @param index start index in `pts` array. Default is 0.
1841
+ * @param copyStart an optional boolean value to indicate if the start index should be used twice. Default is false.
1842
+ * @returns a group of 4 Pts
1843
+ */
1844
+ static controlPoints(
1845
+ pts: PtLikeIterable,
1846
+ index: number = 0,
1847
+ copyStart: boolean = false,
1848
+ ): Group {
1849
+ let _pts = Util.iterToArray(pts);
1850
+
1851
+ if (index > _pts.length - 1) return new Group();
1852
+ let _index = (i: number) => (i < _pts.length - 1 ? i : _pts.length - 1);
1853
+
1854
+ let p0 = _pts[index];
1855
+ index = copyStart ? index : index + 1;
1856
+
1857
+ // get points based on index
1858
+ return new Group(
1859
+ p0,
1860
+ _pts[_index(index++)],
1861
+ _pts[_index(index++)],
1862
+ _pts[_index(index++)],
1863
+ );
1864
+ }
1865
+
1866
+ /**
1867
+ * Build a per-step table of the 4 control-point weights for a curve family.
1868
+ * Computing these once per call (instead of a matrix product per output
1869
+ * point) is what makes the curve functions fast.
1870
+ */
1871
+ private static _weights(
1872
+ steps: number,
1873
+ fill: (t: number, out: Float64Array, o: number) => void,
1874
+ ): Float64Array {
1875
+ const w = new Float64Array((steps + 1) * 4);
1876
+ for (let i = 0; i <= steps; i++) {
1877
+ fill(i / steps, w, i * 4);
1878
+ }
1879
+ return w;
1880
+ }
1881
+
1882
+ /**
1883
+ * Evaluate one curve segment with a precomputed weight table, pushing one
1884
+ * interpolated Pt per step into `out`. Control values are read by index so
1885
+ * both Pts and plain arrays work.
1886
+ */
1887
+ private static _evalSegment(
1888
+ out: Group,
1889
+ c: GroupLike,
1890
+ w: Float64Array,
1891
+ steps: number,
1892
+ ): void {
1893
+ const c0 = c[0];
1894
+ const c1 = c[1];
1895
+ const c2 = c[2];
1896
+ const c3 = c[3];
1897
+ const dim3 = c0.length > 2;
1898
+ for (let i = 0; i <= steps; i++) {
1899
+ const o = i * 4;
1900
+ const w0 = w[o];
1901
+ const w1 = w[o + 1];
1902
+ const w2 = w[o + 2];
1903
+ const w3 = w[o + 3];
1904
+ const x = w0 * c0[0] + w1 * c1[0] + w2 * c2[0] + w3 * c3[0];
1905
+ const y = w0 * c0[1] + w1 * c1[1] + w2 * c2[1] + w3 * c3[1];
1906
+ out.push(
1907
+ dim3
1908
+ ? new Pt(x, y, w0 * c0[2] + w1 * c1[2] + w2 * c2[2] + w3 * c3[2])
1909
+ : new Pt(x, y),
1910
+ );
1911
+ }
1912
+ }
1913
+
1914
+ /**
1915
+ * Calulcate weighted sum to get the interpolated points.
1916
+ * @param ctrls anchors
1917
+ * @param params parameters
1918
+ */
1919
+ static _calcPt(ctrls: GroupLike, params: PtLike): Pt {
1920
+ let x = ctrls.reduce((a, c, i) => a + c.x * params[i], 0);
1921
+ let y = ctrls.reduce((a, c, i) => a + c.y * params[i], 0);
1922
+ if (ctrls[0].length > 2) {
1923
+ let z = ctrls.reduce((a, c, i) => a + c.z * params[i], 0);
1924
+ return new Pt(x, y, z);
1925
+ }
1926
+ return new Pt(x, y);
1927
+ }
1928
+
1929
+ /**
1930
+ * Weighted sum of 4 control points with scalar weights — the shared core
1931
+ * of the single-point step functions, kept consistent with the batch
1932
+ * `_weights` tables by construction.
1933
+ */
1934
+ private static _stepPt(
1935
+ ctrls: GroupLike,
1936
+ w0: number,
1937
+ w1: number,
1938
+ w2: number,
1939
+ w3: number,
1940
+ ): Pt {
1941
+ const c0 = ctrls[0];
1942
+ const c1 = ctrls[1];
1943
+ const c2 = ctrls[2];
1944
+ const c3 = ctrls[3];
1945
+ const x = w0 * c0[0] + w1 * c1[0] + w2 * c2[0] + w3 * c3[0];
1946
+ const y = w0 * c0[1] + w1 * c1[1] + w2 * c2[1] + w3 * c3[1];
1947
+ return c0.length > 2
1948
+ ? new Pt(x, y, w0 * c0[2] + w1 * c1[2] + w2 * c2[2] + w3 * c3[2])
1949
+ : new Pt(x, y);
1950
+ }
1951
+
1952
+ /**
1953
+ * Create a Catmull-Rom curve. Catmull-Rom is a kind of smooth-looking Cardinal curve.
1954
+ * @param pts a Group or an Iterable<PtLike>
1955
+ * @param steps the number of line segments per curve. Defaults to 10 steps
1956
+ * @returns a curve as a group of interpolated Pt
1957
+ */
1958
+ static catmullRom(pts: PtLikeIterable, steps: number = 10): Group {
1959
+ let _pts = Util.iterToArray(pts);
1960
+ if (_pts.length < 2) return new Group();
1961
+
1962
+ let ps = new Group();
1963
+ const w = Curve._weights(steps, (t, out, o) => {
1964
+ const t2 = t * t;
1965
+ const t3 = t2 * t;
1966
+ out[o] = -0.5 * t3 + t2 - 0.5 * t;
1967
+ out[o + 1] = 1.5 * t3 - 2.5 * t2 + 1;
1968
+ out[o + 2] = -1.5 * t3 + 2 * t2 + 0.5 * t;
1969
+ out[o + 3] = 0.5 * t3 - 0.5 * t2;
1970
+ });
1971
+
1972
+ // use first point twice
1973
+ Curve._evalSegment(ps, Curve.controlPoints(_pts, 0, true), w, steps);
1974
+
1975
+ let k = 0;
1976
+ while (k < _pts.length - 2) {
1977
+ let cp = Curve.controlPoints(_pts, k);
1978
+ if (cp.length > 0) {
1979
+ Curve._evalSegment(ps, cp, w, steps);
1980
+ k++;
1981
+ }
1982
+ }
1983
+
1984
+ return ps;
1985
+ }
1986
+
1987
+ /**
1988
+ * Interpolate to get a point on Catmull-Rom curve.
1989
+ * @param step the coefficients [t*t*t, t*t, t, 1]
1990
+ * @param ctrls a group of anchor Pts
1991
+ * @return an interpolated Pt on the curve
1992
+ */
1993
+ static catmullRomStep(step: Pt, ctrls: GroupLike): Pt {
1994
+ // same coefficients as the batch `catmullRom` weight table
1995
+ const t3 = step[0];
1996
+ const t2 = step[1];
1997
+ const t = step[2];
1998
+ return Curve._stepPt(
1999
+ ctrls,
2000
+ -0.5 * t3 + t2 - 0.5 * t,
2001
+ 1.5 * t3 - 2.5 * t2 + 1,
2002
+ -1.5 * t3 + 2 * t2 + 0.5 * t,
2003
+ 0.5 * t3 - 0.5 * t2,
2004
+ );
2005
+ }
2006
+
2007
+ /**
2008
+ * Create a Cardinal curve.
2009
+ * @param pts a Group or an Iterable<PtLike>
2010
+ * @param steps the number of line segments per curve. Defaults to 10 steps.
2011
+ * @param tension optional value between 0 to 1 to specify a "tension". Default to 0.5 which is the tension for Catmull-Rom curve.
2012
+ * @returns a curve as a group of interpolated Pt
2013
+ */
2014
+ static cardinal(
2015
+ pts: PtLikeIterable,
2016
+ steps: number = 10,
2017
+ tension = 0.5,
2018
+ ): Group {
2019
+ let _pts = Util.iterToArray(pts);
2020
+ if (_pts.length < 2) return new Group();
2021
+
2022
+ let ps = new Group();
2023
+ const w = Curve._weights(steps, (t, out, o) => {
2024
+ const t2 = t * t;
2025
+ const t3 = t2 * t;
2026
+ out[o] = tension * (-t3 + 2 * t2 - t);
2027
+ out[o + 1] = tension * (-t3 + t2) + (2 * t3 - 3 * t2 + 1);
2028
+ out[o + 2] = tension * (t3 - 2 * t2 + t) + (-2 * t3 + 3 * t2);
2029
+ out[o + 3] = tension * (t3 - t2);
2030
+ });
2031
+
2032
+ // use first point twice
2033
+ Curve._evalSegment(ps, Curve.controlPoints(_pts, 0, true), w, steps);
2034
+
2035
+ let k = 0;
2036
+ while (k < _pts.length - 2) {
2037
+ let cp = Curve.controlPoints(_pts, k);
2038
+ if (cp.length > 0) {
2039
+ Curve._evalSegment(ps, cp, w, steps);
2040
+ k++;
2041
+ }
2042
+ }
2043
+
2044
+ return ps;
2045
+ }
2046
+
2047
+ /**
2048
+ * Interpolate to get a point on Cardinal curve.
2049
+ * @param step the coefficients [t*t*t, t*t, t, 1]
2050
+ * @param ctrls a group of anchor Pts
2051
+ * @param tension optional value between 0 to 1 to specify a "tension". Default to 0.5 which is the tension for Catmull-Rom curve
2052
+ * @return an interpolated Pt on the curve
2053
+ */
2054
+ static cardinalStep(step: Pt, ctrls: GroupLike, tension: number = 0.5): Pt {
2055
+ // same coefficients as the batch `cardinal` weight table
2056
+ const t3 = step[0];
2057
+ const t2 = step[1];
2058
+ const t = step[2];
2059
+ return Curve._stepPt(
2060
+ ctrls,
2061
+ tension * (-t3 + 2 * t2 - t),
2062
+ tension * (-t3 + t2) + (2 * t3 - 3 * t2 + 1),
2063
+ tension * (t3 - 2 * t2 + t) + (-2 * t3 + 3 * t2),
2064
+ tension * (t3 - t2),
2065
+ );
2066
+ }
2067
+
2068
+ /**
2069
+ * Convert the anchors of a Cardinal curve into cubic Bezier control points, so the same curve can be drawn as a native path
2070
+ * with [`CanvasForm.bezier`](#link) or sampled with [`Curve.bezier`](#link).
2071
+ * With the default `alpha`, the Bezier traces the curve that [`Curve.cardinal`](#link) approximates with line segments,
2072
+ * subject to the float32 rounding of a Pt.
2073
+ * See a [demo here](https://ptsjs.org/demo/?name=curve.cardinal).
2074
+ *
2075
+ * Set `alpha` to 0.5 for centripetal or to 1 for chordal parameterization. At the default tension of 0.5,
2076
+ * centripetal Catmull-Rom segments with distinct adjacent anchors have no internal loops or cusps
2077
+ * (Yuksel, Schaefer and Keyser, 2011); changing tension can introduce them.
2078
+ * For non-uniform curves, coincident consecutive anchors give a constant segment and zero tangents at its ends.
2079
+ * @param pts a Group or an Iterable<PtLike> of anchor points
2080
+ * @param tension optional value between 0 to 1 to specify a "tension". Default to 0.5 which is the tension for Catmull-Rom curve.
2081
+ * @param alpha optional knot parameterization: 0 (default) is uniform, 0.5 is centripetal, 1 is chordal
2082
+ * @returns a Group of `3(n-1)+1` Pts in the layout that [`Curve.bezier`](#link) takes: each anchor is followed by the 2 control points of the segment that starts there
2083
+ * @example `form.bezier( Curve.cardinalToBezier( pts ) )`
2084
+ */
2085
+ static cardinalToBezier(
2086
+ pts: PtLikeIterable,
2087
+ tension: number = 0.5,
2088
+ alpha: number = 0,
2089
+ ): Group {
2090
+ const out = new Group();
2091
+ if (!(alpha >= 0) || alpha === Infinity) {
2092
+ return Util.warn(
2093
+ "cardinalToBezier needs a finite alpha of 0 or more",
2094
+ out,
2095
+ );
2096
+ }
2097
+ const p = Util.iterToArray(pts);
2098
+ const n = p.length;
2099
+ if (n < 2) return out;
2100
+ const dim3 = p[0].length > 2;
2101
+
2102
+ // Uniform curves need no knot array. Keep small nonzero distances: replacing them
2103
+ // with an absolute epsilon changes the curve when coordinates are scaled.
2104
+ const dt = alpha === 0 ? undefined : new Float64Array(n - 1);
2105
+ if (dt) {
2106
+ for (let i = 0; i < n - 1; i++) {
2107
+ const dx = p[i + 1][0] - p[i][0];
2108
+ const dy = p[i + 1][1] - p[i][1];
2109
+ const dz = dim3 ? p[i + 1][2] - p[i][2] : 0;
2110
+ const d = Math.pow(Math.hypot(dx, dy, dz), alpha);
2111
+ dt[i] = d < Infinity ? d : 0; // an overflowing interval acts like a repeated anchor
2112
+ }
2113
+ }
2114
+
2115
+ // Each segment is a cubic Hermite between two anchors, and a Hermite converts to a Bezier by
2116
+ // placing the control points a third of the way along the end tangents. The tangent at an
2117
+ // anchor comes from its two neighbors in the non-uniform Catmull-Rom form; an end anchor stands
2118
+ // in for its missing neighbor, which is the duplicated-endpoint convention of `Curve.cardinal`.
2119
+ const control = (
2120
+ o: PtLike,
2121
+ k: number,
2122
+ mx: number,
2123
+ my: number,
2124
+ mz: number,
2125
+ ) => {
2126
+ const pt = new Pt(dim3 ? 3 : 2);
2127
+ pt[0] = o[0] + k * mx;
2128
+ pt[1] = o[1] + k * my;
2129
+ if (dim3) pt[2] = o[2] + k * mz;
2130
+ return pt;
2131
+ };
2132
+
2133
+ for (let i = 0; i < n; i++) {
2134
+ const p0 = p[Math.max(i - 1, 0)];
2135
+ const p1 = p[i];
2136
+ const p2 = p[Math.min(i + 1, n - 1)];
2137
+ const a = dt ? dt[Math.max(i - 1, 0)] : 1;
2138
+ const b = dt ? dt[Math.min(i, n - 2)] : 1;
2139
+ // Weighted adjacent differences, with weights shared across coordinates.
2140
+ // A repeated anchor has zero tangent on both sides: constant segments stay
2141
+ // constant, and reversing the anchors applies the same degeneracy policy.
2142
+ let wa = 0;
2143
+ let wb = 0;
2144
+ if (dt && a > 0 && b > 0) {
2145
+ const s = (2 * tension) / (a + b);
2146
+ wa = s * (b / a);
2147
+ wb = s * (a / b);
2148
+ }
2149
+ const mx = dt
2150
+ ? wa * (p1[0] - p0[0]) + wb * (p2[0] - p1[0])
2151
+ : tension * (p2[0] - p0[0]);
2152
+ const my = dt
2153
+ ? wa * (p1[1] - p0[1]) + wb * (p2[1] - p1[1])
2154
+ : tension * (p2[1] - p0[1]);
2155
+ const mz = dim3
2156
+ ? dt
2157
+ ? wa * (p1[2] - p0[2]) + wb * (p2[2] - p1[2])
2158
+ : tension * (p2[2] - p0[2])
2159
+ : 0;
2160
+
2161
+ if (i > 0) out.push(control(p1, -a / 3, mx, my, mz));
2162
+ out.push(new Pt(p1));
2163
+ if (i < n - 1) out.push(control(p1, b / 3, mx, my, mz));
2164
+ }
2165
+ return out;
2166
+ }
2167
+
2168
+ /**
2169
+ * Convert a chain of cubic Bezier curves into the anchors of a Cardinal curve: the inverse of [`Curve.cardinalToBezier`](#link).
2170
+ * A Cardinal curve's tangents come from its neighboring anchors, so this keeps every Bezier anchor and drops the Bezier handles.
2171
+ * The Cardinal curve still passes through the same anchors; between them it follows the handles only when they were
2172
+ * in Cardinal form to begin with and the original tension and alpha are reused. Coordinates are rounded to float32.
2173
+ * @param pts a Group or an Iterable<PtLike> in the layout of [`Curve.bezier`](#link); an incomplete trailing segment is ignored
2174
+ * @returns a Group of anchors for [`Curve.cardinal`](#link) or [`Curve.cardinalToBezier`](#link), with the tension and alpha of your choice
2175
+ * @example `Curve.cardinal( Curve.bezierToCardinal( chain ), 10, 0.5 )`
2176
+ */
2177
+ static bezierToCardinal(pts: PtLikeIterable): Group {
2178
+ const p = Util.iterToArray(pts);
2179
+ const m = Math.floor((p.length - 1) / 3); // complete segments
2180
+ const out = new Group();
2181
+ if (m < 1) return out;
2182
+ for (let k = 0; k <= m; k++) out.push(new Pt(p[3 * k]));
2183
+ return out;
2184
+ }
2185
+
2186
+ /**
2187
+ * Create a Bezier curve. In a cubic bezier curve, the first and 4th anchors are end-points, and 2nd and 3rd anchors are control-points.
2188
+ * @param pts a group of anchor Pt
2189
+ * @param steps the number of line segments per curve. Defaults to 10 steps.
2190
+ * @returns a curve as a group of interpolated Pt
2191
+ */
2192
+ static bezier(pts: GroupLike, steps: number = 10) {
2193
+ let _pts = Util.iterToArray(pts);
2194
+ if (_pts.length < 4) return new Group();
2195
+
2196
+ let ps = new Group();
2197
+ const w = Curve._weights(steps, (t, out, o) => {
2198
+ const t2 = t * t;
2199
+ const t3 = t2 * t;
2200
+ out[o] = -t3 + 3 * t2 - 3 * t + 1;
2201
+ out[o + 1] = 3 * t3 - 6 * t2 + 3 * t;
2202
+ out[o + 2] = -3 * t3 + 3 * t2;
2203
+ out[o + 3] = t3;
2204
+ });
2205
+
2206
+ let k = 0;
2207
+ while (k < _pts.length - 3) {
2208
+ let c = Curve.controlPoints(_pts, k);
2209
+ if (c.length > 0) {
2210
+ Curve._evalSegment(ps, c, w, steps);
2211
+
2212
+ // go to the next set of point, but assume current end pt is next start pt
2213
+ k += 3;
2214
+ }
2215
+ }
2216
+
2217
+ return ps;
2218
+ }
2219
+
2220
+ /**
2221
+ * Interpolate to get a point on a cubic Bezier curve.
2222
+ * @param step the coefficients [t*t*t, t*t, t, 1]
2223
+ * @param ctrls a group of anchor Pts
2224
+ * @return an interpolated Pt on the curve
2225
+ */
2226
+ static bezierStep(step: Pt, ctrls: GroupLike) {
2227
+ // same coefficients as the batch `bezier` weight table
2228
+ const t3 = step[0];
2229
+ const t2 = step[1];
2230
+ const t = step[2];
2231
+ return Curve._stepPt(
2232
+ ctrls,
2233
+ -t3 + 3 * t2 - 3 * t + 1,
2234
+ 3 * t3 - 6 * t2 + 3 * t,
2235
+ -3 * t3 + 3 * t2,
2236
+ t3,
2237
+ );
2238
+ }
2239
+
2240
+ /**
2241
+ * Create a basis spline (NURBS) curve.
2242
+ * @param pts a group of anchor Pt
2243
+ * @param steps the number of line segments per curve. Defaults to 10 steps.
2244
+ * @param tension optional value between 0 to n to specify a "tension". Default is 1 which is the usual tension.
2245
+ * @returns a curve as a group of interpolated Pt
2246
+ */
2247
+ static bspline(
2248
+ pts: GroupLike,
2249
+ steps: number = 10,
2250
+ tension: number = 1,
2251
+ ): Group {
2252
+ let _pts = Util.iterToArray(pts);
2253
+ if (_pts.length < 2) return new Group();
2254
+
2255
+ let ps = new Group();
2256
+ const w =
2257
+ tension !== 1
2258
+ ? Curve._weights(steps, (t, out, o) => {
2259
+ const t2 = t * t;
2260
+ const t3 = t2 * t;
2261
+ const b1 = 2 * t3 - 3 * t2 + 1;
2262
+ const b2 = -2 * t3 + 3 * t2;
2263
+ out[o] = tension * (-t3 / 6 + 0.5 * t2 - 0.5 * t + 1 / 6);
2264
+ out[o + 1] = tension * (-1.5 * t3 + 2 * t2 - 1 / 3) + b1;
2265
+ out[o + 2] = tension * (1.5 * t3 - 2.5 * t2 + 0.5 * t + 1 / 6) + b2;
2266
+ out[o + 3] = tension * (t3 / 6);
2267
+ })
2268
+ : Curve._weights(steps, (t, out, o) => {
2269
+ const t2 = t * t;
2270
+ const t3 = t2 * t;
2271
+ out[o] = -t3 / 6 + 0.5 * t2 - 0.5 * t + 1 / 6;
2272
+ out[o + 1] = 0.5 * t3 - t2 + 2 / 3;
2273
+ out[o + 2] = -0.5 * t3 + 0.5 * t2 + 0.5 * t + 1 / 6;
2274
+ out[o + 3] = t3 / 6;
2275
+ });
2276
+
2277
+ let k = 0;
2278
+ while (k < _pts.length - 3) {
2279
+ let c = Curve.controlPoints(_pts, k);
2280
+ if (c.length > 0) {
2281
+ Curve._evalSegment(ps, c, w, steps);
2282
+ k++;
2283
+ }
2284
+ }
2285
+
2286
+ return ps;
2287
+ }
2288
+
2289
+ /**
2290
+ * Interpolate to get a point on a basis spline curve.
2291
+ * @param step the coefficients [t*t*t, t*t, t, 1]
2292
+ * @param ctrls a group of anchor Pts
2293
+ * @return an interpolated Pt on the curve
2294
+ */
2295
+ static bsplineStep(step: Pt, ctrls: GroupLike): Pt {
2296
+ // same coefficients as the batch `bspline` weight table
2297
+ const t3 = step[0];
2298
+ const t2 = step[1];
2299
+ const t = step[2];
2300
+ return Curve._stepPt(
2301
+ ctrls,
2302
+ -t3 / 6 + 0.5 * t2 - 0.5 * t + 1 / 6,
2303
+ 0.5 * t3 - t2 + 2 / 3,
2304
+ -0.5 * t3 + 0.5 * t2 + 0.5 * t + 1 / 6,
2305
+ t3 / 6,
2306
+ );
2307
+ }
2308
+
2309
+ /**
2310
+ * Interpolate to get a point on a basis spline curve with tension.
2311
+ * @param step the coefficients [t*t*t, t*t, t, 1]
2312
+ * @param ctrls a group of anchor Pts
2313
+ * @param tension optional value between 0 to n to specify a "tension". Default to 1 which is the usual tension.
2314
+ * @return an interpolated Pt on the curve
2315
+ */
2316
+ static bsplineTensionStep(
2317
+ step: Pt,
2318
+ ctrls: GroupLike,
2319
+ tension: number = 1,
2320
+ ): Pt {
2321
+ // same coefficients as the batch `bspline` tension weight table
2322
+ const t3 = step[0];
2323
+ const t2 = step[1];
2324
+ const t = step[2];
2325
+ const b1 = 2 * t3 - 3 * t2 + 1;
2326
+ const b2 = -2 * t3 + 3 * t2;
2327
+ return Curve._stepPt(
2328
+ ctrls,
2329
+ tension * (-t3 / 6 + 0.5 * t2 - 0.5 * t + 1 / 6),
2330
+ tension * (-1.5 * t3 + 2 * t2 - 1 / 3) + b1,
2331
+ tension * (1.5 * t3 - 2.5 * t2 + 0.5 * t + 1 / 6) + b2,
2332
+ tension * (t3 / 6),
2333
+ );
2334
+ }
2335
+
2336
+ /**
2337
+ * Convert the anchors of a B-spline curve into cubic Bezier control points, so the same curve can be drawn as a native path
2338
+ * with [`CanvasForm.bezier`](#link) or sampled with [`Curve.bezier`](#link).
2339
+ * The Bezier traces the curve that [`Curve.bspline`](#link) approximates with line segments,
2340
+ * subject to the float32 rounding of a Pt. See a [demo here](https://ptsjs.org/demo/?name=curve.bspline).
2341
+ * @param pts a Group or an Iterable<PtLike> of at least 4 anchor points
2342
+ * @param tension optional value between 0 to n to specify a "tension". Default is 1 which is the usual tension.
2343
+ * @returns a Group of `3(n-3)+1` Pts in the layout that [`Curve.bezier`](#link) takes
2344
+ * @example `form.bezier( Curve.bsplineToBezier( pts ) )`
2345
+ */
2346
+ static bsplineToBezier(pts: PtLikeIterable, tension: number = 1): Group {
2347
+ const p = Util.iterToArray(pts);
2348
+ const n = p.length;
2349
+ const out = new Group();
2350
+ if (n < 4) return out;
2351
+ const dim3 = p[0].length > 2;
2352
+
2353
+ // A segment spans anchors p1..p2 with neighbors p0 and p3. Its end points blend three
2354
+ // anchors (a, 1-2a, a) and its control points blend p1 and p2 only; at tension 1 these
2355
+ // are the (1, 4, 1)/6 and (2, 1)/3 weights of Böhm's knot insertion.
2356
+ const a = tension / 6;
2357
+ const c = 1 - 2 * a;
2358
+ const blend = (
2359
+ u: PtLike,
2360
+ v: PtLike,
2361
+ w: PtLike,
2362
+ wu: number,
2363
+ wv: number,
2364
+ ww: number,
2365
+ ) => {
2366
+ const pt = new Pt(dim3 ? 3 : 2);
2367
+ pt[0] = wu * u[0] + wv * v[0] + ww * w[0];
2368
+ pt[1] = wu * u[1] + wv * v[1] + ww * w[1];
2369
+ if (dim3) pt[2] = wu * u[2] + wv * v[2] + ww * w[2];
2370
+ return pt;
2371
+ };
2372
+
2373
+ out.push(blend(p[0], p[1], p[2], a, c, a));
2374
+ for (let i = 1; i < n - 2; i++) {
2375
+ const p1 = p[i];
2376
+ const p2 = p[i + 1];
2377
+ out.push(
2378
+ blend(p1, p2, p2, c, 2 * a, 0),
2379
+ blend(p1, p2, p2, 2 * a, c, 0),
2380
+ blend(p1, p2, p[i + 2], a, c, a),
2381
+ );
2382
+ }
2383
+ return out;
2384
+ }
2385
+
2386
+ /**
2387
+ * Convert a chain of cubic Bezier curves into the anchors of a B-spline curve: the inverse of [`Curve.bsplineToBezier`](#link) at its default tension.
2388
+ * A B-spline does not pass through its anchors, so they are solved for: the result is the B-spline that passes through
2389
+ * every Bezier anchor and starts and ends along the Bezier's end handles (a tridiagonal system, solved in linear time).
2390
+ * For a chain that `bsplineToBezier` produced at tension 1 this recovers its anchors up to float32 rounding; for any other chain the B-spline keeps
2391
+ * the anchors and the end tangents, and its interior, being smooth to the second derivative, can only approximate the other handles.
2392
+ * @param pts a Group or an Iterable<PtLike> in the layout of [`Curve.bezier`](#link); an incomplete trailing segment is ignored
2393
+ * @returns a Group of `m+3` anchors for `m` Bezier segments, for [`Curve.bspline`](#link) or [`Curve.bsplineToBezier`](#link)
2394
+ * @example `Curve.bspline( Curve.bezierToBspline( chain ) )`
2395
+ */
2396
+ static bezierToBspline(pts: PtLikeIterable): Group {
2397
+ const p = Util.iterToArray(pts);
2398
+ const m = Math.floor((p.length - 1) / 3); // complete segments
2399
+ const out = new Group();
2400
+ if (m < 1) return out;
2401
+ const dim = p[0].length > 2 ? 3 : 2;
2402
+
2403
+ // Unknowns are the anchors P1..P(m+1). Each Bezier anchor gives one row of (1, 4, 1)·P = 6·A,
2404
+ // and the two end rows fold in the end tangents, which also fix P0 and P(m+2) afterwards.
2405
+ // The right-hand sides then simplify to 3× the first handle, 6× each interior anchor, and 3× the last handle.
2406
+ const n = m + 1;
2407
+ const r = new Float64Array(n * dim);
2408
+ const d = new Float64Array(n); // pivots of the tridiagonal (2, 4, ..., 4, 2) with unit off-diagonals
2409
+ for (let k = 0; k < n; k++) {
2410
+ const src = k === 0 ? p[1] : k === m ? p[3 * m - 1] : p[3 * k];
2411
+ const w = k === 0 || k === m ? 3 : 6;
2412
+ for (let j = 0; j < dim; j++) r[k * dim + j] = w * src[j];
2413
+ }
2414
+ d[0] = 2;
2415
+ for (let k = 1; k < n; k++) {
2416
+ const w = 1 / d[k - 1];
2417
+ d[k] = (k < m ? 4 : 2) - w;
2418
+ for (let j = 0; j < dim; j++) r[k * dim + j] -= w * r[(k - 1) * dim + j];
2419
+ }
2420
+ for (let j = 0; j < dim; j++) r[(n - 1) * dim + j] /= d[n - 1];
2421
+ for (let k = n - 2; k >= 0; k--) {
2422
+ for (let j = 0; j < dim; j++) {
2423
+ r[k * dim + j] = (r[k * dim + j] - r[(k + 1) * dim + j]) / d[k];
2424
+ }
2425
+ }
2426
+
2427
+ const first = new Pt(dim);
2428
+ const last = new Pt(dim);
2429
+ for (let j = 0; j < dim; j++) {
2430
+ first[j] = r[dim + j] - 6 * (p[1][j] - p[0][j]);
2431
+ last[j] = r[(m - 1) * dim + j] + 6 * (p[3 * m][j] - p[3 * m - 1][j]);
2432
+ }
2433
+ out.push(first);
2434
+ for (let k = 0; k < n; k++) {
2435
+ const pt = new Pt(dim);
2436
+ for (let j = 0; j < dim; j++) pt[j] = r[k * dim + j];
2437
+ out.push(pt);
2438
+ }
2439
+ out.push(last);
2440
+ return out;
2441
+ }
2442
+ }