@js-draw/math 1.0.0 → 1.0.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (61) hide show
  1. package/LICENSE +21 -0
  2. package/dist/cjs/Color4.test.d.ts +1 -0
  3. package/dist/cjs/Mat33.test.d.ts +1 -0
  4. package/dist/cjs/Vec2.test.d.ts +1 -0
  5. package/dist/cjs/Vec3.test.d.ts +1 -0
  6. package/dist/cjs/polynomial/solveQuadratic.test.d.ts +1 -0
  7. package/dist/cjs/rounding.test.d.ts +1 -0
  8. package/dist/cjs/shapes/LineSegment2.test.d.ts +1 -0
  9. package/dist/cjs/shapes/Path.fromString.test.d.ts +1 -0
  10. package/dist/cjs/shapes/Path.test.d.ts +1 -0
  11. package/dist/cjs/shapes/Path.toString.test.d.ts +1 -0
  12. package/dist/cjs/shapes/QuadraticBezier.test.d.ts +1 -0
  13. package/dist/cjs/shapes/Rect2.test.d.ts +1 -0
  14. package/dist/cjs/shapes/Triangle.test.d.ts +1 -0
  15. package/dist/mjs/Color4.test.d.ts +1 -0
  16. package/dist/mjs/Mat33.test.d.ts +1 -0
  17. package/dist/mjs/Vec2.test.d.ts +1 -0
  18. package/dist/mjs/Vec3.test.d.ts +1 -0
  19. package/dist/mjs/polynomial/solveQuadratic.test.d.ts +1 -0
  20. package/dist/mjs/rounding.test.d.ts +1 -0
  21. package/dist/mjs/shapes/LineSegment2.test.d.ts +1 -0
  22. package/dist/mjs/shapes/Path.fromString.test.d.ts +1 -0
  23. package/dist/mjs/shapes/Path.test.d.ts +1 -0
  24. package/dist/mjs/shapes/Path.toString.test.d.ts +1 -0
  25. package/dist/mjs/shapes/QuadraticBezier.test.d.ts +1 -0
  26. package/dist/mjs/shapes/Rect2.test.d.ts +1 -0
  27. package/dist/mjs/shapes/Triangle.test.d.ts +1 -0
  28. package/dist-test/test_imports/package-lock.json +13 -0
  29. package/dist-test/test_imports/package.json +12 -0
  30. package/dist-test/test_imports/test-imports.js +15 -0
  31. package/dist-test/test_imports/test-require.cjs +15 -0
  32. package/package.json +4 -3
  33. package/src/Color4.test.ts +52 -0
  34. package/src/Color4.ts +318 -0
  35. package/src/Mat33.test.ts +244 -0
  36. package/src/Mat33.ts +450 -0
  37. package/src/Vec2.test.ts +30 -0
  38. package/src/Vec2.ts +49 -0
  39. package/src/Vec3.test.ts +51 -0
  40. package/src/Vec3.ts +245 -0
  41. package/src/lib.ts +42 -0
  42. package/src/polynomial/solveQuadratic.test.ts +39 -0
  43. package/src/polynomial/solveQuadratic.ts +43 -0
  44. package/src/rounding.test.ts +65 -0
  45. package/src/rounding.ts +167 -0
  46. package/src/shapes/Abstract2DShape.ts +63 -0
  47. package/src/shapes/BezierJSWrapper.ts +93 -0
  48. package/src/shapes/CubicBezier.ts +35 -0
  49. package/src/shapes/LineSegment2.test.ts +99 -0
  50. package/src/shapes/LineSegment2.ts +232 -0
  51. package/src/shapes/Path.fromString.test.ts +223 -0
  52. package/src/shapes/Path.test.ts +309 -0
  53. package/src/shapes/Path.toString.test.ts +77 -0
  54. package/src/shapes/Path.ts +963 -0
  55. package/src/shapes/PointShape2D.ts +33 -0
  56. package/src/shapes/QuadraticBezier.test.ts +31 -0
  57. package/src/shapes/QuadraticBezier.ts +142 -0
  58. package/src/shapes/Rect2.test.ts +209 -0
  59. package/src/shapes/Rect2.ts +346 -0
  60. package/src/shapes/Triangle.test.ts +61 -0
  61. package/src/shapes/Triangle.ts +139 -0
