@js-draw/math 1.16.0 → 1.18.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 (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;