@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.
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
  });