@js-draw/math 1.0.0 → 1.2.0
Sign up to get free protection for your applications and to get access to all the features.
- package/LICENSE +21 -0
- package/dist/cjs/Color4.d.ts +40 -0
- package/dist/cjs/Color4.js +102 -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.d.ts +40 -0
- package/dist/mjs/Color4.mjs +102 -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 +94 -0
- package/src/Color4.ts +430 -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
|
+
}
|