@js-draw/math 1.16.0 → 1.18.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (64) hide show
  1. package/dist/cjs/Mat33.js +6 -1
  2. package/dist/cjs/Vec3.d.ts +23 -1
  3. package/dist/cjs/Vec3.js +33 -7
  4. package/dist/cjs/lib.d.ts +2 -1
  5. package/dist/cjs/lib.js +5 -1
  6. package/dist/cjs/shapes/Abstract2DShape.d.ts +3 -0
  7. package/dist/cjs/shapes/BezierJSWrapper.d.ts +19 -5
  8. package/dist/cjs/shapes/BezierJSWrapper.js +170 -18
  9. package/dist/cjs/shapes/LineSegment2.d.ts +45 -5
  10. package/dist/cjs/shapes/LineSegment2.js +89 -11
  11. package/dist/cjs/shapes/Parameterized2DShape.d.ts +36 -0
  12. package/dist/cjs/shapes/Parameterized2DShape.js +20 -0
  13. package/dist/cjs/shapes/Path.d.ts +131 -13
  14. package/dist/cjs/shapes/Path.js +507 -26
  15. package/dist/cjs/shapes/PointShape2D.d.ts +14 -3
  16. package/dist/cjs/shapes/PointShape2D.js +28 -5
  17. package/dist/cjs/shapes/QuadraticBezier.d.ts +6 -3
  18. package/dist/cjs/shapes/QuadraticBezier.js +21 -7
  19. package/dist/cjs/shapes/Rect2.d.ts +9 -1
  20. package/dist/cjs/shapes/Rect2.js +9 -2
  21. package/dist/cjs/utils/convexHull2Of.d.ts +9 -0
  22. package/dist/cjs/utils/convexHull2Of.js +61 -0
  23. package/dist/cjs/utils/convexHull2Of.test.d.ts +1 -0
  24. package/dist/mjs/Mat33.mjs +6 -1
  25. package/dist/mjs/Vec3.d.ts +23 -1
  26. package/dist/mjs/Vec3.mjs +33 -7
  27. package/dist/mjs/lib.d.ts +2 -1
  28. package/dist/mjs/lib.mjs +2 -1
  29. package/dist/mjs/shapes/Abstract2DShape.d.ts +3 -0
  30. package/dist/mjs/shapes/BezierJSWrapper.d.ts +19 -5
  31. package/dist/mjs/shapes/BezierJSWrapper.mjs +168 -18
  32. package/dist/mjs/shapes/LineSegment2.d.ts +45 -5
  33. package/dist/mjs/shapes/LineSegment2.mjs +89 -11
  34. package/dist/mjs/shapes/Parameterized2DShape.d.ts +36 -0
  35. package/dist/mjs/shapes/Parameterized2DShape.mjs +13 -0
  36. package/dist/mjs/shapes/Path.d.ts +131 -13
  37. package/dist/mjs/shapes/Path.mjs +504 -25
  38. package/dist/mjs/shapes/PointShape2D.d.ts +14 -3
  39. package/dist/mjs/shapes/PointShape2D.mjs +28 -5
  40. package/dist/mjs/shapes/QuadraticBezier.d.ts +6 -3
  41. package/dist/mjs/shapes/QuadraticBezier.mjs +21 -7
  42. package/dist/mjs/shapes/Rect2.d.ts +9 -1
  43. package/dist/mjs/shapes/Rect2.mjs +9 -2
  44. package/dist/mjs/utils/convexHull2Of.d.ts +9 -0
  45. package/dist/mjs/utils/convexHull2Of.mjs +59 -0
  46. package/dist/mjs/utils/convexHull2Of.test.d.ts +1 -0
  47. package/package.json +5 -5
  48. package/src/Mat33.ts +8 -2
  49. package/src/Vec3.test.ts +42 -7
  50. package/src/Vec3.ts +37 -8
  51. package/src/lib.ts +5 -0
  52. package/src/shapes/Abstract2DShape.ts +3 -0
  53. package/src/shapes/BezierJSWrapper.ts +195 -14
  54. package/src/shapes/LineSegment2.test.ts +61 -1
  55. package/src/shapes/LineSegment2.ts +110 -12
  56. package/src/shapes/Parameterized2DShape.ts +44 -0
  57. package/src/shapes/Path.test.ts +233 -5
  58. package/src/shapes/Path.ts +593 -37
  59. package/src/shapes/PointShape2D.ts +33 -6
  60. package/src/shapes/QuadraticBezier.test.ts +69 -12
  61. package/src/shapes/QuadraticBezier.ts +25 -8
  62. package/src/shapes/Rect2.ts +10 -3
  63. package/src/utils/convexHull2Of.test.ts +43 -0
  64. package/src/utils/convexHull2Of.ts +71 -0
