@js-draw/math 1.11.1 → 1.17.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
}
|