@js-draw/math 1.21.3 → 1.23.1

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 (81) hide show
  1. package/build-config.json +1 -1
  2. package/dist/cjs/Color4.d.ts +24 -1
  3. package/dist/cjs/Color4.js +35 -3
  4. package/dist/cjs/Mat33.d.ts +21 -11
  5. package/dist/cjs/Mat33.js +28 -24
  6. package/dist/cjs/Vec3.d.ts +12 -3
  7. package/dist/cjs/Vec3.js +20 -9
  8. package/dist/cjs/lib.d.ts +3 -0
  9. package/dist/cjs/lib.js +3 -0
  10. package/dist/cjs/shapes/BezierJSWrapper.d.ts +2 -0
  11. package/dist/cjs/shapes/BezierJSWrapper.js +22 -13
  12. package/dist/cjs/shapes/LineSegment2.js +13 -17
  13. package/dist/cjs/shapes/Parameterized2DShape.js +1 -1
  14. package/dist/cjs/shapes/Path.d.ts +1 -0
  15. package/dist/cjs/shapes/Path.js +50 -47
  16. package/dist/cjs/shapes/QuadraticBezier.d.ts +19 -2
  17. package/dist/cjs/shapes/QuadraticBezier.js +26 -3
  18. package/dist/cjs/shapes/Rect2.d.ts +13 -0
  19. package/dist/cjs/shapes/Rect2.js +35 -16
  20. package/dist/cjs/shapes/Triangle.js +4 -5
  21. package/dist/cjs/utils/convexHull2Of.js +3 -3
  22. package/dist/mjs/Color4.d.ts +24 -1
  23. package/dist/mjs/Color4.mjs +35 -3
  24. package/dist/mjs/Mat33.d.ts +21 -11
  25. package/dist/mjs/Mat33.mjs +28 -24
  26. package/dist/mjs/Vec3.d.ts +12 -3
  27. package/dist/mjs/Vec3.mjs +20 -9
  28. package/dist/mjs/lib.d.ts +3 -0
  29. package/dist/mjs/lib.mjs +3 -0
  30. package/dist/mjs/shapes/BezierJSWrapper.d.ts +2 -0
  31. package/dist/mjs/shapes/BezierJSWrapper.mjs +22 -13
  32. package/dist/mjs/shapes/LineSegment2.mjs +13 -17
  33. package/dist/mjs/shapes/Parameterized2DShape.mjs +1 -1
  34. package/dist/mjs/shapes/Path.d.ts +1 -0
  35. package/dist/mjs/shapes/Path.mjs +50 -47
  36. package/dist/mjs/shapes/QuadraticBezier.d.ts +19 -2
  37. package/dist/mjs/shapes/QuadraticBezier.mjs +26 -3
  38. package/dist/mjs/shapes/Rect2.d.ts +13 -0
  39. package/dist/mjs/shapes/Rect2.mjs +35 -16
  40. package/dist/mjs/shapes/Triangle.mjs +4 -5
  41. package/dist/mjs/utils/convexHull2Of.mjs +3 -3
  42. package/dist-test/test_imports/test-require.cjs +1 -1
  43. package/package.json +3 -3
  44. package/src/Color4.test.ts +21 -21
  45. package/src/Color4.ts +61 -18
  46. package/src/Mat33.fromCSSMatrix.test.ts +32 -46
  47. package/src/Mat33.test.ts +64 -102
  48. package/src/Mat33.ts +81 -104
  49. package/src/Vec2.test.ts +3 -3
  50. package/src/Vec3.test.ts +2 -3
  51. package/src/Vec3.ts +46 -61
  52. package/src/lib.ts +3 -2
  53. package/src/polynomial/solveQuadratic.test.ts +39 -13
  54. package/src/polynomial/solveQuadratic.ts +5 -6
  55. package/src/rounding/cleanUpNumber.test.ts +1 -1
  56. package/src/rounding/constants.ts +1 -3
  57. package/src/rounding/getLenAfterDecimal.ts +1 -2
  58. package/src/rounding/lib.ts +1 -2
  59. package/src/rounding/toRoundedString.test.ts +1 -1
  60. package/src/rounding/toStringOfSamePrecision.test.ts +1 -2
  61. package/src/rounding/toStringOfSamePrecision.ts +1 -1
  62. package/src/shapes/BezierJSWrapper.ts +56 -37
  63. package/src/shapes/CubicBezier.ts +3 -3
  64. package/src/shapes/LineSegment2.test.ts +24 -17
  65. package/src/shapes/LineSegment2.ts +26 -29
  66. package/src/shapes/Parameterized2DShape.ts +5 -4
  67. package/src/shapes/Path.fromString.test.ts +5 -5
  68. package/src/shapes/Path.test.ts +122 -120
  69. package/src/shapes/Path.toString.test.ts +7 -7
  70. package/src/shapes/Path.ts +379 -352
  71. package/src/shapes/PointShape2D.ts +3 -3
  72. package/src/shapes/QuadraticBezier.test.ts +27 -21
  73. package/src/shapes/QuadraticBezier.ts +26 -11
  74. package/src/shapes/Rect2.test.ts +44 -75
  75. package/src/shapes/Rect2.ts +47 -35
  76. package/src/shapes/Triangle.test.ts +31 -29
  77. package/src/shapes/Triangle.ts +17 -18
  78. package/src/utils/convexHull2Of.test.ts +54 -15
  79. package/src/utils/convexHull2Of.ts +9 -7
  80. package/tsconfig.json +1 -3
  81. package/typedoc.json +2 -2
