@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
@@ -2,12 +2,12 @@ import LineSegment2 from './LineSegment2';
2
2
  import Mat33 from '../Mat33';
3
3
  import Rect2 from './Rect2';
4
4
  import { Point2, Vec2 } from '../Vec2';
5
- import Abstract2DShape from './Abstract2DShape';
6
5
  import CubicBezier from './CubicBezier';
7
6
  import QuadraticBezier from './QuadraticBezier';
8
7
  import PointShape2D from './PointShape2D';
9
8
  import toRoundedString from '../rounding/toRoundedString';
10
9
  import toStringOfSamePrecision from '../rounding/toStringOfSamePrecision';
10
+ import Parameterized2DShape from './Parameterized2DShape';
11
11
 
12
12
  export enum PathCommandType {
13
13
  LineTo,
@@ -41,17 +41,29 @@ export interface MoveToPathCommand {
41
41
 
42
42
  export type PathCommand = CubicBezierPathCommand | QuadraticBezierPathCommand | MoveToPathCommand | LinePathCommand;
43
43
 
44
- interface IntersectionResult {
44
+ export interface IntersectionResult {
45
45
  // @internal
46
- curve: Abstract2DShape;
46
+ curve: Parameterized2DShape;
47
+ // @internal
48
+ curveIndex: number;
47
49
 
48
- /** @internal @deprecated */
50
+ /** Parameter value for the closest point **on** the path to the intersection. @internal @deprecated */
49
51
  parameterValue?: number;
50
52
 
51
- // Point at which the intersection occured.
53
+ /** Point at which the intersection occured. */
52
54
  point: Point2;
53
55
  }
54
56
 
57
+ /**
58
+ * Allows indexing a particular part of a path.
59
+ *
60
+ * @see {@link Path.at} {@link Path.tangentAt}
61
+ */
62
+ export interface CurveIndexRecord {
63
+ curveIndex: number;
64
+ parameterValue: number;
65
+ }
66
+
55
67
  /**
56
68
  * Represents a union of lines and curves.
57
69
  */
@@ -97,16 +109,16 @@ export class Path {
97
109
  return Rect2.union(...bboxes);
98
110
  }
99
111
 
100
- private cachedGeometry: Abstract2DShape[]|null = null;
112
+ private cachedGeometry: Parameterized2DShape[]|null = null;
101
113
 
102
114
  // Lazy-loads and returns this path's geometry
103
- public get geometry(): Abstract2DShape[] {
115
+ public get geometry(): Parameterized2DShape[] {
104
116
  if (this.cachedGeometry) {
105
117
  return this.cachedGeometry;
106
118
  }
107
119
 
108
120
  let startPoint = this.startPoint;
109
- const geometry: Abstract2DShape[] = [];
121
+ const geometry: Parameterized2DShape[] = [];
110
122
 
111
123
  for (const part of this.parts) {
112
124
  let exhaustivenessCheck: never;
@@ -270,7 +282,7 @@ export class Path {
270
282
 
271
283
  type DistanceFunction = (point: Point2) => number;
272
284
  type DistanceFunctionRecord = {
273
- part: Abstract2DShape,
285
+ part: Parameterized2DShape,
274
286
  bbox: Rect2,
275
287
  distFn: DistanceFunction,
276
288
  };
@@ -309,9 +321,9 @@ export class Path {
309
321
 
310
322
  // Returns the minimum distance to a part in this stroke, where only parts that the given
311
323
  // line could intersect are considered.
312
- const sdf = (point: Point2): [Abstract2DShape|null, number] => {
324
+ const sdf = (point: Point2): [Parameterized2DShape|null, number] => {
313
325
  let minDist = Infinity;
314
- let minDistPart: Abstract2DShape|null = null;
326
+ let minDistPart: Parameterized2DShape|null = null;
315
327
 
316
328
  const uncheckedDistFunctions: DistanceFunctionRecord[] = [];
317
329
 
@@ -338,7 +350,7 @@ export class Path {
338
350
  for (const { part, distFn, bbox } of uncheckedDistFunctions) {
339
351
  // Skip if impossible for the distance to the target to be lesser than
340
352
  // the current minimum.
341
- if (!bbox.grownBy(minDist).containsPoint(point)) {
353
+ if (isFinite(minDist) && !bbox.grownBy(minDist).containsPoint(point)) {
342
354
  continue;
343
355
  }
344
356
 
@@ -388,7 +400,7 @@ export class Path {
388
400
 
389
401
  const stoppingThreshold = strokeRadius / 1000;
390
402
 
391
- // Returns the maximum x value explored
403
+ // Returns the maximum parameter value explored
392
404
  const raymarchFrom = (
393
405
  startPoint: Point2,
394
406
 
@@ -446,9 +458,15 @@ export class Path {
446
458
  if (lastPart && isOnLineSegment && Math.abs(lastDist) < stoppingThreshold) {
447
459
  result.push({
448
460
  point: currentPoint,
449
- parameterValue: NaN,
461
+ parameterValue: NaN,// lastPart.nearestPointTo(currentPoint).parameterValue,
450
462
  curve: lastPart,
463
+ curveIndex: this.geometry.indexOf(lastPart),
451
464
  });
465
+
466
+ // Slightly increase the parameter value to prevent the same point from being
467
+ // added to the results twice.
468
+ const parameterIncrease = strokeRadius / 20 / line.length;
469
+ lastParameter += isFinite(parameterIncrease) ? parameterIncrease : 0;
452
470
  }
453
471
 
454
472
  return lastParameter;
@@ -489,15 +507,20 @@ export class Path {
489
507
  return [];
490
508
  }
491
509
 
510
+ let index = 0;
492
511
  for (const part of this.geometry) {
493
- const intersection = part.intersectsLineSegment(line);
512
+ const intersections = part.argIntersectsLineSegment(line);
494
513
 
495
- if (intersection.length > 0) {
514
+ for (const intersection of intersections) {
496
515
  result.push({
497
516
  curve: part,
498
- point: intersection[0],
517
+ curveIndex: index,
518
+ point: part.at(intersection),
519
+ parameterValue: intersection,
499
520
  });
500
521
  }
522
+
523
+ index ++;
501
524
  }
502
525
 
503
526
  // If given a non-zero strokeWidth, attempt to raymarch.
@@ -513,6 +536,47 @@ export class Path {
513
536
  return result;
514
537
  }
515
538
 
539
+ /**
540
+ * @returns the nearest point on this path to the given `point`.
541
+ *
542
+ * @internal
543
+ * @beta
544
+ */
545
+ public nearestPointTo(point: Point2): IntersectionResult {
546
+ // Find the closest point on this
547
+ let closestSquareDist = Infinity;
548
+ let closestPartIndex = 0;
549
+ let closestParameterValue = 0;
550
+ let closestPoint: Point2 = this.startPoint;
551
+
552
+ for (let i = 0; i < this.geometry.length; i++) {
553
+ const current = this.geometry[i];
554
+ const nearestPoint = current.nearestPointTo(point);
555
+ const sqareDist = nearestPoint.point.squareDistanceTo(point);
556
+ if (i === 0 || sqareDist < closestSquareDist) {
557
+ closestPartIndex = i;
558
+ closestSquareDist = sqareDist;
559
+ closestParameterValue = nearestPoint.parameterValue;
560
+ closestPoint = nearestPoint.point;
561
+ }
562
+ }
563
+
564
+ return {
565
+ curve: this.geometry[closestPartIndex],
566
+ curveIndex: closestPartIndex,
567
+ parameterValue: closestParameterValue,
568
+ point: closestPoint,
569
+ };
570
+ }
571
+
572
+ public at(index: CurveIndexRecord) {
573
+ return this.geometry[index.curveIndex].at(index.parameterValue);
574
+ }
575
+
576
+ public tangentAt(index: CurveIndexRecord) {
577
+ return this.geometry[index.curveIndex].tangentAt(index.parameterValue);
578
+ }
579
+
516
580
  private static mapPathCommand(part: PathCommand, mapping: (point: Point2)=> Point2): PathCommand {
517
581
  switch (part.kind) {
518
582
  case PathCommandType.MoveTo:
@@ -563,19 +627,88 @@ export class Path {
563
627
  }
564
628
 
565
629
  // Creates a new path by joining [other] to the end of this path
566
- public union(other: Path|null): Path {
630
+ public union(
631
+ other: Path|null,
632
+
633
+ // allowReverse: true iff reversing other or this is permitted if it means
634
+ // no moveTo command is necessary when unioning the paths.
635
+ options: { allowReverse?: boolean } = { allowReverse: true },
636
+ ): Path {
567
637
  if (!other) {
568
638
  return this;
569
639
  }
570
640
 
571
- return new Path(this.startPoint, [
572
- ...this.parts,
641
+ const thisEnd = this.getEndPoint();
642
+
643
+ let newParts: Readonly<PathCommand>[] = [];
644
+ if (thisEnd.eq(other.startPoint)) {
645
+ newParts = this.parts.concat(other.parts);
646
+ } else if (options.allowReverse && this.startPoint.eq(other.getEndPoint())) {
647
+ return other.union(this, { allowReverse: false });
648
+ } else if (options.allowReverse && this.startPoint.eq(other.startPoint)) {
649
+ return this.union(other.reversed(), { allowReverse: false });
650
+ } else {
651
+ newParts = [
652
+ ...this.parts,
653
+ {
654
+ kind: PathCommandType.MoveTo,
655
+ point: other.startPoint,
656
+ },
657
+ ...other.parts,
658
+ ];
659
+ }
660
+ return new Path(this.startPoint, newParts);
661
+ }
662
+
663
+ /**
664
+ * @returns a version of this path with the direction reversed.
665
+ *
666
+ * Example:
667
+ * ```ts,runnable,console
668
+ * import {Path} from '@js-draw/math';
669
+ * console.log(Path.fromString('m0,0l1,1').reversed()); // -> M1,1 L0,0
670
+ * ```
671
+ */
672
+ public reversed() {
673
+ const newStart = this.getEndPoint();
674
+ const newParts: Readonly<PathCommand>[] = [];
675
+ let lastPoint: Point2 = this.startPoint;
676
+ for (const part of this.parts) {
677
+ switch (part.kind) {
678
+ case PathCommandType.LineTo:
679
+ case PathCommandType.MoveTo:
680
+ newParts.push({
681
+ kind: part.kind,
682
+ point: lastPoint,
683
+ });
684
+ lastPoint = part.point;
685
+ break;
686
+ case PathCommandType.CubicBezierTo:
687
+ newParts.push({
688
+ kind: part.kind,
689
+ controlPoint1: part.controlPoint2,
690
+ controlPoint2: part.controlPoint1,
691
+ endPoint: lastPoint,
692
+ });
693
+ lastPoint = part.endPoint;
694
+ break;
695
+ case PathCommandType.QuadraticBezierTo:
696
+ newParts.push({
697
+ kind: part.kind,
698
+ controlPoint: part.controlPoint,
699
+ endPoint: lastPoint,
700
+ });
701
+ lastPoint = part.endPoint;
702
+ break;
703
+ default:
573
704
  {
574
- kind: PathCommandType.MoveTo,
575
- point: other.startPoint,
576
- },
577
- ...other.parts,
578
- ]);
705
+ const exhaustivenessCheck: never = part;
706
+ return exhaustivenessCheck;
707
+ }
708
+ }
709
+ }
710
+ newParts.reverse();
711
+ return new Path(newStart, newParts);
579
712
  }
580
713
 
581
714
  private getEndPoint() {
@@ -682,6 +815,57 @@ export class Path {
682
815
  return false;
683
816
  }
684
817
 
818
+ /** @returns true if all points on this are equivalent to the points on `other` */
819
+ public eq(other: Path, tolerance?: number) {
820
+ if (other.parts.length !== this.parts.length) {
821
+ return false;
822
+ }
823
+
824
+ for (let i = 0; i < this.parts.length; i++) {
825
+ const part1 = this.parts[i];
826
+ const part2 = other.parts[i];
827
+
828
+ switch (part1.kind) {
829
+ case PathCommandType.LineTo:
830
+ case PathCommandType.MoveTo:
831
+ if (part1.kind !== part2.kind) {
832
+ return false;
833
+ } else if(!part1.point.eq(part2.point, tolerance)) {
834
+ return false;
835
+ }
836
+ break;
837
+ case PathCommandType.CubicBezierTo:
838
+ if (part1.kind !== part2.kind) {
839
+ return false;
840
+ } else if (
841
+ !part1.controlPoint1.eq(part2.controlPoint1, tolerance)
842
+ || !part1.controlPoint2.eq(part2.controlPoint2, tolerance)
843
+ || !part1.endPoint.eq(part2.endPoint, tolerance)
844
+ ) {
845
+ return false;
846
+ }
847
+ break;
848
+ case PathCommandType.QuadraticBezierTo:
849
+ if (part1.kind !== part2.kind) {
850
+ return false;
851
+ } else if (
852
+ !part1.controlPoint.eq(part2.controlPoint, tolerance)
853
+ || !part1.endPoint.eq(part2.endPoint, tolerance)
854
+ ) {
855
+ return false;
856
+ }
857
+ break;
858
+ default:
859
+ {
860
+ const exhaustivenessCheck: never = part1;
861
+ return exhaustivenessCheck;
862
+ }
863
+ }
864
+ }
865
+
866
+ return true;
867
+ }
868
+
685
869
  /**
686
870
  * Returns a path that outlines `rect`.
687
871
  *
@@ -1,7 +1,7 @@
1
- import { Point2 } from '../Vec2';
1
+ import { Point2, Vec2 } 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
  /**
@@ -9,18 +9,18 @@ import Rect2 from './Rect2';
9
9
  *
10
10
  * Access the internal `Point2` using the `p` property.
11
11
  */
12
- class PointShape2D extends Abstract2DShape {
12
+ class PointShape2D extends Parameterized2DShape {
13
13
  public constructor(public readonly p: Point2) {
14
14
  super();
15
15
  }
16
16
 
17
17
  public override signedDistance(point: Vec3): number {
18
- return this.p.minus(point).magnitude();
18
+ return this.p.distanceTo(point);
19
19
  }
20
20
 
21
- public override intersectsLineSegment(lineSegment: LineSegment2, epsilon?: number): Vec3[] {
21
+ public override argIntersectsLineSegment(lineSegment: LineSegment2, epsilon?: number): number[] {
22
22
  if (lineSegment.containsPoint(this.p, epsilon)) {
23
- return [ this.p ];
23
+ return [ 0 ];
24
24
  }
25
25
  return [ ];
26
26
  }
@@ -28,6 +28,33 @@ class PointShape2D extends Abstract2DShape {
28
28
  public override getTightBoundingBox(): Rect2 {
29
29
  return new Rect2(this.p.x, this.p.y, 0, 0);
30
30
  }
31
+
32
+ public override at(_t: number) {
33
+ return this.p;
34
+ }
35
+
36
+ /**
37
+ * Returns an arbitrary unit-length vector.
38
+ */
39
+ public override normalAt(_t: number) {
40
+ // Return a vector that makes sense.
41
+ return Vec2.unitY;
42
+ }
43
+
44
+ public override tangentAt(_t: number): Vec3 {
45
+ return Vec2.unitX;
46
+ }
47
+
48
+ public override splitAt(_t: number): [PointShape2D] {
49
+ return [this];
50
+ }
51
+
52
+ public override nearestPointTo(_point: Point2) {
53
+ return {
54
+ point: this.p,
55
+ parameterValue: 0,
56
+ };
57
+ }
31
58
  }
32
59
 
33
60
  export default PointShape2D;
@@ -2,13 +2,12 @@ import { Vec2 } from '../Vec2';
2
2
  import QuadraticBezier from './QuadraticBezier';
3
3
 
4
4
  describe('QuadraticBezier', () => {
5
- it('approxmiateDistance should approximately return the distance to the curve', () => {
6
- const curves = [
7
- new QuadraticBezier(Vec2.zero, Vec2.of(10, 0), Vec2.of(20, 0)),
8
- new QuadraticBezier(Vec2.of(-10, 0), Vec2.of(2, 10), Vec2.of(20, 0)),
9
- new QuadraticBezier(Vec2.of(0, 0), Vec2.of(4, -10), Vec2.of(20, 60)),
10
- new QuadraticBezier(Vec2.of(0, 0), Vec2.of(4, -10), Vec2.of(-20, 60)),
11
- ];
5
+ test.each([
6
+ new QuadraticBezier(Vec2.zero, Vec2.of(10, 0), Vec2.of(20, 0)),
7
+ new QuadraticBezier(Vec2.of(-10, 0), Vec2.of(2, 10), Vec2.of(20, 0)),
8
+ new QuadraticBezier(Vec2.of(0, 0), Vec2.of(4, -10), Vec2.of(20, 60)),
9
+ new QuadraticBezier(Vec2.of(0, 0), Vec2.of(4, -10), Vec2.of(-20, 60)),
10
+ ])('approxmiateDistance should approximately return the distance to the curve (%s)', (curve) => {
12
11
  const testPoints = [
13
12
  Vec2.of(1, 1),
14
13
  Vec2.of(-1, 1),
@@ -18,13 +17,50 @@ describe('QuadraticBezier', () => {
18
17
  Vec2.of(5, 0),
19
18
  ];
20
19
 
20
+ for (const point of testPoints) {
21
+ const actualDist = curve.distance(point);
22
+ const approxDist = curve.approximateDistance(point);
23
+
24
+ expect(approxDist).toBeGreaterThan(actualDist * 0.6 - 0.25);
25
+ expect(approxDist).toBeLessThan(actualDist * 1.5 + 2.6);
26
+ }
27
+ });
28
+
29
+ test.each([
30
+ [ new QuadraticBezier(Vec2.zero, Vec2.unitX, Vec2.unitY), Vec2.zero, 0 ],
31
+ [ new QuadraticBezier(Vec2.zero, Vec2.unitX, Vec2.unitY), Vec2.unitY, 1 ],
32
+
33
+ [ new QuadraticBezier(Vec2.zero, Vec2.of(0.5, 0), Vec2.of(1, 0)), Vec2.of(0.4, 0), 0.4],
34
+ [ new QuadraticBezier(Vec2.zero, Vec2.of(0, 0.5), Vec2.of(0, 1)), Vec2.of(0, 0.4), 0.4],
35
+ [ new QuadraticBezier(Vec2.zero, Vec2.unitX, Vec2.unitY), Vec2.unitX, 0.42514 ],
36
+
37
+ // Should not return an out-of-range parameter
38
+ [ new QuadraticBezier(Vec2.zero, Vec2.of(0, 0.5), Vec2.unitY), Vec2.of(0, -1000), 0 ],
39
+ [ new QuadraticBezier(Vec2.zero, Vec2.of(0, 0.5), Vec2.unitY), Vec2.of(0, 1000), 1 ],
40
+ ])('nearestPointTo should return the nearest point and parameter value on %s to %s', (bezier, point, expectedParameter) => {
41
+ const nearest = bezier.nearestPointTo(point);
42
+ expect(nearest.parameterValue).toBeCloseTo(expectedParameter, 0.0001);
43
+ expect(nearest.point).objEq(bezier.at(nearest.parameterValue));
44
+ });
45
+
46
+ test('.normalAt should return a unit normal vector at the given parameter value', () => {
47
+ const curves = [
48
+ new QuadraticBezier(Vec2.zero, Vec2.unitY, Vec2.unitY.times(2)),
49
+ new QuadraticBezier(Vec2.zero, Vec2.unitX, Vec2.unitY),
50
+ new QuadraticBezier(Vec2.zero, Vec2.unitX, Vec2.unitY.times(-2)),
51
+ new QuadraticBezier(Vec2.of(2, 3), Vec2.of(4, 5.1), Vec2.of(6, 7)),
52
+ new QuadraticBezier(Vec2.of(2, 3), Vec2.of(100, 1000), Vec2.unitY.times(-2)),
53
+ ];
54
+
21
55
  for (const curve of curves) {
22
- for (const point of testPoints) {
23
- const actualDist = curve.distance(point);
24
- const approxDist = curve.approximateDistance(point);
56
+ for (let t = 0; t < 1; t += 0.1) {
57
+ const normal = curve.normalAt(t);
58
+ expect(normal.length()).toBe(1);
59
+
60
+ const tangentApprox = curve.at(t + 0.001).minus(curve.at(t - 0.001));
25
61
 
26
- expect(approxDist).toBeGreaterThan(actualDist * 0.6 - 0.25);
27
- expect(approxDist).toBeLessThan(actualDist * 1.5 + 2.6);
62
+ // The tangent vector should be perpindicular to the normal
63
+ expect(tangentApprox.dot(normal)).toBeCloseTo(0);
28
64
  }
29
65
  }
30
66
  });
@@ -30,10 +30,19 @@ export class QuadraticBezier extends BezierJSWrapper {
30
30
  return -2 * p0 + 2 * p1 + 2 * t * (p0 - 2 * p1 + p2);
31
31
  }
32
32
 
33
+ private static secondDerivativeComponentAt(t: number, p0: number, p1: number, p2: number) {
34
+ return 2 * (p0 - 2 * p1 + p2);
35
+ }
36
+
33
37
  /**
34
38
  * @returns the curve evaluated at `t`.
39
+ *
40
+ * `t` should be a number in `[0, 1]`.
35
41
  */
36
42
  public override at(t: number): Point2 {
43
+ if (t === 0) return this.p0;
44
+ if (t === 1) return this.p2;
45
+
37
46
  const p0 = this.p0;
38
47
  const p1 = this.p1;
39
48
  const p2 = this.p2;
@@ -53,6 +62,16 @@ export class QuadraticBezier extends BezierJSWrapper {
53
62
  );
54
63
  }
55
64
 
65
+ public override secondDerivativeAt(t: number): Point2 {
66
+ const p0 = this.p0;
67
+ const p1 = this.p1;
68
+ const p2 = this.p2;
69
+ return Vec2.of(
70
+ QuadraticBezier.secondDerivativeComponentAt(t, p0.x, p1.x, p2.x),
71
+ QuadraticBezier.secondDerivativeComponentAt(t, p0.y, p1.y, p2.y),
72
+ );
73
+ }
74
+
56
75
  public override normal(t: number): Vec2 {
57
76
  const tangent = this.derivativeAt(t);
58
77
  return tangent.orthog().normalized();
@@ -126,11 +145,10 @@ export class QuadraticBezier extends BezierJSWrapper {
126
145
 
127
146
  const at1 = this.at(min1);
128
147
  const at2 = this.at(min2);
129
- const sqrDist1 = at1.minus(point).magnitudeSquared();
130
- const sqrDist2 = at2.minus(point).magnitudeSquared();
131
- const sqrDist3 = this.at(0).minus(point).magnitudeSquared();
132
- const sqrDist4 = this.at(1).minus(point).magnitudeSquared();
133
-
148
+ const sqrDist1 = at1.squareDistanceTo(point);
149
+ const sqrDist2 = at2.squareDistanceTo(point);
150
+ const sqrDist3 = this.at(0).squareDistanceTo(point);
151
+ const sqrDist4 = this.at(1).squareDistanceTo(point);
134
152
 
135
153
  return Math.sqrt(Math.min(sqrDist1, sqrDist2, sqrDist3, sqrDist4));
136
154
  }
@@ -67,6 +67,9 @@ export class Rect2 extends Abstract2DShape {
67
67
  && this.y + this.h >= other.y + other.h;
68
68
  }
69
69
 
70
+ /**
71
+ * @returns true iff this and `other` overlap
72
+ */
70
73
  public intersects(other: Rect2): boolean {
71
74
  // Project along x/y axes.
72
75
  const thisMinX = this.x;
@@ -181,7 +184,7 @@ export class Rect2 extends Abstract2DShape {
181
184
  let closest: Point2|null = null;
182
185
  let closestDist: number|null = null;
183
186
  for (const point of closestEdgePoints) {
184
- const dist = point.minus(target).length();
187
+ const dist = point.distanceTo(target);
185
188
  if (closestDist === null || dist < closestDist) {
186
189
  closest = point;
187
190
  closestDist = dist;