@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,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;