@js-draw/math 1.17.0 → 1.18.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (56) hide show
  1. package/dist/cjs/Mat33.js +6 -1
  2. package/dist/cjs/Vec3.d.ts +2 -1
  3. package/dist/cjs/Vec3.js +5 -7
  4. package/dist/cjs/lib.d.ts +2 -1
  5. package/dist/cjs/lib.js +5 -1
  6. package/dist/cjs/shapes/BezierJSWrapper.d.ts +4 -0
  7. package/dist/cjs/shapes/BezierJSWrapper.js +35 -0
  8. package/dist/cjs/shapes/LineSegment2.d.ts +11 -0
  9. package/dist/cjs/shapes/LineSegment2.js +26 -1
  10. package/dist/cjs/shapes/Parameterized2DShape.d.ts +6 -1
  11. package/dist/cjs/shapes/Parameterized2DShape.js +6 -1
  12. package/dist/cjs/shapes/Path.d.ts +96 -12
  13. package/dist/cjs/shapes/Path.js +338 -15
  14. package/dist/cjs/shapes/QuadraticBezier.d.ts +2 -3
  15. package/dist/cjs/shapes/QuadraticBezier.js +2 -3
  16. package/dist/cjs/shapes/Rect2.d.ts +6 -1
  17. package/dist/cjs/shapes/Rect2.js +5 -1
  18. package/dist/cjs/utils/convexHull2Of.d.ts +9 -0
  19. package/dist/cjs/utils/convexHull2Of.js +61 -0
  20. package/dist/cjs/utils/convexHull2Of.test.d.ts +1 -0
  21. package/dist/mjs/Mat33.mjs +6 -1
  22. package/dist/mjs/Vec3.d.ts +2 -1
  23. package/dist/mjs/Vec3.mjs +5 -7
  24. package/dist/mjs/lib.d.ts +2 -1
  25. package/dist/mjs/lib.mjs +2 -1
  26. package/dist/mjs/shapes/BezierJSWrapper.d.ts +4 -0
  27. package/dist/mjs/shapes/BezierJSWrapper.mjs +35 -0
  28. package/dist/mjs/shapes/LineSegment2.d.ts +11 -0
  29. package/dist/mjs/shapes/LineSegment2.mjs +26 -1
  30. package/dist/mjs/shapes/Parameterized2DShape.d.ts +6 -1
  31. package/dist/mjs/shapes/Parameterized2DShape.mjs +6 -1
  32. package/dist/mjs/shapes/Path.d.ts +96 -12
  33. package/dist/mjs/shapes/Path.mjs +335 -14
  34. package/dist/mjs/shapes/QuadraticBezier.d.ts +2 -3
  35. package/dist/mjs/shapes/QuadraticBezier.mjs +2 -3
  36. package/dist/mjs/shapes/Rect2.d.ts +6 -1
  37. package/dist/mjs/shapes/Rect2.mjs +5 -1
  38. package/dist/mjs/utils/convexHull2Of.d.ts +9 -0
  39. package/dist/mjs/utils/convexHull2Of.mjs +59 -0
  40. package/dist/mjs/utils/convexHull2Of.test.d.ts +1 -0
  41. package/package.json +2 -2
  42. package/src/Mat33.ts +8 -2
  43. package/src/Vec3.test.ts +16 -0
  44. package/src/Vec3.ts +7 -8
  45. package/src/lib.ts +3 -0
  46. package/src/shapes/BezierJSWrapper.ts +41 -0
  47. package/src/shapes/LineSegment2.test.ts +26 -0
  48. package/src/shapes/LineSegment2.ts +31 -1
  49. package/src/shapes/Parameterized2DShape.ts +6 -1
  50. package/src/shapes/Path.test.ts +173 -5
  51. package/src/shapes/Path.ts +390 -18
  52. package/src/shapes/QuadraticBezier.test.ts +21 -0
  53. package/src/shapes/QuadraticBezier.ts +2 -3
  54. package/src/shapes/Rect2.ts +6 -2
  55. package/src/utils/convexHull2Of.test.ts +43 -0
  56. 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) {
@@ -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 = [
@@ -318,7 +394,7 @@ class Path {
318
394
  if (lastPart && isOnLineSegment && Math.abs(lastDist) < stoppingThreshold) {
319
395
  result.push({
320
396
  point: currentPoint,
321
- parameterValue: NaN, // lastPart.nearestPointTo(currentPoint).parameterValue,
397
+ parameterValue: lastPart.nearestPointTo(currentPoint).parameterValue,
322
398
  curve: lastPart,
323
399
  curveIndex: this.geometry.indexOf(lastPart),
324
400
  });
@@ -358,6 +434,9 @@ class Path {
358
434
  if (!line.bbox.intersects(this.bbox.grownBy(strokeRadius ?? 0))) {
359
435
  return [];
360
436
  }
437
+ if (this.parts.length === 0) {
438
+ return new Path(this.startPoint, [{ kind: PathCommandType.MoveTo, point: this.startPoint }]).intersection(line, strokeRadius);
439
+ }
361
440
  let index = 0;
362
441
  for (const part of this.geometry) {
363
442
  const intersections = part.argIntersectsLineSegment(line);
@@ -384,9 +463,6 @@ class Path {
384
463
  }
385
464
  /**
386
465
  * @returns the nearest point on this path to the given `point`.
387
- *
388
- * @internal
389
- * @beta
390
466
  */
391
467
  nearestPointTo(point) {
392
468
  // Find the closest point on this
@@ -413,11 +489,223 @@ class Path {
413
489
  };
414
490
  }
415
491
  at(index) {
492
+ if (index.curveIndex === 0 && index.parameterValue === 0) {
493
+ return this.startPoint;
494
+ }
416
495
  return this.geometry[index.curveIndex].at(index.parameterValue);
417
496
  }
418
497
  tangentAt(index) {
419
498
  return this.geometry[index.curveIndex].tangentAt(index.parameterValue);
420
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
+ }
421
709
  static mapPathCommand(part, mapping) {
422
710
  switch (part.kind) {
423
711
  case PathCommandType.MoveTo:
@@ -460,6 +748,19 @@ class Path {
460
748
  }
461
749
  return this.mapPoints(point => affineTransfm.transformVec2(point));
462
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
+ }
463
764
  // Creates a new path by joining [other] to the end of this path
464
765
  union(other,
465
766
  // allowReverse: true iff reversing other or this is permitted if it means
@@ -468,6 +769,9 @@ class Path {
468
769
  if (!other) {
469
770
  return this;
470
771
  }
772
+ if (Array.isArray(other)) {
773
+ return new Path(this.startPoint, [...this.parts, ...other]);
774
+ }
471
775
  const thisEnd = this.getEndPoint();
472
776
  let newParts = [];
473
777
  if (thisEnd.eq(other.startPoint)) {
@@ -541,6 +845,7 @@ class Path {
541
845
  newParts.reverse();
542
846
  return new Path(newStart, newParts);
543
847
  }
848
+ /** Computes and returns the end point of this path */
544
849
  getEndPoint() {
545
850
  if (this.parts.length === 0) {
546
851
  return this.startPoint;
@@ -590,10 +895,12 @@ class Path {
590
895
  }
591
896
  return false;
592
897
  }
593
- // Treats this as a closed path and returns true if part of `rect` is *roughly* within
594
- // this path's interior.
595
- //
596
- // 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
+ */
597
904
  closedRoughlyIntersects(rect) {
598
905
  if (rect.containsRect(this.bbox)) {
599
906
  return true;
@@ -819,10 +1126,8 @@ class Path {
819
1126
  /**
820
1127
  * Create a `Path` from a subset of the SVG path specification.
821
1128
  *
822
- * ## To-do
823
- * - TODO: Support a larger subset of SVG paths
824
- * - Elliptical arcs are currently unsupported.
825
- * - 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.
826
1131
  *
827
1132
  * @example
828
1133
  * ```ts,runnable,console
@@ -833,6 +1138,8 @@ class Path {
833
1138
  * ```
834
1139
  */
835
1140
  static fromString(pathString) {
1141
+ // TODO: Support elliptical arcs, and the `s`, `t` command shorthands.
1142
+ //
836
1143
  // See the MDN reference:
837
1144
  // https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/d
838
1145
  // and
@@ -1000,6 +1307,22 @@ class Path {
1000
1307
  result.cachedStringVersion = pathString;
1001
1308
  return result;
1002
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
+ }
1003
1326
  }
1004
1327
  exports.Path = Path;
1005
1328
  // @internal TODO: At present, this isn't really an empty path.
@@ -2,10 +2,9 @@ import { Point2, Vec2 } from '../Vec2';
2
2
  import BezierJSWrapper from './BezierJSWrapper';
3
3
  import Rect2 from './Rect2';
4
4
  /**
5
- * A wrapper around `bezier-js`'s quadratic Bézier.
5
+ * Represents a 2D Bézier curve.
6
6
  *
7
- * This wrappper lazy-loads `bezier-js`'s Bézier and can perform some operations
8
- * without loading it at all (e.g. `normal`, `at`, and `approximateDistance`).
7
+ * **Note**: Many Bézier operations use `bezier-js`'s.
9
8
  */
10
9
  export declare class QuadraticBezier extends BezierJSWrapper {
11
10
  readonly p0: Point2;
@@ -9,10 +9,9 @@ const solveQuadratic_1 = __importDefault(require("../polynomial/solveQuadratic")
9
9
  const BezierJSWrapper_1 = __importDefault(require("./BezierJSWrapper"));
10
10
  const Rect2_1 = __importDefault(require("./Rect2"));
11
11
  /**
12
- * A wrapper around `bezier-js`'s quadratic Bézier.
12
+ * Represents a 2D Bézier curve.
13
13
  *
14
- * This wrappper lazy-loads `bezier-js`'s Bézier and can perform some operations
15
- * without loading it at all (e.g. `normal`, `at`, and `approximateDistance`).
14
+ * **Note**: Many Bézier operations use `bezier-js`'s.
16
15
  */
17
16
  class QuadraticBezier extends BezierJSWrapper_1.default {
18
17
  constructor(p0, p1, p2) {
@@ -3,7 +3,7 @@ import Mat33 from '../Mat33';
3
3
  import { Point2, Vec2 } from '../Vec2';
4
4
  import Abstract2DShape from './Abstract2DShape';
5
5
  import Vec3 from '../Vec3';
6
- /** An object that can be converted to a Rect2. */
6
+ /** An object that can be converted to a {@link Rect2}. */
7
7
  export interface RectTemplate {
8
8
  x: number;
9
9
  y: number;
@@ -12,6 +12,11 @@ export interface RectTemplate {
12
12
  width?: number;
13
13
  height?: number;
14
14
  }
15
+ /**
16
+ * Represents a rectangle in 2D space, parallel to the XY axes.
17
+ *
18
+ * `invariant: w ≥ 0, h ≥ 0, immutable`
19
+ */
15
20
  export declare class Rect2 extends Abstract2DShape {
16
21
  readonly x: number;
17
22
  readonly y: number;
@@ -7,7 +7,11 @@ exports.Rect2 = void 0;
7
7
  const LineSegment2_1 = __importDefault(require("./LineSegment2"));
8
8
  const Vec2_1 = require("../Vec2");
9
9
  const Abstract2DShape_1 = __importDefault(require("./Abstract2DShape"));
10
- // invariant: w ≥ 0, h ≥ 0, immutable
10
+ /**
11
+ * Represents a rectangle in 2D space, parallel to the XY axes.
12
+ *
13
+ * `invariant: w ≥ 0, h ≥ 0, immutable`
14
+ */
11
15
  class Rect2 extends Abstract2DShape_1.default {
12
16
  constructor(x, y, w, h) {
13
17
  super();
@@ -0,0 +1,9 @@
1
+ import { Point2 } from '../Vec2';
2
+ /**
3
+ * Implements Gift Wrapping, in $O(nh)$. This algorithm is not the most efficient in the worst case.
4
+ *
5
+ * See https://en.wikipedia.org/wiki/Gift_wrapping_algorithm
6
+ * and https://www.cs.jhu.edu/~misha/Spring16/06.pdf
7
+ */
8
+ declare const convexHull2Of: (points: Point2[]) => import("../Vec3").Vec3[];
9
+ export default convexHull2Of;
@@ -0,0 +1,61 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const Vec2_1 = require("../Vec2");
4
+ /**
5
+ * Implements Gift Wrapping, in $O(nh)$. This algorithm is not the most efficient in the worst case.
6
+ *
7
+ * See https://en.wikipedia.org/wiki/Gift_wrapping_algorithm
8
+ * and https://www.cs.jhu.edu/~misha/Spring16/06.pdf
9
+ */
10
+ const convexHull2Of = (points) => {
11
+ if (points.length === 0) {
12
+ return [];
13
+ }
14
+ // 1. Start with a vertex on the hull
15
+ const lowestPoint = points.reduce((lowest, current) => current.y < lowest.y ? current : lowest, points[0]);
16
+ const vertices = [lowestPoint];
17
+ let toProcess = [...points.filter(p => !p.eq(lowestPoint))];
18
+ let lastBaseDirection = Vec2_1.Vec2.of(-1, 0);
19
+ // 2. Find the point with greatest angle from the vertex:
20
+ //
21
+ // . . .
22
+ // . . / <- Notice that **all** other points are to the
23
+ // / **left** of the vector from the current
24
+ // ./ vertex to the new point.
25
+ while (toProcess.length > 0) {
26
+ const lastVertex = vertices[vertices.length - 1];
27
+ let smallestDotProductSoFar = lastBaseDirection.dot(lowestPoint.minus(lastVertex).normalizedOrZero());
28
+ let furthestPointSoFar = lowestPoint;
29
+ for (const point of toProcess) {
30
+ // Maximizing the angle is the same as minimizing the dot product:
31
+ // point.minus(lastVertex)
32
+ // ^
33
+ // /
34
+ // /
35
+ // ϑ /
36
+ // <-----. lastBaseDirection
37
+ const currentDotProduct = lastBaseDirection.dot(point.minus(lastVertex).normalizedOrZero());
38
+ if (currentDotProduct <= smallestDotProductSoFar) {
39
+ furthestPointSoFar = point;
40
+ smallestDotProductSoFar = currentDotProduct;
41
+ }
42
+ }
43
+ toProcess = toProcess.filter(p => !p.eq(furthestPointSoFar));
44
+ const newBaseDirection = furthestPointSoFar.minus(lastVertex).normalized();
45
+ // If the last vertex is on the same edge as the current, there's no need to include
46
+ // the previous one.
47
+ if (Math.abs(newBaseDirection.dot(lastBaseDirection)) === 1 && vertices.length > 1) {
48
+ vertices.pop();
49
+ }
50
+ // Stoping condition: We've gone in a full circle.
51
+ if (furthestPointSoFar.eq(lowestPoint)) {
52
+ break;
53
+ }
54
+ else {
55
+ vertices.push(furthestPointSoFar);
56
+ lastBaseDirection = lastVertex.minus(furthestPointSoFar).normalized();
57
+ }
58
+ }
59
+ return vertices;
60
+ };
61
+ exports.default = convexHull2Of;
@@ -0,0 +1 @@
1
+ export {};
@@ -334,7 +334,11 @@ export class Mat33 {
334
334
  return Mat33.identity;
335
335
  }
336
336
  const parseArguments = (argumentString) => {
337
- return argumentString.split(/[, \t\n]+/g).map(argString => {
337
+ const parsed = argumentString.split(/[, \t\n]+/g).map(argString => {
338
+ // Handle trailing spaces/commands
339
+ if (argString.trim() === '') {
340
+ return null;
341
+ }
338
342
  let isPercentage = false;
339
343
  if (argString.endsWith('%')) {
340
344
  isPercentage = true;
@@ -355,6 +359,7 @@ export class Mat33 {
355
359
  }
356
360
  return argNumber;
357
361
  });
362
+ return parsed.filter(n => n !== null);
358
363
  };
359
364
  const keywordToAction = {
360
365
  matrix: (matrixData) => {
@@ -134,7 +134,8 @@ export declare class Vec3 {
134
134
  * Returns a vector with each component acted on by `fn`.
135
135
  *
136
136
  * @example
137
- * ```
137
+ * ```ts,runnable,console
138
+ * import { Vec3 } from '@js-draw/math';
138
139
  * console.log(Vec3.of(1, 2, 3).map(val => val + 1)); // → Vec(2, 3, 4)
139
140
  * ```
140
141
  */
package/dist/mjs/Vec3.mjs CHANGED
@@ -203,7 +203,8 @@ export class Vec3 {
203
203
  * Returns a vector with each component acted on by `fn`.
204
204
  *
205
205
  * @example
206
- * ```
206
+ * ```ts,runnable,console
207
+ * import { Vec3 } from '@js-draw/math';
207
208
  * console.log(Vec3.of(1, 2, 3).map(val => val + 1)); // → Vec(2, 3, 4)
208
209
  * ```
209
210
  */
@@ -227,12 +228,9 @@ export class Vec3 {
227
228
  * ```
228
229
  */
229
230
  eq(other, fuzz = 1e-10) {
230
- for (let i = 0; i < 3; i++) {
231
- if (Math.abs(other.at(i) - this.at(i)) > fuzz) {
232
- return false;
233
- }
234
- }
235
- return true;
231
+ return (Math.abs(other.x - this.x) <= fuzz
232
+ && Math.abs(other.y - this.y) <= fuzz
233
+ && Math.abs(other.z - this.z) <= fuzz);
236
234
  }
237
235
  toString() {
238
236
  return `Vec(${this.x}, ${this.y}, ${this.z})`;