@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
@@ -3,7 +3,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.Path = exports.PathCommandType = void 0;
6
+ exports.Path = exports.stepCurveIndexBy = exports.compareCurveIndices = exports.PathCommandType = void 0;
7
7
  const LineSegment2_1 = __importDefault(require("./LineSegment2"));
8
8
  const Rect2_1 = __importDefault(require("./Rect2"));
9
9
  const Vec2_1 = require("../Vec2");
@@ -12,6 +12,7 @@ const QuadraticBezier_1 = __importDefault(require("./QuadraticBezier"));
12
12
  const PointShape2D_1 = __importDefault(require("./PointShape2D"));
13
13
  const toRoundedString_1 = __importDefault(require("../rounding/toRoundedString"));
14
14
  const toStringOfSamePrecision_1 = __importDefault(require("../rounding/toStringOfSamePrecision"));
15
+ const convexHull2Of_1 = __importDefault(require("../utils/convexHull2Of"));
15
16
  var PathCommandType;
16
17
  (function (PathCommandType) {
17
18
  PathCommandType[PathCommandType["LineTo"] = 0] = "LineTo";
@@ -19,8 +20,64 @@ var PathCommandType;
19
20
  PathCommandType[PathCommandType["CubicBezierTo"] = 2] = "CubicBezierTo";
20
21
  PathCommandType[PathCommandType["QuadraticBezierTo"] = 3] = "QuadraticBezierTo";
21
22
  })(PathCommandType || (exports.PathCommandType = PathCommandType = {}));
23
+ /** Returns a positive number if `a` comes after `b`, 0 if equal, and negative otherwise. */
24
+ const compareCurveIndices = (a, b) => {
25
+ const indexCompare = a.curveIndex - b.curveIndex;
26
+ if (indexCompare === 0) {
27
+ return a.parameterValue - b.parameterValue;
28
+ }
29
+ else {
30
+ return indexCompare;
31
+ }
32
+ };
33
+ exports.compareCurveIndices = compareCurveIndices;
34
+ /**
35
+ * Returns a version of `index` with its parameter value incremented by `stepBy`
36
+ * (which can be either positive or negative).
37
+ */
38
+ const stepCurveIndexBy = (index, stepBy) => {
39
+ if (index.parameterValue + stepBy > 1) {
40
+ return { curveIndex: index.curveIndex + 1, parameterValue: index.parameterValue + stepBy - 1 };
41
+ }
42
+ if (index.parameterValue + stepBy < 0) {
43
+ if (index.curveIndex === 0) {
44
+ return { curveIndex: 0, parameterValue: 0 };
45
+ }
46
+ return { curveIndex: index.curveIndex - 1, parameterValue: index.parameterValue + stepBy + 1 };
47
+ }
48
+ return { curveIndex: index.curveIndex, parameterValue: index.parameterValue + stepBy };
49
+ };
50
+ exports.stepCurveIndexBy = stepCurveIndexBy;
22
51
  /**
23
52
  * Represents a union of lines and curves.
53
+ *
54
+ * To create a path from a string, see {@link fromString}.
55
+ *
56
+ * @example
57
+ * ```ts,runnable,console
58
+ * import {Path, Mat33, Vec2, LineSegment2} from '@js-draw/math';
59
+ *
60
+ * // Creates a path from an SVG path string.
61
+ * // In this case,
62
+ * // 1. Move to (0,0)
63
+ * // 2. Line to (100,0)
64
+ * const path = Path.fromString('M0,0 L100,0');
65
+ *
66
+ * // Logs the distance from (10,0) to the curve 1 unit
67
+ * // away from path. This curve forms a stroke with the path at
68
+ * // its center.
69
+ * const strokeRadius = 1;
70
+ * console.log(path.signedDistance(Vec2.of(10,0), strokeRadius));
71
+ *
72
+ * // Log a version of the path that's scaled by a factor of 4.
73
+ * console.log(path.transformedBy(Mat33.scaling2D(4)).toString());
74
+ *
75
+ * // Log all intersections of a stroked version of the path with
76
+ * // a vertical line segment.
77
+ * // (Try removing the `strokeRadius` parameter).
78
+ * const segment = new LineSegment2(Vec2.of(5, -100), Vec2.of(5, 100));
79
+ * console.log(path.intersection(segment, strokeRadius).map(i => i.point));
80
+ * ```
24
81
  */
