@js-draw/math 1.16.0 → 1.17.0

Sign up to get free protection for your applications and to get access to all the features.
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