@@ -6,6 +6,7 @@ import QuadraticBezier from './QuadraticBezier.mjs';
6
6
  import PointShape2D from './PointShape2D.mjs';
7
7
  import toRoundedString from '../rounding/toRoundedString.mjs';
8
8
  import toStringOfSamePrecision from '../rounding/toStringOfSamePrecision.mjs';
9
+ import convexHull2Of from '../utils/convexHull2Of.mjs';
9
10
  export var PathCommandType;
10
11
  (function (PathCommandType) {
11
12
  PathCommandType[PathCommandType["LineTo"] = 0] = "LineTo";
@@ -13,8 +14,62 @@ export var PathCommandType;
13
14
  PathCommandType[PathCommandType["CubicBezierTo"] = 2] = "CubicBezierTo";
14
15
  PathCommandType[PathCommandType["QuadraticBezierTo"] = 3] = "QuadraticBezierTo";
15
16
  })(PathCommandType || (PathCommandType = {}));
17
+ /** Returns a positive number if `a` comes after `b`, 0 if equal, and negative otherwise. */
18
+ export const compareCurveIndices = (a, b) => {
19
+ const indexCompare = a.curveIndex - b.curveIndex;
20
+ if (indexCompare === 0) {
21
+ return a.parameterValue - b.parameterValue;
22
+ }
23
+ else {
24
+ return indexCompare;
25
+ }
26
+ };
27
+ /**
28
+ * Returns a version of `index` with its parameter value incremented by `stepBy`
29
+ * (which can be either positive or negative).
30
+ */
31
+ export const stepCurveIndexBy = (index, stepBy) => {
32
+ if (index.parameterValue + stepBy > 1) {
33
+ return { curveIndex: index.curveIndex + 1, parameterValue: index.parameterValue + stepBy - 1 };
34
+ }
35
+ if (index.parameterValue + stepBy < 0) {
36
+ if (index.curveIndex === 0) {
37
+ return { curveIndex: 0, parameterValue: 0 };
38
+ }
39
+ return { curveIndex: index.curveIndex - 1, parameterValue: index.parameterValue + stepBy + 1 };
40
+ }
41
+ return { curveIndex: index.curveIndex, parameterValue: index.parameterValue + stepBy };
42
+ };
16
43
  /**
17
44
  * Represents a union of lines and curves.
45
+ *
46
+ * To create a path from a string, see {@link fromString}.
47
+ *
48
+ * @example
49
+ * ```ts,runnable,console
50
+ * import {Path, Mat33, Vec2, LineSegment2} from '@js-draw/math';
51
+ *
52
+ * // Creates a path from an SVG path string.
53
+ * // In this case,
54
+ * // 1. Move to (0,0)
55
+ * // 2. Line to (100,0)
56
+ * const path = Path.fromString('M0,0 L100,0');
57
+ *
58
+ * // Logs the distance from (10,0) to the curve 1 unit
59
+ * // away from path. This curve forms a stroke with the path at
60
+ * // its center.
61
+ * const strokeRadius = 1;
62
+ * console.log(path.signedDistance(Vec2.of(10,0), strokeRadius));
63
+ *
64
+ * // Log a version of the path that's scaled by a factor of 4.
65
+ * console.log(path.transformedBy(Mat33.scaling2D(4)).toString());
66
+ *
67
+ * // Log all intersections of a stroked version of the path with
68
+ * // a vertical line segment.
69
+ * // (Try removing the `strokeRadius` parameter).
70
+ * const segment = new LineSegment2(Vec2.of(5, -100), Vec2.of(5, 100));
71
+ * console.log(path.intersection(segment, strokeRadius).map(i => i.point));
72
+ * ```
18
73
  */
