@js-draw/math 1.0.0 → 1.0.2

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.
Files changed (61) hide show
  1. package/LICENSE +21 -0
  2. package/dist/cjs/Color4.test.d.ts +1 -0
  3. package/dist/cjs/Mat33.test.d.ts +1 -0
  4. package/dist/cjs/Vec2.test.d.ts +1 -0
  5. package/dist/cjs/Vec3.test.d.ts +1 -0
  6. package/dist/cjs/polynomial/solveQuadratic.test.d.ts +1 -0
  7. package/dist/cjs/rounding.test.d.ts +1 -0
  8. package/dist/cjs/shapes/LineSegment2.test.d.ts +1 -0
  9. package/dist/cjs/shapes/Path.fromString.test.d.ts +1 -0
  10. package/dist/cjs/shapes/Path.test.d.ts +1 -0
  11. package/dist/cjs/shapes/Path.toString.test.d.ts +1 -0
  12. package/dist/cjs/shapes/QuadraticBezier.test.d.ts +1 -0
  13. package/dist/cjs/shapes/Rect2.test.d.ts +1 -0
  14. package/dist/cjs/shapes/Triangle.test.d.ts +1 -0
  15. package/dist/mjs/Color4.test.d.ts +1 -0
  16. package/dist/mjs/Mat33.test.d.ts +1 -0
  17. package/dist/mjs/Vec2.test.d.ts +1 -0
  18. package/dist/mjs/Vec3.test.d.ts +1 -0
  19. package/dist/mjs/polynomial/solveQuadratic.test.d.ts +1 -0
  20. package/dist/mjs/rounding.test.d.ts +1 -0
  21. package/dist/mjs/shapes/LineSegment2.test.d.ts +1 -0
  22. package/dist/mjs/shapes/Path.fromString.test.d.ts +1 -0
  23. package/dist/mjs/shapes/Path.test.d.ts +1 -0
  24. package/dist/mjs/shapes/Path.toString.test.d.ts +1 -0
  25. package/dist/mjs/shapes/QuadraticBezier.test.d.ts +1 -0
  26. package/dist/mjs/shapes/Rect2.test.d.ts +1 -0
  27. package/dist/mjs/shapes/Triangle.test.d.ts +1 -0
  28. package/dist-test/test_imports/package-lock.json +13 -0
  29. package/dist-test/test_imports/package.json +12 -0
  30. package/dist-test/test_imports/test-imports.js +15 -0
  31. package/dist-test/test_imports/test-require.cjs +15 -0
  32. package/package.json +4 -3
  33. package/src/Color4.test.ts +52 -0
  34. package/src/Color4.ts +318 -0
  35. package/src/Mat33.test.ts +244 -0
  36. package/src/Mat33.ts +450 -0
  37. package/src/Vec2.test.ts +30 -0
  38. package/src/Vec2.ts +49 -0
  39. package/src/Vec3.test.ts +51 -0
  40. package/src/Vec3.ts +245 -0
  41. package/src/lib.ts +42 -0
  42. package/src/polynomial/solveQuadratic.test.ts +39 -0
  43. package/src/polynomial/solveQuadratic.ts +43 -0
  44. package/src/rounding.test.ts +65 -0
  45. package/src/rounding.ts +167 -0
  46. package/src/shapes/Abstract2DShape.ts +63 -0
  47. package/src/shapes/BezierJSWrapper.ts +93 -0
  48. package/src/shapes/CubicBezier.ts +35 -0
  49. package/src/shapes/LineSegment2.test.ts +99 -0
  50. package/src/shapes/LineSegment2.ts +232 -0
  51. package/src/shapes/Path.fromString.test.ts +223 -0
  52. package/src/shapes/Path.test.ts +309 -0
  53. package/src/shapes/Path.toString.test.ts +77 -0
  54. package/src/shapes/Path.ts +963 -0
  55. package/src/shapes/PointShape2D.ts +33 -0
  56. package/src/shapes/QuadraticBezier.test.ts +31 -0
  57. package/src/shapes/QuadraticBezier.ts +142 -0
  58. package/src/shapes/Rect2.test.ts +209 -0
  59. package/src/shapes/Rect2.ts +346 -0
  60. package/src/shapes/Triangle.test.ts +61 -0
  61. package/src/shapes/Triangle.ts +139 -0
