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/README.md +92 -80
- package/dist/index.d.mts +6208 -1254
- package/dist/index.d.mts.map +1 -0
- package/dist/index.d.ts +6208 -1254
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +9314 -10680
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +9266 -10611
- package/dist/index.mjs.map +1 -0
- package/dist/pts.js +9446 -10777
- package/dist/pts.js.map +1 -0
- package/dist/pts.min.js +3 -5
- package/dist/pts.min.js.map +1 -0
- package/package.json +91 -31
- package/src/Canvas.ts +1642 -0
- package/src/Color.ts +1109 -0
- package/src/Create.ts +1547 -0
- package/src/Dom.ts +940 -0
- package/src/Form.ts +312 -0
- package/src/Image.ts +722 -0
- package/src/LinearAlgebra.ts +530 -0
- package/src/Num.ts +1091 -0
- package/src/Op.ts +2127 -0
- package/src/Physics.ts +1233 -0
- package/src/Play.ts +861 -0
- package/src/Pt.ts +1303 -0
- package/src/Space.ts +898 -0
- package/src/Svg.ts +1573 -0
- package/src/Types.ts +301 -0
- package/src/Typography.ts +228 -0
- package/src/UI.ts +757 -0
- package/src/Util.ts +454 -0
- package/src/_module.ts +18 -0
- package/src/_script.ts +94 -0
- package/src/_triangulate.ts +884 -0
- package/src/uheprng.ts +153 -0
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
|
+
}
|