@js-draw/math 1.11.1 → 1.17.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/Vec3.d.ts +21 -0
- package/dist/cjs/Vec3.js +28 -0
- package/dist/cjs/lib.d.ts +2 -2
- package/dist/cjs/lib.js +16 -3
- package/dist/cjs/rounding/cleanUpNumber.d.ts +3 -0
- package/dist/cjs/rounding/cleanUpNumber.js +35 -0
- package/dist/cjs/rounding/constants.d.ts +1 -0
- package/dist/cjs/rounding/constants.js +4 -0
- package/dist/cjs/rounding/getLenAfterDecimal.d.ts +10 -0
- package/dist/cjs/rounding/getLenAfterDecimal.js +30 -0
- package/dist/cjs/rounding/lib.d.ts +1 -0
- package/dist/cjs/rounding/lib.js +5 -0
- package/dist/cjs/{rounding.d.ts → rounding/toRoundedString.d.ts} +1 -3
- package/dist/cjs/rounding/toRoundedString.js +54 -0
- package/dist/cjs/rounding/toStringOfSamePrecision.d.ts +2 -0
- package/dist/cjs/rounding/toStringOfSamePrecision.js +58 -0
- package/dist/cjs/rounding/toStringOfSamePrecision.test.d.ts +1 -0
- package/dist/cjs/shapes/Abstract2DShape.d.ts +3 -0
- package/dist/cjs/shapes/BezierJSWrapper.d.ts +15 -5
- package/dist/cjs/shapes/BezierJSWrapper.js +135 -18
- package/dist/cjs/shapes/LineSegment2.d.ts +34 -5
- package/dist/cjs/shapes/LineSegment2.js +63 -10
- package/dist/cjs/shapes/Parameterized2DShape.d.ts +31 -0
- package/dist/cjs/shapes/Parameterized2DShape.js +15 -0
- package/dist/cjs/shapes/Path.d.ts +40 -6
- package/dist/cjs/shapes/Path.js +181 -22
- package/dist/cjs/shapes/PointShape2D.d.ts +14 -3
- package/dist/cjs/shapes/PointShape2D.js +28 -5
- package/dist/cjs/shapes/QuadraticBezier.d.ts +4 -0
- package/dist/cjs/shapes/QuadraticBezier.js +19 -4
- package/dist/cjs/shapes/Rect2.d.ts +3 -0
- package/dist/cjs/shapes/Rect2.js +4 -1
- package/dist/mjs/Vec3.d.ts +21 -0
- package/dist/mjs/Vec3.mjs +28 -0
- package/dist/mjs/lib.d.ts +2 -2
- package/dist/mjs/lib.mjs +1 -1
- package/dist/mjs/rounding/cleanUpNumber.d.ts +3 -0
- package/dist/mjs/rounding/cleanUpNumber.mjs +31 -0
- package/dist/mjs/rounding/cleanUpNumber.test.d.ts +1 -0
- package/dist/mjs/rounding/constants.d.ts +1 -0
- package/dist/mjs/rounding/constants.mjs +1 -0
- package/dist/mjs/rounding/getLenAfterDecimal.d.ts +10 -0
- package/dist/mjs/rounding/getLenAfterDecimal.mjs +26 -0
- package/dist/mjs/rounding/lib.d.ts +1 -0
- package/dist/mjs/rounding/lib.mjs +1 -0
- package/dist/mjs/{rounding.d.ts → rounding/toRoundedString.d.ts} +1 -3
- package/dist/mjs/rounding/toRoundedString.mjs +47 -0
- package/dist/mjs/rounding/toRoundedString.test.d.ts +1 -0
- package/dist/mjs/rounding/toStringOfSamePrecision.d.ts +2 -0
- package/dist/mjs/rounding/toStringOfSamePrecision.mjs +51 -0
- package/dist/mjs/rounding/toStringOfSamePrecision.test.d.ts +1 -0
- package/dist/mjs/shapes/Abstract2DShape.d.ts +3 -0
- package/dist/mjs/shapes/BezierJSWrapper.d.ts +15 -5
- package/dist/mjs/shapes/BezierJSWrapper.mjs +133 -18
- package/dist/mjs/shapes/LineSegment2.d.ts +34 -5
- package/dist/mjs/shapes/LineSegment2.mjs +63 -10
- package/dist/mjs/shapes/Parameterized2DShape.d.ts +31 -0
- package/dist/mjs/shapes/Parameterized2DShape.mjs +8 -0
- package/dist/mjs/shapes/Path.d.ts +40 -6
- package/dist/mjs/shapes/Path.mjs +175 -16
- package/dist/mjs/shapes/PointShape2D.d.ts +14 -3
- package/dist/mjs/shapes/PointShape2D.mjs +28 -5
- package/dist/mjs/shapes/QuadraticBezier.d.ts +4 -0
- package/dist/mjs/shapes/QuadraticBezier.mjs +19 -4
- package/dist/mjs/shapes/Rect2.d.ts +3 -0
- package/dist/mjs/shapes/Rect2.mjs +4 -1
- package/package.json +5 -5
- package/src/Vec3.test.ts +26 -7
- package/src/Vec3.ts +30 -0
- package/src/lib.ts +3 -1
- package/src/rounding/cleanUpNumber.test.ts +15 -0
- package/src/rounding/cleanUpNumber.ts +38 -0
- package/src/rounding/constants.ts +3 -0
- package/src/rounding/getLenAfterDecimal.ts +29 -0
- package/src/rounding/lib.ts +2 -0
- package/src/rounding/toRoundedString.test.ts +32 -0
- package/src/rounding/toRoundedString.ts +57 -0
- package/src/rounding/toStringOfSamePrecision.test.ts +21 -0
- package/src/rounding/toStringOfSamePrecision.ts +63 -0
- package/src/shapes/Abstract2DShape.ts +3 -0
- package/src/shapes/BezierJSWrapper.ts +154 -14
- package/src/shapes/LineSegment2.test.ts +35 -1
- package/src/shapes/LineSegment2.ts +79 -11
- package/src/shapes/Parameterized2DShape.ts +39 -0
- package/src/shapes/Path.test.ts +63 -3
- package/src/shapes/Path.ts +211 -26
- package/src/shapes/PointShape2D.ts +33 -6
- package/src/shapes/QuadraticBezier.test.ts +48 -12
- package/src/shapes/QuadraticBezier.ts +23 -5
- package/src/shapes/Rect2.ts +4 -1
- package/dist/cjs/rounding.js +0 -146
- package/dist/mjs/rounding.mjs +0 -139
- package/src/rounding.test.ts +0 -65
- package/src/rounding.ts +0 -168
- /package/dist/cjs/{rounding.test.d.ts → rounding/cleanUpNumber.test.d.ts} +0 -0
- /package/dist/{mjs/rounding.test.d.ts → cjs/rounding/toRoundedString.test.d.ts} +0 -0
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { Point2, Vec2 } from '../Vec2';
|
|
2
|
+
import Abstract2DShape from './Abstract2DShape';
|
|
3
|
+
import LineSegment2 from './LineSegment2';
|
|
4
|
+
|
|
5
|
+
/** A 2-dimensional path with parameter interval $t \in [0, 1]$. */
|
|
6
|
+
export abstract class Parameterized2DShape extends Abstract2DShape {
|
|
7
|
+
/** Returns this at a given parameter. $t \in [0, 1]$ */
|
|
8
|
+
abstract at(t: number): Point2;
|
|
9
|
+
|
|
10
|
+
/** Computes the unit normal vector at $t$. */
|
|
11
|
+
abstract normalAt(t: number): Vec2;
|
|
12
|
+
|
|
13
|
+
abstract tangentAt(t: number): Vec2;
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Divides this shape into two separate shapes at parameter value $t$.
|
|
17
|
+
*/
|
|
18
|
+
abstract splitAt(t: number): [ Parameterized2DShape ] | [ Parameterized2DShape, Parameterized2DShape ];
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Returns the nearest point on `this` to `point` and the `parameterValue` at which
|
|
22
|
+
* that point occurs.
|
|
23
|
+
*/
|
|
24
|
+
abstract nearestPointTo(point: Point2): { point: Point2, parameterValue: number };
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Returns the **parameter values** at which `lineSegment` intersects this shape.
|
|
28
|
+
*
|
|
29
|
+
* See also {@link intersectsLineSegment}
|
|
30
|
+
*/
|
|
31
|
+
public abstract argIntersectsLineSegment(lineSegment: LineSegment2): number[];
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
public override intersectsLineSegment(line: LineSegment2): Point2[] {
|
|
35
|
+
return this.argIntersectsLineSegment(line).map(t => this.at(t));
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export default Parameterized2DShape;
|
package/src/shapes/Path.test.ts
CHANGED
|
@@ -60,6 +60,24 @@ describe('Path', () => {
|
|
|
60
60
|
);
|
|
61
61
|
});
|
|
62
62
|
|
|
63
|
+
it.each([
|
|
64
|
+
[ 'm0,0 L1,1', 'M0,0 L1,1', true ],
|
|
65
|
+
[ 'm0,0 L1,1', 'M1,1 L0,0', false ],
|
|
66
|
+
[ 'm0,0 L1,1 Q2,3 4,5', 'M1,1 L0,0', false ],
|
|
67
|
+
[ 'm0,0 L1,1 Q2,3 4,5', 'M1,1 L0,0 Q2,3 4,5', false ],
|
|
68
|
+
[ 'm0,0 L1,1 Q2,3 4,5', 'M0,0 L1,1 Q2,3 4,5', true ],
|
|
69
|
+
[ 'm0,0 L1,1 Q2,3 4,5 C4,5 6,7 8,9', 'M0,0 L1,1 Q2,3 4,5 C4,5 6,7 8,9', true ],
|
|
70
|
+
[ 'm0,0 L1,1 Q2,3 4,5 C4,5 6,7 8,9Z', 'M0,0 L1,1 Q2,3 4,5 C4,5 6,7 8,9', false ],
|
|
71
|
+
[ 'm0,0 L1,1 Q2,3 4,5 C4,5 6,7 8,9', 'M0,0 L1,1 Q2,3 4,5 C4,5 6,7 8,9Z', false ],
|
|
72
|
+
[ 'm0,0 L1,1 Q2,3 4,5 C4,5 6,7 8,9', 'M0,0 L1,1 Q2,3 4,5 C4,5 6,7 8,9.01', false ],
|
|
73
|
+
[ 'm0,0 L1,1 Q2,3 4,5 C4,5 6,7 8,9', 'M0,0 L1,1 Q2,3 4,5 C4,5 6,7.01 8,9', false ],
|
|
74
|
+
[ 'm0,0 L1,1 Q2,3 4,5 C4,5 6,7 8,9', 'M0,0 L1,1 Q2,3 4,5 C4,5.01 6,7 8,9', false ],
|
|
75
|
+
])('.eq should check equality', (path1Str, path2Str, shouldEqual) => {
|
|
76
|
+
expect(Path.fromString(path1Str)).objEq(Path.fromString(path1Str));
|
|
77
|
+
expect(Path.fromString(path2Str)).objEq(Path.fromString(path2Str));
|
|
78
|
+
expect(Path.fromString(path1Str).eq(Path.fromString(path2Str))).toBe(shouldEqual);
|
|
79
|
+
});
|
|
80
|
+
|
|
63
81
|
describe('intersection', () => {
|
|
64
82
|
it('should give all intersections for a path made up of lines', () => {
|
|
65
83
|
const lineStart = Vec2.of(100, 100);
|
|
@@ -179,7 +197,7 @@ describe('Path', () => {
|
|
|
179
197
|
});
|
|
180
198
|
});
|
|
181
199
|
|
|
182
|
-
it('should
|
|
200
|
+
it('should correctly report intersections for a simple Bézier curve path', () => {
|
|
183
201
|
const lineStart = Vec2.zero;
|
|
184
202
|
const path = new Path(lineStart, [
|
|
185
203
|
{
|
|
@@ -196,13 +214,36 @@ describe('Path', () => {
|
|
|
196
214
|
let intersections = path.intersection(
|
|
197
215
|
new LineSegment2(Vec2.of(-1, 0.5), Vec2.of(2, 0.5)), strokeWidth,
|
|
198
216
|
);
|
|
199
|
-
expect(intersections
|
|
217
|
+
expect(intersections).toHaveLength(0);
|
|
200
218
|
|
|
201
219
|
// Should be an intersection when exiting/entering the edge of the stroke
|
|
202
220
|
intersections = path.intersection(
|
|
203
221
|
new LineSegment2(Vec2.of(0, 0.5), Vec2.of(8, 0.5)), strokeWidth,
|
|
204
222
|
);
|
|
205
|
-
expect(intersections
|
|
223
|
+
expect(intersections).toHaveLength(1);
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
it('should correctly report intersections near the cap of a line-like Bézier', () => {
|
|
227
|
+
const path = Path.fromString('M0,0Q14,0 27,0');
|
|
228
|
+
expect(
|
|
229
|
+
path.intersection(
|
|
230
|
+
new LineSegment2(Vec2.of(0, -100), Vec2.of(0, 100)),
|
|
231
|
+
10,
|
|
232
|
+
),
|
|
233
|
+
|
|
234
|
+
// Should have intersections, despite being at the cap of the Bézier
|
|
235
|
+
// curve.
|
|
236
|
+
).toHaveLength(2);
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
it.each([
|
|
240
|
+
[new LineSegment2(Vec2.of(43.5,-12.5), Vec2.of(40.5,24.5)), 0],
|
|
241
|
+
// TODO: The below case is failing. It seems to be a Bezier-js bug though...
|
|
242
|
+
// (The Bézier.js method returns an empty array).
|
|
243
|
+
//[new LineSegment2(Vec2.of(35.5,19.5), Vec2.of(38.5,-17.5)), 0],
|
|
244
|
+
])('should correctly report positive intersections with a line-like Bézier', (line, strokeRadius) => {
|
|
245
|
+
const bezier = Path.fromString('M0,0 Q50,0 100,0');
|
|
246
|
+
expect(bezier.intersection(line, strokeRadius).length).toBeGreaterThan(0);
|
|
206
247
|
});
|
|
207
248
|
});
|
|
208
249
|
|
|
@@ -306,4 +347,23 @@ describe('Path', () => {
|
|
|
306
347
|
expect(strokedRect.startPoint).objEq(lastSegment.point);
|
|
307
348
|
});
|
|
308
349
|
});
|
|
350
|
+
|
|
351
|
+
it.each([
|
|
352
|
+
[ 'm0,0 L1,1', 'M1,1 L0,0' ],
|
|
353
|
+
[ 'm0,0 L1,1', 'M1,1 L0,0' ],
|
|
354
|
+
[ 'M0,0 L1,1 Q2,2 3,3', 'M3,3 Q2,2 1,1 L0,0' ],
|
|
355
|
+
[ 'M0,0 L1,1 Q4,2 5,3 C12,13 10,9 8,7', 'M8,7 C 10,9 12,13 5,3 Q 4,2 1,1 L 0,0' ],
|
|
356
|
+
])('.reversed should reverse paths', (original, expected) => {
|
|
357
|
+
expect(Path.fromString(original).reversed()).objEq(Path.fromString(expected));
|
|
358
|
+
expect(Path.fromString(expected).reversed()).objEq(Path.fromString(original));
|
|
359
|
+
expect(Path.fromString(original).reversed().reversed()).objEq(Path.fromString(original));
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
it.each([
|
|
363
|
+
[ 'm0,0 l1,0', Vec2.of(0, 0), Vec2.of(0, 0) ],
|
|
364
|
+
[ 'm0,0 l1,0', Vec2.of(0.5, 0), Vec2.of(0.5, 0) ],
|
|
365
|
+
[ 'm0,0 Q1,0 1,2', Vec2.of(1, 0), Vec2.of(0.6236, 0.299) ],
|
|
366
|
+
])('.nearestPointTo should return the closest point on a path to the given parameter (case %#)', (path, point, expectedClosest) => {
|
|
367
|
+
expect(Path.fromString(path).nearestPointTo(point).point).objEq(expectedClosest, 0.002);
|
|
368
|
+
});
|
|
309
369
|
});
|
package/src/shapes/Path.ts
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
|
-
import { toRoundedString, toStringOfSamePrecision } from '../rounding';
|
|
2
1
|
import LineSegment2 from './LineSegment2';
|
|
3
2
|
import Mat33 from '../Mat33';
|
|
4
3
|
import Rect2 from './Rect2';
|
|
5
4
|
import { Point2, Vec2 } from '../Vec2';
|
|
6
|
-
import Abstract2DShape from './Abstract2DShape';
|
|
7
5
|
import CubicBezier from './CubicBezier';
|
|
8
6
|
import QuadraticBezier from './QuadraticBezier';
|
|
9
7
|
import PointShape2D from './PointShape2D';
|
|
8
|
+
import toRoundedString from '../rounding/toRoundedString';
|
|
9
|
+
import toStringOfSamePrecision from '../rounding/toStringOfSamePrecision';
|
|
10
|
+
import Parameterized2DShape from './Parameterized2DShape';
|
|
10
11
|
|
|
11
12
|
export enum PathCommandType {
|
|
12
13
|
LineTo,
|
|
@@ -40,17 +41,29 @@ export interface MoveToPathCommand {
|
|
|
40
41
|
|
|
41
42
|
export type PathCommand = CubicBezierPathCommand | QuadraticBezierPathCommand | MoveToPathCommand | LinePathCommand;
|
|
42
43
|
|
|
43
|
-
interface IntersectionResult {
|
|
44
|
+
export interface IntersectionResult {
|
|
44
45
|
// @internal
|
|
45
|
-
curve:
|
|
46
|
+
curve: Parameterized2DShape;
|
|
47
|
+
// @internal
|
|
48
|
+
curveIndex: number;
|
|
46
49
|
|
|
47
|
-
/** @internal @deprecated */
|
|
50
|
+
/** Parameter value for the closest point **on** the path to the intersection. @internal @deprecated */
|
|
48
51
|
parameterValue?: number;
|
|
49
52
|
|
|
50
|
-
|
|
53
|
+
/** Point at which the intersection occured. */
|
|
51
54
|
point: Point2;
|
|
52
55
|
}
|
|
53
56
|
|
|
57
|
+
/**
|
|
58
|
+
* Allows indexing a particular part of a path.
|
|
59
|
+
*
|
|
60
|
+
* @see {@link Path.at} {@link Path.tangentAt}
|
|
61
|
+
*/
|
|
62
|
+
export interface CurveIndexRecord {
|
|
63
|
+
curveIndex: number;
|
|
64
|
+
parameterValue: number;
|
|
65
|
+
}
|
|
66
|
+
|
|
54
67
|
/**
|
|
55
68
|
* Represents a union of lines and curves.
|
|
56
69
|
*/
|
|
@@ -96,16 +109,16 @@ export class Path {
|
|
|
96
109
|
return Rect2.union(...bboxes);
|
|
97
110
|
}
|
|
98
111
|
|
|
99
|
-
private cachedGeometry:
|
|
112
|
+
private cachedGeometry: Parameterized2DShape[]|null = null;
|
|
100
113
|
|
|
101
114
|
// Lazy-loads and returns this path's geometry
|
|
102
|
-
public get geometry():
|
|
115
|
+
public get geometry(): Parameterized2DShape[] {
|
|
103
116
|
if (this.cachedGeometry) {
|
|
104
117
|
return this.cachedGeometry;
|
|
105
118
|
}
|
|
106
119
|
|
|
107
120
|
let startPoint = this.startPoint;
|
|
108
|
-
const geometry:
|
|
121
|
+
const geometry: Parameterized2DShape[] = [];
|
|
109
122
|
|
|
110
123
|
for (const part of this.parts) {
|
|
111
124
|
let exhaustivenessCheck: never;
|
|
@@ -269,7 +282,7 @@ export class Path {
|
|
|
269
282
|
|
|
270
283
|
type DistanceFunction = (point: Point2) => number;
|
|
271
284
|
type DistanceFunctionRecord = {
|
|
272
|
-
part:
|
|
285
|
+
part: Parameterized2DShape,
|
|
273
286
|
bbox: Rect2,
|
|
274
287
|
distFn: DistanceFunction,
|
|
275
288
|
};
|
|
@@ -308,9 +321,9 @@ export class Path {
|
|
|
308
321
|
|
|
309
322
|
// Returns the minimum distance to a part in this stroke, where only parts that the given
|
|
310
323
|
// line could intersect are considered.
|
|
311
|
-
const sdf = (point: Point2): [
|
|
324
|
+
const sdf = (point: Point2): [Parameterized2DShape|null, number] => {
|
|
312
325
|
let minDist = Infinity;
|
|
313
|
-
let minDistPart:
|
|
326
|
+
let minDistPart: Parameterized2DShape|null = null;
|
|
314
327
|
|
|
315
328
|
const uncheckedDistFunctions: DistanceFunctionRecord[] = [];
|
|
316
329
|
|
|
@@ -337,7 +350,7 @@ export class Path {
|
|
|
337
350
|
for (const { part, distFn, bbox } of uncheckedDistFunctions) {
|
|
338
351
|
// Skip if impossible for the distance to the target to be lesser than
|
|
339
352
|
// the current minimum.
|
|
340
|
-
if (!bbox.grownBy(minDist).containsPoint(point)) {
|
|
353
|
+
if (isFinite(minDist) && !bbox.grownBy(minDist).containsPoint(point)) {
|
|
341
354
|
continue;
|
|
342
355
|
}
|
|
343
356
|
|
|
@@ -387,7 +400,7 @@ export class Path {
|
|
|
387
400
|
|
|
388
401
|
const stoppingThreshold = strokeRadius / 1000;
|
|
389
402
|
|
|
390
|
-
// Returns the maximum
|
|
403
|
+
// Returns the maximum parameter value explored
|
|
391
404
|
const raymarchFrom = (
|
|
392
405
|
startPoint: Point2,
|
|
393
406
|
|
|
@@ -445,9 +458,15 @@ export class Path {
|
|
|
445
458
|
if (lastPart && isOnLineSegment && Math.abs(lastDist) < stoppingThreshold) {
|
|
446
459
|
result.push({
|
|
447
460
|
point: currentPoint,
|
|
448
|
-
parameterValue: NaN,
|
|
461
|
+
parameterValue: NaN,// lastPart.nearestPointTo(currentPoint).parameterValue,
|
|
449
462
|
curve: lastPart,
|
|
463
|
+
curveIndex: this.geometry.indexOf(lastPart),
|
|
450
464
|
});
|
|
465
|
+
|
|
466
|
+
// Slightly increase the parameter value to prevent the same point from being
|
|
467
|
+
// added to the results twice.
|
|
468
|
+
const parameterIncrease = strokeRadius / 20 / line.length;
|
|
469
|
+
lastParameter += isFinite(parameterIncrease) ? parameterIncrease : 0;
|
|
451
470
|
}
|
|
452
471
|
|
|
453
472
|
return lastParameter;
|
|
@@ -488,15 +507,20 @@ export class Path {
|
|
|
488
507
|
return [];
|
|
489
508
|
}
|
|
490
509
|
|
|
510
|
+
let index = 0;
|
|
491
511
|
for (const part of this.geometry) {
|
|
492
|
-
const
|
|
512
|
+
const intersections = part.argIntersectsLineSegment(line);
|
|
493
513
|
|
|
494
|
-
|
|
514
|
+
for (const intersection of intersections) {
|
|
495
515
|
result.push({
|
|
496
516
|
curve: part,
|
|
497
|
-
|
|
517
|
+
curveIndex: index,
|
|
518
|
+
point: part.at(intersection),
|
|
519
|
+
parameterValue: intersection,
|
|
498
520
|
});
|
|
499
521
|
}
|
|
522
|
+
|
|
523
|
+
index ++;
|
|
500
524
|
}
|
|
501
525
|
|
|
502
526
|
// If given a non-zero strokeWidth, attempt to raymarch.
|
|
@@ -512,6 +536,47 @@ export class Path {
|
|
|
512
536
|
return result;
|
|
513
537
|
}
|
|
514
538
|
|
|
539
|
+
/**
|
|
540
|
+
* @returns the nearest point on this path to the given `point`.
|
|
541
|
+
*
|
|
542
|
+
* @internal
|
|
543
|
+
* @beta
|
|
544
|
+
*/
|
|
545
|
+
public nearestPointTo(point: Point2): IntersectionResult {
|
|
546
|
+
// Find the closest point on this
|
|
547
|
+
let closestSquareDist = Infinity;
|
|
548
|
+
let closestPartIndex = 0;
|
|
549
|
+
let closestParameterValue = 0;
|
|
550
|
+
let closestPoint: Point2 = this.startPoint;
|
|
551
|
+
|
|
552
|
+
for (let i = 0; i < this.geometry.length; i++) {
|
|
553
|
+
const current = this.geometry[i];
|
|
554
|
+
const nearestPoint = current.nearestPointTo(point);
|
|
555
|
+
const sqareDist = nearestPoint.point.squareDistanceTo(point);
|
|
556
|
+
if (i === 0 || sqareDist < closestSquareDist) {
|
|
557
|
+
closestPartIndex = i;
|
|
558
|
+
closestSquareDist = sqareDist;
|
|
559
|
+
closestParameterValue = nearestPoint.parameterValue;
|
|
560
|
+
closestPoint = nearestPoint.point;
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
return {
|
|
565
|
+
curve: this.geometry[closestPartIndex],
|
|
566
|
+
curveIndex: closestPartIndex,
|
|
567
|
+
parameterValue: closestParameterValue,
|
|
568
|
+
point: closestPoint,
|
|
569
|
+
};
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
public at(index: CurveIndexRecord) {
|
|
573
|
+
return this.geometry[index.curveIndex].at(index.parameterValue);
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
public tangentAt(index: CurveIndexRecord) {
|
|
577
|
+
return this.geometry[index.curveIndex].tangentAt(index.parameterValue);
|
|
578
|
+
}
|
|
579
|
+
|
|
515
580
|
private static mapPathCommand(part: PathCommand, mapping: (point: Point2)=> Point2): PathCommand {
|
|
516
581
|
switch (part.kind) {
|
|
517
582
|
case PathCommandType.MoveTo:
|
|
@@ -562,19 +627,88 @@ export class Path {
|
|
|
562
627
|
}
|
|
563
628
|
|
|
564
629
|
// Creates a new path by joining [other] to the end of this path
|
|
565
|
-
public union(
|
|
630
|
+
public union(
|
|
631
|
+
other: Path|null,
|
|
632
|
+
|
|
633
|
+
// allowReverse: true iff reversing other or this is permitted if it means
|
|
634
|
+
// no moveTo command is necessary when unioning the paths.
|
|
635
|
+
options: { allowReverse?: boolean } = { allowReverse: true },
|
|
636
|
+
): Path {
|
|
566
637
|
if (!other) {
|
|
567
638
|
return this;
|
|
568
639
|
}
|
|
569
640
|
|
|
570
|
-
|
|
571
|
-
|
|
641
|
+
const thisEnd = this.getEndPoint();
|
|
642
|
+
|
|
643
|
+
let newParts: Readonly<PathCommand>[] = [];
|
|
644
|
+
if (thisEnd.eq(other.startPoint)) {
|
|
645
|
+
newParts = this.parts.concat(other.parts);
|
|
646
|
+
} else if (options.allowReverse && this.startPoint.eq(other.getEndPoint())) {
|
|
647
|
+
return other.union(this, { allowReverse: false });
|
|
648
|
+
} else if (options.allowReverse && this.startPoint.eq(other.startPoint)) {
|
|
649
|
+
return this.union(other.reversed(), { allowReverse: false });
|
|
650
|
+
} else {
|
|
651
|
+
newParts = [
|
|
652
|
+
...this.parts,
|
|
653
|
+
{
|
|
654
|
+
kind: PathCommandType.MoveTo,
|
|
655
|
+
point: other.startPoint,
|
|
656
|
+
},
|
|
657
|
+
...other.parts,
|
|
658
|
+
];
|
|
659
|
+
}
|
|
660
|
+
return new Path(this.startPoint, newParts);
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
/**
|
|
664
|
+
* @returns a version of this path with the direction reversed.
|
|
665
|
+
*
|
|
666
|
+
* Example:
|
|
667
|
+
* ```ts,runnable,console
|
|
668
|
+
* import {Path} from '@js-draw/math';
|
|
669
|
+
* console.log(Path.fromString('m0,0l1,1').reversed()); // -> M1,1 L0,0
|
|
670
|
+
* ```
|
|
671
|
+
*/
|
|
672
|
+
public reversed() {
|
|
673
|
+
const newStart = this.getEndPoint();
|
|
674
|
+
const newParts: Readonly<PathCommand>[] = [];
|
|
675
|
+
let lastPoint: Point2 = this.startPoint;
|
|
676
|
+
for (const part of this.parts) {
|
|
677
|
+
switch (part.kind) {
|
|
678
|
+
case PathCommandType.LineTo:
|
|
679
|
+
case PathCommandType.MoveTo:
|
|
680
|
+
newParts.push({
|
|
681
|
+
kind: part.kind,
|
|
682
|
+
point: lastPoint,
|
|
683
|
+
});
|
|
684
|
+
lastPoint = part.point;
|
|
685
|
+
break;
|
|
686
|
+
case PathCommandType.CubicBezierTo:
|
|
687
|
+
newParts.push({
|
|
688
|
+
kind: part.kind,
|
|
689
|
+
controlPoint1: part.controlPoint2,
|
|
690
|
+
controlPoint2: part.controlPoint1,
|
|
691
|
+
endPoint: lastPoint,
|
|
692
|
+
});
|
|
693
|
+
lastPoint = part.endPoint;
|
|
694
|
+
break;
|
|
695
|
+
case PathCommandType.QuadraticBezierTo:
|
|
696
|
+
newParts.push({
|
|
697
|
+
kind: part.kind,
|
|
698
|
+
controlPoint: part.controlPoint,
|
|
699
|
+
endPoint: lastPoint,
|
|
700
|
+
});
|
|
701
|
+
lastPoint = part.endPoint;
|
|
702
|
+
break;
|
|
703
|
+
default:
|
|
572
704
|
{
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
}
|
|
576
|
-
|
|
577
|
-
|
|
705
|
+
const exhaustivenessCheck: never = part;
|
|
706
|
+
return exhaustivenessCheck;
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
newParts.reverse();
|
|
711
|
+
return new Path(newStart, newParts);
|
|
578
712
|
}
|
|
579
713
|
|
|
580
714
|
private getEndPoint() {
|
|
@@ -681,6 +815,57 @@ export class Path {
|
|
|
681
815
|
return false;
|
|
682
816
|
}
|
|
683
817
|
|
|
818
|
+
/** @returns true if all points on this are equivalent to the points on `other` */
|
|
819
|
+
public eq(other: Path, tolerance?: number) {
|
|
820
|
+
if (other.parts.length !== this.parts.length) {
|
|
821
|
+
return false;
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
for (let i = 0; i < this.parts.length; i++) {
|
|
825
|
+
const part1 = this.parts[i];
|
|
826
|
+
const part2 = other.parts[i];
|
|
827
|
+
|
|
828
|
+
switch (part1.kind) {
|
|
829
|
+
case PathCommandType.LineTo:
|
|
830
|
+
case PathCommandType.MoveTo:
|
|
831
|
+
if (part1.kind !== part2.kind) {
|
|
832
|
+
return false;
|
|
833
|
+
} else if(!part1.point.eq(part2.point, tolerance)) {
|
|
834
|
+
return false;
|
|
835
|
+
}
|
|
836
|
+
break;
|
|
837
|
+
case PathCommandType.CubicBezierTo:
|
|
838
|
+
if (part1.kind !== part2.kind) {
|
|
839
|
+
return false;
|
|
840
|
+
} else if (
|
|
841
|
+
!part1.controlPoint1.eq(part2.controlPoint1, tolerance)
|
|
842
|
+
|| !part1.controlPoint2.eq(part2.controlPoint2, tolerance)
|
|
843
|
+
|| !part1.endPoint.eq(part2.endPoint, tolerance)
|
|
844
|
+
) {
|
|
845
|
+
return false;
|
|
846
|
+
}
|
|
847
|
+
break;
|
|
848
|
+
case PathCommandType.QuadraticBezierTo:
|
|
849
|
+
if (part1.kind !== part2.kind) {
|
|
850
|
+
return false;
|
|
851
|
+
} else if (
|
|
852
|
+
!part1.controlPoint.eq(part2.controlPoint, tolerance)
|
|
853
|
+
|| !part1.endPoint.eq(part2.endPoint, tolerance)
|
|
854
|
+
) {
|
|
855
|
+
return false;
|
|
856
|
+
}
|
|
857
|
+
break;
|
|
858
|
+
default:
|
|
859
|
+
{
|
|
860
|
+
const exhaustivenessCheck: never = part1;
|
|
861
|
+
return exhaustivenessCheck;
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
return true;
|
|
867
|
+
}
|
|
868
|
+
|
|
684
869
|
/**
|
|
685
870
|
* Returns a path that outlines `rect`.
|
|
686
871
|
*
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { Point2 } from '../Vec2';
|
|
1
|
+
import { Point2, Vec2 } 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
|
/**
|
|
@@ -9,18 +9,18 @@ import Rect2 from './Rect2';
|
|
|
9
9
|
*
|
|
10
10
|
* Access the internal `Point2` using the `p` property.
|
|
11
11
|
*/
|
|
12
|
-
class PointShape2D extends
|
|
12
|
+
class PointShape2D extends Parameterized2DShape {
|
|
13
13
|
public constructor(public readonly p: Point2) {
|
|
14
14
|
super();
|
|
15
15
|
}
|
|
16
16
|
|
|
17
17
|
public override signedDistance(point: Vec3): number {
|
|
18
|
-
return this.p.
|
|
18
|
+
return this.p.distanceTo(point);
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
-
public override
|
|
21
|
+
public override argIntersectsLineSegment(lineSegment: LineSegment2, epsilon?: number): number[] {
|
|
22
22
|
if (lineSegment.containsPoint(this.p, epsilon)) {
|
|
23
|
-
return [
|
|
23
|
+
return [ 0 ];
|
|
24
24
|
}
|
|
25
25
|
return [ ];
|
|
26
26
|
}
|
|
@@ -28,6 +28,33 @@ class PointShape2D extends Abstract2DShape {
|
|
|
28
28
|
public override getTightBoundingBox(): Rect2 {
|
|
29
29
|
return new Rect2(this.p.x, this.p.y, 0, 0);
|
|
30
30
|
}
|
|
31
|
+
|
|
32
|
+
public override at(_t: number) {
|
|
33
|
+
return this.p;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Returns an arbitrary unit-length vector.
|
|
38
|
+
*/
|
|
39
|
+
public override normalAt(_t: number) {
|
|
40
|
+
// Return a vector that makes sense.
|
|
41
|
+
return Vec2.unitY;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
public override tangentAt(_t: number): Vec3 {
|
|
45
|
+
return Vec2.unitX;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
public override splitAt(_t: number): [PointShape2D] {
|
|
49
|
+
return [this];
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
public override nearestPointTo(_point: Point2) {
|
|
53
|
+
return {
|
|
54
|
+
point: this.p,
|
|
55
|
+
parameterValue: 0,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
31
58
|
}
|
|
32
59
|
|
|
33
60
|
export default PointShape2D;
|
|
@@ -2,13 +2,12 @@ import { Vec2 } from '../Vec2';
|
|
|
2
2
|
import QuadraticBezier from './QuadraticBezier';
|
|
3
3
|
|
|
4
4
|
describe('QuadraticBezier', () => {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
];
|
|
5
|
+
test.each([
|
|
6
|
+
new QuadraticBezier(Vec2.zero, Vec2.of(10, 0), Vec2.of(20, 0)),
|
|
7
|
+
new QuadraticBezier(Vec2.of(-10, 0), Vec2.of(2, 10), Vec2.of(20, 0)),
|
|
8
|
+
new QuadraticBezier(Vec2.of(0, 0), Vec2.of(4, -10), Vec2.of(20, 60)),
|
|
9
|
+
new QuadraticBezier(Vec2.of(0, 0), Vec2.of(4, -10), Vec2.of(-20, 60)),
|
|
10
|
+
])('approxmiateDistance should approximately return the distance to the curve (%s)', (curve) => {
|
|
12
11
|
const testPoints = [
|
|
13
12
|
Vec2.of(1, 1),
|
|
14
13
|
Vec2.of(-1, 1),
|
|
@@ -18,13 +17,50 @@ describe('QuadraticBezier', () => {
|
|
|
18
17
|
Vec2.of(5, 0),
|
|
19
18
|
];
|
|
20
19
|
|
|
20
|
+
for (const point of testPoints) {
|
|
21
|
+
const actualDist = curve.distance(point);
|
|
22
|
+
const approxDist = curve.approximateDistance(point);
|
|
23
|
+
|
|
24
|
+
expect(approxDist).toBeGreaterThan(actualDist * 0.6 - 0.25);
|
|
25
|
+
expect(approxDist).toBeLessThan(actualDist * 1.5 + 2.6);
|
|
26
|
+
}
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
test.each([
|
|
30
|
+
[ new QuadraticBezier(Vec2.zero, Vec2.unitX, Vec2.unitY), Vec2.zero, 0 ],
|
|
31
|
+
[ new QuadraticBezier(Vec2.zero, Vec2.unitX, Vec2.unitY), Vec2.unitY, 1 ],
|
|
32
|
+
|
|
33
|
+
[ new QuadraticBezier(Vec2.zero, Vec2.of(0.5, 0), Vec2.of(1, 0)), Vec2.of(0.4, 0), 0.4],
|
|
34
|
+
[ new QuadraticBezier(Vec2.zero, Vec2.of(0, 0.5), Vec2.of(0, 1)), Vec2.of(0, 0.4), 0.4],
|
|
35
|
+
[ new QuadraticBezier(Vec2.zero, Vec2.unitX, Vec2.unitY), Vec2.unitX, 0.42514 ],
|
|
36
|
+
|
|
37
|
+
// Should not return an out-of-range parameter
|
|
38
|
+
[ new QuadraticBezier(Vec2.zero, Vec2.of(0, 0.5), Vec2.unitY), Vec2.of(0, -1000), 0 ],
|
|
39
|
+
[ new QuadraticBezier(Vec2.zero, Vec2.of(0, 0.5), Vec2.unitY), Vec2.of(0, 1000), 1 ],
|
|
40
|
+
])('nearestPointTo should return the nearest point and parameter value on %s to %s', (bezier, point, expectedParameter) => {
|
|
41
|
+
const nearest = bezier.nearestPointTo(point);
|
|
42
|
+
expect(nearest.parameterValue).toBeCloseTo(expectedParameter, 0.0001);
|
|
43
|
+
expect(nearest.point).objEq(bezier.at(nearest.parameterValue));
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
test('.normalAt should return a unit normal vector at the given parameter value', () => {
|
|
47
|
+
const curves = [
|
|
48
|
+
new QuadraticBezier(Vec2.zero, Vec2.unitY, Vec2.unitY.times(2)),
|
|
49
|
+
new QuadraticBezier(Vec2.zero, Vec2.unitX, Vec2.unitY),
|
|
50
|
+
new QuadraticBezier(Vec2.zero, Vec2.unitX, Vec2.unitY.times(-2)),
|
|
51
|
+
new QuadraticBezier(Vec2.of(2, 3), Vec2.of(4, 5.1), Vec2.of(6, 7)),
|
|
52
|
+
new QuadraticBezier(Vec2.of(2, 3), Vec2.of(100, 1000), Vec2.unitY.times(-2)),
|
|
53
|
+
];
|
|
54
|
+
|
|
21
55
|
for (const curve of curves) {
|
|
22
|
-
for (
|
|
23
|
-
const
|
|
24
|
-
|
|
56
|
+
for (let t = 0; t < 1; t += 0.1) {
|
|
57
|
+
const normal = curve.normalAt(t);
|
|
58
|
+
expect(normal.length()).toBe(1);
|
|
59
|
+
|
|
60
|
+
const tangentApprox = curve.at(t + 0.001).minus(curve.at(t - 0.001));
|
|
25
61
|
|
|
26
|
-
|
|
27
|
-
expect(
|
|
62
|
+
// The tangent vector should be perpindicular to the normal
|
|
63
|
+
expect(tangentApprox.dot(normal)).toBeCloseTo(0);
|
|
28
64
|
}
|
|
29
65
|
}
|
|
30
66
|
});
|
|
@@ -30,10 +30,19 @@ export class QuadraticBezier extends BezierJSWrapper {
|
|
|
30
30
|
return -2 * p0 + 2 * p1 + 2 * t * (p0 - 2 * p1 + p2);
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
+
private static secondDerivativeComponentAt(t: number, p0: number, p1: number, p2: number) {
|
|
34
|
+
return 2 * (p0 - 2 * p1 + p2);
|
|
35
|
+
}
|
|
36
|
+
|
|
33
37
|
/**
|
|
34
38
|
* @returns the curve evaluated at `t`.
|
|
39
|
+
*
|
|
40
|
+
* `t` should be a number in `[0, 1]`.
|
|
35
41
|
*/
|
|
36
42
|
public override at(t: number): Point2 {
|
|
43
|
+
if (t === 0) return this.p0;
|
|
44
|
+
if (t === 1) return this.p2;
|
|
45
|
+
|
|
37
46
|
const p0 = this.p0;
|
|
38
47
|
const p1 = this.p1;
|
|
39
48
|
const p2 = this.p2;
|
|
@@ -53,6 +62,16 @@ export class QuadraticBezier extends BezierJSWrapper {
|
|
|
53
62
|
);
|
|
54
63
|
}
|
|
55
64
|
|
|
65
|
+
public override secondDerivativeAt(t: number): Point2 {
|
|
66
|
+
const p0 = this.p0;
|
|
67
|
+
const p1 = this.p1;
|
|
68
|
+
const p2 = this.p2;
|
|
69
|
+
return Vec2.of(
|
|
70
|
+
QuadraticBezier.secondDerivativeComponentAt(t, p0.x, p1.x, p2.x),
|
|
71
|
+
QuadraticBezier.secondDerivativeComponentAt(t, p0.y, p1.y, p2.y),
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
|
|
56
75
|
public override normal(t: number): Vec2 {
|
|
57
76
|
const tangent = this.derivativeAt(t);
|
|
58
77
|
return tangent.orthog().normalized();
|
|
@@ -126,11 +145,10 @@ export class QuadraticBezier extends BezierJSWrapper {
|
|
|
126
145
|
|
|
127
146
|
const at1 = this.at(min1);
|
|
128
147
|
const at2 = this.at(min2);
|
|
129
|
-
const sqrDist1 = at1.
|
|
130
|
-
const sqrDist2 = at2.
|
|
131
|
-
const sqrDist3 = this.at(0).
|
|
132
|
-
const sqrDist4 = this.at(1).
|
|
133
|
-
|
|
148
|
+
const sqrDist1 = at1.squareDistanceTo(point);
|
|
149
|
+
const sqrDist2 = at2.squareDistanceTo(point);
|
|
150
|
+
const sqrDist3 = this.at(0).squareDistanceTo(point);
|
|
151
|
+
const sqrDist4 = this.at(1).squareDistanceTo(point);
|
|
134
152
|
|
|
135
153
|
return Math.sqrt(Math.min(sqrDist1, sqrDist2, sqrDist3, sqrDist4));
|
|
136
154
|
}
|