@js-draw/math 1.21.2 → 1.22.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 (63) hide show
  1. package/build-config.json +1 -1
  2. package/dist/cjs/Color4.js +2 -2
  3. package/dist/cjs/Mat33.d.ts +1 -11
  4. package/dist/cjs/Mat33.js +8 -24
  5. package/dist/cjs/Vec3.js +9 -7
  6. package/dist/cjs/shapes/BezierJSWrapper.js +20 -13
  7. package/dist/cjs/shapes/LineSegment2.js +13 -17
  8. package/dist/cjs/shapes/Parameterized2DShape.js +1 -1
  9. package/dist/cjs/shapes/Path.js +49 -47
  10. package/dist/cjs/shapes/Rect2.js +13 -15
  11. package/dist/cjs/shapes/Triangle.js +4 -5
  12. package/dist/cjs/utils/convexHull2Of.js +3 -3
  13. package/dist/mjs/Color4.mjs +2 -2
  14. package/dist/mjs/Mat33.d.ts +1 -11
  15. package/dist/mjs/Mat33.mjs +8 -24
  16. package/dist/mjs/Vec3.mjs +9 -7
  17. package/dist/mjs/shapes/BezierJSWrapper.mjs +20 -13
  18. package/dist/mjs/shapes/LineSegment2.mjs +13 -17
  19. package/dist/mjs/shapes/Parameterized2DShape.mjs +1 -1
  20. package/dist/mjs/shapes/Path.mjs +49 -47
  21. package/dist/mjs/shapes/Rect2.mjs +13 -15
  22. package/dist/mjs/shapes/Triangle.mjs +4 -5
  23. package/dist/mjs/utils/convexHull2Of.mjs +3 -3
  24. package/dist-test/test_imports/test-require.cjs +1 -1
  25. package/package.json +3 -3
  26. package/src/Color4.test.ts +16 -21
  27. package/src/Color4.ts +22 -17
  28. package/src/Mat33.fromCSSMatrix.test.ts +31 -45
  29. package/src/Mat33.test.ts +58 -96
  30. package/src/Mat33.ts +61 -104
  31. package/src/Vec2.test.ts +3 -3
  32. package/src/Vec3.test.ts +2 -3
  33. package/src/Vec3.ts +34 -58
  34. package/src/lib.ts +0 -2
  35. package/src/polynomial/solveQuadratic.test.ts +39 -13
  36. package/src/polynomial/solveQuadratic.ts +5 -6
  37. package/src/rounding/cleanUpNumber.test.ts +1 -1
  38. package/src/rounding/constants.ts +1 -3
  39. package/src/rounding/getLenAfterDecimal.ts +1 -2
  40. package/src/rounding/lib.ts +1 -2
  41. package/src/rounding/toRoundedString.test.ts +1 -1
  42. package/src/rounding/toStringOfSamePrecision.test.ts +1 -2
  43. package/src/rounding/toStringOfSamePrecision.ts +1 -1
  44. package/src/shapes/BezierJSWrapper.ts +54 -37
  45. package/src/shapes/CubicBezier.ts +3 -3
  46. package/src/shapes/LineSegment2.test.ts +24 -17
  47. package/src/shapes/LineSegment2.ts +26 -29
  48. package/src/shapes/Parameterized2DShape.ts +5 -4
  49. package/src/shapes/Path.fromString.test.ts +5 -5
  50. package/src/shapes/Path.test.ts +122 -120
  51. package/src/shapes/Path.toString.test.ts +7 -7
  52. package/src/shapes/Path.ts +378 -352
  53. package/src/shapes/PointShape2D.ts +3 -3
  54. package/src/shapes/QuadraticBezier.test.ts +27 -21
  55. package/src/shapes/QuadraticBezier.ts +4 -9
  56. package/src/shapes/Rect2.test.ts +44 -75
  57. package/src/shapes/Rect2.ts +30 -35
  58. package/src/shapes/Triangle.test.ts +31 -29
  59. package/src/shapes/Triangle.ts +17 -18
  60. package/src/utils/convexHull2Of.test.ts +54 -15
  61. package/src/utils/convexHull2Of.ts +9 -7
  62. package/tsconfig.json +1 -3
  63. package/typedoc.json +2 -2
@@ -41,7 +41,11 @@ export interface MoveToPathCommand {
41
41
  point: Point2;
42
42
  }
43
43
 
44
- export type PathCommand = CubicBezierPathCommand | QuadraticBezierPathCommand | MoveToPathCommand | LinePathCommand;
44
+ export type PathCommand =
45
+ | CubicBezierPathCommand
46
+ | QuadraticBezierPathCommand
47
+ | MoveToPathCommand
48
+ | LinePathCommand;
45
49
 
