@js-draw/math 1.0.0 → 1.0.2
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.
- package/LICENSE +21 -0
- package/dist/cjs/Color4.test.d.ts +1 -0
- package/dist/cjs/Mat33.test.d.ts +1 -0
- package/dist/cjs/Vec2.test.d.ts +1 -0
- package/dist/cjs/Vec3.test.d.ts +1 -0
- package/dist/cjs/polynomial/solveQuadratic.test.d.ts +1 -0
- package/dist/cjs/rounding.test.d.ts +1 -0
- package/dist/cjs/shapes/LineSegment2.test.d.ts +1 -0
- package/dist/cjs/shapes/Path.fromString.test.d.ts +1 -0
- package/dist/cjs/shapes/Path.test.d.ts +1 -0
- package/dist/cjs/shapes/Path.toString.test.d.ts +1 -0
- package/dist/cjs/shapes/QuadraticBezier.test.d.ts +1 -0
- package/dist/cjs/shapes/Rect2.test.d.ts +1 -0
- package/dist/cjs/shapes/Triangle.test.d.ts +1 -0
- package/dist/mjs/Color4.test.d.ts +1 -0
- package/dist/mjs/Mat33.test.d.ts +1 -0
- package/dist/mjs/Vec2.test.d.ts +1 -0
- package/dist/mjs/Vec3.test.d.ts +1 -0
- package/dist/mjs/polynomial/solveQuadratic.test.d.ts +1 -0
- package/dist/mjs/rounding.test.d.ts +1 -0
- package/dist/mjs/shapes/LineSegment2.test.d.ts +1 -0
- package/dist/mjs/shapes/Path.fromString.test.d.ts +1 -0
- package/dist/mjs/shapes/Path.test.d.ts +1 -0
- package/dist/mjs/shapes/Path.toString.test.d.ts +1 -0
- package/dist/mjs/shapes/QuadraticBezier.test.d.ts +1 -0
- package/dist/mjs/shapes/Rect2.test.d.ts +1 -0
- package/dist/mjs/shapes/Triangle.test.d.ts +1 -0
- package/dist-test/test_imports/package-lock.json +13 -0
- package/dist-test/test_imports/package.json +12 -0
- package/dist-test/test_imports/test-imports.js +15 -0
- package/dist-test/test_imports/test-require.cjs +15 -0
- package/package.json +4 -3
- package/src/Color4.test.ts +52 -0
- package/src/Color4.ts +318 -0
- package/src/Mat33.test.ts +244 -0
- package/src/Mat33.ts +450 -0
- package/src/Vec2.test.ts +30 -0
- package/src/Vec2.ts +49 -0
- package/src/Vec3.test.ts +51 -0
- package/src/Vec3.ts +245 -0
- package/src/lib.ts +42 -0
- package/src/polynomial/solveQuadratic.test.ts +39 -0
- package/src/polynomial/solveQuadratic.ts +43 -0
- package/src/rounding.test.ts +65 -0
- package/src/rounding.ts +167 -0
- package/src/shapes/Abstract2DShape.ts +63 -0
- package/src/shapes/BezierJSWrapper.ts +93 -0
- package/src/shapes/CubicBezier.ts +35 -0
- package/src/shapes/LineSegment2.test.ts +99 -0
- package/src/shapes/LineSegment2.ts +232 -0
- package/src/shapes/Path.fromString.test.ts +223 -0
- package/src/shapes/Path.test.ts +309 -0
- package/src/shapes/Path.toString.test.ts +77 -0
- package/src/shapes/Path.ts +963 -0
- package/src/shapes/PointShape2D.ts +33 -0
- package/src/shapes/QuadraticBezier.test.ts +31 -0
- package/src/shapes/QuadraticBezier.ts +142 -0
- package/src/shapes/Rect2.test.ts +209 -0
- package/src/shapes/Rect2.ts +346 -0
- package/src/shapes/Triangle.test.ts +61 -0
- 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
|
+
}
|