25
82
  class Path {
26
83
  /**
@@ -43,6 +100,12 @@ class Path {
43
100
  this.bbox = this.bbox.union(Path.computeBBoxForSegment(startPoint, part));
44
101
  }
45
102
  }
103
+ /**
104
+ * Computes and returns the full bounding box for this path.
105
+ *
106
+ * If a slight over-estimate of a path's bounding box is sufficient, use
107
+ * {@link bbox} instead.
108
+ */
46
109
  getExactBBox() {
47
110
  const bboxes = [];
48
111
  for (const part of this.geometry) {
@@ -161,7 +224,20 @@ class Path {
161
224
  }
162
225
  return Rect2_1.default.bboxOf(points);
163
226
  }
164
- /** **Note**: `strokeRadius = strokeWidth / 2` */
227
+ /**
228
+ * Returns the signed distance between `point` and a curve `strokeRadius` units
229
+ * away from this path.
230
+ *
231
+ * This returns the **signed distance**, which means that points inside this shape
232
+ * have their distance negated. For example,
233
+ * ```ts,runnable,console
234
+ * import {Path, Vec2} from '@js-draw/math';
235
+ * console.log(Path.fromString('m0,0 L100,0').signedDistance(Vec2.zero, 1));
236
+ * ```
237
+ * would print `-1` because (0,0) is on `m0,0 L100,0` and thus one unit away from its boundary.
238
+ *
239
+ * **Note**: `strokeRadius = strokeWidth / 2`
240
+ */
165
241
  signedDistance(point, strokeRadius) {
166
242
  let minDist = Infinity;
167
243
  for (const part of this.geometry) {
@@ -236,7 +312,7 @@ class Path {
236
312
  for (const { part, distFn, bbox } of uncheckedDistFunctions) {
237
313
  // Skip if impossible for the distance to the target to be lesser than
238
314
  // the current minimum.
239
- if (!bbox.grownBy(minDist).containsPoint(point)) {
315
+ if (isFinite(minDist) && !bbox.grownBy(minDist).containsPoint(point)) {
240
316
  continue;
241
317
  }
242
318
  const currentDist = distFn(point);
@@ -248,7 +324,7 @@ class Path {
248
324
  return [minDistPart, minDist - strokeRadius];
249
325
  };
250
326
  // Raymarch:
251
- const maxRaymarchSteps = 7;
327
+ const maxRaymarchSteps = 8;
252
328
  // Start raymarching from each of these points. This allows detection of multiple
253
329
  // intersections.
254
330
  const startPoints = [
@@ -274,7 +350,7 @@ class Path {
274
350
  });
275
351
  const result = [];
276
352
  const stoppingThreshold = strokeRadius / 1000;
277
- // Returns the maximum x value explored
353
+ // Returns the maximum parameter value explored
278
354
  const raymarchFrom = (startPoint,
279
355
  // Direction to march in (multiplies line.direction)
280
356
  directionMultiplier,
@@ -318,9 +394,14 @@ class Path {
318
394
  if (lastPart && isOnLineSegment && Math.abs(lastDist) < stoppingThreshold) {
319
395
  result.push({
320
396
  point: currentPoint,
321
- parameterValue: NaN,
397
+ parameterValue: lastPart.nearestPointTo(currentPoint).parameterValue,
322
398
  curve: lastPart,
399
+ curveIndex: this.geometry.indexOf(lastPart),
323
400
  });
401
+ // Slightly increase the parameter value to prevent the same point from being
402
+ // added to the results twice.
403
+ const parameterIncrease = strokeRadius / 20 / line.length;
404
+ lastParameter += isFinite(parameterIncrease) ? parameterIncrease : 0;
324
405
  }
325
406
  return lastParameter;
326
407
  };
@@ -353,14 +434,21 @@ class Path {
353
434
  if (!line.bbox.intersects(this.bbox.grownBy(strokeRadius ?? 0))) {
354
435
  return [];
355
436
  }
437
+ if (this.parts.length === 0) {
438
+ return new Path(this.startPoint, [{ kind: PathCommandType.MoveTo, point: this.startPoint }]).intersection(line, strokeRadius);
439
+ }
440
+ let index = 0;
356
441
  for (const part of this.geometry) {
357
- const intersection = part.intersectsLineSegment(line);
358
- if (intersection.length > 0) {
442
+ const intersections = part.argIntersectsLineSegment(line);
443
+ for (const intersection of intersections) {
359
444
  result.push({
360
445
  curve: part,
361
- point: intersection[0],
446
+ curveIndex: index,
447
+ point: part.at(intersection),
448
+ parameterValue: intersection,
362
449
  });
363
450
  }
451
+ index++;
364
452
  }
365
453
  // If given a non-zero strokeWidth, attempt to raymarch.
366
454
  // Even if raymarching, we need to collect starting points.
@@ -373,6 +461,251 @@ class Path {
373
461
  }
374
462
  return result;
375
463
  }
464
+ /**
465
+ * @returns the nearest point on this path to the given `point`.
466
+ */
467
+ nearestPointTo(point) {
468
+ // Find the closest point on this
469
+ let closestSquareDist = Infinity;
470
+ let closestPartIndex = 0;
471
+ let closestParameterValue = 0;
472
+ let closestPoint = this.startPoint;
473
+ for (let i = 0; i < this.geometry.length; i++) {
474
+ const current = this.geometry[i];
475
+ const nearestPoint = current.nearestPointTo(point);
476
+ const sqareDist = nearestPoint.point.squareDistanceTo(point);
477
+ if (i === 0 || sqareDist < closestSquareDist) {
478
+ closestPartIndex = i;
479
+ closestSquareDist = sqareDist;
480
+ closestParameterValue = nearestPoint.parameterValue;
481
+ closestPoint = nearestPoint.point;
482
+ }
483
+ }
484
+ return {
485
+ curve: this.geometry[closestPartIndex],
486
+ curveIndex: closestPartIndex,
487
+ parameterValue: closestParameterValue,
488
+ point: closestPoint,
489
+ };
490
+ }
491
+ at(index) {
492
+ if (index.curveIndex === 0 && index.parameterValue === 0) {
493
+ return this.startPoint;
494
+ }
495
+ return this.geometry[index.curveIndex].at(index.parameterValue);
496
+ }
497
+ tangentAt(index) {
498
+ return this.geometry[index.curveIndex].tangentAt(index.parameterValue);
499
+ }
500
+ /** Splits this path in two near the given `point`. */
501
+ splitNear(point, options) {
502
+ const nearest = this.nearestPointTo(point);
503
+ return this.splitAt(nearest, options);
504
+ }
505
+ /**
506
+ * Returns a copy of this path with `deleteFrom` until `deleteUntil` replaced with `insert`.
507
+ *
508
+ * This method is analogous to {@link Array.toSpliced}.
509
+ */
510
+ spliced(deleteFrom, deleteTo, insert, options) {
511
+ const isBeforeOrEqual = (a, b) => {
512
+ return a.curveIndex < b.curveIndex || (a.curveIndex === b.curveIndex && a.parameterValue <= b.parameterValue);
513
+ };
514
+ if (isBeforeOrEqual(deleteFrom, deleteTo)) {
515
+ // deleteFrom deleteTo
516
+ // <---------| |-------------->
517
+ // x x
518
+ // startPoint endPoint
519
+ const firstSplit = this.splitAt(deleteFrom, options);
520
+ const secondSplit = this.splitAt(deleteTo, options);
521
+ const before = firstSplit[0];
522
+ const after = secondSplit[secondSplit.length - 1];
523
+ return insert ? before.union(insert).union(after) : before.union(after);
524
+ }
525
+ else {
526
+ // In this case, we need to handle wrapping at the start/end.
527
+ // deleteTo deleteFrom
528
+ // <---------| keep |-------------->
529
+ // x x
530
+ // startPoint endPoint
531
+ const splitAtFrom = this.splitAt([deleteFrom], options);
532
+ const beforeFrom = splitAtFrom[0];
533
+ // We need splitNear, rather than splitAt, because beforeFrom does not have
534
+ // the same indexing as this.
535
+ const splitAtTo = beforeFrom.splitNear(this.at(deleteTo), options);
536
+ const betweenBoth = splitAtTo[splitAtTo.length - 1];
537
+ return insert ? betweenBoth.union(insert) : betweenBoth;
538
+ }
539
+ }
540
+ // @internal
541
+ splitAt(splitAt, options) {
542
+ if (!Array.isArray(splitAt)) {
543
+ splitAt = [splitAt];
544
+ }
545
+ splitAt = [...splitAt];
546
+ splitAt.sort(exports.compareCurveIndices);
547
+ //
548
+ // Bounds checking & reversal.
549
+ //
550
+ while (splitAt.length > 0
551
+ && splitAt[splitAt.length - 1].curveIndex >= this.parts.length - 1
552
+ && splitAt[splitAt.length - 1].parameterValue >= 1) {
553
+ splitAt.pop();
554
+ }
555
+ splitAt.reverse(); // .reverse() <-- We're `.pop`ing from the end
556
+ while (splitAt.length > 0
557
+ && splitAt[splitAt.length - 1].curveIndex <= 0
558
+ && splitAt[splitAt.length - 1].parameterValue <= 0) {
559
+ splitAt.pop();
560
+ }
561
+ if (splitAt.length === 0 || this.parts.length === 0) {
562
+ return [this];
563
+ }
564
+ const expectedSplitCount = splitAt.length + 1;
565
+ const mapNewPoint = options?.mapNewPoint ?? ((p) => p);
566
+ const result = [];
567
+ let currentStartPoint = this.startPoint;
568
+ let currentPath = [];
569
+ //
570
+ // Splitting
571
+ //
572
+ let { curveIndex, parameterValue } = splitAt.pop();
573
+ for (let i = 0; i < this.parts.length; i++) {
574
+ if (i !== curveIndex) {
575
+ currentPath.push(this.parts[i]);
576
+ }
577
+ else {
578
+ let part = this.parts[i];
579
+ let geom = this.geometry[i];
580
+ while (i === curveIndex) {
581
+ let newPathStart;
582
+ const newPath = [];
583
+ switch (part.kind) {
584
+ case PathCommandType.MoveTo:
585
+ currentPath.push({
586
+ kind: part.kind,
587
+ point: part.point,
588
+ });
589
+ newPathStart = part.point;
590
+ break;
591
+ case PathCommandType.LineTo:
592
+ {
593
+ const split = geom.splitAt(parameterValue);
594
+ currentPath.push({
595
+ kind: part.kind,
596
+ point: mapNewPoint(split[0].p2),
597
+ });
598
+ newPathStart = split[0].p2;
599
+ if (split.length > 1) {
600
+ console.assert(split.length === 2);
601
+ newPath.push({
602
+ kind: part.kind,
603
+ // Don't map: For lines, the end point of the split is
604
+ // the same as the end point of the original:
605
+ point: split[1].p2,
606
+ });
607
+ geom = split[1];
608
+ }
609
+ }
610
+ break;
611
+ case PathCommandType.QuadraticBezierTo:
612
+ case PathCommandType.CubicBezierTo:
613
+ {
614
+ const split = geom.splitAt(parameterValue);
615
+ let isFirstPart = split.length === 2;
616
+ for (const segment of split) {
617
+ geom = segment;
618
+ const targetArray = isFirstPart ? currentPath : newPath;
619
+ const controlPoints = segment.getPoints();
620
+ if (part.kind === PathCommandType.CubicBezierTo) {
621
+ targetArray.push({
622
+ kind: part.kind,
623
+ controlPoint1: mapNewPoint(controlPoints[1]),
624
+ controlPoint2: mapNewPoint(controlPoints[2]),
625
+ endPoint: mapNewPoint(controlPoints[3]),
626
+ });
627
+ }
628
+ else {
629
+ targetArray.push({
630
+ kind: part.kind,
631
+ controlPoint: mapNewPoint(controlPoints[1]),
632
+ endPoint: mapNewPoint(controlPoints[2]),
633
+ });
634
+ }
635
+ // We want the start of the new path to match the start of the
636
+ // FIRST Bézier in the NEW path.
637
+ if (!isFirstPart) {
638
+ newPathStart = controlPoints[0];
639
+ }
640
+ isFirstPart = false;
641
+ }
642
+ }
643
+ break;
644
+ default: {
645
+ const exhaustivenessCheck = part;
646
+ return exhaustivenessCheck;
647
+ }
648
+ }
649
+ result.push(new Path(currentStartPoint, [...currentPath]));
650
+ currentStartPoint = mapNewPoint(newPathStart);
651
+ console.assert(!!currentStartPoint, 'should have a start point');
652
+ currentPath = newPath;
653
+ part = newPath[newPath.length - 1] ?? part;
654
+ const nextSplit = splitAt.pop();
655
+ if (!nextSplit) {
656
+ break;
657
+ }
658
+ else {
659
+ curveIndex = nextSplit.curveIndex;
660
+ if (i === curveIndex) {
661
+ const originalPoint = this.at(nextSplit);
662
+ parameterValue = geom.nearestPointTo(originalPoint).parameterValue;
663
+ currentPath = [];
664
+ }
665
+ else {
666
+ parameterValue = nextSplit.parameterValue;
667
+ }
668
+ }
669
+ }
670
+ }
671
+ }
672
+ result.push(new Path(currentStartPoint, currentPath));
673
+ console.assert(result.length === expectedSplitCount, `should split into splitAt.length + 1 splits (was ${result.length}, expected ${expectedSplitCount})`);
674
+ return result;
675
+ }
676
+ /**
677
+ * Replaces all `MoveTo` commands with `LineTo` commands and connects the end point of this
678
+ * path to the start point.
679
+ */
680
+ asClosed() {
681
+ const newParts = [];
682
+ let hasChanges = false;
683
+ for (const part of this.parts) {
684
+ if (part.kind === PathCommandType.MoveTo) {
685
+ newParts.push({
686
+ kind: PathCommandType.LineTo,
687
+ point: part.point,
688
+ });
689
+ hasChanges = true;
690
+ }
691
+ else {
692
+ newParts.push(part);
693
+ }
694
+ }
695
+ if (!this.getEndPoint().eq(this.startPoint)) {
696
+ newParts.push({
697
+ kind: PathCommandType.LineTo,
698
+ point: this.startPoint,
699
+ });
700
+ hasChanges = true;
701
+ }
702
+ if (!hasChanges) {
703
+ return this;
704
+ }
705
+ const result = new Path(this.startPoint, newParts);
706
+ console.assert(result.getEndPoint().eq(result.startPoint));
707
+ return result;
708
+ }
376
709
  static mapPathCommand(part, mapping) {
377
710
  switch (part.kind) {
378
711
  case PathCommandType.MoveTo:
@@ -415,20 +748,104 @@ class Path {
415
748
  }
416
749
  return this.mapPoints(point => affineTransfm.transformVec2(point));
417
750
  }
751
+ /**
752
+ * @internal
753
+ */
754
+ closedContainsPoint(point) {
755
+ const bbox = this.getExactBBox();
756
+ if (!bbox.containsPoint(point)) {
757
+ return false;
758
+ }
759
+ const pointOutside = point.plus(Vec2_1.Vec2.of(bbox.width, 0));
760
+ const asClosed = this.asClosed();
761
+ const lineToOutside = new LineSegment2_1.default(point, pointOutside);
762
+ return asClosed.intersection(lineToOutside).length % 2 === 1;
763
+ }
418
764
  // Creates a new path by joining [other] to the end of this path
419
- union(other) {
765
+ union(other,
766
+ // allowReverse: true iff reversing other or this is permitted if it means
767
+ // no moveTo command is necessary when unioning the paths.
768
+ options = { allowReverse: true }) {
420
769
  if (!other) {
421
770
  return this;
422
771
  }
423
- return new Path(this.startPoint, [
424
- ...this.parts,
425
- {
426
- kind: PathCommandType.MoveTo,
427
- point: other.startPoint,
428
- },
429
- ...other.parts,
430
- ]);
772
+ if (Array.isArray(other)) {
773
+ return new Path(this.startPoint, [...this.parts, ...other]);
774
+ }
775
+ const thisEnd = this.getEndPoint();
776
+ let newParts = [];
777
+ if (thisEnd.eq(other.startPoint)) {
778
+ newParts = this.parts.concat(other.parts);
779
+ }
780
+ else if (options.allowReverse && this.startPoint.eq(other.getEndPoint())) {
781
+ return other.union(this, { allowReverse: false });
782
+ }
783
+ else if (options.allowReverse && this.startPoint.eq(other.startPoint)) {
784
+ return this.union(other.reversed(), { allowReverse: false });
785
+ }
786
+ else {
787
+ newParts = [
788
+ ...this.parts,
789
+ {
790
+ kind: PathCommandType.MoveTo,
791
+ point: other.startPoint,
792
+ },
793
+ ...other.parts,
794
+ ];
795
+ }
796
+ return new Path(this.startPoint, newParts);
797
+ }
798
+ /**
799
+ * @returns a version of this path with the direction reversed.
800
+ *
801
+ * Example:
802
+ * ```ts,runnable,console
803
+ * import {Path} from '@js-draw/math';
804
+ * console.log(Path.fromString('m0,0l1,1').reversed()); // -> M1,1 L0,0
805
+ * ```
806
+ */
807
+ reversed() {
808
+ const newStart = this.getEndPoint();
809
+ const newParts = [];
810
+ let lastPoint = this.startPoint;
811
+ for (const part of this.parts) {
812
+ switch (part.kind) {
813
+ case PathCommandType.LineTo:
814
+ case PathCommandType.MoveTo:
815
+ newParts.push({
816
+ kind: part.kind,
817
+ point: lastPoint,
818
+ });
819
+ lastPoint = part.point;
820
+ break;
821
+ case PathCommandType.CubicBezierTo:
822
+ newParts.push({
823
+ kind: part.kind,
824
+ controlPoint1: part.controlPoint2,
825
+ controlPoint2: part.controlPoint1,
826
+ endPoint: lastPoint,
827
+ });
828
+ lastPoint = part.endPoint;
829
+ break;
830
+ case PathCommandType.QuadraticBezierTo:
831
+ newParts.push({
832
+ kind: part.kind,
833
+ controlPoint: part.controlPoint,
834
+ endPoint: lastPoint,
835
+ });
836
+ lastPoint = part.endPoint;
837
+ break;
838
+ default:
839
+ {
840
+ const exhaustivenessCheck = part;
841
+ return exhaustivenessCheck;
842
+ }
843
+ }
844
+ }
845
+ newParts.reverse();
846
+ return new Path(newStart, newParts);
431
847
  }
848
+ /** Computes and returns the end point of this path */
432
849
  getEndPoint() {
433
850
  if (this.parts.length === 0) {
434
851
  return this.startPoint;
@@ -478,10 +895,12 @@ class Path {
478
895
  }
479
896
  return false;
480
897
  }
481
- // Treats this as a closed path and returns true if part of `rect` is *roughly* within
482
- // this path's interior.
483
- //
484
- // Note: Assumes that this is a closed, non-self-intersecting path.
898
+ /**
899
+ * Treats this as a closed path and returns true if part of `rect` is *roughly* within
900
+ * this path's interior.
901
+ *
902
+ * **Note**: Assumes that this is a closed, non-self-intersecting path.
903
+ */
485
904
  closedRoughlyIntersects(rect) {
486
905
  if (rect.containsRect(this.bbox)) {
487
906
  return true;
@@ -519,6 +938,52 @@ class Path {
519
938
  // Even? Probably no intersection.
520
939
  return false;
521
940
  }
941
+ /** @returns true if all points on this are equivalent to the points on `other` */
942
+ eq(other, tolerance) {
943
+ if (other.parts.length !== this.parts.length) {
944
+ return false;
945
+ }
946
+ for (let i = 0; i < this.parts.length; i++) {
947
+ const part1 = this.parts[i];
948
+ const part2 = other.parts[i];
949
+ switch (part1.kind) {
950
+ case PathCommandType.LineTo:
951
+ case PathCommandType.MoveTo:
952
+ if (part1.kind !== part2.kind) {
953
+ return false;
954
+ }
955
+ else if (!part1.point.eq(part2.point, tolerance)) {
956
+ return false;
957
+ }
958
+ break;
959
+ case PathCommandType.CubicBezierTo:
960
+ if (part1.kind !== part2.kind) {
961
+ return false;
962
+ }
963
+ else if (!part1.controlPoint1.eq(part2.controlPoint1, tolerance)
964
+ || !part1.controlPoint2.eq(part2.controlPoint2, tolerance)
965
+ || !part1.endPoint.eq(part2.endPoint, tolerance)) {
966
+ return false;
967
+ }
968
+ break;
969
+ case PathCommandType.QuadraticBezierTo:
970
+ if (part1.kind !== part2.kind) {
971
+ return false;
972
+ }
973
+ else if (!part1.controlPoint.eq(part2.controlPoint, tolerance)
974
+ || !part1.endPoint.eq(part2.endPoint, tolerance)) {
975
+ return false;
976
+ }
977
+ break;
978
+ default:
979
+ {
980
+ const exhaustivenessCheck = part1;
981
+ return exhaustivenessCheck;
982
+ }
983
+ }
984
+ }
985
+ return true;
986
+ }
522
987
  /**
523
988
  * Returns a path that outlines `rect`.
524
989
  *
@@ -661,10 +1126,8 @@ class Path {
661
1126
  /**
662
1127
  * Create a `Path` from a subset of the SVG path specification.
663
1128
  *
664
- * ## To-do
665
- * - TODO: Support a larger subset of SVG paths
666
- * - Elliptical arcs are currently unsupported.
667
- * - TODO: Support `s`,`t` commands shorthands.
1129
+ * Currently, this does not support elliptical arcs or `s` and `t` command
1130
+ * shorthands. See https://github.com/personalizedrefrigerator/js-draw/pull/19.
668
1131
  *
669
1132
  * @example
670
1133
  * ```ts,runnable,console
@@ -675,6 +1138,8 @@ class Path {
675
1138
  * ```
676
1139
  */
677
1140
  static fromString(pathString) {
1141
+ // TODO: Support elliptical arcs, and the `s`, `t` command shorthands.
1142
+ //
678
1143
  // See the MDN reference:
679
1144
  // https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/d
680
1145
  // and
@@ -842,6 +1307,22 @@ class Path {
842
1307
  result.cachedStringVersion = pathString;
843
1308
  return result;
844
1309
  }
1310
+ static fromConvexHullOf(points) {
1311
+ if (points.length === 0) {
1312
+ return Path.empty;
1313
+ }
1314
+ const hull = (0, convexHull2Of_1.default)(points);
1315
+ const commands = hull.slice(1).map((p) => ({
1316
+ kind: PathCommandType.LineTo,
1317
+ point: p,
1318
+ }));
1319
+ // Close -- connect back to the start
1320
+ commands.push({
1321
+ kind: PathCommandType.LineTo,
1322
+ point: hull[0],
1323
+ });
1324
+ return new Path(hull[0], commands);
1325
+ }
845
1326
  }
846
1327
  exports.Path = Path;
847
1328
  // @internal TODO: At present, this isn't really an empty path.
@@ -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;