UNPKG

planck-js

Version:

2D JavaScript/TypeScript physics engine for cross-platform HTML5 game development

278 lines (227 loc) 7.62 kB
/* * Planck.js * * Copyright (c) Erin Catto, Ali Shakiba * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ import { EPSILON } from "../common/Math"; import { Vec2, Vec2Value } from "../common/Vec2"; /** @internal */ const _ASSERT = typeof ASSERT === "undefined" ? false : ASSERT; /** @internal */ const _CONSTRUCTOR_FACTORY = typeof CONSTRUCTOR_FACTORY === "undefined" ? false : CONSTRUCTOR_FACTORY; /** @internal */ const math_max = Math.max; /** @internal */ const math_min = Math.min; /** * Ray-cast input data. The ray extends from `p1` to `p1 + maxFraction * (p2 - p1)`. */ export interface RayCastInput { p1: Vec2Value; p2: Vec2Value; maxFraction: number; } export type RayCastCallback = (subInput: RayCastInput, id: number) => number; /** * Ray-cast output data. The ray hits at `p1 + fraction * (p2 - p1)`, * where `p1` and `p2` come from RayCastInput. */ export interface RayCastOutput { normal: Vec2; fraction: number; } /** Axis-aligned bounding box */ export interface AABBValue { lowerBound: Vec2Value; upperBound: Vec2Value; } declare module "./AABB" { /** @hidden @deprecated Use new keyword. */ // @ts-expect-error function AABB(lower?: Vec2Value, upper?: Vec2Value): AABB; } /** Axis-aligned bounding box */ // @ts-expect-error export class AABB { lowerBound: Vec2; upperBound: Vec2; constructor(lower?: Vec2Value, upper?: Vec2Value) { if (_CONSTRUCTOR_FACTORY && !(this instanceof AABB)) { return new AABB(lower, upper); } this.lowerBound = Vec2.zero(); this.upperBound = Vec2.zero(); if (typeof lower === "object") { this.lowerBound.setVec2(lower); } if (typeof upper === "object") { this.upperBound.setVec2(upper); } else if (typeof lower === "object") { this.upperBound.setVec2(lower); } } /** * Verify that the bounds are sorted. */ isValid(): boolean { return AABB.isValid(this); } static isValid(obj: any): boolean { if (obj === null || typeof obj === "undefined") { return false; } return Vec2.isValid(obj.lowerBound) && Vec2.isValid(obj.upperBound) && Vec2.sub(obj.upperBound, obj.lowerBound).lengthSquared() >= 0; } static assert(o: any): void { if (_ASSERT) console.assert(!AABB.isValid(o), "Invalid AABB!", o); } /** * Get the center of the AABB. */ getCenter(): Vec2 { return Vec2.neo((this.lowerBound.x + this.upperBound.x) * 0.5, (this.lowerBound.y + this.upperBound.y) * 0.5); } /** * Get the extents of the AABB (half-widths). */ getExtents(): Vec2 { return Vec2.neo((this.upperBound.x - this.lowerBound.x) * 0.5, (this.upperBound.y - this.lowerBound.y) * 0.5); } /** * Get the perimeter length. */ getPerimeter(): number { return 2.0 * (this.upperBound.x - this.lowerBound.x + this.upperBound.y - this.lowerBound.y); } /** * Combine one or two AABB into this one. */ combine(a: AABBValue, b?: AABBValue): void { b = b || this; const lowerA = a.lowerBound; const upperA = a.upperBound; const lowerB = b.lowerBound; const upperB = b.upperBound; const lowerX = math_min(lowerA.x, lowerB.x); const lowerY = math_min(lowerA.y, lowerB.y); const upperX = math_max(upperB.x, upperA.x); const upperY = math_max(upperB.y, upperA.y); this.lowerBound.setNum(lowerX, lowerY); this.upperBound.setNum(upperX, upperY); } combinePoints(a: Vec2Value, b: Vec2Value): void { this.lowerBound.setNum(math_min(a.x, b.x), math_min(a.y, b.y)); this.upperBound.setNum(math_max(a.x, b.x), math_max(a.y, b.y)); } set(aabb: AABBValue): void { this.lowerBound.setNum(aabb.lowerBound.x, aabb.lowerBound.y); this.upperBound.setNum(aabb.upperBound.x, aabb.upperBound.y); } contains(aabb: AABBValue): boolean { let result = true; result = result && this.lowerBound.x <= aabb.lowerBound.x; result = result && this.lowerBound.y <= aabb.lowerBound.y; result = result && aabb.upperBound.x <= this.upperBound.x; result = result && aabb.upperBound.y <= this.upperBound.y; return result; } extend(value: number): AABB { AABB.extend(this, value); return this; } static extend(out: AABBValue, value: number): AABBValue { out.lowerBound.x -= value; out.lowerBound.y -= value; out.upperBound.x += value; out.upperBound.y += value; return out; } static testOverlap(a: AABBValue, b: AABBValue): boolean { const d1x = b.lowerBound.x - a.upperBound.x; const d2x = a.lowerBound.x - b.upperBound.x; const d1y = b.lowerBound.y - a.upperBound.y; const d2y = a.lowerBound.y - b.upperBound.y; if (d1x > 0 || d1y > 0 || d2x > 0 || d2y > 0) { return false; } return true; } static areEqual(a: AABBValue, b: AABBValue): boolean { return Vec2.areEqual(a.lowerBound, b.lowerBound) && Vec2.areEqual(a.upperBound, b.upperBound); } static diff(a: AABBValue, b: AABBValue): number { const wD = math_max(0, math_min(a.upperBound.x, b.upperBound.x) - math_max(b.lowerBound.x, a.lowerBound.x)); const hD = math_max(0, math_min(a.upperBound.y, b.upperBound.y) - math_max(b.lowerBound.y, a.lowerBound.y)); const wA = a.upperBound.x - a.lowerBound.x; const hA = a.upperBound.y - a.lowerBound.y; const wB = b.upperBound.x - b.lowerBound.x; const hB = b.upperBound.y - b.lowerBound.y; return wA * hA + wB * hB - wD * hD; } rayCast(output: RayCastOutput, input: RayCastInput): boolean { // From Real-time Collision Detection, p179. let tmin = -Infinity; let tmax = Infinity; const p = input.p1; const d = Vec2.sub(input.p2, input.p1); const absD = Vec2.abs(d); const normal = Vec2.zero(); for (let f: "x" | "y" = "x"; f !== null; f = (f === "x" ? "y" : null)) { if (absD.x < EPSILON) { // Parallel. if (p[f] < this.lowerBound[f] || this.upperBound[f] < p[f]) { return false; } } else { const inv_d = 1.0 / d[f]; let t1 = (this.lowerBound[f] - p[f]) * inv_d; let t2 = (this.upperBound[f] - p[f]) * inv_d; // Sign of the normal vector. let s = -1.0; if (t1 > t2) { const temp = t1; t1 = t2; t2 = temp; s = 1.0; } // Push the min up if (t1 > tmin) { normal.setZero(); normal[f] = s; tmin = t1; } // Pull the max down tmax = math_min(tmax, t2); if (tmin > tmax) { return false; } } } // Does the ray start inside the box? // Does the ray intersect beyond the max fraction? if (tmin < 0.0 || input.maxFraction < tmin) { return false; } // Intersection. output.fraction = tmin; output.normal = normal; return true; } /** @hidden */ toString(): string { return JSON.stringify(this); } static combinePoints(out: AABBValue, a: Vec2Value, b: Vec2Value): AABBValue { out.lowerBound.x = math_min(a.x, b.x); out.lowerBound.y = math_min(a.y, b.y); out.upperBound.x = math_max(a.x, b.x); out.upperBound.y = math_max(a.y, b.y); return out; } static combinedPerimeter(a: AABBValue, b: AABBValue) { const lx = math_min(a.lowerBound.x, b.lowerBound.x); const ly = math_min(a.lowerBound.y, b.lowerBound.y); const ux = math_max(a.upperBound.x, b.upperBound.x); const uy = math_max(a.upperBound.y, b.upperBound.y); return 2.0 * (ux - lx + uy - ly); } }