@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/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) {
|
@@ -230,7 +304,7 @@ export class Path {
|
|
230
304
|
for (const { part, distFn, bbox } of uncheckedDistFunctions) {
|
231
305
|
// Skip if impossible for the distance to the target to be lesser than
|
232
306
|
// the current minimum.
|
233
|
-
if (!bbox.grownBy(minDist).containsPoint(point)) {
|
307
|
+
if (isFinite(minDist) && !bbox.grownBy(minDist).containsPoint(point)) {
|
234
308
|
continue;
|
235
309
|
}
|
236
310
|
const currentDist = distFn(point);
|
@@ -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 = [
|
@@ -268,7 +342,7 @@ export class Path {
|
|
268
342
|
});
|
269
343
|
const result = [];
|
270
344
|
const stoppingThreshold = strokeRadius / 1000;
|
271
|
-
// Returns the maximum
|
345
|
+
// Returns the maximum parameter value explored
|
272
346
|
const raymarchFrom = (startPoint,
|
273
347
|
// Direction to march in (multiplies line.direction)
|
274
348
|
directionMultiplier,
|
@@ -312,9 +386,14 @@ 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,
|
391
|
+
curveIndex: this.geometry.indexOf(lastPart),
|
317
392
|
});
|
393
|
+
// Slightly increase the parameter value to prevent the same point from being
|
394
|
+
// added to the results twice.
|
395
|
+
const parameterIncrease = strokeRadius / 20 / line.length;
|
396
|
+
lastParameter += isFinite(parameterIncrease) ? parameterIncrease : 0;
|
318
397
|
}
|
319
398
|
return lastParameter;
|
320
399
|
};
|
@@ -347,14 +426,21 @@ export class Path {
|
|
347
426
|
if (!line.bbox.intersects(this.bbox.grownBy(strokeRadius ?? 0))) {
|
348
427
|
return [];
|
349
428
|
}
|
429
|
+
if (this.parts.length === 0) {
|
430
|
+
return new Path(this.startPoint, [{ kind: PathCommandType.MoveTo, point: this.startPoint }]).intersection(line, strokeRadius);
|
431
|
+
}
|
432
|
+
let index = 0;
|
350
433
|
for (const part of this.geometry) {
|
351
|
-
const
|
352
|
-
|
434
|
+
const intersections = part.argIntersectsLineSegment(line);
|
435
|
+
for (const intersection of intersections) {
|
353
436
|
result.push({
|
354
437
|
curve: part,
|
355
|
-
|
438
|
+
curveIndex: index,
|
439
|
+
point: part.at(intersection),
|
440
|
+
parameterValue: intersection,
|
356
441
|
});
|
357
442
|
}
|
443
|
+
index++;
|
358
444
|
}
|
359
445
|
// If given a non-zero strokeWidth, attempt to raymarch.
|
360
446
|
// Even if raymarching, we need to collect starting points.
|
@@ -367,6 +453,251 @@ export class Path {
|
|
367
453
|
}
|
368
454
|
return result;
|
369
455
|
}
|
456
|
+
/**
|
457
|
+
* @returns the nearest point on this path to the given `point`.
|
458
|
+
*/
|
459
|
+
nearestPointTo(point) {
|
460
|
+
// Find the closest point on this
|
461
|
+
let closestSquareDist = Infinity;
|
462
|
+
let closestPartIndex = 0;
|
463
|
+
let closestParameterValue = 0;
|
464
|
+
let closestPoint = this.startPoint;
|
465
|
+
for (let i = 0; i < this.geometry.length; i++) {
|
466
|
+
const current = this.geometry[i];
|
467
|
+
const nearestPoint = current.nearestPointTo(point);
|
468
|
+
const sqareDist = nearestPoint.point.squareDistanceTo(point);
|
469
|
+
if (i === 0 || sqareDist < closestSquareDist) {
|
470
|
+
closestPartIndex = i;
|
471
|
+
closestSquareDist = sqareDist;
|
472
|
+
closestParameterValue = nearestPoint.parameterValue;
|
473
|
+
closestPoint = nearestPoint.point;
|
474
|
+
}
|
475
|
+
}
|
476
|
+
return {
|
477
|
+
curve: this.geometry[closestPartIndex],
|
478
|
+
curveIndex: closestPartIndex,
|
479
|
+
parameterValue: closestParameterValue,
|
480
|
+
point: closestPoint,
|
481
|
+
};
|
482
|
+
}
|
483
|
+
at(index) {
|
484
|
+
if (index.curveIndex === 0 && index.parameterValue === 0) {
|
485
|
+
return this.startPoint;
|
486
|
+
}
|
487
|
+
return this.geometry[index.curveIndex].at(index.parameterValue);
|
488
|
+
}
|
489
|
+
tangentAt(index) {
|
490
|
+
return this.geometry[index.curveIndex].tangentAt(index.parameterValue);
|
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
|
+
}
|
370
701
|
static mapPathCommand(part, mapping) {
|
371
702
|
switch (part.kind) {
|
372
703
|
case PathCommandType.MoveTo:
|
@@ -409,20 +740,104 @@ export class Path {
|
|
409
740
|
}
|
410
741
|
return this.mapPoints(point => affineTransfm.transformVec2(point));
|
411
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
|
+
}
|
412
756
|
// Creates a new path by joining [other] to the end of this path
|
413
|
-
union(other
|
757
|
+
union(other,
|
758
|
+
// allowReverse: true iff reversing other or this is permitted if it means
|
759
|
+
// no moveTo command is necessary when unioning the paths.
|
760
|
+
options = { allowReverse: true }) {
|
414
761
|
if (!other) {
|
415
762
|
return this;
|
416
763
|
}
|
417
|
-
|
418
|
-
...this.parts,
|
419
|
-
|
420
|
-
|
421
|
-
|
422
|
-
|
423
|
-
|
424
|
-
|
764
|
+
if (Array.isArray(other)) {
|
765
|
+
return new Path(this.startPoint, [...this.parts, ...other]);
|
766
|
+
}
|
767
|
+
const thisEnd = this.getEndPoint();
|
768
|
+
let newParts = [];
|
769
|
+
if (thisEnd.eq(other.startPoint)) {
|
770
|
+
newParts = this.parts.concat(other.parts);
|
771
|
+
}
|
772
|
+
else if (options.allowReverse && this.startPoint.eq(other.getEndPoint())) {
|
773
|
+
return other.union(this, { allowReverse: false });
|
774
|
+
}
|
775
|
+
else if (options.allowReverse && this.startPoint.eq(other.startPoint)) {
|
776
|
+
return this.union(other.reversed(), { allowReverse: false });
|
777
|
+
}
|
778
|
+
else {
|
779
|
+
newParts = [
|
780
|
+
...this.parts,
|
781
|
+
{
|
782
|
+
kind: PathCommandType.MoveTo,
|
783
|
+
point: other.startPoint,
|
784
|
+
},
|
785
|
+
...other.parts,
|
786
|
+
];
|
787
|
+
}
|
788
|
+
return new Path(this.startPoint, newParts);
|
425
789
|
}
|
790
|
+
/**
|
791
|
+
* @returns a version of this path with the direction reversed.
|
792
|
+
*
|
793
|
+
* Example:
|
794
|
+
* ```ts,runnable,console
|
795
|
+
* import {Path} from '@js-draw/math';
|
796
|
+
* console.log(Path.fromString('m0,0l1,1').reversed()); // -> M1,1 L0,0
|
797
|
+
* ```
|
798
|
+
*/
|
799
|
+
reversed() {
|
800
|
+
const newStart = this.getEndPoint();
|
801
|
+
const newParts = [];
|
802
|
+
let lastPoint = this.startPoint;
|
803
|
+
for (const part of this.parts) {
|
804
|
+
switch (part.kind) {
|
805
|
+
case PathCommandType.LineTo:
|
806
|
+
case PathCommandType.MoveTo:
|
807
|
+
newParts.push({
|
808
|
+
kind: part.kind,
|
809
|
+
point: lastPoint,
|
810
|
+
});
|
811
|
+
lastPoint = part.point;
|
812
|
+
break;
|
813
|
+
case PathCommandType.CubicBezierTo:
|
814
|
+
newParts.push({
|
815
|
+
kind: part.kind,
|
816
|
+
controlPoint1: part.controlPoint2,
|
817
|
+
controlPoint2: part.controlPoint1,
|
818
|
+
endPoint: lastPoint,
|
819
|
+
});
|
820
|
+
lastPoint = part.endPoint;
|
821
|
+
break;
|
822
|
+
case PathCommandType.QuadraticBezierTo:
|
823
|
+
newParts.push({
|
824
|
+
kind: part.kind,
|
825
|
+
controlPoint: part.controlPoint,
|
826
|
+
endPoint: lastPoint,
|
827
|
+
});
|
828
|
+
lastPoint = part.endPoint;
|
829
|
+
break;
|
830
|
+
default:
|
831
|
+
{
|
832
|
+
const exhaustivenessCheck = part;
|
833
|
+
return exhaustivenessCheck;
|
834
|
+
}
|
835
|
+
}
|
836
|
+
}
|
837
|
+
newParts.reverse();
|
838
|
+
return new Path(newStart, newParts);
|
839
|
+
}
|
840
|
+
/** Computes and returns the end point of this path */
|
426
841
|
getEndPoint() {
|
427
842
|
if (this.parts.length === 0) {
|
428
843
|
return this.startPoint;
|
@@ -472,10 +887,12 @@ export class Path {
|
|
472
887
|
}
|
473
888
|
return false;
|
474
889
|
}
|
475
|
-
|
476
|
-
|
477
|
-
|
478
|
-
|
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
|
+
*/
|
479
896
|
closedRoughlyIntersects(rect) {
|
480
897
|
if (rect.containsRect(this.bbox)) {
|
481
898
|
return true;
|
@@ -513,6 +930,52 @@ export class Path {
|
|
513
930
|
// Even? Probably no intersection.
|
514
931
|
return false;
|
515
932
|
}
|
933
|
+
/** @returns true if all points on this are equivalent to the points on `other` */
|
934
|
+
eq(other, tolerance) {
|
935
|
+
if (other.parts.length !== this.parts.length) {
|
936
|
+
return false;
|
937
|
+
}
|
938
|
+
for (let i = 0; i < this.parts.length; i++) {
|
939
|
+
const part1 = this.parts[i];
|
940
|
+
const part2 = other.parts[i];
|
941
|
+
switch (part1.kind) {
|
942
|
+
case PathCommandType.LineTo:
|
943
|
+
case PathCommandType.MoveTo:
|
944
|
+
if (part1.kind !== part2.kind) {
|
945
|
+
return false;
|
946
|
+
}
|
947
|
+
else if (!part1.point.eq(part2.point, tolerance)) {
|
948
|
+
return false;
|
949
|
+
}
|
950
|
+
break;
|
951
|
+
case PathCommandType.CubicBezierTo:
|
952
|
+
if (part1.kind !== part2.kind) {
|
953
|
+
return false;
|
954
|
+
}
|
955
|
+
else if (!part1.controlPoint1.eq(part2.controlPoint1, tolerance)
|
956
|
+
|| !part1.controlPoint2.eq(part2.controlPoint2, tolerance)
|
957
|
+
|| !part1.endPoint.eq(part2.endPoint, tolerance)) {
|
958
|
+
return false;
|
959
|
+
}
|
960
|
+
break;
|
961
|
+
case PathCommandType.QuadraticBezierTo:
|
962
|
+
if (part1.kind !== part2.kind) {
|
963
|
+
return false;
|
964
|
+
}
|
965
|
+
else if (!part1.controlPoint.eq(part2.controlPoint, tolerance)
|
966
|
+
|| !part1.endPoint.eq(part2.endPoint, tolerance)) {
|
967
|
+
return false;
|
968
|
+
}
|
969
|
+
break;
|
970
|
+
default:
|
971
|
+
{
|
972
|
+
const exhaustivenessCheck = part1;
|
973
|
+
return exhaustivenessCheck;
|
974
|
+
}
|
975
|
+
}
|
976
|
+
}
|
977
|
+
return true;
|
978
|
+
}
|
516
979
|
/**
|
517
980
|
* Returns a path that outlines `rect`.
|
518
981
|
*
|
@@ -655,10 +1118,8 @@ export class Path {
|
|
655
1118
|
/**
|
656
1119
|
* Create a `Path` from a subset of the SVG path specification.
|
657
1120
|
*
|
658
|
-
*
|
659
|
-
*
|
660
|
-
* - Elliptical arcs are currently unsupported.
|
661
|
-
* - 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.
|
662
1123
|
*
|
663
1124
|
* @example
|
664
1125
|
* ```ts,runnable,console
|
@@ -669,6 +1130,8 @@ export class Path {
|
|
669
1130
|
* ```
|
670
1131
|
*/
|
671
1132
|
static fromString(pathString) {
|
1133
|
+
// TODO: Support elliptical arcs, and the `s`, `t` command shorthands.
|
1134
|
+
//
|
672
1135
|
// See the MDN reference:
|
673
1136
|
// https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/d
|
674
1137
|
// and
|
@@ -836,6 +1299,22 @@ export class Path {
|
|
836
1299
|
result.cachedStringVersion = pathString;
|
837
1300
|
return result;
|
838
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
|
+
}
|
839
1318
|
}
|
840
1319
|
// @internal TODO: At present, this isn't really an empty path.
|
841
1320
|
Path.empty = new Path(Vec2.zero, []);
|
@@ -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;
|