@js-draw/math 1.16.0 → 1.18.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cjs/Mat33.js +6 -1
- package/dist/cjs/Vec3.d.ts +23 -1
- package/dist/cjs/Vec3.js +33 -7
- package/dist/cjs/lib.d.ts +2 -1
- package/dist/cjs/lib.js +5 -1
- package/dist/cjs/shapes/Abstract2DShape.d.ts +3 -0
- package/dist/cjs/shapes/BezierJSWrapper.d.ts +19 -5
- package/dist/cjs/shapes/BezierJSWrapper.js +170 -18
- package/dist/cjs/shapes/LineSegment2.d.ts +45 -5
- package/dist/cjs/shapes/LineSegment2.js +89 -11
- package/dist/cjs/shapes/Parameterized2DShape.d.ts +36 -0
- package/dist/cjs/shapes/Parameterized2DShape.js +20 -0
- package/dist/cjs/shapes/Path.d.ts +131 -13
- package/dist/cjs/shapes/Path.js +507 -26
- package/dist/cjs/shapes/PointShape2D.d.ts +14 -3
- package/dist/cjs/shapes/PointShape2D.js +28 -5
- package/dist/cjs/shapes/QuadraticBezier.d.ts +6 -3
- package/dist/cjs/shapes/QuadraticBezier.js +21 -7
- package/dist/cjs/shapes/Rect2.d.ts +9 -1
- package/dist/cjs/shapes/Rect2.js +9 -2
- package/dist/cjs/utils/convexHull2Of.d.ts +9 -0
- package/dist/cjs/utils/convexHull2Of.js +61 -0
- package/dist/cjs/utils/convexHull2Of.test.d.ts +1 -0
- package/dist/mjs/Mat33.mjs +6 -1
- package/dist/mjs/Vec3.d.ts +23 -1
- package/dist/mjs/Vec3.mjs +33 -7
- package/dist/mjs/lib.d.ts +2 -1
- package/dist/mjs/lib.mjs +2 -1
- package/dist/mjs/shapes/Abstract2DShape.d.ts +3 -0
- package/dist/mjs/shapes/BezierJSWrapper.d.ts +19 -5
- package/dist/mjs/shapes/BezierJSWrapper.mjs +168 -18
- package/dist/mjs/shapes/LineSegment2.d.ts +45 -5
- package/dist/mjs/shapes/LineSegment2.mjs +89 -11
- package/dist/mjs/shapes/Parameterized2DShape.d.ts +36 -0
- package/dist/mjs/shapes/Parameterized2DShape.mjs +13 -0
- package/dist/mjs/shapes/Path.d.ts +131 -13
- package/dist/mjs/shapes/Path.mjs +504 -25
- package/dist/mjs/shapes/PointShape2D.d.ts +14 -3
- package/dist/mjs/shapes/PointShape2D.mjs +28 -5
- package/dist/mjs/shapes/QuadraticBezier.d.ts +6 -3
- package/dist/mjs/shapes/QuadraticBezier.mjs +21 -7
- package/dist/mjs/shapes/Rect2.d.ts +9 -1
- package/dist/mjs/shapes/Rect2.mjs +9 -2
- package/dist/mjs/utils/convexHull2Of.d.ts +9 -0
- package/dist/mjs/utils/convexHull2Of.mjs +59 -0
- package/dist/mjs/utils/convexHull2Of.test.d.ts +1 -0
- package/package.json +5 -5
- package/src/Mat33.ts +8 -2
- package/src/Vec3.test.ts +42 -7
- package/src/Vec3.ts +37 -8
- package/src/lib.ts +5 -0
- package/src/shapes/Abstract2DShape.ts +3 -0
- package/src/shapes/BezierJSWrapper.ts +195 -14
- package/src/shapes/LineSegment2.test.ts +61 -1
- package/src/shapes/LineSegment2.ts +110 -12
- package/src/shapes/Parameterized2DShape.ts +44 -0
- package/src/shapes/Path.test.ts +233 -5
- package/src/shapes/Path.ts +593 -37
- package/src/shapes/PointShape2D.ts +33 -6
- package/src/shapes/QuadraticBezier.test.ts +69 -12
- package/src/shapes/QuadraticBezier.ts +25 -8
- package/src/shapes/Rect2.ts +10 -3
- package/src/utils/convexHull2Of.test.ts +43 -0
- package/src/utils/convexHull2Of.ts +71 -0
package/src/shapes/Path.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
|
});
|