@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
@@ -6,6 +6,7 @@ import QuadraticBezier from './QuadraticBezier.mjs';
6
6
  import PointShape2D from './PointShape2D.mjs';
7
7
  import toRoundedString from '../rounding/toRoundedString.mjs';
8
8
  import toStringOfSamePrecision from '../rounding/toStringOfSamePrecision.mjs';
9
+ import convexHull2Of from '../utils/convexHull2Of.mjs';
9
10
  export var PathCommandType;
10
11
  (function (PathCommandType) {
11
12
  PathCommandType[PathCommandType["LineTo"] = 0] = "LineTo";
@@ -13,8 +14,62 @@ export var PathCommandType;
13
14
  PathCommandType[PathCommandType["CubicBezierTo"] = 2] = "CubicBezierTo";
14
15
  PathCommandType[PathCommandType["QuadraticBezierTo"] = 3] = "QuadraticBezierTo";
15
16
  })(PathCommandType || (PathCommandType = {}));
17
+ /** Returns a positive number if `a` comes after `b`, 0 if equal, and negative otherwise. */
18
+ export const compareCurveIndices = (a, b) => {
19
+ const indexCompare = a.curveIndex - b.curveIndex;
20
+ if (indexCompare === 0) {
21
+ return a.parameterValue - b.parameterValue;
22
+ }
23
+ else {
24
+ return indexCompare;
25
+ }
26
+ };
27
+ /**
28
+ * Returns a version of `index` with its parameter value incremented by `stepBy`
29
+ * (which can be either positive or negative).
30
+ */
31
+ export const stepCurveIndexBy = (index, stepBy) => {
32
+ if (index.parameterValue + stepBy > 1) {
33
+ return { curveIndex: index.curveIndex + 1, parameterValue: index.parameterValue + stepBy - 1 };
34
+ }
35
+ if (index.parameterValue + stepBy < 0) {
36
+ if (index.curveIndex === 0) {
37
+ return { curveIndex: 0, parameterValue: 0 };
38
+ }
39
+ return { curveIndex: index.curveIndex - 1, parameterValue: index.parameterValue + stepBy + 1 };
40
+ }
41
+ return { curveIndex: index.curveIndex, parameterValue: index.parameterValue + stepBy };
42
+ };
16
43
  /**
17
44
  * Represents a union of lines and curves.
45
+ *
46
+ * To create a path from a string, see {@link fromString}.
47
+ *
48
+ * @example
49
+ * ```ts,runnable,console
50
+ * import {Path, Mat33, Vec2, LineSegment2} from '@js-draw/math';
51
+ *
52
+ * // Creates a path from an SVG path string.
53
+ * // In this case,
54
+ * // 1. Move to (0,0)
55
+ * // 2. Line to (100,0)
56
+ * const path = Path.fromString('M0,0 L100,0');
57
+ *
58
+ * // Logs the distance from (10,0) to the curve 1 unit
59
+ * // away from path. This curve forms a stroke with the path at
60
+ * // its center.
61
+ * const strokeRadius = 1;
62
+ * console.log(path.signedDistance(Vec2.of(10,0), strokeRadius));
63
+ *
64
+ * // Log a version of the path that's scaled by a factor of 4.
65
+ * console.log(path.transformedBy(Mat33.scaling2D(4)).toString());
66
+ *
67
+ * // Log all intersections of a stroked version of the path with
68
+ * // a vertical line segment.
69
+ * // (Try removing the `strokeRadius` parameter).
70
+ * const segment = new LineSegment2(Vec2.of(5, -100), Vec2.of(5, 100));
71
+ * console.log(path.intersection(segment, strokeRadius).map(i => i.point));
72
+ * ```
18
73
  */
