@js-draw/math 1.11.1 → 1.17.0

Sign up to get free protection for your applications and to get access to all the features.
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
@@ -0,0 +1,39 @@
1
+ import { Point2, Vec2 } from '../Vec2';
2
+ import Abstract2DShape from './Abstract2DShape';
3
+ import LineSegment2 from './LineSegment2';
4
+
5
+ /** A 2-dimensional path with parameter interval $t \in [0, 1]$. */
6
+ export abstract class Parameterized2DShape extends Abstract2DShape {
7
+ /** Returns this at a given parameter. $t \in [0, 1]$ */
8
+ abstract at(t: number): Point2;
9
+
10
+ /** Computes the unit normal vector at $t$. */
11
+ abstract normalAt(t: number): Vec2;
12
+
13
+ abstract tangentAt(t: number): Vec2;
14
+
15
+ /**
16
+ * Divides this shape into two separate shapes at parameter value $t$.
17
+ */
18
+ abstract splitAt(t: number): [ Parameterized2DShape ] | [ Parameterized2DShape, Parameterized2DShape ];
19
+
20
+ /**
21
+ * Returns the nearest point on `this` to `point` and the `parameterValue` at which
22
+ * that point occurs.
23
+ */
24
+ abstract nearestPointTo(point: Point2): { point: Point2, parameterValue: number };
25
+
26
+ /**
27
+ * Returns the **parameter values** at which `lineSegment` intersects this shape.
28
+ *
29
+ * See also {@link intersectsLineSegment}
30
+ */
31
+ public abstract argIntersectsLineSegment(lineSegment: LineSegment2): number[];
32
+
33
+
34
+ public override intersectsLineSegment(line: LineSegment2): Point2[] {
35
+ return this.argIntersectsLineSegment(line).map(t => this.at(t));
36
+ }
37
+ }
38
+
39
+ export default Parameterized2DShape;
@@ -60,6 +60,24 @@ describe('Path', () => {
60
60
  );
61
61
  });
62
62
 
