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