@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/src/shapes/Path.ts
CHANGED
@@ -2,12 +2,14 @@ import LineSegment2 from './LineSegment2';
|
|
2
2
|
import Mat33 from '../Mat33';
|
3
3
|
import Rect2 from './Rect2';
|
4
4
|
import { Point2, Vec2 } from '../Vec2';
|
5
|
-
import Abstract2DShape from './Abstract2DShape';
|
6
5
|
import CubicBezier from './CubicBezier';
|
7
6
|
import QuadraticBezier from './QuadraticBezier';
|
8
7
|
import PointShape2D from './PointShape2D';
|
9
8
|
import toRoundedString from '../rounding/toRoundedString';
|
10
9
|
import toStringOfSamePrecision from '../rounding/toStringOfSamePrecision';
|
10
|
+
import Parameterized2DShape from './Parameterized2DShape';
|
11
|
+
import BezierJSWrapper from './BezierJSWrapper';
|
12
|
+
import convexHull2Of from '../utils/convexHull2Of';
|
11
13
|
|
12
14
|
export enum PathCommandType {
|
13
15
|
LineTo,
|
@@ -41,19 +43,96 @@ export interface MoveToPathCommand {
|
|
41
43
|
|
42
44
|
export type PathCommand = CubicBezierPathCommand | QuadraticBezierPathCommand | MoveToPathCommand | LinePathCommand;
|
43
45
|
|
44
|
-
interface IntersectionResult {
|
46
|
+
export interface IntersectionResult {
|
45
47
|
// @internal
|
46
|
-
curve:
|
48
|
+
curve: Parameterized2DShape;
|
49
|
+
// @internal
|
50
|
+
curveIndex: number;
|
47
51
|
|
48
|
-
/** @internal
|
49
|
-
parameterValue
|
52
|
+
/** Parameter value for the closest point **on** the path to the intersection. @internal */
|
53
|
+
parameterValue: number;
|
50
54
|
|
51
|
-
|
55
|
+
/** Point at which the intersection occured. */
|
52
56
|
point: Point2;
|
53
57
|
}
|
54
58
|
|
59
|
+
/** Options for {@link Path.splitNear} and {@link Path.splitAt} */
|
60
|
+
export interface PathSplitOptions {
|
61
|
+
/**
|
62
|
+
* Allows mapping points on newly added segments. This is useful, for example,
|
63
|
+
* to round points to prevent long decimals when later saving.
|
64
|
+
*/
|
65
|
+
mapNewPoint?: (point: Point2)=>Point2;
|
66
|
+
}
|
67
|
+
|
68
|
+
/**
|
69
|
+
* Allows indexing a particular part of a path.
|
70
|
+
*
|
71
|
+
* @see {@link Path.at} {@link Path.tangentAt}
|
72
|
+
*/
|
73
|
+
export interface CurveIndexRecord {
|
74
|
+
curveIndex: number;
|
75
|
+
parameterValue: number;
|
76
|
+
}
|
77
|
+
|
78
|
+
/** Returns a positive number if `a` comes after `b`, 0 if equal, and negative otherwise. */
|
79
|
+
export const compareCurveIndices = (a: CurveIndexRecord, b: CurveIndexRecord) => {
|
80
|
+
const indexCompare = a.curveIndex - b.curveIndex;
|
81
|
+
if (indexCompare === 0) {
|
82
|
+
return a.parameterValue - b.parameterValue;
|
83
|
+
} else {
|
84
|
+
return indexCompare;
|
85
|
+
}
|
86
|
+
};
|
87
|
+
|
88
|
+
/**
|
89
|
+
* Returns a version of `index` with its parameter value incremented by `stepBy`
|
90
|
+
* (which can be either positive or negative).
|
91
|
+
*/
|
92
|
+
export const stepCurveIndexBy = (index: CurveIndexRecord, stepBy: number): CurveIndexRecord => {
|
93
|
+
if (index.parameterValue + stepBy > 1) {
|
94
|
+
return { curveIndex: index.curveIndex + 1, parameterValue: index.parameterValue + stepBy - 1 };
|
95
|
+
}
|
96
|
+
if (index.parameterValue + stepBy < 0) {
|
97
|
+
if (index.curveIndex === 0) {
|
98
|
+
return { curveIndex: 0, parameterValue: 0 };
|
99
|
+
}
|
100
|
+
return { curveIndex: index.curveIndex - 1, parameterValue: index.parameterValue + stepBy + 1 };
|
101
|
+
}
|
102
|
+
|
103
|
+
return { curveIndex: index.curveIndex, parameterValue: index.parameterValue + stepBy };
|
104
|
+
};
|
105
|
+
|
55
106
|
/**
|
56
107
|
* Represents a union of lines and curves.
|
108
|
+
*
|
109
|
+
* To create a path from a string, see {@link fromString}.
|
110
|
+
*
|
111
|
+
* @example
|
112
|
+
* ```ts,runnable,console
|
113
|
+
* import {Path, Mat33, Vec2, LineSegment2} from '@js-draw/math';
|
114
|
+
*
|
115
|
+
* // Creates a path from an SVG path string.
|
116
|
+
* // In this case,
|
117
|
+
* // 1. Move to (0,0)
|
118
|
+
* // 2. Line to (100,0)
|
119
|
+
* const path = Path.fromString('M0,0 L100,0');
|
120
|
+
*
|
121
|
+
* // Logs the distance from (10,0) to the curve 1 unit
|
122
|
+
* // away from path. This curve forms a stroke with the path at
|
123
|
+
* // its center.
|
124
|
+
* const strokeRadius = 1;
|
125
|
+
* console.log(path.signedDistance(Vec2.of(10,0), strokeRadius));
|
126
|
+
*
|
127
|
+
* // Log a version of the path that's scaled by a factor of 4.
|
128
|
+
* console.log(path.transformedBy(Mat33.scaling2D(4)).toString());
|
129
|
+
*
|
130
|
+
* // Log all intersections of a stroked version of the path with
|
131
|
+
* // a vertical line segment.
|
132
|
+
* // (Try removing the `strokeRadius` parameter).
|
133
|
+
* const segment = new LineSegment2(Vec2.of(5, -100), Vec2.of(5, 100));
|
134
|
+
* console.log(path.intersection(segment, strokeRadius).map(i => i.point));
|
135
|
+
* ```
|
57
136
|
*/
|
58
137
|
export class Path {
|
59
138
|
/**
|
@@ -88,6 +167,12 @@ export class Path {
|
|
88
167
|
}
|
89
168
|
}
|
90
169
|
|
170
|
+
/**
|
171
|
+
* Computes and returns the full bounding box for this path.
|
172
|
+
*
|
173
|
+
* If a slight over-estimate of a path's bounding box is sufficient, use
|
174
|
+
* {@link bbox} instead.
|
175
|
+
*/
|
91
176
|
public getExactBBox(): Rect2 {
|
92
177
|
const bboxes: Rect2[] = [];
|
93
178
|
for (const part of this.geometry) {
|
@@ -97,16 +182,16 @@ export class Path {
|
|
97
182
|
return Rect2.union(...bboxes);
|
98
183
|
}
|
99
184
|
|
100
|
-
private cachedGeometry:
|
185
|
+
private cachedGeometry: Parameterized2DShape[]|null = null;
|
101
186
|
|
102
187
|
// Lazy-loads and returns this path's geometry
|
103
|
-
public get geometry():
|
188
|
+
public get geometry(): Parameterized2DShape[] {
|
104
189
|
if (this.cachedGeometry) {
|
105
190
|
return this.cachedGeometry;
|
106
191
|
}
|
107
192
|
|
108
193
|
let startPoint = this.startPoint;
|
109
|
-
const geometry:
|
194
|
+
const geometry: Parameterized2DShape[] = [];
|
110
195
|
|
111
196
|
for (const part of this.parts) {
|
112
197
|
let exhaustivenessCheck: never;
|
@@ -237,7 +322,20 @@ export class Path {
|
|
237
322
|
return Rect2.bboxOf(points);
|
238
323
|
}
|
239
324
|
|
240
|
-
/**
|
325
|
+
/**
|
326
|
+
* Returns the signed distance between `point` and a curve `strokeRadius` units
|
327
|
+
* away from this path.
|
328
|
+
*
|
329
|
+
* This returns the **signed distance**, which means that points inside this shape
|
330
|
+
* have their distance negated. For example,
|
331
|
+
* ```ts,runnable,console
|
332
|
+
* import {Path, Vec2} from '@js-draw/math';
|
333
|
+
* console.log(Path.fromString('m0,0 L100,0').signedDistance(Vec2.zero, 1));
|
334
|
+
* ```
|
335
|
+
* would print `-1` because (0,0) is on `m0,0 L100,0` and thus one unit away from its boundary.
|
336
|
+
*
|
337
|
+
* **Note**: `strokeRadius = strokeWidth / 2`
|
338
|
+
*/
|
241
339
|
public signedDistance(point: Point2, strokeRadius: number) {
|
242
340
|
let minDist = Infinity;
|
243
341
|
|
@@ -270,7 +368,7 @@ export class Path {
|
|
270
368
|
|
271
369
|
type DistanceFunction = (point: Point2) => number;
|
272
370
|
type DistanceFunctionRecord = {
|
273
|
-
part:
|
371
|
+
part: Parameterized2DShape,
|
274
372
|
bbox: Rect2,
|
275
373
|
distFn: DistanceFunction,
|
276
374
|
};
|
@@ -309,9 +407,9 @@ export class Path {
|
|
309
407
|
|
310
408
|
// Returns the minimum distance to a part in this stroke, where only parts that the given
|
311
409
|
// line could intersect are considered.
|
312
|
-
const sdf = (point: Point2): [
|
410
|
+
const sdf = (point: Point2): [Parameterized2DShape|null, number] => {
|
313
411
|
let minDist = Infinity;
|
314
|
-
let minDistPart:
|
412
|
+
let minDistPart: Parameterized2DShape|null = null;
|
315
413
|
|
316
414
|
const uncheckedDistFunctions: DistanceFunctionRecord[] = [];
|
317
415
|
|
@@ -338,7 +436,7 @@ export class Path {
|
|
338
436
|
for (const { part, distFn, bbox } of uncheckedDistFunctions) {
|
339
437
|
// Skip if impossible for the distance to the target to be lesser than
|
340
438
|
// the current minimum.
|
341
|
-
if (!bbox.grownBy(minDist).containsPoint(point)) {
|
439
|
+
if (isFinite(minDist) && !bbox.grownBy(minDist).containsPoint(point)) {
|
342
440
|
continue;
|
343
441
|
}
|
344
442
|
|
@@ -355,7 +453,7 @@ export class Path {
|
|
355
453
|
|
356
454
|
|
357
455
|
// Raymarch:
|
358
|
-
const maxRaymarchSteps =
|
456
|
+
const maxRaymarchSteps = 8;
|
359
457
|
|
360
458
|
// Start raymarching from each of these points. This allows detection of multiple
|
361
459
|
// intersections.
|
@@ -388,7 +486,7 @@ export class Path {
|
|
388
486
|
|
389
487
|
const stoppingThreshold = strokeRadius / 1000;
|
390
488
|
|
391
|
-
// Returns the maximum
|
489
|
+
// Returns the maximum parameter value explored
|
392
490
|
const raymarchFrom = (
|
393
491
|
startPoint: Point2,
|
394
492
|
|
@@ -446,9 +544,15 @@ export class Path {
|
|
446
544
|
if (lastPart && isOnLineSegment && Math.abs(lastDist) < stoppingThreshold) {
|
447
545
|
result.push({
|
448
546
|
point: currentPoint,
|
449
|
-
parameterValue:
|
547
|
+
parameterValue: lastPart.nearestPointTo(currentPoint).parameterValue,
|
450
548
|
curve: lastPart,
|
549
|
+
curveIndex: this.geometry.indexOf(lastPart),
|
451
550
|
});
|
551
|
+
|
552
|
+
// Slightly increase the parameter value to prevent the same point from being
|
553
|
+
// added to the results twice.
|
554
|
+
const parameterIncrease = strokeRadius / 20 / line.length;
|
555
|
+
lastParameter += isFinite(parameterIncrease) ? parameterIncrease : 0;
|
452
556
|
}
|
453
557
|
|
454
558
|
return lastParameter;
|
@@ -489,15 +593,24 @@ export class Path {
|
|
489
593
|
return [];
|
490
594
|
}
|
491
595
|
|
596
|
+
if (this.parts.length === 0) {
|
597
|
+
return new Path(this.startPoint, [{ kind: PathCommandType.MoveTo, point: this.startPoint }]).intersection(line, strokeRadius);
|
598
|
+
}
|
599
|
+
|
600
|
+
let index = 0;
|
492
601
|
for (const part of this.geometry) {
|
493
|
-
const
|
602
|
+
const intersections = part.argIntersectsLineSegment(line);
|
494
603
|
|
495
|
-
|
604
|
+
for (const intersection of intersections) {
|
496
605
|
result.push({
|
497
606
|
curve: part,
|
498
|
-
|
607
|
+
curveIndex: index,
|
608
|
+
point: part.at(intersection),
|
609
|
+
parameterValue: intersection,
|
499
610
|
});
|
500
611
|
}
|
612
|
+
|
613
|
+
index ++;
|
501
614
|
}
|
502
615
|
|
503
616
|
// If given a non-zero strokeWidth, attempt to raymarch.
|
@@ -513,6 +626,287 @@ export class Path {
|
|
513
626
|
return result;
|
514
627
|
}
|
515
628
|
|
629
|
+
/**
|
630
|
+
* @returns the nearest point on this path to the given `point`.
|
631
|
+
*/
|
632
|
+
public nearestPointTo(point: Point2): IntersectionResult {
|
633
|
+
// Find the closest point on this
|
634
|
+
let closestSquareDist = Infinity;
|
635
|
+
let closestPartIndex = 0;
|
636
|
+
let closestParameterValue = 0;
|
637
|
+
let closestPoint: Point2 = this.startPoint;
|
638
|
+
|
639
|
+
for (let i = 0; i < this.geometry.length; i++) {
|
640
|
+
const current = this.geometry[i];
|
641
|
+
const nearestPoint = current.nearestPointTo(point);
|
642
|
+
const sqareDist = nearestPoint.point.squareDistanceTo(point);
|
643
|
+
if (i === 0 || sqareDist < closestSquareDist) {
|
644
|
+
closestPartIndex = i;
|
645
|
+
closestSquareDist = sqareDist;
|
646
|
+
closestParameterValue = nearestPoint.parameterValue;
|
647
|
+
closestPoint = nearestPoint.point;
|
648
|
+
}
|
649
|
+
}
|
650
|
+
|
651
|
+
return {
|
652
|
+
curve: this.geometry[closestPartIndex],
|
653
|
+
curveIndex: closestPartIndex,
|
654
|
+
parameterValue: closestParameterValue,
|
655
|
+
point: closestPoint,
|
656
|
+
};
|
657
|
+
}
|
658
|
+
|
659
|
+
public at(index: CurveIndexRecord) {
|
660
|
+
if (index.curveIndex === 0 && index.parameterValue === 0) {
|
661
|
+
return this.startPoint;
|
662
|
+
}
|
663
|
+
return this.geometry[index.curveIndex].at(index.parameterValue);
|
664
|
+
}
|
665
|
+
|
666
|
+
public tangentAt(index: CurveIndexRecord) {
|
667
|
+
return this.geometry[index.curveIndex].tangentAt(index.parameterValue);
|
668
|
+
}
|
669
|
+
|
670
|
+
/** Splits this path in two near the given `point`. */
|
671
|
+
public splitNear(point: Point2, options?: PathSplitOptions) {
|
672
|
+
const nearest = this.nearestPointTo(point);
|
673
|
+
return this.splitAt(nearest, options);
|
674
|
+
}
|
675
|
+
|
676
|
+
/**
|
677
|
+
* Returns a copy of this path with `deleteFrom` until `deleteUntil` replaced with `insert`.
|
678
|
+
*
|
679
|
+
* This method is analogous to {@link Array.toSpliced}.
|
680
|
+
*/
|
681
|
+
public spliced(deleteFrom: CurveIndexRecord, deleteTo: CurveIndexRecord, insert: Path|undefined, options?: PathSplitOptions): Path {
|
682
|
+
const isBeforeOrEqual = (a: CurveIndexRecord, b: CurveIndexRecord) => {
|
683
|
+
return a.curveIndex < b.curveIndex || (a.curveIndex === b.curveIndex && a.parameterValue <= b.parameterValue);
|
684
|
+
};
|
685
|
+
|
686
|
+
if (isBeforeOrEqual(deleteFrom, deleteTo)) {
|
687
|
+
// deleteFrom deleteTo
|
688
|
+
// <---------| |-------------->
|
689
|
+
// x x
|
690
|
+
// startPoint endPoint
|
691
|
+
const firstSplit = this.splitAt(deleteFrom, options);
|
692
|
+
const secondSplit = this.splitAt(deleteTo, options);
|
693
|
+
const before = firstSplit[0];
|
694
|
+
const after = secondSplit[secondSplit.length - 1];
|
695
|
+
return insert ? before.union(insert).union(after) : before.union(after);
|
696
|
+
} else {
|
697
|
+
// In this case, we need to handle wrapping at the start/end.
|
698
|
+
// deleteTo deleteFrom
|
699
|
+
// <---------| keep |-------------->
|
700
|
+
// x x
|
701
|
+
// startPoint endPoint
|
702
|
+
const splitAtFrom = this.splitAt([deleteFrom], options);
|
703
|
+
const beforeFrom = splitAtFrom[0];
|
704
|
+
|
705
|
+
// We need splitNear, rather than splitAt, because beforeFrom does not have
|
706
|
+
// the same indexing as this.
|
707
|
+
const splitAtTo = beforeFrom.splitNear(this.at(deleteTo), options);
|
708
|
+
|
709
|
+
const betweenBoth = splitAtTo[splitAtTo.length - 1];
|
710
|
+
return insert ? betweenBoth.union(insert) : betweenBoth;
|
711
|
+
}
|
712
|
+
}
|
713
|
+
|
714
|
+
public splitAt(at: CurveIndexRecord, options?: PathSplitOptions): [Path]|[Path, Path];
|
715
|
+
public splitAt(at: CurveIndexRecord[], options?: PathSplitOptions): Path[];
|
716
|
+
|
717
|
+
// @internal
|
718
|
+
public splitAt(splitAt: CurveIndexRecord[]|CurveIndexRecord, options?: PathSplitOptions): Path[] {
|
719
|
+
if (!Array.isArray(splitAt)) {
|
720
|
+
splitAt = [splitAt];
|
721
|
+
}
|
722
|
+
|
723
|
+
splitAt = [...splitAt];
|
724
|
+
splitAt.sort(compareCurveIndices);
|
725
|
+
|
726
|
+
//
|
727
|
+
// Bounds checking & reversal.
|
728
|
+
//
|
729
|
+
|
730
|
+
while (
|
731
|
+
splitAt.length > 0
|
732
|
+
&& splitAt[splitAt.length - 1].curveIndex >= this.parts.length - 1
|
733
|
+
&& splitAt[splitAt.length - 1].parameterValue >= 1
|
734
|
+
) {
|
735
|
+
splitAt.pop();
|
736
|
+
}
|
737
|
+
|
738
|
+
splitAt.reverse(); // .reverse() <-- We're `.pop`ing from the end
|
739
|
+
|
740
|
+
while (
|
741
|
+
splitAt.length > 0
|
742
|
+
&& splitAt[splitAt.length - 1].curveIndex <= 0
|
743
|
+
&& splitAt[splitAt.length - 1].parameterValue <= 0
|
744
|
+
) {
|
745
|
+
splitAt.pop();
|
746
|
+
}
|
747
|
+
|
748
|
+
if (splitAt.length === 0 || this.parts.length === 0) {
|
749
|
+
return [this];
|
750
|
+
}
|
751
|
+
|
752
|
+
const expectedSplitCount = splitAt.length + 1;
|
753
|
+
const mapNewPoint = options?.mapNewPoint ?? ((p: Point2)=>p);
|
754
|
+
|
755
|
+
const result: Path[] = [];
|
756
|
+
let currentStartPoint = this.startPoint;
|
757
|
+
let currentPath: PathCommand[] = [];
|
758
|
+
|
759
|
+
//
|
760
|
+
// Splitting
|
761
|
+
//
|
762
|
+
|
763
|
+
let { curveIndex, parameterValue } = splitAt.pop()!;
|
764
|
+
|
765
|
+
for (let i = 0; i < this.parts.length; i ++) {
|
766
|
+
if (i !== curveIndex) {
|
767
|
+
currentPath.push(this.parts[i]);
|
768
|
+
} else {
|
769
|
+
let part = this.parts[i];
|
770
|
+
let geom = this.geometry[i];
|
771
|
+
while (i === curveIndex) {
|
772
|
+
let newPathStart: Point2;
|
773
|
+
const newPath: PathCommand[] = [];
|
774
|
+
|
775
|
+
switch (part.kind) {
|
776
|
+
case PathCommandType.MoveTo:
|
777
|
+
currentPath.push({
|
778
|
+
kind: part.kind,
|
779
|
+
point: part.point,
|
780
|
+
});
|
781
|
+
newPathStart = part.point;
|
782
|
+
break;
|
783
|
+
case PathCommandType.LineTo:
|
784
|
+
{
|
785
|
+
const split = (geom as LineSegment2).splitAt(parameterValue);
|
786
|
+
currentPath.push({
|
787
|
+
kind: part.kind,
|
788
|
+
point: mapNewPoint(split[0].p2),
|
789
|
+
});
|
790
|
+
newPathStart = split[0].p2;
|
791
|
+
if (split.length > 1) {
|
792
|
+
console.assert(split.length === 2);
|
793
|
+
newPath.push({
|
794
|
+
kind: part.kind,
|
795
|
+
|
796
|
+
// Don't map: For lines, the end point of the split is
|
797
|
+
// the same as the end point of the original:
|
798
|
+
point: split[1]!.p2,
|
799
|
+
});
|
800
|
+
geom = split[1]!;
|
801
|
+
}
|
802
|
+
}
|
803
|
+
break;
|
804
|
+
case PathCommandType.QuadraticBezierTo:
|
805
|
+
case PathCommandType.CubicBezierTo:
|
806
|
+
{
|
807
|
+
const split = (geom as BezierJSWrapper).splitAt(parameterValue);
|
808
|
+
let isFirstPart = split.length === 2;
|
809
|
+
for (const segment of split) {
|
810
|
+
geom = segment;
|
811
|
+
const targetArray = isFirstPart ? currentPath : newPath;
|
812
|
+
const controlPoints = segment.getPoints();
|
813
|
+
if (part.kind === PathCommandType.CubicBezierTo) {
|
814
|
+
targetArray.push({
|
815
|
+
kind: part.kind,
|
816
|
+
controlPoint1: mapNewPoint(controlPoints[1]),
|
817
|
+
controlPoint2: mapNewPoint(controlPoints[2]),
|
818
|
+
endPoint: mapNewPoint(controlPoints[3]),
|
819
|
+
});
|
820
|
+
} else {
|
821
|
+
targetArray.push({
|
822
|
+
kind: part.kind,
|
823
|
+
controlPoint: mapNewPoint(controlPoints[1]),
|
824
|
+
endPoint: mapNewPoint(controlPoints[2]),
|
825
|
+
});
|
826
|
+
}
|
827
|
+
|
828
|
+
// We want the start of the new path to match the start of the
|
829
|
+
// FIRST Bézier in the NEW path.
|
830
|
+
if (!isFirstPart) {
|
831
|
+
newPathStart = controlPoints[0];
|
832
|
+
}
|
833
|
+
isFirstPart = false;
|
834
|
+
}
|
835
|
+
}
|
836
|
+
break;
|
837
|
+
default: {
|
838
|
+
const exhaustivenessCheck: never = part;
|
839
|
+
return exhaustivenessCheck;
|
840
|
+
}
|
841
|
+
}
|
842
|
+
|
843
|
+
result.push(new Path(currentStartPoint, [...currentPath]));
|
844
|
+
currentStartPoint = mapNewPoint(newPathStart!);
|
845
|
+
console.assert(!!currentStartPoint, 'should have a start point');
|
846
|
+
currentPath = newPath;
|
847
|
+
part = newPath[newPath.length - 1] ?? part;
|
848
|
+
|
849
|
+
const nextSplit = splitAt.pop();
|
850
|
+
if (!nextSplit) {
|
851
|
+
break;
|
852
|
+
} else {
|
853
|
+
curveIndex = nextSplit.curveIndex;
|
854
|
+
if (i === curveIndex) {
|
855
|
+
const originalPoint = this.at(nextSplit);
|
856
|
+
parameterValue = geom.nearestPointTo(originalPoint).parameterValue;
|
857
|
+
currentPath = [];
|
858
|
+
} else {
|
859
|
+
parameterValue = nextSplit.parameterValue;
|
860
|
+
}
|
861
|
+
}
|
862
|
+
}
|
863
|
+
}
|
864
|
+
}
|
865
|
+
|
866
|
+
result.push(new Path(currentStartPoint, currentPath));
|
867
|
+
|
868
|
+
console.assert(
|
869
|
+
result.length === expectedSplitCount,
|
870
|
+
`should split into splitAt.length + 1 splits (was ${result.length}, expected ${expectedSplitCount})`
|
871
|
+
);
|
872
|
+
return result;
|
873
|
+
}
|
874
|
+
|
875
|
+
/**
|
876
|
+
* Replaces all `MoveTo` commands with `LineTo` commands and connects the end point of this
|
877
|
+
* path to the start point.
|
878
|
+
*/
|
879
|
+
public asClosed() {
|
880
|
+
const newParts: PathCommand[] = [];
|
881
|
+
let hasChanges = false;
|
882
|
+
for (const part of this.parts) {
|
883
|
+
if (part.kind === PathCommandType.MoveTo) {
|
884
|
+
newParts.push({
|
885
|
+
kind: PathCommandType.LineTo,
|
886
|
+
point: part.point,
|
887
|
+
});
|
888
|
+
hasChanges = true;
|
889
|
+
} else {
|
890
|
+
newParts.push(part);
|
891
|
+
}
|
892
|
+
}
|
893
|
+
if (!this.getEndPoint().eq(this.startPoint)) {
|
894
|
+
newParts.push({
|
895
|
+
kind: PathCommandType.LineTo,
|
896
|
+
point: this.startPoint,
|
897
|
+
});
|
898
|
+
hasChanges = true;
|
899
|
+
}
|
900
|
+
|
901
|
+
if (!hasChanges) {
|
902
|
+
return this;
|
903
|
+
}
|
904
|
+
|
905
|
+
const result = new Path(this.startPoint, newParts);
|
906
|
+
console.assert(result.getEndPoint().eq(result.startPoint));
|
907
|
+
return result;
|
908
|
+
}
|
909
|
+
|
516
910
|
private static mapPathCommand(part: PathCommand, mapping: (point: Point2)=> Point2): PathCommand {
|
517
911
|
switch (part.kind) {
|
518
912
|
case PathCommandType.MoveTo:
|
@@ -562,23 +956,112 @@ export class Path {
|
|
562
956
|
return this.mapPoints(point => affineTransfm.transformVec2(point));
|
563
957
|
}
|
564
958
|
|
959
|
+
/**
|
960
|
+
* @internal
|
961
|
+
*/
|
962
|
+
public closedContainsPoint(point: Point2) {
|
963
|
+
const bbox = this.getExactBBox();
|
964
|
+
if (!bbox.containsPoint(point)) {
|
965
|
+
return false;
|
966
|
+
}
|
967
|
+
|
968
|
+
const pointOutside = point.plus(Vec2.of(bbox.width, 0));
|
969
|
+
const asClosed = this.asClosed();
|
970
|
+
|
971
|
+
const lineToOutside = new LineSegment2(point, pointOutside);
|
972
|
+
return asClosed.intersection(lineToOutside).length % 2 === 1;
|
973
|
+
}
|
974
|
+
|
565
975
|
// Creates a new path by joining [other] to the end of this path
|
566
|
-
public union(
|
976
|
+
public union(
|
977
|
+
other: Path|PathCommand[]|null,
|
978
|
+
|
979
|
+
// allowReverse: true iff reversing other or this is permitted if it means
|
980
|
+
// no moveTo command is necessary when unioning the paths.
|
981
|
+
options: { allowReverse?: boolean } = { allowReverse: true },
|
982
|
+
): Path {
|
567
983
|
if (!other) {
|
568
984
|
return this;
|
569
985
|
}
|
986
|
+
if (Array.isArray(other)) {
|
987
|
+
return new Path(this.startPoint, [...this.parts, ...other]);
|
988
|
+
}
|
989
|
+
|
990
|
+
const thisEnd = this.getEndPoint();
|
991
|
+
|
992
|
+
let newParts: Readonly<PathCommand>[] = [];
|
993
|
+
if (thisEnd.eq(other.startPoint)) {
|
994
|
+
newParts = this.parts.concat(other.parts);
|
995
|
+
} else if (options.allowReverse && this.startPoint.eq(other.getEndPoint())) {
|
996
|
+
return other.union(this, { allowReverse: false });
|
997
|
+
} else if (options.allowReverse && this.startPoint.eq(other.startPoint)) {
|
998
|
+
return this.union(other.reversed(), { allowReverse: false });
|
999
|
+
} else {
|
1000
|
+
newParts = [
|
1001
|
+
...this.parts,
|
1002
|
+
{
|
1003
|
+
kind: PathCommandType.MoveTo,
|
1004
|
+
point: other.startPoint,
|
1005
|
+
},
|
1006
|
+
...other.parts,
|
1007
|
+
];
|
1008
|
+
}
|
1009
|
+
return new Path(this.startPoint, newParts);
|
1010
|
+
}
|
570
1011
|
|
571
|
-
|
572
|
-
|
1012
|
+
/**
|
1013
|
+
* @returns a version of this path with the direction reversed.
|
1014
|
+
*
|
1015
|
+
* Example:
|
1016
|
+
* ```ts,runnable,console
|
1017
|
+
* import {Path} from '@js-draw/math';
|
1018
|
+
* console.log(Path.fromString('m0,0l1,1').reversed()); // -> M1,1 L0,0
|
1019
|
+
* ```
|
1020
|
+
*/
|
1021
|
+
public reversed() {
|
1022
|
+
const newStart = this.getEndPoint();
|
1023
|
+
const newParts: Readonly<PathCommand>[] = [];
|
1024
|
+
let lastPoint: Point2 = this.startPoint;
|
1025
|
+
for (const part of this.parts) {
|
1026
|
+
switch (part.kind) {
|
1027
|
+
case PathCommandType.LineTo:
|
1028
|
+
case PathCommandType.MoveTo:
|
1029
|
+
newParts.push({
|
1030
|
+
kind: part.kind,
|
1031
|
+
point: lastPoint,
|
1032
|
+
});
|
1033
|
+
lastPoint = part.point;
|
1034
|
+
break;
|
1035
|
+
case PathCommandType.CubicBezierTo:
|
1036
|
+
newParts.push({
|
1037
|
+
kind: part.kind,
|
1038
|
+
controlPoint1: part.controlPoint2,
|
1039
|
+
controlPoint2: part.controlPoint1,
|
1040
|
+
endPoint: lastPoint,
|
1041
|
+
});
|
1042
|
+
lastPoint = part.endPoint;
|
1043
|
+
break;
|
1044
|
+
case PathCommandType.QuadraticBezierTo:
|
1045
|
+
newParts.push({
|
1046
|
+
kind: part.kind,
|
1047
|
+
controlPoint: part.controlPoint,
|
1048
|
+
endPoint: lastPoint,
|
1049
|
+
});
|
1050
|
+
lastPoint = part.endPoint;
|
1051
|
+
break;
|
1052
|
+
default:
|
573
1053
|
{
|
574
|
-
|
575
|
-
|
576
|
-
}
|
577
|
-
|
578
|
-
|
1054
|
+
const exhaustivenessCheck: never = part;
|
1055
|
+
return exhaustivenessCheck;
|
1056
|
+
}
|
1057
|
+
}
|
1058
|
+
}
|
1059
|
+
newParts.reverse();
|
1060
|
+
return new Path(newStart, newParts);
|
579
1061
|
}
|
580
1062
|
|
581
|
-
|
1063
|
+
/** Computes and returns the end point of this path */
|
1064
|
+
public getEndPoint() {
|
582
1065
|
if (this.parts.length === 0) {
|
583
1066
|
return this.startPoint;
|
584
1067
|
}
|
@@ -633,10 +1116,12 @@ export class Path {
|
|
633
1116
|
return false;
|
634
1117
|
}
|
635
1118
|
|
636
|
-
|
637
|
-
|
638
|
-
|
639
|
-
|
1119
|
+
/**
|
1120
|
+
* Treats this as a closed path and returns true if part of `rect` is *roughly* within
|
1121
|
+
* this path's interior.
|
1122
|
+
*
|
1123
|
+
* **Note**: Assumes that this is a closed, non-self-intersecting path.
|
1124
|
+
*/
|
640
1125
|
public closedRoughlyIntersects(rect: Rect2): boolean {
|
641
1126
|
if (rect.containsRect(this.bbox)) {
|
642
1127
|
return true;
|
@@ -682,6 +1167,57 @@ export class Path {
|
|
682
1167
|
return false;
|
683
1168
|
}
|
684
1169
|
|
1170
|
+
/** @returns true if all points on this are equivalent to the points on `other` */
|
1171
|
+
public eq(other: Path, tolerance?: number) {
|
1172
|
+
if (other.parts.length !== this.parts.length) {
|
1173
|
+
return false;
|
1174
|
+
}
|
1175
|
+
|
1176
|
+
for (let i = 0; i < this.parts.length; i++) {
|
1177
|
+
const part1 = this.parts[i];
|
1178
|
+
const part2 = other.parts[i];
|
1179
|
+
|
1180
|
+
switch (part1.kind) {
|
1181
|
+
case PathCommandType.LineTo:
|
1182
|
+
case PathCommandType.MoveTo:
|
1183
|
+
if (part1.kind !== part2.kind) {
|
1184
|
+
return false;
|
1185
|
+
} else if(!part1.point.eq(part2.point, tolerance)) {
|
1186
|
+
return false;
|
1187
|
+
}
|
1188
|
+
break;
|
1189
|
+
case PathCommandType.CubicBezierTo:
|
1190
|
+
if (part1.kind !== part2.kind) {
|
1191
|
+
return false;
|
1192
|
+
} else if (
|
1193
|
+
!part1.controlPoint1.eq(part2.controlPoint1, tolerance)
|
1194
|
+
|| !part1.controlPoint2.eq(part2.controlPoint2, tolerance)
|
1195
|
+
|| !part1.endPoint.eq(part2.endPoint, tolerance)
|
1196
|
+
) {
|
1197
|
+
return false;
|
1198
|
+
}
|
1199
|
+
break;
|
1200
|
+
case PathCommandType.QuadraticBezierTo:
|
1201
|
+
if (part1.kind !== part2.kind) {
|
1202
|
+
return false;
|
1203
|
+
} else if (
|
1204
|
+
!part1.controlPoint.eq(part2.controlPoint, tolerance)
|
1205
|
+
|| !part1.endPoint.eq(part2.endPoint, tolerance)
|
1206
|
+
) {
|
1207
|
+
return false;
|
1208
|
+
}
|
1209
|
+
break;
|
1210
|
+
default:
|
1211
|
+
{
|
1212
|
+
const exhaustivenessCheck: never = part1;
|
1213
|
+
return exhaustivenessCheck;
|
1214
|
+
}
|
1215
|
+
}
|
1216
|
+
}
|
1217
|
+
|
1218
|
+
return true;
|
1219
|
+
}
|
1220
|
+
|
685
1221
|
/**
|
686
1222
|
* Returns a path that outlines `rect`.
|
687
1223
|
*
|
@@ -851,10 +1387,8 @@ export class Path {
|
|
851
1387
|
/**
|
852
1388
|
* Create a `Path` from a subset of the SVG path specification.
|
853
1389
|
*
|
854
|
-
*
|
855
|
-
*
|
856
|
-
* - Elliptical arcs are currently unsupported.
|
857
|
-
* - TODO: Support `s`,`t` commands shorthands.
|
1390
|
+
* Currently, this does not support elliptical arcs or `s` and `t` command
|
1391
|
+
* shorthands. See https://github.com/personalizedrefrigerator/js-draw/pull/19.
|
858
1392
|
*
|
859
1393
|
* @example
|
860
1394
|
* ```ts,runnable,console
|
@@ -865,6 +1399,8 @@ export class Path {
|
|
865
1399
|
* ```
|
866
1400
|
*/
|
867
1401
|
public static fromString(pathString: string): Path {
|
1402
|
+
// TODO: Support elliptical arcs, and the `s`, `t` command shorthands.
|
1403
|
+
//
|
868
1404
|
// See the MDN reference:
|
869
1405
|
// https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/d
|
870
1406
|
// and
|
@@ -1052,6 +1588,26 @@ export class Path {
|
|
1052
1588
|
return result;
|
1053
1589
|
}
|
1054
1590
|
|
1591
|
+
public static fromConvexHullOf(points: Point2[]) {
|
1592
|
+
if (points.length === 0) {
|
1593
|
+
return Path.empty;
|
1594
|
+
}
|
1595
|
+
|
1596
|
+
const hull = convexHull2Of(points);
|
1597
|
+
|
1598
|
+
const commands = hull.slice(1).map((p): LinePathCommand => ({
|
1599
|
+
kind: PathCommandType.LineTo,
|
1600
|
+
point: p,
|
1601
|
+
}));
|
1602
|
+
// Close -- connect back to the start
|
1603
|
+
commands.push({
|
1604
|
+
kind: PathCommandType.LineTo,
|
1605
|
+
point: hull[0],
|
1606
|
+
});
|
1607
|
+
|
1608
|
+
return new Path(hull[0], commands);
|
1609
|
+
}
|
1610
|
+
|
1055
1611
|
// @internal TODO: At present, this isn't really an empty path.
|
1056
1612
|
public static empty: Path = new Path(Vec2.zero, []);
|
1057
1613
|
}
|