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