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