@@ -20,9 +20,9 @@ class PointShape2D extends Parameterized2DShape {
20
20
 
21
21
  public override argIntersectsLineSegment(lineSegment: LineSegment2, epsilon?: number): number[] {
22
22
  if (lineSegment.containsPoint(this.p, epsilon)) {
23
- return [ 0 ];
23
+ return [0];
24
24
  }
25
- return [ ];
25
+ return [];
26
26
  }
27
27
 
28
28
  public override getTightBoundingBox(): Rect2 {
@@ -57,4 +57,4 @@ class PointShape2D extends Parameterized2DShape {
57
57
  }
58
58
  }
59
59
 
60
- export default PointShape2D;
60
+ export default PointShape2D;
@@ -27,24 +27,27 @@ describe('QuadraticBezier', () => {
27
27
  });
28
28
 
29
29
  test.each([
30
- [ new QuadraticBezier(Vec2.zero, Vec2.unitX, Vec2.unitY), Vec2.zero, 0 ],
31
- [ new QuadraticBezier(Vec2.zero, Vec2.unitX, Vec2.unitY), Vec2.unitY, 1 ],
30
+ [new QuadraticBezier(Vec2.zero, Vec2.unitX, Vec2.unitY), Vec2.zero, 0],
31
+ [new QuadraticBezier(Vec2.zero, Vec2.unitX, Vec2.unitY), Vec2.unitY, 1],
32
32
 
33
- [ new QuadraticBezier(Vec2.zero, Vec2.of(0.5, 0), Vec2.of(1, 0)), Vec2.of(0.4, 0), 0.4],
34
- [ new QuadraticBezier(Vec2.zero, Vec2.of(0, 0.5), Vec2.of(0, 1)), Vec2.of(0, 0.4), 0.4],
35
- [ new QuadraticBezier(Vec2.zero, Vec2.unitX, Vec2.unitY), Vec2.unitX, 0.42514 ],
33
+ [new QuadraticBezier(Vec2.zero, Vec2.of(0.5, 0), Vec2.of(1, 0)), Vec2.of(0.4, 0), 0.4],
34
+ [new QuadraticBezier(Vec2.zero, Vec2.of(0, 0.5), Vec2.of(0, 1)), Vec2.of(0, 0.4), 0.4],
35
+ [new QuadraticBezier(Vec2.zero, Vec2.unitX, Vec2.unitY), Vec2.unitX, 0.42514],
36
36
 
37
37
  // Should not return an out-of-range parameter
38
- [ new QuadraticBezier(Vec2.zero, Vec2.of(0, 0.5), Vec2.unitY), Vec2.of(0, -1000), 0 ],
39
- [ new QuadraticBezier(Vec2.zero, Vec2.of(0, 0.5), Vec2.unitY), Vec2.of(0, 1000), 1 ],
38
+ [new QuadraticBezier(Vec2.zero, Vec2.of(0, 0.5), Vec2.unitY), Vec2.of(0, -1000), 0],
39
+ [new QuadraticBezier(Vec2.zero, Vec2.of(0, 0.5), Vec2.unitY), Vec2.of(0, 1000), 1],
40
40
 
41
41
  // Edge case -- just a point
42
- [ new QuadraticBezier(Vec2.zero, Vec2.zero, Vec2.zero), Vec2.of(0, 1000), 0 ],
43
- ])('nearestPointTo should return the nearest point and parameter value on %s to %s', (bezier, point, expectedParameter) => {
44
- const nearest = bezier.nearestPointTo(point);
45
- expect(nearest.parameterValue).toBeCloseTo(expectedParameter, 0.0001);
46
- expect(nearest.point).objEq(bezier.at(nearest.parameterValue));
47
- });
42
+ [new QuadraticBezier(Vec2.zero, Vec2.zero, Vec2.zero), Vec2.of(0, 1000), 0],
43
+ ])(
44
+ 'nearestPointTo should return the nearest point and parameter value on %s to %s',
45
+ (bezier, point, expectedParameter) => {
46
+ const nearest = bezier.nearestPointTo(point);
47
+ expect(nearest.parameterValue).toBeCloseTo(expectedParameter, 0.0001);
48
+ expect(nearest.point).objEq(bezier.at(nearest.parameterValue));
49
+ },
50
+ );
48
51
 
