@immugio/three-math-extensions 0.3.4 → 0.3.6
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/CHANGELOG.md +17 -1
- package/cjs/Line2D.js +90 -13
- package/docs/classes/BoundingBox.md +121 -121
- package/docs/classes/Line2D.md +1366 -1366
- package/docs/classes/Line3D.md +831 -831
- package/docs/classes/Polygon.md +297 -297
- package/docs/classes/Rectangle.md +291 -291
- package/docs/classes/Size2.md +55 -55
- package/docs/classes/Vec2.md +282 -282
- package/docs/classes/Vec3.md +338 -338
- package/docs/interfaces/Point2.md +30 -30
- package/docs/interfaces/Point3.md +41 -41
- package/docs/modules.md +209 -209
- package/eslint.config.mjs +111 -111
- package/esm/Line2D.js +90 -13
- package/package.json +62 -62
- package/src/BoundingBox.ts +13 -13
- package/src/Line2D.ts +951 -857
- package/src/Line3D.ts +586 -586
- package/src/MathConstants.ts +1 -1
- package/src/Point2.ts +3 -3
- package/src/Point3.ts +4 -4
- package/src/Polygon.ts +286 -286
- package/src/Rectangle.ts +92 -92
- package/src/Size2.ts +3 -3
- package/src/Vec2.ts +124 -124
- package/src/Vec3.ts +167 -167
- package/src/containsPoint.ts +65 -65
- package/src/directions.ts +9 -9
- package/src/directions2d.ts +7 -7
- package/src/ensurePolygonClockwise.ts +9 -9
- package/src/extendOrTrimPolylinesAtIntersections.ts +10 -10
- package/src/getPolygonArea.ts +21 -21
- package/src/index.ts +24 -24
- package/src/isContinuousClosedShape.ts +24 -24
- package/src/isPointInPolygon.ts +23 -23
- package/src/isPolygonClockwise.ts +15 -15
- package/src/normalizeAngleDegrees.ts +6 -6
- package/src/normalizeAngleRadians.ts +14 -14
- package/src/offsetPolyline.ts +26 -26
- package/src/polygonPerimeter.ts +13 -13
- package/src/sortLinesByConnections.ts +45 -45
- package/types/Line2D.d.ts +12 -5
package/src/Line2D.ts
CHANGED
|
@@ -1,858 +1,952 @@
|
|
|
1
|
-
|
|
2
|
-
import { MathUtils, Vector2 } from "three";
|
|
3
|
-
import { Vec2 } from "./Vec2";
|
|
4
|
-
import { TwoPI } from "./MathConstants";
|
|
5
|
-
import { Line3D } from "./Line3D";
|
|
6
|
-
import { directions2d } from "./directions2d";
|
|
7
|
-
import { sortLinesByConnections } from "./sortLinesByConnections";
|
|
8
|
-
|
|
9
|
-
const _startP = /*@__PURE__*/ new Vec2();
|
|
10
|
-
const _startEnd = /*@__PURE__*/ new Vec2();
|
|
11
|
-
|
|
12
|
-
export class Line2D {
|
|
13
|
-
|
|
14
|
-
readonly #target: Vec2 = new Vec2();
|
|
15
|
-
|
|
16
|
-
constructor(public start: Vec2, public end: Vec2, public index: number = 0) {
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
public static fromCoordinates(x1: number, y1: number, x2: number, y2: number, index: number = 0): Line2D {
|
|
20
|
-
return new Line2D(new Vec2(x1, y1), new Vec2(x2, y2), index);
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
public static fromPoints(p1: Point2, p2: Point2, index: number = 0): Line2D {
|
|
24
|
-
return new Line2D(new Vec2(p1.x, p1.y), new Vec2(p2.x, p2.y), index);
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
/**
|
|
28
|
-
* Creates a polygon formed by an array of lines from points provided.
|
|
29
|
-
* The polygon will only be closed if either
|
|
30
|
-
* 1) the first and last points are the same or 2) `forceClosedPolygon` is true.
|
|
31
|
-
*/
|
|
32
|
-
public static fromPolygon(polygon: Point2[], forceClosedPolygon: boolean = false): Line2D[] {
|
|
33
|
-
if (!polygon || polygon.length < 2) {
|
|
34
|
-
return [];
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
if (forceClosedPolygon && (polygon[0].x !== polygon.at(-1).x || polygon[0].y !== polygon.at(-1).y)) {
|
|
38
|
-
polygon = [...polygon, polygon[0]];
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
const lines: Line2D[] = [];
|
|
42
|
-
for (let i = 0; i < polygon.length - 1; i++) {
|
|
43
|
-
lines.push(Line2D.fromPoints(polygon[i], polygon[i + 1], i));
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
return lines;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
public static fromLength(length: number): Line2D {
|
|
50
|
-
return Line2D.fromCoordinates(-length / 2, 0, length / 2, 0);
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
public get center(): Vec2 {
|
|
54
|
-
return new Vec2((this.start.x + this.end.x) / 2, (this.start.y + this.end.y) / 2);
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
/**
|
|
58
|
-
* Set the center of the line to the provided point. Length and direction remain unchanged.
|
|
59
|
-
* Modifies this line.
|
|
60
|
-
* @param value
|
|
61
|
-
*/
|
|
62
|
-
public set center(value: Point2) {
|
|
63
|
-
const current = this.center;
|
|
64
|
-
const diffX = current.x - value.x;
|
|
65
|
-
const diffY = current.y - value.y;
|
|
66
|
-
this.start.x -= diffX;
|
|
67
|
-
this.start.y -= diffY;
|
|
68
|
-
this.end.x -= diffX;
|
|
69
|
-
this.end.y -= diffY;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
/**
|
|
73
|
-
* Set the center of the line to the provided point. Length and direction remain unchanged.
|
|
74
|
-
* Modifies this line.
|
|
75
|
-
* @param value
|
|
76
|
-
*/
|
|
77
|
-
public setCenter(value: Point2): this {
|
|
78
|
-
this.center = value;
|
|
79
|
-
return this;
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
/*
|
|
83
|
-
* Extends or reduces the line by the given length while keeping the center of the line constant.
|
|
84
|
-
* Modifies this line.
|
|
85
|
-
*/
|
|
86
|
-
public resize(amount: number): this {
|
|
87
|
-
this.moveStartPoint(amount / 2);
|
|
88
|
-
this.moveEndPoint(amount / 2);
|
|
89
|
-
return this;
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
public isParallelTo(other: Line2D, angleTolerance: number = Number.EPSILON): boolean {
|
|
93
|
-
const direction = this.direction;
|
|
94
|
-
const otherDirection = other.direction;
|
|
95
|
-
|
|
96
|
-
const angle = direction.angleTo(otherDirection);
|
|
97
|
-
if (angle <= angleTolerance) {
|
|
98
|
-
return true;
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
const opposite = direction.angleTo(otherDirection.negate());
|
|
102
|
-
return opposite <= angleTolerance;
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
/*
|
|
106
|
-
* Moves start point on the line by the given amount. Plus values move the point further away from the center.
|
|
107
|
-
* Modifies this line.
|
|
108
|
-
*/
|
|
109
|
-
public moveStartPoint(amount: number): this {
|
|
110
|
-
this.start.add(this.direction.multiplyScalar(-amount));
|
|
111
|
-
return this;
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
/**
|
|
115
|
-
* Moves end point on the line by the given amount. Plus values move the point further away from the center.
|
|
116
|
-
* Modifies this line.
|
|
117
|
-
*/
|
|
118
|
-
public moveEndPoint(amount: number): this {
|
|
119
|
-
this.end.add(this.direction.multiplyScalar(amount));
|
|
120
|
-
return this;
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
/**
|
|
124
|
-
* Set the length of this line. Center and direction remain unchanged.
|
|
125
|
-
* Modifies this line.
|
|
126
|
-
* @param l
|
|
127
|
-
*/
|
|
128
|
-
public set length(l: number) {
|
|
129
|
-
const length = this.length;
|
|
130
|
-
this.resize(l - length);
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
public get length(): number {
|
|
134
|
-
return this.start.distanceTo(this.end);
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
/**
|
|
138
|
-
* Set the length of this line. Center and direction remain unchanged.
|
|
139
|
-
* @param length
|
|
140
|
-
*/
|
|
141
|
-
public setLength(length: number): this {
|
|
142
|
-
this.length = length;
|
|
143
|
-
return this;
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
/**
|
|
147
|
-
* Returns the start and end points of the line as an array.
|
|
148
|
-
* Endpoints are not cloned.
|
|
149
|
-
*/
|
|
150
|
-
public get endpoints(): Vec2[] {
|
|
151
|
-
return [this.start, this.end];
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
/**
|
|
155
|
-
* Check that this line segment contains provided point.
|
|
156
|
-
* @param p
|
|
157
|
-
* @param tolerance
|
|
158
|
-
*/
|
|
159
|
-
public containsPoint(p: Vector2, tolerance: number = 0): boolean {
|
|
160
|
-
const closestPointToPoint = this.closestPointToPoint(p, true, this.#target);
|
|
161
|
-
return closestPointToPoint.distanceTo(p) <= tolerance;
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
/**
|
|
165
|
-
* Distance from this line to the provided point.
|
|
166
|
-
* @param param
|
|
167
|
-
* @param clampToLine
|
|
168
|
-
*/
|
|
169
|
-
public distanceToPoint(p: Vector2, clampToLine: boolean = true): number {
|
|
170
|
-
const closestPointToPoint = this.closestPointToPoint(p, clampToLine, this.#target);
|
|
171
|
-
return closestPointToPoint.distanceTo(p);
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
/**
|
|
175
|
-
* Returns a copy of @other line, the direction of @other is reversed if needed.
|
|
176
|
-
* Returns null if lines are not parallel.
|
|
177
|
-
* @param other
|
|
178
|
-
* @param parallelTolerance
|
|
179
|
-
*/
|
|
180
|
-
public getParallelLineInTheSameDirection(other: Line2D, parallelTolerance: number = 0): Line2D {
|
|
181
|
-
const direction = this.direction;
|
|
182
|
-
|
|
183
|
-
if (direction.angleTo(other.direction) <= parallelTolerance) {
|
|
184
|
-
return other.clone();
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
const otherLineOppositeDirection = other.clone().flip();
|
|
188
|
-
if (otherLineOppositeDirection.direction.angleTo(direction) <= parallelTolerance) {
|
|
189
|
-
return otherLineOppositeDirection;
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
return null;
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
/**
|
|
196
|
-
* Returns the direction of this line.
|
|
197
|
-
*/
|
|
198
|
-
public get direction(): Vec2 {
|
|
199
|
-
return this.end.clone().sub(this.start).normalize();
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
/**
|
|
203
|
-
* Inverts the direction of the line.
|
|
204
|
-
* Modifies this line.
|
|
205
|
-
*/
|
|
206
|
-
public flip(): this {
|
|
207
|
-
this.#target.copy(this.start);
|
|
208
|
-
this.start.copy(this.end);
|
|
209
|
-
this.end.copy(this.#target);
|
|
210
|
-
|
|
211
|
-
return this;
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
/**
|
|
215
|
-
* Rotates the line around the center by the given angle in radians.
|
|
216
|
-
* Modifies this line.
|
|
217
|
-
* @param radians Positive values rotate counter-clockwise.
|
|
218
|
-
* @param center
|
|
219
|
-
*/
|
|
220
|
-
public rotate(radians: number, center: Vec2 = this.center): this {
|
|
221
|
-
this.start.rotateAround(center, radians);
|
|
222
|
-
this.end.rotateAround(center, radians);
|
|
223
|
-
|
|
224
|
-
return this;
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
/**
|
|
228
|
-
* Move the line by the given vector.
|
|
229
|
-
* Modifies this line.
|
|
230
|
-
*/
|
|
231
|
-
public translate(value: Point2): this {
|
|
232
|
-
this.start.x += value.x;
|
|
233
|
-
this.start.y += value.y;
|
|
234
|
-
this.end.x += value.x;
|
|
235
|
-
this.end.y += value.y;
|
|
236
|
-
|
|
237
|
-
return this;
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
/**
|
|
241
|
-
* Move the line to its left by the given amount.
|
|
242
|
-
* Modifies this line.
|
|
243
|
-
*/
|
|
244
|
-
public translateLeft(amount: number): this {
|
|
245
|
-
const translation = this.direction.rotateAround(new Vec2(), -Math.PI / 2).multiplyScalar(amount);
|
|
246
|
-
return this.translate(translation);
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
/**
|
|
250
|
-
* Move the line to its right by the given amount.
|
|
251
|
-
* Modifies this line.
|
|
252
|
-
*/
|
|
253
|
-
public translateRight(amount: number): this {
|
|
254
|
-
const translation = this.direction.rotateAround(new Vec2(), Math.PI / 2).multiplyScalar(amount);
|
|
255
|
-
return this.translate(translation);
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
/**
|
|
259
|
-
* Returns true when the point is actually inside the (finite) line segment.
|
|
260
|
-
* https://jsfiddle.net/c06zdxtL/2/
|
|
261
|
-
* https://stackoverflow.com/questions/6865832/detecting-if-a-point-is-of-a-line-segment/6877674
|
|
262
|
-
* @param point: Point2
|
|
263
|
-
*/
|
|
264
|
-
public isPointOnLineSection(point: Point2): boolean {
|
|
265
|
-
if (!this.isPointOnInfiniteLine(point)) {
|
|
266
|
-
return false;
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
return this.isPointBesideLineSection(point);
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
/**
|
|
273
|
-
* Returns true when the point is beside the line **segment** and within the maxDistance.
|
|
274
|
-
* @param point
|
|
275
|
-
* @param maxDistance
|
|
276
|
-
*/
|
|
277
|
-
public isPointCloseToAndBesideLineSection(point: Point2, maxDistance: number): boolean {
|
|
278
|
-
const distance = this.distanceToPointOnInfiniteLine(point);
|
|
279
|
-
return distance <= maxDistance && this.isPointBesideLineSection(point);
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
/**
|
|
283
|
-
* Returns true when the point is beside the line **segment**
|
|
284
|
-
* @param point
|
|
285
|
-
*/
|
|
286
|
-
public isPointBesideLineSection(point: Point2): boolean {
|
|
287
|
-
const l2 = (((this.end.x - this.start.x) * (this.end.x - this.start.x)) + ((this.end.y - this.start.y) * (this.end.y - this.start.y)));
|
|
288
|
-
if (l2 === 0) return false;
|
|
289
|
-
const r = (((point.x - this.start.x) * (this.end.x - this.start.x)) + ((point.y - this.start.y) * (this.end.y - this.start.y))) / l2;
|
|
290
|
-
|
|
291
|
-
return (0 <= r) && (r <= 1);
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
/**
|
|
295
|
-
* Returns true when the point is on the **infinite** line.
|
|
296
|
-
* @param point
|
|
297
|
-
*/
|
|
298
|
-
public isPointOnInfiniteLine(point: Point2): boolean {
|
|
299
|
-
return (point.y - this.start.y) * (this.end.x - this.start.x) === (this.end.y - this.start.y) * (point.x - this.start.x);
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
/**
|
|
303
|
-
* Returns true if other line is collinear and overlaps or at least touching this line.
|
|
304
|
-
*
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
const
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
*
|
|
443
|
-
*
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
const
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
}
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
const
|
|
485
|
-
const
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
*
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
const
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
}
|
|
704
|
-
|
|
705
|
-
return
|
|
706
|
-
}
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
if (
|
|
716
|
-
|
|
717
|
-
}
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
*
|
|
780
|
-
* @param
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
}
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
(this.start.
|
|
825
|
-
(
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
1
|
+
import { Point2 } from "./Point2";
|
|
2
|
+
import { MathUtils, Vector2 } from "three";
|
|
3
|
+
import { Vec2 } from "./Vec2";
|
|
4
|
+
import { TwoPI } from "./MathConstants";
|
|
5
|
+
import { Line3D } from "./Line3D";
|
|
6
|
+
import { directions2d } from "./directions2d";
|
|
7
|
+
import { sortLinesByConnections } from "./sortLinesByConnections";
|
|
8
|
+
|
|
9
|
+
const _startP = /*@__PURE__*/ new Vec2();
|
|
10
|
+
const _startEnd = /*@__PURE__*/ new Vec2();
|
|
11
|
+
|
|
12
|
+
export class Line2D {
|
|
13
|
+
|
|
14
|
+
readonly #target: Vec2 = new Vec2();
|
|
15
|
+
|
|
16
|
+
constructor(public start: Vec2, public end: Vec2, public index: number = 0) {
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
public static fromCoordinates(x1: number, y1: number, x2: number, y2: number, index: number = 0): Line2D {
|
|
20
|
+
return new Line2D(new Vec2(x1, y1), new Vec2(x2, y2), index);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
public static fromPoints(p1: Point2, p2: Point2, index: number = 0): Line2D {
|
|
24
|
+
return new Line2D(new Vec2(p1.x, p1.y), new Vec2(p2.x, p2.y), index);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Creates a polygon formed by an array of lines from points provided.
|
|
29
|
+
* The polygon will only be closed if either
|
|
30
|
+
* 1) the first and last points are the same or 2) `forceClosedPolygon` is true.
|
|
31
|
+
*/
|
|
32
|
+
public static fromPolygon(polygon: Point2[], forceClosedPolygon: boolean = false): Line2D[] {
|
|
33
|
+
if (!polygon || polygon.length < 2) {
|
|
34
|
+
return [];
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (forceClosedPolygon && (polygon[0].x !== polygon.at(-1).x || polygon[0].y !== polygon.at(-1).y)) {
|
|
38
|
+
polygon = [...polygon, polygon[0]];
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const lines: Line2D[] = [];
|
|
42
|
+
for (let i = 0; i < polygon.length - 1; i++) {
|
|
43
|
+
lines.push(Line2D.fromPoints(polygon[i], polygon[i + 1], i));
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return lines;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
public static fromLength(length: number): Line2D {
|
|
50
|
+
return Line2D.fromCoordinates(-length / 2, 0, length / 2, 0);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
public get center(): Vec2 {
|
|
54
|
+
return new Vec2((this.start.x + this.end.x) / 2, (this.start.y + this.end.y) / 2);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Set the center of the line to the provided point. Length and direction remain unchanged.
|
|
59
|
+
* Modifies this line.
|
|
60
|
+
* @param value
|
|
61
|
+
*/
|
|
62
|
+
public set center(value: Point2) {
|
|
63
|
+
const current = this.center;
|
|
64
|
+
const diffX = current.x - value.x;
|
|
65
|
+
const diffY = current.y - value.y;
|
|
66
|
+
this.start.x -= diffX;
|
|
67
|
+
this.start.y -= diffY;
|
|
68
|
+
this.end.x -= diffX;
|
|
69
|
+
this.end.y -= diffY;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Set the center of the line to the provided point. Length and direction remain unchanged.
|
|
74
|
+
* Modifies this line.
|
|
75
|
+
* @param value
|
|
76
|
+
*/
|
|
77
|
+
public setCenter(value: Point2): this {
|
|
78
|
+
this.center = value;
|
|
79
|
+
return this;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/*
|
|
83
|
+
* Extends or reduces the line by the given length while keeping the center of the line constant.
|
|
84
|
+
* Modifies this line.
|
|
85
|
+
*/
|
|
86
|
+
public resize(amount: number): this {
|
|
87
|
+
this.moveStartPoint(amount / 2);
|
|
88
|
+
this.moveEndPoint(amount / 2);
|
|
89
|
+
return this;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
public isParallelTo(other: Line2D, angleTolerance: number = Number.EPSILON): boolean {
|
|
93
|
+
const direction = this.direction;
|
|
94
|
+
const otherDirection = other.direction;
|
|
95
|
+
|
|
96
|
+
const angle = direction.angleTo(otherDirection);
|
|
97
|
+
if (angle <= angleTolerance) {
|
|
98
|
+
return true;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const opposite = direction.angleTo(otherDirection.negate());
|
|
102
|
+
return opposite <= angleTolerance;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/*
|
|
106
|
+
* Moves start point on the line by the given amount. Plus values move the point further away from the center.
|
|
107
|
+
* Modifies this line.
|
|
108
|
+
*/
|
|
109
|
+
public moveStartPoint(amount: number): this {
|
|
110
|
+
this.start.add(this.direction.multiplyScalar(-amount));
|
|
111
|
+
return this;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Moves end point on the line by the given amount. Plus values move the point further away from the center.
|
|
116
|
+
* Modifies this line.
|
|
117
|
+
*/
|
|
118
|
+
public moveEndPoint(amount: number): this {
|
|
119
|
+
this.end.add(this.direction.multiplyScalar(amount));
|
|
120
|
+
return this;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Set the length of this line. Center and direction remain unchanged.
|
|
125
|
+
* Modifies this line.
|
|
126
|
+
* @param l
|
|
127
|
+
*/
|
|
128
|
+
public set length(l: number) {
|
|
129
|
+
const length = this.length;
|
|
130
|
+
this.resize(l - length);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
public get length(): number {
|
|
134
|
+
return this.start.distanceTo(this.end);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Set the length of this line. Center and direction remain unchanged.
|
|
139
|
+
* @param length
|
|
140
|
+
*/
|
|
141
|
+
public setLength(length: number): this {
|
|
142
|
+
this.length = length;
|
|
143
|
+
return this;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Returns the start and end points of the line as an array.
|
|
148
|
+
* Endpoints are not cloned.
|
|
149
|
+
*/
|
|
150
|
+
public get endpoints(): Vec2[] {
|
|
151
|
+
return [this.start, this.end];
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Check that this line segment contains provided point.
|
|
156
|
+
* @param p
|
|
157
|
+
* @param tolerance
|
|
158
|
+
*/
|
|
159
|
+
public containsPoint(p: Vector2, tolerance: number = 0): boolean {
|
|
160
|
+
const closestPointToPoint = this.closestPointToPoint(p, true, this.#target);
|
|
161
|
+
return closestPointToPoint.distanceTo(p) <= tolerance;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Distance from this line to the provided point.
|
|
166
|
+
* @param param
|
|
167
|
+
* @param clampToLine
|
|
168
|
+
*/
|
|
169
|
+
public distanceToPoint(p: Vector2, clampToLine: boolean = true): number {
|
|
170
|
+
const closestPointToPoint = this.closestPointToPoint(p, clampToLine, this.#target);
|
|
171
|
+
return closestPointToPoint.distanceTo(p);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Returns a copy of @other line, the direction of @other is reversed if needed.
|
|
176
|
+
* Returns null if lines are not parallel.
|
|
177
|
+
* @param other
|
|
178
|
+
* @param parallelTolerance
|
|
179
|
+
*/
|
|
180
|
+
public getParallelLineInTheSameDirection(other: Line2D, parallelTolerance: number = 0): Line2D {
|
|
181
|
+
const direction = this.direction;
|
|
182
|
+
|
|
183
|
+
if (direction.angleTo(other.direction) <= parallelTolerance) {
|
|
184
|
+
return other.clone();
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const otherLineOppositeDirection = other.clone().flip();
|
|
188
|
+
if (otherLineOppositeDirection.direction.angleTo(direction) <= parallelTolerance) {
|
|
189
|
+
return otherLineOppositeDirection;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
return null;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Returns the direction of this line.
|
|
197
|
+
*/
|
|
198
|
+
public get direction(): Vec2 {
|
|
199
|
+
return this.end.clone().sub(this.start).normalize();
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Inverts the direction of the line.
|
|
204
|
+
* Modifies this line.
|
|
205
|
+
*/
|
|
206
|
+
public flip(): this {
|
|
207
|
+
this.#target.copy(this.start);
|
|
208
|
+
this.start.copy(this.end);
|
|
209
|
+
this.end.copy(this.#target);
|
|
210
|
+
|
|
211
|
+
return this;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Rotates the line around the center by the given angle in radians.
|
|
216
|
+
* Modifies this line.
|
|
217
|
+
* @param radians Positive values rotate counter-clockwise.
|
|
218
|
+
* @param center
|
|
219
|
+
*/
|
|
220
|
+
public rotate(radians: number, center: Vec2 = this.center): this {
|
|
221
|
+
this.start.rotateAround(center, radians);
|
|
222
|
+
this.end.rotateAround(center, radians);
|
|
223
|
+
|
|
224
|
+
return this;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Move the line by the given vector.
|
|
229
|
+
* Modifies this line.
|
|
230
|
+
*/
|
|
231
|
+
public translate(value: Point2): this {
|
|
232
|
+
this.start.x += value.x;
|
|
233
|
+
this.start.y += value.y;
|
|
234
|
+
this.end.x += value.x;
|
|
235
|
+
this.end.y += value.y;
|
|
236
|
+
|
|
237
|
+
return this;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Move the line to its left by the given amount.
|
|
242
|
+
* Modifies this line.
|
|
243
|
+
*/
|
|
244
|
+
public translateLeft(amount: number): this {
|
|
245
|
+
const translation = this.direction.rotateAround(new Vec2(), -Math.PI / 2).multiplyScalar(amount);
|
|
246
|
+
return this.translate(translation);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Move the line to its right by the given amount.
|
|
251
|
+
* Modifies this line.
|
|
252
|
+
*/
|
|
253
|
+
public translateRight(amount: number): this {
|
|
254
|
+
const translation = this.direction.rotateAround(new Vec2(), Math.PI / 2).multiplyScalar(amount);
|
|
255
|
+
return this.translate(translation);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Returns true when the point is actually inside the (finite) line segment.
|
|
260
|
+
* https://jsfiddle.net/c06zdxtL/2/
|
|
261
|
+
* https://stackoverflow.com/questions/6865832/detecting-if-a-point-is-of-a-line-segment/6877674
|
|
262
|
+
* @param point: Point2
|
|
263
|
+
*/
|
|
264
|
+
public isPointOnLineSection(point: Point2): boolean {
|
|
265
|
+
if (!this.isPointOnInfiniteLine(point)) {
|
|
266
|
+
return false;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
return this.isPointBesideLineSection(point);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* Returns true when the point is beside the line **segment** and within the maxDistance.
|
|
274
|
+
* @param point
|
|
275
|
+
* @param maxDistance
|
|
276
|
+
*/
|
|
277
|
+
public isPointCloseToAndBesideLineSection(point: Point2, maxDistance: number): boolean {
|
|
278
|
+
const distance = this.distanceToPointOnInfiniteLine(point);
|
|
279
|
+
return distance <= maxDistance && this.isPointBesideLineSection(point);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* Returns true when the point is beside the line **segment**
|
|
284
|
+
* @param point
|
|
285
|
+
*/
|
|
286
|
+
public isPointBesideLineSection(point: Point2): boolean {
|
|
287
|
+
const l2 = (((this.end.x - this.start.x) * (this.end.x - this.start.x)) + ((this.end.y - this.start.y) * (this.end.y - this.start.y)));
|
|
288
|
+
if (l2 === 0) return false;
|
|
289
|
+
const r = (((point.x - this.start.x) * (this.end.x - this.start.x)) + ((point.y - this.start.y) * (this.end.y - this.start.y))) / l2;
|
|
290
|
+
|
|
291
|
+
return (0 <= r) && (r <= 1);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* Returns true when the point is on the **infinite** line.
|
|
296
|
+
* @param point
|
|
297
|
+
*/
|
|
298
|
+
public isPointOnInfiniteLine(point: Point2): boolean {
|
|
299
|
+
return (point.y - this.start.y) * (this.end.x - this.start.x) === (this.end.y - this.start.y) * (point.x - this.start.x);
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* Returns true if other line is collinear and overlaps or at least touching this line.
|
|
304
|
+
* Uses tolerances to allow for small gaps and angle differences.
|
|
305
|
+
* @param other
|
|
306
|
+
* @param distanceTolerance Maximum distance between lines or points to be considered touching/overlapping
|
|
307
|
+
* @param parallelTolerance Maximum angle difference in radians for lines to be considered parallel
|
|
308
|
+
*/
|
|
309
|
+
public isCollinearWithTouchOrOverlap(other: Line2D, distanceTolerance: number = 0, parallelTolerance: number = 0): boolean {
|
|
310
|
+
// Exact logic (no tolerances)
|
|
311
|
+
if (!distanceTolerance && !parallelTolerance) {
|
|
312
|
+
if (!this.isPointOnInfiniteLine(other.start) || !this.isPointOnInfiniteLine(other.end)) {
|
|
313
|
+
return false;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
return this.isPointOnLineSection(other.start) || this.isPointOnLineSection(other.end) ||
|
|
317
|
+
other.isPointOnLineSection(this.start) || other.isPointOnLineSection(this.end);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// If tolerances are provided, use tolerance-aware logic
|
|
321
|
+
// Check if lines are parallel
|
|
322
|
+
if (!this.isParallelTo(other, parallelTolerance)) {
|
|
323
|
+
return false;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// Check if points are close enough to the infinite line
|
|
327
|
+
const otherStartDistance = this.distanceToPointOnInfiniteLine(other.start);
|
|
328
|
+
const otherEndDistance = this.distanceToPointOnInfiniteLine(other.end);
|
|
329
|
+
const thisStartDistance = other.distanceToPointOnInfiniteLine(this.start);
|
|
330
|
+
const thisEndDistance = other.distanceToPointOnInfiniteLine(this.end);
|
|
331
|
+
|
|
332
|
+
if (otherStartDistance > distanceTolerance || otherEndDistance > distanceTolerance ||
|
|
333
|
+
thisStartDistance > distanceTolerance || thisEndDistance > distanceTolerance) {
|
|
334
|
+
return false;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// Check if any endpoint is close to and beside the other line section
|
|
338
|
+
// OR if the lines are close enough to each other (for gap handling)
|
|
339
|
+
const hasOverlap = this.isPointCloseToAndBesideLineSection(other.start, distanceTolerance) ||
|
|
340
|
+
this.isPointCloseToAndBesideLineSection(other.end, distanceTolerance) ||
|
|
341
|
+
other.isPointCloseToAndBesideLineSection(this.start, distanceTolerance) ||
|
|
342
|
+
other.isPointCloseToAndBesideLineSection(this.end, distanceTolerance);
|
|
343
|
+
|
|
344
|
+
if (hasOverlap) {
|
|
345
|
+
return true;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
// Check if lines are close enough to bridge a gap
|
|
349
|
+
// Project all endpoints onto the line's direction and check if projections are within tolerance
|
|
350
|
+
const direction = this.direction;
|
|
351
|
+
const startPoint = this.start;
|
|
352
|
+
|
|
353
|
+
// Project all points onto the line's direction vector
|
|
354
|
+
const projectPoint = (p: Vec2): number => {
|
|
355
|
+
const toPoint = new Vec2().subVectors(p, startPoint);
|
|
356
|
+
return toPoint.dot(direction);
|
|
357
|
+
};
|
|
358
|
+
|
|
359
|
+
const thisStartProj = projectPoint(this.start);
|
|
360
|
+
const thisEndProj = projectPoint(this.end);
|
|
361
|
+
const otherStartProj = projectPoint(other.start);
|
|
362
|
+
const otherEndProj = projectPoint(other.end);
|
|
363
|
+
|
|
364
|
+
const minThis = Math.min(thisStartProj, thisEndProj);
|
|
365
|
+
const maxThis = Math.max(thisStartProj, thisEndProj);
|
|
366
|
+
const minOther = Math.min(otherStartProj, otherEndProj);
|
|
367
|
+
const maxOther = Math.max(otherStartProj, otherEndProj);
|
|
368
|
+
|
|
369
|
+
// Check if projections overlap or are within tolerance
|
|
370
|
+
const gap = Math.max(minThis, minOther) - Math.min(maxThis, maxOther);
|
|
371
|
+
return gap <= distanceTolerance;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
/**
|
|
375
|
+
* Returns true if there is any overlap between this line and the @other line section.
|
|
376
|
+
*/
|
|
377
|
+
public overlaps(other: Line2D, distanceTolerance: number = 0, parallelTolerance: number = 0): boolean {
|
|
378
|
+
// Special case
|
|
379
|
+
if (this.equals(other, distanceTolerance)) {
|
|
380
|
+
return true;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
// Always have to be parallel
|
|
384
|
+
if (this.isParallelTo(other, parallelTolerance)) {
|
|
385
|
+
// To pass as overlapping, at least one point has to be within the other line, but they mush not be equal - "touching" is not considered overlapping
|
|
386
|
+
|
|
387
|
+
const otherStartEqualsToAnyOfThisPoint = other.start.distanceTo(this.start) <= distanceTolerance || other.start.distanceTo(this.end) <= distanceTolerance;
|
|
388
|
+
if (this.containsPoint(other.start, distanceTolerance) && !otherStartEqualsToAnyOfThisPoint) {
|
|
389
|
+
return true;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
const otherEndEqualsToAnyOfThisPoint = other.end.distanceTo(this.start) <= distanceTolerance || other.end.distanceTo(this.end) <= distanceTolerance;
|
|
393
|
+
if (this.containsPoint(other.end, distanceTolerance) && !otherEndEqualsToAnyOfThisPoint) {
|
|
394
|
+
return true;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
const thisStartEqualsToAnyOfOtherPoint = this.start.distanceTo(other.start) <= distanceTolerance || this.start.distanceTo(other.end) <= distanceTolerance;
|
|
398
|
+
if (other.containsPoint(this.start, distanceTolerance) && !thisStartEqualsToAnyOfOtherPoint) {
|
|
399
|
+
return true;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
const thisEndEqualsToAnyOfOtherPoint = this.end.distanceTo(other.start) <= distanceTolerance || this.end.distanceTo(other.end) <= distanceTolerance;
|
|
403
|
+
if (other.containsPoint(this.end, distanceTolerance) && !thisEndEqualsToAnyOfOtherPoint) {
|
|
404
|
+
return true;
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
return false;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
/**
|
|
412
|
+
* Logical AND of this and the other line section.
|
|
413
|
+
* @param other
|
|
414
|
+
*/
|
|
415
|
+
public getOverlap(other: Line2D): Line2D {
|
|
416
|
+
if (!this.overlaps(other)) {
|
|
417
|
+
return null;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
if (this.equals(other)) {
|
|
421
|
+
return this.clone();
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
const points = [
|
|
425
|
+
[this.start, this.end].filter(thisPoint => other.isPointOnLineSection(thisPoint)),
|
|
426
|
+
[other.start, other.end].filter(otherPoint => this.isPointOnLineSection(otherPoint))
|
|
427
|
+
].flat();
|
|
428
|
+
|
|
429
|
+
if (points.length !== 2) {
|
|
430
|
+
return null;
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
const overlap = Line2D.fromPoints(points[0], points[1]);
|
|
434
|
+
if (overlap.direction.manhattanDistanceTo(this.direction) > Number.EPSILON) {
|
|
435
|
+
overlap.flip();
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
return overlap;
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
/**
|
|
442
|
+
* Joins a copy of @line with the @other line.
|
|
443
|
+
* Other must be parallel to this line (within tolerance).
|
|
444
|
+
* Returns null if there is no overlap
|
|
445
|
+
* Clones the line, does not modify.
|
|
446
|
+
* @param line
|
|
447
|
+
* @param other
|
|
448
|
+
* @param distanceTolerance Maximum distance between lines or points to be considered touching/overlapping
|
|
449
|
+
* @param parallelTolerance Maximum angle difference in radians for lines to be considered parallel
|
|
450
|
+
*/
|
|
451
|
+
public static joinLine(line: Line2D, other: Line2D, distanceTolerance: number = 0, parallelTolerance: number = 0): Line2D {
|
|
452
|
+
if (!line.isCollinearWithTouchOrOverlap(other, distanceTolerance, parallelTolerance)) {
|
|
453
|
+
return null;
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
// When using tolerances, we need to check if points are close enough to the line section
|
|
457
|
+
const useTolerance = distanceTolerance > 0 || parallelTolerance > 0;
|
|
458
|
+
|
|
459
|
+
if (!useTolerance) {
|
|
460
|
+
const p1 = !line.isPointOnLineSection(other.start) ? other.start : line.start;
|
|
461
|
+
const p2 = !line.isPointOnLineSection(other.end) ? other.end : line.end;
|
|
462
|
+
return new Line2D(p1.clone(), p2.clone(), line.index);
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
// Determine the endpoints of the joined line
|
|
466
|
+
// We want the outermost points that encompass both lines
|
|
467
|
+
const points = [line.start, line.end, other.start, other.end];
|
|
468
|
+
|
|
469
|
+
// Project all points onto the line's direction to find min/max
|
|
470
|
+
const direction = line.direction;
|
|
471
|
+
const startPoint = line.start;
|
|
472
|
+
|
|
473
|
+
// Project each point onto the line's direction vector
|
|
474
|
+
const projections = points.map(p => {
|
|
475
|
+
const toPoint = new Vec2().subVectors(p, startPoint);
|
|
476
|
+
const t = toPoint.dot(direction);
|
|
477
|
+
return { point: p, t };
|
|
478
|
+
});
|
|
479
|
+
|
|
480
|
+
// Find the points with min and max projections
|
|
481
|
+
const minProj = projections.reduce((min, curr) => curr.t < min.t ? curr : min);
|
|
482
|
+
const maxProj = projections.reduce((max, curr) => curr.t > max.t ? curr : max);
|
|
483
|
+
|
|
484
|
+
const p1 = minProj.point;
|
|
485
|
+
const p2 = maxProj.point;
|
|
486
|
+
|
|
487
|
+
return new Line2D(p1.clone(), p2.clone(), line.index);
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
/**
|
|
491
|
+
* Joins provided lines into several joined lines.
|
|
492
|
+
* Lines must be parallel for joining (within tolerance).
|
|
493
|
+
* Clone the lines, does not modify.
|
|
494
|
+
* @param lines
|
|
495
|
+
* @param distanceTolerance Maximum distance between lines or points to be considered touching/overlapping
|
|
496
|
+
* @param parallelTolerance Maximum angle difference in radians for lines to be considered parallel
|
|
497
|
+
*/
|
|
498
|
+
public static joinLines(lines: Line2D[], distanceTolerance?: number, parallelTolerance?: number): Line2D[] {
|
|
499
|
+
const distTol = distanceTolerance ?? 0;
|
|
500
|
+
const parTol = parallelTolerance ?? 0;
|
|
501
|
+
|
|
502
|
+
if (lines.length < 2) {
|
|
503
|
+
return lines.map(x => x.clone());
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
// Start with cloned lines as initial result
|
|
507
|
+
const result: Line2D[] = lines.map(x => x.clone());
|
|
508
|
+
|
|
509
|
+
// Keep trying to join lines until no more joins are possible
|
|
510
|
+
let joinedInLastPass = true;
|
|
511
|
+
while (joinedInLastPass) {
|
|
512
|
+
joinedInLastPass = false;
|
|
513
|
+
|
|
514
|
+
// Try to join each pair of lines
|
|
515
|
+
for (let i = 0; i < result.length; i++) {
|
|
516
|
+
for (let j = i + 1; j < result.length; j++) {
|
|
517
|
+
const joinedLine = Line2D.joinLine(result[i], result[j], distTol, parTol);
|
|
518
|
+
if (joinedLine) {
|
|
519
|
+
// Replace the first line with the joined line and remove the second
|
|
520
|
+
result[i] = joinedLine;
|
|
521
|
+
result.splice(j, 1);
|
|
522
|
+
joinedInLastPass = true;
|
|
523
|
+
// Start over from the beginning since we modified the array
|
|
524
|
+
i = -1;
|
|
525
|
+
break;
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
return result;
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
/**
|
|
535
|
+
* Checks if the current line covers another line.
|
|
536
|
+
* A line is considered to cover another line if they are parallel and both the start and end points of the other line are contained within the current line.
|
|
537
|
+
* Both distance and angle tolerance can be provided.
|
|
538
|
+
*/
|
|
539
|
+
public covers(other: Line2D, tolerance: number = 0, parallelTolerance: number = 0): boolean {
|
|
540
|
+
if (!this.isParallelTo(other, parallelTolerance)) {
|
|
541
|
+
return false;
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
return this.containsPoint(other.start, tolerance) && this.containsPoint(other.end, tolerance);
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
/**
|
|
548
|
+
* Returns a new line that is the projection of this line onto @other. Uses `closestPointToPoint` to find the projection.
|
|
549
|
+
* @param other
|
|
550
|
+
* @param clampToLine
|
|
551
|
+
*/
|
|
552
|
+
public projectOn(other: Line2D, clampToLine: boolean): Line2D {
|
|
553
|
+
const p1 = other.closestPointToPoint(this.start, clampToLine, new Vec2());
|
|
554
|
+
const p2 = other.closestPointToPoint(this.end, clampToLine, new Vec2());
|
|
555
|
+
|
|
556
|
+
return p1.distanceTo(this.start) < p2.distanceTo(this.start) ? new Line2D(p1, p2) : new Line2D(p2, p1);
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
/**
|
|
560
|
+
* Divides the Line3D into a number of segments of the given length.
|
|
561
|
+
* Clone the line, does not modify.
|
|
562
|
+
* @param maxSegmentLength number
|
|
563
|
+
*/
|
|
564
|
+
public chunk(maxSegmentLength: number): Line2D[] {
|
|
565
|
+
const source = this.clone();
|
|
566
|
+
const result: Line2D[] = [];
|
|
567
|
+
while (source.length > maxSegmentLength) {
|
|
568
|
+
const chunk = source.clone();
|
|
569
|
+
chunk.moveEndPoint(-(chunk.length - maxSegmentLength));
|
|
570
|
+
result.push(chunk);
|
|
571
|
+
source.start = chunk.end.clone();
|
|
572
|
+
}
|
|
573
|
+
result.push(source);
|
|
574
|
+
return result;
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
public split(segmentsCount: number): Line2D[] {
|
|
578
|
+
const result: Line2D[] = [];
|
|
579
|
+
const segmentLength = this.length / segmentsCount;
|
|
580
|
+
for (let i = 0; i < segmentsCount; i++) {
|
|
581
|
+
const line = this.clone();
|
|
582
|
+
line.moveStartPoint(-i * segmentLength);
|
|
583
|
+
line.moveEndPoint(-(segmentsCount - i - 1) * segmentLength);
|
|
584
|
+
result.push(line);
|
|
585
|
+
}
|
|
586
|
+
return result;
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
/**
|
|
590
|
+
* Returns the closest point on the line to the given point.
|
|
591
|
+
* @param point
|
|
592
|
+
* @param clampToLine boolean (optional)
|
|
593
|
+
* @param target Vec2 (optional)
|
|
594
|
+
*/
|
|
595
|
+
public closestPointToPoint(point: Vector2, clampToLine?: boolean, target?: Vec2): Vec2 {
|
|
596
|
+
const t = this.closestPointToPointParameter(point, clampToLine);
|
|
597
|
+
return this.delta(target || new Vec2()).multiplyScalar(t).add(this.start);
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
public delta(target: Vec2): Vec2 {
|
|
601
|
+
return target.subVectors(this.end, this.start);
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
public closestPointToPointParameter(point: Vector2, clampToLine: boolean): number {
|
|
605
|
+
_startP.subVectors(point, this.start);
|
|
606
|
+
_startEnd.subVectors(this.end, this.start);
|
|
607
|
+
|
|
608
|
+
const startEnd2 = _startEnd.dot(_startEnd);
|
|
609
|
+
const startEnd_startP = _startEnd.dot(_startP);
|
|
610
|
+
|
|
611
|
+
let t = startEnd_startP / startEnd2;
|
|
612
|
+
|
|
613
|
+
if (clampToLine) {
|
|
614
|
+
t = MathUtils.clamp(t, 0, 1);
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
return t;
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
/**
|
|
621
|
+
* Returns the distance between the **infinite** line and the point.
|
|
622
|
+
* @param point
|
|
623
|
+
*/
|
|
624
|
+
public distanceToPointOnInfiniteLine(point: Point2): number {
|
|
625
|
+
const l2 = (((this.end.x - this.start.x) * (this.end.x - this.start.x)) + ((this.end.y - this.start.y) * (this.end.y - this.start.y)));
|
|
626
|
+
if (l2 === 0) return Infinity;
|
|
627
|
+
const s = (((this.start.y - point.y) * (this.end.x - this.start.x)) - ((this.start.x - point.x) * (this.end.y - this.start.y))) / l2;
|
|
628
|
+
return Math.abs(s) * Math.sqrt(l2);
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
/**
|
|
632
|
+
* Returns lines that are the result of clipping @source line by the @clips.
|
|
633
|
+
* Clips must be parallel to this line.
|
|
634
|
+
* Clones the line, does not modify this.
|
|
635
|
+
* @param source
|
|
636
|
+
* @param clips
|
|
637
|
+
*/
|
|
638
|
+
public static clipLines(source: Line2D, clips: Line2D[], distanceTolerance: number = 0, parallelTolerance: number = 0): Line2D[] {
|
|
639
|
+
if (!clips || clips.length === 0) return [source];
|
|
640
|
+
|
|
641
|
+
clips = clips.map(c => {
|
|
642
|
+
const copy = c.clone();
|
|
643
|
+
if (copy.direction.manhattanDistanceTo(source.direction) > Number.EPSILON) {
|
|
644
|
+
copy.flip();
|
|
645
|
+
}
|
|
646
|
+
return copy;
|
|
647
|
+
});
|
|
648
|
+
|
|
649
|
+
const free: Line2D[] = [];
|
|
650
|
+
const sources = [source];
|
|
651
|
+
|
|
652
|
+
while (sources.length > 0) {
|
|
653
|
+
|
|
654
|
+
let isFree = true;
|
|
655
|
+
|
|
656
|
+
const tested = sources.pop();
|
|
657
|
+
|
|
658
|
+
for (const cover of clips) {
|
|
659
|
+
|
|
660
|
+
if (tested.overlaps(cover, distanceTolerance, parallelTolerance)) {
|
|
661
|
+
isFree = false;
|
|
662
|
+
const subtracted = this.subtractSingle(tested, cover);
|
|
663
|
+
sources.push(...subtracted);
|
|
664
|
+
break;
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
if (isFree) free.push(tested);
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
return this.order(source, free);
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
/**
|
|
675
|
+
* Returns the original line section split into two parts, if the line **sections** overlap, otherwise null
|
|
676
|
+
*/
|
|
677
|
+
public splitAtIntersection(other: Line2D, tolerance: number = 0): Line2D[] {
|
|
678
|
+
const intersection = this.intersect(other);
|
|
679
|
+
if (intersection) {
|
|
680
|
+
if (this.isPointCloseToAndBesideLineSection(intersection, tolerance) && other.isPointCloseToAndBesideLineSection(intersection, tolerance)) {
|
|
681
|
+
return [
|
|
682
|
+
Line2D.fromPoints(this.start, intersection),
|
|
683
|
+
Line2D.fromPoints(intersection, this.end)
|
|
684
|
+
];
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
return null;
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
/**
|
|
692
|
+
* If lines **sections** overlap, returns the original line section split into two parts, sorted by length
|
|
693
|
+
* Else, if the **infinite** lines intersect, returns a new line extended to the intersection point
|
|
694
|
+
* Otherwise, null if the lines are parallel and do not intersect
|
|
695
|
+
*/
|
|
696
|
+
public splitAtOrExtendToIntersection(other: Line2D): Line2D[] {
|
|
697
|
+
const intersection = this.intersect(other);
|
|
698
|
+
if (intersection) {
|
|
699
|
+
return [
|
|
700
|
+
Line2D.fromPoints(this.start, intersection),
|
|
701
|
+
Line2D.fromPoints(intersection, this.end)
|
|
702
|
+
].filter(x => x.length > Number.EPSILON).sort((a, b) => a.length - b.length);
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
return null;
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
private static order(source: Line2D, lines: Line2D[]): Line2D[] {
|
|
709
|
+
if (source.start.x < source.end.x) {
|
|
710
|
+
lines.sort((a, b) => a.start.x - b.start.x);
|
|
711
|
+
} else if (source.start.x > source.end.x) {
|
|
712
|
+
lines.sort((a, b) => b.start.x - a.start.x);
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
if (source.start.y < source.end.y) {
|
|
716
|
+
lines.sort((a, b) => a.start.y - b.start.y);
|
|
717
|
+
} else if (source.start.y > source.end.y) {
|
|
718
|
+
lines.sort((a, b) => b.start.y - a.start.y);
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
return lines;
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
private static subtractSingle(source: Line2D, cover: Line2D): Line2D[] {
|
|
725
|
+
const left = source.clone();
|
|
726
|
+
left.end.copy(cover.start);
|
|
727
|
+
|
|
728
|
+
const right = source.clone();
|
|
729
|
+
right.start.copy(cover.end);
|
|
730
|
+
|
|
731
|
+
return [left, right].filter(x => x.length > 1 && x.direction.manhattanDistanceTo(source.direction) < Number.EPSILON);
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
/**
|
|
735
|
+
* If other line is not contained within this line, the excess is trimmed.
|
|
736
|
+
* Does not create a copy. Provided line is modified.
|
|
737
|
+
* @param lineToTrim
|
|
738
|
+
*/
|
|
739
|
+
public trimExcess(lineToTrim: Line2D): void {
|
|
740
|
+
if (!this.isCollinearWithTouchOrOverlap(lineToTrim)) {
|
|
741
|
+
return;
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
if (!this.isPointOnLineSection(lineToTrim.start)) {
|
|
745
|
+
const closest = this.closestPointToPoint(lineToTrim.start, true);
|
|
746
|
+
lineToTrim.start.copy(closest);
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
if (!this.isPointOnLineSection(lineToTrim.end)) {
|
|
750
|
+
const closest = this.closestPointToPoint(lineToTrim.end, true);
|
|
751
|
+
lineToTrim.end.copy(closest);
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
/**
|
|
756
|
+
* If other line is shorter than this, endpoints are moved to extend other
|
|
757
|
+
* Does not create a copy. Provided line is modified.
|
|
758
|
+
* @param lineToExtend
|
|
759
|
+
* @param tolerance
|
|
760
|
+
*/
|
|
761
|
+
public extendToEnds(lineToExtend: Line2D, tolerance: number): void {
|
|
762
|
+
if (!this.isCollinearWithTouchOrOverlap(lineToExtend)) {
|
|
763
|
+
console.log("Can't clip, lines that are not collinear with touch or overlap");
|
|
764
|
+
return;
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
if (this.start.distanceTo(lineToExtend.start) <= tolerance) {
|
|
768
|
+
lineToExtend.start.copy(this.start);
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
if (this.end.distanceTo(lineToExtend.end) <= tolerance) {
|
|
772
|
+
lineToExtend.end.copy(this.end);
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
/**
|
|
777
|
+
* If there is an intersection between this and other, this line is extended to the intersection point. Lines are assumed to be infinite.
|
|
778
|
+
* Modifies this line.
|
|
779
|
+
* @param other
|
|
780
|
+
* @param maxDistanceToIntersection
|
|
781
|
+
*/
|
|
782
|
+
public extendToOrTrimAtIntersection(other: Line2D, maxDistanceToIntersection: number = Number.MAX_VALUE): this {
|
|
783
|
+
const intersection = this.intersect(other);
|
|
784
|
+
|
|
785
|
+
if (intersection) {
|
|
786
|
+
const distanceToStart = this.start.distanceTo(intersection);
|
|
787
|
+
const distanceToEnd = this.end.distanceTo(intersection);
|
|
788
|
+
|
|
789
|
+
if (distanceToStart <= maxDistanceToIntersection || distanceToEnd <= maxDistanceToIntersection) {
|
|
790
|
+
if (distanceToStart < distanceToEnd) {
|
|
791
|
+
this.start.copy(intersection);
|
|
792
|
+
|
|
793
|
+
} else {
|
|
794
|
+
this.end.copy(intersection);
|
|
795
|
+
}
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
return this;
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
/**
|
|
803
|
+
* Returns the intersection point of two lines.
|
|
804
|
+
* @param other
|
|
805
|
+
* @param lineSegmentOnly If true, only return the intersection if it is within the line segments. Otherwise, return the intersection if the lines intersect anywhere.
|
|
806
|
+
*/
|
|
807
|
+
public intersect(other: Line2D, lineSegmentOnly?: boolean): Vec2 {
|
|
808
|
+
// Check if none of the lines are of length 0
|
|
809
|
+
if ((this.start.x === this.end.x && this.start.y === this.end.y) || (other.start.x === other.end.x && other.start.y === other.end.y)) {
|
|
810
|
+
return null;
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
const denominator = ((other.end.y - other.start.y) * (this.end.x - this.start.x) - (other.end.x - other.start.x) * (this.end.y - this.start.y));
|
|
814
|
+
|
|
815
|
+
// Lines are parallel
|
|
816
|
+
if (denominator === 0) {
|
|
817
|
+
return null;
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
const ua = ((other.end.x - other.start.x) * (this.start.y - other.start.y) - (other.end.y - other.start.y) * (this.start.x - other.start.x)) / denominator;
|
|
821
|
+
|
|
822
|
+
// Check if the intersection point is within the bounds of the line segments if required
|
|
823
|
+
if (lineSegmentOnly) {
|
|
824
|
+
const ub = ((this.end.x - this.start.x) * (this.start.y - other.start.y) - (this.end.y - this.start.y) * (this.start.x - other.start.x)) / denominator;
|
|
825
|
+
if (ua < 0 || ua > 1 || ub < 0 || ub > 1) {
|
|
826
|
+
return null;
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
// Return an object with the x and y coordinates of the intersection
|
|
831
|
+
const x = this.start.x + ua * (this.end.x - this.start.x);
|
|
832
|
+
const y = this.start.y + ua * (this.end.y - this.start.y);
|
|
833
|
+
|
|
834
|
+
return new Vec2(x, y);
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
/**
|
|
838
|
+
* Check that the line section intersect and that they are in the specified angle to each other
|
|
839
|
+
* @param other Line
|
|
840
|
+
* @param expectedAngleInRads number
|
|
841
|
+
* @param angleTolerance number
|
|
842
|
+
* @param distanceTolerance number
|
|
843
|
+
*/
|
|
844
|
+
public hasIntersectionWithAngle(other: Line2D, expectedAngleInRads: number, angleTolerance = Number.EPSILON, distanceTolerance = Number.EPSILON): Vec2 {
|
|
845
|
+
const angle = this.direction.angle() % TwoPI;
|
|
846
|
+
const otherAngle = other.direction.angle() % TwoPI;
|
|
847
|
+
const actualAngle = Math.abs(angle - otherAngle);
|
|
848
|
+
|
|
849
|
+
if (Math.abs(actualAngle - expectedAngleInRads) <= angleTolerance) {
|
|
850
|
+
const intersection = this.intersect(other);
|
|
851
|
+
if (
|
|
852
|
+
intersection &&
|
|
853
|
+
this.closestPointToPoint(intersection, true).distanceTo(intersection) <= distanceTolerance &&
|
|
854
|
+
other.closestPointToPoint(intersection, true).distanceTo(intersection) <= distanceTolerance
|
|
855
|
+
) {
|
|
856
|
+
return intersection;
|
|
857
|
+
}
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
return null;
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
public get isCloserToHorizontal(): boolean {
|
|
864
|
+
const direction = this.direction;
|
|
865
|
+
return direction.angleTo(directions2d.Right) < Math.PI / 4 || direction.angleTo(directions2d.Left) < Math.PI / 4;
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
public get isCloserToVertical(): boolean {
|
|
869
|
+
return !this.isCloserToHorizontal;
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
/**
|
|
873
|
+
* Accepts an array of Line2D and groups them into arrays of connected lines
|
|
874
|
+
* @param lines Lines to be grouped
|
|
875
|
+
* @param tolerance Tolerance for considering lines as connected
|
|
876
|
+
* @param breakpoints Endpoints on breakpoints are not considered as connected
|
|
877
|
+
*/
|
|
878
|
+
public static groupConnectedLines(lines: Line2D[], tolerance: number = 0, breakpoints: Vec2[] = []): Line2D[][] {
|
|
879
|
+
const visited: Set<Line2D> = new Set();
|
|
880
|
+
|
|
881
|
+
// Use graph-based approach. Each line can be considered as an edge in the graph, and the endpoints of the lines can be considered as vertices.
|
|
882
|
+
// Then use Depth-First Search (DFS) to find connected components in the graph.
|
|
883
|
+
const dfs = (line: Line2D, group: Line2D[]) => {
|
|
884
|
+
if (visited.has(line)) return;
|
|
885
|
+
visited.add(line);
|
|
886
|
+
group.push(line);
|
|
887
|
+
|
|
888
|
+
lines.forEach(neighbor => {
|
|
889
|
+
if (!visited.has(neighbor)) {
|
|
890
|
+
if (line.connectsTo(neighbor, tolerance, breakpoints)) {
|
|
891
|
+
dfs(neighbor, group);
|
|
892
|
+
}
|
|
893
|
+
}
|
|
894
|
+
});
|
|
895
|
+
};
|
|
896
|
+
|
|
897
|
+
const connectedLines: Line2D[][] = [];
|
|
898
|
+
|
|
899
|
+
lines.forEach(line => {
|
|
900
|
+
if (!visited.has(line)) {
|
|
901
|
+
const group: Line2D[] = [];
|
|
902
|
+
dfs(line, group);
|
|
903
|
+
connectedLines.push(sortLinesByConnections(group, 1));
|
|
904
|
+
}
|
|
905
|
+
});
|
|
906
|
+
|
|
907
|
+
return connectedLines;
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
/**
|
|
911
|
+
* Returns true if any endpoint of this line is within the tolerance of any @other line's endpoints.
|
|
912
|
+
* @param other
|
|
913
|
+
* @param tolerance
|
|
914
|
+
* @param breakpoints
|
|
915
|
+
*/
|
|
916
|
+
public connectsTo(other: Line2D, tolerance: number = 0, breakpoints: typeof other.start[] = []): boolean {
|
|
917
|
+
return (
|
|
918
|
+
(this.start.isNear(other.start, tolerance) && breakpoints.every(b => !b.isNear(this.start, tolerance))) ||
|
|
919
|
+
(this.start.isNear(other.end, tolerance) && breakpoints.every(b => !b.isNear(this.start, tolerance))) ||
|
|
920
|
+
(this.end.isNear(other.start, tolerance) && breakpoints.every(b => !b.isNear(this.end, tolerance))) ||
|
|
921
|
+
(this.end.isNear(other.end, tolerance) && breakpoints.every(b => !b.isNear(this.end, tolerance)))
|
|
922
|
+
);
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
/**
|
|
926
|
+
* Project the line to 2D space. For start and end points Vec2.y becomes Vec3.z. and Vec3.y is provided as an argument.
|
|
927
|
+
* @param y - The y value of the new Vec3 instance.
|
|
928
|
+
* @returns A new Line3D instance.
|
|
929
|
+
*/
|
|
930
|
+
public in3DSpace(y: number = 0): Line3D {
|
|
931
|
+
return new Line3D(this.start.in3DSpace(y), this.end.in3DSpace(y));
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
public equals(other: Line2D, tolerance: number = 0): boolean {
|
|
935
|
+
if (!tolerance) {
|
|
936
|
+
return !!other && this.start.equals(other.start) && this.end.equals(other.end);
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
return !!other && this.start.distanceTo(other.start) <= tolerance && this.end.distanceTo(other.end) <= tolerance;
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
/**
|
|
943
|
+
* Deep clone of this line
|
|
944
|
+
*/
|
|
945
|
+
public clone(): Line2D {
|
|
946
|
+
return new Line2D(this.start.clone(), this.end.clone(), this.index);
|
|
947
|
+
}
|
|
948
|
+
|
|
949
|
+
public toString(): string {
|
|
950
|
+
return `Line(${this.start.x}, ${this.start.y}, ${this.end.x}, ${this.end.y})`;
|
|
951
|
+
}
|
|
858
952
|
}
|