@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.
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,8 +1,8 @@
1
1
  import { Bezier } from 'bezier-js';
2
2
  import { Point2, Vec2 } from '../Vec2';
3
- import Abstract2DShape from './Abstract2DShape';
4
3
  import LineSegment2 from './LineSegment2';
5
4
  import Rect2 from './Rect2';
5
+ import Parameterized2DShape from './Parameterized2DShape';
6
6
 
7
7
  /**
8
8
  * A lazy-initializing wrapper around Bezier-js.
@@ -10,14 +10,24 @@ import Rect2 from './Rect2';
10
10
  * Subclasses may override `at`, `derivativeAt`, and `normal` with functions
11
11
  * that do not initialize a `bezier-js` `Bezier`.
12
12
  *
13
- * Do not use this class directly. It may be removed/replaced in a future release.
13
+ * **Do not use this class directly.** It may be removed/replaced in a future release.
14
14
  * @internal
15
15
  */
16
- abstract class BezierJSWrapper extends Abstract2DShape {
16
+ export abstract class BezierJSWrapper extends Parameterized2DShape {
17
17
  #bezierJs: Bezier|null = null;
18
18
 
19
+ protected constructor(
20
+ bezierJsBezier?: Bezier
21
+ ) {
22
+ super();
23
+
24
+ if (bezierJsBezier) {
25
+ this.#bezierJs = bezierJsBezier;
26
+ }
27
+ }
28
+
19
29
  /** Returns the start, control points, and end point of this Bézier. */
20
- public abstract getPoints(): Point2[];
30
+ public abstract getPoints(): readonly Point2[];
21
31
 
22
32
  protected getBezier() {
23
33
  if (!this.#bezierJs) {
@@ -28,7 +38,7 @@ abstract class BezierJSWrapper extends Abstract2DShape {
28
38
 
29
39
  public override signedDistance(point: Point2): number {
30
40
  // .d: Distance
31
- return this.getBezier().project(point.xy).d!;
41
+ return this.nearestPointTo(point).point.distanceTo(point);
32
42
  }
33
43
 
34
44
  /**
@@ -44,7 +54,7 @@ abstract class BezierJSWrapper extends Abstract2DShape {
44
54
  /**
45
55
  * @returns the curve evaluated at `t`.
46
56
  */
47
- public at(t: number): Point2 {
57
+ public override at(t: number): Point2 {
48
58
  return Vec2.ofXY(this.getBezier().get(t));
49
59
  }
50
60
 
@@ -52,10 +62,22 @@ abstract class BezierJSWrapper extends Abstract2DShape {
52
62
  return Vec2.ofXY(this.getBezier().derivative(t));
53
63
  }
54
64
 
65
+ public secondDerivativeAt(t: number): Point2 {
66
+ return Vec2.ofXY((this.getBezier() as any).dderivative(t));
67
+ }
68
+
55
69
  public normal(t: number): Vec2 {
56
70
  return Vec2.ofXY(this.getBezier().normal(t));
57
71
  }
58
72
 
73
+ public override normalAt(t: number): Vec2 {
74
+ return this.normal(t);
75
+ }
76
+
77
+ public override tangentAt(t: number): Vec2 {
78
+ return this.derivativeAt(t).normalized();
79
+ }
80
+
59
81
  public override getTightBoundingBox(): Rect2 {
60
82
  const bbox = this.getBezier().bbox();
61
83
  const width = bbox.x.max - bbox.x.min;
@@ -64,10 +86,22 @@ abstract class BezierJSWrapper extends Abstract2DShape {
64
86
  return new Rect2(bbox.x.min, bbox.y.min, width, height);
65
87
  }
66
88
 
67
- public override intersectsLineSegment(line: LineSegment2): Point2[] {
89
+ public override argIntersectsLineSegment(line: LineSegment2): number[] {
90
+ // Bezier-js has a bug when all control points of a Bezier curve lie on
91
+ // a line. Our solution involves converting the Bezier into a line, then
92
+ // finding the parameter value that produced the intersection.
93
+ //
94
+ // TODO: This is unnecessarily slow. A better solution would be to fix
95
+ // the bug upstream.
96
+ const asLine = LineSegment2.ofSmallestContainingPoints(this.getPoints());
97
+ if (asLine) {
98
+ const intersection = asLine.intersectsLineSegment(line);
99
+ return intersection.map(p => this.nearestPointTo(p).parameterValue);
100
+ }
101
+
68
102
  const bezier = this.getBezier();
69
103
 
70
- const intersectionPoints = bezier.intersects(line).map(t => {
104
+ return bezier.intersects(line).map(t => {
71
105
  // We're using the .intersects(line) function, which is documented
72
106
  // to always return numbers. However, to satisfy the type checker (and
73
107
  // possibly improperly-defined types),
@@ -75,18 +109,165 @@ abstract class BezierJSWrapper extends Abstract2DShape {
75
109
  t = parseFloat(t);
76
110
  }
77
111
 
78
- const point = Vec2.ofXY(bezier.get(t));
112
+ const point = Vec2.ofXY(this.at(t));
79
113
 
80
114
  // 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) {
115
+ if (point.distanceTo(line.p1) > line.length
116
+ || point.distanceTo(line.p2) > line.length) {
83
117
  return null;
84
118
  }
85
119
 
86
- return point;
87
- }).filter(entry => entry !== null) as Point2[];
120
+ return t;
121
+ }).filter(entry => entry !== null) as number[];
122
+ }
123
+
124
+ public override splitAt(t: number): [BezierJSWrapper] | [BezierJSWrapper, BezierJSWrapper] {
125
+ if (t <= 0 || t >= 1) {
126
+ return [ this ];
127
+ }
128
+
129
+ const bezier = this.getBezier();
130
+ const split = bezier.split(t);
131
+ return [
132
+ new BezierJSWrapperImpl(split.left.points.map(point => Vec2.ofXY(point)), split.left),
133
+ new BezierJSWrapperImpl(split.right.points.map(point => Vec2.ofXY(point)), split.right),
134
+ ];
135
+ }
136
+
137
+ public override nearestPointTo(point: Point2) {
138
+ // One implementation could be similar to this:
139
+ // const projection = this.getBezier().project(point);
140
+ // return {
141
+ // point: Vec2.ofXY(projection),
142
+ // parameterValue: projection.t!,
143
+ // };
144
+ // However, Bezier-js is rather impercise (and relies on a lookup table).
145
+ // Thus, we instead use Newton's Method:
146
+
147
+ // We want to find t such that f(t) = |B(t) - p|² is minimized.
148
+ // Expanding,
149
+ // f(t) = (Bₓ(t) - pₓ)² + (Bᵧ(t) - pᵧ)²
150
+ // ⇒ f'(t) = Dₜ(Bₓ(t) - pₓ)² + Dₜ(Bᵧ(t) - pᵧ)²
151
+ // ⇒ f'(t) = 2(Bₓ(t) - pₓ)(Bₓ'(t)) + 2(Bᵧ(t) - pᵧ)(Bᵧ'(t))
152
+ // = 2Bₓ(t)Bₓ'(t) - 2pₓBₓ'(t) + 2Bᵧ(t)Bᵧ'(t) - 2pᵧBᵧ'(t)
153
+ // ⇒ f''(t)= 2Bₓ'(t)Bₓ'(t) + 2Bₓ(t)Bₓ''(t) - 2pₓBₓ''(t) + 2Bᵧ'(t)Bᵧ'(t)
154
+ // + 2Bᵧ(t)Bᵧ''(t) - 2pᵧBᵧ''(t)
155
+ // Because f'(t) = 0 at relative extrema, we can use Newton's Method
156
+ // to improve on an initial guess.
157
+
158
+ const sqrDistAt = (t: number) => point.squareDistanceTo(this.at(t));
159
+ const yIntercept = sqrDistAt(0);
160
+ let t = 0;
161
+ let minSqrDist = yIntercept;
162
+
163
+ // Start by testing a few points:
164
+ const pointsToTest = 4;
165
+ for (let i = 0; i < pointsToTest; i ++) {
166
+ const testT = i / (pointsToTest - 1);
167
+ const testMinSqrDist = sqrDistAt(testT);
168
+
169
+ if (testMinSqrDist < minSqrDist) {
170
+ t = testT;
171
+ minSqrDist = testMinSqrDist;
172
+ }
173
+ }
174
+
175
+ // To use Newton's Method, we need to evaluate the second derivative of the distance
176
+ // function:
177
+ const secondDerivativeAt = (t: number) => {
178
+ // f''(t) = 2Bₓ'(t)Bₓ'(t) + 2Bₓ(t)Bₓ''(t) - 2pₓBₓ''(t)
179
+ // + 2Bᵧ'(t)Bᵧ'(t) + 2Bᵧ(t)Bᵧ''(t) - 2pᵧBᵧ''(t)
180
+ const b = this.at(t);
181
+ const bPrime = this.derivativeAt(t);
182
+ const bPrimePrime = this.secondDerivativeAt(t);
183
+ return (
184
+ 2 * bPrime.x * bPrime.x + 2 * b.x * bPrimePrime.x - 2 * point.x * bPrimePrime.x
185
+ + 2 * bPrime.y * bPrime.y + 2 * b.y * bPrimePrime.y - 2 * point.y * bPrimePrime.y
186
+ );
187
+ };
188
+ // Because we're zeroing f'(t), we also need to be able to compute it:
189
+ const derivativeAt = (t: number) => {
190
+ // f'(t) = 2Bₓ(t)Bₓ'(t) - 2pₓBₓ'(t) + 2Bᵧ(t)Bᵧ'(t) - 2pᵧBᵧ'(t)
191
+ const b = this.at(t);
192
+ const bPrime = this.derivativeAt(t);
193
+ return (
194
+ 2 * b.x * bPrime.x - 2 * point.x * bPrime.x
195
+ + 2 * b.y * bPrime.y - 2 * point.y * bPrime.y
196
+ );
197
+ };
198
+
199
+ const iterate = () => {
200
+ const slope = secondDerivativeAt(t);
201
+ if (slope === 0) return;
202
+
203
+ // We intersect a line through the point on f'(t) at t with the x-axis:
204
+ // y = m(x - x₀) + y₀
205
+ // ⇒ x - x₀ = (y - y₀) / m
206
+ // ⇒ x = (y - y₀) / m + x₀
207
+ //
208
+ // Thus, when zeroed,
209
+ // tN = (0 - f'(t)) / m + t
210
+ const newT = (0 - derivativeAt(t)) / slope + t;
211
+ //const distDiff = sqrDistAt(newT) - sqrDistAt(t);
212
+ //console.assert(distDiff <= 0, `${-distDiff} >= 0`);
213
+ t = newT;
214
+ if (t > 1) {
215
+ t = 1;
216
+ } else if (t < 0) {
217
+ t = 0;
218
+ }
219
+ };
220
+
221
+ for (let i = 0; i < 12; i++) {
222
+ iterate();
223
+ }
224
+
225
+ return { parameterValue: t, point: this.at(t) };
226
+ }
227
+
228
+ public intersectsBezier(other: BezierJSWrapper) {
229
+ const intersections = this.getBezier().intersects(other.getBezier()) as (string[] | null | undefined);
230
+ if (!intersections || intersections.length === 0) {
231
+ return [];
232
+ }
233
+
234
+ const result = [];
235
+ for (const intersection of intersections) {
236
+ // From http://pomax.github.io/bezierjs/#intersect-curve,
237
+ // .intersects returns an array of 't1/t2' pairs, where curve1.at(t1) gives the point.
238
+ const match = /^([-0-9.eE]+)\/([-0-9.eE]+)$/.exec(intersection);
239
+
240
+ if (!match) {
241
+ throw new Error(
242
+ `Incorrect format returned by .intersects: ${intersections} should be array of "number/number"!`
243
+ );
244
+ }
245
+
246
+ const t = parseFloat(match[1]);
247
+ result.push({
248
+ parameterValue: t,
249
+ point: this.at(t),
250
+ });
251
+ }
252
+ return result;
253
+ }
254
+
255
+ public override toString() {
256
+ return `Bézier(${this.getPoints().map(point => point.toString()).join(', ')})`;
257
+ }
258
+ }
259
+
260
+ /**
261
+ * Private concrete implementation of `BezierJSWrapper`, used by methods above that need to return a wrapper
262
+ * around a `Bezier`.
263
+ */
264
+ class BezierJSWrapperImpl extends BezierJSWrapper {
265
+ public constructor(private controlPoints: readonly Point2[], curve?: Bezier) {
266
+ super(curve);
267
+ }
88
268
 
89
- return intersectionPoints;
269
+ public override getPoints() {
270
+ return this.controlPoints;
90
271
  }
91
272
  }
92
273
 
@@ -28,7 +28,7 @@ describe('Line2', () => {
28
28
 
29
29
  expect(line1.intersection(line2)?.point).objEq(Vec2.of(0, 10));
30
30
 
31
- // t=10 implies 10 units along he line from (10, 10) to (-10, 10)
31
+ // t=10 implies 10 units along the line from (10, 10) to (-10, 10)
32
32
  expect(line1.intersection(line2)?.t).toBe(10);
33
33
 
34
34
  // Similarly, t = 12 implies 12 units above (0, -2) in the direction of (0, 200)
@@ -74,6 +74,14 @@ describe('Line2', () => {
74
74
  expect(line2.intersection(line1)).toBeNull();
75
75
  });
76
76
 
77
+ it('(9.559000000000001, 11.687)->(9.559, 11.67673) should intersect (9.56069, 11.68077)->(9.55719, 11.68077)', () => {
78
+ // Points taken from an issue observed in the editor.
79
+ const l1 = new LineSegment2(Vec2.of(9.559000000000001, 11.687), Vec2.of(9.559, 11.67673));
80
+ const l2 = new LineSegment2(Vec2.of(9.56069, 11.68077), Vec2.of(9.55719, 11.68077));
81
+ expect(l2.intersects(l1)).toBe(true);
82
+ expect(l1.intersects(l2)).toBe(true);
83
+ });
84
+
77
85
  it('Closest point to (0,0) on the line x = 1 should be (1,0)', () => {
78
86
  const line = new LineSegment2(Vec2.of(1, 100), Vec2.of(1, -100));
79
87
  expect(line.closestPointTo(Vec2.zero)).objEq(Vec2.of(1, 0));
@@ -96,4 +104,56 @@ describe('Line2', () => {
96
104
  p2: Vec2.of(3, 98),
97
105
  });
98
106
  });
107
+
108
+ it.each([
109
+ { from: Vec2.of(0, 0), to: Vec2.of(2, 2) },
110
+ { from: Vec2.of(100, 0), to: Vec2.of(2, 2) },
111
+ ])('should be able to split a line segment between %j', ({ from, to }) => {
112
+ const midpoint = from.lerp(to, 0.5);
113
+ const lineSegment = new LineSegment2(from, to);
114
+
115
+ // Halving
116
+ //
117
+ expect(lineSegment.at(0.5)).objEq(midpoint);
118
+ const [ firstHalf, secondHalf ] = lineSegment.splitAt(0.5);
119
+
120
+ if (!secondHalf) {
121
+ throw new Error('Splitting a line segment in half should yield two line segments.');
122
+ }
123
+
124
+ expect(firstHalf.p2).objEq(midpoint);
125
+ expect(firstHalf.p1).objEq(from);
126
+ expect(secondHalf.p2).objEq(to);
127
+ expect(secondHalf.p1).objEq(midpoint);
128
+
129
+ // Before start/end
130
+ expect(lineSegment.splitAt(0)[0]).objEq(lineSegment);
131
+ expect(lineSegment.splitAt(0)).toHaveLength(1);
132
+ expect(lineSegment.splitAt(1)).toHaveLength(1);
133
+ expect(lineSegment.splitAt(2)).toHaveLength(1);
134
+ });
135
+
136
+ it('equivalence check should allow ignoring direction', () => {
137
+ expect(new LineSegment2(Vec2.zero, Vec2.unitX)).objEq(new LineSegment2(Vec2.zero, Vec2.unitX));
138
+ expect(new LineSegment2(Vec2.zero, Vec2.unitX)).objEq(new LineSegment2(Vec2.unitX, Vec2.zero));
139
+ expect(new LineSegment2(Vec2.zero, Vec2.unitX)).not.objEq(new LineSegment2(Vec2.unitX, Vec2.zero), { ignoreDirection: false });
140
+ });
141
+
142
+ it('should support creating from a collection of points', () => {
143
+ expect(LineSegment2.ofSmallestContainingPoints([])).toBeNull();
144
+ expect(LineSegment2.ofSmallestContainingPoints([Vec2.of(1, 1)])).toBeNull();
145
+ expect(LineSegment2.ofSmallestContainingPoints(
146
+ [Vec2.of(1, 1), Vec2.of(1, 2), Vec2.of(3, 3)]
147
+ )).toBeNull();
148
+
149
+ expect(LineSegment2.ofSmallestContainingPoints(
150
+ [Vec2.of(1, 1), Vec2.of(1, 2)]
151
+ )).objEq(new LineSegment2(Vec2.of(1, 1), Vec2.of(1, 2)));
152
+ expect(LineSegment2.ofSmallestContainingPoints(
153
+ [Vec2.of(1, 1), Vec2.of(2, 2), Vec2.of(3, 3)]
154
+ )).objEq(new LineSegment2(Vec2.of(1, 1), Vec2.of(3, 3)));
155
+ expect(LineSegment2.ofSmallestContainingPoints(
156
+ [Vec2.of(3, 3), Vec2.of(2, 2), Vec2.of(2.4, 2.4), Vec2.of(3, 3)]
157
+ )).objEq(new LineSegment2(Vec2.of(2, 2), Vec2.of(3, 3)));
158
+ });
99
159
  });
@@ -1,7 +1,8 @@
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
 
6
7
  interface IntersectionResult {
7
8
  point: Point2;
@@ -9,7 +10,7 @@ interface IntersectionResult {
9
10
  }
10
11
 
11
12
  /** Represents a line segment. A `LineSegment2` is immutable. */
12
- export class LineSegment2 extends Abstract2DShape {
13
+ export class LineSegment2 extends Parameterized2DShape {
13
14
  // invariant: ||direction|| = 1
14
15
 
15
16
  /**
@@ -45,6 +46,31 @@ export class LineSegment2 extends Abstract2DShape {
45
46
  }
46
47
  }
47
48
 
49
+ /**
50
+ * Returns the smallest line segment that contains all points in `points`, or `null`
51
+ * if no such line segment exists.
52
+ *
53
+ * @example
54
+ * ```ts,runnable
55
+ * import {LineSegment2, Vec2} from '@js-draw/math';
56
+ * console.log(LineSegment2.ofSmallestContainingPoints([Vec2.of(1, 0), Vec2.of(0, 1)]));
57
+ * ```
58
+ */
59
+ public static ofSmallestContainingPoints(points: readonly Point2[]) {
60
+ if (points.length <= 1) return null;
61
+
62
+ const sorted = [...points].sort((a, b) => a.x !== b.x ? a.x - b.x : a.y - b.y);
63
+ const line = new LineSegment2(sorted[0], sorted[sorted.length - 1]);
64
+
65
+ for (const point of sorted) {
66
+ if (!line.containsPoint(point)) {
67
+ return null;
68
+ }
69
+ }
70
+
71
+ return line;
72
+ }
73
+
48
74
  // Accessors to make LineSegment2 compatible with bezier-js's
49
75
  // interface
50
76
 
@@ -58,8 +84,12 @@ export class LineSegment2 extends Abstract2DShape {
58
84
  return this.point2;
59
85
  }
60
86
 
87
+ public get center(): Point2 {
88
+ return this.point1.lerp(this.point2, 0.5);
89
+ }
90
+
61
91
  /**
62
- * Gets a point a distance `t` along this line.
92
+ * Gets a point a **distance** `t` along this line.
63
93
  *
64
94
  * @deprecated
65
95
  */
@@ -74,11 +104,40 @@ export class LineSegment2 extends Abstract2DShape {
74
104
  *
75
105
  * `t` should be in `[0, 1]`.
76
106
  */
77
- public at(t: number): Point2 {
107
+ public override at(t: number): Point2 {
78
108
  return this.get(t * this.length);
79
109
  }
80
110
 
111
+ public override normalAt(_t: number): Vec2 {
112
+ return this.direction.orthog();
113
+ }
114
+
115
+ public override tangentAt(_t: number): Vec3 {
116
+ return this.direction;
117
+ }
118
+
119
+ public splitAt(t: number): [LineSegment2]|[LineSegment2,LineSegment2] {
120
+ if (t <= 0 || t >= 1) {
121
+ return [this];
122
+ }
123
+
124
+ return [
125
+ new LineSegment2(this.point1, this.at(t)),
126
+ new LineSegment2(this.at(t), this.point2),
127
+ ];
128
+ }
129
+
130
+ /**
131
+ * Returns the intersection of this with another line segment.
132
+ *
133
+ * **WARNING**: The parameter value returned by this method does not range from 0 to 1 and
134
+ * is currently a length.
135
+ * This will change in a future release.
136
+ * @deprecated
137
+ */
81
138
  public intersection(other: LineSegment2): IntersectionResult|null {
139
+ // TODO(v2.0.0): Make this return a `t` value from `0` to `1`.
140
+
82
141
  // We want x₁(t) = x₂(t) and y₁(t) = y₂(t)
83
142
  // Observe that
84
143
  // x = this.point1.x + this.direction.x · t₁
@@ -105,7 +164,11 @@ export class LineSegment2 extends Abstract2DShape {
105
164
  // = ((o₁ᵧ - o₂ᵧ)((d₁ₓd₂ₓ)) + (d₂ᵧd₁ₓ)(o₂ₓ) - (d₁ᵧd₂ₓ)(o₁ₓ))/(d₂ᵧd₁ₓ - d₁ᵧd₂ₓ)
106
165
  // ⇒ y = o₁ᵧ + d₁ᵧ · (x - o₁ₓ) / d₁ₓ = ...
107
166
  let resultPoint, resultT;
108
- if (this.direction.x === 0) {
167
+
168
+ // Consider very-near-vertical lines to be vertical --- not doing so can lead to
169
+ // precision error when dividing by this.direction.x.
170
+ const small = 4e-13;
171
+ if (Math.abs(this.direction.x) < small) {
109
172
  // Vertical line: Where does the other have x = this.point1.x?
110
173
  // x = o₁ₓ = o₂ₓ + d₂ₓ · (y - o₂ᵧ) / d₂ᵧ
111
174
  // ⇒ (o₁ₓ - o₂ₓ)(d₂ᵧ/d₂ₓ) + o₂ᵧ = y
@@ -146,10 +209,11 @@ export class LineSegment2 extends Abstract2DShape {
146
209
  }
147
210
 
148
211
  // 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();
212
+ const resultToP1 = resultPoint.distanceTo(this.point1);
213
+ const resultToP2 = resultPoint.distanceTo(this.point2);
214
+ const resultToP3 = resultPoint.distanceTo(other.point1);
215
+ const resultToP4 = resultPoint.distanceTo(other.point2);
216
+
153
217
  if (resultToP1 > this.length
154
218
  || resultToP2 > this.length
155
219
  || resultToP3 > other.length
@@ -167,6 +231,15 @@ export class LineSegment2 extends Abstract2DShape {
167
231
  return this.intersection(other) !== null;
168
232
  }
169
233
 
234
+ public override argIntersectsLineSegment(lineSegment: LineSegment2) {
235
+ const intersection = this.intersection(lineSegment);
236
+
237
+ if (intersection) {
238
+ return [ intersection.t / this.length ];
239
+ }
240
+ return [];
241
+ }
242
+
170
243
  /**
171
244
  * Returns the points at which this line segment intersects the
172
245
  * given line segment.
@@ -186,6 +259,10 @@ export class LineSegment2 extends Abstract2DShape {
186
259
 
187
260
  // Returns the closest point on this to [target]
188
261
  public closestPointTo(target: Point2) {
262
+ return this.nearestPointTo(target).point;
263
+ }
264
+
265
+ public override nearestPointTo(target: Vec3): { point: Vec3; parameterValue: number; } {
189
266
  // Distance from P1 along this' direction.
190
267
  const projectedDistFromP1 = target.minus(this.p1).dot(this.direction);
191
268
  const projectedDistFromP2 = this.length - projectedDistFromP1;
@@ -193,13 +270,13 @@ export class LineSegment2 extends Abstract2DShape {
193
270
  const projection = this.p1.plus(this.direction.times(projectedDistFromP1));
194
271
 
195
272
  if (projectedDistFromP1 > 0 && projectedDistFromP1 < this.length) {
196
- return projection;
273
+ return { point: projection, parameterValue: projectedDistFromP1 / this.length };
197
274
  }
198
275
 
199
276
  if (Math.abs(projectedDistFromP2) < Math.abs(projectedDistFromP1)) {
200
- return this.p2;
277
+ return { point: this.p2, parameterValue: 1 };
201
278
  } else {
202
- return this.p1;
279
+ return { point: this.p1, parameterValue: 0 };
203
280
  }
204
281
  }
205
282
 
@@ -228,5 +305,26 @@ export class LineSegment2 extends Abstract2DShape {
228
305
  public override toString() {
229
306
  return `LineSegment(${this.p1.toString()}, ${this.p2.toString()})`;
230
307
  }
308
+
309
+ /**
310
+ * Returns `true` iff this is equivalent to `other`.
311
+ *
312
+ * **Options**:
313
+ * - `tolerance`: The maximum difference between endpoints. (Default: 0)
314
+ * - `ignoreDirection`: Allow matching a version of `this` with opposite direction. (Default: `true`)
315
+ */
316
+ public eq(other: LineSegment2, options?: { tolerance?: number, ignoreDirection?: boolean }) {
317
+ if (!(other instanceof LineSegment2)) {
318
+ return false;
319
+ }
320
+
321
+ const tolerance = options?.tolerance;
322
+ const ignoreDirection = options?.ignoreDirection ?? true;
323
+
324
+ return (
325
+ (other.p1.eq(this.p1, tolerance) && other.p2.eq(this.p2, tolerance))
326
+ || (ignoreDirection && other.p1.eq(this.p2, tolerance) && other.p2.eq(this.p1, tolerance))
327
+ );
328
+ }
231
329
  }
232
330
  export default LineSegment2;
@@ -0,0 +1,44 @@
1
+ import { Point2, Vec2 } from '../Vec2';
2
+ import Abstract2DShape from './Abstract2DShape';
3
+ import LineSegment2 from './LineSegment2';
4
+
5
+ /**
6
+ * A 2-dimensional path with parameter interval $t \in [0, 1]$.
7
+ *
8
+ * **Note:** Avoid extending this class outside of `js-draw` --- new abstract methods
9
+ * may be added between minor versions.
10
+ */
11
+ export abstract class Parameterized2DShape extends Abstract2DShape {
12
+ /** Returns this at a given parameter. $t \in [0, 1]$ */
13
+ abstract at(t: number): Point2;
14
+
15
+ /** Computes the unit normal vector at $t$. */
16
+ abstract normalAt(t: number): Vec2;
17
+
18
+ abstract tangentAt(t: number): Vec2;
19
+
20
+ /**
21
+ * Divides this shape into two separate shapes at parameter value $t$.
22
+ */
23
+ abstract splitAt(t: number): [ Parameterized2DShape ] | [ Parameterized2DShape, Parameterized2DShape ];
24
+
25
+ /**
26
+ * Returns the nearest point on `this` to `point` and the `parameterValue` at which
27
+ * that point occurs.
28
+ */
29
+ abstract nearestPointTo(point: Point2): { point: Point2, parameterValue: number };
30
+
31
+ /**
32
+ * Returns the **parameter values** at which `lineSegment` intersects this shape.
33
+ *
34
+ * See also {@link intersectsLineSegment}
35
+ */
36
+ public abstract argIntersectsLineSegment(lineSegment: LineSegment2): number[];
37
+
38
+
39
+ public override intersectsLineSegment(line: LineSegment2): Point2[] {
40
+ return this.argIntersectsLineSegment(line).map(t => this.at(t));
41
+ }
42
+ }
43
+
44
+ export default Parameterized2DShape;