@js-draw/math 1.16.0 → 1.18.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cjs/Mat33.js +6 -1
- package/dist/cjs/Vec3.d.ts +23 -1
- package/dist/cjs/Vec3.js +33 -7
- package/dist/cjs/lib.d.ts +2 -1
- package/dist/cjs/lib.js +5 -1
- package/dist/cjs/shapes/Abstract2DShape.d.ts +3 -0
- package/dist/cjs/shapes/BezierJSWrapper.d.ts +19 -5
- package/dist/cjs/shapes/BezierJSWrapper.js +170 -18
- package/dist/cjs/shapes/LineSegment2.d.ts +45 -5
- package/dist/cjs/shapes/LineSegment2.js +89 -11
- package/dist/cjs/shapes/Parameterized2DShape.d.ts +36 -0
- package/dist/cjs/shapes/Parameterized2DShape.js +20 -0
- package/dist/cjs/shapes/Path.d.ts +131 -13
- package/dist/cjs/shapes/Path.js +507 -26
- package/dist/cjs/shapes/PointShape2D.d.ts +14 -3
- package/dist/cjs/shapes/PointShape2D.js +28 -5
- package/dist/cjs/shapes/QuadraticBezier.d.ts +6 -3
- package/dist/cjs/shapes/QuadraticBezier.js +21 -7
- package/dist/cjs/shapes/Rect2.d.ts +9 -1
- package/dist/cjs/shapes/Rect2.js +9 -2
- package/dist/cjs/utils/convexHull2Of.d.ts +9 -0
- package/dist/cjs/utils/convexHull2Of.js +61 -0
- package/dist/cjs/utils/convexHull2Of.test.d.ts +1 -0
- package/dist/mjs/Mat33.mjs +6 -1
- package/dist/mjs/Vec3.d.ts +23 -1
- package/dist/mjs/Vec3.mjs +33 -7
- package/dist/mjs/lib.d.ts +2 -1
- package/dist/mjs/lib.mjs +2 -1
- package/dist/mjs/shapes/Abstract2DShape.d.ts +3 -0
- package/dist/mjs/shapes/BezierJSWrapper.d.ts +19 -5
- package/dist/mjs/shapes/BezierJSWrapper.mjs +168 -18
- package/dist/mjs/shapes/LineSegment2.d.ts +45 -5
- package/dist/mjs/shapes/LineSegment2.mjs +89 -11
- package/dist/mjs/shapes/Parameterized2DShape.d.ts +36 -0
- package/dist/mjs/shapes/Parameterized2DShape.mjs +13 -0
- package/dist/mjs/shapes/Path.d.ts +131 -13
- package/dist/mjs/shapes/Path.mjs +504 -25
- package/dist/mjs/shapes/PointShape2D.d.ts +14 -3
- package/dist/mjs/shapes/PointShape2D.mjs +28 -5
- package/dist/mjs/shapes/QuadraticBezier.d.ts +6 -3
- package/dist/mjs/shapes/QuadraticBezier.mjs +21 -7
- package/dist/mjs/shapes/Rect2.d.ts +9 -1
- package/dist/mjs/shapes/Rect2.mjs +9 -2
- package/dist/mjs/utils/convexHull2Of.d.ts +9 -0
- package/dist/mjs/utils/convexHull2Of.mjs +59 -0
- package/dist/mjs/utils/convexHull2Of.test.d.ts +1 -0
- package/package.json +5 -5
- package/src/Mat33.ts +8 -2
- package/src/Vec3.test.ts +42 -7
- package/src/Vec3.ts +37 -8
- package/src/lib.ts +5 -0
- package/src/shapes/Abstract2DShape.ts +3 -0
- package/src/shapes/BezierJSWrapper.ts +195 -14
- package/src/shapes/LineSegment2.test.ts +61 -1
- package/src/shapes/LineSegment2.ts +110 -12
- package/src/shapes/Parameterized2DShape.ts +44 -0
- package/src/shapes/Path.test.ts +233 -5
- package/src/shapes/Path.ts +593 -37
- package/src/shapes/PointShape2D.ts +33 -6
- package/src/shapes/QuadraticBezier.test.ts +69 -12
- package/src/shapes/QuadraticBezier.ts +25 -8
- package/src/shapes/Rect2.ts +10 -3
- package/src/utils/convexHull2Of.test.ts +43 -0
- package/src/utils/convexHull2Of.ts +71 -0
@@ -1,32 +1,36 @@
|
|
1
|
-
var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (receiver, state, kind, f) {
|
2
|
-
if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a getter");
|
3
|
-
if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it");
|
4
|
-
return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
|
5
|
-
};
|
6
1
|
var __classPrivateFieldSet = (this && this.__classPrivateFieldSet) || function (receiver, state, value, kind, f) {
|
7
2
|
if (kind === "m") throw new TypeError("Private method is not writable");
|
8
3
|
if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a setter");
|
9
4
|
if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot write private member to an object whose class did not declare it");
|
10
5
|
return (kind === "a" ? f.call(receiver, value) : f ? f.value = value : state.set(receiver, value)), value;
|
11
6
|
};
|
7
|
+
var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (receiver, state, kind, f) {
|
8
|
+
if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a getter");
|
9
|
+
if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it");
|
10
|
+
return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
|
11
|
+
};
|
12
12
|
var _BezierJSWrapper_bezierJs;
|
13
13
|
import { Bezier } from 'bezier-js';
|
14
14
|
import { Vec2 } from '../Vec2.mjs';
|
15
|
-
import
|
15
|
+
import LineSegment2 from './LineSegment2.mjs';
|
16
16
|
import Rect2 from './Rect2.mjs';
|
17
|
+
import Parameterized2DShape from './Parameterized2DShape.mjs';
|
17
18
|
/**
|
18
19
|
* A lazy-initializing wrapper around Bezier-js.
|
19
20
|
*
|
20
21
|
* Subclasses may override `at`, `derivativeAt`, and `normal` with functions
|
21
22
|
* that do not initialize a `bezier-js` `Bezier`.
|
22
23
|
*
|
23
|
-
* Do not use this class directly
|
24
|
+
* **Do not use this class directly.** It may be removed/replaced in a future release.
|
24
25
|
* @internal
|
25
26
|
*/
|
26
|
-
class BezierJSWrapper extends
|
27
|
-
constructor() {
|
28
|
-
super(
|
27
|
+
export class BezierJSWrapper extends Parameterized2DShape {
|
28
|
+
constructor(bezierJsBezier) {
|
29
|
+
super();
|
29
30
|
_BezierJSWrapper_bezierJs.set(this, null);
|
31
|
+
if (bezierJsBezier) {
|
32
|
+
__classPrivateFieldSet(this, _BezierJSWrapper_bezierJs, bezierJsBezier, "f");
|
33
|
+
}
|
30
34
|
}
|
31
35
|
getBezier() {
|
32
36
|
if (!__classPrivateFieldGet(this, _BezierJSWrapper_bezierJs, "f")) {
|
@@ -36,7 +40,7 @@ class BezierJSWrapper extends Abstract2DShape {
|
|
36
40
|
}
|
37
41
|
signedDistance(point) {
|
38
42
|
// .d: Distance
|
39
|
-
return this.
|
43
|
+
return this.nearestPointTo(point).point.distanceTo(point);
|
40
44
|
}
|
41
45
|
/**
|
42
46
|
* @returns the (more) exact distance from `point` to this.
|
@@ -56,34 +60,180 @@ class BezierJSWrapper extends Abstract2DShape {
|
|
56
60
|
derivativeAt(t) {
|
57
61
|
return Vec2.ofXY(this.getBezier().derivative(t));
|
58
62
|
}
|
63
|
+
secondDerivativeAt(t) {
|
64
|
+
return Vec2.ofXY(this.getBezier().dderivative(t));
|
65
|
+
}
|
59
66
|
normal(t) {
|
60
67
|
return Vec2.ofXY(this.getBezier().normal(t));
|
61
68
|
}
|
69
|
+
normalAt(t) {
|
70
|
+
return this.normal(t);
|
71
|
+
}
|
72
|
+
tangentAt(t) {
|
73
|
+
return this.derivativeAt(t).normalized();
|
74
|
+
}
|
62
75
|
getTightBoundingBox() {
|
63
76
|
const bbox = this.getBezier().bbox();
|
64
77
|
const width = bbox.x.max - bbox.x.min;
|
65
78
|
const height = bbox.y.max - bbox.y.min;
|
66
79
|
return new Rect2(bbox.x.min, bbox.y.min, width, height);
|
67
80
|
}
|
68
|
-
|
81
|
+
argIntersectsLineSegment(line) {
|
82
|
+
// Bezier-js has a bug when all control points of a Bezier curve lie on
|
83
|
+
// a line. Our solution involves converting the Bezier into a line, then
|
84
|
+
// finding the parameter value that produced the intersection.
|
85
|
+
//
|
86
|
+
// TODO: This is unnecessarily slow. A better solution would be to fix
|
87
|
+
// the bug upstream.
|
88
|
+
const asLine = LineSegment2.ofSmallestContainingPoints(this.getPoints());
|
89
|
+
if (asLine) {
|
90
|
+
const intersection = asLine.intersectsLineSegment(line);
|
91
|
+
return intersection.map(p => this.nearestPointTo(p).parameterValue);
|
92
|
+
}
|
69
93
|
const bezier = this.getBezier();
|
70
|
-
|
94
|
+
return bezier.intersects(line).map(t => {
|
71
95
|
// We're using the .intersects(line) function, which is documented
|
72
96
|
// to always return numbers. However, to satisfy the type checker (and
|
73
97
|
// possibly improperly-defined types),
|
74
98
|
if (typeof t === 'string') {
|
75
99
|
t = parseFloat(t);
|
76
100
|
}
|
77
|
-
const point = Vec2.ofXY(
|
101
|
+
const point = Vec2.ofXY(this.at(t));
|
78
102
|
// Ensure that the intersection is on the line segment
|
79
|
-
if (point.
|
80
|
-
|| point.
|
103
|
+
if (point.distanceTo(line.p1) > line.length
|
104
|
+
|| point.distanceTo(line.p2) > line.length) {
|
81
105
|
return null;
|
82
106
|
}
|
83
|
-
return
|
107
|
+
return t;
|
84
108
|
}).filter(entry => entry !== null);
|
85
|
-
|
109
|
+
}
|
110
|
+
splitAt(t) {
|
111
|
+
if (t <= 0 || t >= 1) {
|
112
|
+
return [this];
|
113
|
+
}
|
114
|
+
const bezier = this.getBezier();
|
115
|
+
const split = bezier.split(t);
|
116
|
+
return [
|
117
|
+
new BezierJSWrapperImpl(split.left.points.map(point => Vec2.ofXY(point)), split.left),
|
118
|
+
new BezierJSWrapperImpl(split.right.points.map(point => Vec2.ofXY(point)), split.right),
|
119
|
+
];
|
120
|
+
}
|
121
|
+
nearestPointTo(point) {
|
122
|
+
// One implementation could be similar to this:
|
123
|
+
// const projection = this.getBezier().project(point);
|
124
|
+
// return {
|
125
|
+
// point: Vec2.ofXY(projection),
|
126
|
+
// parameterValue: projection.t!,
|
127
|
+
// };
|
128
|
+
// However, Bezier-js is rather impercise (and relies on a lookup table).
|
129
|
+
// Thus, we instead use Newton's Method:
|
130
|
+
// We want to find t such that f(t) = |B(t) - p|² is minimized.
|
131
|
+
// Expanding,
|
132
|
+
// f(t) = (Bₓ(t) - pₓ)² + (Bᵧ(t) - pᵧ)²
|
133
|
+
// ⇒ f'(t) = Dₜ(Bₓ(t) - pₓ)² + Dₜ(Bᵧ(t) - pᵧ)²
|
134
|
+
// ⇒ f'(t) = 2(Bₓ(t) - pₓ)(Bₓ'(t)) + 2(Bᵧ(t) - pᵧ)(Bᵧ'(t))
|
135
|
+
// = 2Bₓ(t)Bₓ'(t) - 2pₓBₓ'(t) + 2Bᵧ(t)Bᵧ'(t) - 2pᵧBᵧ'(t)
|
136
|
+
// ⇒ f''(t)= 2Bₓ'(t)Bₓ'(t) + 2Bₓ(t)Bₓ''(t) - 2pₓBₓ''(t) + 2Bᵧ'(t)Bᵧ'(t)
|
137
|
+
// + 2Bᵧ(t)Bᵧ''(t) - 2pᵧBᵧ''(t)
|
138
|
+
// Because f'(t) = 0 at relative extrema, we can use Newton's Method
|
139
|
+
// to improve on an initial guess.
|
140
|
+
const sqrDistAt = (t) => point.squareDistanceTo(this.at(t));
|
141
|
+
const yIntercept = sqrDistAt(0);
|
142
|
+
let t = 0;
|
143
|
+
let minSqrDist = yIntercept;
|
144
|
+
// Start by testing a few points:
|
145
|
+
const pointsToTest = 4;
|
146
|
+
for (let i = 0; i < pointsToTest; i++) {
|
147
|
+
const testT = i / (pointsToTest - 1);
|
148
|
+
const testMinSqrDist = sqrDistAt(testT);
|
149
|
+
if (testMinSqrDist < minSqrDist) {
|
150
|
+
t = testT;
|
151
|
+
minSqrDist = testMinSqrDist;
|
152
|
+
}
|
153
|
+
}
|
154
|
+
// To use Newton's Method, we need to evaluate the second derivative of the distance
|
155
|
+
// function:
|
156
|
+
const secondDerivativeAt = (t) => {
|
157
|
+
// f''(t) = 2Bₓ'(t)Bₓ'(t) + 2Bₓ(t)Bₓ''(t) - 2pₓBₓ''(t)
|
158
|
+
// + 2Bᵧ'(t)Bᵧ'(t) + 2Bᵧ(t)Bᵧ''(t) - 2pᵧBᵧ''(t)
|
159
|
+
const b = this.at(t);
|
160
|
+
const bPrime = this.derivativeAt(t);
|
161
|
+
const bPrimePrime = this.secondDerivativeAt(t);
|
162
|
+
return (2 * bPrime.x * bPrime.x + 2 * b.x * bPrimePrime.x - 2 * point.x * bPrimePrime.x
|
163
|
+
+ 2 * bPrime.y * bPrime.y + 2 * b.y * bPrimePrime.y - 2 * point.y * bPrimePrime.y);
|
164
|
+
};
|
165
|
+
// Because we're zeroing f'(t), we also need to be able to compute it:
|
166
|
+
const derivativeAt = (t) => {
|
167
|
+
// f'(t) = 2Bₓ(t)Bₓ'(t) - 2pₓBₓ'(t) + 2Bᵧ(t)Bᵧ'(t) - 2pᵧBᵧ'(t)
|
168
|
+
const b = this.at(t);
|
169
|
+
const bPrime = this.derivativeAt(t);
|
170
|
+
return (2 * b.x * bPrime.x - 2 * point.x * bPrime.x
|
171
|
+
+ 2 * b.y * bPrime.y - 2 * point.y * bPrime.y);
|
172
|
+
};
|
173
|
+
const iterate = () => {
|
174
|
+
const slope = secondDerivativeAt(t);
|
175
|
+
if (slope === 0)
|
176
|
+
return;
|
177
|
+
// We intersect a line through the point on f'(t) at t with the x-axis:
|
178
|
+
// y = m(x - x₀) + y₀
|
179
|
+
// ⇒ x - x₀ = (y - y₀) / m
|
180
|
+
// ⇒ x = (y - y₀) / m + x₀
|
181
|
+
//
|
182
|
+
// Thus, when zeroed,
|
183
|
+
// tN = (0 - f'(t)) / m + t
|
184
|
+
const newT = (0 - derivativeAt(t)) / slope + t;
|
185
|
+
//const distDiff = sqrDistAt(newT) - sqrDistAt(t);
|
186
|
+
//console.assert(distDiff <= 0, `${-distDiff} >= 0`);
|
187
|
+
t = newT;
|
188
|
+
if (t > 1) {
|
189
|
+
t = 1;
|
190
|
+
}
|
191
|
+
else if (t < 0) {
|
192
|
+
t = 0;
|
193
|
+
}
|
194
|
+
};
|
195
|
+
for (let i = 0; i < 12; i++) {
|
196
|
+
iterate();
|
197
|
+
}
|
198
|
+
return { parameterValue: t, point: this.at(t) };
|
199
|
+
}
|
200
|
+
intersectsBezier(other) {
|
201
|
+
const intersections = this.getBezier().intersects(other.getBezier());
|
202
|
+
if (!intersections || intersections.length === 0) {
|
203
|
+
return [];
|
204
|
+
}
|
205
|
+
const result = [];
|
206
|
+
for (const intersection of intersections) {
|
207
|
+
// From http://pomax.github.io/bezierjs/#intersect-curve,
|
208
|
+
// .intersects returns an array of 't1/t2' pairs, where curve1.at(t1) gives the point.
|
209
|
+
const match = /^([-0-9.eE]+)\/([-0-9.eE]+)$/.exec(intersection);
|
210
|
+
if (!match) {
|
211
|
+
throw new Error(`Incorrect format returned by .intersects: ${intersections} should be array of "number/number"!`);
|
212
|
+
}
|
213
|
+
const t = parseFloat(match[1]);
|
214
|
+
result.push({
|
215
|
+
parameterValue: t,
|
216
|
+
point: this.at(t),
|
217
|
+
});
|
218
|
+
}
|
219
|
+
return result;
|
220
|
+
}
|
221
|
+
toString() {
|
222
|
+
return `Bézier(${this.getPoints().map(point => point.toString()).join(', ')})`;
|
86
223
|
}
|
87
224
|
}
|
88
225
|
_BezierJSWrapper_bezierJs = new WeakMap();
|
226
|
+
/**
|
227
|
+
* Private concrete implementation of `BezierJSWrapper`, used by methods above that need to return a wrapper
|
228
|
+
* around a `Bezier`.
|
229
|
+
*/
|
230
|
+
class BezierJSWrapperImpl extends BezierJSWrapper {
|
231
|
+
constructor(controlPoints, curve) {
|
232
|
+
super(curve);
|
233
|
+
this.controlPoints = controlPoints;
|
234
|
+
}
|
235
|
+
getPoints() {
|
236
|
+
return this.controlPoints;
|
237
|
+
}
|
238
|
+
}
|
89
239
|
export default BezierJSWrapper;
|
@@ -1,13 +1,14 @@
|
|
1
1
|
import Mat33 from '../Mat33';
|
2
2
|
import Rect2 from './Rect2';
|
3
3
|
import { Vec2, Point2 } from '../Vec2';
|
4
|
-
import
|
4
|
+
import Parameterized2DShape from './Parameterized2DShape';
|
5
|
+
import Vec3 from '../Vec3';
|
5
6
|
interface IntersectionResult {
|
6
7
|
point: Point2;
|
7
8
|
t: number;
|
8
9
|
}
|
9
10
|
/** Represents a line segment. A `LineSegment2` is immutable. */
|
10
|
-
export declare class LineSegment2 extends
|
11
|
+
export declare class LineSegment2 extends Parameterized2DShape {
|
11
12
|
private readonly point1;
|
12
13
|
private readonly point2;
|
13
14
|
/**
|
@@ -24,12 +25,24 @@ export declare class LineSegment2 extends Abstract2DShape {
|
|
24
25
|
readonly bbox: Rect2;
|
25
26
|
/** Creates a new `LineSegment2` from its endpoints. */
|
26
27
|
constructor(point1: Point2, point2: Point2);
|
28
|
+
/**
|
29
|
+
* Returns the smallest line segment that contains all points in `points`, or `null`
|
30
|
+
* if no such line segment exists.
|
31
|
+
*
|
32
|
+
* @example
|
33
|
+
* ```ts,runnable
|
34
|
+
* import {LineSegment2, Vec2} from '@js-draw/math';
|
35
|
+
* console.log(LineSegment2.ofSmallestContainingPoints([Vec2.of(1, 0), Vec2.of(0, 1)]));
|
36
|
+
* ```
|
37
|
+
*/
|
38
|
+
static ofSmallestContainingPoints(points: readonly Point2[]): LineSegment2 | null;
|
27
39
|
/** Alias for `point1`. */
|
28
40
|
get p1(): Point2;
|
29
41
|
/** Alias for `point2`. */
|
30
42
|
get p2(): Point2;
|
43
|
+
get center(): Point2;
|
31
44
|
/**
|
32
|
-
* Gets a point a distance `t` along this line.
|
45
|
+
* Gets a point a **distance** `t` along this line.
|
33
46
|
*
|
34
47
|
* @deprecated
|
35
48
|
*/
|
@@ -42,8 +55,20 @@ export declare class LineSegment2 extends Abstract2DShape {
|
|
42
55
|
* `t` should be in `[0, 1]`.
|
43
56
|
*/
|
44
57
|
at(t: number): Point2;
|
58
|
+
normalAt(_t: number): Vec2;
|
59
|
+
tangentAt(_t: number): Vec3;
|
60
|
+
splitAt(t: number): [LineSegment2] | [LineSegment2, LineSegment2];
|
61
|
+
/**
|
62
|
+
* Returns the intersection of this with another line segment.
|
63
|
+
*
|
64
|
+
* **WARNING**: The parameter value returned by this method does not range from 0 to 1 and
|
65
|
+
* is currently a length.
|
66
|
+
* This will change in a future release.
|
67
|
+
* @deprecated
|
68
|
+
*/
|
45
69
|
intersection(other: LineSegment2): IntersectionResult | null;
|
46
70
|
intersects(other: LineSegment2): boolean;
|
71
|
+
argIntersectsLineSegment(lineSegment: LineSegment2): number[];
|
47
72
|
/**
|
48
73
|
* Returns the points at which this line segment intersects the
|
49
74
|
* given line segment.
|
@@ -52,8 +77,12 @@ export declare class LineSegment2 extends Abstract2DShape {
|
|
52
77
|
* line segment. This method, by contrast, returns **the point** at which the intersection
|
53
78
|
* occurs, if such a point exists.
|
54
79
|
*/
|
55
|
-
intersectsLineSegment(lineSegment: LineSegment2):
|
56
|
-
closestPointTo(target: Point2):
|
80
|
+
intersectsLineSegment(lineSegment: LineSegment2): Vec3[];
|
81
|
+
closestPointTo(target: Point2): Vec3;
|
82
|
+
nearestPointTo(target: Vec3): {
|
83
|
+
point: Vec3;
|
84
|
+
parameterValue: number;
|
85
|
+
};
|
57
86
|
/**
|
58
87
|
* Returns the distance from this line segment to `target`.
|
59
88
|
*
|
@@ -66,5 +95,16 @@ export declare class LineSegment2 extends Abstract2DShape {
|
|
66
95
|
/** @inheritdoc */
|
67
96
|
getTightBoundingBox(): Rect2;
|
68
97
|
toString(): string;
|
98
|
+
/**
|
99
|
+
* Returns `true` iff this is equivalent to `other`.
|
100
|
+
*
|
101
|
+
* **Options**:
|
102
|
+
* - `tolerance`: The maximum difference between endpoints. (Default: 0)
|
103
|
+
* - `ignoreDirection`: Allow matching a version of `this` with opposite direction. (Default: `true`)
|
104
|
+
*/
|
105
|
+
eq(other: LineSegment2, options?: {
|
106
|
+
tolerance?: number;
|
107
|
+
ignoreDirection?: boolean;
|
108
|
+
}): boolean;
|
69
109
|
}
|
70
110
|
export default LineSegment2;
|
@@ -1,8 +1,8 @@
|
|
1
1
|
import Rect2 from './Rect2.mjs';
|
2
2
|
import { Vec2 } from '../Vec2.mjs';
|
3
|
-
import
|
3
|
+
import Parameterized2DShape from './Parameterized2DShape.mjs';
|
4
4
|
/** Represents a line segment. A `LineSegment2` is immutable. */
|
5
|
-
export class LineSegment2 extends
|
5
|
+
export class LineSegment2 extends Parameterized2DShape {
|
6
6
|
/** Creates a new `LineSegment2` from its endpoints. */
|
7
7
|
constructor(point1, point2) {
|
8
8
|
super();
|
@@ -16,6 +16,28 @@ export class LineSegment2 extends Abstract2DShape {
|
|
16
16
|
this.direction = this.direction.times(1 / this.length);
|
17
17
|
}
|
18
18
|
}
|
19
|
+
/**
|
20
|
+
* Returns the smallest line segment that contains all points in `points`, or `null`
|
21
|
+
* if no such line segment exists.
|
22
|
+
*
|
23
|
+
* @example
|
24
|
+
* ```ts,runnable
|
25
|
+
* import {LineSegment2, Vec2} from '@js-draw/math';
|
26
|
+
* console.log(LineSegment2.ofSmallestContainingPoints([Vec2.of(1, 0), Vec2.of(0, 1)]));
|
27
|
+
* ```
|
28
|
+
*/
|
29
|
+
static ofSmallestContainingPoints(points) {
|
30
|
+
if (points.length <= 1)
|
31
|
+
return null;
|
32
|
+
const sorted = [...points].sort((a, b) => a.x !== b.x ? a.x - b.x : a.y - b.y);
|
33
|
+
const line = new LineSegment2(sorted[0], sorted[sorted.length - 1]);
|
34
|
+
for (const point of sorted) {
|
35
|
+
if (!line.containsPoint(point)) {
|
36
|
+
return null;
|
37
|
+
}
|
38
|
+
}
|
39
|
+
return line;
|
40
|
+
}
|
19
41
|
// Accessors to make LineSegment2 compatible with bezier-js's
|
20
42
|
// interface
|
21
43
|
/** Alias for `point1`. */
|
@@ -26,8 +48,11 @@ export class LineSegment2 extends Abstract2DShape {
|
|
26
48
|
get p2() {
|
27
49
|
return this.point2;
|
28
50
|
}
|
51
|
+
get center() {
|
52
|
+
return this.point1.lerp(this.point2, 0.5);
|
53
|
+
}
|
29
54
|
/**
|
30
|
-
* Gets a point a distance `t` along this line.
|
55
|
+
* Gets a point a **distance** `t` along this line.
|
31
56
|
*
|
32
57
|
* @deprecated
|
33
58
|
*/
|
@@ -44,7 +69,31 @@ export class LineSegment2 extends Abstract2DShape {
|
|
44
69
|
at(t) {
|
45
70
|
return this.get(t * this.length);
|
46
71
|
}
|
72
|
+
normalAt(_t) {
|
73
|
+
return this.direction.orthog();
|
74
|
+
}
|
75
|
+
tangentAt(_t) {
|
76
|
+
return this.direction;
|
77
|
+
}
|
78
|
+
splitAt(t) {
|
79
|
+
if (t <= 0 || t >= 1) {
|
80
|
+
return [this];
|
81
|
+
}
|
82
|
+
return [
|
83
|
+
new LineSegment2(this.point1, this.at(t)),
|
84
|
+
new LineSegment2(this.at(t), this.point2),
|
85
|
+
];
|
86
|
+
}
|
87
|
+
/**
|
88
|
+
* Returns the intersection of this with another line segment.
|
89
|
+
*
|
90
|
+
* **WARNING**: The parameter value returned by this method does not range from 0 to 1 and
|
91
|
+
* is currently a length.
|
92
|
+
* This will change in a future release.
|
93
|
+
* @deprecated
|
94
|
+
*/
|
47
95
|
intersection(other) {
|
96
|
+
// TODO(v2.0.0): Make this return a `t` value from `0` to `1`.
|
48
97
|
// We want x₁(t) = x₂(t) and y₁(t) = y₂(t)
|
49
98
|
// Observe that
|
50
99
|
// x = this.point1.x + this.direction.x · t₁
|
@@ -71,7 +120,10 @@ export class LineSegment2 extends Abstract2DShape {
|
|
71
120
|
// = ((o₁ᵧ - o₂ᵧ)((d₁ₓd₂ₓ)) + (d₂ᵧd₁ₓ)(o₂ₓ) - (d₁ᵧd₂ₓ)(o₁ₓ))/(d₂ᵧd₁ₓ - d₁ᵧd₂ₓ)
|
72
121
|
// ⇒ y = o₁ᵧ + d₁ᵧ · (x - o₁ₓ) / d₁ₓ = ...
|
73
122
|
let resultPoint, resultT;
|
74
|
-
|
123
|
+
// Consider very-near-vertical lines to be vertical --- not doing so can lead to
|
124
|
+
// precision error when dividing by this.direction.x.
|
125
|
+
const small = 4e-13;
|
126
|
+
if (Math.abs(this.direction.x) < small) {
|
75
127
|
// Vertical line: Where does the other have x = this.point1.x?
|
76
128
|
// x = o₁ₓ = o₂ₓ + d₂ₓ · (y - o₂ᵧ) / d₂ᵧ
|
77
129
|
// ⇒ (o₁ₓ - o₂ₓ)(d₂ᵧ/d₂ₓ) + o₂ᵧ = y
|
@@ -103,10 +155,10 @@ export class LineSegment2 extends Abstract2DShape {
|
|
103
155
|
resultT = (xIntersect - this.point1.x) / this.direction.x;
|
104
156
|
}
|
105
157
|
// Ensure the result is in this/the other segment.
|
106
|
-
const resultToP1 = resultPoint.
|
107
|
-
const resultToP2 = resultPoint.
|
108
|
-
const resultToP3 = resultPoint.
|
109
|
-
const resultToP4 = resultPoint.
|
158
|
+
const resultToP1 = resultPoint.distanceTo(this.point1);
|
159
|
+
const resultToP2 = resultPoint.distanceTo(this.point2);
|
160
|
+
const resultToP3 = resultPoint.distanceTo(other.point1);
|
161
|
+
const resultToP4 = resultPoint.distanceTo(other.point2);
|
110
162
|
if (resultToP1 > this.length
|
111
163
|
|| resultToP2 > this.length
|
112
164
|
|| resultToP3 > other.length
|
@@ -121,6 +173,13 @@ export class LineSegment2 extends Abstract2DShape {
|
|
121
173
|
intersects(other) {
|
122
174
|
return this.intersection(other) !== null;
|
123
175
|
}
|
176
|
+
argIntersectsLineSegment(lineSegment) {
|
177
|
+
const intersection = this.intersection(lineSegment);
|
178
|
+
if (intersection) {
|
179
|
+
return [intersection.t / this.length];
|
180
|
+
}
|
181
|
+
return [];
|
182
|
+
}
|
124
183
|
/**
|
125
184
|
* Returns the points at which this line segment intersects the
|
126
185
|
* given line segment.
|
@@ -138,18 +197,21 @@ export class LineSegment2 extends Abstract2DShape {
|
|
138
197
|
}
|
139
198
|
// Returns the closest point on this to [target]
|
140
199
|
closestPointTo(target) {
|
200
|
+
return this.nearestPointTo(target).point;
|
201
|
+
}
|
202
|
+
nearestPointTo(target) {
|
141
203
|
// Distance from P1 along this' direction.
|
142
204
|
const projectedDistFromP1 = target.minus(this.p1).dot(this.direction);
|
143
205
|
const projectedDistFromP2 = this.length - projectedDistFromP1;
|
144
206
|
const projection = this.p1.plus(this.direction.times(projectedDistFromP1));
|
145
207
|
if (projectedDistFromP1 > 0 && projectedDistFromP1 < this.length) {
|
146
|
-
return projection;
|
208
|
+
return { point: projection, parameterValue: projectedDistFromP1 / this.length };
|
147
209
|
}
|
148
210
|
if (Math.abs(projectedDistFromP2) < Math.abs(projectedDistFromP1)) {
|
149
|
-
return this.p2;
|
211
|
+
return { point: this.p2, parameterValue: 1 };
|
150
212
|
}
|
151
213
|
else {
|
152
|
-
return this.p1;
|
214
|
+
return { point: this.p1, parameterValue: 0 };
|
153
215
|
}
|
154
216
|
}
|
155
217
|
/**
|
@@ -172,5 +234,21 @@ export class LineSegment2 extends Abstract2DShape {
|
|
172
234
|
toString() {
|
173
235
|
return `LineSegment(${this.p1.toString()}, ${this.p2.toString()})`;
|
174
236
|
}
|
237
|
+
/**
|
238
|
+
* Returns `true` iff this is equivalent to `other`.
|
239
|
+
*
|
240
|
+
* **Options**:
|
241
|
+
* - `tolerance`: The maximum difference between endpoints. (Default: 0)
|
242
|
+
* - `ignoreDirection`: Allow matching a version of `this` with opposite direction. (Default: `true`)
|
243
|
+
*/
|
244
|
+
eq(other, options) {
|
245
|
+
if (!(other instanceof LineSegment2)) {
|
246
|
+
return false;
|
247
|
+
}
|
248
|
+
const tolerance = options?.tolerance;
|
249
|
+
const ignoreDirection = options?.ignoreDirection ?? true;
|
250
|
+
return ((other.p1.eq(this.p1, tolerance) && other.p2.eq(this.p2, tolerance))
|
251
|
+
|| (ignoreDirection && other.p1.eq(this.p2, tolerance) && other.p2.eq(this.p1, tolerance)));
|
252
|
+
}
|
175
253
|
}
|
176
254
|
export default LineSegment2;
|
@@ -0,0 +1,36 @@
|
|
1
|
+
import { Point2, Vec2 } from '../Vec2';
|
2
|
+
import Abstract2DShape from './Abstract2DShape';
|
3
|
+
import LineSegment2 from './LineSegment2';
|
4
|
+
/**
|
5
|
+
* A 2-dimensional path with parameter interval $t \in [0, 1]$.
|
6
|
+
*
|
7
|
+
* **Note:** Avoid extending this class outside of `js-draw` --- new abstract methods
|
8
|
+
* may be added between minor versions.
|
9
|
+
*/
|
10
|
+
export declare abstract class Parameterized2DShape extends Abstract2DShape {
|
11
|
+
/** Returns this at a given parameter. $t \in [0, 1]$ */
|
12
|
+
abstract at(t: number): Point2;
|
13
|
+
/** Computes the unit normal vector at $t$. */
|
14
|
+
abstract normalAt(t: number): Vec2;
|
15
|
+
abstract tangentAt(t: number): Vec2;
|
16
|
+
/**
|
17
|
+
* Divides this shape into two separate shapes at parameter value $t$.
|
18
|
+
*/
|
19
|
+
abstract splitAt(t: number): [Parameterized2DShape] | [Parameterized2DShape, Parameterized2DShape];
|
20
|
+
/**
|
21
|
+
* Returns the nearest point on `this` to `point` and the `parameterValue` at which
|
22
|
+
* that point occurs.
|
23
|
+
*/
|
24
|
+
abstract nearestPointTo(point: Point2): {
|
25
|
+
point: Point2;
|
26
|
+
parameterValue: number;
|
27
|
+
};
|
28
|
+
/**
|
29
|
+
* Returns the **parameter values** at which `lineSegment` intersects this shape.
|
30
|
+
*
|
31
|
+
* See also {@link intersectsLineSegment}
|
32
|
+
*/
|
33
|
+
abstract argIntersectsLineSegment(lineSegment: LineSegment2): number[];
|
34
|
+
intersectsLineSegment(line: LineSegment2): Point2[];
|
35
|
+
}
|
36
|
+
export default Parameterized2DShape;
|
@@ -0,0 +1,13 @@
|
|
1
|
+
import Abstract2DShape from './Abstract2DShape.mjs';
|
2
|
+
/**
|
3
|
+
* A 2-dimensional path with parameter interval $t \in [0, 1]$.
|
4
|
+
*
|
5
|
+
* **Note:** Avoid extending this class outside of `js-draw` --- new abstract methods
|
6
|
+
* may be added between minor versions.
|
7
|
+
*/
|
8
|
+
export class Parameterized2DShape extends Abstract2DShape {
|
9
|
+
intersectsLineSegment(line) {
|
10
|
+
return this.argIntersectsLineSegment(line).map(t => this.at(t));
|
11
|
+
}
|
12
|
+
}
|
13
|
+
export default Parameterized2DShape;
|