@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.
Files changed (64) hide show
  1. package/dist/cjs/Mat33.js +6 -1
  2. package/dist/cjs/Vec3.d.ts +23 -1
  3. package/dist/cjs/Vec3.js +33 -7
  4. package/dist/cjs/lib.d.ts +2 -1
  5. package/dist/cjs/lib.js +5 -1
  6. package/dist/cjs/shapes/Abstract2DShape.d.ts +3 -0
  7. package/dist/cjs/shapes/BezierJSWrapper.d.ts +19 -5
  8. package/dist/cjs/shapes/BezierJSWrapper.js +170 -18
  9. package/dist/cjs/shapes/LineSegment2.d.ts +45 -5
  10. package/dist/cjs/shapes/LineSegment2.js +89 -11
  11. package/dist/cjs/shapes/Parameterized2DShape.d.ts +36 -0
  12. package/dist/cjs/shapes/Parameterized2DShape.js +20 -0
  13. package/dist/cjs/shapes/Path.d.ts +131 -13
  14. package/dist/cjs/shapes/Path.js +507 -26
  15. package/dist/cjs/shapes/PointShape2D.d.ts +14 -3
  16. package/dist/cjs/shapes/PointShape2D.js +28 -5
  17. package/dist/cjs/shapes/QuadraticBezier.d.ts +6 -3
  18. package/dist/cjs/shapes/QuadraticBezier.js +21 -7
  19. package/dist/cjs/shapes/Rect2.d.ts +9 -1
  20. package/dist/cjs/shapes/Rect2.js +9 -2
  21. package/dist/cjs/utils/convexHull2Of.d.ts +9 -0
  22. package/dist/cjs/utils/convexHull2Of.js +61 -0
  23. package/dist/cjs/utils/convexHull2Of.test.d.ts +1 -0
  24. package/dist/mjs/Mat33.mjs +6 -1
  25. package/dist/mjs/Vec3.d.ts +23 -1
  26. package/dist/mjs/Vec3.mjs +33 -7
  27. package/dist/mjs/lib.d.ts +2 -1
  28. package/dist/mjs/lib.mjs +2 -1
  29. package/dist/mjs/shapes/Abstract2DShape.d.ts +3 -0
  30. package/dist/mjs/shapes/BezierJSWrapper.d.ts +19 -5
  31. package/dist/mjs/shapes/BezierJSWrapper.mjs +168 -18
  32. package/dist/mjs/shapes/LineSegment2.d.ts +45 -5
  33. package/dist/mjs/shapes/LineSegment2.mjs +89 -11
  34. package/dist/mjs/shapes/Parameterized2DShape.d.ts +36 -0
  35. package/dist/mjs/shapes/Parameterized2DShape.mjs +13 -0
  36. package/dist/mjs/shapes/Path.d.ts +131 -13
  37. package/dist/mjs/shapes/Path.mjs +504 -25
  38. package/dist/mjs/shapes/PointShape2D.d.ts +14 -3
  39. package/dist/mjs/shapes/PointShape2D.mjs +28 -5
  40. package/dist/mjs/shapes/QuadraticBezier.d.ts +6 -3
  41. package/dist/mjs/shapes/QuadraticBezier.mjs +21 -7
  42. package/dist/mjs/shapes/Rect2.d.ts +9 -1
  43. package/dist/mjs/shapes/Rect2.mjs +9 -2
  44. package/dist/mjs/utils/convexHull2Of.d.ts +9 -0
  45. package/dist/mjs/utils/convexHull2Of.mjs +59 -0
  46. package/dist/mjs/utils/convexHull2Of.test.d.ts +1 -0
  47. package/package.json +5 -5
  48. package/src/Mat33.ts +8 -2
  49. package/src/Vec3.test.ts +42 -7
  50. package/src/Vec3.ts +37 -8
  51. package/src/lib.ts +5 -0
  52. package/src/shapes/Abstract2DShape.ts +3 -0
  53. package/src/shapes/BezierJSWrapper.ts +195 -14
  54. package/src/shapes/LineSegment2.test.ts +61 -1
  55. package/src/shapes/LineSegment2.ts +110 -12
  56. package/src/shapes/Parameterized2DShape.ts +44 -0
  57. package/src/shapes/Path.test.ts +233 -5
  58. package/src/shapes/Path.ts +593 -37
  59. package/src/shapes/PointShape2D.ts +33 -6
  60. package/src/shapes/QuadraticBezier.test.ts +69 -12
  61. package/src/shapes/QuadraticBezier.ts +25 -8
  62. package/src/shapes/Rect2.ts +10 -3
  63. package/src/utils/convexHull2Of.test.ts +43 -0
  64. package/src/utils/convexHull2Of.ts +71 -0
@@ -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 give all intersections for a Bézier stroked path', () => {
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.length).toBe(0);
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.length).toBe(1);
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
  });