@js-draw/math 1.11.1 → 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 (96) 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 +2 -2
  4. package/dist/cjs/lib.js +16 -3
  5. package/dist/cjs/rounding/cleanUpNumber.d.ts +3 -0
  6. package/dist/cjs/rounding/cleanUpNumber.js +35 -0
  7. package/dist/cjs/rounding/constants.d.ts +1 -0
  8. package/dist/cjs/rounding/constants.js +4 -0
  9. package/dist/cjs/rounding/getLenAfterDecimal.d.ts +10 -0
  10. package/dist/cjs/rounding/getLenAfterDecimal.js +30 -0
  11. package/dist/cjs/rounding/lib.d.ts +1 -0
  12. package/dist/cjs/rounding/lib.js +5 -0
  13. package/dist/cjs/{rounding.d.ts → rounding/toRoundedString.d.ts} +1 -3
  14. package/dist/cjs/rounding/toRoundedString.js +54 -0
  15. package/dist/cjs/rounding/toStringOfSamePrecision.d.ts +2 -0
  16. package/dist/cjs/rounding/toStringOfSamePrecision.js +58 -0
  17. package/dist/cjs/rounding/toStringOfSamePrecision.test.d.ts +1 -0
  18. package/dist/cjs/shapes/Abstract2DShape.d.ts +3 -0
  19. package/dist/cjs/shapes/BezierJSWrapper.d.ts +15 -5
  20. package/dist/cjs/shapes/BezierJSWrapper.js +135 -18
  21. package/dist/cjs/shapes/LineSegment2.d.ts +34 -5
  22. package/dist/cjs/shapes/LineSegment2.js +63 -10
  23. package/dist/cjs/shapes/Parameterized2DShape.d.ts +31 -0
  24. package/dist/cjs/shapes/Parameterized2DShape.js +15 -0
  25. package/dist/cjs/shapes/Path.d.ts +40 -6
  26. package/dist/cjs/shapes/Path.js +181 -22
  27. package/dist/cjs/shapes/PointShape2D.d.ts +14 -3
  28. package/dist/cjs/shapes/PointShape2D.js +28 -5
  29. package/dist/cjs/shapes/QuadraticBezier.d.ts +4 -0
  30. package/dist/cjs/shapes/QuadraticBezier.js +19 -4
  31. package/dist/cjs/shapes/Rect2.d.ts +3 -0
  32. package/dist/cjs/shapes/Rect2.js +4 -1
  33. package/dist/mjs/Vec3.d.ts +21 -0
  34. package/dist/mjs/Vec3.mjs +28 -0
  35. package/dist/mjs/lib.d.ts +2 -2
  36. package/dist/mjs/lib.mjs +1 -1
  37. package/dist/mjs/rounding/cleanUpNumber.d.ts +3 -0
  38. package/dist/mjs/rounding/cleanUpNumber.mjs +31 -0
  39. package/dist/mjs/rounding/cleanUpNumber.test.d.ts +1 -0
  40. package/dist/mjs/rounding/constants.d.ts +1 -0
  41. package/dist/mjs/rounding/constants.mjs +1 -0
  42. package/dist/mjs/rounding/getLenAfterDecimal.d.ts +10 -0
  43. package/dist/mjs/rounding/getLenAfterDecimal.mjs +26 -0
  44. package/dist/mjs/rounding/lib.d.ts +1 -0
  45. package/dist/mjs/rounding/lib.mjs +1 -0
  46. package/dist/mjs/{rounding.d.ts → rounding/toRoundedString.d.ts} +1 -3
  47. package/dist/mjs/rounding/toRoundedString.mjs +47 -0
  48. package/dist/mjs/rounding/toRoundedString.test.d.ts +1 -0
  49. package/dist/mjs/rounding/toStringOfSamePrecision.d.ts +2 -0
  50. package/dist/mjs/rounding/toStringOfSamePrecision.mjs +51 -0
  51. package/dist/mjs/rounding/toStringOfSamePrecision.test.d.ts +1 -0
  52. package/dist/mjs/shapes/Abstract2DShape.d.ts +3 -0
  53. package/dist/mjs/shapes/BezierJSWrapper.d.ts +15 -5
  54. package/dist/mjs/shapes/BezierJSWrapper.mjs +133 -18
  55. package/dist/mjs/shapes/LineSegment2.d.ts +34 -5
  56. package/dist/mjs/shapes/LineSegment2.mjs +63 -10
  57. package/dist/mjs/shapes/Parameterized2DShape.d.ts +31 -0
  58. package/dist/mjs/shapes/Parameterized2DShape.mjs +8 -0
  59. package/dist/mjs/shapes/Path.d.ts +40 -6
  60. package/dist/mjs/shapes/Path.mjs +175 -16
  61. package/dist/mjs/shapes/PointShape2D.d.ts +14 -3
  62. package/dist/mjs/shapes/PointShape2D.mjs +28 -5
  63. package/dist/mjs/shapes/QuadraticBezier.d.ts +4 -0
  64. package/dist/mjs/shapes/QuadraticBezier.mjs +19 -4
  65. package/dist/mjs/shapes/Rect2.d.ts +3 -0
  66. package/dist/mjs/shapes/Rect2.mjs +4 -1
  67. package/package.json +5 -5
  68. package/src/Vec3.test.ts +26 -7
  69. package/src/Vec3.ts +30 -0
  70. package/src/lib.ts +3 -1
  71. package/src/rounding/cleanUpNumber.test.ts +15 -0
  72. package/src/rounding/cleanUpNumber.ts +38 -0
  73. package/src/rounding/constants.ts +3 -0
  74. package/src/rounding/getLenAfterDecimal.ts +29 -0
  75. package/src/rounding/lib.ts +2 -0
  76. package/src/rounding/toRoundedString.test.ts +32 -0
  77. package/src/rounding/toRoundedString.ts +57 -0
  78. package/src/rounding/toStringOfSamePrecision.test.ts +21 -0
  79. package/src/rounding/toStringOfSamePrecision.ts +63 -0
  80. package/src/shapes/Abstract2DShape.ts +3 -0
  81. package/src/shapes/BezierJSWrapper.ts +154 -14
  82. package/src/shapes/LineSegment2.test.ts +35 -1
  83. package/src/shapes/LineSegment2.ts +79 -11
  84. package/src/shapes/Parameterized2DShape.ts +39 -0
  85. package/src/shapes/Path.test.ts +63 -3
  86. package/src/shapes/Path.ts +211 -26
  87. package/src/shapes/PointShape2D.ts +33 -6
  88. package/src/shapes/QuadraticBezier.test.ts +48 -12
  89. package/src/shapes/QuadraticBezier.ts +23 -5
  90. package/src/shapes/Rect2.ts +4 -1
  91. package/dist/cjs/rounding.js +0 -146
  92. package/dist/mjs/rounding.mjs +0 -139
  93. package/src/rounding.test.ts +0 -65
  94. package/src/rounding.ts +0 -168
  95. /package/dist/cjs/{rounding.test.d.ts → rounding/cleanUpNumber.test.d.ts} +0 -0
  96. /package/dist/{mjs/rounding.test.d.ts → cjs/rounding/toRoundedString.test.d.ts} +0 -0
