@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/mjs/shapes/Path.mjs
CHANGED
@@ -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
|
-
/**
|
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 =
|
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:
|
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
|
-
|
588
|
-
|
589
|
-
|
590
|
-
|
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
|
-
*
|
817
|
-
*
|
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
|
-
*
|
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;
|
@@ -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
|
-
*
|
6
|
+
* Represents a 2D Bézier curve.
|
7
7
|
*
|
8
|
-
*
|
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
|
-
|
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.
|
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": "
|
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
|
-
|
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
|
});
|