19
74
  export class Path {
20
75
  /**
@@ -37,6 +92,12 @@ export class Path {
37
92
  this.bbox = this.bbox.union(Path.computeBBoxForSegment(startPoint, part));
38
93
  }
39
94
  }
95
+ /**
96
+ * Computes and returns the full bounding box for this path.
97
+ *
98
+ * If a slight over-estimate of a path's bounding box is sufficient, use
99
+ * {@link bbox} instead.
100
+ */
40
101
  getExactBBox() {
41
102
  const bboxes = [];
42
103
  for (const part of this.geometry) {
@@ -155,7 +216,20 @@ export class Path {
155
216
  }
156
217
  return Rect2.bboxOf(points);
157
218
  }
158
- /** **Note**: `strokeRadius = strokeWidth / 2` */
219
+ /**
220
+ * Returns the signed distance between `point` and a curve `strokeRadius` units
221
+ * away from this path.
222
+ *
223
+ * This returns the **signed distance**, which means that points inside this shape
224
+ * have their distance negated. For example,
225
+ * ```ts,runnable,console
226
+ * import {Path, Vec2} from '@js-draw/math';
227
+ * console.log(Path.fromString('m0,0 L100,0').signedDistance(Vec2.zero, 1));
228
+ * ```
229
+ * would print `-1` because (0,0) is on `m0,0 L100,0` and thus one unit away from its boundary.
230
+ *
231
+ * **Note**: `strokeRadius = strokeWidth / 2`
232
+ */
159
233
  signedDistance(point, strokeRadius) {
160
234
  let minDist = Infinity;
161
235
  for (const part of this.geometry) {
@@ -242,7 +316,7 @@ export class Path {
242
316
  return [minDistPart, minDist - strokeRadius];
243
317
  };
244
318
  // Raymarch:
245
- const maxRaymarchSteps = 7;
319
+ const maxRaymarchSteps = 8;
246
320
  // Start raymarching from each of these points. This allows detection of multiple
247
321
  // intersections.
248
322
  const startPoints = [
@@ -312,7 +386,7 @@ export class Path {
312
386
  if (lastPart && isOnLineSegment && Math.abs(lastDist) < stoppingThreshold) {
313
387
  result.push({
314
388
  point: currentPoint,
315
- parameterValue: NaN, // lastPart.nearestPointTo(currentPoint).parameterValue,
389
+ parameterValue: lastPart.nearestPointTo(currentPoint).parameterValue,
316
390
  curve: lastPart,
317
391
  curveIndex: this.geometry.indexOf(lastPart),
318
392
  });
@@ -352,6 +426,9 @@ export class Path {
352
426
  if (!line.bbox.intersects(this.bbox.grownBy(strokeRadius ?? 0))) {
353
427
  return [];
354
428
  }
429
+ if (this.parts.length === 0) {
430
+ return new Path(this.startPoint, [{ kind: PathCommandType.MoveTo, point: this.startPoint }]).intersection(line, strokeRadius);
431
+ }
355
432
  let index = 0;
356
433
  for (const part of this.geometry) {
357
434
  const intersections = part.argIntersectsLineSegment(line);
@@ -378,9 +455,6 @@ export class Path {
378
455
  }
379
456
  /**
380
457
  * @returns the nearest point on this path to the given `point`.
381
- *
382
- * @internal
383
- * @beta
384
458
  */
385
459
  nearestPointTo(point) {
386
460
  // Find the closest point on this
@@ -407,11 +481,223 @@ export class Path {
407
481
  };
408
482
  }
409
483
  at(index) {
484
+ if (index.curveIndex === 0 && index.parameterValue === 0) {
485
+ return this.startPoint;
486
+ }
410
487
  return this.geometry[index.curveIndex].at(index.parameterValue);
411
488
  }
412
489
  tangentAt(index) {
413
490
  return this.geometry[index.curveIndex].tangentAt(index.parameterValue);
414
491
  }
492
+ /** Splits this path in two near the given `point`. */
493
+ splitNear(point, options) {
494
+ const nearest = this.nearestPointTo(point);
495
+ return this.splitAt(nearest, options);
496
+ }
497
+ /**
498
+ * Returns a copy of this path with `deleteFrom` until `deleteUntil` replaced with `insert`.
499
+ *
500
+ * This method is analogous to {@link Array.toSpliced}.
501
+ */
502
+ spliced(deleteFrom, deleteTo, insert, options) {
503
+ const isBeforeOrEqual = (a, b) => {
504
+ return a.curveIndex < b.curveIndex || (a.curveIndex === b.curveIndex && a.parameterValue <= b.parameterValue);
505
+ };
506
+ if (isBeforeOrEqual(deleteFrom, deleteTo)) {
507
+ // deleteFrom deleteTo
508
+ // <---------| |-------------->
509
+ // x x
510
+ // startPoint endPoint
511
+ const firstSplit = this.splitAt(deleteFrom, options);
512
+ const secondSplit = this.splitAt(deleteTo, options);
513
+ const before = firstSplit[0];
514
+ const after = secondSplit[secondSplit.length - 1];
515
+ return insert ? before.union(insert).union(after) : before.union(after);
516
+ }
517
+ else {
518
+ // In this case, we need to handle wrapping at the start/end.
519
+ // deleteTo deleteFrom
520
+ // <---------| keep |-------------->
521
+ // x x
522
+ // startPoint endPoint
523
+ const splitAtFrom = this.splitAt([deleteFrom], options);
524
+ const beforeFrom = splitAtFrom[0];
525
+ // We need splitNear, rather than splitAt, because beforeFrom does not have
526
+ // the same indexing as this.
527
+ const splitAtTo = beforeFrom.splitNear(this.at(deleteTo), options);
528
+ const betweenBoth = splitAtTo[splitAtTo.length - 1];
529
+ return insert ? betweenBoth.union(insert) : betweenBoth;
530
+ }
531
+ }
532
+ // @internal
533
+ splitAt(splitAt, options) {
534
+ if (!Array.isArray(splitAt)) {
535
+ splitAt = [splitAt];
536
+ }
537
+ splitAt = [...splitAt];
538
+ splitAt.sort(compareCurveIndices);
539
+ //
540
+ // Bounds checking & reversal.
541
+ //
542
+ while (splitAt.length > 0
543
+ && splitAt[splitAt.length - 1].curveIndex >= this.parts.length - 1
544
+ && splitAt[splitAt.length - 1].parameterValue >= 1) {
545
+ splitAt.pop();
546
+ }
547
+ splitAt.reverse(); // .reverse() <-- We're `.pop`ing from the end
548
+ while (splitAt.length > 0
549
+ && splitAt[splitAt.length - 1].curveIndex <= 0
550
+ && splitAt[splitAt.length - 1].parameterValue <= 0) {
551
+ splitAt.pop();
552
+ }
553
+ if (splitAt.length === 0 || this.parts.length === 0) {
554
+ return [this];
555
+ }
556
+ const expectedSplitCount = splitAt.length + 1;
557
+ const mapNewPoint = options?.mapNewPoint ?? ((p) => p);
558
+ const result = [];
559
+ let currentStartPoint = this.startPoint;
560
+ let currentPath = [];
561
+ //
562
+ // Splitting
563
+ //
564
+ let { curveIndex, parameterValue } = splitAt.pop();
565
+ for (let i = 0; i < this.parts.length; i++) {
566
+ if (i !== curveIndex) {
567
+ currentPath.push(this.parts[i]);
568
+ }
569
+ else {
570
+ let part = this.parts[i];
571
+ let geom = this.geometry[i];
572
+ while (i === curveIndex) {
573
+ let newPathStart;
574
+ const newPath = [];
575
+ switch (part.kind) {
576
+ case PathCommandType.MoveTo:
577
+ currentPath.push({
578
+ kind: part.kind,
579
+ point: part.point,
580
+ });
581
+ newPathStart = part.point;
582
+ break;
583
+ case PathCommandType.LineTo:
584
+ {
585
+ const split = geom.splitAt(parameterValue);
586
+ currentPath.push({
587
+ kind: part.kind,
588
+ point: mapNewPoint(split[0].p2),
589
+ });
590
+ newPathStart = split[0].p2;
591
+ if (split.length > 1) {
592
+ console.assert(split.length === 2);
593
+ newPath.push({
594
+ kind: part.kind,
595
+ // Don't map: For lines, the end point of the split is
596
+ // the same as the end point of the original:
597
+ point: split[1].p2,
598
+ });
599
+ geom = split[1];
600
+ }
601
+ }
602
+ break;
603
+ case PathCommandType.QuadraticBezierTo:
604
+ case PathCommandType.CubicBezierTo:
605
+ {
606
+ const split = geom.splitAt(parameterValue);
607
+ let isFirstPart = split.length === 2;
608
+ for (const segment of split) {
609
+ geom = segment;
610
+ const targetArray = isFirstPart ? currentPath : newPath;
611
+ const controlPoints = segment.getPoints();
612
+ if (part.kind === PathCommandType.CubicBezierTo) {
613
+ targetArray.push({
614
+ kind: part.kind,
615
+ controlPoint1: mapNewPoint(controlPoints[1]),
616
+ controlPoint2: mapNewPoint(controlPoints[2]),
617
+ endPoint: mapNewPoint(controlPoints[3]),
618
+ });
619
+ }
620
+ else {
621
+ targetArray.push({
622
+ kind: part.kind,
623
+ controlPoint: mapNewPoint(controlPoints[1]),
624
+ endPoint: mapNewPoint(controlPoints[2]),
625
+ });
626
+ }
627
+ // We want the start of the new path to match the start of the
628
+ // FIRST Bézier in the NEW path.
629
+ if (!isFirstPart) {
630
+ newPathStart = controlPoints[0];
631
+ }
632
+ isFirstPart = false;
633
+ }
634
+ }
635
+ break;
636
+ default: {
637
+ const exhaustivenessCheck = part;
638
+ return exhaustivenessCheck;
639
+ }
640
+ }
641
+ result.push(new Path(currentStartPoint, [...currentPath]));
642
+ currentStartPoint = mapNewPoint(newPathStart);
643
+ console.assert(!!currentStartPoint, 'should have a start point');
644
+ currentPath = newPath;
645
+ part = newPath[newPath.length - 1] ?? part;
646
+ const nextSplit = splitAt.pop();
647
+ if (!nextSplit) {
648
+ break;
649
+ }
650
+ else {
651
+ curveIndex = nextSplit.curveIndex;
652
+ if (i === curveIndex) {
653
+ const originalPoint = this.at(nextSplit);
654
+ parameterValue = geom.nearestPointTo(originalPoint).parameterValue;
655
+ currentPath = [];
656
+ }
657
+ else {
658
+ parameterValue = nextSplit.parameterValue;
659
+ }
660
+ }
661
+ }
662
+ }
663
+ }
664
+ result.push(new Path(currentStartPoint, currentPath));
665
+ console.assert(result.length === expectedSplitCount, `should split into splitAt.length + 1 splits (was ${result.length}, expected ${expectedSplitCount})`);
666
+ return result;
667
+ }
668
+ /**
669
+ * Replaces all `MoveTo` commands with `LineTo` commands and connects the end point of this
670
+ * path to the start point.
671
+ */
672
+ asClosed() {
673
+ const newParts = [];
674
+ let hasChanges = false;
675
+ for (const part of this.parts) {
676
+ if (part.kind === PathCommandType.MoveTo) {
677
+ newParts.push({
678
+ kind: PathCommandType.LineTo,
679
+ point: part.point,
680
+ });
681
+ hasChanges = true;
682
+ }
683
+ else {
684
+ newParts.push(part);
685
+ }
686
+ }
687
+ if (!this.getEndPoint().eq(this.startPoint)) {
688
+ newParts.push({
689
+ kind: PathCommandType.LineTo,
690
+ point: this.startPoint,
691
+ });
692
+ hasChanges = true;
693
+ }
694
+ if (!hasChanges) {
695
+ return this;
696
+ }
697
+ const result = new Path(this.startPoint, newParts);
698
+ console.assert(result.getEndPoint().eq(result.startPoint));
699
+ return result;
700
+ }
415
701
  static mapPathCommand(part, mapping) {
416
702
  switch (part.kind) {
417
703
  case PathCommandType.MoveTo:
@@ -454,6 +740,19 @@ export class Path {
454
740
  }
455
741
  return this.mapPoints(point => affineTransfm.transformVec2(point));
456
742
  }
743
+ /**
744
+ * @internal
745
+ */
746
+ closedContainsPoint(point) {
747
+ const bbox = this.getExactBBox();
748
+ if (!bbox.containsPoint(point)) {
749
+ return false;
750
+ }
751
+ const pointOutside = point.plus(Vec2.of(bbox.width, 0));
752
+ const asClosed = this.asClosed();
753
+ const lineToOutside = new LineSegment2(point, pointOutside);
754
+ return asClosed.intersection(lineToOutside).length % 2 === 1;
755
+ }
457
756
  // Creates a new path by joining [other] to the end of this path
458
757
  union(other,
459
758
  // allowReverse: true iff reversing other or this is permitted if it means
@@ -462,6 +761,9 @@ export class Path {
462
761
  if (!other) {
463
762
  return this;
464
763
  }
764
+ if (Array.isArray(other)) {
765
+ return new Path(this.startPoint, [...this.parts, ...other]);
766
+ }
465
767
  const thisEnd = this.getEndPoint();
466
768
  let newParts = [];
467
769
  if (thisEnd.eq(other.startPoint)) {
@@ -535,6 +837,7 @@ export class Path {
535
837
  newParts.reverse();
536
838
  return new Path(newStart, newParts);
537
839
  }
840
+ /** Computes and returns the end point of this path */
538
841
  getEndPoint() {
539
842
  if (this.parts.length === 0) {
540
843
  return this.startPoint;
@@ -584,10 +887,12 @@ export class Path {
584
887
  }
585
888
  return false;
586
889
  }
587
- // Treats this as a closed path and returns true if part of `rect` is *roughly* within
588
- // this path's interior.
589
- //
590
- // Note: Assumes that this is a closed, non-self-intersecting path.
890
+ /**
891
+ * Treats this as a closed path and returns true if part of `rect` is *roughly* within
892
+ * this path's interior.
893
+ *
894
+ * **Note**: Assumes that this is a closed, non-self-intersecting path.
895
+ */
591
896
  closedRoughlyIntersects(rect) {
592
897
  if (rect.containsRect(this.bbox)) {
593
898
  return true;
@@ -813,10 +1118,8 @@ export class Path {
813
1118
  /**
814
1119
  * Create a `Path` from a subset of the SVG path specification.
815
1120
  *
816
- * ## To-do
817
- * - TODO: Support a larger subset of SVG paths
818
- * - Elliptical arcs are currently unsupported.
819
- * - TODO: Support `s`,`t` commands shorthands.
1121
+ * Currently, this does not support elliptical arcs or `s` and `t` command
1122
+ * shorthands. See https://github.com/personalizedrefrigerator/js-draw/pull/19.
820
1123
  *
821
1124
  * @example
822
1125
  * ```ts,runnable,console
@@ -827,6 +1130,8 @@ export class Path {
827
1130
  * ```
828
1131
  */
829
1132
  static fromString(pathString) {
1133
+ // TODO: Support elliptical arcs, and the `s`, `t` command shorthands.
1134
+ //
830
1135
  // See the MDN reference:
831
1136
  // https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/d
832
1137
  // and
@@ -994,6 +1299,22 @@ export class Path {
994
1299
  result.cachedStringVersion = pathString;
995
1300
  return result;
996
1301
  }
1302
+ static fromConvexHullOf(points) {
1303
+ if (points.length === 0) {
1304
+ return Path.empty;
1305
+ }
1306
+ const hull = convexHull2Of(points);
1307
+ const commands = hull.slice(1).map((p) => ({
1308
+ kind: PathCommandType.LineTo,
1309
+ point: p,
1310
+ }));
1311
+ // Close -- connect back to the start
1312
+ commands.push({
1313
+ kind: PathCommandType.LineTo,
1314
+ point: hull[0],
1315
+ });
1316
+ return new Path(hull[0], commands);
1317
+ }
997
1318
  }
998
1319
  // @internal TODO: At present, this isn't really an empty path.
999
1320
  Path.empty = new Path(Vec2.zero, []);
@@ -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;
@@ -3,10 +3,9 @@ import solveQuadratic from '../polynomial/solveQuadratic.mjs';
3
3
  import BezierJSWrapper from './BezierJSWrapper.mjs';
4
4
  import Rect2 from './Rect2.mjs';
5
5
  /**
6
- * A wrapper around `bezier-js`'s quadratic Bézier.
6
+ * Represents a 2D Bézier curve.
7
7
  *
8
- * This wrappper lazy-loads `bezier-js`'s Bézier and can perform some operations
9
- * without loading it at all (e.g. `normal`, `at`, and `approximateDistance`).
8
+ * **Note**: Many Bézier operations use `bezier-js`'s.
10
9
  */
11
10
  export class QuadraticBezier extends BezierJSWrapper {
12
11
  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;
@@ -1,7 +1,11 @@
1
1
  import LineSegment2 from './LineSegment2.mjs';
2
2
  import { Vec2 } from '../Vec2.mjs';
3
3
  import Abstract2DShape from './Abstract2DShape.mjs';
4
- // invariant: w ≥ 0, h ≥ 0, immutable
4
+ /**
5
+ * Represents a rectangle in 2D space, parallel to the XY axes.
6
+ *
7
+ * `invariant: w ≥ 0, h ≥ 0, immutable`
8
+ */
5
9
  export class Rect2 extends Abstract2DShape {
6
10
  constructor(x, y, w, h) {
7
11
  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,59 @@
1
+ import { Vec2 } from '../Vec2.mjs';
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
+ const convexHull2Of = (points) => {
9
+ if (points.length === 0) {
10
+ return [];
11
+ }
12
+ // 1. Start with a vertex on the hull
13
+ const lowestPoint = points.reduce((lowest, current) => current.y < lowest.y ? current : lowest, points[0]);
14
+ const vertices = [lowestPoint];
15
+ let toProcess = [...points.filter(p => !p.eq(lowestPoint))];
16
+ let lastBaseDirection = Vec2.of(-1, 0);
17
+ // 2. Find the point with greatest angle from the vertex:
18
+ //
19
+ // . . .
20
+ // . . / <- Notice that **all** other points are to the
21
+ // / **left** of the vector from the current
22
+ // ./ vertex to the new point.
23
+ while (toProcess.length > 0) {
24
+ const lastVertex = vertices[vertices.length - 1];
25
+ let smallestDotProductSoFar = lastBaseDirection.dot(lowestPoint.minus(lastVertex).normalizedOrZero());
26
+ let furthestPointSoFar = lowestPoint;
27
+ for (const point of toProcess) {
28
+ // Maximizing the angle is the same as minimizing the dot product:
29
+ // point.minus(lastVertex)
30
+ // ^
31
+ // /
32
+ // /
33
+ // ϑ /
34
+ // <-----. lastBaseDirection
35
+ const currentDotProduct = lastBaseDirection.dot(point.minus(lastVertex).normalizedOrZero());
36
+ if (currentDotProduct <= smallestDotProductSoFar) {
37
+ furthestPointSoFar = point;
38
+ smallestDotProductSoFar = currentDotProduct;
39
+ }
40
+ }
41
+ toProcess = toProcess.filter(p => !p.eq(furthestPointSoFar));
42
+ const newBaseDirection = furthestPointSoFar.minus(lastVertex).normalized();
43
+ // If the last vertex is on the same edge as the current, there's no need to include
44
+ // the previous one.
45
+ if (Math.abs(newBaseDirection.dot(lastBaseDirection)) === 1 && vertices.length > 1) {
46
+ vertices.pop();
47
+ }
48
+ // Stoping condition: We've gone in a full circle.
49
+ if (furthestPointSoFar.eq(lowestPoint)) {
50
+ break;
51
+ }
52
+ else {
53
+ vertices.push(furthestPointSoFar);
54
+ lastBaseDirection = lastVertex.minus(furthestPointSoFar).normalized();
55
+ }
56
+ }
57
+ return vertices;
58
+ };
59
+ export default convexHull2Of;
@@ -0,0 +1 @@
1
+ export {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@js-draw/math",
3
- "version": "1.17.0",
3
+ "version": "1.18.0",
4
4
  "description": "A math library for js-draw. ",
5
5
  "types": "./dist/mjs/lib.d.ts",
6
6
  "main": "./dist/cjs/lib.js",
@@ -45,5 +45,5 @@
45
45
  "svg",
46
46
  "math"
47
47
  ],
48
- "gitHead": "d0eff585750ab5670af3acda8ddff090e8825bd3"
48
+ "gitHead": "73c0d802a8439b5d408ba1e60f91be029db7e402"
49
49
  }
package/src/Mat33.ts CHANGED
@@ -444,8 +444,13 @@ export class Mat33 {
444
444
  return Mat33.identity;
445
445
  }
446
446
 
447
- const parseArguments = (argumentString: string) => {
448
- return argumentString.split(/[, \t\n]+/g).map(argString => {
447
+ const parseArguments = (argumentString: string): number[] => {
448
+ const parsed = argumentString.split(/[, \t\n]+/g).map(argString => {
449
+ // Handle trailing spaces/commands
450
+ if (argString.trim() === '') {
451
+ return null;
452
+ }
453
+
449
454
  let isPercentage = false;
450
455
  if (argString.endsWith('%')) {
451
456
  isPercentage = true;
@@ -476,6 +481,7 @@ export class Mat33 {
476
481
 
477
482
  return argNumber;
478
483
  });
484
+ return parsed.filter(n => n !== null) as number[];
479
485
  };
480
486
 
481
487
 
package/src/Vec3.test.ts CHANGED
@@ -57,6 +57,7 @@ describe('Vec3', () => {
57
57
  { from: Vec3.of(1, 1, 1), to: Vec3.of(0, 1, 0), expected: 2 },
58
58
  { from: Vec3.of(1, 1, 1), to: Vec3.of(0, 0, 0), expected: 3 },
59
59
  { from: Vec3.of(-1, -10, 0), to: Vec3.of(1, 2, 0), expected: 148 },
60
+ { from: Vec3.of(-1, -10, 0), to: Vec3.of(1, 2, 0), expected: 148 },
60
61
  ])(
61
62
  '.squareDistanceTo and .distanceTo should return correct square and euclidean distances (%j)',
62
63
  ({ from , to, expected }) => {
@@ -67,4 +68,19 @@ describe('Vec3', () => {
67
68
  expect(from.minus(to).magnitudeSquared()).toBe(expected);
68
69
  },
69
70
  );
71
+
72
+ test.each([
73
+ { a: Vec3.of(1, 2, 3), b: Vec3.of(4, 5, 6), tolerance: 0.1, eq: false },
74
+ { a: Vec3.of(1, 2, 3), b: Vec3.of(4, 5, 6), tolerance: 10, eq: true },
75
+ { a: Vec3.of(1, 2, 3), b: Vec3.of(1, 2, 3), tolerance: 0, eq: true },
76
+ { a: Vec3.of(1, 2, 3), b: Vec3.of(1, 2, 4), tolerance: 0, eq: false },
77
+ { a: Vec3.of(1, 2, 3), b: Vec3.of(1, 4, 3), tolerance: 0, eq: false },
78
+ { a: Vec3.of(1, 2, 3), b: Vec3.of(4, 2, 3), tolerance: 0, eq: false },
79
+ { a: Vec3.of(1, 2, 3.0001), b: Vec3.of(1, 2, 3), tolerance: 1e-12, eq: false },
80
+ { a: Vec3.of(1, 2, 3.0001), b: Vec3.of(1, 2, 3), tolerance: 1e-3, eq: true },
81
+ { a: Vec3.of(1, 2.00001, 3.0001), b: Vec3.of(1.00001, 2, 3), tolerance: 1e-3, eq: true },
82
+ ])('.eq should support tolerance (case %#)', ({ a, b, tolerance, eq }) => {
83
+ expect(a.eq(b, tolerance)).toBe(eq);
84
+ expect(b.eq(a, tolerance)).toBe(eq);
85
+ });
70
86
  });