@@ -0,0 +1,346 @@
1
+ import LineSegment2 from './LineSegment2';
2
+ import Mat33 from '../Mat33';
3
+ import { Point2, Vec2 } from '../Vec2';
4
+ import Abstract2DShape from './Abstract2DShape';
5
+ import Vec3 from '../Vec3';
6
+
7
+ /** An object that can be converted to a Rect2. */
8
+ export interface RectTemplate {
9
+ x: number;
10
+ y: number;
11
+ w?: number;
12
+ h?: number;
13
+ width?: number;
14
+ height?: number;
15
+ }
16
+
17
+ // invariant: w ≥ 0, h ≥ 0, immutable
18
+ export class Rect2 extends Abstract2DShape {
19
+ // Derived state:
20
+
21
+ // topLeft assumes up is -y
22
+ public readonly topLeft: Point2;
23
+ public readonly size: Vec2;
24
+ public readonly bottomRight: Point2;
25
+ public readonly area: number;
26
+
27
+ public constructor(
28
+ public readonly x: number,
29
+ public readonly y: number,
30
+ public readonly w: number,
31
+ public readonly h: number
32
+ ) {
33
+ super();
34
+
35
+ if (w < 0) {
36
+ this.x += w;
37
+ this.w = Math.abs(w);
38
+ }
39
+
40
+ if (h < 0) {
41
+ this.y += h;
42
+ this.h = Math.abs(h);
43
+ }
44
+
45
+ // Precompute/store vector forms.
46
+ this.topLeft = Vec2.of(this.x, this.y);
47
+ this.size = Vec2.of(this.w, this.h);
48
+ this.bottomRight = this.topLeft.plus(this.size);
49
+ this.area = this.w * this.h;
50
+ }
51
+
52
+ public translatedBy(vec: Vec2): Rect2 {
53
+ return new Rect2(vec.x + this.x, vec.y + this.y, this.w, this.h);
54
+ }
55
+
56
+ // Returns a copy of this with the given size (but same top-left).
57
+ public resizedTo(size: Vec2): Rect2 {
58
+ return new Rect2(this.x, this.y, size.x, size.y);
59
+ }
60
+
61
+ public override containsPoint(other: Point2): boolean {
62
+ return this.x <= other.x && this.y <= other.y
63
+ && this.x + this.w >= other.x && this.y + this.h >= other.y;
64
+ }
65
+
66
+ public containsRect(other: Rect2): boolean {
67
+ return this.x <= other.x && this.y <= other.y
68
+ && this.bottomRight.x >= other.bottomRight.x
69
+ && this.bottomRight.y >= other.bottomRight.y;
70
+ }
71
+
72
+ public intersects(other: Rect2): boolean {
73
+ // Project along x/y axes.
74
+ const thisMinX = this.x;
75
+ const thisMaxX = thisMinX + this.w;
76
+ const otherMinX = other.x;
77
+ const otherMaxX = other.x + other.w;
78
+
79
+ if (thisMaxX < otherMinX || thisMinX > otherMaxX) {
80
+ return false;
81
+ }
82
+
83
+
84
+ const thisMinY = this.y;
85
+ const thisMaxY = thisMinY + this.h;
86
+ const otherMinY = other.y;
87
+ const otherMaxY = other.y + other.h;
88
+
89
+ if (thisMaxY < otherMinY || thisMinY > otherMaxY) {
90
+ return false;
91
+ }
92
+
93
+ return true;
94
+ }
95
+
96
+ // Returns the overlap of this and [other], or null, if no such
97
+ // overlap exists
98
+ public intersection(other: Rect2): Rect2|null {
99
+ if (!this.intersects(other)) {
100
+ return null;
101
+ }
102
+
103
+ const topLeft = this.topLeft.zip(other.topLeft, Math.max);
104
+ const bottomRight = this.bottomRight.zip(other.bottomRight, Math.min);
105
+
106
+ return Rect2.fromCorners(topLeft, bottomRight);
107
+ }
108
+
109
+ // Returns a new rectangle containing both [this] and [other].
110
+ public union(other: Rect2): Rect2 {
111
+ return Rect2.union(this, other);
112
+ }
113
+
114
+ // Returns a the subdivision of this into [columns] columns
115
+ // and [rows] rows. For example,
116
+ // Rect2.unitSquare.divideIntoGrid(2, 2)
117
+ // -> [ Rect2(0, 0, 0.5, 0.5), Rect2(0.5, 0, 0.5, 0.5), Rect2(0, 0.5, 0.5, 0.5), Rect2(0.5, 0.5, 0.5, 0.5) ]
118
+ // The rectangles are ordered in row-major order.
119
+ public divideIntoGrid(columns: number, rows: number): Rect2[] {
120
+ const result: Rect2[] = [];
121
+ if (columns <= 0 || rows <= 0) {
122
+ return result;
123
+ }
124
+
125
+ const eachRectWidth = this.w / columns;
126
+ const eachRectHeight = this.h / rows;
127
+
128
+ if (eachRectWidth === 0) {
129
+ columns = 1;
130
+ }
131
+ if (eachRectHeight === 0) {
132
+ rows = 1;
133
+ }
134
+
135
+ for (let j = 0; j < rows; j++) {
136
+ for (let i = 0; i < columns; i++) {
137
+ const x = eachRectWidth * i + this.x;
138
+ const y = eachRectHeight * j + this.y;
139
+ result.push(new Rect2(x, y, eachRectWidth, eachRectHeight));
140
+ }
141
+ }
142
+ return result;
143
+ }
144
+
145
+ // Returns a rectangle containing this and [point].
146
+ // [margin] is the minimum distance between the new point and the edge
147
+ // of the resultant rectangle.
148
+ public grownToPoint(point: Point2, margin: number = 0): Rect2 {
149
+ const otherRect = new Rect2(
150
+ point.x - margin, point.y - margin,
151
+ margin * 2, margin * 2
152
+ );
153
+ return this.union(otherRect);
154
+ }
155
+
156
+ // Returns this grown by [margin] in both the x and y directions.
157
+ public grownBy(margin: number): Rect2 {
158
+ if (margin === 0) {
159
+ return this;
160
+ }
161
+
162
+ return new Rect2(
163
+ this.x - margin, this.y - margin, this.w + margin * 2, this.h + margin * 2
164
+ );
165
+ }
166
+
167
+ public getClosestPointOnBoundaryTo(target: Point2) {
168
+ const closestEdgePoints = this.getEdges().map(edge => {
169
+ return edge.closestPointTo(target);
170
+ });
171
+
172
+ let closest: Point2|null = null;
173
+ let closestDist: number|null = null;
174
+ for (const point of closestEdgePoints) {
175
+ const dist = point.minus(target).length();
176
+ if (closestDist === null || dist < closestDist) {
177
+ closest = point;
178
+ closestDist = dist;
179
+ }
180
+ }
181
+ return closest!;
182
+ }
183
+
184
+ public get corners(): Point2[] {
185
+ return [
186
+ this.bottomRight,
187
+ this.topRight,
188
+ this.topLeft,
189
+ this.bottomLeft,
190
+ ];
191
+ }
192
+
193
+ public get maxDimension() {
194
+ return Math.max(this.w, this.h);
195
+ }
196
+
197
+ public get topRight() {
198
+ return this.bottomRight.plus(Vec2.of(0, -this.h));
199
+ }
200
+
201
+ public get bottomLeft() {
202
+ return this.topLeft.plus(Vec2.of(0, this.h));
203
+ }
204
+
205
+ public get width() {
206
+ return this.w;
207
+ }
208
+
209
+ public get height() {
210
+ return this.h;
211
+ }
212
+
213
+ public get center() {
214
+ return this.topLeft.plus(this.size.times(0.5));
215
+ }
216
+
217
+ // Returns edges in the order
218
+ // [ rightEdge, topEdge, leftEdge, bottomEdge ]
219
+ public getEdges(): LineSegment2[] {
220
+ const corners = this.corners;
221
+ return [
222
+ new LineSegment2(corners[0], corners[1]),
223
+ new LineSegment2(corners[1], corners[2]),
224
+ new LineSegment2(corners[2], corners[3]),
225
+ new LineSegment2(corners[3], corners[0]),
226
+ ];
227
+ }
228
+
229
+ public override intersectsLineSegment(lineSegment: LineSegment2): Point2[] {
230
+ const result: Point2[] = [];
231
+
232
+ for (const edge of this.getEdges()) {
233
+ const intersection = edge.intersectsLineSegment(lineSegment);
234
+ intersection.forEach(point => result.push(point));
235
+ }
236
+
237
+ return result;
238
+ }
239
+
240
+ public override signedDistance(point: Vec3): number {
241
+ const closestBoundaryPoint = this.getClosestPointOnBoundaryTo(point);
242
+ const dist = point.minus(closestBoundaryPoint).magnitude();
243
+
244
+ if (this.containsPoint(point)) {
245
+ return -dist;
246
+ }
247
+ return dist;
248
+ }
249
+
250
+ public override getTightBoundingBox(): Rect2 {
251
+ return this;
252
+ }
253
+
254
+ // [affineTransform] is a transformation matrix that both scales and **translates**.
255
+ // the bounding box of this' four corners after transformed by the given affine transformation.
256
+ public transformedBoundingBox(affineTransform: Mat33): Rect2 {
257
+ return Rect2.bboxOf(this.corners.map(corner => affineTransform.transformVec2(corner)));
258
+ }
259
+
260
+ /** @return true iff this is equal to [other] ± fuzz */
261
+ public eq(other: Rect2, fuzz: number = 0): boolean {
262
+ return this.topLeft.eq(other.topLeft, fuzz) && this.size.eq(other.size, fuzz);
263
+ }
264
+
265
+ public override toString(): string {
266
+ return `Rect(point(${this.x}, ${this.y}), size(${this.w}, ${this.h}))`;
267
+ }
268
+
269
+
270
+ public static fromCorners(corner1: Point2, corner2: Point2) {
271
+ return new Rect2(
272
+ Math.min(corner1.x, corner2.x),
273
+ Math.min(corner1.y, corner2.y),
274
+ Math.abs(corner1.x - corner2.x),
275
+ Math.abs(corner1.y - corner2.y)
276
+ );
277
+ }
278
+
279
+ // Returns a box that contains all points in [points] with at least [margin]
280
+ // between each point and the edge of the box.
281
+ public static bboxOf(points: Point2[], margin: number = 0) {
282
+ let minX = 0;
283
+ let minY = 0;
284
+ let maxX = 0;
285
+ let maxY = 0;
286
+ let isFirst = true;
287
+
288
+ for (const point of points) {
289
+ if (isFirst) {
290
+ minX = point.x;
291
+ minY = point.y;
292
+ maxX = point.x;
293
+ maxY = point.y;
294
+
295
+ isFirst = false;
296
+ }
297
+
298
+ minX = Math.min(minX, point.x);
299
+ minY = Math.min(minY, point.y);
300
+ maxX = Math.max(maxX, point.x);
301
+ maxY = Math.max(maxY, point.y);
302
+ }
303
+
304
+ return Rect2.fromCorners(
305
+ Vec2.of(minX - margin, minY - margin),
306
+ Vec2.of(maxX + margin, maxY + margin)
307
+ );
308
+ }
309
+
310
+ // @returns a rectangle that contains all of the given rectangles, the bounding box
311
+ // of the given rectangles.
312
+ public static union(...rects: Rect2[]): Rect2 {
313
+ if (rects.length === 0) {
314
+ return Rect2.empty;
315
+ }
316
+
317
+ 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;
322
+
323
+ for (let i = 1; i < rects.length; i++) {
324
+ 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);
329
+ }
330
+
331
+ return new Rect2(
332
+ minX, minY, maxX - minX, maxY - minY,
333
+ );
334
+ }
335
+
336
+ public static of(template: RectTemplate) {
337
+ const width = template.width ?? template.w ?? 0;
338
+ const height = template.height ?? template.h ?? 0;
339
+ return new Rect2(template.x, template.y, width, height);
340
+ }
341
+
342
+ public static empty = new Rect2(0, 0, 0, 0);
343
+ public static unitSquare = new Rect2(0, 0, 1, 1);
344
+ }
345
+
346
+ export default Rect2;
@@ -0,0 +1,61 @@
1
+ import { Vec2 } from '../Vec2';
2
+ import Triangle from './Triangle';
3
+
4
+ describe('Triangle', () => {
5
+ describe('signed distance function should return correct values', () => {
6
+ it('signed distance function should be zero along the boundary of a shape', () => {
7
+ const testTriangle = Triangle.fromVertices(Vec2.of(-1, -1), Vec2.of(0, 1), Vec2.of(1, -1));
8
+
9
+ // SDF for each vertex should be zero.
10
+ for (const vertex of testTriangle.vertices) {
11
+ expect(testTriangle.signedDistance(vertex)).toBeCloseTo(0);
12
+ }
13
+
14
+ // SDF along each side should be zero
15
+ for (const side of testTriangle.getEdges()) {
16
+ for (let t = 0.1; t < 1; t += 0.1) {
17
+ expect(testTriangle.signedDistance(side.at(t))).toBeCloseTo(0);
18
+ }
19
+ }
20
+ });
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);
28
+
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);
31
+
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);
34
+
35
+
36
+ const firstSide = testTriangle.getEdges()[0];
37
+ const firstSideMidpoint = firstSide.at(0.5);
38
+ const firstSideNormal = firstSide.direction.orthog();
39
+
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);
45
+
46
+ // Inside the shape
47
+ if (t > 0) {
48
+ // Inside the shape
49
+ expect(testTriangle.containsPoint(point)).toBe(true);
50
+
51
+ expect(signedDist).toBeCloseTo(-distFromSide1);
52
+ } else {
53
+ // Outside the shape
54
+ expect(testTriangle.containsPoint(point)).toBe(false);
55
+
56
+ expect(signedDist).toBeCloseTo(distFromSide1);
57
+ }
58
+ }
59
+ });
60
+ });
61
+ });
@@ -0,0 +1,139 @@
1
+ import Mat33 from '../Mat33';
2
+ import { Point2 } from '../Vec2';
3
+ import Vec3 from '../Vec3';
4
+ import Abstract2DShape from './Abstract2DShape';
5
+ import LineSegment2 from './LineSegment2';
6
+ import Rect2 from './Rect2';
7
+
8
+ type TriangleBoundary = [ LineSegment2, LineSegment2, LineSegment2 ];
9
+
10
+ export default class Triangle extends Abstract2DShape {
11
+ /**
12
+ * @see {@link fromVertices}
13
+ */
14
+ protected constructor(
15
+ public readonly vertex1: Vec3,
16
+ public readonly vertex2: Vec3,
17
+ public readonly vertex3: Vec3,
18
+ ) {
19
+ super();
20
+ }
21
+
22
+ /**
23
+ * Creates a triangle from its three corners. Corners may be stored in a different
24
+ * order than given.
25
+ */
26
+ public static fromVertices(vertex1: Vec3, vertex2: Vec3, vertex3: Vec3) {
27
+ return new Triangle(vertex1, vertex2, vertex3);
28
+ }
29
+
30
+ public get vertices(): [ Point2, Point2, Point2 ] {
31
+ return [ this.vertex1, this.vertex2, this.vertex3 ];
32
+ }
33
+
34
+ public map(mapping: (vertex: Vec3)=>Vec3): Triangle {
35
+ return new Triangle(
36
+ mapping(this.vertex1),
37
+ mapping(this.vertex2),
38
+ mapping(this.vertex3),
39
+ );
40
+ }
41
+
42
+ // Transform, treating this as composed of 2D points.
43
+ public transformed2DBy(affineTransform: Mat33) {
44
+ return this.map(affineTransform.transformVec2);
45
+ }
46
+
47
+ // Transforms this by a linear transform --- verticies are treated as
48
+ // 3D points.
49
+ public transformedBy(linearTransform: Mat33) {
50
+ return this.map(linearTransform.transformVec3);
51
+ }
52
+
53
+ #sides: TriangleBoundary|undefined = undefined;
54
+
55
+ /**
56
+ * Returns the sides of this triangle, as an array of `LineSegment2`s.
57
+ *
58
+ * The first side is from `vertex1` to `vertex2`, the next from `vertex2` to `vertex3`,
59
+ * and the last from `vertex3` to `vertex1`.
60
+ */
61
+ public getEdges(): TriangleBoundary {
62
+ if (this.#sides) {
63
+ return this.#sides;
64
+ }
65
+
66
+ const side1 = new LineSegment2(this.vertex1, this.vertex2);
67
+ const side2 = new LineSegment2(this.vertex2, this.vertex3);
68
+ const side3 = new LineSegment2(this.vertex3, this.vertex1);
69
+
70
+ const sides: TriangleBoundary = [ side1, side2, side3 ];
71
+ this.#sides = sides;
72
+ return sides;
73
+ }
74
+
75
+ public override intersectsLineSegment(lineSegment: LineSegment2): Vec3[] {
76
+ const result: Point2[] = [];
77
+
78
+ for (const edge of this.getEdges()) {
79
+ edge.intersectsLineSegment(lineSegment)
80
+ .forEach(point => result.push(point));
81
+ }
82
+
83
+ return result;
84
+ }
85
+
86
+ /** @inheritdoc */
87
+ public override containsPoint(point: Vec3, epsilon: number = Abstract2DShape.smallValue): boolean {
88
+ // Project `point` onto normals to each of this' sides.
89
+ // Uses the Separating Axis Theorem (https://en.wikipedia.org/wiki/Hyperplane_separation_theorem#Use_in_collision_detection)
90
+ const sides = this.getEdges();
91
+
92
+ for (const side of sides) {
93
+ const orthog = side.direction.orthog();
94
+
95
+ // Project all three vertices
96
+ // TODO: Performance can be improved here (two vertices will always have the same projection)
97
+ const projv1 = orthog.dot(this.vertex1);
98
+ const projv2 = orthog.dot(this.vertex2);
99
+ const projv3 = orthog.dot(this.vertex3);
100
+
101
+ const minProjVertex = Math.min(projv1, projv2, projv3);
102
+ const maxProjVertex = Math.max(projv1, projv2, projv3);
103
+
104
+ const projPoint = orthog.dot(point);
105
+
106
+ const inProjection = projPoint >= minProjVertex - epsilon && projPoint <= maxProjVertex + epsilon;
107
+ if (!inProjection) {
108
+ return false;
109
+ }
110
+ }
111
+
112
+ return true;
113
+ }
114
+
115
+ /**
116
+ * @returns the signed distance from `point` to the closest edge of this triangle.
117
+ *
118
+ * If `point` is inside `this`, the result is negative, otherwise, the result is
119
+ * positive.
120
+ */
121
+ public override signedDistance(point: Vec3): number {
122
+ const sides = this.getEdges();
123
+ const distances = sides.map(side => side.distance(point));
124
+ const distance = Math.min(...distances);
125
+
126
+ // If the point is in this' interior, signedDistance must return a negative
127
+ // number.
128
+ if (this.containsPoint(point, 0)) {
129
+ return -distance;
130
+ } else {
131
+ return distance;
132
+ }
133
+ }
134
+
135
+ /** @inheritdoc */
136
+ public override getTightBoundingBox(): Rect2 {
137
+ return Rect2.bboxOf(this.vertices);
138
+ }
139
+ }