@js-draw/math 1.16.0 → 1.17.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 (51) hide show
  1. package/dist/cjs/Vec3.d.ts +21 -0
  2. package/dist/cjs/Vec3.js +28 -0
  3. package/dist/cjs/lib.d.ts +1 -1
  4. package/dist/cjs/shapes/Abstract2DShape.d.ts +3 -0
  5. package/dist/cjs/shapes/BezierJSWrapper.d.ts +15 -5
  6. package/dist/cjs/shapes/BezierJSWrapper.js +135 -18
  7. package/dist/cjs/shapes/LineSegment2.d.ts +34 -5
  8. package/dist/cjs/shapes/LineSegment2.js +63 -10
  9. package/dist/cjs/shapes/Parameterized2DShape.d.ts +31 -0
  10. package/dist/cjs/shapes/Parameterized2DShape.js +15 -0
  11. package/dist/cjs/shapes/Path.d.ts +40 -6
  12. package/dist/cjs/shapes/Path.js +173 -15
  13. package/dist/cjs/shapes/PointShape2D.d.ts +14 -3
  14. package/dist/cjs/shapes/PointShape2D.js +28 -5
  15. package/dist/cjs/shapes/QuadraticBezier.d.ts +4 -0
  16. package/dist/cjs/shapes/QuadraticBezier.js +19 -4
  17. package/dist/cjs/shapes/Rect2.d.ts +3 -0
  18. package/dist/cjs/shapes/Rect2.js +4 -1
  19. package/dist/mjs/Vec3.d.ts +21 -0
  20. package/dist/mjs/Vec3.mjs +28 -0
  21. package/dist/mjs/lib.d.ts +1 -1
  22. package/dist/mjs/shapes/Abstract2DShape.d.ts +3 -0
  23. package/dist/mjs/shapes/BezierJSWrapper.d.ts +15 -5
  24. package/dist/mjs/shapes/BezierJSWrapper.mjs +133 -18
  25. package/dist/mjs/shapes/LineSegment2.d.ts +34 -5
  26. package/dist/mjs/shapes/LineSegment2.mjs +63 -10
  27. package/dist/mjs/shapes/Parameterized2DShape.d.ts +31 -0
  28. package/dist/mjs/shapes/Parameterized2DShape.mjs +8 -0
  29. package/dist/mjs/shapes/Path.d.ts +40 -6
  30. package/dist/mjs/shapes/Path.mjs +173 -15
  31. package/dist/mjs/shapes/PointShape2D.d.ts +14 -3
  32. package/dist/mjs/shapes/PointShape2D.mjs +28 -5
  33. package/dist/mjs/shapes/QuadraticBezier.d.ts +4 -0
  34. package/dist/mjs/shapes/QuadraticBezier.mjs +19 -4
  35. package/dist/mjs/shapes/Rect2.d.ts +3 -0
  36. package/dist/mjs/shapes/Rect2.mjs +4 -1
  37. package/package.json +5 -5
  38. package/src/Vec3.test.ts +26 -7
  39. package/src/Vec3.ts +30 -0
  40. package/src/lib.ts +2 -0
  41. package/src/shapes/Abstract2DShape.ts +3 -0
  42. package/src/shapes/BezierJSWrapper.ts +154 -14
  43. package/src/shapes/LineSegment2.test.ts +35 -1
  44. package/src/shapes/LineSegment2.ts +79 -11
  45. package/src/shapes/Parameterized2DShape.ts +39 -0
  46. package/src/shapes/Path.test.ts +63 -3
  47. package/src/shapes/Path.ts +209 -25
  48. package/src/shapes/PointShape2D.ts +33 -6
  49. package/src/shapes/QuadraticBezier.test.ts +48 -12
  50. package/src/shapes/QuadraticBezier.ts +23 -5
  51. package/src/shapes/Rect2.ts +4 -1
