pts 0.12.8 → 1.0.0

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