49
52
  test('.normalAt should return a unit normal vector at the given parameter value', () => {
50
53
  const curves = [
@@ -72,17 +75,20 @@ describe('QuadraticBezier', () => {
72
75
  new QuadraticBezier(Vec2.zero, Vec2.unitY, Vec2.unitY.times(2)),
73
76
  new QuadraticBezier(Vec2.zero, Vec2.unitX, Vec2.unitY),
74
77
  new QuadraticBezier(Vec2.zero, Vec2.unitY, Vec2.unitX),
75
- ])('.derivativeAt should return a derivative vector with the correct direction (curve: %s)', (curve) => {
76
- for (let t = 0; t < 1; t += 0.1) {
77
- const derivative = curve.derivativeAt(t);
78
- const derivativeApprox = curve.at(t + 0.001).minus(curve.at(t - 0.001));
79
- expect(derivativeApprox.normalized()).objEq(derivative.normalized(), 0.01);
80
- }
81
- });
78
+ ])(
79
+ '.derivativeAt should return a derivative vector with the correct direction (curve: %s)',
80
+ (curve) => {
81
+ for (let t = 0; t < 1; t += 0.1) {
82
+ const derivative = curve.derivativeAt(t);
83
+ const derivativeApprox = curve.at(t + 0.001).minus(curve.at(t - 0.001));
84
+ expect(derivativeApprox.normalized()).objEq(derivative.normalized(), 0.01);
85
+ }
86
+ },
87
+ );
82
88
 
83
89
  test('should support Bezier-Bezier intersections', () => {
84
90
  const b1 = new QuadraticBezier(Vec2.zero, Vec2.unitX, Vec2.unitY);
85
91
  const b2 = new QuadraticBezier(Vec2.of(-1, 0.5), Vec2.of(0, 0.6), Vec2.of(1, 0.4));
86
92
  expect(b1.intersectsBezier(b2)).toHaveLength(1);
87
93
  });
88
- });
94
+ });
@@ -4,15 +4,35 @@ import BezierJSWrapper from './BezierJSWrapper';
4
4
  import Rect2 from './Rect2';
5
5
 
6
6
  /**
7
- * Represents a 2D Bézier curve.
7
+ * Represents a 2D [Bézier curve](https://en.wikipedia.org/wiki/B%C3%A9zier_curve).
8
8
  *
9
- * **Note**: Many Bézier operations use `bezier-js`'s.
9
+ * Example:
10
+ * ```ts,runnable,console
11
+ * import { QuadraticBezier, Vec2 } from '@js-draw/math';
12
+ *
13
+ * const startPoint = Vec2.of(4, 3);
14
+ * const controlPoint = Vec2.of(1, 1);
15
+ * const endPoint = Vec2.of(1, 3);
16
+ *
17
+ * const curve = new QuadraticBezier(
18
+ * startPoint,
19
+ * controlPoint,
20
+ * endPoint,
21
+ * );
22
+ *
23
+ * console.log('Curve:', curve);
24
+ * ```
25
+ *
26
+ * **Note**: Some Bézier operations internally use the `bezier-js` library.
10
27
  */
