@js-draw/math 1.21.2 → 1.22.0

Sign up to get free protection for your applications and to get access to all the features.
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,