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