@@ -0,0 +1,63 @@
1
+ import LineSegment2 from './LineSegment2';
2
+ import { Point2 } from '../Vec2';
3
+ import Rect2 from './Rect2';
4
+
5
+ abstract class Abstract2DShape {
6
+ protected static readonly smallValue = 1e-12;
7
+
8
+ /**
9
+ * @returns the distance from `point` to this shape. If `point` is within this shape,
10
+ * this returns the distance from `point` to the edge of this shape.
11
+ *
12
+ * @see {@link signedDistance}
13
+ */
14
+ public distance(point: Point2) {
15
+ return Math.abs(this.signedDistance(point));
16
+ }
17
+
18
+ /**
19
+ * Computes the [signed distance function](https://en.wikipedia.org/wiki/Signed_distance_function)
20
+ * for this shape.
21
+ */
22
+ public abstract signedDistance(point: Point2): number;
23
+
24
+ /**
25
+ * @returns points at which this shape intersects the given `lineSegment`.
26
+ *
27
+ * If this is a closed shape, returns points where the given `lineSegment` intersects
28
+ * the **boundary** of this.
29
+ */
30
+ public abstract intersectsLineSegment(lineSegment: LineSegment2): Point2[];
31
+
32
+ /**
33
+ * Returns `true` if and only if the given `point` is contained within this shape.
34
+ *
35
+ * `epsilon` is a small number used to counteract floating point error. Thus, if
36
+ * `point` is within `epsilon` of the inside of this shape, `containsPoint` may also
37
+ * return `true`.
38
+ *
39
+ * The default implementation relies on `signedDistance`.
40
+ * Subclasses may override this method to provide a more efficient implementation.
41
+ */
42
+ public containsPoint(point: Point2, epsilon: number = Abstract2DShape.smallValue): boolean {
43
+ return this.signedDistance(point) < epsilon;
44
+ }
45
+
46
+ /**
47
+ * Returns a bounding box that precisely fits the content of this shape.
48
+ */
49
+ public abstract getTightBoundingBox(): Rect2;
50
+
51
+ /**
52
+ * Returns a bounding box that **loosely** fits the content of this shape.
53
+ *
54
+ * The result of this call can be larger than the result of {@link getTightBoundingBox},
55
+ * **but should not be smaller**. Thus, a call to `getLooseBoundingBox` can be significantly
56
+ * faster than a call to {@link getTightBoundingBox} for some shapes.
57
+ */
58
+ public getLooseBoundingBox(): Rect2 {
59
+ return this.getTightBoundingBox();
60
+ }
61
+ }
62
+
63
+ export default Abstract2DShape;
@@ -0,0 +1,93 @@
1
+ import { Bezier } from 'bezier-js';
2
+ import { Point2, Vec2 } from '../Vec2';
3
+ import Abstract2DShape from './Abstract2DShape';
4
+ import LineSegment2 from './LineSegment2';
5
+ import Rect2 from './Rect2';
6
+
7
+ /**
8
+ * A lazy-initializing wrapper around Bezier-js.
9
+ *
10
+ * Subclasses may override `at`, `derivativeAt`, and `normal` with functions
11
+ * that do not initialize a `bezier-js` `Bezier`.
12
+ *
13
+ * Do not use this class directly. It may be removed/replaced in a future release.
14
+ * @internal
15
+ */
16
+ abstract class BezierJSWrapper extends Abstract2DShape {
17
+ #bezierJs: Bezier|null = null;
18
+
19
+ /** Returns the start, control points, and end point of this Bézier. */
20
+ public abstract getPoints(): Point2[];
21
+
22
+ protected getBezier() {
23
+ if (!this.#bezierJs) {
24
+ this.#bezierJs = new Bezier(this.getPoints().map(p => p.xy));
25
+ }
26
+ return this.#bezierJs;
27
+ }
28
+
29
+ public override signedDistance(point: Point2): number {
30
+ // .d: Distance
31
+ return this.getBezier().project(point.xy).d!;
32
+ }
33
+
34
+ /**
35
+ * @returns the (more) exact distance from `point` to this.
36
+ *
37
+ * @see {@link approximateDistance}
38
+ */
39
+ public override distance(point: Point2) {
40
+ // A Bézier curve has no interior, thus, signed distance is the same as distance.
41
+ return this.signedDistance(point);
42
+ }
43
+
44
+ /**
45
+ * @returns the curve evaluated at `t`.
46
+ */
47
+ public at(t: number): Point2 {
48
+ return Vec2.ofXY(this.getBezier().get(t));
49
+ }
50
+
51
+ public derivativeAt(t: number): Point2 {
52
+ return Vec2.ofXY(this.getBezier().derivative(t));
53
+ }
54
+
55
+ public normal(t: number): Vec2 {
56
+ return Vec2.ofXY(this.getBezier().normal(t));
57
+ }
58
+
59
+ public override getTightBoundingBox(): Rect2 {
60
+ const bbox = this.getBezier().bbox();
61
+ const width = bbox.x.max - bbox.x.min;
62
+ const height = bbox.y.max - bbox.y.min;
63
+
64
+ return new Rect2(bbox.x.min, bbox.y.min, width, height);
65
+ }
66
+
67
+ public override intersectsLineSegment(line: LineSegment2): Point2[] {
68
+ const bezier = this.getBezier();
69
+
70
+ const intersectionPoints = bezier.intersects(line).map(t => {
71
+ // We're using the .intersects(line) function, which is documented
72
+ // to always return numbers. However, to satisfy the type checker (and
73
+ // possibly improperly-defined types),
74
+ if (typeof t === 'string') {
75
+ t = parseFloat(t);
76
+ }
77
+
78
+ const point = Vec2.ofXY(bezier.get(t));
79
+
80
+ // Ensure that the intersection is on the line segment
81
+ if (point.minus(line.p1).magnitude() > line.length
82
+ || point.minus(line.p2).magnitude() > line.length) {
83
+ return null;
84
+ }
85
+
86
+ return point;
87
+ }).filter(entry => entry !== null) as Point2[];
88
+
89
+ return intersectionPoints;
90
+ }
91
+ }
92
+
93
+ export default BezierJSWrapper;
@@ -0,0 +1,35 @@
1
+ import { Point2 } from '../Vec2';
2
+ import BezierJSWrapper from './BezierJSWrapper';
3
+ import Rect2 from './Rect2';
4
+
5
+ /**
6
+ * A wrapper around [`bezier-js`](https://github.com/Pomax/bezierjs)'s cubic Bezier.
7
+ */
8
+ class CubicBezier extends BezierJSWrapper {
9
+ public constructor(
10
+ // Start point
11
+ public readonly p0: Point2,
12
+
13
+ // Control point 1
14
+ public readonly p1: Point2,
15
+
16
+ // Control point 2
17
+ public readonly p2: Point2,
18
+
19
+ // End point
20
+ public readonly p3: Point2,
21
+ ) {
22
+ super();
23
+ }
24
+
25
+ public override getPoints() {
26
+ return [ this.p0, this.p1, this.p2, this.p3 ];
27
+ }
28
+
29
+ /** Returns an overestimate of this shape's bounding box. */
30
+ public override getLooseBoundingBox(): Rect2 {
31
+ return Rect2.bboxOf([ this.p0, this.p1, this.p2, this.p3 ]);
32
+ }
33
+ }
34
+
35
+ export default CubicBezier;
@@ -0,0 +1,99 @@
1
+ import LineSegment2 from './LineSegment2';
2
+ import { Vec2 } from '../Vec2';
3
+ import Mat33 from '../Mat33';
4
+
5
+
6
+ describe('Line2', () => {
7
+ it('x and y axes should intersect at (0, 0)', () => {
8
+ const xAxis = new LineSegment2(Vec2.of(-10, 0), Vec2.of(10, 0));
9
+ const yAxis = new LineSegment2(Vec2.of(0, -10), Vec2.of(0, 10));
10
+ expect(xAxis.intersection(yAxis)?.point).objEq(Vec2.zero);
11
+ expect(yAxis.intersection(xAxis)?.point).objEq(Vec2.zero);
12
+ });
13
+
14
+ it('y = -2x + 2 and y = 2x - 2 should intersect at (1,0)', () => {
15
+ // y = -4x + 2
16
+ const line1 = new LineSegment2(Vec2.of(0, 2), Vec2.of(1, -2));
17
+ // y = 4x - 2
18
+ const line2 = new LineSegment2(Vec2.of(0, -2), Vec2.of(1, 2));
19
+
20
+ expect(line1.intersection(line2)?.point).objEq(Vec2.of(0.5, 0));
21
+ expect(line2.intersection(line1)?.point).objEq(Vec2.of(0.5, 0));
22
+ });
23
+
24
+ it('line from (10, 10) to (-100, 10) should intersect with the y-axis at t = 10', () => {
25
+ const line1 = new LineSegment2(Vec2.of(10, 10), Vec2.of(-10, 10));
26
+ // y = 2x - 2
27
+ const line2 = new LineSegment2(Vec2.of(0, -2), Vec2.of(0, 200));
28
+
29
+ expect(line1.intersection(line2)?.point).objEq(Vec2.of(0, 10));
30
+
31
+ // t=10 implies 10 units along he line from (10, 10) to (-10, 10)
32
+ expect(line1.intersection(line2)?.t).toBe(10);
33
+
34
+ // Similarly, t = 12 implies 12 units above (0, -2) in the direction of (0, 200)
35
+ expect(line2.intersection(line1)?.t).toBe(12);
36
+ });
37
+
38
+ it('y=2 and y=0 should not intersect', () => {
39
+ const line1 = new LineSegment2(Vec2.of(-10, 2), Vec2.of(10, 2));
40
+ const line2 = new LineSegment2(Vec2.of(-10, 0), Vec2.of(10, 0));
41
+ expect(line1.intersection(line2)).toBeNull();
42
+ expect(line2.intersection(line1)).toBeNull();
43
+ });
44
+
45
+ it('x=2 and x=-1 should not intersect', () => {
46
+ const line1 = new LineSegment2(Vec2.of(2, -10), Vec2.of(2, 10));
47
+ const line2 = new LineSegment2(Vec2.of(-1, 10), Vec2.of(-1, -10));
48
+ expect(line1.intersection(line2)).toBeNull();
49
+ expect(line2.intersection(line1)).toBeNull();
50
+ });
51
+
52
+ it('Line from (0, 0) to (1, 0) should not intersect line from (1.1, 0) to (2, 0)', () => {
53
+ const line1 = new LineSegment2(Vec2.of(0, 0), Vec2.of(1, 0));
54
+ const line2 = new LineSegment2(Vec2.of(1.1, 0), Vec2.of(2, 0));
55
+ expect(line1.intersection(line2)).toBeNull();
56
+ expect(line2.intersection(line1)).toBeNull();
57
+ });
58
+
59
+ it('Line segment from (1, 1) to (3, 1) should have length 2', () => {
60
+ const segment = new LineSegment2(Vec2.of(1, 1), Vec2.of(3, 1));
61
+ expect(segment.length).toBe(2);
62
+ });
63
+
64
+ it('(769.612,221.037)->(770.387,224.962) should not intersect (763.359,223.667)->(763.5493, 223.667)', () => {
65
+ // Points taken from issue observed directly in editor
66
+ const p1 = Vec2.of(769.6126045442547, 221.037877485765);
67
+ const p2 = Vec2.of(770.3873954557453, 224.962122514235);
68
+ const p3 = Vec2.of( 763.3590010920082, 223.66723995850086);
69
+ const p4 = Vec2.of(763.5494167642871, 223.66723995850086);
70
+
71
+ const line1 = new LineSegment2(p1, p2);
72
+ const line2 = new LineSegment2(p3, p4);
73
+ expect(line1.intersection(line2)).toBeNull();
74
+ expect(line2.intersection(line1)).toBeNull();
75
+ });
76
+
77
+ it('Closest point to (0,0) on the line x = 1 should be (1,0)', () => {
78
+ const line = new LineSegment2(Vec2.of(1, 100), Vec2.of(1, -100));
79
+ expect(line.closestPointTo(Vec2.zero)).objEq(Vec2.of(1, 0));
80
+ });
81
+
82
+ it('Closest point from (-1,-2) to segment((1,1) -> (2,4)) should be (1,1)', () => {
83
+ const line = new LineSegment2(Vec2.of(1, 1), Vec2.of(2, 4));
84
+ expect(line.closestPointTo(Vec2.of(-1, -2))).objEq(Vec2.of(1, 1));
85
+ });
86
+
87
+ it('Closest point from (5,8) to segment((1,1) -> (2,4)) should be (2,4)', () => {
88
+ const line = new LineSegment2(Vec2.of(1, 1), Vec2.of(2, 4));
89
+ expect(line.closestPointTo(Vec2.of(5, 8))).objEq(Vec2.of(2, 4));
90
+ });
91
+
92
+ it('Should translate when translated by a translation matrix', () => {
93
+ const line = new LineSegment2(Vec2.of(-1, 1), Vec2.of(2, 100));
94
+ expect(line.transformedBy(Mat33.translation(Vec2.of(1, -2)))).toMatchObject({
95
+ p1: Vec2.of(0, -1),
96
+ p2: Vec2.of(3, 98),
97
+ });
98
+ });
99
+ });
@@ -0,0 +1,232 @@
1
+ import Mat33 from '../Mat33';
2
+ import Rect2 from './Rect2';
3
+ import { Vec2, Point2 } from '../Vec2';
4
+ import Abstract2DShape from './Abstract2DShape';
5
+
6
+ interface IntersectionResult {
7
+ point: Point2;
8
+ t: number;
9
+ }
10
+
11
+ /** Represents a line segment. A `LineSegment2` is immutable. */
12
+ export class LineSegment2 extends Abstract2DShape {
13
+ // invariant: ||direction|| = 1
14
+
15
+ /**
16
+ * The **unit** direction vector of this line segment, from
17
+ * `point1` to `point2`.
18
+ *
19
+ * In other words, `direction` is `point2.minus(point1).normalized()`
20
+ * (perhaps except when `point1` is equal to `point2`).
21
+ */
22
+ public readonly direction: Vec2;
23
+
24
+ /** The distance between `point1` and `point2`. */
25
+ public readonly length: number;
26
+
27
+ /** The bounding box of this line segment. */
28
+ public readonly bbox;
29
+
30
+ /** Creates a new `LineSegment2` from its endpoints. */
31
+ public constructor(
32
+ private readonly point1: Point2,
33
+ private readonly point2: Point2
34
+ ) {
35
+ super();
36
+
37
+ this.bbox = Rect2.bboxOf([point1, point2]);
38
+
39
+ this.direction = point2.minus(point1);
40
+ this.length = this.direction.magnitude();
41
+
42
+ // Normalize
43
+ if (this.length > 0) {
44
+ this.direction = this.direction.times(1 / this.length);
45
+ }
46
+ }
47
+
48
+ // Accessors to make LineSegment2 compatible with bezier-js's
49
+ // interface
50
+
51
+ /** Alias for `point1`. */
52
+ public get p1(): Point2 {
53
+ return this.point1;
54
+ }
55
+
56
+ /** Alias for `point2`. */
57
+ public get p2(): Point2 {
58
+ return this.point2;
59
+ }
60
+
61
+ /**
62
+ * Gets a point a distance `t` along this line.
63
+ *
64
+ * @deprecated
65
+ */
66
+ public get(t: number): Point2 {
67
+ return this.point1.plus(this.direction.times(t));
68
+ }
69
+
70
+ /**
71
+ * Returns a point a fraction, `t`, along this line segment.
72
+ * Thus, `segment.at(0)` returns `segment.p1` and `segment.at(1)` returns
73
+ * `segment.p2`.
74
+ *
75
+ * `t` should be in `[0, 1]`.
76
+ */
77
+ public at(t: number): Point2 {
78
+ return this.get(t * this.length);
79
+ }
80
+
81
+ public intersection(other: LineSegment2): IntersectionResult|null {
82
+ // We want x₁(t) = x₂(t) and y₁(t) = y₂(t)
83
+ // Observe that
84
+ // x = this.point1.x + this.direction.x · t₁
85
+ // = other.point1.x + other.direction.x · t₂
86
+ // Thus,
87
+ // t₁ = (x - this.point1.x) / this.direction.x
88
+ // = (y - this.point1.y) / this.direction.y
89
+ // and
90
+ // t₂ = (x - other.point1.x) / other.direction.x
91
+ // (and similarly for y)
92
+ //
93
+ // Letting o₁ₓ = this.point1.x, o₂ₓ = other.point1.x,
94
+ // d₁ᵧ = this.direction.y, ...
95
+ //
96
+ // We can substitute these into the equations for y:
97
+ // y = o₁ᵧ + d₁ᵧ · (x - o₁ₓ) / d₁ₓ
98
+ // = o₂ᵧ + d₂ᵧ · (x - o₂ₓ) / d₂ₓ
99
+ // ⇒ o₁ᵧ - o₂ᵧ = d₂ᵧ · (x - o₂ₓ) / d₂ₓ - d₁ᵧ · (x - o₁ₓ) / d₁ₓ
100
+ // = (d₂ᵧ/d₂ₓ)(x) - (d₂ᵧ/d₂ₓ)(o₂ₓ) - (d₁ᵧ/d₁ₓ)(x) + (d₁ᵧ/d₁ₓ)(o₁ₓ)
101
+ // = (x)(d₂ᵧ/d₂ₓ - d₁ᵧ/d₁ₓ) - (d₂ᵧ/d₂ₓ)(o₂ₓ) + (d₁ᵧ/d₁ₓ)(o₁ₓ)
102
+ // ⇒ (x)(d₂ᵧ/d₂ₓ - d₁ᵧ/d₁ₓ) = o₁ᵧ - o₂ᵧ + (d₂ᵧ/d₂ₓ)(o₂ₓ) - (d₁ᵧ/d₁ₓ)(o₁ₓ)
103
+ // ⇒ x = (o₁ᵧ - o₂ᵧ + (d₂ᵧ/d₂ₓ)(o₂ₓ) - (d₁ᵧ/d₁ₓ)(o₁ₓ))/(d₂ᵧ/d₂ₓ - d₁ᵧ/d₁ₓ)
104
+ // = (d₁ₓd₂ₓ)(o₁ᵧ - o₂ᵧ + (d₂ᵧ/d₂ₓ)(o₂ₓ) - (d₁ᵧ/d₁ₓ)(o₁ₓ))/(d₂ᵧd₁ₓ - d₁ᵧd₂ₓ)
105
+ // = ((o₁ᵧ - o₂ᵧ)((d₁ₓd₂ₓ)) + (d₂ᵧd₁ₓ)(o₂ₓ) - (d₁ᵧd₂ₓ)(o₁ₓ))/(d₂ᵧd₁ₓ - d₁ᵧd₂ₓ)
106
+ // ⇒ y = o₁ᵧ + d₁ᵧ · (x - o₁ₓ) / d₁ₓ = ...
107
+ let resultPoint, resultT;
108
+ if (this.direction.x === 0) {
109
+ // Vertical line: Where does the other have x = this.point1.x?
110
+ // x = o₁ₓ = o₂ₓ + d₂ₓ · (y - o₂ᵧ) / d₂ᵧ
111
+ // ⇒ (o₁ₓ - o₂ₓ)(d₂ᵧ/d₂ₓ) + o₂ᵧ = y
112
+
113
+ // Avoid division by zero
114
+ if (other.direction.x === 0 || this.direction.y === 0) {
115
+ return null;
116
+ }
117
+
118
+ const xIntersect = this.point1.x;
119
+ const yIntersect =
120
+ (this.point1.x - other.point1.x) * other.direction.y / other.direction.x + other.point1.y;
121
+ resultPoint = Vec2.of(xIntersect, yIntersect);
122
+ resultT = (yIntersect - this.point1.y) / this.direction.y;
123
+ } else {
124
+ // From above,
125
+ // x = ((o₁ᵧ - o₂ᵧ)(d₁ₓd₂ₓ) + (d₂ᵧd₁ₓ)(o₂ₓ) - (d₁ᵧd₂ₓ)(o₁ₓ))/(d₂ᵧd₁ₓ - d₁ᵧd₂ₓ)
126
+ const numerator = (
127
+ (this.point1.y - other.point1.y) * this.direction.x * other.direction.x
128
+ + this.direction.x * other.direction.y * other.point1.x
129
+ - this.direction.y * other.direction.x * this.point1.x
130
+ );
131
+ const denominator = (
132
+ other.direction.y * this.direction.x
133
+ - this.direction.y * other.direction.x
134
+ );
135
+
136
+ // Avoid dividing by zero. It means there is no intersection
137
+ if (denominator === 0) {
138
+ return null;
139
+ }
140
+
141
+ const xIntersect = numerator / denominator;
142
+ const t1 = (xIntersect - this.point1.x) / this.direction.x;
143
+ const yIntersect = this.point1.y + this.direction.y * t1;
144
+ resultPoint = Vec2.of(xIntersect, yIntersect);
145
+ resultT = (xIntersect - this.point1.x) / this.direction.x;
146
+ }
147
+
148
+ // Ensure the result is in this/the other segment.
149
+ const resultToP1 = resultPoint.minus(this.point1).magnitude();
150
+ const resultToP2 = resultPoint.minus(this.point2).magnitude();
151
+ const resultToP3 = resultPoint.minus(other.point1).magnitude();
152
+ const resultToP4 = resultPoint.minus(other.point2).magnitude();
153
+ if (resultToP1 > this.length
154
+ || resultToP2 > this.length
155
+ || resultToP3 > other.length
156
+ || resultToP4 > other.length) {
157
+ return null;
158
+ }
159
+
160
+ return {
161
+ point: resultPoint,
162
+ t: resultT,
163
+ };
164
+ }
165
+
166
+ public intersects(other: LineSegment2) {
167
+ return this.intersection(other) !== null;
168
+ }
169
+
170
+ /**
171
+ * Returns the points at which this line segment intersects the
172
+ * given line segment.
173
+ *
174
+ * Note that {@link intersects} returns *whether* this line segment intersects another
175
+ * line segment. This method, by contrast, returns **the point** at which the intersection
176
+ * occurs, if such a point exists.
177
+ */
178
+ public override intersectsLineSegment(lineSegment: LineSegment2) {
179
+ const intersection = this.intersection(lineSegment);
180
+
181
+ if (intersection) {
182
+ return [ intersection.point ];
183
+ }
184
+ return [];
185
+ }
186
+
187
+ // Returns the closest point on this to [target]
188
+ public closestPointTo(target: Point2) {
189
+ // Distance from P1 along this' direction.
190
+ const projectedDistFromP1 = target.minus(this.p1).dot(this.direction);
191
+ const projectedDistFromP2 = this.length - projectedDistFromP1;
192
+
193
+ const projection = this.p1.plus(this.direction.times(projectedDistFromP1));
194
+
195
+ if (projectedDistFromP1 > 0 && projectedDistFromP1 < this.length) {
196
+ return projection;
197
+ }
198
+
199
+ if (Math.abs(projectedDistFromP2) < Math.abs(projectedDistFromP1)) {
200
+ return this.p2;
201
+ } else {
202
+ return this.p1;
203
+ }
204
+ }
205
+
206
+ /**
207
+ * Returns the distance from this line segment to `target`.
208
+ *
209
+ * Because a line segment has no interior, this signed distance is equivalent to
210
+ * the full distance between `target` and this line segment.
211
+ */
212
+ public signedDistance(target: Point2) {
213
+ return this.closestPointTo(target).minus(target).magnitude();
214
+ }
215
+
216
+ /** Returns a copy of this line segment transformed by the given `affineTransfm`. */
217
+ public transformedBy(affineTransfm: Mat33): LineSegment2 {
218
+ return new LineSegment2(
219
+ affineTransfm.transformVec2(this.p1), affineTransfm.transformVec2(this.p2)
220
+ );
221
+ }
222
+
223
+ /** @inheritdoc */
224
+ public override getTightBoundingBox(): Rect2 {
225
+ return this.bbox;
226
+ }
227
+
228
+ public override toString() {
229
+ return `LineSegment(${this.p1.toString()}, ${this.p2.toString()})`;
230
+ }
231
+ }
232
+ export default LineSegment2;