@js-draw/math
Version:
A math library for js-draw.
139 lines (114 loc) • 4.03 kB
text/typescript
import Mat33 from '../Mat33';
import { Point2 } from '../Vec2';
import Vec3 from '../Vec3';
import Abstract2DShape from './Abstract2DShape';
import LineSegment2 from './LineSegment2';
import Rect2 from './Rect2';
type TriangleBoundary = [LineSegment2, LineSegment2, LineSegment2];
export default class Triangle extends Abstract2DShape {
/**
* @see {@link fromVertices}
*/
protected constructor(
public readonly vertex1: Vec3,
public readonly vertex2: Vec3,
public readonly vertex3: Vec3,
) {
super();
}
/**
* Creates a triangle from its three corners. Corners may be stored in a different
* order than given.
*/
public static fromVertices(vertex1: Vec3, vertex2: Vec3, vertex3: Vec3) {
return new Triangle(vertex1, vertex2, vertex3);
}
public get vertices(): [Point2, Point2, Point2] {
return [this.vertex1, this.vertex2, this.vertex3];
}
public map(mapping: (vertex: Vec3) => Vec3): Triangle {
return new Triangle(mapping(this.vertex1), mapping(this.vertex2), mapping(this.vertex3));
}
// Transform, treating this as composed of 2D points.
public transformed2DBy(affineTransform: Mat33) {
return this.map((vertex) => affineTransform.transformVec2(vertex));
}
// Transforms this by a linear transform --- verticies are treated as
// 3D points.
public transformedBy(linearTransform: Mat33) {
return this.map((vertex) => linearTransform.transformVec3(vertex));
}
#sides: TriangleBoundary | undefined = undefined;
/**
* Returns the sides of this triangle, as an array of `LineSegment2`s.
*
* The first side is from `vertex1` to `vertex2`, the next from `vertex2` to `vertex3`,
* and the last from `vertex3` to `vertex1`.
*/
public getEdges(): TriangleBoundary {
if (this.#sides) {
return this.#sides;
}
const side1 = new LineSegment2(this.vertex1, this.vertex2);
const side2 = new LineSegment2(this.vertex2, this.vertex3);
const side3 = new LineSegment2(this.vertex3, this.vertex1);
const sides: TriangleBoundary = [side1, side2, side3];
this.#sides = sides;
return sides;
}
public override intersectsLineSegment(lineSegment: LineSegment2): Vec3[] {
const result: Point2[] = [];
for (const edge of this.getEdges()) {
edge.intersectsLineSegment(lineSegment).forEach((point) => result.push(point));
}
return result;
}
/** @inheritdoc */
public override containsPoint(
point: Vec3,
epsilon: number = Abstract2DShape.smallValue,
): boolean {
// Project `point` onto normals to each of this' sides.
// Uses the Separating Axis Theorem (https://en.wikipedia.org/wiki/Hyperplane_separation_theorem#Use_in_collision_detection)
const sides = this.getEdges();
for (const side of sides) {
const orthog = side.direction.orthog();
// Project all three vertices
// TODO: Performance can be improved here (two vertices will always have the same projection)
const projv1 = orthog.dot(this.vertex1);
const projv2 = orthog.dot(this.vertex2);
const projv3 = orthog.dot(this.vertex3);
const minProjVertex = Math.min(projv1, projv2, projv3);
const maxProjVertex = Math.max(projv1, projv2, projv3);
const projPoint = orthog.dot(point);
const inProjection =
projPoint >= minProjVertex - epsilon && projPoint <= maxProjVertex + epsilon;
if (!inProjection) {
return false;
}
}
return true;
}
/**
* @returns the signed distance from `point` to the closest edge of this triangle.
*
* If `point` is inside `this`, the result is negative, otherwise, the result is
* positive.
*/
public override signedDistance(point: Vec3): number {
const sides = this.getEdges();
const distances = sides.map((side) => side.distance(point));
const distance = Math.min(...distances);
// If the point is in this' interior, signedDistance must return a negative
// number.
if (this.containsPoint(point, 0)) {
return -distance;
} else {
return distance;
}
}
/** @inheritdoc */
public override getTightBoundingBox(): Rect2 {
return Rect2.bboxOf(this.vertices);
}
}