@js-draw/math 1.16.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 +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.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
|
|
@@ -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,45 @@ 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
|
+
[new LineSegment2(Vec2.of(35.5,19.5), Vec2.of(38.5,-17.5)), 0],
|
242
|
+
])('should correctly report positive intersections with a line-like Bézier', (line, strokeRadius) => {
|
243
|
+
const bezier = Path.fromString('M0,0 Q50,0 100,0');
|
244
|
+
expect(bezier.intersection(line, strokeRadius).length).toBeGreaterThan(0);
|
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);
|
206
256
|
});
|
207
257
|
});
|
208
258
|
|
@@ -306,4 +356,182 @@ describe('Path', () => {
|
|
306
356
|
expect(strokedRect.startPoint).objEq(lastSegment.point);
|
307
357
|
});
|
308
358
|
});
|
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
|
+
|
500
|
+
it.each([
|
501
|
+
[ 'm0,0 L1,1', 'M1,1 L0,0' ],
|
502
|
+
[ 'm0,0 L1,1', 'M1,1 L0,0' ],
|
503
|
+
[ 'M0,0 L1,1 Q2,2 3,3', 'M3,3 Q2,2 1,1 L0,0' ],
|
504
|
+
[ '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' ],
|
505
|
+
])('.reversed should reverse paths', (original, expected) => {
|
506
|
+
expect(Path.fromString(original).reversed()).objEq(Path.fromString(expected));
|
507
|
+
expect(Path.fromString(expected).reversed()).objEq(Path.fromString(original));
|
508
|
+
expect(Path.fromString(original).reversed().reversed()).objEq(Path.fromString(original));
|
509
|
+
});
|
510
|
+
|
511
|
+
it.each([
|
512
|
+
[ 'm0,0 l1,0', Vec2.of(0, 0), Vec2.of(0, 0) ],
|
513
|
+
[ 'm0,0 l1,0', Vec2.of(0.5, 0), Vec2.of(0.5, 0) ],
|
514
|
+
[ 'm0,0 Q1,0 1,2', Vec2.of(1, 0), Vec2.of(0.6236, 0.299) ],
|
515
|
+
])('.nearestPointTo should return the closest point on a path to the given parameter (case %#)', (path, point, expectedClosest) => {
|
516
|
+
expect(Path.fromString(path).nearestPointTo(point).point).objEq(expectedClosest, 0.002);
|
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
|
+
});
|
309
537
|
});
|