@immugio/three-math-extensions 0.0.11 → 0.0.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/Line2D.ts CHANGED
@@ -1,661 +1,661 @@
1
- import { Point2 } from "./Point2";
2
- import { Vector2 } from "three";
3
- import { Vec2 } from "./Vec2";
4
-
5
- export class Line2D {
6
-
7
- constructor(public start: Vec2, public end: Vec2, public index: number = 0) {
8
- }
9
-
10
- public static fromCoordinates(x1: number, y1: number, x2: number, y2: number, index: number = 0): Line2D {
11
- return new Line2D(new Vec2(x1, y1), new Vec2(x2, y2), index);
12
- }
13
-
14
- public static fromPoints(p1: Point2, p2: Point2, index: number = 0): Line2D {
15
- return new Line2D(new Vec2(p1.x, p1.y), new Vec2(p2.x, p2.y), index);
16
- }
17
-
18
- /**
19
- * Creates a polygon formed by an array of lines from points provided.
20
- * The polygon will only be closed if either
21
- * 1) the first and last points are the same or 2) `forceClosedPolygon` is true.
22
- */
23
- public static fromPolygon(polygon: Point2[], forceClosedPolygon: boolean = false): Line2D[] {
24
- if (!polygon || polygon.length < 2) {
25
- return [];
26
- }
27
-
28
- if (forceClosedPolygon && (polygon[0].x !== polygon.at(-1).x || polygon[0].y !== polygon.at(-1).y)) {
29
- polygon = [...polygon, polygon[0]];
30
- }
31
-
32
- const lines: Line2D[] = [];
33
- for (let i = 0; i < polygon.length - 1; i++) {
34
- lines.push(Line2D.fromPoints(polygon[i], polygon[i + 1], i));
35
- }
36
-
37
- return lines;
38
- }
39
-
40
- public static fromLength(length: number): Line2D {
41
- return Line2D.fromCoordinates(-length / 2, 0, length / 2, 0);
42
- }
43
-
44
- public get center(): Vec2 {
45
- return new Vec2((this.start.x + this.end.x) / 2, (this.start.y + this.end.y) / 2);
46
- }
47
-
48
- /**
49
- * Set the center of the line to the provided point. Length and direction remain unchanged.
50
- * Modifies this line.
51
- * @param value
52
- */
53
- public set center(value: Point2) {
54
- const current = this.center;
55
- const diffX = current.x - value.x;
56
- const diffY = current.y - value.y;
57
- this.start.x -= diffX;
58
- this.start.y -= diffY;
59
- this.end.x -= diffX;
60
- this.end.y -= diffY;
61
- }
62
-
63
- /**
64
- * Set the center of the line to the provided point. Length and direction remain unchanged.
65
- * Modifies this line.
66
- * @param value
67
- */
68
- public setCenter(value: Point2): Line2D {
69
- this.center = value;
70
- return this;
71
- }
72
-
73
- /*
74
- * Extends or reduces the line by the given length while keeping the center of the line constant.
75
- * Modifies this line.
76
- */
77
- public resize(amount: number): Line2D {
78
- this.moveStartPoint(amount / 2);
79
- this.moveEndPoint(amount / 2);
80
- return this;
81
- }
82
-
83
- /*
84
- * Moves start point on the line by the given amount. Plus values move the point further away from the center.
85
- * Modifies this line.
86
- */
87
- public moveStartPoint(amount: number): Line2D {
88
- const p1 = this.movePointOnThisLine(this.start, amount);
89
- this.start.copy(p1);
90
-
91
- return this;
92
- }
93
-
94
- /**
95
- * Moves end point on the line by the given amount. Plus values move the point further away from the center.
96
- * Modifies this line.
97
- */
98
- public moveEndPoint(amount: number): Line2D {
99
- const p2 = this.movePointOnThisLine(this.end, amount);
100
- this.end.copy(p2);
101
-
102
- return this;
103
- }
104
-
105
- private movePointOnThisLine(point: Point2, amount: number): Vec2 {
106
- const vec = new Vector2(this.center.x - point.x, this.center.y - point.y);
107
- const length = vec.length();
108
- vec.normalize().multiplyScalar(length + amount);
109
-
110
- return new Vec2(this.center.x - vec.x,this.center.y - vec.y);
111
- }
112
-
113
- /**
114
- * Set the length of this line. Center and direction remain unchanged.
115
- * Modifies this line.
116
- * @param l
117
- */
118
- public set length(l: number) {
119
- const length = this.length;
120
- this.resize(l - length);
121
- }
122
-
123
- public get length(): number {
124
- return this.start.distanceTo(this.end);
125
- }
126
-
127
- /**
128
- * Set the length of this line. Center and direction remain unchanged.
129
- * @param length
130
- */
131
- public setLength(length: number): this {
132
- this.length = length;
133
- return this;
134
- }
135
-
136
- /**
137
- * Returns the start and end points of the line as an array.
138
- * Endpoints are not cloned.
139
- */
140
- public get endpoints(): Vec2[] {
141
- return [this.start, this.end];
142
- }
143
-
144
- /**
145
- * Returns the direction of this line.
146
- */
147
- public get direction(): Vec2 {
148
- return this.end.clone().sub(this.start).normalize();
149
- }
150
-
151
- /**
152
- * Inverts the direction of the line.
153
- * Modifies this line.
154
- */
155
- public flip(): Line2D {
156
- const temp = this.start.clone();
157
- this.start.copy(this.end);
158
- this.end.copy(temp);
159
-
160
- return this;
161
- }
162
-
163
- /**
164
- * Rotates the line around the center by the given angle in radians.
165
- * Modifies this line.
166
- * @param radians Positive values rotate counter-clockwise.
167
- * @param center
168
- */
169
- public rotate(radians: number, center: Vec2 = this.center): Line2D {
170
- this.start.rotateAround(center, radians);
171
- this.end.rotateAround(center, radians);
172
-
173
- return this;
174
- }
175
-
176
- /**
177
- * Move the line by the given vector.
178
- * Modifies this line.
179
- */
180
- public translate(value: Point2): Line2D {
181
- this.start.x += value.x;
182
- this.start.y += value.y;
183
- this.end.x += value.x;
184
- this.end.y += value.y;
185
-
186
- return this;
187
- }
188
-
189
- /**
190
- * Move the line to its left by the given amount.
191
- * Modifies this line.
192
- */
193
- public translateLeft(amount: number): Line2D {
194
- const translation = this.direction.rotateAround(new Vec2(), -Math.PI / 2).multiplyScalar(amount);
195
- return this.translate(translation);
196
- }
197
-
198
- /**
199
- * Move the line to its right by the given amount.
200
- * Modifies this line.
201
- */
202
- public translateRight(amount: number): Line2D {
203
- const translation = this.direction.rotateAround(new Vec2(), Math.PI / 2).multiplyScalar(amount);
204
- return this.translate(translation);
205
- }
206
-
207
- /**
208
- * Returns true when the point is actually inside the (finite) line segment.
209
- * https://jsfiddle.net/c06zdxtL/2/
210
- * https://stackoverflow.com/questions/6865832/detecting-if-a-point-is-of-a-line-segment/6877674
211
- * @param point: Point2
212
- */
213
- public isPointOnLineSection(point: Point2): boolean {
214
- if (!this.isPointOnInfiniteLine(point)) {
215
- return false;
216
- }
217
-
218
- return this.isPointBesideLineSection(point);
219
- }
220
-
221
- /**
222
- * Returns true when the point is beside the line **segment** and within the maxDistance.
223
- * @param point
224
- * @param maxDistance
225
- */
226
- public isPointCloseToAndBesideLineSection(point: Point2, maxDistance: number): boolean {
227
- const distance = this.distanceToPointOnInfiniteLine(point);
228
- return distance <= maxDistance && this.isPointBesideLineSection(point);
229
- }
230
-
231
- /**
232
- * Returns true when the point is beside the line **segment**
233
- * @param point
234
- */
235
- public isPointBesideLineSection(point: Point2): boolean {
236
- 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)));
237
- if (l2 == 0) return false;
238
- 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;
239
-
240
- return (0 <= r) && (r <= 1);
241
- }
242
-
243
- /**
244
- * Returns true when the point is on the **infinite** line.
245
- * @param point
246
- */
247
- public isPointOnInfiniteLine(point: Point2): boolean {
248
- return (point.y - this.start.y) * (this.end.x - this.start.x) === (this.end.y - this.start.y) * (point.x - this.start.x);
249
- }
250
-
251
- /**
252
- * Returns true if other line is collinear and overlaps or at least touching this line.
253
- * @param other
254
- */
255
- public isCollinearWithTouchOrOverlap(other: Line2D): boolean {
256
- if (!this.isPointOnInfiniteLine(other.start) || !this.isPointOnInfiniteLine(other.end)) {
257
- return false;
258
- }
259
-
260
- return this.isPointOnLineSection(other.start) || this.isPointOnLineSection(other.end) ||
261
- other.isPointOnLineSection(this.start) || other.isPointOnLineSection(this.end);
262
- }
263
-
264
- /**
265
- * Returns true if there is any overlap between this line and the @other line section.
266
- */
267
- public overlaps(other: Line2D): boolean {
268
- if (!this.isCollinearWithTouchOrOverlap(other)) {
269
- return false;
270
- }
271
-
272
- if (this.start.equals(other.start) && this.end.equals(other.end)) {
273
- return true;
274
- }
275
-
276
- return !this.start.equals(other.end) && !this.end.equals(other.start);
277
- }
278
-
279
- /**
280
- * Logical AND of this and the other line section.
281
- * @param other
282
- */
283
- public getOverlap(other: Line2D): Line2D {
284
- if (!this.overlaps(other)) {
285
- return null;
286
- }
287
-
288
- if (this.equals(other)) {
289
- return this.clone();
290
- }
291
-
292
- const points = [
293
- [this.start, this.end].filter(thisPoint => other.isPointOnLineSection(thisPoint)),
294
- [other.start, other.end].filter(otherPoint => this.isPointOnLineSection(otherPoint))
295
- ].flat();
296
-
297
- if (points.length !== 2) {
298
- return null;
299
- }
300
-
301
- const overlap = Line2D.fromPoints(points[0], points[1]);
302
- if (overlap.direction.manhattanDistanceTo(this.direction) > Number.EPSILON) {
303
- overlap.flip();
304
- }
305
-
306
- return overlap;
307
- }
308
-
309
- /**
310
- * Joins a copy of @line with the @other line.
311
- * Other must be parallel to this line.
312
- * Returns null if there is no overlap
313
- * Clones the line, does not modify.
314
- * @param line
315
- * @param other
316
- */
317
- public static joinLine(line: Line2D, other: Line2D): Line2D {
318
- if (!line.isCollinearWithTouchOrOverlap(other)) {
319
- return null;
320
- }
321
-
322
- const p1 = !line.isPointOnLineSection(other.start) ? other.start : line.start;
323
- const p2 = !line.isPointOnLineSection(other.end) ? other.end : line.end;
324
-
325
- return new Line2D(p1.clone(), p2.clone(), line.index);
326
- }
327
-
328
- /**
329
- * Joins provided lines into several joined lines.
330
- * Lines must be parallel for joining.
331
- * Clone the lines, does not modify.
332
- * @param lines
333
- */
334
- public static joinLines(lines: Line2D[]): Line2D[] {
335
- if (lines.length < 2) {
336
- return lines.map(x => x.clone());
337
- }
338
-
339
- const toProcess = lines.slice();
340
- const result: Line2D[] = [];
341
-
342
- while (toProcess.length > 0) {
343
-
344
- const current = toProcess.pop();
345
- let joined = false;
346
-
347
- for (let i = 0; i < result.length; i++) {
348
- const other = result[i];
349
- const joinedLine = Line2D.joinLine(current, other);
350
- if (joinedLine) {
351
- result[i] = joinedLine;
352
- joined = true;
353
- break;
354
- }
355
- }
356
-
357
- if (!joined) {
358
- result.push(current.clone());
359
- }
360
- }
361
-
362
- return result;
363
- }
364
-
365
- /**
366
- * Divides the Line3D into a number of segments of the given length.
367
- * Clone the line, does not modify.
368
- * @param maxSegmentLength number
369
- */
370
- public chunk(maxSegmentLength: number): Line2D[] {
371
- const source = this.clone();
372
- const result: Line2D[] = [];
373
- while (source.length > maxSegmentLength) {
374
- const chunk = source.clone();
375
- chunk.moveEndPoint(-(chunk.length - maxSegmentLength));
376
- result.push(chunk);
377
- source.start = chunk.end.clone();
378
- }
379
- result.push(source);
380
- return result;
381
- }
382
-
383
- /**
384
- * Returns the closest point parameter on the **infinite** line to the given point.
385
- * @param point
386
- */
387
- public closestPointToPointParameterOnInfiniteLine(point: Vector2): number {
388
- const startP = new Vec2().subVectors(point, this.start);
389
- const startEnd = new Vec2().subVectors(this.end, this.start);
390
-
391
- const startEnd2 = startEnd.dot(startEnd);
392
- const startEnd_startP = startEnd.dot(startP);
393
-
394
- return startEnd_startP / startEnd2;
395
- }
396
-
397
- /**
398
- * Returns the closest point on the **infinite** line to the given point.
399
- * @param point
400
- */
401
- public closestPointOnInfiniteLine(point: Vector2): Vec2 {
402
- const t = this.closestPointToPointParameterOnInfiniteLine(point);
403
- return new Vec2().subVectors(this.end, this.start).multiplyScalar(t).add(this.start);
404
- }
405
-
406
- /**
407
- * Returns the closest point on the line **section** to the given point.
408
- * @param point
409
- */
410
- public closestPointOnLine(point: Vector2): Vec2 {
411
- const closestPoint = this.closestPointOnInfiniteLine(point);
412
- if (this.isPointOnLineSection(closestPoint)) {
413
- return closestPoint;
414
- }
415
-
416
- return closestPoint.distanceTo(this.start) < closestPoint.distanceTo(this.end) ? this.start : this.end;
417
- }
418
-
419
- /**
420
- * Returns the distance between the **infinite** line and the point.
421
- * @param point
422
- */
423
- public distanceToPointOnInfiniteLine(point: Point2): number {
424
- 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)));
425
- if (l2 == 0) return Infinity;
426
- 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;
427
- return Math.abs(s) * Math.sqrt(l2);
428
- }
429
-
430
- /**
431
- * Returns lines that are the result of clipping @source line by the @clips.
432
- * Clips must be parallel to this line.
433
- * Clones the line, does not modify this.
434
- * @param source
435
- * @param clips
436
- */
437
- public static clipLines(source: Line2D, clips: Line2D[]): Line2D[] {
438
- if (!clips || clips.length === 0) return [source];
439
-
440
- clips = clips.map(c => {
441
- const copy = c.clone();
442
- if (copy.direction.manhattanDistanceTo(source.direction) > Number.EPSILON) {
443
- copy.flip();
444
- }
445
- return copy;
446
- });
447
-
448
- const free: Line2D[] = [];
449
- const sources = [source];
450
-
451
- while (sources.length > 0) {
452
-
453
- let isFree = true;
454
-
455
- const tested = sources.pop();
456
-
457
- for (const cover of clips) {
458
-
459
- if (tested.overlaps(cover)) {
460
- isFree = false;
461
- const subtracted = this.subtractSingle(tested, cover);
462
- sources.push(...subtracted);
463
- break;
464
- }
465
- }
466
-
467
- if (isFree) free.push(tested);
468
- }
469
-
470
- return this.order(source, free);
471
- }
472
-
473
- /**
474
- * Returns the original line section split into two parts, if the line **sections** overlap, otherwise null
475
- */
476
- public splitAtIntersection(other: Line2D, tolerance: number = 0): Line2D[] {
477
- const intersection = this.intersect(other);
478
- if (intersection) {
479
- if (this.isPointCloseToAndBesideLineSection(intersection, tolerance) && other.isPointCloseToAndBesideLineSection(intersection, tolerance)) {
480
- return [
481
- Line2D.fromPoints(this.start, intersection),
482
- Line2D.fromPoints(intersection, this.end)
483
- ];
484
- }
485
- }
486
-
487
- return null;
488
- }
489
-
490
- /**
491
- * If lines **sections** overlap, returns the original line section split into two parts, sorted by length
492
- * Else, if the **infinite** lines intersect, returns a new line extended to the intersection point
493
- * Otherwise, null if the lines are parallel and do not intersect
494
- */
495
- public splitAtOrExtendToIntersection(other: Line2D): Line2D[] {
496
- const intersection = this.intersect(other);
497
- if (intersection) {
498
- return [
499
- Line2D.fromPoints(this.start, intersection),
500
- Line2D.fromPoints(intersection, this.end)
501
- ].filter(x => x.length > Number.EPSILON).sort((a, b) => a.length - b.length);
502
- }
503
-
504
- return null;
505
- }
506
-
507
- private static order(source: Line2D, lines: Line2D[]): Line2D[] {
508
- if (source.start.x < source.end.x) {
509
- lines.sort((a, b) => a.start.x - b.start.x);
510
- } else if (source.start.x > source.end.x) {
511
- lines.sort((a, b) => b.start.x - a.start.x);
512
- }
513
-
514
- if (source.start.y < source.end.y) {
515
- lines.sort((a, b) => a.start.y - b.start.y);
516
- } else if (source.start.y > source.end.y) {
517
- lines.sort((a, b) => b.start.y - a.start.y);
518
- }
519
-
520
- return lines;
521
- }
522
-
523
- private static subtractSingle(source: Line2D, cover: Line2D): Line2D[] {
524
- const left = source.clone();
525
- left.end.copy(cover.start);
526
-
527
- const right = source.clone();
528
- right.start.copy(cover.end);
529
-
530
- return [left, right].filter(x => x.length > 1 && x.direction.manhattanDistanceTo(source.direction) < Number.EPSILON);
531
- }
532
-
533
- /**
534
- * If other line is not contained within this line, the excess is trimmed.
535
- * Does not create a copy. Provided line is modified.
536
- * @param lineToTrim
537
- */
538
- public trimExcess(lineToTrim: Line2D): void {
539
- if (!this.isCollinearWithTouchOrOverlap(lineToTrim)) {
540
- return;
541
- }
542
-
543
- if (!this.isPointOnLineSection(lineToTrim.start)) {
544
- const closest = this.closestPointOnLine(lineToTrim.start);
545
- lineToTrim.start.copy(closest);
546
- }
547
-
548
- if (!this.isPointOnLineSection(lineToTrim.end)) {
549
- const closest = this.closestPointOnLine(lineToTrim.end);
550
- lineToTrim.end.copy(closest);
551
- }
552
- }
553
-
554
- /**
555
- * If other line is shorter than this, endpoints are moved to extend other
556
- * Does not create a copy. Provided line is modified.
557
- * @param lineToExtend
558
- * @param tolerance
559
- */
560
- public extendToEnds(lineToExtend: Line2D, tolerance: number): void {
561
- if (!this.isCollinearWithTouchOrOverlap(lineToExtend)) {
562
- console.log("Can't clip, lines that are not collinear with touch or overlap");
563
- return;
564
- }
565
-
566
- if (this.start.distanceTo(lineToExtend.start) <= tolerance) {
567
- lineToExtend.start.copy(this.start);
568
- }
569
-
570
- if (this.end.distanceTo(lineToExtend.end) <= tolerance) {
571
- lineToExtend.end.copy(this.end);
572
- }
573
- }
574
-
575
- /**
576
- * If there is an intersection between this and other, this line is extended to the intersection point. Lines are assumed to be infinite.
577
- * Modifies this line.
578
- * @param other
579
- * @param maxDistanceToIntersection
580
- */
581
- public extendToOrTrimAtIntersection(other: Line2D, maxDistanceToIntersection: number = Number.MAX_VALUE): Line2D {
582
- const intersection = this.intersect(other);
583
-
584
- if (intersection) {
585
- const distanceToStart = this.start.distanceTo(intersection);
586
- const distanceToEnd = this.end.distanceTo(intersection);
587
-
588
- if (distanceToStart <= maxDistanceToIntersection || distanceToEnd <= maxDistanceToIntersection) {
589
- if (distanceToStart < distanceToEnd) {
590
- this.start.copy(intersection);
591
-
592
- } else {
593
- this.end.copy(intersection);
594
- }
595
- }
596
- }
597
-
598
- return this;
599
- }
600
-
601
- /**
602
- * Returns the intersection point of two lines. The lines are assumed to be infinite.
603
- */
604
- public intersect(other: Line2D): Vec2 {
605
- // Check if none of the lines are of length 0
606
- 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)) {
607
- return null;
608
- }
609
-
610
- 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));
611
-
612
- // Lines are parallel
613
- if (denominator === 0) {
614
- return null;
615
- }
616
-
617
- 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;
618
-
619
- // Return an object with the x and y coordinates of the intersection
620
- const x = this.start.x + ua * (this.end.x - this.start.x);
621
- const y = this.start.y + ua * (this.end.y - this.start.y);
622
-
623
- return new Vec2(x, y);
624
- }
625
-
626
- /**
627
- * Check that the infinite lines intersect and that they are in the specified angle to each other
628
- * @param other Line
629
- * @param expectedAngleInRads number
630
- */
631
- public hasIntersectionWithAngle(other: Line2D, expectedAngleInRads: number): Vec2 {
632
- const angle = this.direction.angle();
633
- const otherAngle = other.direction.angle();
634
- const actualAngle = Math.abs(angle - otherAngle);
635
-
636
- if (Math.abs(actualAngle - expectedAngleInRads) < Number.EPSILON) {
637
- const intersection = this.intersect(other);
638
- if (intersection && this.isPointOnLineSection(intersection) && other.isPointOnLineSection(intersection)) {
639
-
640
- return intersection;
641
- }
642
- }
643
-
644
- return null;
645
- }
646
-
647
- /**
648
- * Deep clone of this line
649
- */
650
- public clone(): Line2D {
651
- return new Line2D(this.start.clone(), this.end.clone(), this.index);
652
- }
653
-
654
- public toString(): string {
655
- return `Line(${this.start.x}, ${this.start.y}, ${this.end.x}, ${this.end.y})`;
656
- }
657
-
658
- public equals(other: Line2D): boolean {
659
- return !!other && this.start.equals(other.start) && this.end.equals(other.end);
660
- }
1
+ import { Point2 } from "./Point2";
2
+ import { Vector2 } from "three";
3
+ import { Vec2 } from "./Vec2";
4
+
5
+ export class Line2D {
6
+
7
+ constructor(public start: Vec2, public end: Vec2, public index: number = 0) {
8
+ }
9
+
10
+ public static fromCoordinates(x1: number, y1: number, x2: number, y2: number, index: number = 0): Line2D {
11
+ return new Line2D(new Vec2(x1, y1), new Vec2(x2, y2), index);
12
+ }
13
+
14
+ public static fromPoints(p1: Point2, p2: Point2, index: number = 0): Line2D {
15
+ return new Line2D(new Vec2(p1.x, p1.y), new Vec2(p2.x, p2.y), index);
16
+ }
17
+
18
+ /**
19
+ * Creates a polygon formed by an array of lines from points provided.
20
+ * The polygon will only be closed if either
21
+ * 1) the first and last points are the same or 2) `forceClosedPolygon` is true.
22
+ */
23
+ public static fromPolygon(polygon: Point2[], forceClosedPolygon: boolean = false): Line2D[] {
24
+ if (!polygon || polygon.length < 2) {
25
+ return [];
26
+ }
27
+
28
+ if (forceClosedPolygon && (polygon[0].x !== polygon.at(-1).x || polygon[0].y !== polygon.at(-1).y)) {
29
+ polygon = [...polygon, polygon[0]];
30
+ }
31
+
32
+ const lines: Line2D[] = [];
33
+ for (let i = 0; i < polygon.length - 1; i++) {
34
+ lines.push(Line2D.fromPoints(polygon[i], polygon[i + 1], i));
35
+ }
36
+
37
+ return lines;
38
+ }
39
+
40
+ public static fromLength(length: number): Line2D {
41
+ return Line2D.fromCoordinates(-length / 2, 0, length / 2, 0);
42
+ }
43
+
44
+ public get center(): Vec2 {
45
+ return new Vec2((this.start.x + this.end.x) / 2, (this.start.y + this.end.y) / 2);
46
+ }
47
+
48
+ /**
49
+ * Set the center of the line to the provided point. Length and direction remain unchanged.
50
+ * Modifies this line.
51
+ * @param value
52
+ */
53
+ public set center(value: Point2) {
54
+ const current = this.center;
55
+ const diffX = current.x - value.x;
56
+ const diffY = current.y - value.y;
57
+ this.start.x -= diffX;
58
+ this.start.y -= diffY;
59
+ this.end.x -= diffX;
60
+ this.end.y -= diffY;
61
+ }
62
+
63
+ /**
64
+ * Set the center of the line to the provided point. Length and direction remain unchanged.
65
+ * Modifies this line.
66
+ * @param value
67
+ */
68
+ public setCenter(value: Point2): Line2D {
69
+ this.center = value;
70
+ return this;
71
+ }
72
+
73
+ /*
74
+ * Extends or reduces the line by the given length while keeping the center of the line constant.
75
+ * Modifies this line.
76
+ */
77
+ public resize(amount: number): Line2D {
78
+ this.moveStartPoint(amount / 2);
79
+ this.moveEndPoint(amount / 2);
80
+ return this;
81
+ }
82
+
83
+ /*
84
+ * Moves start point on the line by the given amount. Plus values move the point further away from the center.
85
+ * Modifies this line.
86
+ */
87
+ public moveStartPoint(amount: number): Line2D {
88
+ const p1 = this.movePointOnThisLine(this.start, amount);
89
+ this.start.copy(p1);
90
+
91
+ return this;
92
+ }
93
+
94
+ /**
95
+ * Moves end point on the line by the given amount. Plus values move the point further away from the center.
96
+ * Modifies this line.
97
+ */
98
+ public moveEndPoint(amount: number): Line2D {
99
+ const p2 = this.movePointOnThisLine(this.end, amount);
100
+ this.end.copy(p2);
101
+
102
+ return this;
103
+ }
104
+
105
+ private movePointOnThisLine(point: Point2, amount: number): Vec2 {
106
+ const vec = new Vector2(this.center.x - point.x, this.center.y - point.y);
107
+ const length = vec.length();
108
+ vec.normalize().multiplyScalar(length + amount);
109
+
110
+ return new Vec2(this.center.x - vec.x,this.center.y - vec.y);
111
+ }
112
+
113
+ /**
114
+ * Set the length of this line. Center and direction remain unchanged.
115
+ * Modifies this line.
116
+ * @param l
117
+ */
118
+ public set length(l: number) {
119
+ const length = this.length;
120
+ this.resize(l - length);
121
+ }
122
+
123
+ public get length(): number {
124
+ return this.start.distanceTo(this.end);
125
+ }
126
+
127
+ /**
128
+ * Set the length of this line. Center and direction remain unchanged.
129
+ * @param length
130
+ */
131
+ public setLength(length: number): this {
132
+ this.length = length;
133
+ return this;
134
+ }
135
+
136
+ /**
137
+ * Returns the start and end points of the line as an array.
138
+ * Endpoints are not cloned.
139
+ */
140
+ public get endpoints(): Vec2[] {
141
+ return [this.start, this.end];
142
+ }
143
+
144
+ /**
145
+ * Returns the direction of this line.
146
+ */
147
+ public get direction(): Vec2 {
148
+ return this.end.clone().sub(this.start).normalize();
149
+ }
150
+
151
+ /**
152
+ * Inverts the direction of the line.
153
+ * Modifies this line.
154
+ */
155
+ public flip(): Line2D {
156
+ const temp = this.start.clone();
157
+ this.start.copy(this.end);
158
+ this.end.copy(temp);
159
+
160
+ return this;
161
+ }
162
+
163
+ /**
164
+ * Rotates the line around the center by the given angle in radians.
165
+ * Modifies this line.
166
+ * @param radians Positive values rotate counter-clockwise.
167
+ * @param center
168
+ */
169
+ public rotate(radians: number, center: Vec2 = this.center): Line2D {
170
+ this.start.rotateAround(center, radians);
171
+ this.end.rotateAround(center, radians);
172
+
173
+ return this;
174
+ }
175
+
176
+ /**
177
+ * Move the line by the given vector.
178
+ * Modifies this line.
179
+ */
180
+ public translate(value: Point2): Line2D {
181
+ this.start.x += value.x;
182
+ this.start.y += value.y;
183
+ this.end.x += value.x;
184
+ this.end.y += value.y;
185
+
186
+ return this;
187
+ }
188
+
189
+ /**
190
+ * Move the line to its left by the given amount.
191
+ * Modifies this line.
192
+ */
193
+ public translateLeft(amount: number): Line2D {
194
+ const translation = this.direction.rotateAround(new Vec2(), -Math.PI / 2).multiplyScalar(amount);
195
+ return this.translate(translation);
196
+ }
197
+
198
+ /**
199
+ * Move the line to its right by the given amount.
200
+ * Modifies this line.
201
+ */
202
+ public translateRight(amount: number): Line2D {
203
+ const translation = this.direction.rotateAround(new Vec2(), Math.PI / 2).multiplyScalar(amount);
204
+ return this.translate(translation);
205
+ }
206
+
207
+ /**
208
+ * Returns true when the point is actually inside the (finite) line segment.
209
+ * https://jsfiddle.net/c06zdxtL/2/
210
+ * https://stackoverflow.com/questions/6865832/detecting-if-a-point-is-of-a-line-segment/6877674
211
+ * @param point: Point2
212
+ */
213
+ public isPointOnLineSection(point: Point2): boolean {
214
+ if (!this.isPointOnInfiniteLine(point)) {
215
+ return false;
216
+ }
217
+
218
+ return this.isPointBesideLineSection(point);
219
+ }
220
+
221
+ /**
222
+ * Returns true when the point is beside the line **segment** and within the maxDistance.
223
+ * @param point
224
+ * @param maxDistance
225
+ */
226
+ public isPointCloseToAndBesideLineSection(point: Point2, maxDistance: number): boolean {
227
+ const distance = this.distanceToPointOnInfiniteLine(point);
228
+ return distance <= maxDistance && this.isPointBesideLineSection(point);
229
+ }
230
+
231
+ /**
232
+ * Returns true when the point is beside the line **segment**
233
+ * @param point
234
+ */
235
+ public isPointBesideLineSection(point: Point2): boolean {
236
+ 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)));
237
+ if (l2 == 0) return false;
238
+ 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;
239
+
240
+ return (0 <= r) && (r <= 1);
241
+ }
242
+
243
+ /**
244
+ * Returns true when the point is on the **infinite** line.
245
+ * @param point
246
+ */
247
+ public isPointOnInfiniteLine(point: Point2): boolean {
248
+ return (point.y - this.start.y) * (this.end.x - this.start.x) === (this.end.y - this.start.y) * (point.x - this.start.x);
249
+ }
250
+
251
+ /**
252
+ * Returns true if other line is collinear and overlaps or at least touching this line.
253
+ * @param other
254
+ */
255
+ public isCollinearWithTouchOrOverlap(other: Line2D): boolean {
256
+ if (!this.isPointOnInfiniteLine(other.start) || !this.isPointOnInfiniteLine(other.end)) {
257
+ return false;
258
+ }
259
+
260
+ return this.isPointOnLineSection(other.start) || this.isPointOnLineSection(other.end) ||
261
+ other.isPointOnLineSection(this.start) || other.isPointOnLineSection(this.end);
262
+ }
263
+
264
+ /**
265
+ * Returns true if there is any overlap between this line and the @other line section.
266
+ */
267
+ public overlaps(other: Line2D): boolean {
268
+ if (!this.isCollinearWithTouchOrOverlap(other)) {
269
+ return false;
270
+ }
271
+
272
+ if (this.start.equals(other.start) && this.end.equals(other.end)) {
273
+ return true;
274
+ }
275
+
276
+ return !this.start.equals(other.end) && !this.end.equals(other.start);
277
+ }
278
+
279
+ /**
280
+ * Logical AND of this and the other line section.
281
+ * @param other
282
+ */
283
+ public getOverlap(other: Line2D): Line2D {
284
+ if (!this.overlaps(other)) {
285
+ return null;
286
+ }
287
+
288
+ if (this.equals(other)) {
289
+ return this.clone();
290
+ }
291
+
292
+ const points = [
293
+ [this.start, this.end].filter(thisPoint => other.isPointOnLineSection(thisPoint)),
294
+ [other.start, other.end].filter(otherPoint => this.isPointOnLineSection(otherPoint))
295
+ ].flat();
296
+
297
+ if (points.length !== 2) {
298
+ return null;
299
+ }
300
+
301
+ const overlap = Line2D.fromPoints(points[0], points[1]);
302
+ if (overlap.direction.manhattanDistanceTo(this.direction) > Number.EPSILON) {
303
+ overlap.flip();
304
+ }
305
+
306
+ return overlap;
307
+ }
308
+
309
+ /**
310
+ * Joins a copy of @line with the @other line.
311
+ * Other must be parallel to this line.
312
+ * Returns null if there is no overlap
313
+ * Clones the line, does not modify.
314
+ * @param line
315
+ * @param other
316
+ */
317
+ public static joinLine(line: Line2D, other: Line2D): Line2D {
318
+ if (!line.isCollinearWithTouchOrOverlap(other)) {
319
+ return null;
320
+ }
321
+
322
+ const p1 = !line.isPointOnLineSection(other.start) ? other.start : line.start;
323
+ const p2 = !line.isPointOnLineSection(other.end) ? other.end : line.end;
324
+
325
+ return new Line2D(p1.clone(), p2.clone(), line.index);
326
+ }
327
+
328
+ /**
329
+ * Joins provided lines into several joined lines.
330
+ * Lines must be parallel for joining.
331
+ * Clone the lines, does not modify.
332
+ * @param lines
333
+ */
334
+ public static joinLines(lines: Line2D[]): Line2D[] {
335
+ if (lines.length < 2) {
336
+ return lines.map(x => x.clone());
337
+ }
338
+
339
+ const toProcess = lines.slice();
340
+ const result: Line2D[] = [];
341
+
342
+ while (toProcess.length > 0) {
343
+
344
+ const current = toProcess.pop();
345
+ let joined = false;
346
+
347
+ for (let i = 0; i < result.length; i++) {
348
+ const other = result[i];
349
+ const joinedLine = Line2D.joinLine(current, other);
350
+ if (joinedLine) {
351
+ result[i] = joinedLine;
352
+ joined = true;
353
+ break;
354
+ }
355
+ }
356
+
357
+ if (!joined) {
358
+ result.push(current.clone());
359
+ }
360
+ }
361
+
362
+ return result;
363
+ }
364
+
365
+ /**
366
+ * Divides the Line3D into a number of segments of the given length.
367
+ * Clone the line, does not modify.
368
+ * @param maxSegmentLength number
369
+ */
370
+ public chunk(maxSegmentLength: number): Line2D[] {
371
+ const source = this.clone();
372
+ const result: Line2D[] = [];
373
+ while (source.length > maxSegmentLength) {
374
+ const chunk = source.clone();
375
+ chunk.moveEndPoint(-(chunk.length - maxSegmentLength));
376
+ result.push(chunk);
377
+ source.start = chunk.end.clone();
378
+ }
379
+ result.push(source);
380
+ return result;
381
+ }
382
+
383
+ /**
384
+ * Returns the closest point parameter on the **infinite** line to the given point.
385
+ * @param point
386
+ */
387
+ public closestPointToPointParameterOnInfiniteLine(point: Vector2): number {
388
+ const startP = new Vec2().subVectors(point, this.start);
389
+ const startEnd = new Vec2().subVectors(this.end, this.start);
390
+
391
+ const startEnd2 = startEnd.dot(startEnd);
392
+ const startEnd_startP = startEnd.dot(startP);
393
+
394
+ return startEnd_startP / startEnd2;
395
+ }
396
+
397
+ /**
398
+ * Returns the closest point on the **infinite** line to the given point.
399
+ * @param point
400
+ */
401
+ public closestPointOnInfiniteLine(point: Vector2): Vec2 {
402
+ const t = this.closestPointToPointParameterOnInfiniteLine(point);
403
+ return new Vec2().subVectors(this.end, this.start).multiplyScalar(t).add(this.start);
404
+ }
405
+
406
+ /**
407
+ * Returns the closest point on the line **section** to the given point.
408
+ * @param point
409
+ */
410
+ public closestPointOnLine(point: Vector2): Vec2 {
411
+ const closestPoint = this.closestPointOnInfiniteLine(point);
412
+ if (this.isPointOnLineSection(closestPoint)) {
413
+ return closestPoint;
414
+ }
415
+
416
+ return closestPoint.distanceTo(this.start) < closestPoint.distanceTo(this.end) ? this.start : this.end;
417
+ }
418
+
419
+ /**
420
+ * Returns the distance between the **infinite** line and the point.
421
+ * @param point
422
+ */
423
+ public distanceToPointOnInfiniteLine(point: Point2): number {
424
+ 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)));
425
+ if (l2 == 0) return Infinity;
426
+ 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;
427
+ return Math.abs(s) * Math.sqrt(l2);
428
+ }
429
+
430
+ /**
431
+ * Returns lines that are the result of clipping @source line by the @clips.
432
+ * Clips must be parallel to this line.
433
+ * Clones the line, does not modify this.
434
+ * @param source
435
+ * @param clips
436
+ */
437
+ public static clipLines(source: Line2D, clips: Line2D[]): Line2D[] {
438
+ if (!clips || clips.length === 0) return [source];
439
+
440
+ clips = clips.map(c => {
441
+ const copy = c.clone();
442
+ if (copy.direction.manhattanDistanceTo(source.direction) > Number.EPSILON) {
443
+ copy.flip();
444
+ }
445
+ return copy;
446
+ });
447
+
448
+ const free: Line2D[] = [];
449
+ const sources = [source];
450
+
451
+ while (sources.length > 0) {
452
+
453
+ let isFree = true;
454
+
455
+ const tested = sources.pop();
456
+
457
+ for (const cover of clips) {
458
+
459
+ if (tested.overlaps(cover)) {
460
+ isFree = false;
461
+ const subtracted = this.subtractSingle(tested, cover);
462
+ sources.push(...subtracted);
463
+ break;
464
+ }
465
+ }
466
+
467
+ if (isFree) free.push(tested);
468
+ }
469
+
470
+ return this.order(source, free);
471
+ }
472
+
473
+ /**
474
+ * Returns the original line section split into two parts, if the line **sections** overlap, otherwise null
475
+ */
476
+ public splitAtIntersection(other: Line2D, tolerance: number = 0): Line2D[] {
477
+ const intersection = this.intersect(other);
478
+ if (intersection) {
479
+ if (this.isPointCloseToAndBesideLineSection(intersection, tolerance) && other.isPointCloseToAndBesideLineSection(intersection, tolerance)) {
480
+ return [
481
+ Line2D.fromPoints(this.start, intersection),
482
+ Line2D.fromPoints(intersection, this.end)
483
+ ];
484
+ }
485
+ }
486
+
487
+ return null;
488
+ }
489
+
490
+ /**
491
+ * If lines **sections** overlap, returns the original line section split into two parts, sorted by length
492
+ * Else, if the **infinite** lines intersect, returns a new line extended to the intersection point
493
+ * Otherwise, null if the lines are parallel and do not intersect
494
+ */
495
+ public splitAtOrExtendToIntersection(other: Line2D): Line2D[] {
496
+ const intersection = this.intersect(other);
497
+ if (intersection) {
498
+ return [
499
+ Line2D.fromPoints(this.start, intersection),
500
+ Line2D.fromPoints(intersection, this.end)
501
+ ].filter(x => x.length > Number.EPSILON).sort((a, b) => a.length - b.length);
502
+ }
503
+
504
+ return null;
505
+ }
506
+
507
+ private static order(source: Line2D, lines: Line2D[]): Line2D[] {
508
+ if (source.start.x < source.end.x) {
509
+ lines.sort((a, b) => a.start.x - b.start.x);
510
+ } else if (source.start.x > source.end.x) {
511
+ lines.sort((a, b) => b.start.x - a.start.x);
512
+ }
513
+
514
+ if (source.start.y < source.end.y) {
515
+ lines.sort((a, b) => a.start.y - b.start.y);
516
+ } else if (source.start.y > source.end.y) {
517
+ lines.sort((a, b) => b.start.y - a.start.y);
518
+ }
519
+
520
+ return lines;
521
+ }
522
+
523
+ private static subtractSingle(source: Line2D, cover: Line2D): Line2D[] {
524
+ const left = source.clone();
525
+ left.end.copy(cover.start);
526
+
527
+ const right = source.clone();
528
+ right.start.copy(cover.end);
529
+
530
+ return [left, right].filter(x => x.length > 1 && x.direction.manhattanDistanceTo(source.direction) < Number.EPSILON);
531
+ }
532
+
533
+ /**
534
+ * If other line is not contained within this line, the excess is trimmed.
535
+ * Does not create a copy. Provided line is modified.
536
+ * @param lineToTrim
537
+ */
538
+ public trimExcess(lineToTrim: Line2D): void {
539
+ if (!this.isCollinearWithTouchOrOverlap(lineToTrim)) {
540
+ return;
541
+ }
542
+
543
+ if (!this.isPointOnLineSection(lineToTrim.start)) {
544
+ const closest = this.closestPointOnLine(lineToTrim.start);
545
+ lineToTrim.start.copy(closest);
546
+ }
547
+
548
+ if (!this.isPointOnLineSection(lineToTrim.end)) {
549
+ const closest = this.closestPointOnLine(lineToTrim.end);
550
+ lineToTrim.end.copy(closest);
551
+ }
552
+ }
553
+
554
+ /**
555
+ * If other line is shorter than this, endpoints are moved to extend other
556
+ * Does not create a copy. Provided line is modified.
557
+ * @param lineToExtend
558
+ * @param tolerance
559
+ */
560
+ public extendToEnds(lineToExtend: Line2D, tolerance: number): void {
561
+ if (!this.isCollinearWithTouchOrOverlap(lineToExtend)) {
562
+ console.log("Can't clip, lines that are not collinear with touch or overlap");
563
+ return;
564
+ }
565
+
566
+ if (this.start.distanceTo(lineToExtend.start) <= tolerance) {
567
+ lineToExtend.start.copy(this.start);
568
+ }
569
+
570
+ if (this.end.distanceTo(lineToExtend.end) <= tolerance) {
571
+ lineToExtend.end.copy(this.end);
572
+ }
573
+ }
574
+
575
+ /**
576
+ * If there is an intersection between this and other, this line is extended to the intersection point. Lines are assumed to be infinite.
577
+ * Modifies this line.
578
+ * @param other
579
+ * @param maxDistanceToIntersection
580
+ */
581
+ public extendToOrTrimAtIntersection(other: Line2D, maxDistanceToIntersection: number = Number.MAX_VALUE): Line2D {
582
+ const intersection = this.intersect(other);
583
+
584
+ if (intersection) {
585
+ const distanceToStart = this.start.distanceTo(intersection);
586
+ const distanceToEnd = this.end.distanceTo(intersection);
587
+
588
+ if (distanceToStart <= maxDistanceToIntersection || distanceToEnd <= maxDistanceToIntersection) {
589
+ if (distanceToStart < distanceToEnd) {
590
+ this.start.copy(intersection);
591
+
592
+ } else {
593
+ this.end.copy(intersection);
594
+ }
595
+ }
596
+ }
597
+
598
+ return this;
599
+ }
600
+
601
+ /**
602
+ * Returns the intersection point of two lines. The lines are assumed to be infinite.
603
+ */
604
+ public intersect(other: Line2D): Vec2 {
605
+ // Check if none of the lines are of length 0
606
+ 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)) {
607
+ return null;
608
+ }
609
+
610
+ 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));
611
+
612
+ // Lines are parallel
613
+ if (denominator === 0) {
614
+ return null;
615
+ }
616
+
617
+ 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;
618
+
619
+ // Return an object with the x and y coordinates of the intersection
620
+ const x = this.start.x + ua * (this.end.x - this.start.x);
621
+ const y = this.start.y + ua * (this.end.y - this.start.y);
622
+
623
+ return new Vec2(x, y);
624
+ }
625
+
626
+ /**
627
+ * Check that the infinite lines intersect and that they are in the specified angle to each other
628
+ * @param other Line
629
+ * @param expectedAngleInRads number
630
+ */
631
+ public hasIntersectionWithAngle(other: Line2D, expectedAngleInRads: number): Vec2 {
632
+ const angle = this.direction.angle();
633
+ const otherAngle = other.direction.angle();
634
+ const actualAngle = Math.abs(angle - otherAngle);
635
+
636
+ if (Math.abs(actualAngle - expectedAngleInRads) < Number.EPSILON) {
637
+ const intersection = this.intersect(other);
638
+ if (intersection && this.isPointOnLineSection(intersection) && other.isPointOnLineSection(intersection)) {
639
+
640
+ return intersection;
641
+ }
642
+ }
643
+
644
+ return null;
645
+ }
646
+
647
+ /**
648
+ * Deep clone of this line
649
+ */
650
+ public clone(): Line2D {
651
+ return new Line2D(this.start.clone(), this.end.clone(), this.index);
652
+ }
653
+
654
+ public toString(): string {
655
+ return `Line(${this.start.x}, ${this.start.y}, ${this.end.x}, ${this.end.y})`;
656
+ }
657
+
658
+ public equals(other: Line2D): boolean {
659
+ return !!other && this.start.equals(other.start) && this.end.equals(other.end);
660
+ }
661
661
  }