@js-draw/math 1.2.1 → 1.3.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -12,7 +12,7 @@ import Vec3 from './Vec3';
12
12
  * console.log('To string:', Color4.orange.toHexString());
13
13
  * ```
14
14
  */
15
- export default class Color4 {
15
+ export declare class Color4 {
16
16
  /** Red component. Should be in the range [0, 1]. */
17
17
  readonly r: number;
18
18
  /** Green component. ${\tt g} \in [0, 1]$ */
@@ -120,4 +120,4 @@ export default class Color4 {
120
120
  static gray: Color4;
121
121
  static white: Color4;
122
122
  }
123
- export { Color4 };
123
+ export default Color4;
@@ -111,6 +111,10 @@ class Color4 {
111
111
  canvas.width = 1;
112
112
  canvas.height = 1;
113
113
  const ctx = canvas.getContext('2d');
114
+ // Default to black if no canvas is available.
115
+ if (!ctx) {
116
+ return Color4.black;
117
+ }
114
118
  ctx.fillStyle = text;
115
119
  ctx.fillRect(0, 0, 1, 1);
116
120
  const data = ctx.getImageData(0, 0, 1, 1);
@@ -53,6 +53,13 @@ export declare class Path {
53
53
  getExactBBox(): Rect2;
54
54
  private cachedGeometry;
55
55
  get geometry(): GeometryArrayType;
56
+ /**
57
+ * Iterates through the start/end points of each component in this path.
58
+ *
59
+ * If a start point is equivalent to the end point of the previous segment,
60
+ * the point is **not** emitted twice.
61
+ */
62
+ startEndPoints(): Generator<import("../Vec3").Vec3, undefined, unknown>;
56
63
  private cachedPolylineApproximation;
57
64
  polylineApproximation(): LineSegment2[];
58
65
  static computeBBoxForSegment(startPoint: Point2, part: PathCommand): Rect2;
@@ -51,6 +51,7 @@ class Path {
51
51
  let startPoint = this.startPoint;
52
52
  const geometry = [];
53
53
  for (const part of this.parts) {
54
+ let exhaustivenessCheck;
54
55
  switch (part.kind) {
55
56
  case PathCommandType.CubicBezierTo:
56
57
  geometry.push(new CubicBezier_1.default(startPoint, part.controlPoint1, part.controlPoint2, part.endPoint));
@@ -68,11 +69,43 @@ class Path {
68
69
  geometry.push(new PointShape2D_1.default(part.point));
69
70
  startPoint = part.point;
70
71
  break;
72
+ default:
73
+ exhaustivenessCheck = part;
74
+ return exhaustivenessCheck;
71
75
  }
72
76
  }
73
77
  this.cachedGeometry = geometry;
74
78
  return this.cachedGeometry;
75
79
  }
80
+ /**
81
+ * Iterates through the start/end points of each component in this path.
82
+ *
83
+ * If a start point is equivalent to the end point of the previous segment,
84
+ * the point is **not** emitted twice.
85
+ */
86
+ *startEndPoints() {
87
+ yield this.startPoint;
88
+ for (const part of this.parts) {
89
+ let exhaustivenessCheck;
90
+ switch (part.kind) {
91
+ case PathCommandType.CubicBezierTo:
92
+ yield part.endPoint;
93
+ break;
94
+ case PathCommandType.QuadraticBezierTo:
95
+ yield part.endPoint;
96
+ break;
97
+ case PathCommandType.LineTo:
98
+ yield part.point;
99
+ break;
100
+ case PathCommandType.MoveTo:
101
+ yield part.point;
102
+ break;
103
+ default:
104
+ exhaustivenessCheck = part;
105
+ return exhaustivenessCheck;
106
+ }
107
+ }
108
+ }
76
109
  // Approximates this path with a group of line segments.
77
110
  polylineApproximation() {
78
111
  if (this.cachedPolylineApproximation) {
@@ -19,7 +19,6 @@ export declare class Rect2 extends Abstract2DShape {
19
19
  readonly h: number;
20
20
  readonly topLeft: Point2;
21
21
  readonly size: Vec2;
22
- readonly bottomRight: Point2;
23
22
  readonly area: number;
24
23
  constructor(x: number, y: number, w: number, h: number);
25
24
  translatedBy(vec: Vec2): Rect2;
@@ -35,6 +34,7 @@ export declare class Rect2 extends Abstract2DShape {
35
34
  getClosestPointOnBoundaryTo(target: Point2): Vec3;
36
35
  get corners(): Point2[];
37
36
  get maxDimension(): number;
37
+ get bottomRight(): Vec3;
38
38
  get topRight(): Vec3;
39
39
  get bottomLeft(): Vec3;
40
40
  get width(): number;
@@ -26,7 +26,6 @@ class Rect2 extends Abstract2DShape_1.default {
26
26
  // Precompute/store vector forms.
27
27
  this.topLeft = Vec2_1.Vec2.of(this.x, this.y);
28
28
  this.size = Vec2_1.Vec2.of(this.w, this.h);
29
- this.bottomRight = this.topLeft.plus(this.size);
30
29
  this.area = this.w * this.h;
31
30
  }
32
31
  translatedBy(vec) {
@@ -42,8 +41,8 @@ class Rect2 extends Abstract2DShape_1.default {
42
41
  }
43
42
  containsRect(other) {
44
43
  return this.x <= other.x && this.y <= other.y
45
- && this.bottomRight.x >= other.bottomRight.x
46
- && this.bottomRight.y >= other.bottomRight.y;
44
+ && this.x + this.w >= other.x + other.w
45
+ && this.y + this.h >= other.y + other.h;
47
46
  }
48
47
  intersects(other) {
49
48
  // Project along x/y axes.
@@ -116,6 +115,12 @@ class Rect2 extends Abstract2DShape_1.default {
116
115
  if (margin === 0) {
117
116
  return this;
118
117
  }
118
+ // Prevent width/height from being negative
119
+ if (margin < 0) {
120
+ const xMargin = -Math.min(-margin, this.w / 2);
121
+ const yMargin = -Math.min(-margin, this.h / 2);
122
+ return new Rect2(this.x - xMargin, this.y - yMargin, this.w + xMargin * 2, this.h + yMargin * 2);
123
+ }
119
124
  return new Rect2(this.x - margin, this.y - margin, this.w + margin * 2, this.h + margin * 2);
120
125
  }
121
126
  getClosestPointOnBoundaryTo(target) {
@@ -144,6 +149,9 @@ class Rect2 extends Abstract2DShape_1.default {
144
149
  get maxDimension() {
145
150
  return Math.max(this.w, this.h);
146
151
  }
152
+ get bottomRight() {
153
+ return this.topLeft.plus(this.size);
154
+ }
147
155
  get topRight() {
148
156
  return this.bottomRight.plus(Vec2_1.Vec2.of(0, -this.h));
149
157
  }
@@ -234,16 +242,16 @@ class Rect2 extends Abstract2DShape_1.default {
234
242
  return Rect2.empty;
235
243
  }
236
244
  const firstRect = rects[0];
237
- let minX = firstRect.topLeft.x;
238
- let minY = firstRect.topLeft.y;
239
- let maxX = firstRect.bottomRight.x;
240
- let maxY = firstRect.bottomRight.y;
245
+ let minX = firstRect.x;
246
+ let minY = firstRect.y;
247
+ let maxX = firstRect.x + firstRect.w;
248
+ let maxY = firstRect.y + firstRect.h;
241
249
  for (let i = 1; i < rects.length; i++) {
242
250
  const rect = rects[i];
243
- minX = Math.min(minX, rect.topLeft.x);
244
- minY = Math.min(minY, rect.topLeft.y);
245
- maxX = Math.max(maxX, rect.bottomRight.x);
246
- maxY = Math.max(maxY, rect.bottomRight.y);
251
+ minX = Math.min(minX, rect.x);
252
+ minY = Math.min(minY, rect.y);
253
+ maxX = Math.max(maxX, rect.x + rect.w);
254
+ maxY = Math.max(maxY, rect.y + rect.h);
247
255
  }
248
256
  return new Rect2(minX, minY, maxX - minX, maxY - minY);
249
257
  }
@@ -12,7 +12,7 @@ import Vec3 from './Vec3';
12
12
  * console.log('To string:', Color4.orange.toHexString());
13
13
  * ```
