@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.
- package/dist/cjs/Mat33.js +6 -1
- package/dist/cjs/Vec3.d.ts +2 -1
- package/dist/cjs/Vec3.js +5 -7
- package/dist/cjs/lib.d.ts +2 -1
- package/dist/cjs/lib.js +5 -1
- package/dist/cjs/shapes/BezierJSWrapper.d.ts +4 -0
- package/dist/cjs/shapes/BezierJSWrapper.js +35 -0
- package/dist/cjs/shapes/LineSegment2.d.ts +11 -0
- package/dist/cjs/shapes/LineSegment2.js +26 -1
- package/dist/cjs/shapes/Parameterized2DShape.d.ts +6 -1
- package/dist/cjs/shapes/Parameterized2DShape.js +6 -1
- package/dist/cjs/shapes/Path.d.ts +96 -12
- package/dist/cjs/shapes/Path.js +338 -15
- package/dist/cjs/shapes/QuadraticBezier.d.ts +2 -3
- package/dist/cjs/shapes/QuadraticBezier.js +2 -3
- package/dist/cjs/shapes/Rect2.d.ts +6 -1
- package/dist/cjs/shapes/Rect2.js +5 -1
- package/dist/cjs/utils/convexHull2Of.d.ts +9 -0
- package/dist/cjs/utils/convexHull2Of.js +61 -0
- package/dist/cjs/utils/convexHull2Of.test.d.ts +1 -0
- package/dist/mjs/Mat33.mjs +6 -1
- package/dist/mjs/Vec3.d.ts +2 -1
- package/dist/mjs/Vec3.mjs +5 -7
- package/dist/mjs/lib.d.ts +2 -1
- package/dist/mjs/lib.mjs +2 -1
- package/dist/mjs/shapes/BezierJSWrapper.d.ts +4 -0
- package/dist/mjs/shapes/BezierJSWrapper.mjs +35 -0
- package/dist/mjs/shapes/LineSegment2.d.ts +11 -0
- package/dist/mjs/shapes/LineSegment2.mjs +26 -1
- package/dist/mjs/shapes/Parameterized2DShape.d.ts +6 -1
- package/dist/mjs/shapes/Parameterized2DShape.mjs +6 -1
- package/dist/mjs/shapes/Path.d.ts +96 -12
- package/dist/mjs/shapes/Path.mjs +335 -14
- package/dist/mjs/shapes/QuadraticBezier.d.ts +2 -3
- package/dist/mjs/shapes/QuadraticBezier.mjs +2 -3
- package/dist/mjs/shapes/Rect2.d.ts +6 -1
- package/dist/mjs/shapes/Rect2.mjs +5 -1
- package/dist/mjs/utils/convexHull2Of.d.ts +9 -0
- package/dist/mjs/utils/convexHull2Of.mjs +59 -0
- package/dist/mjs/utils/convexHull2Of.test.d.ts +1 -0
- package/package.json +2 -2
- package/src/Mat33.ts +8 -2
- package/src/Vec3.test.ts +16 -0
- package/src/Vec3.ts +7 -8
- package/src/lib.ts +3 -0
- package/src/shapes/BezierJSWrapper.ts +41 -0
- package/src/shapes/LineSegment2.test.ts +26 -0
- package/src/shapes/LineSegment2.ts +31 -1
- package/src/shapes/Parameterized2DShape.ts +6 -1
- package/src/shapes/Path.test.ts +173 -5
- package/src/shapes/Path.ts +390 -18
- package/src/shapes/QuadraticBezier.test.ts +21 -0
- package/src/shapes/QuadraticBezier.ts +2 -3
- package/src/shapes/Rect2.ts +6 -2
- package/src/utils/convexHull2Of.test.ts +43 -0
- package/src/utils/convexHull2Of.ts +71 -0
package/dist/cjs/shapes/Path.js
CHANGED
@@ -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
|
-
/**
|
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 =
|
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:
|
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
|
-
|
594
|
-
|
595
|
-
|
596
|
-
|
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
|
-
*
|
823
|
-
*
|
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
|
-
*
|
5
|
+
* Represents a 2D Bézier curve.
|
6
6
|
*
|
7
|
-
*
|
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
|
-
*
|
12
|
+
* Represents a 2D Bézier curve.
|
13
13
|
*
|
14
|
-
*
|
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;
|
package/dist/cjs/shapes/Rect2.js
CHANGED
@@ -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
|
-
|
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 {};
|
package/dist/mjs/Mat33.mjs
CHANGED
@@ -334,7 +334,11 @@ export class Mat33 {
|
|
334
334
|
return Mat33.identity;
|
335
335
|
}
|
336
336
|
const parseArguments = (argumentString) => {
|
337
|
-
|
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) => {
|
package/dist/mjs/Vec3.d.ts
CHANGED
@@ -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
|
-
|
231
|
-
|
232
|
-
|
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})`;
|