@@ -1,10 +1,11 @@
1
- import { toRoundedString, toStringOfSamePrecision } from '../rounding.mjs';
2
1
  import LineSegment2 from './LineSegment2.mjs';
3
2
  import Rect2 from './Rect2.mjs';
4
3
  import { Vec2 } from '../Vec2.mjs';
5
4
  import CubicBezier from './CubicBezier.mjs';
6
5
  import QuadraticBezier from './QuadraticBezier.mjs';
7
6
  import PointShape2D from './PointShape2D.mjs';
7
+ import toRoundedString from '../rounding/toRoundedString.mjs';
8
+ import toStringOfSamePrecision from '../rounding/toStringOfSamePrecision.mjs';
8
9
  export var PathCommandType;
9
10
  (function (PathCommandType) {
10
11
  PathCommandType[PathCommandType["LineTo"] = 0] = "LineTo";
@@ -229,7 +230,7 @@ export class Path {
229
230
  for (const { part, distFn, bbox } of uncheckedDistFunctions) {
230
231
  // Skip if impossible for the distance to the target to be lesser than
231
232
  // the current minimum.
232
- if (!bbox.grownBy(minDist).containsPoint(point)) {
233
+ if (isFinite(minDist) && !bbox.grownBy(minDist).containsPoint(point)) {
233
234
  continue;
234
235
  }
235
236
  const currentDist = distFn(point);
@@ -267,7 +268,7 @@ export class Path {
267
268
  });
268
269
  const result = [];
269
270
  const stoppingThreshold = strokeRadius / 1000;
270
- // Returns the maximum x value explored
271
+ // Returns the maximum parameter value explored
271
272
  const raymarchFrom = (startPoint,
272
273
  // Direction to march in (multiplies line.direction)
273
274
  directionMultiplier,
@@ -311,9 +312,14 @@ export class Path {
311
312
  if (lastPart && isOnLineSegment && Math.abs(lastDist) < stoppingThreshold) {
312
313
  result.push({
313
314
  point: currentPoint,
314
- parameterValue: NaN,
315
+ parameterValue: NaN, // lastPart.nearestPointTo(currentPoint).parameterValue,
315
316
  curve: lastPart,
317
+ curveIndex: this.geometry.indexOf(lastPart),
316
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;
317
323
  }
318
324
  return lastParameter;
319
325
  };
@@ -346,14 +352,18 @@ export class Path {
346
352
  if (!line.bbox.intersects(this.bbox.grownBy(strokeRadius ?? 0))) {
347
353
  return [];
348
354
  }
355
+ let index = 0;
349
356
  for (const part of this.geometry) {
350
- const intersection = part.intersectsLineSegment(line);
351
- if (intersection.length > 0) {
357
+ const intersections = part.argIntersectsLineSegment(line);
358
+ for (const intersection of intersections) {
352
359
  result.push({
353
360
  curve: part,
354
- point: intersection[0],
361
+ curveIndex: index,
362
+ point: part.at(intersection),
363
+ parameterValue: intersection,
355
364
  });
356
365
  }
366
+ index++;
357
367
  }
358
368
  // If given a non-zero strokeWidth, attempt to raymarch.
359
369
  // Even if raymarching, we need to collect starting points.
@@ -366,6 +376,42 @@ export class Path {
366
376
  }
367
377
  return result;
368
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
+ }
369
415
  static mapPathCommand(part, mapping) {
370
416
  switch (part.kind) {
371
417
  case PathCommandType.MoveTo:
@@ -409,18 +455,85 @@ export class Path {
409
455
  return this.mapPoints(point => affineTransfm.transformVec2(point));
410
456
  }
411
457
  // Creates a new path by joining [other] to the end of this path
412
- 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 }) {
413
462
  if (!other) {
414
463
  return this;
415
464
  }
416
- return new Path(this.startPoint, [
417
- ...this.parts,
418
- {
419
- kind: PathCommandType.MoveTo,
420
- point: other.startPoint,
421
- },
422
- ...other.parts,
423
- ]);
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);
424
537
  }
425
538
  getEndPoint() {
426
539
  if (this.parts.length === 0) {
@@ -512,6 +625,52 @@ export class Path {
512
625
  // Even? Probably no intersection.
513
626
  return false;
514
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
+ }
515
674
  /**
516
675
  * Returns a path that outlines `rect`.
517
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.11.1",
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": "695cfe01116839842668233a14fa858ad4ae0bac"
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,
@@ -36,7 +38,7 @@ export { Mat33, Mat33Array } from './Mat33';
36
38
  export { Point2, Vec2 } from './Vec2';
37
39
  export { Vec3 } from './Vec3';
38
40
  export { Color4 } from './Color4';
39
- export { toRoundedString } from './rounding';
41
+ export * from './rounding/lib';
40
42
 
41
43
 
42
44
  // Note: All above exports cannot use `export { default as ... } from "..."` because this
@@ -0,0 +1,15 @@
1
+ import cleanUpNumber from './cleanUpNumber';
2
+
3
+ it('cleanUpNumber', () => {
4
+ expect(cleanUpNumber('000.0000')).toBe('0');
5
+ expect(cleanUpNumber('-000.0000')).toBe('0');
6
+ expect(cleanUpNumber('0.0000')).toBe('0');
7
+ expect(cleanUpNumber('0.001')).toBe('.001');
8
+ expect(cleanUpNumber('-0.001')).toBe('-.001');
9
+ expect(cleanUpNumber('-0.000000001')).toBe('-.000000001');
10
+ expect(cleanUpNumber('-0.00000000100')).toBe('-.000000001');
11
+ expect(cleanUpNumber('1234')).toBe('1234');
12
+ expect(cleanUpNumber('1234.5')).toBe('1234.5');
13
+ expect(cleanUpNumber('1234.500')).toBe('1234.5');
14
+ expect(cleanUpNumber('1.1368683772161603e-13')).toBe('0');
15
+ });
@@ -0,0 +1,38 @@
1
+ /** Cleans up stringified numbers */
2
+ export const cleanUpNumber = (text: string) => {
3
+ // Regular expression substitions can be somewhat expensive. Only do them
4
+ // if necessary.
5
+
6
+ if (text.indexOf('e') > 0) {
7
+ // Round to zero.
8
+ if (text.match(/[eE][-]\d{2,}$/)) {
9
+ return '0';
10
+ }
11
+ }
12
+
13
+ const lastChar = text.charAt(text.length - 1);
14
+ if (lastChar === '0' || lastChar === '.') {
15
+ // Remove trailing zeroes
16
+ text = text.replace(/([.]\d*[^0]+)0+$/, '$1');
17
+ text = text.replace(/[.]0+$/, '.');
18
+
19
+ // Remove trailing period
20
+ text = text.replace(/[.]$/, '');
21
+ }
22
+
23
+ const firstChar = text.charAt(0);
24
+ if (firstChar === '0' || firstChar === '-') {
25
+ // Remove unnecessary leading zeroes.
26
+ text = text.replace(/^(0+)[.]/, '.');
27
+ text = text.replace(/^-(0+)[.]/, '-.');
28
+ text = text.replace(/^(-?)0+$/, '$10');
29
+ }
30
+
31
+ if (text === '-0') {
32
+ return '0';
33
+ }
34
+
35
+ return text;
36
+ };
37
+
38
+ export default cleanUpNumber;
@@ -0,0 +1,3 @@
1
+
2
+
3
+ export const numberRegex = /^([-]?)(\d*)[.](\d+)$/;