@js-draw/math 1.16.0 → 1.18.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (64) hide show
  1. package/dist/cjs/Mat33.js +6 -1
  2. package/dist/cjs/Vec3.d.ts +23 -1
  3. package/dist/cjs/Vec3.js +33 -7
  4. package/dist/cjs/lib.d.ts +2 -1
  5. package/dist/cjs/lib.js +5 -1
  6. package/dist/cjs/shapes/Abstract2DShape.d.ts +3 -0
  7. package/dist/cjs/shapes/BezierJSWrapper.d.ts +19 -5
  8. package/dist/cjs/shapes/BezierJSWrapper.js +170 -18
  9. package/dist/cjs/shapes/LineSegment2.d.ts +45 -5
  10. package/dist/cjs/shapes/LineSegment2.js +89 -11
  11. package/dist/cjs/shapes/Parameterized2DShape.d.ts +36 -0
  12. package/dist/cjs/shapes/Parameterized2DShape.js +20 -0
  13. package/dist/cjs/shapes/Path.d.ts +131 -13
  14. package/dist/cjs/shapes/Path.js +507 -26
  15. package/dist/cjs/shapes/PointShape2D.d.ts +14 -3
  16. package/dist/cjs/shapes/PointShape2D.js +28 -5
  17. package/dist/cjs/shapes/QuadraticBezier.d.ts +6 -3
  18. package/dist/cjs/shapes/QuadraticBezier.js +21 -7
  19. package/dist/cjs/shapes/Rect2.d.ts +9 -1
  20. package/dist/cjs/shapes/Rect2.js +9 -2
  21. package/dist/cjs/utils/convexHull2Of.d.ts +9 -0
  22. package/dist/cjs/utils/convexHull2Of.js +61 -0
  23. package/dist/cjs/utils/convexHull2Of.test.d.ts +1 -0
  24. package/dist/mjs/Mat33.mjs +6 -1
  25. package/dist/mjs/Vec3.d.ts +23 -1
  26. package/dist/mjs/Vec3.mjs +33 -7
  27. package/dist/mjs/lib.d.ts +2 -1
  28. package/dist/mjs/lib.mjs +2 -1
  29. package/dist/mjs/shapes/Abstract2DShape.d.ts +3 -0
  30. package/dist/mjs/shapes/BezierJSWrapper.d.ts +19 -5
  31. package/dist/mjs/shapes/BezierJSWrapper.mjs +168 -18
  32. package/dist/mjs/shapes/LineSegment2.d.ts +45 -5
  33. package/dist/mjs/shapes/LineSegment2.mjs +89 -11
  34. package/dist/mjs/shapes/Parameterized2DShape.d.ts +36 -0
  35. package/dist/mjs/shapes/Parameterized2DShape.mjs +13 -0
  36. package/dist/mjs/shapes/Path.d.ts +131 -13
  37. package/dist/mjs/shapes/Path.mjs +504 -25
  38. package/dist/mjs/shapes/PointShape2D.d.ts +14 -3
  39. package/dist/mjs/shapes/PointShape2D.mjs +28 -5
  40. package/dist/mjs/shapes/QuadraticBezier.d.ts +6 -3
  41. package/dist/mjs/shapes/QuadraticBezier.mjs +21 -7
  42. package/dist/mjs/shapes/Rect2.d.ts +9 -1
  43. package/dist/mjs/shapes/Rect2.mjs +9 -2
  44. package/dist/mjs/utils/convexHull2Of.d.ts +9 -0
  45. package/dist/mjs/utils/convexHull2Of.mjs +59 -0
  46. package/dist/mjs/utils/convexHull2Of.test.d.ts +1 -0
  47. package/package.json +5 -5
  48. package/src/Mat33.ts +8 -2
  49. package/src/Vec3.test.ts +42 -7
  50. package/src/Vec3.ts +37 -8
  51. package/src/lib.ts +5 -0
  52. package/src/shapes/Abstract2DShape.ts +3 -0
  53. package/src/shapes/BezierJSWrapper.ts +195 -14
  54. package/src/shapes/LineSegment2.test.ts +61 -1
  55. package/src/shapes/LineSegment2.ts +110 -12
  56. package/src/shapes/Parameterized2DShape.ts +44 -0
  57. package/src/shapes/Path.test.ts +233 -5
  58. package/src/shapes/Path.ts +593 -37
  59. package/src/shapes/PointShape2D.ts +33 -6
  60. package/src/shapes/QuadraticBezier.test.ts +69 -12
  61. package/src/shapes/QuadraticBezier.ts +25 -8
  62. package/src/shapes/Rect2.ts +10 -3
  63. package/src/utils/convexHull2Of.test.ts +43 -0
  64. 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 Abstract2DShape from './Abstract2DShape.mjs';
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. It may be removed/replaced in a future release.
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 Abstract2DShape {
27
- constructor() {
28
- super(...arguments);
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.getBezier().project(point.xy).d;
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
- intersectsLineSegment(line) {
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
- const intersectionPoints = bezier.intersects(line).map(t => {
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(bezier.get(t));
101
+ const point = Vec2.ofXY(this.at(t));
78
102
  // Ensure that the intersection is on the line segment
79
- if (point.minus(line.p1).magnitude() > line.length
80
- || point.minus(line.p2).magnitude() > line.length) {
103
+ if (point.distanceTo(line.p1) > line.length
104
+ || point.distanceTo(line.p2) > line.length) {
81
105
  return null;
82
106
  }
83
- return point;
107
+ return t;
84
108
  }).filter(entry => entry !== null);
85
- return intersectionPoints;
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 Abstract2DShape from './Abstract2DShape';
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 Abstract2DShape {
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): import("../Vec3").Vec3[];
56
- closestPointTo(target: Point2): import("../Vec3").Vec3;
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 Abstract2DShape from './Abstract2DShape.mjs';
3
+ import Parameterized2DShape from './Parameterized2DShape.mjs';
4
4
  /** Represents a line segment. A `LineSegment2` is immutable. */
5
- export class LineSegment2 extends Abstract2DShape {
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
- if (this.direction.x === 0) {
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.minus(this.point1).magnitude();
107
- const resultToP2 = resultPoint.minus(this.point2).magnitude();
108
- const resultToP3 = resultPoint.minus(other.point1).magnitude();
109
- const resultToP4 = resultPoint.minus(other.point2).magnitude();
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;