@@ -230,7 +230,7 @@ export class Path {
230
230
  for (const { part, distFn, bbox } of uncheckedDistFunctions) {
231
231
  // Skip if impossible for the distance to the target to be lesser than
232
232
  // the current minimum.
233
- if (!bbox.grownBy(minDist).containsPoint(point)) {
233
+ if (isFinite(minDist) && !bbox.grownBy(minDist).containsPoint(point)) {
234
234
  continue;
235
235
  }
236
236
  const currentDist = distFn(point);
@@ -268,7 +268,7 @@ export class Path {
268
268
  });
269
269
  const result = [];
270
270
  const stoppingThreshold = strokeRadius / 1000;
271
- // Returns the maximum x value explored
271
+ // Returns the maximum parameter value explored
272
272
  const raymarchFrom = (startPoint,
273
273
  // Direction to march in (multiplies line.direction)
274
274
  directionMultiplier,
@@ -312,9 +312,14 @@ export class Path {
312
312
  if (lastPart && isOnLineSegment && Math.abs(lastDist) < stoppingThreshold) {
313
313
  result.push({
314
314
  point: currentPoint,
315
- parameterValue: NaN,
315
+ parameterValue: NaN, // lastPart.nearestPointTo(currentPoint).parameterValue,
316
316
  curve: lastPart,
317
+ curveIndex: this.geometry.indexOf(lastPart),
317
318
  });
319
+ // Slightly increase the parameter value to prevent the same point from being
320
+ // added to the results twice.
321
+ const parameterIncrease = strokeRadius / 20 / line.length;
322
+ lastParameter += isFinite(parameterIncrease) ? parameterIncrease : 0;
318
323
  }
319
324
  return lastParameter;
320
325
  };
@@ -347,14 +352,18 @@ export class Path {
347
352
  if (!line.bbox.intersects(this.bbox.grownBy(strokeRadius ?? 0))) {
348
353
  return [];
349
354
  }
355
+ let index = 0;
350
356
  for (const part of this.geometry) {
351
- const intersection = part.intersectsLineSegment(line);
352
- if (intersection.length > 0) {
357
+ const intersections = part.argIntersectsLineSegment(line);
358
+ for (const intersection of intersections) {
353
359
  result.push({
354
360
  curve: part,
355
- point: intersection[0],
361
+ curveIndex: index,
362
+ point: part.at(intersection),
363
+ parameterValue: intersection,
356
364
  });
357
365
  }
366
+ index++;
358
367
  }
359
368
  // If given a non-zero strokeWidth, attempt to raymarch.
360
369
  // Even if raymarching, we need to collect starting points.
@@ -367,6 +376,42 @@ export class Path {
367
376
  }
368
377
  return result;
369
378
  }
379
+ /**
380
+ * @returns the nearest point on this path to the given `point`.
381
+ *
382
+ * @internal
383
+ * @beta
384
+ */
385
+ nearestPointTo(point) {
386
+ // Find the closest point on this
387
+ let closestSquareDist = Infinity;
388
+ let closestPartIndex = 0;
389
+ let closestParameterValue = 0;
390
+ let closestPoint = this.startPoint;
391
+ for (let i = 0; i < this.geometry.length; i++) {
392
+ const current = this.geometry[i];
393
+ const nearestPoint = current.nearestPointTo(point);
394
+ const sqareDist = nearestPoint.point.squareDistanceTo(point);
395
+ if (i === 0 || sqareDist < closestSquareDist) {
396
+ closestPartIndex = i;
397
+ closestSquareDist = sqareDist;
398
+ closestParameterValue = nearestPoint.parameterValue;
399
+ closestPoint = nearestPoint.point;
400
+ }
401
+ }
402
+ return {
403
+ curve: this.geometry[closestPartIndex],
404
+ curveIndex: closestPartIndex,
405
+ parameterValue: closestParameterValue,
406
+ point: closestPoint,
407
+ };
408
+ }
409
+ at(index) {
410
+ return this.geometry[index.curveIndex].at(index.parameterValue);
411
+ }
412
+ tangentAt(index) {
413
+ return this.geometry[index.curveIndex].tangentAt(index.parameterValue);
414
+ }
370
415
  static mapPathCommand(part, mapping) {
371
416
  switch (part.kind) {
372
417
  case PathCommandType.MoveTo:
@@ -410,18 +455,85 @@ export class Path {
410
455
  return this.mapPoints(point => affineTransfm.transformVec2(point));
411
456
  }
412
457
  // Creates a new path by joining [other] to the end of this path
413
- union(other) {
458
+ union(other,
459
+ // allowReverse: true iff reversing other or this is permitted if it means
460
+ // no moveTo command is necessary when unioning the paths.
461
+ options = { allowReverse: true }) {
414
462
  if (!other) {
415
463
  return this;
416
464
  }
417
- return new Path(this.startPoint, [
418
- ...this.parts,
419
- {
420
- kind: PathCommandType.MoveTo,
421
- point: other.startPoint,
422
- },
423
- ...other.parts,
424
- ]);
465
+ const thisEnd = this.getEndPoint();
466
+ let newParts = [];
467
+ if (thisEnd.eq(other.startPoint)) {
468
+ newParts = this.parts.concat(other.parts);
469
+ }
470
+ else if (options.allowReverse && this.startPoint.eq(other.getEndPoint())) {
471
+ return other.union(this, { allowReverse: false });
472
+ }
473
+ else if (options.allowReverse && this.startPoint.eq(other.startPoint)) {
474
+ return this.union(other.reversed(), { allowReverse: false });
475
+ }
476
+ else {
477
+ newParts = [
478
+ ...this.parts,
479
+ {
480
+ kind: PathCommandType.MoveTo,
481
+ point: other.startPoint,
482
+ },
483
+ ...other.parts,
484
+ ];
485
+ }
486
+ return new Path(this.startPoint, newParts);
487
+ }
488
+ /**
489
+ * @returns a version of this path with the direction reversed.
490
+ *
491
+ * Example:
492
+ * ```ts,runnable,console
493
+ * import {Path} from '@js-draw/math';
494
+ * console.log(Path.fromString('m0,0l1,1').reversed()); // -> M1,1 L0,0
495
+ * ```
496
+ */
497
+ reversed() {
498
+ const newStart = this.getEndPoint();
499
+ const newParts = [];
500
+ let lastPoint = this.startPoint;
501
+ for (const part of this.parts) {
502
+ switch (part.kind) {
503
+ case PathCommandType.LineTo:
504
+ case PathCommandType.MoveTo:
505
+ newParts.push({
506
+ kind: part.kind,
507
+ point: lastPoint,
508
+ });
509
+ lastPoint = part.point;
510
+ break;
511
+ case PathCommandType.CubicBezierTo:
512
+ newParts.push({
513
+ kind: part.kind,
514
+ controlPoint1: part.controlPoint2,
515
+ controlPoint2: part.controlPoint1,
516
+ endPoint: lastPoint,
517
+ });
518
+ lastPoint = part.endPoint;
519
+ break;
520
+ case PathCommandType.QuadraticBezierTo:
521
+ newParts.push({
522
+ kind: part.kind,
523
+ controlPoint: part.controlPoint,
524
+ endPoint: lastPoint,
525
+ });
526
+ lastPoint = part.endPoint;
527
+ break;
528
+ default:
529
+ {
530
+ const exhaustivenessCheck = part;
531
+ return exhaustivenessCheck;
532
+ }
533
+ }
534
+ }
535
+ newParts.reverse();
536
+ return new Path(newStart, newParts);
425
537
  }
426
538
  getEndPoint() {
427
539
  if (this.parts.length === 0) {
@@ -513,6 +625,52 @@ export class Path {
513
625
  // Even? Probably no intersection.
514
626
  return false;
515
627
  }
628
+ /** @returns true if all points on this are equivalent to the points on `other` */
629
+ eq(other, tolerance) {
630
+ if (other.parts.length !== this.parts.length) {
631
+ return false;
632
+ }
633
+ for (let i = 0; i < this.parts.length; i++) {
634
+ const part1 = this.parts[i];
635
+ const part2 = other.parts[i];
636
+ switch (part1.kind) {
637
+ case PathCommandType.LineTo:
638
+ case PathCommandType.MoveTo:
639
+ if (part1.kind !== part2.kind) {
640
+ return false;
641
+ }
642
+ else if (!part1.point.eq(part2.point, tolerance)) {
643
+ return false;
644
+ }
645
+ break;
646
+ case PathCommandType.CubicBezierTo:
647
+ if (part1.kind !== part2.kind) {
648
+ return false;
649
+ }
650
+ else if (!part1.controlPoint1.eq(part2.controlPoint1, tolerance)
651
+ || !part1.controlPoint2.eq(part2.controlPoint2, tolerance)
652
+ || !part1.endPoint.eq(part2.endPoint, tolerance)) {
653
+ return false;
654
+ }
655
+ break;
656
+ case PathCommandType.QuadraticBezierTo:
657
+ if (part1.kind !== part2.kind) {
658
+ return false;
659
+ }
660
+ else if (!part1.controlPoint.eq(part2.controlPoint, tolerance)
661
+ || !part1.endPoint.eq(part2.endPoint, tolerance)) {
662
+ return false;
663
+ }
664
+ break;
665
+ default:
666
+ {
667
+ const exhaustivenessCheck = part1;
668
+ return exhaustivenessCheck;
669
+ }
670
+ }
671
+ }
672
+ return true;
673
+ }
516
674
  /**
517
675
  * Returns a path that outlines `rect`.
518
676
  *
@@ -1,18 +1,29 @@
1
1
  import { Point2 } 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
  * Like a {@link Point2}, but with additional functionality (e.g. SDF).
8
8
  *
9
9
  * Access the internal `Point2` using the `p` property.
10
10
  */
11
- declare class PointShape2D extends Abstract2DShape {
11
+ declare class PointShape2D extends Parameterized2DShape {
12
12
  readonly p: Point2;
13
13
  constructor(p: Point2);
14
14
  signedDistance(point: Vec3): number;
15
- intersectsLineSegment(lineSegment: LineSegment2, epsilon?: number): Vec3[];
15
+ argIntersectsLineSegment(lineSegment: LineSegment2, epsilon?: number): number[];
16
16
  getTightBoundingBox(): Rect2;
17
+ at(_t: number): Vec3;
18
+ /**
19
+ * Returns an arbitrary unit-length vector.
20
+ */
21
+ normalAt(_t: number): Vec3;
22
+ tangentAt(_t: number): Vec3;
23
+ splitAt(_t: number): [PointShape2D];
24
+ nearestPointTo(_point: Point2): {
25
+ point: Vec3;
26
+ parameterValue: number;
27
+ };
17
28
  }
18
29
  export default PointShape2D;
@@ -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;
@@ -18,11 +18,15 @@ export declare class QuadraticBezier extends BezierJSWrapper {
18
18
  */
19
19
  private static componentAt;
20
20
  private static derivativeComponentAt;
21
+ private static secondDerivativeComponentAt;
21
22
  /**
22
23
  * @returns the curve evaluated at `t`.
24
+ *
25
+ * `t` should be a number in `[0, 1]`.
23
26
  */
24
27
  at(t: number): Point2;
25
28
  derivativeAt(t: number): Point2;
29
+ secondDerivativeAt(t: number): Point2;
26
30
  normal(t: number): Vec2;
27
31
  /** @returns an overestimate of this shape's bounding box. */
28
32
  getLooseBoundingBox(): Rect2;
@@ -25,10 +25,19 @@ export class QuadraticBezier extends BezierJSWrapper {
25
25
  static derivativeComponentAt(t, p0, p1, p2) {
26
26
  return -2 * p0 + 2 * p1 + 2 * t * (p0 - 2 * p1 + p2);
27
27
  }
28
+ static secondDerivativeComponentAt(t, p0, p1, p2) {
29
+ return 2 * (p0 - 2 * p1 + p2);
30
+ }
28
31
  /**
29
32
  * @returns the curve evaluated at `t`.
33
+ *
34
+ * `t` should be a number in `[0, 1]`.
30
35
  */
31
36
  at(t) {
37
+ if (t === 0)
38
+ return this.p0;
39
+ if (t === 1)
40
+ return this.p2;
32
41
  const p0 = this.p0;
33
42
  const p1 = this.p1;
34
43
  const p2 = this.p2;
@@ -40,6 +49,12 @@ export class QuadraticBezier extends BezierJSWrapper {
40
49
  const p2 = this.p2;
41
50
  return Vec2.of(QuadraticBezier.derivativeComponentAt(t, p0.x, p1.x, p2.x), QuadraticBezier.derivativeComponentAt(t, p0.y, p1.y, p2.y));
42
51
  }
52
+ secondDerivativeAt(t) {
53
+ const p0 = this.p0;
54
+ const p1 = this.p1;
55
+ const p2 = this.p2;
56
+ return Vec2.of(QuadraticBezier.secondDerivativeComponentAt(t, p0.x, p1.x, p2.x), QuadraticBezier.secondDerivativeComponentAt(t, p0.y, p1.y, p2.y));
57
+ }
43
58
  normal(t) {
44
59
  const tangent = this.derivativeAt(t);
45
60
  return tangent.orthog().normalized();
@@ -100,10 +115,10 @@ export class QuadraticBezier extends BezierJSWrapper {
100
115
  }
101
116
  const at1 = this.at(min1);
102
117
  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();
118
+ const sqrDist1 = at1.squareDistanceTo(point);
119
+ const sqrDist2 = at2.squareDistanceTo(point);
120
+ const sqrDist3 = this.at(0).squareDistanceTo(point);
121
+ const sqrDist4 = this.at(1).squareDistanceTo(point);
107
122
  return Math.sqrt(Math.min(sqrDist1, sqrDist2, sqrDist3, sqrDist4));
108
123
  }
109
124
  getPoints() {
@@ -25,6 +25,9 @@ export declare class Rect2 extends Abstract2DShape {
25
25
  resizedTo(size: Vec2): Rect2;
26
26
  containsPoint(other: Point2): boolean;
27
27
  containsRect(other: Rect2): boolean;
28
+ /**
29
+ * @returns true iff this and `other` overlap
30
+ */
28
31
  intersects(other: Rect2): boolean;
29
32
  intersection(other: Rect2): Rect2 | null;
30
33
  union(other: Rect2): Rect2;
@@ -38,6 +38,9 @@ export class Rect2 extends Abstract2DShape {
38
38
  && this.x + this.w >= other.x + other.w
39
39
  && this.y + this.h >= other.y + other.h;
40
40
  }
41
+ /**
42
+ * @returns true iff this and `other` overlap
43
+ */
41
44
  intersects(other) {
42
45
  // Project along x/y axes.
43
46
  const thisMinX = this.x;
@@ -124,7 +127,7 @@ export class Rect2 extends Abstract2DShape {
124
127
  let closest = null;
125
128
  let closestDist = null;
126
129
  for (const point of closestEdgePoints) {
127
- const dist = point.minus(target).length();
130
+ const dist = point.distanceTo(target);
128
131
  if (closestDist === null || dist < closestDist) {
129
132
  closest = point;
130
133
  closestDist = dist;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@js-draw/math",
3
- "version": "1.16.0",
3
+ "version": "1.17.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": "d0eff585750ab5670af3acda8ddff090e8825bd3"
49
49
  }
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,61 @@ 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
+ ])(
61
+ '.squareDistanceTo and .distanceTo should return correct square and euclidean distances (%j)',
62
+ ({ from , to, expected }) => {
63
+ expect(from.squareDistanceTo(to)).toBe(expected);
64
+ expect(to.squareDistanceTo(from)).toBe(expected);
65
+ expect(to.distanceTo(from)).toBeCloseTo(Math.sqrt(expected));
66
+ expect(to.minus(from).magnitudeSquared()).toBe(expected);
67
+ expect(from.minus(to).magnitudeSquared()).toBe(expected);
68
+ },
69
+ );
51
70
  });
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)
package/src/lib.ts CHANGED
@@ -21,6 +21,8 @@ export { LineSegment2 } from './shapes/LineSegment2';
21
21
  export {
22
22
  Path,
23
23
 
24
+ IntersectionResult as PathIntersectionResult,
25
+ CurveIndexRecord as PathCurveIndex,
24
26
  PathCommandType,
25
27
  PathCommand,
26
28
  LinePathCommand,
@@ -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