@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.
- package/LICENSE +21 -0
- package/dist/cjs/Color4.test.d.ts +1 -0
- package/dist/cjs/Mat33.test.d.ts +1 -0
- package/dist/cjs/Vec2.test.d.ts +1 -0
- package/dist/cjs/Vec3.test.d.ts +1 -0
- package/dist/cjs/polynomial/solveQuadratic.test.d.ts +1 -0
- package/dist/cjs/rounding.test.d.ts +1 -0
- package/dist/cjs/shapes/LineSegment2.test.d.ts +1 -0
- package/dist/cjs/shapes/Path.fromString.test.d.ts +1 -0
- package/dist/cjs/shapes/Path.test.d.ts +1 -0
- package/dist/cjs/shapes/Path.toString.test.d.ts +1 -0
- package/dist/cjs/shapes/QuadraticBezier.test.d.ts +1 -0
- package/dist/cjs/shapes/Rect2.test.d.ts +1 -0
- package/dist/cjs/shapes/Triangle.test.d.ts +1 -0
- package/dist/mjs/Color4.test.d.ts +1 -0
- package/dist/mjs/Mat33.test.d.ts +1 -0
- package/dist/mjs/Vec2.test.d.ts +1 -0
- package/dist/mjs/Vec3.test.d.ts +1 -0
- package/dist/mjs/polynomial/solveQuadratic.test.d.ts +1 -0
- package/dist/mjs/rounding.test.d.ts +1 -0
- package/dist/mjs/shapes/LineSegment2.test.d.ts +1 -0
- package/dist/mjs/shapes/Path.fromString.test.d.ts +1 -0
- package/dist/mjs/shapes/Path.test.d.ts +1 -0
- package/dist/mjs/shapes/Path.toString.test.d.ts +1 -0
- package/dist/mjs/shapes/QuadraticBezier.test.d.ts +1 -0
- package/dist/mjs/shapes/Rect2.test.d.ts +1 -0
- package/dist/mjs/shapes/Triangle.test.d.ts +1 -0
- package/dist-test/test_imports/package-lock.json +13 -0
- package/dist-test/test_imports/package.json +12 -0
- package/dist-test/test_imports/test-imports.js +15 -0
- package/dist-test/test_imports/test-require.cjs +15 -0
- package/package.json +4 -3
- package/src/Color4.test.ts +52 -0
- package/src/Color4.ts +318 -0
- package/src/Mat33.test.ts +244 -0
- package/src/Mat33.ts +450 -0
- package/src/Vec2.test.ts +30 -0
- package/src/Vec2.ts +49 -0
- package/src/Vec3.test.ts +51 -0
- package/src/Vec3.ts +245 -0
- package/src/lib.ts +42 -0
- package/src/polynomial/solveQuadratic.test.ts +39 -0
- package/src/polynomial/solveQuadratic.ts +43 -0
- package/src/rounding.test.ts +65 -0
- package/src/rounding.ts +167 -0
- package/src/shapes/Abstract2DShape.ts +63 -0
- package/src/shapes/BezierJSWrapper.ts +93 -0
- package/src/shapes/CubicBezier.ts +35 -0
- package/src/shapes/LineSegment2.test.ts +99 -0
- package/src/shapes/LineSegment2.ts +232 -0
- package/src/shapes/Path.fromString.test.ts +223 -0
- package/src/shapes/Path.test.ts +309 -0
- package/src/shapes/Path.toString.test.ts +77 -0
- package/src/shapes/Path.ts +963 -0
- package/src/shapes/PointShape2D.ts +33 -0
- package/src/shapes/QuadraticBezier.test.ts +31 -0
- package/src/shapes/QuadraticBezier.ts +142 -0
- package/src/shapes/Rect2.test.ts +209 -0
- package/src/shapes/Rect2.ts +346 -0
- package/src/shapes/Triangle.test.ts +61 -0
- 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;
|