19
74
  export class Path {
20
75
  /**
@@ -37,6 +92,12 @@ export class Path {
37
92
  this.bbox = this.bbox.union(Path.computeBBoxForSegment(startPoint, part));
38
93
  }
39
94
  }
95
+ /**
96
+ * Computes and returns the full bounding box for this path.
97
+ *
98
+ * If a slight over-estimate of a path's bounding box is sufficient, use
99
+ * {@link bbox} instead.
100
+ */
40
101
  getExactBBox() {
41
102
  const bboxes = [];
42
103
  for (const part of this.geometry) {
@@ -155,7 +216,20 @@ export class Path {
155
216
  }
156
217
  return Rect2.bboxOf(points);
157
218
  }
158
- /** **Note**: `strokeRadius = strokeWidth / 2` */
219
+ /**
220
+ * Returns the signed distance between `point` and a curve `strokeRadius` units
221
+ * away from this path.
222
+ *
223
+ * This returns the **signed distance**, which means that points inside this shape
224
+ * have their distance negated. For example,
225
+ * ```ts,runnable,console
226
+ * import {Path, Vec2} from '@js-draw/math';
227
+ * console.log(Path.fromString('m0,0 L100,0').signedDistance(Vec2.zero, 1));
228
+ * ```
229
+ * would print `-1` because (0,0) is on `m0,0 L100,0` and thus one unit away from its boundary.
230
+ *
231
+ * **Note**: `strokeRadius = strokeWidth / 2`
232
+ */
159
233
  signedDistance(point, strokeRadius) {
160
234
  let minDist = Infinity;
161
235
  for (const part of this.geometry) {
@@ -230,7 +304,7 @@ export class Path {
230
304
  for (const { part, distFn, bbox } of uncheckedDistFunctions) {
231
305
  // Skip if impossible for the distance to the target to be lesser than
232
306
  // the current minimum.
233
- if (!bbox.grownBy(minDist).containsPoint(point)) {
307
+ if (isFinite(minDist) && !bbox.grownBy(minDist).containsPoint(point)) {
234
308
  continue;
235
309
  }
236
310
  const currentDist = distFn(point);
@@ -242,7 +316,7 @@ export class Path {
242
316
  return [minDistPart, minDist - strokeRadius];
243
317
  };
244
318
  // Raymarch:
245
- const maxRaymarchSteps = 7;
319
+ const maxRaymarchSteps = 8;
246
320
  // Start raymarching from each of these points. This allows detection of multiple
247
321
  // intersections.
248
322
  const startPoints = [
@@ -268,7 +342,7 @@ export class Path {
268
342
  });
269
343
  const result = [];
270
344
  const stoppingThreshold = strokeRadius / 1000;
271
- // Returns the maximum x value explored
345
+ // Returns the maximum parameter value explored
272
346
  const raymarchFrom = (startPoint,
273
347
  // Direction to march in (multiplies line.direction)
274
348
  directionMultiplier,
@@ -312,9 +386,14 @@ export class Path {
312
386
  if (lastPart && isOnLineSegment && Math.abs(lastDist) < stoppingThreshold) {
313
387
  result.push({
314
388
  point: currentPoint,
315
- parameterValue: NaN,
389
+ parameterValue: lastPart.nearestPointTo(currentPoint).parameterValue,
316
390
  curve: lastPart,
391
+ curveIndex: this.geometry.indexOf(lastPart),
317
392
  });
393
+ // Slightly increase the parameter value to prevent the same point from being
394
+ // added to the results twice.
395
+ const parameterIncrease = strokeRadius / 20 / line.length;
396
+ lastParameter += isFinite(parameterIncrease) ? parameterIncrease : 0;
318
397
  }
319
398
  return lastParameter;
320
399
  };
@@ -347,14 +426,21 @@ export class Path {
347
426
  if (!line.bbox.intersects(this.bbox.grownBy(strokeRadius ?? 0))) {
348
427
  return [];
349
428
  }
429
+ if (this.parts.length === 0) {
430
+ return new Path(this.startPoint, [{ kind: PathCommandType.MoveTo, point: this.startPoint }]).intersection(line, strokeRadius);
431
+ }
432
+ let index = 0;
350
433
  for (const part of this.geometry) {
351
- const intersection = part.intersectsLineSegment(line);
352
- if (intersection.length > 0) {
434
+ const intersections = part.argIntersectsLineSegment(line);
435
+ for (const intersection of intersections) {
353
436
  result.push({
354
437
  curve: part,
355
- point: intersection[0],
438
+ curveIndex: index,
439
+ point: part.at(intersection),
440
+ parameterValue: intersection,
356
441
  });
357
442
  }
443
+ index++;
358
444
  }
359
445
  // If given a non-zero strokeWidth, attempt to raymarch.
360
446
  // Even if raymarching, we need to collect starting points.
@@ -367,6 +453,251 @@ export class Path {
367
453
  }
368
454
  return result;
369
455
  }
456
+ /**
457
+ * @returns the nearest point on this path to the given `point`.
458
+ */
459
+ nearestPointTo(point) {
460
+ // Find the closest point on this
461
+ let closestSquareDist = Infinity;
462
+ let closestPartIndex = 0;
463
+ let closestParameterValue = 0;
464
+ let closestPoint = this.startPoint;
465
+ for (let i = 0; i < this.geometry.length; i++) {
466
+ const current = this.geometry[i];
467
+ const nearestPoint = current.nearestPointTo(point);
468
+ const sqareDist = nearestPoint.point.squareDistanceTo(point);
469
+ if (i === 0 || sqareDist < closestSquareDist) {
470
+ closestPartIndex = i;
471
+ closestSquareDist = sqareDist;
472
+ closestParameterValue = nearestPoint.parameterValue;
473
+ closestPoint = nearestPoint.point;
474
+ }
475
+ }
476
+ return {
477
+ curve: this.geometry[closestPartIndex],
478
+ curveIndex: closestPartIndex,
479
+ parameterValue: closestParameterValue,
480
+ point: closestPoint,
481
+ };
482
+ }
483
+ at(index) {
484
+ if (index.curveIndex === 0 && index.parameterValue === 0) {
485
+ return this.startPoint;
486
+ }
487
+ return this.geometry[index.curveIndex].at(index.parameterValue);
488
+ }
489
+ tangentAt(index) {
490
+ return this.geometry[index.curveIndex].tangentAt(index.parameterValue);
491
+ }
492
+ /** Splits this path in two near the given `point`. */
493
+ splitNear(point, options) {
494
+ const nearest = this.nearestPointTo(point);
495
+ return this.splitAt(nearest, options);
496
+ }
497
+ /**
498
+ * Returns a copy of this path with `deleteFrom` until `deleteUntil` replaced with `insert`.
499
+ *
500
+ * This method is analogous to {@link Array.toSpliced}.
501
+ */
502
+ spliced(deleteFrom, deleteTo, insert, options) {
503
+ const isBeforeOrEqual = (a, b) => {
504
+ return a.curveIndex < b.curveIndex || (a.curveIndex === b.curveIndex && a.parameterValue <= b.parameterValue);
505
+ };
506
+ if (isBeforeOrEqual(deleteFrom, deleteTo)) {
507
+ // deleteFrom deleteTo
508
+ // <---------| |-------------->
509
+ // x x
510
+ // startPoint endPoint
511
+ const firstSplit = this.splitAt(deleteFrom, options);
512
+ const secondSplit = this.splitAt(deleteTo, options);
513
+ const before = firstSplit[0];
514
+ const after = secondSplit[secondSplit.length - 1];
515
+ return insert ? before.union(insert).union(after) : before.union(after);
516
+ }
517
+ else {
518
+ // In this case, we need to handle wrapping at the start/end.
519
+ // deleteTo deleteFrom
520
+ // <---------| keep |-------------->
521
+ // x x
522
+ // startPoint endPoint
523
+ const splitAtFrom = this.splitAt([deleteFrom], options);
524
+ const beforeFrom = splitAtFrom[0];
525
+ // We need splitNear, rather than splitAt, because beforeFrom does not have
526
+ // the same indexing as this.
527
+ const splitAtTo = beforeFrom.splitNear(this.at(deleteTo), options);
528
+ const betweenBoth = splitAtTo[splitAtTo.length - 1];
529
+ return insert ? betweenBoth.union(insert) : betweenBoth;
530
+ }
531
+ }
532
+ // @internal
533
+ splitAt(splitAt, options) {
534
+ if (!Array.isArray(splitAt)) {
535
+ splitAt = [splitAt];
536
+ }
537
+ splitAt = [...splitAt];
538
+ splitAt.sort(compareCurveIndices);
539
+ //
540
+ // Bounds checking & reversal.
541
+ //
542
+ while (splitAt.length > 0
543
+ && splitAt[splitAt.length - 1].curveIndex >= this.parts.length - 1
544
+ && splitAt[splitAt.length - 1].parameterValue >= 1) {
545
+ splitAt.pop();
546
+ }
547
+ splitAt.reverse(); // .reverse() <-- We're `.pop`ing from the end
548
+ while (splitAt.length > 0
549
+ && splitAt[splitAt.length - 1].curveIndex <= 0
550
+ && splitAt[splitAt.length - 1].parameterValue <= 0) {
551
+ splitAt.pop();
552
+ }
553
+ if (splitAt.length === 0 || this.parts.length === 0) {
554
+ return [this];
555
+ }
556
+ const expectedSplitCount = splitAt.length + 1;
557
+ const mapNewPoint = options?.mapNewPoint ?? ((p) => p);
558
+ const result = [];
559
+ let currentStartPoint = this.startPoint;
560
+ let currentPath = [];
561
+ //
562
+ // Splitting
563
+ //
564
+ let { curveIndex, parameterValue } = splitAt.pop();
565
+ for (let i = 0; i < this.parts.length; i++) {
566
+ if (i !== curveIndex) {
567
+ currentPath.push(this.parts[i]);
568
+ }
569
+ else {
570
+ let part = this.parts[i];
571
+ let geom = this.geometry[i];
572
+ while (i === curveIndex) {
573
+ let newPathStart;
574
+ const newPath = [];
575
+ switch (part.kind) {
576
+ case PathCommandType.MoveTo:
577
+ currentPath.push({
578
+ kind: part.kind,
579
+ point: part.point,
580
+ });
581
+ newPathStart = part.point;
582
+ break;
583
+ case PathCommandType.LineTo:
584
+ {
585
+ const split = geom.splitAt(parameterValue);
586
+ currentPath.push({
587
+ kind: part.kind,
588
+ point: mapNewPoint(split[0].p2),
589
+ });
590
+ newPathStart = split[0].p2;
591
+ if (split.length > 1) {
592
+ console.assert(split.length === 2);
593
+ newPath.push({
594
+ kind: part.kind,
595
+ // Don't map: For lines, the end point of the split is
596
+ // the same as the end point of the original:
597
+ point: split[1].p2,
598
+ });
599
+ geom = split[1];
600
+ }
601
+ }
602
+ break;
603
+ case PathCommandType.QuadraticBezierTo:
604
+ case PathCommandType.CubicBezierTo:
605
+ {
606
+ const split = geom.splitAt(parameterValue);
607
+ let isFirstPart = split.length === 2;
608
+ for (const segment of split) {
609
+ geom = segment;
610
+ const targetArray = isFirstPart ? currentPath : newPath;
611
+ const controlPoints = segment.getPoints();
612
+ if (part.kind === PathCommandType.CubicBezierTo) {
613
+ targetArray.push({
614
+ kind: part.kind,
615
+ controlPoint1: mapNewPoint(controlPoints[1]),
616
+ controlPoint2: mapNewPoint(controlPoints[2]),
617
+ endPoint: mapNewPoint(controlPoints[3]),
618
+ });
619
+ }
620
+ else {
621
+ targetArray.push({
622
+ kind: part.kind,
623
+ controlPoint: mapNewPoint(controlPoints[1]),
624
+ endPoint: mapNewPoint(controlPoints[2]),
625
+ });
626
+ }
627
+ // We want the start of the new path to match the start of the
628
+ // FIRST Bézier in the NEW path.
629
+ if (!isFirstPart) {
630
+ newPathStart = controlPoints[0];
631
+ }
632
+ isFirstPart = false;
633
+ }
634
+ }
635
+ break;
636
+ default: {
637
+ const exhaustivenessCheck = part;
638
+ return exhaustivenessCheck;
639
+ }
640
+ }
641
+ result.push(new Path(currentStartPoint, [...currentPath]));
642
+ currentStartPoint = mapNewPoint(newPathStart);
643
+ console.assert(!!currentStartPoint, 'should have a start point');
644
+ currentPath = newPath;
645
+ part = newPath[newPath.length - 1] ?? part;
646
+ const nextSplit = splitAt.pop();
647
+ if (!nextSplit) {
648
+ break;
649
+ }
650
+ else {
651
+ curveIndex = nextSplit.curveIndex;
652
+ if (i === curveIndex) {
653
+ const originalPoint = this.at(nextSplit);
654
+ parameterValue = geom.nearestPointTo(originalPoint).parameterValue;
655
+ currentPath = [];
656
+ }
657
+ else {
658
+ parameterValue = nextSplit.parameterValue;
659
+ }
660
+ }
661
+ }
662
+ }
663
+ }
664
+ result.push(new Path(currentStartPoint, currentPath));
665
+ console.assert(result.length === expectedSplitCount, `should split into splitAt.length + 1 splits (was ${result.length}, expected ${expectedSplitCount})`);
666
+ return result;
667
+ }
668
+ /**
669
+ * Replaces all `MoveTo` commands with `LineTo` commands and connects the end point of this
670
+ * path to the start point.
671
+ */
672
+ asClosed() {
673
+ const newParts = [];
674
+ let hasChanges = false;
675
+ for (const part of this.parts) {
676
+ if (part.kind === PathCommandType.MoveTo) {
677
+ newParts.push({
678
+ kind: PathCommandType.LineTo,
679
+ point: part.point,
680
+ });
681
+ hasChanges = true;
682
+ }
683
+ else {
684
+ newParts.push(part);
685
+ }
686
+ }
687
+ if (!this.getEndPoint().eq(this.startPoint)) {
688
+ newParts.push({
689
+ kind: PathCommandType.LineTo,
690
+ point: this.startPoint,
691
+ });
692
+ hasChanges = true;
693
+ }
694
+ if (!hasChanges) {
695
+ return this;
696
+ }
697
+ const result = new Path(this.startPoint, newParts);
698
+ console.assert(result.getEndPoint().eq(result.startPoint));
699
+ return result;
700
+ }
370
701
  static mapPathCommand(part, mapping) {
371
702
  switch (part.kind) {
372
703
  case PathCommandType.MoveTo:
@@ -409,20 +740,104 @@ export class Path {
409
740
  }
410
741
  return this.mapPoints(point => affineTransfm.transformVec2(point));
411
742
  }
743
+ /**
744
+ * @internal
745
+ */
746
+ closedContainsPoint(point) {
747
+ const bbox = this.getExactBBox();
748
+ if (!bbox.containsPoint(point)) {
749
+ return false;
750
+ }
751
+ const pointOutside = point.plus(Vec2.of(bbox.width, 0));
752
+ const asClosed = this.asClosed();
753
+ const lineToOutside = new LineSegment2(point, pointOutside);
754
+ return asClosed.intersection(lineToOutside).length % 2 === 1;
755
+ }
412
756
  // Creates a new path by joining [other] to the end of this path
413
- union(other) {
757
+ union(other,
758
+ // allowReverse: true iff reversing other or this is permitted if it means
759
+ // no moveTo command is necessary when unioning the paths.
760
+ options = { allowReverse: true }) {
414
761
  if (!other) {
415
762
  return this;
416
763
  }
417
- return new Path(this.startPoint, [
418
- ...this.parts,
419
- {
420
- kind: PathCommandType.MoveTo,
421
- point: other.startPoint,
422
- },
423
- ...other.parts,
424
- ]);
764
+ if (Array.isArray(other)) {
765
+ return new Path(this.startPoint, [...this.parts, ...other]);
766
+ }
767
+ const thisEnd = this.getEndPoint();
768
+ let newParts = [];
769
+ if (thisEnd.eq(other.startPoint)) {
770
+ newParts = this.parts.concat(other.parts);
771
+ }
772
+ else if (options.allowReverse && this.startPoint.eq(other.getEndPoint())) {
773
+ return other.union(this, { allowReverse: false });
774
+ }
775
+ else if (options.allowReverse && this.startPoint.eq(other.startPoint)) {
776
+ return this.union(other.reversed(), { allowReverse: false });
777
+ }
778
+ else {
779
+ newParts = [
780
+ ...this.parts,
781
+ {
782
+ kind: PathCommandType.MoveTo,
783
+ point: other.startPoint,
784
+ },
785
+ ...other.parts,
786
+ ];
787
+ }
788
+ return new Path(this.startPoint, newParts);
425
789
  }
790
+ /**
791
+ * @returns a version of this path with the direction reversed.
792
+ *
793
+ * Example:
794
+ * ```ts,runnable,console
795
+ * import {Path} from '@js-draw/math';
796
+ * console.log(Path.fromString('m0,0l1,1').reversed()); // -> M1,1 L0,0
797
+ * ```
798
+ */
799
+ reversed() {
800
+ const newStart = this.getEndPoint();
801
+ const newParts = [];
802
+ let lastPoint = this.startPoint;
803
+ for (const part of this.parts) {
804
+ switch (part.kind) {
805
+ case PathCommandType.LineTo:
806
+ case PathCommandType.MoveTo:
807
+ newParts.push({
808
+ kind: part.kind,
809
+ point: lastPoint,
810
+ });
811
+ lastPoint = part.point;
812
+ break;
813
+ case PathCommandType.CubicBezierTo:
814
+ newParts.push({
815
+ kind: part.kind,
816
+ controlPoint1: part.controlPoint2,
817
+ controlPoint2: part.controlPoint1,
818
+ endPoint: lastPoint,
819
+ });
820
+ lastPoint = part.endPoint;
821
+ break;
822
+ case PathCommandType.QuadraticBezierTo:
823
+ newParts.push({
824
+ kind: part.kind,
825
+ controlPoint: part.controlPoint,
826
+ endPoint: lastPoint,
827
+ });
828
+ lastPoint = part.endPoint;
829
+ break;
830
+ default:
831
+ {
832
+ const exhaustivenessCheck = part;
833
+ return exhaustivenessCheck;
834
+ }
835
+ }
836
+ }
837
+ newParts.reverse();
838
+ return new Path(newStart, newParts);
839
+ }
840
+ /** Computes and returns the end point of this path */
426
841
  getEndPoint() {
427
842
  if (this.parts.length === 0) {
428
843
  return this.startPoint;
@@ -472,10 +887,12 @@ export class Path {
472
887
  }
473
888
  return false;
474
889
  }
475
- // Treats this as a closed path and returns true if part of `rect` is *roughly* within
476
- // this path's interior.
477
- //
478
- // Note: Assumes that this is a closed, non-self-intersecting path.
890
+ /**
891
+ * Treats this as a closed path and returns true if part of `rect` is *roughly* within
892
+ * this path's interior.
893
+ *
894
+ * **Note**: Assumes that this is a closed, non-self-intersecting path.
895
+ */
479
896
  closedRoughlyIntersects(rect) {
480
897
  if (rect.containsRect(this.bbox)) {
481
898
  return true;
@@ -513,6 +930,52 @@ export class Path {
513
930
  // Even? Probably no intersection.
514
931
  return false;
515
932
  }
933
+ /** @returns true if all points on this are equivalent to the points on `other` */
934
+ eq(other, tolerance) {
935
+ if (other.parts.length !== this.parts.length) {
936
+ return false;
937
+ }
938
+ for (let i = 0; i < this.parts.length; i++) {
939
+ const part1 = this.parts[i];
940
+ const part2 = other.parts[i];
941
+ switch (part1.kind) {
942
+ case PathCommandType.LineTo:
943
+ case PathCommandType.MoveTo:
944
+ if (part1.kind !== part2.kind) {
945
+ return false;
946
+ }
947
+ else if (!part1.point.eq(part2.point, tolerance)) {
948
+ return false;
949
+ }
950
+ break;
951
+ case PathCommandType.CubicBezierTo:
952
+ if (part1.kind !== part2.kind) {
953
+ return false;
954
+ }
955
+ else if (!part1.controlPoint1.eq(part2.controlPoint1, tolerance)
956
+ || !part1.controlPoint2.eq(part2.controlPoint2, tolerance)
957
+ || !part1.endPoint.eq(part2.endPoint, tolerance)) {
958
+ return false;
959
+ }
960
+ break;
961
+ case PathCommandType.QuadraticBezierTo:
962
+ if (part1.kind !== part2.kind) {
963
+ return false;
964
+ }
965
+ else if (!part1.controlPoint.eq(part2.controlPoint, tolerance)
966
+ || !part1.endPoint.eq(part2.endPoint, tolerance)) {
967
+ return false;
968
+ }
969
+ break;
970
+ default:
971
+ {
972
+ const exhaustivenessCheck = part1;
973
+ return exhaustivenessCheck;
974
+ }
975
+ }
976
+ }
977
+ return true;
978
+ }
516
979
  /**
517
980
  * Returns a path that outlines `rect`.
518
981
  *
@@ -655,10 +1118,8 @@ export class Path {
655
1118
  /**
656
1119
  * Create a `Path` from a subset of the SVG path specification.
657
1120
  *
658
- * ## To-do
659
- * - TODO: Support a larger subset of SVG paths
660
- * - Elliptical arcs are currently unsupported.
661
- * - TODO: Support `s`,`t` commands shorthands.
1121
+ * Currently, this does not support elliptical arcs or `s` and `t` command
1122
+ * shorthands. See https://github.com/personalizedrefrigerator/js-draw/pull/19.
662
1123
  *
663
1124
  * @example
664
1125
  * ```ts,runnable,console
@@ -669,6 +1130,8 @@ export class Path {
669
1130
  * ```
670
1131
  */
671
1132
  static fromString(pathString) {
1133
+ // TODO: Support elliptical arcs, and the `s`, `t` command shorthands.
1134
+ //
672
1135
  // See the MDN reference:
673
1136
  // https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/d
674
1137
  // and
@@ -836,6 +1299,22 @@ export class Path {
836
1299
  result.cachedStringVersion = pathString;
837
1300
  return result;
838
1301
  }
1302
+ static fromConvexHullOf(points) {
1303
+ if (points.length === 0) {
1304
+ return Path.empty;
1305
+ }
1306
+ const hull = convexHull2Of(points);
1307
+ const commands = hull.slice(1).map((p) => ({
1308
+ kind: PathCommandType.LineTo,
1309
+ point: p,
1310
+ }));
1311
+ // Close -- connect back to the start
1312
+ commands.push({
1313
+ kind: PathCommandType.LineTo,
1314
+ point: hull[0],
1315
+ });
1316
+ return new Path(hull[0], commands);
1317
+ }
839
1318
  }
840
1319
  // @internal TODO: At present, this isn't really an empty path.
841
1320
  Path.empty = new Path(Vec2.zero, []);
@@ -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;