@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,7 +1,7 @@
1
- import { Point2 } from '../Vec2';
1
+ import { Point2, Vec2 } from '../Vec2';
2
2
  import Vec3 from '../Vec3';
3
- import Abstract2DShape from './Abstract2DShape';
4
3
  import LineSegment2 from './LineSegment2';
4
+ import Parameterized2DShape from './Parameterized2DShape';
5
5
  import Rect2 from './Rect2';
6
6
 
7
7
  /**
@@ -9,18 +9,18 @@ import Rect2 from './Rect2';
9
9
  *
10
10
  * Access the internal `Point2` using the `p` property.
11
11
  */
12
- class PointShape2D extends Abstract2DShape {
12
+ class PointShape2D extends Parameterized2DShape {
13
13
  public constructor(public readonly p: Point2) {
14
14
  super();
15
15
  }
16
16
 
17
17
  public override signedDistance(point: Vec3): number {
18
- return this.p.minus(point).magnitude();
18
+ return this.p.distanceTo(point);
19
19
  }
20
20
 
21
- public override intersectsLineSegment(lineSegment: LineSegment2, epsilon?: number): Vec3[] {
21
+ public override argIntersectsLineSegment(lineSegment: LineSegment2, epsilon?: number): number[] {
22
22
  if (lineSegment.containsPoint(this.p, epsilon)) {
23
- return [ this.p ];
23
+ return [ 0 ];
24
24
  }
25
25
  return [ ];
26
26
  }
@@ -28,6 +28,33 @@ class PointShape2D extends Abstract2DShape {
28
28
  public override getTightBoundingBox(): Rect2 {
29
29
  return new Rect2(this.p.x, this.p.y, 0, 0);
30
30
  }
31
+
32
+ public override at(_t: number) {
33
+ return this.p;
34
+ }
35
+
36
+ /**
37
+ * Returns an arbitrary unit-length vector.
38
+ */
39
+ public override normalAt(_t: number) {
40
+ // Return a vector that makes sense.
41
+ return Vec2.unitY;
42
+ }
43
+
44
+ public override tangentAt(_t: number): Vec3 {
45
+ return Vec2.unitX;
46
+ }
47
+
48
+ public override splitAt(_t: number): [PointShape2D] {
49
+ return [this];
50
+ }
51
+
52
+ public override nearestPointTo(_point: Point2) {
53
+ return {
54
+ point: this.p,
55
+ parameterValue: 0,
56
+ };
57
+ }
31
58
  }
32
59
 
33
60
  export default PointShape2D;
@@ -2,13 +2,12 @@ import { Vec2 } from '../Vec2';
2
2
  import QuadraticBezier from './QuadraticBezier';
3
3
 
4
4
  describe('QuadraticBezier', () => {
5
- it('approxmiateDistance should approximately return the distance to the curve', () => {
6
- const curves = [
7
- new QuadraticBezier(Vec2.zero, Vec2.of(10, 0), Vec2.of(20, 0)),
8
- new QuadraticBezier(Vec2.of(-10, 0), Vec2.of(2, 10), Vec2.of(20, 0)),
9
- new QuadraticBezier(Vec2.of(0, 0), Vec2.of(4, -10), Vec2.of(20, 60)),
10
- new QuadraticBezier(Vec2.of(0, 0), Vec2.of(4, -10), Vec2.of(-20, 60)),
11
- ];
5
+ test.each([
6
+ new QuadraticBezier(Vec2.zero, Vec2.of(10, 0), Vec2.of(20, 0)),
7
+ new QuadraticBezier(Vec2.of(-10, 0), Vec2.of(2, 10), Vec2.of(20, 0)),
8
+ new QuadraticBezier(Vec2.of(0, 0), Vec2.of(4, -10), Vec2.of(20, 60)),
9
+ new QuadraticBezier(Vec2.of(0, 0), Vec2.of(4, -10), Vec2.of(-20, 60)),
10
+ ])('approxmiateDistance should approximately return the distance to the curve (%s)', (curve) => {
12
11
  const testPoints = [
13
12
  Vec2.of(1, 1),
14
13
  Vec2.of(-1, 1),
@@ -18,14 +17,72 @@ describe('QuadraticBezier', () => {
18
17
  Vec2.of(5, 0),
19
18
  ];
20
19
 
20
+ for (const point of testPoints) {
21
+ const actualDist = curve.distance(point);
22
+ const approxDist = curve.approximateDistance(point);
23
+
24
+ expect(approxDist).toBeGreaterThan(actualDist * 0.6 - 0.25);
25
+ expect(approxDist).toBeLessThan(actualDist * 1.5 + 2.6);
26
+ }
27
+ });
28
+
29
+ test.each([
30
+ [ new QuadraticBezier(Vec2.zero, Vec2.unitX, Vec2.unitY), Vec2.zero, 0 ],
31
+ [ new QuadraticBezier(Vec2.zero, Vec2.unitX, Vec2.unitY), Vec2.unitY, 1 ],
32
+
33
+ [ new QuadraticBezier(Vec2.zero, Vec2.of(0.5, 0), Vec2.of(1, 0)), Vec2.of(0.4, 0), 0.4],
34
+ [ new QuadraticBezier(Vec2.zero, Vec2.of(0, 0.5), Vec2.of(0, 1)), Vec2.of(0, 0.4), 0.4],
35
+ [ new QuadraticBezier(Vec2.zero, Vec2.unitX, Vec2.unitY), Vec2.unitX, 0.42514 ],
36
+
37
+ // Should not return an out-of-range parameter
38
+ [ new QuadraticBezier(Vec2.zero, Vec2.of(0, 0.5), Vec2.unitY), Vec2.of(0, -1000), 0 ],
39
+ [ new QuadraticBezier(Vec2.zero, Vec2.of(0, 0.5), Vec2.unitY), Vec2.of(0, 1000), 1 ],
40
+
41
+ // Edge case -- just a point
42
+ [ new QuadraticBezier(Vec2.zero, Vec2.zero, Vec2.zero), Vec2.of(0, 1000), 0 ],
43
+ ])('nearestPointTo should return the nearest point and parameter value on %s to %s', (bezier, point, expectedParameter) => {
44
+ const nearest = bezier.nearestPointTo(point);
45
+ expect(nearest.parameterValue).toBeCloseTo(expectedParameter, 0.0001);
46
+ expect(nearest.point).objEq(bezier.at(nearest.parameterValue));
47
+ });
48
+
49
+ test('.normalAt should return a unit normal vector at the given parameter value', () => {
50
+ const curves = [
51
+ new QuadraticBezier(Vec2.zero, Vec2.unitY, Vec2.unitY.times(2)),
52
+ new QuadraticBezier(Vec2.zero, Vec2.unitX, Vec2.unitY),
53
+ new QuadraticBezier(Vec2.zero, Vec2.unitX, Vec2.unitY.times(-2)),
54
+ new QuadraticBezier(Vec2.of(2, 3), Vec2.of(4, 5.1), Vec2.of(6, 7)),
55
+ new QuadraticBezier(Vec2.of(2, 3), Vec2.of(100, 1000), Vec2.unitY.times(-2)),
56
+ ];
57
+
21
58
  for (const curve of curves) {
22
- for (const point of testPoints) {
23
- const actualDist = curve.distance(point);
24
- const approxDist = curve.approximateDistance(point);
59
+ for (let t = 0; t < 1; t += 0.1) {
60
+ const normal = curve.normalAt(t);
61
+ expect(normal.length()).toBe(1);
62
+
63
+ const tangentApprox = curve.at(t + 0.001).minus(curve.at(t - 0.001));
25
64
 
26
- expect(approxDist).toBeGreaterThan(actualDist * 0.6 - 0.25);
27
- expect(approxDist).toBeLessThan(actualDist * 1.5 + 2.6);
65
+ // The tangent vector should be perpindicular to the normal
66
+ expect(tangentApprox.dot(normal)).toBeCloseTo(0);
28
67
  }
29
68
  }
30
69
  });
70
+
71
+ test.each([
72
+ new QuadraticBezier(Vec2.zero, Vec2.unitY, Vec2.unitY.times(2)),
73
+ new QuadraticBezier(Vec2.zero, Vec2.unitX, Vec2.unitY),
74
+ new QuadraticBezier(Vec2.zero, Vec2.unitY, Vec2.unitX),
75
+ ])('.derivativeAt should return a derivative vector with the correct direction (curve: %s)', (curve) => {
76
+ for (let t = 0; t < 1; t += 0.1) {
77
+ const derivative = curve.derivativeAt(t);
78
+ const derivativeApprox = curve.at(t + 0.001).minus(curve.at(t - 0.001));
79
+ expect(derivativeApprox.normalized()).objEq(derivative.normalized(), 0.01);
80
+ }
81
+ });
82
+
83
+ test('should support Bezier-Bezier intersections', () => {
84
+ const b1 = new QuadraticBezier(Vec2.zero, Vec2.unitX, Vec2.unitY);
85
+ const b2 = new QuadraticBezier(Vec2.of(-1, 0.5), Vec2.of(0, 0.6), Vec2.of(1, 0.4));
86
+ expect(b1.intersectsBezier(b2)).toHaveLength(1);
87
+ });
31
88
  });
@@ -4,10 +4,9 @@ import BezierJSWrapper from './BezierJSWrapper';
4
4
  import Rect2 from './Rect2';
5
5
 
6
6
  /**
7
- * A wrapper around `bezier-js`'s quadratic Bézier.
7
+ * Represents a 2D Bézier curve.
8
8
  *
9
- * This wrappper lazy-loads `bezier-js`'s Bézier and can perform some operations
10
- * without loading it at all (e.g. `normal`, `at`, and `approximateDistance`).
9
+ * **Note**: Many Bézier operations use `bezier-js`'s.
11
10
  */
12
11
  export class QuadraticBezier extends BezierJSWrapper {
13
12
  public constructor(
@@ -30,10 +29,19 @@ export class QuadraticBezier extends BezierJSWrapper {
30
29
  return -2 * p0 + 2 * p1 + 2 * t * (p0 - 2 * p1 + p2);
31
30
  }
32
31
 
32
+ private static secondDerivativeComponentAt(t: number, p0: number, p1: number, p2: number) {
33
+ return 2 * (p0 - 2 * p1 + p2);
34
+ }
35
+
33
36
  /**
34
37
  * @returns the curve evaluated at `t`.
38
+ *
39
+ * `t` should be a number in `[0, 1]`.
35
40
  */
36
41
  public override at(t: number): Point2 {
42
+ if (t === 0) return this.p0;
43
+ if (t === 1) return this.p2;
44
+
37
45
  const p0 = this.p0;
38
46
  const p1 = this.p1;
39
47
  const p2 = this.p2;
@@ -53,6 +61,16 @@ export class QuadraticBezier extends BezierJSWrapper {
53
61
  );
54
62
  }
55
63
 
64
+ public override secondDerivativeAt(t: number): Point2 {
65
+ const p0 = this.p0;
66
+ const p1 = this.p1;
67
+ const p2 = this.p2;
68
+ return Vec2.of(
69
+ QuadraticBezier.secondDerivativeComponentAt(t, p0.x, p1.x, p2.x),
70
+ QuadraticBezier.secondDerivativeComponentAt(t, p0.y, p1.y, p2.y),
71
+ );
72
+ }
73
+
56
74
  public override normal(t: number): Vec2 {
57
75
  const tangent = this.derivativeAt(t);
58
76
  return tangent.orthog().normalized();
@@ -126,11 +144,10 @@ export class QuadraticBezier extends BezierJSWrapper {
126
144
 
127
145
  const at1 = this.at(min1);
128
146
  const at2 = this.at(min2);
129
- const sqrDist1 = at1.minus(point).magnitudeSquared();
130
- const sqrDist2 = at2.minus(point).magnitudeSquared();
131
- const sqrDist3 = this.at(0).minus(point).magnitudeSquared();
132
- const sqrDist4 = this.at(1).minus(point).magnitudeSquared();
133
-
147
+ const sqrDist1 = at1.squareDistanceTo(point);
148
+ const sqrDist2 = at2.squareDistanceTo(point);
149
+ const sqrDist3 = this.at(0).squareDistanceTo(point);
150
+ const sqrDist4 = this.at(1).squareDistanceTo(point);
134
151
 
135
152
  return Math.sqrt(Math.min(sqrDist1, sqrDist2, sqrDist3, sqrDist4));
136
153
  }
@@ -4,7 +4,7 @@ import { Point2, Vec2 } from '../Vec2';
4
4
  import Abstract2DShape from './Abstract2DShape';
5
5
  import Vec3 from '../Vec3';
6
6
 
7
- /** An object that can be converted to a Rect2. */
7
+ /** An object that can be converted to a {@link Rect2}. */
8
8
  export interface RectTemplate {
9
9
  x: number;
10
10
  y: number;
@@ -14,7 +14,11 @@ export interface RectTemplate {
14
14
  height?: number;
15
15
  }
16
16
 
17
- // invariant: w ≥ 0, h ≥ 0, immutable
17
+ /**
18
+ * Represents a rectangle in 2D space, parallel to the XY axes.
19
+ *
20
+ * `invariant: w ≥ 0, h ≥ 0, immutable`
21
+ */
18
22
  export class Rect2 extends Abstract2DShape {
19
23
  // Derived state:
20
24
 
@@ -67,6 +71,9 @@ export class Rect2 extends Abstract2DShape {
67
71
  && this.y + this.h >= other.y + other.h;
68
72
  }
69
73
 
74
+ /**
75
+ * @returns true iff this and `other` overlap
76
+ */
70
77
  public intersects(other: Rect2): boolean {
71
78
  // Project along x/y axes.
72
79
  const thisMinX = this.x;
@@ -181,7 +188,7 @@ export class Rect2 extends Abstract2DShape {
181
188
  let closest: Point2|null = null;
182
189
  let closestDist: number|null = null;
183
190
  for (const point of closestEdgePoints) {
184
- const dist = point.minus(target).length();
191
+ const dist = point.distanceTo(target);
185
192
  if (closestDist === null || dist < closestDist) {
186
193
  closest = point;
187
194
  closestDist = dist;
@@ -0,0 +1,43 @@
1
+ import { Vec2 } from '../Vec2';
2
+ import { Rect2 } from '../shapes/Rect2';
3
+ import convexHull2Of from './convexHull2Of';
4
+
5
+ describe('convexHull2Of', () => {
6
+ it.each([
7
+ [ [ Vec2.of(1, 1) ] , [ Vec2.of(1, 1) ] ],
8
+
9
+ // Line
10
+ [ [ Vec2.of(1, 1), Vec2.of(2, 2) ] , [ Vec2.of(1, 1), Vec2.of(2, 2) ] ],
11
+
12
+ // Just a triangle
13
+ [ [ Vec2.of(1, 1), Vec2.of(4, 2), Vec2.of(3, 3) ] , [ Vec2.of(1, 1), Vec2.of(4, 2), Vec2.of(3, 3) ]],
14
+
15
+ // Triangle with an extra point
16
+ [ [ Vec2.of(1, 1), Vec2.of(2, 20), Vec2.of(3, 5), Vec2.of(4, 3) ] , [ Vec2.of(1, 1), Vec2.of(4, 3), Vec2.of(2, 20) ]],
17
+
18
+ // Points within a triangle
19
+ [
20
+ [ Vec2.of(28, 5), Vec2.of(4, 5), Vec2.of(-100, -100), Vec2.of(7, 120), Vec2.of(1, 8), Vec2.of(100, -100), Vec2.of(2, 4), Vec2.of(3, 4), Vec2.of(4, 5) ],
21
+ [ Vec2.of(-100, -100), Vec2.of(100, -100), Vec2.of(7, 120) ],
22
+ ],
23
+
24
+ // Points within a triangle (repeated vertex)
25
+ [
26
+ [ Vec2.of(28, 5), Vec2.of(4, 5), Vec2.of(-100, -100), Vec2.of(-100, -100), Vec2.of(7, 120), Vec2.of(1, 8), Vec2.of(100, -100), Vec2.of(2, 4), Vec2.of(3, 4), Vec2.of(4, 5) ],
27
+ [ Vec2.of(-100, -100), Vec2.of(100, -100), Vec2.of(7, 120) ],
28
+ ],
29
+
30
+ // Points within a square
31
+ [
32
+ [ Vec2.of(28, 5), Vec2.of(4, 5), Vec2.of(-100, -100), Vec2.of(100, 100), Vec2.of(7, 100), Vec2.of(1, 8), Vec2.of(-100, 100), Vec2.of(100, -100), Vec2.of(2, 4), Vec2.of(3, 4), Vec2.of(4, 5) ],
33
+ [ Vec2.of(-100, -100), Vec2.of(100, -100), Vec2.of(100, 100), Vec2.of(-100, 100) ],
34
+ ],
35
+
36
+ [
37
+ Rect2.unitSquare.corners,
38
+ [ Vec2.of(1, 0), Vec2.of(1, 1), Vec2.of(0, 1), Vec2.of(0, 0) ],
39
+ ]
40
+ ])('should compute the convex hull of a set of points (%j)', (points, expected) => {
41
+ expect(convexHull2Of(points)).toMatchObject(expected);
42
+ });
43
+ });
@@ -0,0 +1,71 @@
1
+ import { Point2, Vec2 } from '../Vec2';
2
+
3
+ /**
4
+ * Implements Gift Wrapping, in $O(nh)$. This algorithm is not the most efficient in the worst case.
5
+ *
6
+ * See https://en.wikipedia.org/wiki/Gift_wrapping_algorithm
7
+ * and https://www.cs.jhu.edu/~misha/Spring16/06.pdf
8
+ */
9
+ const convexHull2Of = (points: Point2[]) => {
10
+ if (points.length === 0) {
11
+ return [];
12
+ }
13
+
14
+ // 1. Start with a vertex on the hull
15
+ const lowestPoint = points.reduce(
16
+ (lowest, current) => current.y < lowest.y ? current : lowest,
17
+ points[0]
18
+ );
19
+ const vertices = [ lowestPoint ];
20
+ let toProcess = [...points.filter(p => !p.eq(lowestPoint))];
21
+ let lastBaseDirection = Vec2.of(-1, 0);
22
+
23
+ // 2. Find the point with greatest angle from the vertex:
24
+ //
25
+ // . . .
26
+ // . . / <- Notice that **all** other points are to the
27
+ // / **left** of the vector from the current
28
+ // ./ vertex to the new point.
29
+ while (toProcess.length > 0) {
30
+ const lastVertex = vertices[vertices.length - 1];
31
+
32
+ let smallestDotProductSoFar: number = lastBaseDirection.dot(lowestPoint.minus(lastVertex).normalizedOrZero());
33
+ let furthestPointSoFar = lowestPoint;
34
+ for (const point of toProcess) {
35
+ // Maximizing the angle is the same as minimizing the dot product:
36
+ // point.minus(lastVertex)
37
+ // ^
38
+ // /
39
+ // /
40
+ // ϑ /
41
+ // <-----. lastBaseDirection
42
+ const currentDotProduct = lastBaseDirection.dot(point.minus(lastVertex).normalizedOrZero());
43
+
44
+ if (currentDotProduct <= smallestDotProductSoFar) {
45
+ furthestPointSoFar = point;
46
+ smallestDotProductSoFar = currentDotProduct;
47
+ }
48
+ }
49
+ toProcess = toProcess.filter(p => !p.eq(furthestPointSoFar));
50
+
51
+ const newBaseDirection = furthestPointSoFar.minus(lastVertex).normalized();
52
+
53
+ // If the last vertex is on the same edge as the current, there's no need to include
54
+ // the previous one.
55
+ if (Math.abs(newBaseDirection.dot(lastBaseDirection)) === 1 && vertices.length > 1) {
56
+ vertices.pop();
57
+ }
58
+
59
+ // Stoping condition: We've gone in a full circle.
60
+ if (furthestPointSoFar.eq(lowestPoint)) {
61
+ break;
62
+ } else {
63
+ vertices.push(furthestPointSoFar);
64
+ lastBaseDirection = lastVertex.minus(furthestPointSoFar).normalized();
65
+ }
66
+ }
67
+
68
+ return vertices;
69
+ };
70
+
71
+ export default convexHull2Of;