46
50
  export interface IntersectionResult {
47
51
  // @internal
@@ -62,7 +66,7 @@ export interface PathSplitOptions {
62
66
  * Allows mapping points on newly added segments. This is useful, for example,
63
67
  * to round points to prevent long decimals when later saving.
64
68
  */
65
- mapNewPoint?: (point: Point2)=>Point2;
69
+ mapNewPoint?: (point: Point2) => Point2;
66
70
  }
67
71
 
68
72
  /**
@@ -182,7 +186,7 @@ export class Path {
182
186
  return Rect2.union(...bboxes);
183
187
  }
184
188
 
185
- private cachedGeometry: Parameterized2DShape[]|null = null;
189
+ private cachedGeometry: Parameterized2DShape[] | null = null;
186
190
 
187
191
  // Lazy-loads and returns this path's geometry
188
192
  public get geometry(): Parameterized2DShape[] {
@@ -197,35 +201,27 @@ export class Path {
197
201
  let exhaustivenessCheck: never;
198
202
 
199
203
  switch (part.kind) {
200
- case PathCommandType.CubicBezierTo:
201
- geometry.push(
202
- new CubicBezier(
203
- startPoint, part.controlPoint1, part.controlPoint2, part.endPoint
204
- )
205
- );
206
- startPoint = part.endPoint;
207
- break;
208
- case PathCommandType.QuadraticBezierTo:
209
- geometry.push(
210
- new QuadraticBezier(
211
- startPoint, part.controlPoint, part.endPoint
212
- )
213
- );
214
- startPoint = part.endPoint;
215
- break;
216
- case PathCommandType.LineTo:
217
- geometry.push(
218
- new LineSegment2(startPoint, part.point)
219
- );
220
- startPoint = part.point;
221
- break;
222
- case PathCommandType.MoveTo:
223
- geometry.push(new PointShape2D(part.point));
224
- startPoint = part.point;
225
- break;
226
- default:
227
- exhaustivenessCheck = part;
228
- return exhaustivenessCheck;
204
+ case PathCommandType.CubicBezierTo:
205
+ geometry.push(
206
+ new CubicBezier(startPoint, part.controlPoint1, part.controlPoint2, part.endPoint),
207
+ );
208
+ startPoint = part.endPoint;
209
+ break;
210
+ case PathCommandType.QuadraticBezierTo:
211
+ geometry.push(new QuadraticBezier(startPoint, part.controlPoint, part.endPoint));
212
+ startPoint = part.endPoint;
213
+ break;
214
+ case PathCommandType.LineTo:
215
+ geometry.push(new LineSegment2(startPoint, part.point));
216
+ startPoint = part.point;
217
+ break;
218
+ case PathCommandType.MoveTo:
219
+ geometry.push(new PointShape2D(part.point));
220
+ startPoint = part.point;
221
+ break;
222
+ default:
223
+ exhaustivenessCheck = part;
224
+ return exhaustivenessCheck;
229
225
  }
230
226
  }
231
227
 
@@ -246,26 +242,26 @@ export class Path {
246
242
  let exhaustivenessCheck: never;
247
243
 
248
244
  switch (part.kind) {
249
- case PathCommandType.CubicBezierTo:
250
- yield part.endPoint;
251
- break;
252
- case PathCommandType.QuadraticBezierTo:
253
- yield part.endPoint;
254
- break;
255
- case PathCommandType.LineTo:
256
- yield part.point;
257
- break;
258
- case PathCommandType.MoveTo:
259
- yield part.point;
260
- break;
261
- default:
262
- exhaustivenessCheck = part;
263
- return exhaustivenessCheck;
245
+ case PathCommandType.CubicBezierTo:
246
+ yield part.endPoint;
247
+ break;
248
+ case PathCommandType.QuadraticBezierTo:
249
+ yield part.endPoint;
250
+ break;
251
+ case PathCommandType.LineTo:
252
+ yield part.point;
253
+ break;
254
+ case PathCommandType.MoveTo:
255
+ yield part.point;
256
+ break;
257
+ default:
258
+ exhaustivenessCheck = part;
259
+ return exhaustivenessCheck;
264
260
  }
265
261
  }
266
262
  }
267
263
 
268
- private cachedPolylineApproximation: LineSegment2[]|null = null;
264
+ private cachedPolylineApproximation: LineSegment2[] | null = null;
269
265
 
270
266
  // Approximates this path with a group of line segments.
271
267
  public polylineApproximation(): LineSegment2[] {
@@ -277,16 +273,16 @@ export class Path {
277
273
 
278
274
  for (const part of this.parts) {
279
275
  switch (part.kind) {
280
- case PathCommandType.CubicBezierTo:
281
- points.push(part.controlPoint1, part.controlPoint2, part.endPoint);
282
- break;
283
- case PathCommandType.QuadraticBezierTo:
284
- points.push(part.controlPoint, part.endPoint);
285
- break;
286
- case PathCommandType.MoveTo:
287
- case PathCommandType.LineTo:
288
- points.push(part.point);
289
- break;
276
+ case PathCommandType.CubicBezierTo:
277
+ points.push(part.controlPoint1, part.controlPoint2, part.endPoint);
278
+ break;
279
+ case PathCommandType.QuadraticBezierTo:
280
+ points.push(part.controlPoint, part.endPoint);
281
+ break;
282
+ case PathCommandType.MoveTo:
283
+ case PathCommandType.LineTo:
284
+ points.push(part.point);
285
+ break;
290
286
  }
291
287
  }
292
288
 
@@ -304,19 +300,19 @@ export class Path {
304
300
  const points = [startPoint];
305
301
  let exhaustivenessCheck: never;
306
302
  switch (part.kind) {
307
- case PathCommandType.MoveTo:
308
- case PathCommandType.LineTo:
309
- points.push(part.point);
310
- break;
311
- case PathCommandType.CubicBezierTo:
312
- points.push(part.controlPoint1, part.controlPoint2, part.endPoint);
313
- break;
314
- case PathCommandType.QuadraticBezierTo:
315
- points.push(part.controlPoint, part.endPoint);
316
- break;
317
- default:
318
- exhaustivenessCheck = part;
319
- return exhaustivenessCheck;
303
+ case PathCommandType.MoveTo:
304
+ case PathCommandType.LineTo:
305
+ points.push(part.point);
306
+ break;
307
+ case PathCommandType.CubicBezierTo:
308
+ points.push(part.controlPoint1, part.controlPoint2, part.endPoint);
309
+ break;
310
+ case PathCommandType.QuadraticBezierTo:
311
+ points.push(part.controlPoint, part.endPoint);
312
+ break;
313
+ default:
314
+ exhaustivenessCheck = part;
315
+ return exhaustivenessCheck;
320
316
  }
321
317
 
322
318
  return Rect2.bboxOf(points);
@@ -356,7 +352,9 @@ export class Path {
356
352
  * both end points of `line` and each point in `additionalRaymarchStartPoints`.
357
353
  */
358
354
  private raymarchIntersectionWith(
359
- line: LineSegment2, strokeRadius: number, additionalRaymarchStartPoints: Point2[] = []
355
+ line: LineSegment2,
356
+ strokeRadius: number,
357
+ additionalRaymarchStartPoints: Point2[] = [],
360
358
  ): IntersectionResult[] {
361
359
  // No intersection between bounding boxes: No possible intersection
362
360
  // of the interior.
@@ -368,9 +366,9 @@ export class Path {
368
366
 
369
367
  type DistanceFunction = (point: Point2) => number;
370
368
  type DistanceFunctionRecord = {
371
- part: Parameterized2DShape,
372
- bbox: Rect2,
373
- distFn: DistanceFunction,
369
+ part: Parameterized2DShape;
370
+ bbox: Rect2;
371
+ distFn: DistanceFunction;
374
372
  };
375
373
  const partDistFunctionRecords: DistanceFunctionRecord[] = [];
376
374
 
@@ -407,9 +405,9 @@ export class Path {
407
405
 
408
406
  // Returns the minimum distance to a part in this stroke, where only parts that the given
409
407
  // line could intersect are considered.
410
- const sdf = (point: Point2): [Parameterized2DShape|null, number] => {
408
+ const sdf = (point: Point2): [Parameterized2DShape | null, number] => {
411
409
  let minDist = Infinity;
412
- let minDistPart: Parameterized2DShape|null = null;
410
+ let minDistPart: Parameterized2DShape | null = null;
413
411
 
414
412
  const uncheckedDistFunctions: DistanceFunctionRecord[] = [];
415
413
 
@@ -448,18 +446,15 @@ export class Path {
448
446
  }
449
447
  }
450
448
 
451
- return [ minDistPart, minDist - strokeRadius ];
449
+ return [minDistPart, minDist - strokeRadius];
452
450
  };
453
451
 
454
-
455
452
  // Raymarch:
456
453
  const maxRaymarchSteps = 8;
457
454
 
458
455
  // Start raymarching from each of these points. This allows detection of multiple
459
456
  // intersections.
460
- const startPoints = [
461
- line.p1, ...additionalRaymarchStartPoints, line.p2
462
- ];
457
+ const startPoints = [line.p1, ...additionalRaymarchStartPoints, line.p2];
463
458
 
464
459
  // Converts a point ON THE LINE to a parameter
465
460
  const pointToParameter = (point: Point2) => {
@@ -491,12 +486,12 @@ export class Path {
491
486
  startPoint: Point2,
492
487
 
493
488
  // Direction to march in (multiplies line.direction)
494
- directionMultiplier: -1|1,
489
+ directionMultiplier: -1 | 1,
495
490
 
496
491
  // Terminate if the current point corresponds to a parameter
497
492
  // below this.
498
493
  minimumLineParameter: number,
499
- ): number|null => {
494
+ ): number | null => {
500
495
  let currentPoint = startPoint;
501
496
  let [lastPart, lastDist] = sdf(currentPoint);
502
497
  let lastParameter = pointToParameter(currentPoint);
@@ -594,7 +589,9 @@ export class Path {
594
589
  }
595
590
 
596
591
  if (this.parts.length === 0) {
597
- return new Path(this.startPoint, [{ kind: PathCommandType.MoveTo, point: this.startPoint }]).intersection(line, strokeRadius);
592
+ return new Path(this.startPoint, [
593
+ { kind: PathCommandType.MoveTo, point: this.startPoint },
594
+ ]).intersection(line, strokeRadius);
598
595
  }
599
596
 
600
597
  let index = 0;
@@ -610,7 +607,7 @@ export class Path {
610
607
  });
611
608
  }
612
609
 
613
- index ++;
610
+ index++;
614
611
  }
615
612
 
616
613
  // If given a non-zero strokeWidth, attempt to raymarch.
@@ -619,7 +616,7 @@ export class Path {
619
616
  const doRaymarching = strokeRadius && strokeRadius > 1e-8;
620
617
  if (doRaymarching) {
621
618
  // Starting points for raymarching (in addition to the end points of the line).
622
- const startPoints = result.map(intersection => intersection.point);
619
+ const startPoints = result.map((intersection) => intersection.point);
623
620
  result = this.raymarchIntersectionWith(line, strokeRadius, startPoints);
624
621
  }
625
622
 
@@ -678,9 +675,17 @@ export class Path {
678
675
  *
679
676
  * This method is analogous to {@link Array.toSpliced}.
680
677
  */
681
- public spliced(deleteFrom: CurveIndexRecord, deleteTo: CurveIndexRecord, insert: Path|undefined, options?: PathSplitOptions): Path {
678
+ public spliced(
679
+ deleteFrom: CurveIndexRecord,
680
+ deleteTo: CurveIndexRecord,
681
+ insert: Path | undefined,
682
+ options?: PathSplitOptions,
683
+ ): Path {
682
684
  const isBeforeOrEqual = (a: CurveIndexRecord, b: CurveIndexRecord) => {
683
- return a.curveIndex < b.curveIndex || (a.curveIndex === b.curveIndex && a.parameterValue <= b.parameterValue);
685
+ return (
686
+ a.curveIndex < b.curveIndex ||
687
+ (a.curveIndex === b.curveIndex && a.parameterValue <= b.parameterValue)
688
+ );
684
689
  };
685
690
 
686
691
  if (isBeforeOrEqual(deleteFrom, deleteTo)) {
@@ -711,11 +716,14 @@ export class Path {
711
716
  }
712
717
  }
713
718
 
714
- public splitAt(at: CurveIndexRecord, options?: PathSplitOptions): [Path]|[Path, Path];
719
+ public splitAt(at: CurveIndexRecord, options?: PathSplitOptions): [Path] | [Path, Path];
715
720
  public splitAt(at: CurveIndexRecord[], options?: PathSplitOptions): Path[];
716
721
 
717
722
  // @internal
718
- public splitAt(splitAt: CurveIndexRecord[]|CurveIndexRecord, options?: PathSplitOptions): Path[] {
723
+ public splitAt(
724
+ splitAt: CurveIndexRecord[] | CurveIndexRecord,
725
+ options?: PathSplitOptions,
726
+ ): Path[] {
719
727
  if (!Array.isArray(splitAt)) {
720
728
  splitAt = [splitAt];
721
729
  }
@@ -728,9 +736,9 @@ export class Path {
728
736
  //
729
737
 
730
738
  while (
731
- splitAt.length > 0
732
- && splitAt[splitAt.length - 1].curveIndex >= this.parts.length - 1
733
- && splitAt[splitAt.length - 1].parameterValue >= 1
739
+ splitAt.length > 0 &&
740
+ splitAt[splitAt.length - 1].curveIndex >= this.parts.length - 1 &&
741
+ splitAt[splitAt.length - 1].parameterValue >= 1
734
742
  ) {
735
743
  splitAt.pop();
736
744
  }
@@ -738,9 +746,9 @@ export class Path {
738
746
  splitAt.reverse(); // .reverse() <-- We're `.pop`ing from the end
739
747
 
740
748
  while (
741
- splitAt.length > 0
742
- && splitAt[splitAt.length - 1].curveIndex <= 0
743
- && splitAt[splitAt.length - 1].parameterValue <= 0
749
+ splitAt.length > 0 &&
750
+ splitAt[splitAt.length - 1].curveIndex <= 0 &&
751
+ splitAt[splitAt.length - 1].parameterValue <= 0
744
752
  ) {
745
753
  splitAt.pop();
746
754
  }
@@ -750,7 +758,7 @@ export class Path {
750
758
  }
751
759
 
752
760
  const expectedSplitCount = splitAt.length + 1;
753
- const mapNewPoint = options?.mapNewPoint ?? ((p: Point2)=>p);
761
+ const mapNewPoint = options?.mapNewPoint ?? ((p: Point2) => p);
754
762
 
755
763
  const result: Path[] = [];
756
764
  let currentStartPoint = this.startPoint;
@@ -762,7 +770,7 @@ export class Path {
762
770
 
763
771
  let { curveIndex, parameterValue } = splitAt.pop()!;
764
772
 
765
- for (let i = 0; i < this.parts.length; i ++) {
773
+ for (let i = 0; i < this.parts.length; i++) {
766
774
  if (i !== curveIndex) {
767
775
  currentPath.push(this.parts[i]);
768
776
  } else {
@@ -773,71 +781,71 @@ export class Path {
773
781
  const newPath: PathCommand[] = [];
774
782
 
775
783
  switch (part.kind) {
776
- case PathCommandType.MoveTo:
777
- currentPath.push({
778
- kind: part.kind,
779
- point: part.point,
780
- });
781
- newPathStart = part.point;
782
- break;
783
- case PathCommandType.LineTo:
784
- {
785
- const split = (geom as LineSegment2).splitAt(parameterValue);
784
+ case PathCommandType.MoveTo:
786
785
  currentPath.push({
787
786
  kind: part.kind,
788
- point: mapNewPoint(split[0].p2),
787
+ point: part.point,
789
788
  });
790
- newPathStart = split[0].p2;
791
- if (split.length > 1) {
792
- console.assert(split.length === 2);
793
- newPath.push({
789
+ newPathStart = part.point;
790
+ break;
791
+ case PathCommandType.LineTo:
792
+ {
793
+ const split = (geom as LineSegment2).splitAt(parameterValue);
794
+ currentPath.push({
794
795
  kind: part.kind,
795
-
796
- // Don't map: For lines, the end point of the split is
797
- // the same as the end point of the original:
798
- point: split[1]!.p2,
796
+ point: mapNewPoint(split[0].p2),
799
797
  });
800
- geom = split[1]!;
801
- }
802
- }
803
- break;
804
- case PathCommandType.QuadraticBezierTo:
805
- case PathCommandType.CubicBezierTo:
806
- {
807
- const split = (geom as BezierJSWrapper).splitAt(parameterValue);
808
- let isFirstPart = split.length === 2;
809
- for (const segment of split) {
810
- geom = segment;
811
- const targetArray = isFirstPart ? currentPath : newPath;
812
- const controlPoints = segment.getPoints();
813
- if (part.kind === PathCommandType.CubicBezierTo) {
814
- targetArray.push({
815
- kind: part.kind,
816
- controlPoint1: mapNewPoint(controlPoints[1]),
817
- controlPoint2: mapNewPoint(controlPoints[2]),
818
- endPoint: mapNewPoint(controlPoints[3]),
819
- });
820
- } else {
821
- targetArray.push({
798
+ newPathStart = split[0].p2;
799
+ if (split.length > 1) {
800
+ console.assert(split.length === 2);
801
+ newPath.push({
822
802
  kind: part.kind,
823
- controlPoint: mapNewPoint(controlPoints[1]),
824
- endPoint: mapNewPoint(controlPoints[2]),
803
+
804
+ // Don't map: For lines, the end point of the split is
805
+ // the same as the end point of the original:
806
+ point: split[1]!.p2,
825
807
  });
808
+ geom = split[1]!;
826
809
  }
827
-
828
- // We want the start of the new path to match the start of the
829
- // FIRST Bézier in the NEW path.
830
- if (!isFirstPart) {
831
- newPathStart = controlPoints[0];
810
+ }
811
+ break;
812
+ case PathCommandType.QuadraticBezierTo:
813
+ case PathCommandType.CubicBezierTo:
814
+ {
815
+ const split = (geom as BezierJSWrapper).splitAt(parameterValue);
816
+ let isFirstPart = split.length === 2;
817
+ for (const segment of split) {
818
+ geom = segment;
819
+ const targetArray = isFirstPart ? currentPath : newPath;
820
+ const controlPoints = segment.getPoints();
821
+ if (part.kind === PathCommandType.CubicBezierTo) {
822
+ targetArray.push({
823
+ kind: part.kind,
824
+ controlPoint1: mapNewPoint(controlPoints[1]),
825
+ controlPoint2: mapNewPoint(controlPoints[2]),
826
+ endPoint: mapNewPoint(controlPoints[3]),
827
+ });
828
+ } else {
829
+ targetArray.push({
830
+ kind: part.kind,
831
+ controlPoint: mapNewPoint(controlPoints[1]),
832
+ endPoint: mapNewPoint(controlPoints[2]),
833
+ });
834
+ }
835
+
836
+ // We want the start of the new path to match the start of the
837
+ // FIRST Bézier in the NEW path.
838
+ if (!isFirstPart) {
839
+ newPathStart = controlPoints[0];
840
+ }
841
+ isFirstPart = false;
832
842
  }
833
- isFirstPart = false;
834
843
  }
844
+ break;
845
+ default: {
846
+ const exhaustivenessCheck: never = part;
847
+ return exhaustivenessCheck;
835
848
  }
836
- break;
837
- default: {
838
- const exhaustivenessCheck: never = part;
839
- return exhaustivenessCheck;
840
- }
841
849
  }
842
850
 
843
851
  result.push(new Path(currentStartPoint, [...currentPath]));
@@ -867,7 +875,7 @@ export class Path {
867
875
 
868
876
  console.assert(
869
877
  result.length === expectedSplitCount,
870
- `should split into splitAt.length + 1 splits (was ${result.length}, expected ${expectedSplitCount})`
878
+ `should split into splitAt.length + 1 splits (was ${result.length}, expected ${expectedSplitCount})`,
871
879
  );
872
880
  return result;
873
881
  }
@@ -907,37 +915,40 @@ export class Path {
907
915
  return result;
908
916
  }
909
917
 
910
- private static mapPathCommand(part: PathCommand, mapping: (point: Point2)=> Point2): PathCommand {
918
+ private static mapPathCommand(
919
+ part: PathCommand,
920
+ mapping: (point: Point2) => Point2,
921
+ ): PathCommand {
911
922
  switch (part.kind) {
912
- case PathCommandType.MoveTo:
913
- case PathCommandType.LineTo:
914
- return {
915
- kind: part.kind,
916
- point: mapping(part.point),
917
- };
918
- break;
919
- case PathCommandType.CubicBezierTo:
920
- return {
921
- kind: part.kind,
922
- controlPoint1: mapping(part.controlPoint1),
923
- controlPoint2: mapping(part.controlPoint2),
924
- endPoint: mapping(part.endPoint),
925
- };
926
- break;
927
- case PathCommandType.QuadraticBezierTo:
928
- return {
929
- kind: part.kind,
930
- controlPoint: mapping(part.controlPoint),
931
- endPoint: mapping(part.endPoint),
932
- };
933
- break;
923
+ case PathCommandType.MoveTo:
924
+ case PathCommandType.LineTo:
925
+ return {
926
+ kind: part.kind,
927
+ point: mapping(part.point),
928
+ };
929
+ break;
930
+ case PathCommandType.CubicBezierTo:
931
+ return {
932
+ kind: part.kind,
933
+ controlPoint1: mapping(part.controlPoint1),
934
+ controlPoint2: mapping(part.controlPoint2),
935
+ endPoint: mapping(part.endPoint),
936
+ };
937
+ break;
938
+ case PathCommandType.QuadraticBezierTo:
939
+ return {
940
+ kind: part.kind,
941
+ controlPoint: mapping(part.controlPoint),
942
+ endPoint: mapping(part.endPoint),
943
+ };
944
+ break;
934
945
  }
935
946
 
936
947
  const exhaustivenessCheck: never = part;
937
948
  return exhaustivenessCheck;
938
949
  }
939
950
 
940
- public mapPoints(mapping: (point: Point2)=>Point2): Path {
951
+ public mapPoints(mapping: (point: Point2) => Point2): Path {
941
952
  const startPoint = mapping(this.startPoint);
942
953
  const newParts: PathCommand[] = [];
943
954
 
@@ -953,7 +964,7 @@ export class Path {
953
964
  return this;
954
965
  }
955
966
 
956
- return this.mapPoints(point => affineTransfm.transformVec2(point));
967
+ return this.mapPoints((point) => affineTransfm.transformVec2(point));
957
968
  }
958
969
 
959
970
  /**
@@ -974,7 +985,7 @@ export class Path {
974
985
 
975
986
  // Creates a new path by joining [other] to the end of this path
976
987
  public union(
977
- other: Path|PathCommand[]|null,
988
+ other: Path | PathCommand[] | null,
978
989
 
979
990
  // allowReverse: true iff reversing other or this is permitted if it means
980
991
  // no moveTo command is necessary when unioning the paths.
@@ -1024,36 +1035,35 @@ export class Path {
1024
1035
  let lastPoint: Point2 = this.startPoint;
1025
1036
  for (const part of this.parts) {
1026
1037
  switch (part.kind) {
1027
- case PathCommandType.LineTo:
1028
- case PathCommandType.MoveTo:
1029
- newParts.push({
1030
- kind: part.kind,
1031
- point: lastPoint,
1032
- });
1033
- lastPoint = part.point;
1034
- break;
1035
- case PathCommandType.CubicBezierTo:
1036
- newParts.push({
1037
- kind: part.kind,
1038
- controlPoint1: part.controlPoint2,
1039
- controlPoint2: part.controlPoint1,
1040
- endPoint: lastPoint,
1041
- });
1042
- lastPoint = part.endPoint;
1043
- break;
1044
- case PathCommandType.QuadraticBezierTo:
1045
- newParts.push({
1046
- kind: part.kind,
1047
- controlPoint: part.controlPoint,
1048
- endPoint: lastPoint,
1049
- });
1050
- lastPoint = part.endPoint;
1051
- break;
1052
- default:
1053
- {
1054
- const exhaustivenessCheck: never = part;
1055
- return exhaustivenessCheck;
1056
- }
1038
+ case PathCommandType.LineTo:
1039
+ case PathCommandType.MoveTo:
1040
+ newParts.push({
1041
+ kind: part.kind,
1042
+ point: lastPoint,
1043
+ });
1044
+ lastPoint = part.point;
1045
+ break;
1046
+ case PathCommandType.CubicBezierTo:
1047
+ newParts.push({
1048
+ kind: part.kind,
1049
+ controlPoint1: part.controlPoint2,
1050
+ controlPoint2: part.controlPoint1,
1051
+ endPoint: lastPoint,
1052
+ });
1053
+ lastPoint = part.endPoint;
1054
+ break;
1055
+ case PathCommandType.QuadraticBezierTo:
1056
+ newParts.push({
1057
+ kind: part.kind,
1058
+ controlPoint: part.controlPoint,
1059
+ endPoint: lastPoint,
1060
+ });
1061
+ lastPoint = part.endPoint;
1062
+ break;
1063
+ default: {
1064
+ const exhaustivenessCheck: never = part;
1065
+ return exhaustivenessCheck;
1066
+ }
1057
1067
  }
1058
1068
  }
1059
1069
  newParts.reverse();
@@ -1066,7 +1076,10 @@ export class Path {
1066
1076
  return this.startPoint;
1067
1077
  }
1068
1078
  const lastPart = this.parts[this.parts.length - 1];
1069
- if (lastPart.kind === PathCommandType.QuadraticBezierTo || lastPart.kind === PathCommandType.CubicBezierTo) {
1079
+ if (
1080
+ lastPart.kind === PathCommandType.QuadraticBezierTo ||
1081
+ lastPart.kind === PathCommandType.CubicBezierTo
1082
+ ) {
1070
1083
  return lastPart.endPoint;
1071
1084
  } else {
1072
1085
  return lastPart.point;
@@ -1138,7 +1151,7 @@ export class Path {
1138
1151
  let intersectionCount = 0;
1139
1152
  for (const line of polygon) {
1140
1153
  if (line.intersects(testLine)) {
1141
- intersectionCount ++;
1154
+ intersectionCount++;
1142
1155
  }
1143
1156
  }
1144
1157
 
@@ -1178,40 +1191,39 @@ export class Path {
1178
1191
  const part2 = other.parts[i];
1179
1192
 
1180
1193
  switch (part1.kind) {
1181
- case PathCommandType.LineTo:
1182
- case PathCommandType.MoveTo:
1183
- if (part1.kind !== part2.kind) {
1184
- return false;
1185
- } else if(!part1.point.eq(part2.point, tolerance)) {
1186
- return false;
1187
- }
1188
- break;
1189
- case PathCommandType.CubicBezierTo:
1190
- if (part1.kind !== part2.kind) {
1191
- return false;
1192
- } else if (
1193
- !part1.controlPoint1.eq(part2.controlPoint1, tolerance)
1194
- || !part1.controlPoint2.eq(part2.controlPoint2, tolerance)
1195
- || !part1.endPoint.eq(part2.endPoint, tolerance)
1196
- ) {
1197
- return false;
1198
- }
1199
- break;
1200
- case PathCommandType.QuadraticBezierTo:
1201
- if (part1.kind !== part2.kind) {
1202
- return false;
1203
- } else if (
1204
- !part1.controlPoint.eq(part2.controlPoint, tolerance)
1205
- || !part1.endPoint.eq(part2.endPoint, tolerance)
1206
- ) {
1207
- return false;
1194
+ case PathCommandType.LineTo:
1195
+ case PathCommandType.MoveTo:
1196
+ if (part1.kind !== part2.kind) {
1197
+ return false;
1198
+ } else if (!part1.point.eq(part2.point, tolerance)) {
1199
+ return false;
1200
+ }
1201
+ break;
1202
+ case PathCommandType.CubicBezierTo:
1203
+ if (part1.kind !== part2.kind) {
1204
+ return false;
1205
+ } else if (
1206
+ !part1.controlPoint1.eq(part2.controlPoint1, tolerance) ||
1207
+ !part1.controlPoint2.eq(part2.controlPoint2, tolerance) ||
1208
+ !part1.endPoint.eq(part2.endPoint, tolerance)
1209
+ ) {
1210
+ return false;
1211
+ }
1212
+ break;
1213
+ case PathCommandType.QuadraticBezierTo:
1214
+ if (part1.kind !== part2.kind) {
1215
+ return false;
1216
+ } else if (
1217
+ !part1.controlPoint.eq(part2.controlPoint, tolerance) ||
1218
+ !part1.endPoint.eq(part2.endPoint, tolerance)
1219
+ ) {
1220
+ return false;
1221
+ }
1222
+ break;
1223
+ default: {
1224
+ const exhaustivenessCheck: never = part1;
1225
+ return exhaustivenessCheck;
1208
1226
  }
1209
- break;
1210
- default:
1211
- {
1212
- const exhaustivenessCheck: never = part1;
1213
- return exhaustivenessCheck;
1214
- }
1215
1227
  }
1216
1228
  }
1217
1229
 
@@ -1225,7 +1237,7 @@ export class Path {
1225
1237
  * border around `rect`. Otherwise, the resultant path is just the border
1226
1238
  * of `rect`.
1227
1239
  */
1228
- public static fromRect(rect: Rect2, lineWidth: number|null = null): Path {
1240
+ public static fromRect(rect: Rect2, lineWidth: number | null = null): Path {
1229
1241
  const commands: PathCommand[] = [];
1230
1242
 
1231
1243
  let corners;
@@ -1237,18 +1249,14 @@ export class Path {
1237
1249
  const cornerToEdge = Vec2.of(lineWidth, lineWidth).times(0.5);
1238
1250
  const innerRect = Rect2.fromCorners(
1239
1251
  rect.topLeft.plus(cornerToEdge),
1240
- rect.bottomRight.minus(cornerToEdge)
1252
+ rect.bottomRight.minus(cornerToEdge),
1241
1253
  );
1242
1254
  const outerRect = Rect2.fromCorners(
1243
1255
  rect.topLeft.minus(cornerToEdge),
1244
- rect.bottomRight.plus(cornerToEdge)
1256
+ rect.bottomRight.plus(cornerToEdge),
1245
1257
  );
1246
1258
 
1247
- corners = [
1248
- innerRect.corners[3],
1249
- ...innerRect.corners,
1250
- ...outerRect.corners.reverse(),
1251
- ];
1259
+ corners = [innerRect.corners[3], ...innerRect.corners, ...outerRect.corners.reverse()];
1252
1260
  startPoint = outerRect.corners[3];
1253
1261
  } else {
1254
1262
  corners = rect.corners.slice(1);
@@ -1271,7 +1279,7 @@ export class Path {
1271
1279
  return new Path(startPoint, commands);
1272
1280
  }
1273
1281
 
1274
- private cachedStringVersion: string|null = null;
1282
+ private cachedStringVersion: string | null = null;
1275
1283
 
1276
1284
  /**
1277
1285
  * Convert to an [SVG path representation](https://developer.mozilla.org/en-US/docs/Web/SVG/Tutorial/Paths).
@@ -1302,10 +1310,14 @@ export class Path {
1302
1310
 
1303
1311
  // @param onlyAbsCommands - True if we should avoid converting absolute coordinates to relative offsets -- such
1304
1312
  // conversions can lead to smaller output strings, but also take time.
1305
- public static toString(startPoint: Point2, parts: PathCommand[], onlyAbsCommands?: boolean): string {
1313
+ public static toString(
1314
+ startPoint: Point2,
1315
+ parts: PathCommand[],
1316
+ onlyAbsCommands?: boolean,
1317
+ ): string {
1306
1318
  const result: string[] = [];
1307
1319
 
1308
- let prevPoint: Point2|undefined;
1320
+ let prevPoint: Point2 | undefined;
1309
1321
  const addCommand = (command: string, ...points: Point2[]) => {
1310
1322
  const absoluteCommandParts: string[] = [];
1311
1323
  const relativeCommandParts: string[] = [];
@@ -1319,8 +1331,18 @@ export class Path {
1319
1331
 
1320
1332
  // Relative commands are often shorter as strings than absolute commands.
1321
1333
  if (!makeAbsCommand) {
1322
- const xComponentRelative = toStringOfSamePrecision(point.x - prevPoint!.x, xComponent, roundedPrevX, roundedPrevY);
1323
- const yComponentRelative = toStringOfSamePrecision(point.y - prevPoint!.y, yComponent, roundedPrevX, roundedPrevY);
1334
+ const xComponentRelative = toStringOfSamePrecision(
1335
+ point.x - prevPoint!.x,
1336
+ xComponent,
1337
+ roundedPrevX,
1338
+ roundedPrevY,
1339
+ );
1340
+ const yComponentRelative = toStringOfSamePrecision(
1341
+ point.y - prevPoint!.y,
1342
+ yComponent,
1343
+ roundedPrevX,
1344
+ roundedPrevY,
1345
+ );
1324
1346
 
1325
1347
  // No need for an additional separator if it starts with a '-'
1326
1348
  if (yComponentRelative.charAt(0) === '-') {
@@ -1363,21 +1385,21 @@ export class Path {
1363
1385
  const part = parts[i];
1364
1386
 
1365
1387
  switch (part.kind) {
1366
- case PathCommandType.MoveTo:
1367
- addCommand('M', part.point);
1368
- break;
1369
- case PathCommandType.LineTo:
1370
- addCommand('L', part.point);
1371
- break;
1372
- case PathCommandType.CubicBezierTo:
1373
- addCommand('C', part.controlPoint1, part.controlPoint2, part.endPoint);
1374
- break;
1375
- case PathCommandType.QuadraticBezierTo:
1376
- addCommand('Q', part.controlPoint, part.endPoint);
1377
- break;
1378
- default:
1379
- exhaustivenessCheck = part;
1380
- return exhaustivenessCheck;
1388
+ case PathCommandType.MoveTo:
1389
+ addCommand('M', part.point);
1390
+ break;
1391
+ case PathCommandType.LineTo:
1392
+ addCommand('L', part.point);
1393
+ break;
1394
+ case PathCommandType.CubicBezierTo:
1395
+ addCommand('C', part.controlPoint1, part.controlPoint2, part.endPoint);
1396
+ break;
1397
+ case PathCommandType.QuadraticBezierTo:
1398
+ addCommand('Q', part.controlPoint, part.endPoint);
1399
+ break;
1400
+ default:
1401
+ exhaustivenessCheck = part;
1402
+ return exhaustivenessCheck;
1381
1403
  }
1382
1404
  }
1383
1405
 
@@ -1410,12 +1432,11 @@ export class Path {
1410
1432
  pathString = pathString.split('\n').join(' ');
1411
1433
 
1412
1434
  let lastPos: Point2 = Vec2.zero;
1413
- let firstPos: Point2|null = null;
1414
- let startPos: Point2|null = null;
1435
+ let firstPos: Point2 | null = null;
1436
+ let startPos: Point2 | null = null;
1415
1437
  let isFirstCommand: boolean = true;
1416
1438
  const commands: PathCommand[] = [];
1417
1439
 
1418
-
1419
1440
  const moveTo = (point: Point2) => {
1420
1441
  // The first moveTo/lineTo is already handled by the [startPoint] parameter of the Path constructor.
1421
1442
  if (isFirstCommand) {
@@ -1455,36 +1476,38 @@ export class Path {
1455
1476
  });
1456
1477
  };
1457
1478
  const commandArgCounts: Record<string, number> = {
1458
- 'm': 1,
1459
- 'l': 1,
1460
- 'c': 3,
1461
- 'q': 2,
1462
- 'z': 0,
1463
- 'h': 1,
1464
- 'v': 1,
1479
+ m: 1,
1480
+ l: 1,
1481
+ c: 3,
1482
+ q: 2,
1483
+ z: 0,
1484
+ h: 1,
1485
+ v: 1,
1465
1486
  };
1466
1487
 
1467
1488
  // Each command: Command character followed by anything that isn't a command character
1468
- const commandExp = /([MZLHVCSQTA])\s*([^MZLHVCSQTA]*)/ig;
1489
+ const commandExp = /([MZLHVCSQTA])\s*([^MZLHVCSQTA]*)/gi;
1469
1490
  let current;
1470
1491
  while ((current = commandExp.exec(pathString)) !== null) {
1471
- const argParts = current[2].trim().split(/[^0-9Ee.-]/).filter(
1472
- part => part.length > 0
1473
- ).reduce((accumualtor: string[], current: string): string[] => {
1474
- // As of 09/2022, iOS Safari doesn't support support lookbehind in regular
1475
- // expressions. As such, we need an alternative.
1476
- // Because '-' can be used as a path separator, unless preceeded by an 'e' (as in 1e-5),
1477
- // we need special cases:
1478
- current = current.replace(/([^eE])[-]/g, '$1 -');
1479
- const parts = current.split(' -');
1480
- if (parts[0] !== '') {
1481
- accumualtor.push(parts[0]);
1482
- }
1483
- accumualtor.push(...parts.slice(1).map(part => `-${part}`));
1484
- return accumualtor;
1485
- }, []);
1492
+ const argParts = current[2]
1493
+ .trim()
1494
+ .split(/[^0-9Ee.-]/)
1495
+ .filter((part) => part.length > 0)
1496
+ .reduce((accumualtor: string[], current: string): string[] => {
1497
+ // As of 09/2022, iOS Safari doesn't support support lookbehind in regular
1498
+ // expressions. As such, we need an alternative.
1499
+ // Because '-' can be used as a path separator, unless preceeded by an 'e' (as in 1e-5),
1500
+ // we need special cases:
1501
+ current = current.replace(/([^eE])[-]/g, '$1 -');
1502
+ const parts = current.split(' -');
1503
+ if (parts[0] !== '') {
1504
+ accumualtor.push(parts[0]);
1505
+ }
1506
+ accumualtor.push(...parts.slice(1).map((part) => `-${part}`));
1507
+ return accumualtor;
1508
+ }, []);
1486
1509
 
1487
- let numericArgs = argParts.map(arg => parseFloat(arg));
1510
+ let numericArgs = argParts.map((arg) => parseFloat(arg));
1488
1511
 
1489
1512
  let commandChar = current[1].toLowerCase();
1490
1513
  let uppercaseCommand = current[1] !== commandChar;
@@ -1501,7 +1524,7 @@ export class Path {
1501
1524
  commandChar = 'l';
1502
1525
  } else if (commandChar === 'z') {
1503
1526
  if (firstPos) {
1504
- numericArgs = [ firstPos.x, firstPos.y ];
1527
+ numericArgs = [firstPos.x, firstPos.y];
1505
1528
  firstPos = lastPos;
1506
1529
  } else {
1507
1530
  continue;
@@ -1512,65 +1535,66 @@ export class Path {
1512
1535
  commandChar = 'l';
1513
1536
  }
1514
1537
 
1515
-
1516
1538
  const commandArgCount: number = commandArgCounts[commandChar] ?? 0;
1517
- const allArgs = numericArgs.reduce((
1518
- accumulator: Point2[], current, index, parts
1519
- ): Point2[] => {
1520
- if (index % 2 !== 0) {
1521
- const currentAsFloat = current;
1522
- const prevAsFloat = parts[index - 1];
1523
- return accumulator.concat(Vec2.of(prevAsFloat, currentAsFloat));
1524
- } else {
1525
- return accumulator;
1526
- }
1527
- }, []).map((coordinate, index): Point2 => {
1528
- // Lowercase commands are relative, uppercase commands use absolute
1529
- // positioning
1530
- let newPos;
1531
- if (uppercaseCommand) {
1532
- newPos = coordinate;
1533
- } else {
1534
- newPos = lastPos.plus(coordinate);
1535
- }
1539
+ const allArgs = numericArgs
1540
+ .reduce((accumulator: Point2[], current, index, parts): Point2[] => {
1541
+ if (index % 2 !== 0) {
1542
+ const currentAsFloat = current;
1543
+ const prevAsFloat = parts[index - 1];
1544
+ return accumulator.concat(Vec2.of(prevAsFloat, currentAsFloat));
1545
+ } else {
1546
+ return accumulator;
1547
+ }
1548
+ }, [])
1549
+ .map((coordinate, index): Point2 => {
1550
+ // Lowercase commands are relative, uppercase commands use absolute
1551
+ // positioning
1552
+ let newPos;
1553
+ if (uppercaseCommand) {
1554
+ newPos = coordinate;
1555
+ } else {
1556
+ newPos = lastPos.plus(coordinate);
1557
+ }
1536
1558
 
1537
- if ((index + 1) % commandArgCount === 0) {
1538
- lastPos = newPos;
1539
- }
1559
+ if ((index + 1) % commandArgCount === 0) {
1560
+ lastPos = newPos;
1561
+ }
1540
1562
 
1541
- return newPos;
1542
- });
1563
+ return newPos;
1564
+ });
1543
1565
 
1544
1566
  if (allArgs.length % commandArgCount !== 0) {
1545
- throw new Error([
1546
- `Incorrect number of arguments: got ${JSON.stringify(allArgs)} with a length of ${allArgs.length} ≠ ${commandArgCount}k, k ∈ ℤ.`,
1547
- `The number of arguments to ${commandChar} must be a multiple of ${commandArgCount}!`,
1548
- `Command: ${current[0]}`,
1549
- ].join('\n'));
1567
+ throw new Error(
1568
+ [
1569
+ `Incorrect number of arguments: got ${JSON.stringify(allArgs)} with a length of ${allArgs.length} ≠ ${commandArgCount}k, k ∈ ℤ.`,
1570
+ `The number of arguments to ${commandChar} must be a multiple of ${commandArgCount}!`,
1571
+ `Command: ${current[0]}`,
1572
+ ].join('\n'),
1573
+ );
1550
1574
  }
1551
1575
 
1552
1576
  for (let argPos = 0; argPos < allArgs.length; argPos += commandArgCount) {
1553
1577
  const args = allArgs.slice(argPos, argPos + commandArgCount);
1554
1578
 
1555
1579
  switch (commandChar.toLowerCase()) {
1556
- case 'm':
1557
- if (argPos === 0) {
1558
- moveTo(args[0]);
1559
- } else {
1580
+ case 'm':
1581
+ if (argPos === 0) {
1582
+ moveTo(args[0]);
1583
+ } else {
1584
+ lineTo(args[0]);
1585
+ }
1586
+ break;
1587
+ case 'l':
1560
1588
  lineTo(args[0]);
1561
- }
1562
- break;
1563
- case 'l':
1564
- lineTo(args[0]);
1565
- break;
1566
- case 'c':
1567
- cubicBezierTo(args[0], args[1], args[2]);
1568
- break;
1569
- case 'q':
1570
- quadraticBeierTo(args[0], args[1]);
1571
- break;
1572
- default:
1573
- throw new Error(`Unknown path command ${commandChar}`);
1589
+ break;
1590
+ case 'c':
1591
+ cubicBezierTo(args[0], args[1], args[2]);
1592
+ break;
1593
+ case 'q':
1594
+ quadraticBeierTo(args[0], args[1]);
1595
+ break;
1596
+ default:
1597
+ throw new Error(`Unknown path command ${commandChar}`);
1574
1598
  }
1575
1599
 
1576
1600
  isFirstCommand = false;
@@ -1595,10 +1619,12 @@ export class Path {
1595
1619
 
1596
1620
  const hull = convexHull2Of(points);
1597
1621
 
1598
- const commands = hull.slice(1).map((p): LinePathCommand => ({
1599
- kind: PathCommandType.LineTo,
1600
- point: p,
1601
- }));
1622
+ const commands = hull.slice(1).map(
1623
+ (p): LinePathCommand => ({
1624
+ kind: PathCommandType.LineTo,
1625
+ point: p,
1626
+ }),
1627
+ );
1602
1628
  // Close -- connect back to the start
1603
1629
  commands.push({
1604
1630
  kind: PathCommandType.LineTo,