14
14
  */
15
- export default class Color4 {
15
+ export declare class Color4 {
16
16
  /** Red component. Should be in the range [0, 1]. */
17
17
  readonly r: number;
18
18
  /** Green component. ${\tt g} \in [0, 1]$ */
@@ -120,4 +120,4 @@ export default class Color4 {
120
120
  static gray: Color4;
121
121
  static white: Color4;
122
122
  }
123
- export { Color4 };
123
+ export default Color4;
@@ -12,7 +12,7 @@ import Vec3 from './Vec3.mjs';
12
12
  * console.log('To string:', Color4.orange.toHexString());
13
13
  * ```
14
14
  */
15
- class Color4 {
15
+ export class Color4 {
16
16
  constructor(
17
17
  /** Red component. Should be in the range [0, 1]. */
18
18
  r,
@@ -105,6 +105,10 @@ class Color4 {
105
105
  canvas.width = 1;
106
106
  canvas.height = 1;
107
107
  const ctx = canvas.getContext('2d');
108
+ // Default to black if no canvas is available.
109
+ if (!ctx) {
110
+ return Color4.black;
111
+ }
108
112
  ctx.fillStyle = text;
109
113
  ctx.fillRect(0, 0, 1, 1);
110
114
  const data = ctx.getImageData(0, 0, 1, 1);
@@ -373,4 +377,3 @@ Color4.black = Color4.ofRGB(0, 0, 0);
373
377
  Color4.gray = Color4.ofRGB(0.5, 0.5, 0.5);
374
378
  Color4.white = Color4.ofRGB(1, 1, 1);
375
379
  export default Color4;
376
- export { Color4 };
@@ -53,6 +53,13 @@ export declare class Path {
53
53
  getExactBBox(): Rect2;
54
54
  private cachedGeometry;
55
55
  get geometry(): GeometryArrayType;
56
+ /**
57
+ * Iterates through the start/end points of each component in this path.
58
+ *
59
+ * If a start point is equivalent to the end point of the previous segment,
60
+ * the point is **not** emitted twice.
61
+ */
62
+ startEndPoints(): Generator<import("../Vec3").Vec3, undefined, unknown>;
56
63
  private cachedPolylineApproximation;
57
64
  polylineApproximation(): LineSegment2[];
58
65
  static computeBBoxForSegment(startPoint: Point2, part: PathCommand): Rect2;
@@ -45,6 +45,7 @@ export class Path {
45
45
  let startPoint = this.startPoint;
46
46
  const geometry = [];
47
47
  for (const part of this.parts) {
48
+ let exhaustivenessCheck;
48
49
  switch (part.kind) {
49
50
  case PathCommandType.CubicBezierTo:
50
51
  geometry.push(new CubicBezier(startPoint, part.controlPoint1, part.controlPoint2, part.endPoint));
@@ -62,11 +63,43 @@ export class Path {
62
63
  geometry.push(new PointShape2D(part.point));
63
64
  startPoint = part.point;
64
65
  break;
66
+ default:
67
+ exhaustivenessCheck = part;
68
+ return exhaustivenessCheck;
65
69
  }
66
70
  }
67
71
  this.cachedGeometry = geometry;
68
72
  return this.cachedGeometry;
69
73
  }
74
+ /**
75
+ * Iterates through the start/end points of each component in this path.
76
+ *
77
+ * If a start point is equivalent to the end point of the previous segment,
78
+ * the point is **not** emitted twice.
79
+ */
80
+ *startEndPoints() {
81
+ yield this.startPoint;
82
+ for (const part of this.parts) {
83
+ let exhaustivenessCheck;
84
+ switch (part.kind) {
85
+ case PathCommandType.CubicBezierTo:
86
+ yield part.endPoint;
87
+ break;
88
+ case PathCommandType.QuadraticBezierTo:
89
+ yield part.endPoint;
90
+ break;
91
+ case PathCommandType.LineTo:
92
+ yield part.point;
93
+ break;
94
+ case PathCommandType.MoveTo:
95
+ yield part.point;
96
+ break;
97
+ default:
98
+ exhaustivenessCheck = part;
99
+ return exhaustivenessCheck;
100
+ }
101
+ }
102
+ }
70
103
  // Approximates this path with a group of line segments.
71
104
  polylineApproximation() {
72
105
  if (this.cachedPolylineApproximation) {
@@ -19,7 +19,6 @@ export declare class Rect2 extends Abstract2DShape {
19
19
  readonly h: number;
20
20
  readonly topLeft: Point2;
21
21
  readonly size: Vec2;
22
- readonly bottomRight: Point2;
23
22
  readonly area: number;
24
23
  constructor(x: number, y: number, w: number, h: number);
25
24
  translatedBy(vec: Vec2): Rect2;
@@ -35,6 +34,7 @@ export declare class Rect2 extends Abstract2DShape {
35
34
  getClosestPointOnBoundaryTo(target: Point2): Vec3;
36
35
  get corners(): Point2[];
37
36
  get maxDimension(): number;
37
+ get bottomRight(): Vec3;
38
38
  get topRight(): Vec3;
39
39
  get bottomLeft(): Vec3;
40
40
  get width(): number;
@@ -20,7 +20,6 @@ export class Rect2 extends Abstract2DShape {
20
20
  // Precompute/store vector forms.
21
21
  this.topLeft = Vec2.of(this.x, this.y);
22
22
  this.size = Vec2.of(this.w, this.h);
23
- this.bottomRight = this.topLeft.plus(this.size);
24
23
  this.area = this.w * this.h;
25
24
  }
26
25
  translatedBy(vec) {
@@ -36,8 +35,8 @@ export class Rect2 extends Abstract2DShape {
36
35
  }
37
36
  containsRect(other) {
38
37
  return this.x <= other.x && this.y <= other.y
39
- && this.bottomRight.x >= other.bottomRight.x
40
- && this.bottomRight.y >= other.bottomRight.y;
38
+ && this.x + this.w >= other.x + other.w
39
+ && this.y + this.h >= other.y + other.h;
41
40
  }
42
41
  intersects(other) {
43
42
  // Project along x/y axes.
@@ -110,6 +109,12 @@ export class Rect2 extends Abstract2DShape {
110
109
  if (margin === 0) {
111
110
  return this;
112
111
  }
112
+ // Prevent width/height from being negative
113
+ if (margin < 0) {
114
+ const xMargin = -Math.min(-margin, this.w / 2);
115
+ const yMargin = -Math.min(-margin, this.h / 2);
116
+ return new Rect2(this.x - xMargin, this.y - yMargin, this.w + xMargin * 2, this.h + yMargin * 2);
117
+ }
113
118
  return new Rect2(this.x - margin, this.y - margin, this.w + margin * 2, this.h + margin * 2);
114
119
  }
115
120
  getClosestPointOnBoundaryTo(target) {
@@ -138,6 +143,9 @@ export class Rect2 extends Abstract2DShape {
138
143
  get maxDimension() {
139
144
  return Math.max(this.w, this.h);
140
145
  }
146
+ get bottomRight() {
147
+ return this.topLeft.plus(this.size);
148
+ }
141
149
  get topRight() {
142
150
  return this.bottomRight.plus(Vec2.of(0, -this.h));
143
151
  }
@@ -228,16 +236,16 @@ export class Rect2 extends Abstract2DShape {
228
236
  return Rect2.empty;
229
237
  }
230
238
  const firstRect = rects[0];
231
- let minX = firstRect.topLeft.x;
232
- let minY = firstRect.topLeft.y;
233
- let maxX = firstRect.bottomRight.x;
234
- let maxY = firstRect.bottomRight.y;
239
+ let minX = firstRect.x;
240
+ let minY = firstRect.y;
241
+ let maxX = firstRect.x + firstRect.w;
242
+ let maxY = firstRect.y + firstRect.h;
235
243
  for (let i = 1; i < rects.length; i++) {
236
244
  const rect = rects[i];
237
- minX = Math.min(minX, rect.topLeft.x);
238
- minY = Math.min(minY, rect.topLeft.y);
239
- maxX = Math.max(maxX, rect.bottomRight.x);
240
- maxY = Math.max(maxY, rect.bottomRight.y);
245
+ minX = Math.min(minX, rect.x);
246
+ minY = Math.min(minY, rect.y);
247
+ maxX = Math.max(maxX, rect.x + rect.w);
248
+ maxY = Math.max(maxY, rect.y + rect.h);
241
249
  }
242
250
  return new Rect2(minX, minY, maxX - minX, maxY - minY);
243
251
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@js-draw/math",
3
- "version": "1.2.1",
3
+ "version": "1.3.0",
4
4
  "description": "A math library for js-draw. ",
5
5
  "types": "./dist/mjs/lib.d.ts",
6
6
  "main": "./dist/cjs/lib.js",
@@ -45,5 +45,5 @@
45
45
  "svg",
46
46
  "math"
47
47
  ],
48
- "gitHead": "0cb756d3150c8b33dd9a3217faa7d18229688f34"
48
+ "gitHead": "46b3d8f819f8e083f6e3e1d01e027e4311355456"
49
49
  }
package/src/Color4.ts CHANGED
@@ -13,7 +13,7 @@ import Vec3 from './Vec3';
13
13
  * console.log('To string:', Color4.orange.toHexString());
14
14
  * ```
15
15
  */
16
- export default class Color4 {
16
+ export class Color4 {
17
17
  private constructor(
18
18
  /** Red component. Should be in the range [0, 1]. */
19
19
  public readonly r: number,
@@ -125,7 +125,13 @@ export default class Color4 {
125
125
  canvas.width = 1;
126
126
  canvas.height = 1;
127
127
 
128
- const ctx = canvas.getContext('2d')!;
128
+ const ctx = canvas.getContext('2d');
129
+
130
+ // Default to black if no canvas is available.
131
+ if (!ctx) {
132
+ return Color4.black;
133
+ }
134
+
129
135
  ctx.fillStyle = text;
130
136
  ctx.fillRect(0, 0, 1, 1);
131
137
 
@@ -431,4 +437,4 @@ export default class Color4 {
431
437
  public static white = Color4.ofRGB(1, 1, 1);
432
438
  }
433
439
 
434
- export { Color4 };
440
+ export default Color4;
@@ -97,6 +97,8 @@ export class Path {
97
97
  const geometry: GeometryArrayType = [];
98
98
 
99
99
  for (const part of this.parts) {
100
+ let exhaustivenessCheck: never;
101
+
100
102
  switch (part.kind) {
101
103
  case PathCommandType.CubicBezierTo:
102
104
  geometry.push(
@@ -124,6 +126,9 @@ export class Path {
124
126
  geometry.push(new PointShape2D(part.point));
125
127
  startPoint = part.point;
126
128
  break;
129
+ default:
130
+ exhaustivenessCheck = part;
131
+ return exhaustivenessCheck;
127
132
  }
128
133
  }
129
134
 
@@ -131,6 +136,38 @@ export class Path {
131
136
  return this.cachedGeometry;
132
137
  }
133
138
 
139
+ /**
140
+ * Iterates through the start/end points of each component in this path.
141
+ *
142
+ * If a start point is equivalent to the end point of the previous segment,
143
+ * the point is **not** emitted twice.
144
+ */
145
+ public *startEndPoints() {
146
+ yield this.startPoint;
147
+
148
+ for (const part of this.parts) {
149
+ let exhaustivenessCheck: never;
150
+
151
+ switch (part.kind) {
152
+ case PathCommandType.CubicBezierTo:
153
+ yield part.endPoint;
154
+ break;
155
+ case PathCommandType.QuadraticBezierTo:
156
+ yield part.endPoint;
157
+ break;
158
+ case PathCommandType.LineTo:
159
+ yield part.point;
160
+ break;
161
+ case PathCommandType.MoveTo:
162
+ yield part.point;
163
+ break;
164
+ default:
165
+ exhaustivenessCheck = part;
166
+ return exhaustivenessCheck;
167
+ }
168
+ }
169
+ }
170
+
134
171
  private cachedPolylineApproximation: LineSegment2[]|null = null;
135
172
 
136
173
  // Approximates this path with a group of line segments.
@@ -111,6 +111,27 @@ describe('Rect2', () => {
111
111
  expect(new Rect2(-100, -1, 200, 2).intersects(new Rect2(-5, 50, 10, 30))).toBe(false);
112
112
  });
113
113
 
114
+ it('should correctly compute the intersection of one rectangle and several others', () => {
115
+ const mainRect = new Rect2(334,156,333,179);
116
+ const shouldIntersect = [
117
+ 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),
125
+ ];
126
+
127
+ for (const rect of shouldIntersect) {
128
+ expect(mainRect.intersects(rect)).toBe(true);
129
+ }
130
+ for (const rect of shouldNotIntersect) {
131
+ expect(mainRect.intersects(rect)).toBe(false);
132
+ }
133
+ });
134
+
114
135
  it('intersecting rectangles should have their intersections correctly computed', () => {
115
136
  expect(new Rect2(-1, -1, 2, 2).intersection(Rect2.empty)).objEq(Rect2.empty);
116
137
  expect(new Rect2(-1, -1, 2, 2).intersection(new Rect2(0, 0, 3, 3))).objEq(
@@ -130,6 +151,19 @@ describe('Rect2', () => {
130
151
  expect(transformedBBox.containsRect(rect)).toBe(true);
131
152
  });
132
153
 
154
+ it('.grownBy should expand a rectangle by the given margin', () => {
155
+ expect(Rect2.empty.grownBy(0)).toBe(Rect2.empty);
156
+
157
+ // Should add padding to all sides.
158
+ expect(new Rect2(1, 2, 3, 4).grownBy(1)).objEq(new Rect2(0, 1, 5, 6));
159
+
160
+ // Shrinking should not result in negative widths/heights and
161
+ // should adjust x/y appropriately
162
+ expect(new Rect2(1, 2, 1, 2).grownBy(-1)).objEq(new Rect2(1.5, 3, 0, 0));
163
+ expect(new Rect2(1, 2, 4, 4).grownBy(-1)).objEq(new Rect2(2, 3, 2, 2));
164
+ expect(new Rect2(1, 2, 2, 8).grownBy(-2)).objEq(new Rect2(2, 4, 0, 4));
165
+ });
166
+
133
167
  describe('should correctly expand to include a given point', () => {
134
168
  it('Growing an empty rectange to include (1, 0)', () => {
135
169
  const originalRect = Rect2.empty;
@@ -21,7 +21,6 @@ export class Rect2 extends Abstract2DShape {
21
21
  // topLeft assumes up is -y
22
22
  public readonly topLeft: Point2;
23
23
  public readonly size: Vec2;
24
- public readonly bottomRight: Point2;
25
24
  public readonly area: number;
26
25
 
27
26
  public constructor(
@@ -45,7 +44,6 @@ export class Rect2 extends Abstract2DShape {
45
44
  // Precompute/store vector forms.
46
45
  this.topLeft = Vec2.of(this.x, this.y);
47
46
  this.size = Vec2.of(this.w, this.h);
48
- this.bottomRight = this.topLeft.plus(this.size);
49
47
  this.area = this.w * this.h;
50
48
  }
51
49
 
@@ -65,8 +63,8 @@ export class Rect2 extends Abstract2DShape {
65
63
 
66
64
  public containsRect(other: Rect2): boolean {
67
65
  return this.x <= other.x && this.y <= other.y
68
- && this.bottomRight.x >= other.bottomRight.x
69
- && this.bottomRight.y >= other.bottomRight.y;
66
+ && this.x + this.w >= other.x + other.w
67
+ && this.y + this.h >= other.y + other.h;
70
68
  }
71
69
 
72
70
  public intersects(other: Rect2): boolean {
@@ -159,6 +157,17 @@ export class Rect2 extends Abstract2DShape {
159
157
  return this;
160
158
  }
161
159
 
160
+ // Prevent width/height from being negative
161
+ if (margin < 0) {
162
+ const xMargin = -Math.min(-margin, this.w / 2);
163
+ const yMargin = -Math.min(-margin, this.h / 2);
164
+
165
+ return new Rect2(
166
+ this.x - xMargin, this.y - yMargin,
167
+ this.w + xMargin * 2, this.h + yMargin * 2,
168
+ );
169
+ }
170
+
162
171
  return new Rect2(
163
172
  this.x - margin, this.y - margin, this.w + margin * 2, this.h + margin * 2
164
173
  );
@@ -194,6 +203,10 @@ export class Rect2 extends Abstract2DShape {
194
203
  return Math.max(this.w, this.h);
195
204
  }
196
205
 
206
+ public get bottomRight() {
207
+ return this.topLeft.plus(this.size);
208
+ }
209
+
197
210
  public get topRight() {
198
211
  return this.bottomRight.plus(Vec2.of(0, -this.h));
199
212
  }
@@ -315,17 +328,17 @@ export class Rect2 extends Abstract2DShape {
315
328
  }
316
329
 
317
330
  const firstRect = rects[0];
318
- let minX: number = firstRect.topLeft.x;
319
- let minY: number = firstRect.topLeft.y;
320
- let maxX: number = firstRect.bottomRight.x;
321
- let maxY: number = firstRect.bottomRight.y;
331
+ let minX: number = firstRect.x;
332
+ let minY: number = firstRect.y;
333
+ let maxX: number = firstRect.x + firstRect.w;
334
+ let maxY: number = firstRect.y + firstRect.h;
322
335
 
323
336
  for (let i = 1; i < rects.length; i++) {
324
337
  const rect = rects[i];
325
- minX = Math.min(minX, rect.topLeft.x);
326
- minY = Math.min(minY, rect.topLeft.y);
327
- maxX = Math.max(maxX, rect.bottomRight.x);
328
- maxY = Math.max(maxY, rect.bottomRight.y);
338
+ minX = Math.min(minX, rect.x);
339
+ minY = Math.min(minY, rect.y);
340
+ maxX = Math.max(maxX, rect.x + rect.w);
341
+ maxY = Math.max(maxY, rect.y + rect.h);
329
342
  }
330
343
 
331
344
  return new Rect2(