11
28
  export class QuadraticBezier extends BezierJSWrapper {
12
29
  public constructor(
30
+ // Start point
13
31
  public readonly p0: Point2,
32
+ // Control point
14
33
  public readonly p1: Point2,
15
- public readonly p2: Point2
34
+ // End point
35
+ public readonly p2: Point2,
16
36
  ) {
17
37
  super();
18
38
  }
@@ -78,7 +98,7 @@ export class QuadraticBezier extends BezierJSWrapper {
78
98
 
79
99
  /** @returns an overestimate of this shape's bounding box. */
80
100
  public override getLooseBoundingBox(): Rect2 {
81
- return Rect2.bboxOf([ this.p0, this.p1, this.p2 ]);
101
+ return Rect2.bboxOf([this.p0, this.p1, this.p2]);
82
102
  }
83
103
 
84
104
  /**
@@ -124,14 +144,9 @@ export class QuadraticBezier extends BezierJSWrapper {
124
144
  const f2ndDerivAtZero = b;
125
145
  const f3rdDerivAtZero = 2 * c;
126
146
 
127
-
128
147
  // Using the first few terms of a Maclaurin series to approximate f'(t),
129
148
  // f'(t) ≈ f'(0) + t f''(0) + t² f'''(0) / 2
130
- let [ min1, min2 ] = solveQuadratic(
131
- f3rdDerivAtZero / 2,
132
- f2ndDerivAtZero,
133
- fDerivAtZero,
134
- );
149
+ let [min1, min2] = solveQuadratic(f3rdDerivAtZero / 2, f2ndDerivAtZero, fDerivAtZero);
135
150
 
136
151
  // If the quadratic has no solutions, approximate.
137
152
  if (isNaN(min1)) {
@@ -153,7 +168,7 @@ export class QuadraticBezier extends BezierJSWrapper {
153
168
  }
154
169
 
155
170
  public override getPoints() {
156
- return [ this.p0, this.p1, this.p2 ];
171
+ return [this.p0, this.p1, this.p2];
157
172
  }
158
173
  }
159
174
  export default QuadraticBezier;
@@ -1,4 +1,3 @@
1
-
2
1
  import Rect2 from './Rect2';
3
2
  import { Vec2 } from '../Vec2';
4
3
  import Mat33 from '../Mat33';
@@ -7,42 +6,21 @@ describe('Rect2', () => {
7
6
  it('width, height should always be positive', () => {
8
7
  expect(new Rect2(-1, -2, -3, 4)).objEq(new Rect2(-4, -2, 3, 4));
9
8
  expect(new Rect2(0, 0, 0, 0).size).objEq(Vec2.zero);
10
- expect(Rect2.fromCorners(
11
- Vec2.of(-3, -3),
12
- Vec2.of(-1, -1)
13
- )).objEq(new Rect2(
14
- -3, -3,
15
- 2, 2
16
- ));
9
+ expect(Rect2.fromCorners(Vec2.of(-3, -3), Vec2.of(-1, -1))).objEq(new Rect2(-3, -3, 2, 2));
17
10
  });
18
11
 
19
12
  it('bounding boxes should be correctly computed', () => {
20
- expect(Rect2.bboxOf([
21
- Vec2.zero,
22
- ])).objEq(Rect2.empty);
23
-
24
- expect(Rect2.bboxOf([
25
- Vec2.of(-1, -1),
26
- Vec2.of(1, 2),
27
- Vec2.of(3, 4),
28
- Vec2.of(1, -4),
29
- ])).objEq(new Rect2(
30
- -1, -4,
31
- 4, 8
32
- ));
33
-
34
- expect(Rect2.bboxOf([
35
- Vec2.zero,
36
- ], 10)).objEq(new Rect2(
37
- -10, -10,
38
- 20, 20
39
- ));
13
+ expect(Rect2.bboxOf([Vec2.zero])).objEq(Rect2.empty);
14
+
15
+ expect(Rect2.bboxOf([Vec2.of(-1, -1), Vec2.of(1, 2), Vec2.of(3, 4), Vec2.of(1, -4)])).objEq(
16
+ new Rect2(-1, -4, 4, 8),
17
+ );
18
+
19
+ expect(Rect2.bboxOf([Vec2.zero], 10)).objEq(new Rect2(-10, -10, 20, 20));
40
20
  });
41
21
 
42
22
  it('"union"s should contain both composite rectangles.', () => {
43
- expect(new Rect2(0, 0, 1, 1).union(new Rect2(1, 1, 2, 2))).objEq(
44
- new Rect2(0, 0, 3, 3)
45
- );
23
+ expect(new Rect2(0, 0, 1, 1).union(new Rect2(1, 1, 2, 2))).objEq(new Rect2(0, 0, 3, 3));
46
24
  expect(Rect2.empty.union(Rect2.empty)).objEq(Rect2.empty);
47
25
  });
48
26
 
@@ -51,21 +29,15 @@ describe('Rect2', () => {
51
29
  });
52
30
 
53
31
  it('should correctly union multiple rectangles', () => {
54
- expect(Rect2.union(new Rect2(0, 0, 1, 1), new Rect2(1, 1, 2, 2))).objEq(
55
- new Rect2(0, 0, 3, 3)
56
- );
32
+ expect(Rect2.union(new Rect2(0, 0, 1, 1), new Rect2(1, 1, 2, 2))).objEq(new Rect2(0, 0, 3, 3));
57
33
 
58
34
  expect(
59
- Rect2.union(new Rect2(-1, 0, 1, 1), new Rect2(1, 1, 2, 2), new Rect2(1, 10, 1, 0.1))
60
- ).objEq(
61
- new Rect2(-1, 0, 4, 10.1)
62
- );
35
+ Rect2.union(new Rect2(-1, 0, 1, 1), new Rect2(1, 1, 2, 2), new Rect2(1, 10, 1, 0.1)),
36
+ ).objEq(new Rect2(-1, 0, 4, 10.1));
63
37
 
64
38
  expect(
65
- Rect2.union(new Rect2(-1, 0, 1, 1), new Rect2(1, -11.1, 2, 2), new Rect2(1, 10, 1, 0.1))
66
- ).objEq(
67
- new Rect2(-1, -11.1, 4, 21.2)
68
- );
39
+ Rect2.union(new Rect2(-1, 0, 1, 1), new Rect2(1, -11.1, 2, 2), new Rect2(1, 10, 1, 0.1)),
40
+ ).objEq(new Rect2(-1, -11.1, 4, 21.2));
69
41
  });
70
42
 
71
43
  it('should contain points that are within a rectangle', () => {
@@ -112,17 +84,15 @@ describe('Rect2', () => {
112
84
  });
113
85
 
114
86
  it('should correctly compute the intersection of one rectangle and several others', () => {
115
- const mainRect = new Rect2(334,156,333,179);
87
+ const mainRect = new Rect2(334, 156, 333, 179);
116
88
  const shouldIntersect = [
117
89
  new Rect2(400.8, 134.8, 8.4, 161.4),
118
- new Rect2(324.8,93,164.4,75.2),
119
- new Rect2(435.8,146.8,213.2,192.6),
120
- new Rect2(550.8,211.8,3.4,3.4),
121
- new Rect2(478.8,93.8,212.4,95.4),
122
- ];
123
- const shouldNotIntersect = [
124
- new Rect2(200, 200, 1, 1),
90
+ new Rect2(324.8, 93, 164.4, 75.2),
91
+ new Rect2(435.8, 146.8, 213.2, 192.6),
92
+ new Rect2(550.8, 211.8, 3.4, 3.4),
93
+ new Rect2(478.8, 93.8, 212.4, 95.4),
125
94
  ];
95
+ const shouldNotIntersect = [new Rect2(200, 200, 1, 1)];
126
96
 
127
97
  for (const rect of shouldIntersect) {
128
98
  expect(mainRect.intersects(rect)).toBe(true);
@@ -135,10 +105,10 @@ describe('Rect2', () => {
135
105
  it('intersecting rectangles should have their intersections correctly computed', () => {
136
106
  expect(new Rect2(-1, -1, 2, 2).intersection(Rect2.empty)).objEq(Rect2.empty);
137
107
  expect(new Rect2(-1, -1, 2, 2).intersection(new Rect2(0, 0, 3, 3))).objEq(
138
- new Rect2(0, 0, 1, 1)
108
+ new Rect2(0, 0, 1, 1),
139
109
  );
140
110
  expect(new Rect2(-2, 0, 1, 2).intersection(new Rect2(-3, 0, 2, 2))).objEq(
141
- new Rect2(-2, 0, 1, 2)
111
+ new Rect2(-2, 0, 1, 2),
142
112
  );
143
113
  expect(new Rect2(-1, -1, 2, 2).intersection(new Rect2(3, 3, 10, 10))).toBe(null);
144
114
  });
@@ -192,39 +162,38 @@ describe('Rect2', () => {
192
162
 
193
163
  describe('divideIntoGrid', () => {
194
164
  it('division of unit square', () => {
195
- expect(Rect2.unitSquare.divideIntoGrid(2, 2)).toMatchObject(
196
- [
197
- new Rect2(0, 0, 0.5, 0.5), new Rect2(0.5, 0, 0.5, 0.5),
198
- new Rect2(0, 0.5, 0.5, 0.5), new Rect2(0.5, 0.5, 0.5, 0.5),
199
- ]
200
- );
165
+ expect(Rect2.unitSquare.divideIntoGrid(2, 2)).toMatchObject([
166
+ new Rect2(0, 0, 0.5, 0.5),
167
+ new Rect2(0.5, 0, 0.5, 0.5),
168
+ new Rect2(0, 0.5, 0.5, 0.5),
169
+ new Rect2(0.5, 0.5, 0.5, 0.5),
170
+ ]);
201
171
  expect(Rect2.unitSquare.divideIntoGrid(0, 0).length).toBe(0);
202
172
  expect(Rect2.unitSquare.divideIntoGrid(100, 0).length).toBe(0);
203
- expect(Rect2.unitSquare.divideIntoGrid(4, 1)).toMatchObject(
204
- [
205
- new Rect2(0, 0, 0.25, 1), new Rect2(0.25, 0, 0.25, 1),
206
- new Rect2(0.5, 0, 0.25, 1), new Rect2(0.75, 0, 0.25, 1),
207
- ]
208
- );
173
+ expect(Rect2.unitSquare.divideIntoGrid(4, 1)).toMatchObject([
174
+ new Rect2(0, 0, 0.25, 1),
175
+ new Rect2(0.25, 0, 0.25, 1),
176
+ new Rect2(0.5, 0, 0.25, 1),
177
+ new Rect2(0.75, 0, 0.25, 1),
178
+ ]);
209
179
  });
210
180
  it('division of translated square', () => {
211
- expect(new Rect2(3, -3, 4, 4).divideIntoGrid(2, 1)).toMatchObject(
212
- [
213
- new Rect2(3, -3, 2, 4), new Rect2(5, -3, 2, 4),
214
- ]
215
- );
181
+ expect(new Rect2(3, -3, 4, 4).divideIntoGrid(2, 1)).toMatchObject([
182
+ new Rect2(3, -3, 2, 4),
183
+ new Rect2(5, -3, 2, 4),
184
+ ]);
216
185
  });
217
186
  it('division of empty square', () => {
218
187
  expect(Rect2.empty.divideIntoGrid(1000, 10000).length).toBe(1);
219
188
  });
220
189
 
221
190
  it('division of rectangle', () => {
222
- expect(new Rect2(0, 0, 2, 1).divideIntoGrid(2, 2)).toMatchObject(
223
- [
224
- new Rect2(0, 0, 1, 0.5), new Rect2(1, 0, 1, 0.5),
225
- new Rect2(0, 0.5, 1, 0.5), new Rect2(1, 0.5, 1, 0.5),
226
- ]
227
- );
191
+ expect(new Rect2(0, 0, 2, 1).divideIntoGrid(2, 2)).toMatchObject([
192
+ new Rect2(0, 0, 1, 0.5),
193
+ new Rect2(1, 0, 1, 0.5),
194
+ new Rect2(0, 0.5, 1, 0.5),
195
+ new Rect2(1, 0.5, 1, 0.5),
196
+ ]);
228
197
  });
229
198
  });
230
199
 
@@ -17,6 +17,18 @@ export interface RectTemplate {
17
17
  /**
18
18
  * Represents a rectangle in 2D space, parallel to the XY axes.
19
19
  *
20
+ * **Example**:
21
+ * ```ts,runnable,console
22
+ * import { Rect2, Vec2 } from '@js-draw/math';
23
+ *
24
+ * const rect = Rect2.fromCorners(
25
+ * Vec2.of(0, 0),
26
+ * Vec2.of(10, 10),
27
+ * );
28
+ * console.log('area', rect.area);
29
+ * console.log('topLeft', rect.topLeft);
30
+ * ```
31
+ *
20
32
  * `invariant: w ≥ 0, h ≥ 0, immutable`
21
33
  */
22
34
  export class Rect2 extends Abstract2DShape {
@@ -28,10 +40,14 @@ export class Rect2 extends Abstract2DShape {
28
40
  public readonly area: number;
29
41
 
30
42
  public constructor(
43
+ // Top left x coordinate
31
44
  public readonly x: number,
45
+ // Top left y coordinate
32
46
  public readonly y: number,
47
+ // Width
33
48
  public readonly w: number,
34
- public readonly h: number
49
+ // Height
50
+ public readonly h: number,
35
51
  ) {
36
52
  super();
37
53
 
@@ -61,14 +77,22 @@ export class Rect2 extends Abstract2DShape {
61
77
  }
62
78
 
63
79
  public override containsPoint(other: Point2): boolean {
64
- return this.x <= other.x && this.y <= other.y
65
- && this.x + this.w >= other.x && this.y + this.h >= other.y;
80
+ return (
81
+ this.x <= other.x &&
82
+ this.y <= other.y &&
83
+ this.x + this.w >= other.x &&
84
+ this.y + this.h >= other.y
85
+ );
66
86
  }
67
87
 
88
+ /** @returns true iff `other` is completely within this `Rect2`. */
68
89
  public containsRect(other: Rect2): boolean {
69
- return this.x <= other.x && this.y <= other.y
70
- && this.x + this.w >= other.x + other.w
71
- && this.y + this.h >= other.y + other.h;
90
+ return (
91
+ this.x <= other.x &&
92
+ this.y <= other.y &&
93
+ this.x + this.w >= other.x + other.w &&
94
+ this.y + this.h >= other.y + other.h
95
+ );
72
96
  }
73
97
 
74
98
  /**
@@ -85,7 +109,6 @@ export class Rect2 extends Abstract2DShape {
85
109
  return false;
86
110
  }
87
111
 
88
-
89
112
  const thisMinY = this.y;
90
113
  const thisMaxY = thisMinY + this.h;
91
114
  const otherMinY = other.y;
@@ -100,7 +123,7 @@ export class Rect2 extends Abstract2DShape {
100
123
 
101
124
  // Returns the overlap of this and [other], or null, if no such
102
125
  // overlap exists
103
- public intersection(other: Rect2): Rect2|null {
126
+ public intersection(other: Rect2): Rect2 | null {
104
127
  if (!this.intersects(other)) {
105
128
  return null;
106
129
  }
@@ -151,10 +174,7 @@ export class Rect2 extends Abstract2DShape {
151
174
  // [margin] is the minimum distance between the new point and the edge
152
175
  // of the resultant rectangle.
153
176
  public grownToPoint(point: Point2, margin: number = 0): Rect2 {
154
- const otherRect = new Rect2(
155
- point.x - margin, point.y - margin,
156
- margin * 2, margin * 2
157
- );
177
+ const otherRect = new Rect2(point.x - margin, point.y - margin, margin * 2, margin * 2);
158
178
  return this.union(otherRect);
159
179
  }
160
180
 
@@ -170,23 +190,23 @@ export class Rect2 extends Abstract2DShape {
170
190
  const yMargin = -Math.min(-margin, this.h / 2);
171
191
 
172
192
  return new Rect2(
173
- this.x - xMargin, this.y - yMargin,
174
- this.w + xMargin * 2, this.h + yMargin * 2,
193
+ this.x - xMargin,
194
+ this.y - yMargin,
195
+ this.w + xMargin * 2,
196
+ this.h + yMargin * 2,
175
197
  );
176
198
  }
177
199
 
178
- return new Rect2(
179
- this.x - margin, this.y - margin, this.w + margin * 2, this.h + margin * 2
180
- );
200
+ return new Rect2(this.x - margin, this.y - margin, this.w + margin * 2, this.h + margin * 2);
181
201
  }
182
202
 
183
203
  public getClosestPointOnBoundaryTo(target: Point2) {
184
- const closestEdgePoints = this.getEdges().map(edge => {
204
+ const closestEdgePoints = this.getEdges().map((edge) => {
185
205
  return edge.closestPointTo(target);
186
206
  });
187
207
 
188
- let closest: Point2|null = null;
189
- let closestDist: number|null = null;
208
+ let closest: Point2 | null = null;
209
+ let closestDist: number | null = null;
190
210
  for (const point of closestEdgePoints) {
191
211
  const dist = point.distanceTo(target);
192
212
  if (closestDist === null || dist < closestDist) {
@@ -211,16 +231,11 @@ export class Rect2 extends Abstract2DShape {
211
231
  }
212
232
 
213
233
  const squareRadius = radius * radius;
214
- return this.corners.every(corner => corner.minus(point).magnitudeSquared() < squareRadius);
234
+ return this.corners.every((corner) => corner.minus(point).magnitudeSquared() < squareRadius);
215
235
  }
216
236
 
217
237
  public get corners(): Point2[] {
218
- return [
219
- this.bottomRight,
220
- this.topRight,
221
- this.topLeft,
222
- this.bottomLeft,
223
- ];
238
+ return [this.bottomRight, this.topRight, this.topLeft, this.bottomLeft];
224
239
  }
225
240
 
226
241
  public get maxDimension() {
@@ -272,7 +287,7 @@ export class Rect2 extends Abstract2DShape {
272
287
 
273
288
  for (const edge of this.getEdges()) {
274
289
  const intersection = edge.intersectsLineSegment(lineSegment);
275
- intersection.forEach(point => result.push(point));
290
+ intersection.forEach((point) => result.push(point));
276
291
  }
277
292
 
278
293
  return result;
@@ -295,7 +310,7 @@ export class Rect2 extends Abstract2DShape {
295
310
  // [affineTransform] is a transformation matrix that both scales and **translates**.
296
311
  // the bounding box of this' four corners after transformed by the given affine transformation.
297
312
  public transformedBoundingBox(affineTransform: Mat33): Rect2 {
298
- return Rect2.bboxOf(this.corners.map(corner => affineTransform.transformVec2(corner)));
313
+ return Rect2.bboxOf(this.corners.map((corner) => affineTransform.transformVec2(corner)));
299
314
  }
300
315
 
301
316
  /** @return true iff this is equal to `other ± tolerance` */
@@ -307,13 +322,12 @@ export class Rect2 extends Abstract2DShape {
307
322
  return `Rect(point(${this.x}, ${this.y}), size(${this.w}, ${this.h}))`;
308
323
  }
309
324
 
310
-
311
325
  public static fromCorners(corner1: Point2, corner2: Point2) {
312
326
  return new Rect2(
313
327
  Math.min(corner1.x, corner2.x),
314
328
  Math.min(corner1.y, corner2.y),
315
329
  Math.abs(corner1.x - corner2.x),
316
- Math.abs(corner1.y - corner2.y)
330
+ Math.abs(corner1.y - corner2.y),
317
331
  );
318
332
  }
319
333
 
@@ -344,7 +358,7 @@ export class Rect2 extends Abstract2DShape {
344
358
 
345
359
  return Rect2.fromCorners(
346
360
  Vec2.of(minX - margin, minY - margin),
347
- Vec2.of(maxX + margin, maxY + margin)
361
+ Vec2.of(maxX + margin, maxY + margin),
348
362
  );
349
363
  }
350
364
 
@@ -369,9 +383,7 @@ export class Rect2 extends Abstract2DShape {
369
383
  maxY = Math.max(maxY, rect.y + rect.h);
370
384
  }
371
385
 
372
- return new Rect2(
373
- minX, minY, maxX - minX, maxY - minY,
374
- );
386
+ return new Rect2(minX, minY, maxX - minX, maxY - minY);
375
387
  }
376
388
 
377
389
  public static of(template: RectTemplate) {
@@ -19,43 +19,45 @@ describe('Triangle', () => {
19
19
  }
20
20
  });
21
21
 
22
- it('signed distance function should be the negative distance to the edge '
23
- + 'of the triangle on the interior of a shape, same as distance outside of shape', () => {
24
- const testTriangle = Triangle.fromVertices(Vec2.of(-1, -1), Vec2.of(0, 1), Vec2.of(1, -1));
25
-
26
- // A point vertically above the triangle: Outside, so positive SDF
27
- expect(testTriangle.signedDistance(Vec2.of(0, 2))).toBeCloseTo(1);
22
+ it(
23
+ 'signed distance function should be the negative distance to the edge ' +
24
+ 'of the triangle on the interior of a shape, same as distance outside of shape',
25
+ () => {
26
+ const testTriangle = Triangle.fromVertices(Vec2.of(-1, -1), Vec2.of(0, 1), Vec2.of(1, -1));
28
27
 
29
- // Similarly, a point vertically below the triangle is outside, so should have positive SDF
30
- expect(testTriangle.signedDistance(Vec2.of(0, -2))).toBeCloseTo(1);
28
+ // A point vertically above the triangle: Outside, so positive SDF
29
+ expect(testTriangle.signedDistance(Vec2.of(0, 2))).toBeCloseTo(1);
31
30
 
32
- // A point just above the left side (and outside the triangle) should also have positive SDF
33
- expect(testTriangle.signedDistance(Vec2.of(-0.8, 0.8))).toBeGreaterThan(0);
31
+ // Similarly, a point vertically below the triangle is outside, so should have positive SDF
32
+ expect(testTriangle.signedDistance(Vec2.of(0, -2))).toBeCloseTo(1);
34
33
 
34
+ // A point just above the left side (and outside the triangle) should also have positive SDF
35
+ expect(testTriangle.signedDistance(Vec2.of(-0.8, 0.8))).toBeGreaterThan(0);
35
36
 
36
- const firstSide = testTriangle.getEdges()[0];
37
- const firstSideMidpoint = firstSide.at(0.5);
38
- const firstSideNormal = firstSide.direction.orthog();
37
+ const firstSide = testTriangle.getEdges()[0];
38
+ const firstSideMidpoint = firstSide.at(0.5);
39
+ const firstSideNormal = firstSide.direction.orthog();
39
40
 
40
- // Move a point towards the first side
41
- for (let t = 0.5; t > -0.5; t -= 0.1) {
42
- const point = firstSideMidpoint.minus(firstSideNormal.times(t));
43
- const distFromSide1 = firstSide.distance(point);
44
- const signedDist = testTriangle.signedDistance(point);
41
+ // Move a point towards the first side
42
+ for (let t = 0.5; t > -0.5; t -= 0.1) {
43
+ const point = firstSideMidpoint.minus(firstSideNormal.times(t));
44
+ const distFromSide1 = firstSide.distance(point);
45
+ const signedDist = testTriangle.signedDistance(point);
45
46
 
46
- // Inside the shape
47
- if (t > 0) {
48
47
  // Inside the shape
49
- expect(testTriangle.containsPoint(point)).toBe(true);
48
+ if (t > 0) {
49
+ // Inside the shape
50
+ expect(testTriangle.containsPoint(point)).toBe(true);
50
51
 
51
- expect(signedDist).toBeCloseTo(-distFromSide1);
52
- } else {
53
- // Outside the shape
54
- expect(testTriangle.containsPoint(point)).toBe(false);
52
+ expect(signedDist).toBeCloseTo(-distFromSide1);
53
+ } else {
54
+ // Outside the shape
55
+ expect(testTriangle.containsPoint(point)).toBe(false);
55
56
 
56
- expect(signedDist).toBeCloseTo(distFromSide1);
57
+ expect(signedDist).toBeCloseTo(distFromSide1);
58
+ }
57
59
  }
58
- }
59
- });
60
+ },
61
+ );
60
62
  });
61
- });
63
+ });