@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,26 +1,49 @@
1
- import Abstract2DShape from './Abstract2DShape.mjs';
1
+ import { Vec2 } from '../Vec2.mjs';
2
+ import Parameterized2DShape from './Parameterized2DShape.mjs';
2
3
  import Rect2 from './Rect2.mjs';
3
4
  /**
4
5
  * Like a {@link Point2}, but with additional functionality (e.g. SDF).
5
6
  *
6
7
  * Access the internal `Point2` using the `p` property.
7
8
  */
8
- class PointShape2D extends Abstract2DShape {
9
+ class PointShape2D extends Parameterized2DShape {
9
10
  constructor(p) {
10
11
  super();
11
12
  this.p = p;
12
13
  }
13
14
  signedDistance(point) {
14
- return this.p.minus(point).magnitude();
15
+ return this.p.distanceTo(point);
15
16
  }
16
- intersectsLineSegment(lineSegment, epsilon) {
17
+ argIntersectsLineSegment(lineSegment, epsilon) {
17
18
  if (lineSegment.containsPoint(this.p, epsilon)) {
18
- return [this.p];
19
+ return [0];
19
20
  }
20
21
  return [];
21
22
  }
22
23
  getTightBoundingBox() {
23
24
  return new Rect2(this.p.x, this.p.y, 0, 0);
24
25
  }
26
+ at(_t) {
27
+ return this.p;
28
+ }
29
+ /**
30
+ * Returns an arbitrary unit-length vector.
31
+ */
32
+ normalAt(_t) {
33
+ // Return a vector that makes sense.
34
+ return Vec2.unitY;
35
+ }
36
+ tangentAt(_t) {
37
+ return Vec2.unitX;
38
+ }
39
+ splitAt(_t) {
40
+ return [this];
41
+ }
42
+ nearestPointTo(_point) {
43
+ return {
44
+ point: this.p,
45
+ parameterValue: 0,
46
+ };
47
+ }
25
48
  }
26
49
  export default PointShape2D;
@@ -2,10 +2,9 @@ import { Point2, Vec2 } from '../Vec2';
2
2
  import BezierJSWrapper from './BezierJSWrapper';
3
3
  import Rect2 from './Rect2';
4
4
  /**
5
- * A wrapper around `bezier-js`'s quadratic Bézier.
5
+ * Represents a 2D Bézier curve.
6
6
  *
7
- * This wrappper lazy-loads `bezier-js`'s Bézier and can perform some operations
8
- * without loading it at all (e.g. `normal`, `at`, and `approximateDistance`).
7
+ * **Note**: Many Bézier operations use `bezier-js`'s.
9
8
  */
10
9
  export declare class QuadraticBezier extends BezierJSWrapper {
11
10
  readonly p0: Point2;
@@ -18,11 +17,15 @@ export declare class QuadraticBezier extends BezierJSWrapper {
18
17
  */
19
18
  private static componentAt;
20
19
  private static derivativeComponentAt;
20
+ private static secondDerivativeComponentAt;
21
21
  /**
22
22
  * @returns the curve evaluated at `t`.
23
+ *
24
+ * `t` should be a number in `[0, 1]`.
23
25
  */
24
26
  at(t: number): Point2;
25
27
  derivativeAt(t: number): Point2;
28
+ secondDerivativeAt(t: number): Point2;
26
29
  normal(t: number): Vec2;
27
30
  /** @returns an overestimate of this shape's bounding box. */
28
31
  getLooseBoundingBox(): Rect2;
@@ -3,10 +3,9 @@ import solveQuadratic from '../polynomial/solveQuadratic.mjs';
3
3
  import BezierJSWrapper from './BezierJSWrapper.mjs';
4
4
  import Rect2 from './Rect2.mjs';
5
5
  /**
6
- * A wrapper around `bezier-js`'s quadratic Bézier.
6
+ * Represents a 2D Bézier curve.
7
7
  *
8
- * This wrappper lazy-loads `bezier-js`'s Bézier and can perform some operations
9
- * without loading it at all (e.g. `normal`, `at`, and `approximateDistance`).
8
+ * **Note**: Many Bézier operations use `bezier-js`'s.
10
9
  */
11
10
  export class QuadraticBezier extends BezierJSWrapper {
12
11
  constructor(p0, p1, p2) {
@@ -25,10 +24,19 @@ export class QuadraticBezier extends BezierJSWrapper {
25
24
  static derivativeComponentAt(t, p0, p1, p2) {
26
25
  return -2 * p0 + 2 * p1 + 2 * t * (p0 - 2 * p1 + p2);
27
26
  }
27
+ static secondDerivativeComponentAt(t, p0, p1, p2) {
28
+ return 2 * (p0 - 2 * p1 + p2);
29
+ }
28
30
  /**
29
31
  * @returns the curve evaluated at `t`.
32
+ *
33
+ * `t` should be a number in `[0, 1]`.
30
34
  */
31
35
  at(t) {
36
+ if (t === 0)
37
+ return this.p0;
38
+ if (t === 1)
39
+ return this.p2;
32
40
  const p0 = this.p0;
33
41
  const p1 = this.p1;
34
42
  const p2 = this.p2;
@@ -40,6 +48,12 @@ export class QuadraticBezier extends BezierJSWrapper {
40
48
  const p2 = this.p2;
41
49
  return Vec2.of(QuadraticBezier.derivativeComponentAt(t, p0.x, p1.x, p2.x), QuadraticBezier.derivativeComponentAt(t, p0.y, p1.y, p2.y));
42
50
  }
51
+ secondDerivativeAt(t) {
52
+ const p0 = this.p0;
53
+ const p1 = this.p1;
54
+ const p2 = this.p2;
55
+ return Vec2.of(QuadraticBezier.secondDerivativeComponentAt(t, p0.x, p1.x, p2.x), QuadraticBezier.secondDerivativeComponentAt(t, p0.y, p1.y, p2.y));
56
+ }
43
57
  normal(t) {
44
58
  const tangent = this.derivativeAt(t);
45
59
  return tangent.orthog().normalized();
@@ -100,10 +114,10 @@ export class QuadraticBezier extends BezierJSWrapper {
100
114
  }
101
115
  const at1 = this.at(min1);
102
116
  const at2 = this.at(min2);
103
- const sqrDist1 = at1.minus(point).magnitudeSquared();
104
- const sqrDist2 = at2.minus(point).magnitudeSquared();
105
- const sqrDist3 = this.at(0).minus(point).magnitudeSquared();
106
- const sqrDist4 = this.at(1).minus(point).magnitudeSquared();
117
+ const sqrDist1 = at1.squareDistanceTo(point);
118
+ const sqrDist2 = at2.squareDistanceTo(point);
119
+ const sqrDist3 = this.at(0).squareDistanceTo(point);
120
+ const sqrDist4 = this.at(1).squareDistanceTo(point);
107
121
  return Math.sqrt(Math.min(sqrDist1, sqrDist2, sqrDist3, sqrDist4));
108
122
  }
109
123
  getPoints() {
@@ -3,7 +3,7 @@ import Mat33 from '../Mat33';
3
3
  import { Point2, Vec2 } from '../Vec2';
4
4
  import Abstract2DShape from './Abstract2DShape';
5
5
  import Vec3 from '../Vec3';
6
- /** An object that can be converted to a Rect2. */
6
+ /** An object that can be converted to a {@link Rect2}. */
7
7
  export interface RectTemplate {
8
8
  x: number;
9
9
  y: number;
@@ -12,6 +12,11 @@ export interface RectTemplate {
12
12
  width?: number;
13
13
  height?: number;
14
14
  }
15
+ /**
16
+ * Represents a rectangle in 2D space, parallel to the XY axes.
17
+ *
18
+ * `invariant: w ≥ 0, h ≥ 0, immutable`
19
+ */
15
20
  export declare class Rect2 extends Abstract2DShape {
16
21
  readonly x: number;
17
22
  readonly y: number;
@@ -25,6 +30,9 @@ export declare class Rect2 extends Abstract2DShape {
25
30
  resizedTo(size: Vec2): Rect2;
26
31
  containsPoint(other: Point2): boolean;
27
32
  containsRect(other: Rect2): boolean;
33
+ /**
34
+ * @returns true iff this and `other` overlap
35
+ */
28
36
  intersects(other: Rect2): boolean;
29
37
  intersection(other: Rect2): Rect2 | null;
30
38
  union(other: Rect2): Rect2;
@@ -1,7 +1,11 @@
1
1
  import LineSegment2 from './LineSegment2.mjs';
2
2
  import { Vec2 } from '../Vec2.mjs';
3
3
  import Abstract2DShape from './Abstract2DShape.mjs';
4
- // invariant: w ≥ 0, h ≥ 0, immutable
4
+ /**
5
+ * Represents a rectangle in 2D space, parallel to the XY axes.
6
+ *
7
+ * `invariant: w ≥ 0, h ≥ 0, immutable`
8
+ */
5
9
  export class Rect2 extends Abstract2DShape {
6
10
  constructor(x, y, w, h) {
7
11
  super();
@@ -38,6 +42,9 @@ export class Rect2 extends Abstract2DShape {
38
42
  && this.x + this.w >= other.x + other.w
39
43
  && this.y + this.h >= other.y + other.h;
40
44
  }
45
+ /**
46
+ * @returns true iff this and `other` overlap
47
+ */
41
48
  intersects(other) {
42
49
  // Project along x/y axes.
43
50
  const thisMinX = this.x;
@@ -124,7 +131,7 @@ export class Rect2 extends Abstract2DShape {
124
131
  let closest = null;
125
132
  let closestDist = null;
126
133
  for (const point of closestEdgePoints) {
127
- const dist = point.minus(target).length();
134
+ const dist = point.distanceTo(target);
128
135
  if (closestDist === null || dist < closestDist) {
129
136
  closest = point;
130
137
  closestDist = dist;
@@ -0,0 +1,9 @@
1
+ import { Point2 } from '../Vec2';
2
+ /**
3
+ * Implements Gift Wrapping, in $O(nh)$. This algorithm is not the most efficient in the worst case.
4
+ *
5
+ * See https://en.wikipedia.org/wiki/Gift_wrapping_algorithm
6
+ * and https://www.cs.jhu.edu/~misha/Spring16/06.pdf
7
+ */
8
+ declare const convexHull2Of: (points: Point2[]) => import("../Vec3").Vec3[];
9
+ export default convexHull2Of;
@@ -0,0 +1,59 @@
1
+ import { Vec2 } from '../Vec2.mjs';
2
+ /**
3
+ * Implements Gift Wrapping, in $O(nh)$. This algorithm is not the most efficient in the worst case.
4
+ *
5
+ * See https://en.wikipedia.org/wiki/Gift_wrapping_algorithm
6
+ * and https://www.cs.jhu.edu/~misha/Spring16/06.pdf
7
+ */
8
+ const convexHull2Of = (points) => {
9
+ if (points.length === 0) {
10
+ return [];
11
+ }
12
+ // 1. Start with a vertex on the hull
13
+ const lowestPoint = points.reduce((lowest, current) => current.y < lowest.y ? current : lowest, points[0]);
14
+ const vertices = [lowestPoint];
15
+ let toProcess = [...points.filter(p => !p.eq(lowestPoint))];
16
+ let lastBaseDirection = Vec2.of(-1, 0);
17
+ // 2. Find the point with greatest angle from the vertex:
18
+ //
19
+ // . . .
20
+ // . . / <- Notice that **all** other points are to the
21
+ // / **left** of the vector from the current
22
+ // ./ vertex to the new point.
23
+ while (toProcess.length > 0) {
24
+ const lastVertex = vertices[vertices.length - 1];
25
+ let smallestDotProductSoFar = lastBaseDirection.dot(lowestPoint.minus(lastVertex).normalizedOrZero());
26
+ let furthestPointSoFar = lowestPoint;
27
+ for (const point of toProcess) {
28
+ // Maximizing the angle is the same as minimizing the dot product:
29
+ // point.minus(lastVertex)
30
+ // ^
31
+ // /
32
+ // /
33
+ // ϑ /
34
+ // <-----. lastBaseDirection
35
+ const currentDotProduct = lastBaseDirection.dot(point.minus(lastVertex).normalizedOrZero());
36
+ if (currentDotProduct <= smallestDotProductSoFar) {
37
+ furthestPointSoFar = point;
38
+ smallestDotProductSoFar = currentDotProduct;
39
+ }
40
+ }
41
+ toProcess = toProcess.filter(p => !p.eq(furthestPointSoFar));
42
+ const newBaseDirection = furthestPointSoFar.minus(lastVertex).normalized();
43
+ // If the last vertex is on the same edge as the current, there's no need to include
44
+ // the previous one.
45
+ if (Math.abs(newBaseDirection.dot(lastBaseDirection)) === 1 && vertices.length > 1) {
46
+ vertices.pop();
47
+ }
48
+ // Stoping condition: We've gone in a full circle.
49
+ if (furthestPointSoFar.eq(lowestPoint)) {
50
+ break;
51
+ }
52
+ else {
53
+ vertices.push(furthestPointSoFar);
54
+ lastBaseDirection = lastVertex.minus(furthestPointSoFar).normalized();
55
+ }
56
+ }
57
+ return vertices;
58
+ };
59
+ export default convexHull2Of;
@@ -0,0 +1 @@
1
+ export {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@js-draw/math",
3
- "version": "1.16.0",
3
+ "version": "1.18.0",
4
4
  "description": "A math library for js-draw. ",
5
5
  "types": "./dist/mjs/lib.d.ts",
6
6
  "main": "./dist/cjs/lib.js",
@@ -21,14 +21,14 @@
21
21
  "scripts": {
22
22
  "dist-test": "cd dist-test/test_imports && npm install && npm run test",
23
23
  "dist": "npm run build && npm run dist-test",
24
- "build": "rm -rf ./dist && mkdir dist && build-tool build",
25
- "watch": "rm -rf ./dist/* && mkdir -p dist && build-tool watch"
24
+ "build": "rm -rf ./dist && build-tool build",
25
+ "watch": "build-tool watch"
26
26
  },
27
27
  "dependencies": {
28
28
  "bezier-js": "6.1.3"
29
29
  },
30
30
  "devDependencies": {
31
- "@js-draw/build-tool": "^1.11.1",
31
+ "@js-draw/build-tool": "^1.17.0",
32
32
  "@types/bezier-js": "4.1.0",
33
33
  "@types/jest": "29.5.5",
34
34
  "@types/jsdom": "21.1.3"
@@ -45,5 +45,5 @@
45
45
  "svg",
46
46
  "math"
47
47
  ],
48
- "gitHead": "b0b6d7165d76582e1c197d0f56a10bfe6b46e2bc"
48
+ "gitHead": "73c0d802a8439b5d408ba1e60f91be029db7e402"
49
49
  }
package/src/Mat33.ts CHANGED
@@ -444,8 +444,13 @@ export class Mat33 {
444
444
  return Mat33.identity;
445
445
  }
446
446
 
447
- const parseArguments = (argumentString: string) => {
448
- return argumentString.split(/[, \t\n]+/g).map(argString => {
447
+ const parseArguments = (argumentString: string): number[] => {
448
+ const parsed = argumentString.split(/[, \t\n]+/g).map(argString => {
449
+ // Handle trailing spaces/commands
450
+ if (argString.trim() === '') {
451
+ return null;
452
+ }
453
+
449
454
  let isPercentage = false;
450
455
  if (argString.endsWith('%')) {
451
456
  isPercentage = true;
@@ -476,6 +481,7 @@ export class Mat33 {
476
481
 
477
482
  return argNumber;
478
483
  });
484
+ return parsed.filter(n => n !== null) as number[];
479
485
  };
480
486
 
481
487
 
package/src/Vec3.test.ts CHANGED
@@ -2,7 +2,7 @@
2
2
  import Vec3 from './Vec3';
3
3
 
4
4
  describe('Vec3', () => {
5
- it('.xy should contain the x and y components', () => {
5
+ test('.xy should contain the x and y components', () => {
6
6
  const vec = Vec3.of(1, 2, 3);
7
7
  expect(vec.xy).toMatchObject({
8
8
  x: 1,
@@ -10,42 +10,77 @@ describe('Vec3', () => {
10
10
  });
11
11
  });
12
12
 
13
- it('should be combinable with other vectors via .zip', () => {
13
+ test('should be combinable with other vectors via .zip', () => {
14
14
  const vec1 = Vec3.unitX;
15
15
  const vec2 = Vec3.unitY;
16
16
  expect(vec1.zip(vec2, Math.min)).objEq(Vec3.zero);
17
17
  expect(vec1.zip(vec2, Math.max)).objEq(Vec3.of(1, 1, 0));
18
18
  });
19
19
 
20
- it('.cross should obey the right hand rule', () => {
20
+ test('.cross should obey the right hand rule', () => {
21
21
  const vec1 = Vec3.unitX;
22
22
  const vec2 = Vec3.unitY;
23
23
  expect(vec1.cross(vec2)).objEq(Vec3.unitZ);
24
24
  expect(vec2.cross(vec1)).objEq(Vec3.unitZ.times(-1));
25
25
  });
26
26
 
27
- it('.orthog should return an orthogonal vector', () => {
27
+ test('.orthog should return an orthogonal vector', () => {
28
28
  expect(Vec3.unitZ.orthog().dot(Vec3.unitZ)).toBe(0);
29
29
 
30
30
  // Should return some orthogonal vector, even if given the zero vector
31
31
  expect(Vec3.zero.orthog().dot(Vec3.zero)).toBe(0);
32
32
  });
33
33
 
34
- it('.minus should return the difference between two vectors', () => {
34
+ test('.minus should return the difference between two vectors', () => {
35
35
  expect(Vec3.of(1, 2, 3).minus(Vec3.of(4, 5, 6))).objEq(Vec3.of(1 - 4, 2 - 5, 3 - 6));
36
36
  });
37
37
 
38
- it('.orthog should return a unit vector', () => {
38
+ test('.orthog should return a unit vector', () => {
39
39
  expect(Vec3.zero.orthog().magnitude()).toBe(1);
40
40
  expect(Vec3.unitZ.orthog().magnitude()).toBe(1);
41
41
  expect(Vec3.unitX.orthog().magnitude()).toBe(1);
42
42
  expect(Vec3.unitY.orthog().magnitude()).toBe(1);
43
43
  });
44
44
 
45
- it('.normalizedOrZero should normalize the given vector or return zero', () => {
45
+ test('.normalizedOrZero should normalize the given vector or return zero', () => {
46
46
  expect(Vec3.zero.normalizedOrZero()).objEq(Vec3.zero);
47
47
  expect(Vec3.unitX.normalizedOrZero()).objEq(Vec3.unitX);
48
48
  expect(Vec3.unitX.times(22).normalizedOrZero()).objEq(Vec3.unitX);
49
49
  expect(Vec3.of(1, 1, 1).times(22).normalizedOrZero().length()).toBeCloseTo(1);
50
50
  });
51
+
52
+ test.each([
53
+ { from: Vec3.of(1, 1, 1), to: Vec3.of(1, 2, 1), expected: 1 },
54
+ { from: Vec3.of(1, 1, 1), to: Vec3.of(1, 2, 2), expected: 2 },
55
+ { from: Vec3.of(1, 1, 1), to: Vec3.of(2, 2, 2), expected: 3 },
56
+ { from: Vec3.of(1, 1, 1), to: Vec3.of(0, 1, 1), expected: 1 },
57
+ { from: Vec3.of(1, 1, 1), to: Vec3.of(0, 1, 0), expected: 2 },
58
+ { from: Vec3.of(1, 1, 1), to: Vec3.of(0, 0, 0), expected: 3 },
59
+ { from: Vec3.of(-1, -10, 0), to: Vec3.of(1, 2, 0), expected: 148 },
60
+ { from: Vec3.of(-1, -10, 0), to: Vec3.of(1, 2, 0), expected: 148 },
61
+ ])(
62
+ '.squareDistanceTo and .distanceTo should return correct square and euclidean distances (%j)',
63
+ ({ from , to, expected }) => {
64
+ expect(from.squareDistanceTo(to)).toBe(expected);
65
+ expect(to.squareDistanceTo(from)).toBe(expected);
66
+ expect(to.distanceTo(from)).toBeCloseTo(Math.sqrt(expected));
67
+ expect(to.minus(from).magnitudeSquared()).toBe(expected);
68
+ expect(from.minus(to).magnitudeSquared()).toBe(expected);
69
+ },
70
+ );
71
+
72
+ test.each([
73
+ { a: Vec3.of(1, 2, 3), b: Vec3.of(4, 5, 6), tolerance: 0.1, eq: false },
74
+ { a: Vec3.of(1, 2, 3), b: Vec3.of(4, 5, 6), tolerance: 10, eq: true },
75
+ { a: Vec3.of(1, 2, 3), b: Vec3.of(1, 2, 3), tolerance: 0, eq: true },
76
+ { a: Vec3.of(1, 2, 3), b: Vec3.of(1, 2, 4), tolerance: 0, eq: false },
77
+ { a: Vec3.of(1, 2, 3), b: Vec3.of(1, 4, 3), tolerance: 0, eq: false },
78
+ { a: Vec3.of(1, 2, 3), b: Vec3.of(4, 2, 3), tolerance: 0, eq: false },
79
+ { a: Vec3.of(1, 2, 3.0001), b: Vec3.of(1, 2, 3), tolerance: 1e-12, eq: false },
80
+ { a: Vec3.of(1, 2, 3.0001), b: Vec3.of(1, 2, 3), tolerance: 1e-3, eq: true },
81
+ { a: Vec3.of(1, 2.00001, 3.0001), b: Vec3.of(1.00001, 2, 3), tolerance: 1e-3, eq: true },
82
+ ])('.eq should support tolerance (case %#)', ({ a, b, tolerance, eq }) => {
83
+ expect(a.eq(b, tolerance)).toBe(eq);
84
+ expect(b.eq(a, tolerance)).toBe(eq);
85
+ });
51
86
  });
package/src/Vec3.ts CHANGED
@@ -63,11 +63,40 @@ export class Vec3 {
63
63
  return this.dot(this);
64
64
  }
65
65
 
66
+ /**
67
+ * Interpreting this vector as a point in ℝ^3, computes the square distance
68
+ * to another point, `p`.
69
+ *
70
+ * Equivalent to `.minus(p).magnitudeSquared()`.
71
+ */
72
+ public squareDistanceTo(p: Vec3) {
73
+ const dx = this.x - p.x;
74
+ const dy = this.y - p.y;
75
+ const dz = this.z - p.z;
76
+ return dx * dx + dy * dy + dz * dz;
77
+ }
78
+
79
+ /**
80
+ * Interpreting this vector as a point in ℝ³, returns the distance to the point
81
+ * `p`.
82
+ *
83
+ * Equivalent to `.minus(p).magnitude()`.
84
+ */
85
+ public distanceTo(p: Vec3) {
86
+ return Math.sqrt(this.squareDistanceTo(p));
87
+ }
88
+
66
89
  /**
67
90
  * Returns the entry of this with the greatest magnitude.
68
91
  *
69
92
  * In other words, returns $\max \{ |x| : x \in {\bf v} \}$, where ${\bf v}$ is the set of
70
93
  * all entries of this vector.
94
+ *
95
+ * **Example**:
96
+ * ```ts,runnable,console
97
+ * import { Vec3 } from '@js-draw/math';
98
+ * console.log(Vec3.of(-1, -10, 8).maximumEntryMagnitude()); // -> 10
99
+ * ```
71
100
  */
72
101
  public maximumEntryMagnitude(): number {
73
102
  return Math.max(Math.abs(this.x), Math.max(Math.abs(this.y), Math.abs(this.z)));
@@ -81,6 +110,7 @@ export class Vec3 {
81
110
  * As such, observing that `Math.atan2(-0, -1)` $\approx -\pi$ and `Math.atan2(0, -1)`$\approx \pi$
82
111
  * the resultant angle is in the range $[-\pi, pi]$.
83
112
  *
113
+ * **Example**:
84
114
  * ```ts,runnable,console
85
115
  * import { Vec2 } from '@js-draw/math';
86
116
  * console.log(Vec2.of(-1, -0).angle()); // atan2(-0, -1)
@@ -214,7 +244,8 @@ export class Vec3 {
214
244
  * Returns a vector with each component acted on by `fn`.
215
245
  *
216
246
  * @example
217
- * ```
247
+ * ```ts,runnable,console
248
+ * import { Vec3 } from '@js-draw/math';
218
249
  * console.log(Vec3.of(1, 2, 3).map(val => val + 1)); // → Vec(2, 3, 4)
219
250
  * ```
220
251
  */
@@ -242,13 +273,11 @@ export class Vec3 {
242
273
  * ```
243
274
  */
244
275
  public eq(other: Vec3, fuzz: number = 1e-10): boolean {
245
- for (let i = 0; i < 3; i++) {
246
- if (Math.abs(other.at(i) - this.at(i)) > fuzz) {
247
- return false;
248
- }
249
- }
250
-
251
- return true;
276
+ return (
277
+ Math.abs(other.x - this.x) <= fuzz
278
+ && Math.abs(other.y - this.y) <= fuzz
279
+ && Math.abs(other.z - this.z) <= fuzz
280
+ );
252
281
  }
253
282
 
254
283
  public toString(): string {
package/src/lib.ts CHANGED
@@ -21,6 +21,10 @@ export { LineSegment2 } from './shapes/LineSegment2';
21
21
  export {
22
22
  Path,
23
23
 
24
+ IntersectionResult as PathIntersectionResult,
25
+ CurveIndexRecord as PathCurveIndex,
26
+ stepCurveIndexBy as stepPathIndexBy,
27
+ compareCurveIndices as comparePathIndices,
24
28
  PathCommandType,
25
29
  PathCommand,
26
30
  LinePathCommand,
@@ -29,6 +33,7 @@ export {
29
33
  CubicBezierPathCommand,
30
34
  } from './shapes/Path';
31
35
  export { Rect2 } from './shapes/Rect2';
36
+ export { Parameterized2DShape } from './shapes/Parameterized2DShape';
32
37
  export { QuadraticBezier } from './shapes/QuadraticBezier';
33
38
  export { Abstract2DShape } from './shapes/Abstract2DShape';
34
39
 
@@ -49,6 +49,9 @@ export abstract class Abstract2DShape {
49
49
 
50
50
  /**
51
51
  * Returns a bounding box that precisely fits the content of this shape.
52
+ *
53
+ * **Note**: This bounding box should aligned with the x/y axes. (Thus, it may be
54
+ * possible to find a tighter bounding box not axes-aligned).
52
55
  */
53
56
  public abstract getTightBoundingBox(): Rect2;
54
57