@js-draw/math 1.16.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 +23 -1
- package/dist/cjs/Vec3.js +33 -7
- package/dist/cjs/lib.d.ts +2 -1
- package/dist/cjs/lib.js +5 -1
- package/dist/cjs/shapes/Abstract2DShape.d.ts +3 -0
- package/dist/cjs/shapes/BezierJSWrapper.d.ts +19 -5
- package/dist/cjs/shapes/BezierJSWrapper.js +170 -18
- package/dist/cjs/shapes/LineSegment2.d.ts +45 -5
- package/dist/cjs/shapes/LineSegment2.js +89 -11
- package/dist/cjs/shapes/Parameterized2DShape.d.ts +36 -0
- package/dist/cjs/shapes/Parameterized2DShape.js +20 -0
- package/dist/cjs/shapes/Path.d.ts +131 -13
- package/dist/cjs/shapes/Path.js +507 -26
- package/dist/cjs/shapes/PointShape2D.d.ts +14 -3
- package/dist/cjs/shapes/PointShape2D.js +28 -5
- package/dist/cjs/shapes/QuadraticBezier.d.ts +6 -3
- package/dist/cjs/shapes/QuadraticBezier.js +21 -7
- package/dist/cjs/shapes/Rect2.d.ts +9 -1
- package/dist/cjs/shapes/Rect2.js +9 -2
- 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 +23 -1
- package/dist/mjs/Vec3.mjs +33 -7
- package/dist/mjs/lib.d.ts +2 -1
- package/dist/mjs/lib.mjs +2 -1
- package/dist/mjs/shapes/Abstract2DShape.d.ts +3 -0
- package/dist/mjs/shapes/BezierJSWrapper.d.ts +19 -5
- package/dist/mjs/shapes/BezierJSWrapper.mjs +168 -18
- package/dist/mjs/shapes/LineSegment2.d.ts +45 -5
- package/dist/mjs/shapes/LineSegment2.mjs +89 -11
- package/dist/mjs/shapes/Parameterized2DShape.d.ts +36 -0
- package/dist/mjs/shapes/Parameterized2DShape.mjs +13 -0
- package/dist/mjs/shapes/Path.d.ts +131 -13
- package/dist/mjs/shapes/Path.mjs +504 -25
- package/dist/mjs/shapes/PointShape2D.d.ts +14 -3
- package/dist/mjs/shapes/PointShape2D.mjs +28 -5
- package/dist/mjs/shapes/QuadraticBezier.d.ts +6 -3
- package/dist/mjs/shapes/QuadraticBezier.mjs +21 -7
- package/dist/mjs/shapes/Rect2.d.ts +9 -1
- package/dist/mjs/shapes/Rect2.mjs +9 -2
- 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 +5 -5
- package/src/Mat33.ts +8 -2
- package/src/Vec3.test.ts +42 -7
- package/src/Vec3.ts +37 -8
- package/src/lib.ts +5 -0
- package/src/shapes/Abstract2DShape.ts +3 -0
- package/src/shapes/BezierJSWrapper.ts +195 -14
- package/src/shapes/LineSegment2.test.ts +61 -1
- package/src/shapes/LineSegment2.ts +110 -12
- package/src/shapes/Parameterized2DShape.ts +44 -0
- package/src/shapes/Path.test.ts +233 -5
- package/src/shapes/Path.ts +593 -37
- package/src/shapes/PointShape2D.ts +33 -6
- package/src/shapes/QuadraticBezier.test.ts +69 -12
- package/src/shapes/QuadraticBezier.ts +25 -8
- package/src/shapes/Rect2.ts +10 -3
- 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) {
|
@@ -236,7 +312,7 @@ class Path {
|
|
236
312
|
for (const { part, distFn, bbox } of uncheckedDistFunctions) {
|
237
313
|
// Skip if impossible for the distance to the target to be lesser than
|
238
314
|
// the current minimum.
|
239
|
-
if (!bbox.grownBy(minDist).containsPoint(point)) {
|
315
|
+
if (isFinite(minDist) && !bbox.grownBy(minDist).containsPoint(point)) {
|
240
316
|
continue;
|
241
317
|
}
|
242
318
|
const currentDist = distFn(point);
|
@@ -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 = [
|
@@ -274,7 +350,7 @@ class Path {
|
|
274
350
|
});
|
275
351
|
const result = [];
|
276
352
|
const stoppingThreshold = strokeRadius / 1000;
|
277
|
-
// Returns the maximum
|
353
|
+
// Returns the maximum parameter value explored
|
278
354
|
const raymarchFrom = (startPoint,
|
279
355
|
// Direction to march in (multiplies line.direction)
|
280
356
|
directionMultiplier,
|
@@ -318,9 +394,14 @@ 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,
|
399
|
+
curveIndex: this.geometry.indexOf(lastPart),
|
323
400
|
});
|
401
|
+
// Slightly increase the parameter value to prevent the same point from being
|
402
|
+
// added to the results twice.
|
403
|
+
const parameterIncrease = strokeRadius / 20 / line.length;
|
404
|
+
lastParameter += isFinite(parameterIncrease) ? parameterIncrease : 0;
|
324
405
|
}
|
325
406
|
return lastParameter;
|
326
407
|
};
|
@@ -353,14 +434,21 @@ class Path {
|
|
353
434
|
if (!line.bbox.intersects(this.bbox.grownBy(strokeRadius ?? 0))) {
|
354
435
|
return [];
|
355
436
|
}
|
437
|
+
if (this.parts.length === 0) {
|
438
|
+
return new Path(this.startPoint, [{ kind: PathCommandType.MoveTo, point: this.startPoint }]).intersection(line, strokeRadius);
|
439
|
+
}
|
440
|
+
let index = 0;
|
356
441
|
for (const part of this.geometry) {
|
357
|
-
const
|
358
|
-
|
442
|
+
const intersections = part.argIntersectsLineSegment(line);
|
443
|
+
for (const intersection of intersections) {
|
359
444
|
result.push({
|
360
445
|
curve: part,
|
361
|
-
|
446
|
+
curveIndex: index,
|
447
|
+
point: part.at(intersection),
|
448
|
+
parameterValue: intersection,
|
362
449
|
});
|
363
450
|
}
|
451
|
+
index++;
|
364
452
|
}
|
365
453
|
// If given a non-zero strokeWidth, attempt to raymarch.
|
366
454
|
// Even if raymarching, we need to collect starting points.
|
@@ -373,6 +461,251 @@ class Path {
|
|
373
461
|
}
|
374
462
|
return result;
|
375
463
|
}
|
464
|
+
/**
|
465
|
+
* @returns the nearest point on this path to the given `point`.
|
466
|
+
*/
|
467
|
+
nearestPointTo(point) {
|
468
|
+
// Find the closest point on this
|
469
|
+
let closestSquareDist = Infinity;
|
470
|
+
let closestPartIndex = 0;
|
471
|
+
let closestParameterValue = 0;
|
472
|
+
let closestPoint = this.startPoint;
|
473
|
+
for (let i = 0; i < this.geometry.length; i++) {
|
474
|
+
const current = this.geometry[i];
|
475
|
+
const nearestPoint = current.nearestPointTo(point);
|
476
|
+
const sqareDist = nearestPoint.point.squareDistanceTo(point);
|
477
|
+
if (i === 0 || sqareDist < closestSquareDist) {
|
478
|
+
closestPartIndex = i;
|
479
|
+
closestSquareDist = sqareDist;
|
480
|
+
closestParameterValue = nearestPoint.parameterValue;
|
481
|
+
closestPoint = nearestPoint.point;
|
482
|
+
}
|
483
|
+
}
|
484
|
+
return {
|
485
|
+
curve: this.geometry[closestPartIndex],
|
486
|
+
curveIndex: closestPartIndex,
|
487
|
+
parameterValue: closestParameterValue,
|
488
|
+
point: closestPoint,
|
489
|
+
};
|
490
|
+
}
|
491
|
+
at(index) {
|
492
|
+
if (index.curveIndex === 0 && index.parameterValue === 0) {
|
493
|
+
return this.startPoint;
|
494
|
+
}
|
495
|
+
return this.geometry[index.curveIndex].at(index.parameterValue);
|
496
|
+
}
|
497
|
+
tangentAt(index) {
|
498
|
+
return this.geometry[index.curveIndex].tangentAt(index.parameterValue);
|
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
|
+
}
|
376
709
|
static mapPathCommand(part, mapping) {
|
377
710
|
switch (part.kind) {
|
378
711
|
case PathCommandType.MoveTo:
|
@@ -415,20 +748,104 @@ class Path {
|
|
415
748
|
}
|
416
749
|
return this.mapPoints(point => affineTransfm.transformVec2(point));
|
417
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
|
+
}
|
418
764
|
// Creates a new path by joining [other] to the end of this path
|
419
|
-
union(other
|
765
|
+
union(other,
|
766
|
+
// allowReverse: true iff reversing other or this is permitted if it means
|
767
|
+
// no moveTo command is necessary when unioning the paths.
|
768
|
+
options = { allowReverse: true }) {
|
420
769
|
if (!other) {
|
421
770
|
return this;
|
422
771
|
}
|
423
|
-
|
424
|
-
...this.parts,
|
425
|
-
|
426
|
-
|
427
|
-
|
428
|
-
|
429
|
-
|
430
|
-
|
772
|
+
if (Array.isArray(other)) {
|
773
|
+
return new Path(this.startPoint, [...this.parts, ...other]);
|
774
|
+
}
|
775
|
+
const thisEnd = this.getEndPoint();
|
776
|
+
let newParts = [];
|
777
|
+
if (thisEnd.eq(other.startPoint)) {
|
778
|
+
newParts = this.parts.concat(other.parts);
|
779
|
+
}
|
780
|
+
else if (options.allowReverse && this.startPoint.eq(other.getEndPoint())) {
|
781
|
+
return other.union(this, { allowReverse: false });
|
782
|
+
}
|
783
|
+
else if (options.allowReverse && this.startPoint.eq(other.startPoint)) {
|
784
|
+
return this.union(other.reversed(), { allowReverse: false });
|
785
|
+
}
|
786
|
+
else {
|
787
|
+
newParts = [
|
788
|
+
...this.parts,
|
789
|
+
{
|
790
|
+
kind: PathCommandType.MoveTo,
|
791
|
+
point: other.startPoint,
|
792
|
+
},
|
793
|
+
...other.parts,
|
794
|
+
];
|
795
|
+
}
|
796
|
+
return new Path(this.startPoint, newParts);
|
797
|
+
}
|
798
|
+
/**
|
799
|
+
* @returns a version of this path with the direction reversed.
|
800
|
+
*
|
801
|
+
* Example:
|
802
|
+
* ```ts,runnable,console
|
803
|
+
* import {Path} from '@js-draw/math';
|
804
|
+
* console.log(Path.fromString('m0,0l1,1').reversed()); // -> M1,1 L0,0
|
805
|
+
* ```
|
806
|
+
*/
|
807
|
+
reversed() {
|
808
|
+
const newStart = this.getEndPoint();
|
809
|
+
const newParts = [];
|
810
|
+
let lastPoint = this.startPoint;
|
811
|
+
for (const part of this.parts) {
|
812
|
+
switch (part.kind) {
|
813
|
+
case PathCommandType.LineTo:
|
814
|
+
case PathCommandType.MoveTo:
|
815
|
+
newParts.push({
|
816
|
+
kind: part.kind,
|
817
|
+
point: lastPoint,
|
818
|
+
});
|
819
|
+
lastPoint = part.point;
|
820
|
+
break;
|
821
|
+
case PathCommandType.CubicBezierTo:
|
822
|
+
newParts.push({
|
823
|
+
kind: part.kind,
|
824
|
+
controlPoint1: part.controlPoint2,
|
825
|
+
controlPoint2: part.controlPoint1,
|
826
|
+
endPoint: lastPoint,
|
827
|
+
});
|
828
|
+
lastPoint = part.endPoint;
|
829
|
+
break;
|
830
|
+
case PathCommandType.QuadraticBezierTo:
|
831
|
+
newParts.push({
|
832
|
+
kind: part.kind,
|
833
|
+
controlPoint: part.controlPoint,
|
834
|
+
endPoint: lastPoint,
|
835
|
+
});
|
836
|
+
lastPoint = part.endPoint;
|
837
|
+
break;
|
838
|
+
default:
|
839
|
+
{
|
840
|
+
const exhaustivenessCheck = part;
|
841
|
+
return exhaustivenessCheck;
|
842
|
+
}
|
843
|
+
}
|
844
|
+
}
|
845
|
+
newParts.reverse();
|
846
|
+
return new Path(newStart, newParts);
|
431
847
|
}
|
848
|
+
/** Computes and returns the end point of this path */
|
432
849
|
getEndPoint() {
|
433
850
|
if (this.parts.length === 0) {
|
434
851
|
return this.startPoint;
|
@@ -478,10 +895,12 @@ class Path {
|
|
478
895
|
}
|
479
896
|
return false;
|
480
897
|
}
|
481
|
-
|
482
|
-
|
483
|
-
|
484
|
-
|
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
|
+
*/
|
485
904
|
closedRoughlyIntersects(rect) {
|
486
905
|
if (rect.containsRect(this.bbox)) {
|
487
906
|
return true;
|
@@ -519,6 +938,52 @@ class Path {
|
|
519
938
|
// Even? Probably no intersection.
|
520
939
|
return false;
|
521
940
|
}
|
941
|
+
/** @returns true if all points on this are equivalent to the points on `other` */
|
942
|
+
eq(other, tolerance) {
|
943
|
+
if (other.parts.length !== this.parts.length) {
|
944
|
+
return false;
|
945
|
+
}
|
946
|
+
for (let i = 0; i < this.parts.length; i++) {
|
947
|
+
const part1 = this.parts[i];
|
948
|
+
const part2 = other.parts[i];
|
949
|
+
switch (part1.kind) {
|
950
|
+
case PathCommandType.LineTo:
|
951
|
+
case PathCommandType.MoveTo:
|
952
|
+
if (part1.kind !== part2.kind) {
|
953
|
+
return false;
|
954
|
+
}
|
955
|
+
else if (!part1.point.eq(part2.point, tolerance)) {
|
956
|
+
return false;
|
957
|
+
}
|
958
|
+
break;
|
959
|
+
case PathCommandType.CubicBezierTo:
|
960
|
+
if (part1.kind !== part2.kind) {
|
961
|
+
return false;
|
962
|
+
}
|
963
|
+
else if (!part1.controlPoint1.eq(part2.controlPoint1, tolerance)
|
964
|
+
|| !part1.controlPoint2.eq(part2.controlPoint2, tolerance)
|
965
|
+
|| !part1.endPoint.eq(part2.endPoint, tolerance)) {
|
966
|
+
return false;
|
967
|
+
}
|
968
|
+
break;
|
969
|
+
case PathCommandType.QuadraticBezierTo:
|
970
|
+
if (part1.kind !== part2.kind) {
|
971
|
+
return false;
|
972
|
+
}
|
973
|
+
else if (!part1.controlPoint.eq(part2.controlPoint, tolerance)
|
974
|
+
|| !part1.endPoint.eq(part2.endPoint, tolerance)) {
|
975
|
+
return false;
|
976
|
+
}
|
977
|
+
break;
|
978
|
+
default:
|
979
|
+
{
|
980
|
+
const exhaustivenessCheck = part1;
|
981
|
+
return exhaustivenessCheck;
|
982
|
+
}
|
983
|
+
}
|
984
|
+
}
|
985
|
+
return true;
|
986
|
+
}
|
522
987
|
/**
|
523
988
|
* Returns a path that outlines `rect`.
|
524
989
|
*
|
@@ -661,10 +1126,8 @@ class Path {
|
|
661
1126
|
/**
|
662
1127
|
* Create a `Path` from a subset of the SVG path specification.
|
663
1128
|
*
|
664
|
-
*
|
665
|
-
*
|
666
|
-
* - Elliptical arcs are currently unsupported.
|
667
|
-
* - 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.
|
668
1131
|
*
|
669
1132
|
* @example
|
670
1133
|
* ```ts,runnable,console
|
@@ -675,6 +1138,8 @@ class Path {
|
|
675
1138
|
* ```
|
676
1139
|
*/
|
677
1140
|
static fromString(pathString) {
|
1141
|
+
// TODO: Support elliptical arcs, and the `s`, `t` command shorthands.
|
1142
|
+
//
|
678
1143
|
// See the MDN reference:
|
679
1144
|
// https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/d
|
680
1145
|
// and
|
@@ -842,6 +1307,22 @@ class Path {
|
|
842
1307
|
result.cachedStringVersion = pathString;
|
843
1308
|
return result;
|
844
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
|
+
}
|
845
1326
|
}
|
846
1327
|
exports.Path = Path;
|
847
1328
|
// @internal TODO: At present, this isn't really an empty path.
|
@@ -1,18 +1,29 @@
|
|
1
1
|
import { Point2 } from '../Vec2';
|
2
2
|
import Vec3 from '../Vec3';
|
3
|
-
import Abstract2DShape from './Abstract2DShape';
|
4
3
|
import LineSegment2 from './LineSegment2';
|
4
|
+
import Parameterized2DShape from './Parameterized2DShape';
|
5
5
|
import Rect2 from './Rect2';
|
6
6
|
/**
|
7
7
|
* Like a {@link Point2}, but with additional functionality (e.g. SDF).
|
8
8
|
*
|
9
9
|
* Access the internal `Point2` using the `p` property.
|
10
10
|
*/
|
11
|
-
declare class PointShape2D extends
|
11
|
+
declare class PointShape2D extends Parameterized2DShape {
|
12
12
|
readonly p: Point2;
|
13
13
|
constructor(p: Point2);
|
14
14
|
signedDistance(point: Vec3): number;
|
15
|
-
|
15
|
+
argIntersectsLineSegment(lineSegment: LineSegment2, epsilon?: number): number[];
|
16
16
|
getTightBoundingBox(): Rect2;
|
17
|
+
at(_t: number): Vec3;
|
18
|
+
/**
|
19
|
+
* Returns an arbitrary unit-length vector.
|
20
|
+
*/
|
21
|
+
normalAt(_t: number): Vec3;
|
22
|
+
tangentAt(_t: number): Vec3;
|
23
|
+
splitAt(_t: number): [PointShape2D];
|
24
|
+
nearestPointTo(_point: Point2): {
|
25
|
+
point: Vec3;
|
26
|
+
parameterValue: number;
|
27
|
+
};
|
17
28
|
}
|
18
29
|
export default PointShape2D;
|