@js-draw/math 1.17.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 (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})`;