@js-draw/math 1.17.0 → 1.18.0
Sign up to get free protection for your applications and to get access to all the features.
- package/dist/cjs/Mat33.js +6 -1
- package/dist/cjs/Vec3.d.ts +2 -1
- package/dist/cjs/Vec3.js +5 -7
- package/dist/cjs/lib.d.ts +2 -1
- package/dist/cjs/lib.js +5 -1
- package/dist/cjs/shapes/BezierJSWrapper.d.ts +4 -0
- package/dist/cjs/shapes/BezierJSWrapper.js +35 -0
- package/dist/cjs/shapes/LineSegment2.d.ts +11 -0
- package/dist/cjs/shapes/LineSegment2.js +26 -1
- package/dist/cjs/shapes/Parameterized2DShape.d.ts +6 -1
- package/dist/cjs/shapes/Parameterized2DShape.js +6 -1
- package/dist/cjs/shapes/Path.d.ts +96 -12
- package/dist/cjs/shapes/Path.js +338 -15
- package/dist/cjs/shapes/QuadraticBezier.d.ts +2 -3
- package/dist/cjs/shapes/QuadraticBezier.js +2 -3
- package/dist/cjs/shapes/Rect2.d.ts +6 -1
- package/dist/cjs/shapes/Rect2.js +5 -1
- 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 +2 -1
- package/dist/mjs/Vec3.mjs +5 -7
- package/dist/mjs/lib.d.ts +2 -1
- package/dist/mjs/lib.mjs +2 -1
- package/dist/mjs/shapes/BezierJSWrapper.d.ts +4 -0
- package/dist/mjs/shapes/BezierJSWrapper.mjs +35 -0
- package/dist/mjs/shapes/LineSegment2.d.ts +11 -0
- package/dist/mjs/shapes/LineSegment2.mjs +26 -1
- package/dist/mjs/shapes/Parameterized2DShape.d.ts +6 -1
- package/dist/mjs/shapes/Parameterized2DShape.mjs +6 -1
- package/dist/mjs/shapes/Path.d.ts +96 -12
- package/dist/mjs/shapes/Path.mjs +335 -14
- package/dist/mjs/shapes/QuadraticBezier.d.ts +2 -3
- package/dist/mjs/shapes/QuadraticBezier.mjs +2 -3
- package/dist/mjs/shapes/Rect2.d.ts +6 -1
- package/dist/mjs/shapes/Rect2.mjs +5 -1
- 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 +2 -2
- package/src/Mat33.ts +8 -2
- package/src/Vec3.test.ts +16 -0
- package/src/Vec3.ts +7 -8
- package/src/lib.ts +3 -0
- package/src/shapes/BezierJSWrapper.ts +41 -0
- package/src/shapes/LineSegment2.test.ts +26 -0
- package/src/shapes/LineSegment2.ts +31 -1
- package/src/shapes/Parameterized2DShape.ts +6 -1
- package/src/shapes/Path.test.ts +173 -5
- package/src/shapes/Path.ts +390 -18
- package/src/shapes/QuadraticBezier.test.ts +21 -0
- package/src/shapes/QuadraticBezier.ts +2 -3
- package/src/shapes/Rect2.ts +6 -2
- package/src/utils/convexHull2Of.test.ts +43 -0
- package/src/utils/convexHull2Of.ts +71 -0
package/src/Vec3.ts
CHANGED
@@ -244,7 +244,8 @@ export class Vec3 {
|
|
244
244
|
* Returns a vector with each component acted on by `fn`.
|
245
245
|
*
|
246
246
|
* @example
|
247
|
-
* ```
|
247
|
+
* ```ts,runnable,console
|
248
|
+
* import { Vec3 } from '@js-draw/math';
|
248
249
|
* console.log(Vec3.of(1, 2, 3).map(val => val + 1)); // → Vec(2, 3, 4)
|
249
250
|
* ```
|
250
251
|
*/
|
@@ -272,13 +273,11 @@ export class Vec3 {
|
|
272
273
|
* ```
|
273
274
|
*/
|
274
275
|
public eq(other: Vec3, fuzz: number = 1e-10): boolean {
|
275
|
-
|
276
|
-
|
277
|
-
|
278
|
-
|
279
|
-
|
280
|
-
|
281
|
-
return true;
|
276
|
+
return (
|
277
|
+
Math.abs(other.x - this.x) <= fuzz
|
278
|
+
&& Math.abs(other.y - this.y) <= fuzz
|
279
|
+
&& Math.abs(other.z - this.z) <= fuzz
|
280
|
+
);
|
282
281
|
}
|
283
282
|
|
284
283
|
public toString(): string {
|
package/src/lib.ts
CHANGED
@@ -23,6 +23,8 @@ export {
|
|
23
23
|
|
24
24
|
IntersectionResult as PathIntersectionResult,
|
25
25
|
CurveIndexRecord as PathCurveIndex,
|
26
|
+
stepCurveIndexBy as stepPathIndexBy,
|
27
|
+
compareCurveIndices as comparePathIndices,
|
26
28
|
PathCommandType,
|
27
29
|
PathCommand,
|
28
30
|
LinePathCommand,
|
@@ -31,6 +33,7 @@ export {
|
|
31
33
|
CubicBezierPathCommand,
|
32
34
|
} from './shapes/Path';
|
33
35
|
export { Rect2 } from './shapes/Rect2';
|
36
|
+
export { Parameterized2DShape } from './shapes/Parameterized2DShape';
|
34
37
|
export { QuadraticBezier } from './shapes/QuadraticBezier';
|
35
38
|
export { Abstract2DShape } from './shapes/Abstract2DShape';
|
36
39
|
|
@@ -87,6 +87,18 @@ export abstract class BezierJSWrapper extends Parameterized2DShape {
|
|
87
87
|
}
|
88
88
|
|
89
89
|
public override argIntersectsLineSegment(line: LineSegment2): number[] {
|
90
|
+
// Bezier-js has a bug when all control points of a Bezier curve lie on
|
91
|
+
// a line. Our solution involves converting the Bezier into a line, then
|
92
|
+
// finding the parameter value that produced the intersection.
|
93
|
+
//
|
94
|
+
// TODO: This is unnecessarily slow. A better solution would be to fix
|
95
|
+
// the bug upstream.
|
96
|
+
const asLine = LineSegment2.ofSmallestContainingPoints(this.getPoints());
|
97
|
+
if (asLine) {
|
98
|
+
const intersection = asLine.intersectsLineSegment(line);
|
99
|
+
return intersection.map(p => this.nearestPointTo(p).parameterValue);
|
100
|
+
}
|
101
|
+
|
90
102
|
const bezier = this.getBezier();
|
91
103
|
|
92
104
|
return bezier.intersects(line).map(t => {
|
@@ -186,6 +198,8 @@ export abstract class BezierJSWrapper extends Parameterized2DShape {
|
|
186
198
|
|
187
199
|
const iterate = () => {
|
188
200
|
const slope = secondDerivativeAt(t);
|
201
|
+
if (slope === 0) return;
|
202
|
+
|
189
203
|
// We intersect a line through the point on f'(t) at t with the x-axis:
|
190
204
|
// y = m(x - x₀) + y₀
|
191
205
|
// ⇒ x - x₀ = (y - y₀) / m
|
@@ -211,6 +225,33 @@ export abstract class BezierJSWrapper extends Parameterized2DShape {
|
|
211
225
|
return { parameterValue: t, point: this.at(t) };
|
212
226
|
}
|
213
227
|
|
228
|
+
public intersectsBezier(other: BezierJSWrapper) {
|
229
|
+
const intersections = this.getBezier().intersects(other.getBezier()) as (string[] | null | undefined);
|
230
|
+
if (!intersections || intersections.length === 0) {
|
231
|
+
return [];
|
232
|
+
}
|
233
|
+
|
234
|
+
const result = [];
|
235
|
+
for (const intersection of intersections) {
|
236
|
+
// From http://pomax.github.io/bezierjs/#intersect-curve,
|
237
|
+
// .intersects returns an array of 't1/t2' pairs, where curve1.at(t1) gives the point.
|
238
|
+
const match = /^([-0-9.eE]+)\/([-0-9.eE]+)$/.exec(intersection);
|
239
|
+
|
240
|
+
if (!match) {
|
241
|
+
throw new Error(
|
242
|
+
`Incorrect format returned by .intersects: ${intersections} should be array of "number/number"!`
|
243
|
+
);
|
244
|
+
}
|
245
|
+
|
246
|
+
const t = parseFloat(match[1]);
|
247
|
+
result.push({
|
248
|
+
parameterValue: t,
|
249
|
+
point: this.at(t),
|
250
|
+
});
|
251
|
+
}
|
252
|
+
return result;
|
253
|
+
}
|
254
|
+
|
214
255
|
public override toString() {
|
215
256
|
return `Bézier(${this.getPoints().map(point => point.toString()).join(', ')})`;
|
216
257
|
}
|
@@ -74,6 +74,14 @@ describe('Line2', () => {
|
|
74
74
|
expect(line2.intersection(line1)).toBeNull();
|
75
75
|
});
|
76
76
|
|
77
|
+
it('(9.559000000000001, 11.687)->(9.559, 11.67673) should intersect (9.56069, 11.68077)->(9.55719, 11.68077)', () => {
|
78
|
+
// Points taken from an issue observed in the editor.
|
79
|
+
const l1 = new LineSegment2(Vec2.of(9.559000000000001, 11.687), Vec2.of(9.559, 11.67673));
|
80
|
+
const l2 = new LineSegment2(Vec2.of(9.56069, 11.68077), Vec2.of(9.55719, 11.68077));
|
81
|
+
expect(l2.intersects(l1)).toBe(true);
|
82
|
+
expect(l1.intersects(l2)).toBe(true);
|
83
|
+
});
|
84
|
+
|
77
85
|
it('Closest point to (0,0) on the line x = 1 should be (1,0)', () => {
|
78
86
|
const line = new LineSegment2(Vec2.of(1, 100), Vec2.of(1, -100));
|
79
87
|
expect(line.closestPointTo(Vec2.zero)).objEq(Vec2.of(1, 0));
|
@@ -130,4 +138,22 @@ describe('Line2', () => {
|
|
130
138
|
expect(new LineSegment2(Vec2.zero, Vec2.unitX)).objEq(new LineSegment2(Vec2.unitX, Vec2.zero));
|
131
139
|
expect(new LineSegment2(Vec2.zero, Vec2.unitX)).not.objEq(new LineSegment2(Vec2.unitX, Vec2.zero), { ignoreDirection: false });
|
132
140
|
});
|
141
|
+
|
142
|
+
it('should support creating from a collection of points', () => {
|
143
|
+
expect(LineSegment2.ofSmallestContainingPoints([])).toBeNull();
|
144
|
+
expect(LineSegment2.ofSmallestContainingPoints([Vec2.of(1, 1)])).toBeNull();
|
145
|
+
expect(LineSegment2.ofSmallestContainingPoints(
|
146
|
+
[Vec2.of(1, 1), Vec2.of(1, 2), Vec2.of(3, 3)]
|
147
|
+
)).toBeNull();
|
148
|
+
|
149
|
+
expect(LineSegment2.ofSmallestContainingPoints(
|
150
|
+
[Vec2.of(1, 1), Vec2.of(1, 2)]
|
151
|
+
)).objEq(new LineSegment2(Vec2.of(1, 1), Vec2.of(1, 2)));
|
152
|
+
expect(LineSegment2.ofSmallestContainingPoints(
|
153
|
+
[Vec2.of(1, 1), Vec2.of(2, 2), Vec2.of(3, 3)]
|
154
|
+
)).objEq(new LineSegment2(Vec2.of(1, 1), Vec2.of(3, 3)));
|
155
|
+
expect(LineSegment2.ofSmallestContainingPoints(
|
156
|
+
[Vec2.of(3, 3), Vec2.of(2, 2), Vec2.of(2.4, 2.4), Vec2.of(3, 3)]
|
157
|
+
)).objEq(new LineSegment2(Vec2.of(2, 2), Vec2.of(3, 3)));
|
158
|
+
});
|
133
159
|
});
|
@@ -46,6 +46,31 @@ export class LineSegment2 extends Parameterized2DShape {
|
|
46
46
|
}
|
47
47
|
}
|
48
48
|
|
49
|
+
/**
|
50
|
+
* Returns the smallest line segment that contains all points in `points`, or `null`
|
51
|
+
* if no such line segment exists.
|
52
|
+
*
|
53
|
+
* @example
|
54
|
+
* ```ts,runnable
|
55
|
+
* import {LineSegment2, Vec2} from '@js-draw/math';
|
56
|
+
* console.log(LineSegment2.ofSmallestContainingPoints([Vec2.of(1, 0), Vec2.of(0, 1)]));
|
57
|
+
* ```
|
58
|
+
*/
|
59
|
+
public static ofSmallestContainingPoints(points: readonly Point2[]) {
|
60
|
+
if (points.length <= 1) return null;
|
61
|
+
|
62
|
+
const sorted = [...points].sort((a, b) => a.x !== b.x ? a.x - b.x : a.y - b.y);
|
63
|
+
const line = new LineSegment2(sorted[0], sorted[sorted.length - 1]);
|
64
|
+
|
65
|
+
for (const point of sorted) {
|
66
|
+
if (!line.containsPoint(point)) {
|
67
|
+
return null;
|
68
|
+
}
|
69
|
+
}
|
70
|
+
|
71
|
+
return line;
|
72
|
+
}
|
73
|
+
|
49
74
|
// Accessors to make LineSegment2 compatible with bezier-js's
|
50
75
|
// interface
|
51
76
|
|
@@ -139,7 +164,11 @@ export class LineSegment2 extends Parameterized2DShape {
|
|
139
164
|
// = ((o₁ᵧ - o₂ᵧ)((d₁ₓd₂ₓ)) + (d₂ᵧd₁ₓ)(o₂ₓ) - (d₁ᵧd₂ₓ)(o₁ₓ))/(d₂ᵧd₁ₓ - d₁ᵧd₂ₓ)
|
140
165
|
// ⇒ y = o₁ᵧ + d₁ᵧ · (x - o₁ₓ) / d₁ₓ = ...
|
141
166
|
let resultPoint, resultT;
|
142
|
-
|
167
|
+
|
168
|
+
// Consider very-near-vertical lines to be vertical --- not doing so can lead to
|
169
|
+
// precision error when dividing by this.direction.x.
|
170
|
+
const small = 4e-13;
|
171
|
+
if (Math.abs(this.direction.x) < small) {
|
143
172
|
// Vertical line: Where does the other have x = this.point1.x?
|
144
173
|
// x = o₁ₓ = o₂ₓ + d₂ₓ · (y - o₂ᵧ) / d₂ᵧ
|
145
174
|
// ⇒ (o₁ₓ - o₂ₓ)(d₂ᵧ/d₂ₓ) + o₂ᵧ = y
|
@@ -184,6 +213,7 @@ export class LineSegment2 extends Parameterized2DShape {
|
|
184
213
|
const resultToP2 = resultPoint.distanceTo(this.point2);
|
185
214
|
const resultToP3 = resultPoint.distanceTo(other.point1);
|
186
215
|
const resultToP4 = resultPoint.distanceTo(other.point2);
|
216
|
+
|
187
217
|
if (resultToP1 > this.length
|
188
218
|
|| resultToP2 > this.length
|
189
219
|
|| resultToP3 > other.length
|
@@ -2,7 +2,12 @@ import { Point2, Vec2 } from '../Vec2';
|
|
2
2
|
import Abstract2DShape from './Abstract2DShape';
|
3
3
|
import LineSegment2 from './LineSegment2';
|
4
4
|
|
5
|
-
/**
|
5
|
+
/**
|
6
|
+
* A 2-dimensional path with parameter interval $t \in [0, 1]$.
|
7
|
+
*
|
8
|
+
* **Note:** Avoid extending this class outside of `js-draw` --- new abstract methods
|
9
|
+
* may be added between minor versions.
|
10
|
+
*/
|
6
11
|
export abstract class Parameterized2DShape extends Abstract2DShape {
|
7
12
|
/** Returns this at a given parameter. $t \in [0, 1]$ */
|
8
13
|
abstract at(t: number): Point2;
|
package/src/shapes/Path.test.ts
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
import LineSegment2 from './LineSegment2';
|
2
|
-
import Path, { PathCommandType } from './Path';
|
2
|
+
import Path, { CurveIndexRecord, PathCommandType } from './Path';
|
3
3
|
import Rect2 from './Rect2';
|
4
|
-
import { Vec2 } from '../Vec2';
|
4
|
+
import { Point2, Vec2 } from '../Vec2';
|
5
5
|
import CubicBezier from './CubicBezier';
|
6
6
|
import QuadraticBezier from './QuadraticBezier';
|
7
7
|
|
@@ -238,13 +238,22 @@ describe('Path', () => {
|
|
238
238
|
|
239
239
|
it.each([
|
240
240
|
[new LineSegment2(Vec2.of(43.5,-12.5), Vec2.of(40.5,24.5)), 0],
|
241
|
-
|
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],
|
241
|
+
[new LineSegment2(Vec2.of(35.5,19.5), Vec2.of(38.5,-17.5)), 0],
|
244
242
|
])('should correctly report positive intersections with a line-like Bézier', (line, strokeRadius) => {
|
245
243
|
const bezier = Path.fromString('M0,0 Q50,0 100,0');
|
246
244
|
expect(bezier.intersection(line, strokeRadius).length).toBeGreaterThan(0);
|
247
245
|
});
|
246
|
+
|
247
|
+
it('should handle near-vertical lines', () => {
|
248
|
+
const intersections = Path.fromString('M0,0 Q50,0 100,0').intersection(new LineSegment2(Vec2.of(44, -12), Vec2.of(39, 25)));
|
249
|
+
expect(intersections).toHaveLength(1);
|
250
|
+
});
|
251
|
+
|
252
|
+
it('should handle single-point strokes', () => {
|
253
|
+
const stroke = new Path(Vec2.zero, []);
|
254
|
+
expect(stroke.intersection(new LineSegment2(Vec2.of(-2, -20), Vec2.of(-2, -1)), 1)).toHaveLength(0);
|
255
|
+
expect(stroke.intersection(new LineSegment2(Vec2.of(-2, -2), Vec2.of(2, 2)), 1)).toHaveLength(2);
|
256
|
+
});
|
248
257
|
});
|
249
258
|
|
250
259
|
describe('polylineApproximation', () => {
|
@@ -348,6 +357,146 @@ describe('Path', () => {
|
|
348
357
|
});
|
349
358
|
});
|
350
359
|
|
360
|
+
describe('splitAt', () => {
|
361
|
+
it.each([
|
362
|
+
2, 3, 4, 5,
|
363
|
+
])('should split a line into %d sections', (numSections) => {
|
364
|
+
const path = Path.fromString('m0,0 l1,0');
|
365
|
+
|
366
|
+
const splitIndices: CurveIndexRecord[] = [];
|
367
|
+
for (let i = 0; i < numSections; i++) {
|
368
|
+
splitIndices.push({ curveIndex: 0, parameterValue: (i + 1) / (numSections + 1) });
|
369
|
+
}
|
370
|
+
const split = path.splitAt(splitIndices);
|
371
|
+
|
372
|
+
expect(split).toHaveLength(numSections + 1);
|
373
|
+
expect(split[numSections].getEndPoint()).objEq(Vec2.unitX);
|
374
|
+
for (let i = 0; i < numSections; i ++) {
|
375
|
+
expect(split[i].geometry).toHaveLength(1);
|
376
|
+
const geom = split[i].geometry[0] as LineSegment2;
|
377
|
+
expect(geom.p1.y).toBeCloseTo(0);
|
378
|
+
expect(geom.p1.x).toBeCloseTo(i / (numSections + 1));
|
379
|
+
expect(geom.p2.y).toBeCloseTo(0);
|
380
|
+
expect(geom.p2.x).toBeCloseTo((i + 1) / (numSections + 1));
|
381
|
+
}
|
382
|
+
});
|
383
|
+
|
384
|
+
it('should handle the case where the first division is at the beginning of the path', () => {
|
385
|
+
const path = Path.fromString('m0,0 l1,0');
|
386
|
+
const beginningSplit = path.splitAt({ curveIndex: 0, parameterValue: 0 });
|
387
|
+
expect(beginningSplit).toHaveLength(1);
|
388
|
+
|
389
|
+
const endSplit = path.splitAt({ curveIndex: 0, parameterValue: 1 });
|
390
|
+
expect(endSplit).toHaveLength(1);
|
391
|
+
|
392
|
+
expect(beginningSplit[0]).objEq(path);
|
393
|
+
expect(beginningSplit[0]).objEq(endSplit[0]);
|
394
|
+
});
|
395
|
+
});
|
396
|
+
|
397
|
+
describe('splitNear', () => {
|
398
|
+
it('should divide a line in half', () => {
|
399
|
+
const path = Path.fromString('m0,0l8,0');
|
400
|
+
const split = path.splitNear(Vec2.of(4, 0));
|
401
|
+
expect(split).toHaveLength(2);
|
402
|
+
expect(split[0].toString()).toBe('M0,0L4,0');
|
403
|
+
expect(split[1]!.toString()).toBe('M4,0L8,0');
|
404
|
+
});
|
405
|
+
|
406
|
+
it('should divide a polyline into parts', () => {
|
407
|
+
const path = Path.fromString('m0,0L8,0L8,8');
|
408
|
+
const split = path.splitNear(Vec2.of(8, 4));
|
409
|
+
expect(split).toHaveLength(2);
|
410
|
+
expect(split[0].toString()).toBe('M0,0L8,0L8,4');
|
411
|
+
expect(split[1]!.toString()).toBe('M8,4L8,8');
|
412
|
+
});
|
413
|
+
|
414
|
+
it('should divide a quadratic Bézier in half', () => {
|
415
|
+
const path = Path.fromString('m0,0 Q4,0 8,0');
|
416
|
+
const split = path.splitNear(Vec2.of(4, 0));
|
417
|
+
expect(split).toHaveLength(2);
|
418
|
+
expect(split[0].toString()).toBe('M0,0Q2,0 4,0');
|
419
|
+
expect(split[1]!.toString()).toBe('M4,0Q6,0 8,0');
|
420
|
+
});
|
421
|
+
|
422
|
+
it('should divide two quadratic Béziers half', () => {
|
423
|
+
const path = Path.fromString('m0,0 Q4,0 8,0 Q8,4 8,8');
|
424
|
+
const split = path.splitNear(Vec2.of(8, 4));
|
425
|
+
expect(split).toHaveLength(2);
|
426
|
+
expect(split[0].toString()).toBe('M0,0Q4,0 8,0Q8,2 8,4');
|
427
|
+
expect(split[1]!.toString()).toBe('M8,4Q8,6 8,8');
|
428
|
+
});
|
429
|
+
|
430
|
+
it.each([
|
431
|
+
{
|
432
|
+
original: 'm0,0 Q4,0 8,0 Q8,4 8,8',
|
433
|
+
near: Vec2.of(8, 4),
|
434
|
+
map: (p: Point2) => p.plus(Vec2.of(1, 1)),
|
435
|
+
expected: [ 'M0,0Q4,0 8,0Q9,3 9,5', 'M9,5Q9,7 9,9' ],
|
436
|
+
},
|
437
|
+
{
|
438
|
+
original: 'm0,0 L0,10',
|
439
|
+
near: Vec2.of(0, 5),
|
440
|
+
map: (p: Point2) => p.plus(Vec2.of(100, 0)),
|
441
|
+
expected: [ 'M0,0L100,5', 'M100,5L0,10' ],
|
442
|
+
},
|
443
|
+
{
|
444
|
+
// Tested using SVG data similar to:
|
445
|
+
// <path d="m1,1 C1,2 2,10 4,4 C5,0 9,3 7,7" fill="none" stroke="#ff0000"/>
|
446
|
+
// <path d="M2,6C3,6 3,6 4,4C5,0 9,3 7,7" fill="none" stroke="#00ff0080"/>
|
447
|
+
// Because of the rounding, the fit path should be slightly off.
|
448
|
+
original: 'm1,1 C1,2 2,10 4,4 C5,0 9,3 7,7',
|
449
|
+
near: Vec2.of(3, 5),
|
450
|
+
map: (p: Point2) => Vec2.of(Math.round(p.x), Math.round(p.y)),
|
451
|
+
expected: [ 'M1,1C1,2 1,6 2,6', 'M2,6C3,6 3,6 4,4C5,0 9,3 7,7' ],
|
452
|
+
},
|
453
|
+
])('should support mapping newly-added points while splitting (case %j)', ({ original, near, map, expected }) => {
|
454
|
+
const path = Path.fromString(original);
|
455
|
+
const split = path.splitNear(near, { mapNewPoint: map });
|
456
|
+
expect(split.map(p => p.toString(false))).toMatchObject(expected);
|
457
|
+
});
|
458
|
+
});
|
459
|
+
|
460
|
+
describe('spliced', () => {
|
461
|
+
it.each([
|
462
|
+
// should support insertion splicing
|
463
|
+
{
|
464
|
+
curve: 'm0,0 l2,0',
|
465
|
+
from: { i: 0, t: 0.5 },
|
466
|
+
to: { i: 0, t: 0.5 },
|
467
|
+
insert: 'm1,0 l0,10 z',
|
468
|
+
expected: 'M0,0 L1,0 L1,10 L1,0 L2,0',
|
469
|
+
},
|
470
|
+
|
471
|
+
// should support removing a segment when splicing
|
472
|
+
{
|
473
|
+
curve: 'm0,0 l4,0',
|
474
|
+
from: { i: 0, t: 0.25 },
|
475
|
+
to: { i: 0, t: 0.75 },
|
476
|
+
insert: 'M1,0 L1,1 L3,1 L3,0',
|
477
|
+
expected: 'M0,0 L1,0 L1,1 L3,1 L3,0 L4,0',
|
478
|
+
},
|
479
|
+
|
480
|
+
// should support reverse splicing and reverse `insert` as necessary
|
481
|
+
{
|
482
|
+
curve: 'M0,0 l4,0',
|
483
|
+
from: { i: 0, t: 0.75 },
|
484
|
+
to: { i: 0, t: 0.25 },
|
485
|
+
insert: 'M1,0 L1,1 L3,1 L3,0',
|
486
|
+
expected: 'M1,0 L3,0 L3,1 L1,1 L1,0',
|
487
|
+
},
|
488
|
+
])('.spliced should support inserting paths inbetween other paths (case %#)', ({ curve, from, to, insert, expected }) => {
|
489
|
+
const originalCurve = Path.fromString(curve);
|
490
|
+
expect(
|
491
|
+
originalCurve.spliced(
|
492
|
+
{ curveIndex: from.i, parameterValue: from.t },
|
493
|
+
{ curveIndex: to.i, parameterValue: to.t },
|
494
|
+
Path.fromString(insert),
|
495
|
+
)
|
496
|
+
).objEq(Path.fromString(expected));
|
497
|
+
});
|
498
|
+
});
|
499
|
+
|
351
500
|
it.each([
|
352
501
|
[ 'm0,0 L1,1', 'M1,1 L0,0' ],
|
353
502
|
[ 'm0,0 L1,1', 'M1,1 L0,0' ],
|
@@ -366,4 +515,23 @@ describe('Path', () => {
|
|
366
515
|
])('.nearestPointTo should return the closest point on a path to the given parameter (case %#)', (path, point, expectedClosest) => {
|
367
516
|
expect(Path.fromString(path).nearestPointTo(point).point).objEq(expectedClosest, 0.002);
|
368
517
|
});
|
518
|
+
|
519
|
+
it.each([
|
520
|
+
// Polyline
|
521
|
+
[ 'm0,0 l1,0 l0,1', [ 0, 0.5 ], Vec2.of(1, 0) ],
|
522
|
+
[ 'm0,0 l1,0 l0,1', [ 0, 0.99 ], Vec2.of(1, 0) ],
|
523
|
+
[ 'm0,0 l1,0 l0,1', [ 1, 0 ], Vec2.of(0, 1) ],
|
524
|
+
[ 'm0,0 l1,0 l0,1', [ 1, 0.5 ], Vec2.of(0, 1) ],
|
525
|
+
[ 'm0,0 l1,0 l0,1', [ 1, 1 ], Vec2.of(0, 1) ],
|
526
|
+
|
527
|
+
// Shape with quadratic Bézier curves
|
528
|
+
[ 'M0,0 Q1,0 0,1', [ 0, 0 ], Vec2.of(1, 0) ],
|
529
|
+
[ 'M0,0 Q1,1 0,1', [ 0, 1 ], Vec2.of(-1, 0) ],
|
530
|
+
[ 'M0,0 Q1,0 1,1 Q0,1 0,2', [ 0, 1 ], Vec2.of(0, 1) ],
|
531
|
+
[ 'M0,0 Q1,0 1,1 Q0,1 0,2', [ 1, 1 ], Vec2.of(0, 1) ],
|
532
|
+
])('.tangentAt should point in the direction of increasing parameter values, for curve %s at %j', (pathString, evalAt, expected) => {
|
533
|
+
const at: CurveIndexRecord = { curveIndex: evalAt[0], parameterValue: evalAt[1] };
|
534
|
+
const path = Path.fromString(pathString);
|
535
|
+
expect(path.tangentAt(at)).objEq(expected);
|
536
|
+
});
|
369
537
|
});
|