63
+ it.each([
64
+ [ 'm0,0 L1,1', 'M0,0 L1,1', true ],
65
+ [ 'm0,0 L1,1', 'M1,1 L0,0', false ],
66
+ [ 'm0,0 L1,1 Q2,3 4,5', 'M1,1 L0,0', false ],
67
+ [ 'm0,0 L1,1 Q2,3 4,5', 'M1,1 L0,0 Q2,3 4,5', false ],
68
+ [ 'm0,0 L1,1 Q2,3 4,5', 'M0,0 L1,1 Q2,3 4,5', true ],
69
+ [ 'm0,0 L1,1 Q2,3 4,5 C4,5 6,7 8,9', 'M0,0 L1,1 Q2,3 4,5 C4,5 6,7 8,9', true ],
70
+ [ 'm0,0 L1,1 Q2,3 4,5 C4,5 6,7 8,9Z', 'M0,0 L1,1 Q2,3 4,5 C4,5 6,7 8,9', false ],
71
+ [ 'm0,0 L1,1 Q2,3 4,5 C4,5 6,7 8,9', 'M0,0 L1,1 Q2,3 4,5 C4,5 6,7 8,9Z', false ],
72
+ [ 'm0,0 L1,1 Q2,3 4,5 C4,5 6,7 8,9', 'M0,0 L1,1 Q2,3 4,5 C4,5 6,7 8,9.01', false ],
73
+ [ 'm0,0 L1,1 Q2,3 4,5 C4,5 6,7 8,9', 'M0,0 L1,1 Q2,3 4,5 C4,5 6,7.01 8,9', false ],
74
+ [ 'm0,0 L1,1 Q2,3 4,5 C4,5 6,7 8,9', 'M0,0 L1,1 Q2,3 4,5 C4,5.01 6,7 8,9', false ],
75
+ ])('.eq should check equality', (path1Str, path2Str, shouldEqual) => {
76
+ expect(Path.fromString(path1Str)).objEq(Path.fromString(path1Str));
77
+ expect(Path.fromString(path2Str)).objEq(Path.fromString(path2Str));
78
+ expect(Path.fromString(path1Str).eq(Path.fromString(path2Str))).toBe(shouldEqual);
79
+ });
80
+
63
81
  describe('intersection', () => {
64
82
  it('should give all intersections for a path made up of lines', () => {
65
83
  const lineStart = Vec2.of(100, 100);
@@ -179,7 +197,7 @@ describe('Path', () => {
179
197
  });
180
198
  });
181
199
 
182
- it('should give all intersections for a Bézier stroked path', () => {
200
+ it('should correctly report intersections for a simple Bézier curve path', () => {
183
201
  const lineStart = Vec2.zero;
184
202
  const path = new Path(lineStart, [
185
203
  {
@@ -196,13 +214,36 @@ describe('Path', () => {
196
214
  let intersections = path.intersection(
197
215
  new LineSegment2(Vec2.of(-1, 0.5), Vec2.of(2, 0.5)), strokeWidth,
198
216
  );
199
- expect(intersections.length).toBe(0);
217
+ expect(intersections).toHaveLength(0);
200
218
 
201
219
  // Should be an intersection when exiting/entering the edge of the stroke
202
220
  intersections = path.intersection(
203
221
  new LineSegment2(Vec2.of(0, 0.5), Vec2.of(8, 0.5)), strokeWidth,
204
222
  );
205
- expect(intersections.length).toBe(1);
223
+ expect(intersections).toHaveLength(1);
224
+ });
225
+
226
+ it('should correctly report intersections near the cap of a line-like Bézier', () => {
227
+ const path = Path.fromString('M0,0Q14,0 27,0');
228
+ expect(
229
+ path.intersection(
230
+ new LineSegment2(Vec2.of(0, -100), Vec2.of(0, 100)),
231
+ 10,
232
+ ),
233
+
234
+ // Should have intersections, despite being at the cap of the Bézier
235
+ // curve.
236
+ ).toHaveLength(2);
237
+ });
238
+
239
+ it.each([
240
+ [new LineSegment2(Vec2.of(43.5,-12.5), Vec2.of(40.5,24.5)), 0],
241
+ // TODO: The below case is failing. It seems to be a Bezier-js bug though...
242
+ // (The Bézier.js method returns an empty array).
243
+ //[new LineSegment2(Vec2.of(35.5,19.5), Vec2.of(38.5,-17.5)), 0],
244
+ ])('should correctly report positive intersections with a line-like Bézier', (line, strokeRadius) => {
245
+ const bezier = Path.fromString('M0,0 Q50,0 100,0');
246
+ expect(bezier.intersection(line, strokeRadius).length).toBeGreaterThan(0);
206
247
  });
207
248
  });
208
249
 
@@ -306,4 +347,23 @@ describe('Path', () => {
306
347
  expect(strokedRect.startPoint).objEq(lastSegment.point);
307
348
  });
308
349
  });
350
+
351
+ it.each([
352
+ [ 'm0,0 L1,1', 'M1,1 L0,0' ],
353
+ [ 'm0,0 L1,1', 'M1,1 L0,0' ],
354
+ [ 'M0,0 L1,1 Q2,2 3,3', 'M3,3 Q2,2 1,1 L0,0' ],
355
+ [ 'M0,0 L1,1 Q4,2 5,3 C12,13 10,9 8,7', 'M8,7 C 10,9 12,13 5,3 Q 4,2 1,1 L 0,0' ],
356
+ ])('.reversed should reverse paths', (original, expected) => {
357
+ expect(Path.fromString(original).reversed()).objEq(Path.fromString(expected));
358
+ expect(Path.fromString(expected).reversed()).objEq(Path.fromString(original));
359
+ expect(Path.fromString(original).reversed().reversed()).objEq(Path.fromString(original));
360
+ });
361
+
362
+ it.each([
363
+ [ 'm0,0 l1,0', Vec2.of(0, 0), Vec2.of(0, 0) ],
364
+ [ 'm0,0 l1,0', Vec2.of(0.5, 0), Vec2.of(0.5, 0) ],
365
+ [ 'm0,0 Q1,0 1,2', Vec2.of(1, 0), Vec2.of(0.6236, 0.299) ],
366
+ ])('.nearestPointTo should return the closest point on a path to the given parameter (case %#)', (path, point, expectedClosest) => {
367
+ expect(Path.fromString(path).nearestPointTo(point).point).objEq(expectedClosest, 0.002);
368
+ });
309
369
  });
@@ -1,12 +1,13 @@
1
- import { toRoundedString, toStringOfSamePrecision } from '../rounding';
2
1
  import LineSegment2 from './LineSegment2';
3
2
  import Mat33 from '../Mat33';
4
3
  import Rect2 from './Rect2';
5
4
  import { Point2, Vec2 } from '../Vec2';
6
- import Abstract2DShape from './Abstract2DShape';
7
5
  import CubicBezier from './CubicBezier';
8
6
  import QuadraticBezier from './QuadraticBezier';
9
7
  import PointShape2D from './PointShape2D';
8
+ import toRoundedString from '../rounding/toRoundedString';
9
+ import toStringOfSamePrecision from '../rounding/toStringOfSamePrecision';
10
+ import Parameterized2DShape from './Parameterized2DShape';
10
11
 
11
12
  export enum PathCommandType {
12
13
  LineTo,
@@ -40,17 +41,29 @@ export interface MoveToPathCommand {
40
41
 
41
42
  export type PathCommand = CubicBezierPathCommand | QuadraticBezierPathCommand | MoveToPathCommand | LinePathCommand;
42
43
 
43
- interface IntersectionResult {
44
+ export interface IntersectionResult {
44
45
  // @internal
45
- curve: Abstract2DShape;
46
+ curve: Parameterized2DShape;
47
+ // @internal
48
+ curveIndex: number;
46
49
 
47
- /** @internal @deprecated */
50
+ /** Parameter value for the closest point **on** the path to the intersection. @internal @deprecated */
48
51
  parameterValue?: number;
49
52
 
50
- // Point at which the intersection occured.
53
+ /** Point at which the intersection occured. */
51
54
  point: Point2;
52
55
  }
53
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
+
54
67
  /**
55
68
  * Represents a union of lines and curves.
56
69
  */
@@ -96,16 +109,16 @@ export class Path {
96
109
  return Rect2.union(...bboxes);
97
110
  }
98
111
 
99
- private cachedGeometry: Abstract2DShape[]|null = null;
112
+ private cachedGeometry: Parameterized2DShape[]|null = null;
100
113
 
101
114
  // Lazy-loads and returns this path's geometry
102
- public get geometry(): Abstract2DShape[] {
115
+ public get geometry(): Parameterized2DShape[] {
103
116
  if (this.cachedGeometry) {
104
117
  return this.cachedGeometry;
105
118
  }
106
119
 
107
120
  let startPoint = this.startPoint;
108
- const geometry: Abstract2DShape[] = [];
121
+ const geometry: Parameterized2DShape[] = [];
109
122
 
110
123
  for (const part of this.parts) {
111
124
  let exhaustivenessCheck: never;
@@ -269,7 +282,7 @@ export class Path {
269
282
 
270
283
  type DistanceFunction = (point: Point2) => number;
271
284
  type DistanceFunctionRecord = {
272
- part: Abstract2DShape,
285
+ part: Parameterized2DShape,
273
286
  bbox: Rect2,
274
287
  distFn: DistanceFunction,
275
288
  };
@@ -308,9 +321,9 @@ export class Path {
308
321
 
309
322
  // Returns the minimum distance to a part in this stroke, where only parts that the given
310
323
  // line could intersect are considered.
311
- const sdf = (point: Point2): [Abstract2DShape|null, number] => {
324
+ const sdf = (point: Point2): [Parameterized2DShape|null, number] => {
312
325
  let minDist = Infinity;
313
- let minDistPart: Abstract2DShape|null = null;
326
+ let minDistPart: Parameterized2DShape|null = null;
314
327
 
315
328
  const uncheckedDistFunctions: DistanceFunctionRecord[] = [];
316
329
 
@@ -337,7 +350,7 @@ export class Path {
337
350
  for (const { part, distFn, bbox } of uncheckedDistFunctions) {
338
351
  // Skip if impossible for the distance to the target to be lesser than
339
352
  // the current minimum.
340
- if (!bbox.grownBy(minDist).containsPoint(point)) {
353
+ if (isFinite(minDist) && !bbox.grownBy(minDist).containsPoint(point)) {
341
354
  continue;
342
355
  }
343
356
 
@@ -387,7 +400,7 @@ export class Path {
387
400
 
388
401
  const stoppingThreshold = strokeRadius / 1000;
389
402
 
390
- // Returns the maximum x value explored
403
+ // Returns the maximum parameter value explored
391
404
  const raymarchFrom = (
392
405
  startPoint: Point2,
393
406
 
@@ -445,9 +458,15 @@ export class Path {
445
458
  if (lastPart && isOnLineSegment && Math.abs(lastDist) < stoppingThreshold) {
446
459
  result.push({
447
460
  point: currentPoint,
448
- parameterValue: NaN,
461
+ parameterValue: NaN,// lastPart.nearestPointTo(currentPoint).parameterValue,
449
462
  curve: lastPart,
463
+ curveIndex: this.geometry.indexOf(lastPart),
450
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;
451
470
  }
452
471
 
453
472
  return lastParameter;
@@ -488,15 +507,20 @@ export class Path {
488
507
  return [];
489
508
  }
490
509
 
510
+ let index = 0;
491
511
  for (const part of this.geometry) {
492
- const intersection = part.intersectsLineSegment(line);
512
+ const intersections = part.argIntersectsLineSegment(line);
493
513
 
494
- if (intersection.length > 0) {
514
+ for (const intersection of intersections) {
495
515
  result.push({
496
516
  curve: part,
497
- point: intersection[0],
517
+ curveIndex: index,
518
+ point: part.at(intersection),
519
+ parameterValue: intersection,
498
520
  });
499
521
  }
522
+
523
+ index ++;
500
524
  }
501
525
 
502
526
  // If given a non-zero strokeWidth, attempt to raymarch.
@@ -512,6 +536,47 @@ export class Path {
512
536
  return result;
513
537
  }
514
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
+
515
580
  private static mapPathCommand(part: PathCommand, mapping: (point: Point2)=> Point2): PathCommand {
516
581
  switch (part.kind) {
517
582
  case PathCommandType.MoveTo:
@@ -562,19 +627,88 @@ export class Path {
562
627
  }
563
628
 
564
629
  // Creates a new path by joining [other] to the end of this path
565
- 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 {
566
637
  if (!other) {
567
638
  return this;
568
639
  }
569
640
 
570
- return new Path(this.startPoint, [
571
- ...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:
572
704
  {
573
- kind: PathCommandType.MoveTo,
574
- point: other.startPoint,
575
- },
576
- ...other.parts,
577
- ]);
705
+ const exhaustivenessCheck: never = part;
706
+ return exhaustivenessCheck;
707
+ }
708
+ }
709
+ }
710
+ newParts.reverse();
711
+ return new Path(newStart, newParts);
578
712
  }
579
713
 
580
714
  private getEndPoint() {
@@ -681,6 +815,57 @@ export class Path {
681
815
  return false;
682
816
  }
683
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
+
684
869
  /**
685
870
  * Returns a path that outlines `rect`.
686
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
  }