UNPKG

@woosh/meep-engine

Version:

Pure JavaScript game engine. Fully featured and production ready.

687 lines (585 loc) • 14.8 kB
import { assert } from "../../../assert.js"; import { clamp } from "../../../math/clamp.js"; import { max2 } from "../../../math/max2.js"; import { min2 } from "../../../math/min2.js"; import Vector2 from "../../Vector2.js"; import { line_segment_compute_line_segment_intersection_2d } from "../line/line_segment_compute_line_segment_intersection_2d.js"; import { aabb2_compute_area } from "./aabb2_compute_area.js"; import { aabb2_compute_overlap } from "./aabb2_compute_overlap.js"; import { aabb2_overlap_exists } from "./aabb2_overlap_exists.js"; /** * * Axis-Aligned Bounding Box in 2D */ class AABB2 { /** * @param {number} [x0=0] * @param {number} [y0=0] * @param {number} [x1=0] * @param {number} [y1=0] * @constructor */ constructor( x0 = 0, y0 = 0, x1 = 0, y1 = 0 ) { assert.isNumber(x0, `x0`); assert.isNumber(y0, `y0`); assert.isNumber(x1, `x1`); assert.isNumber(y1, `y1`); assert.notNaN(x0, `x0`); assert.notNaN(y0, `y0`); assert.notNaN(x1, `x1`); assert.notNaN(y1, `y1`); /** * * @type {number} */ this.x0 = x0; /** * * @type {number} */ this.y0 = y0; /** * * @type {number} */ this.x1 = x1; /** * * @type {number} */ this.y1 = y1; } /** * * @returns {number} */ get 0(){ return this.x0; } /** * * @returns {number} */ get 1(){ return this.y0; } /** * * @returns {number} */ get 2(){ return this.x1; } /** * * @returns {number} */ get 3(){ return this.y1; } /** * * @param {number} size */ growWidth(size){ assert.isNumber(size,'size'); assert.notNaN(size,'size'); this.x0 -= size; this.x1 += size; } /** * * @param {number} size */ growHeight(size){ assert.isNumber(size,'size'); assert.notNaN(size,'size'); this.y0 -= size; this.y1 += size; } /** * Expands box in every direction by a given amount * @param {number} size */ grow(size) { this.growWidth(size); this.growHeight(size); } /** * Shrinks the box in every direction by a given amount * @param {number} size */ shrink(size) { this.grow(-size); } /** * * @param {number[]} m */ applyMatrix3(m) { const m0 = m[0]; const m1 = m[1]; const m3 = m[3]; const m4 = m[4]; const m6 = m[6]; const m7 = m[7]; const x0 = this.x0; const y0 = this.y0; const x1 = this.x1; const y1 = this.y1; const _xa = m0 * x0 + m3 * y0 + m6; const _ya = m1 * x0 + m4 * y0 + m7; const _xb = m0 * x1 + m3 * y1 + m6; const _yb = m1 * x1 + m4 * y1 + m7; let _x0 = min2(_xa, _xb); let _x1 = max2(_xa, _xb); let _y0 = min2(_ya, _yb); let _y1 = max2(_ya, _yb); this.set(_x0, _y0, _x1, _y1); } /** * * @param {number} value */ multiplyScalar(value) { this.set( this.x0 * value, this.y0 * value, this.x1 * value, this.y1 * value ); } /** * * @param {AABB2} other * @param {AABB2} result Overlapping region will be written here * @returns {boolean} true if there is overlap, result is also written. false otherwise */ computeOverlap(other, result) { const ax0 = this.x0; const ay0 = this.y0; const ax1 = this.x1; const ay1 = this.y1; const bx0 = other.x0; const by0 = other.y0; const bx1 = other.x1; const by1 = other.y1; return aabb2_compute_overlap(ax0, ay0, ax1, ay1, bx0, by0, bx1, by1, result); } /** * * @param {AABB2} other * @returns {boolean} */ overlapExists(other) { const ax0 = this.x0; const ay0 = this.y0; const ax1 = this.x1; const ay1 = this.y1; const bx0 = other.x0; const by0 = other.y0; const bx1 = other.x1; const by1 = other.y1; return aabb2_overlap_exists(ax0, ay0, ax1, ay1, bx0, by0, bx1, by1); } /** * * @param {Number} x0 * @param {Number} y0 * @param {Number} x1 * @param {Number} y1 * @public */ _expandToFit(x0, y0, x1, y1) { this.x0 = min2(this.x0, x0); this.y0 = min2(this.y0, y0); this.x1 = max2(this.x1, x1); this.y1 = max2(this.y1, y1); } /** * * @param {number} x * @param {number} y */ _expandToFitPoint(x, y) { // shortcut, use a 0-size box this._expandToFit(x, y, x, y); } /** * NOTE: only 1 intersection point is produced * @param {Vector2} p0 * @param {Vector2} p1 * @param {Vector2} result * @returns {boolean} */ lineIntersectionPoint(p0, p1, result) { const x0 = this.x0; const y0 = this.y0; const x1 = this.x1; const y1 = this.y1; if (line_segment_compute_line_segment_intersection_2d(p0.x, p0.y, p1.x, p1.y, x0, y0, x1, y0, result)) { //top return true; } if (line_segment_compute_line_segment_intersection_2d(p0.x, p0.y, p1.x, p1.y, x0, y1, x1, y1, result)) { //bottom return true; } if (line_segment_compute_line_segment_intersection_2d(p0.x, p0.y, p1.x, p1.y, x0, y0, x0, y1, result)) { //left return true; } if (line_segment_compute_line_segment_intersection_2d(p0.x, p0.y, p1.x, p1.y, x1, y0, x1, y1, result)) { //right return true; } return false; } /** * * @param {Vector2} point * @param {Vector2} result */ computeNearestPointToPoint(point, result) { let x, y; const x0 = this.x0; const y0 = this.y0; const x1 = this.x1; const y1 = this.y1; const pX = point.x; const pY = point.y; x = clamp(pX, x0, x1); y = clamp(pY, y0, y1); result.set(x, y); } /** * * @param {AABB2} other * @returns {number} */ costForInclusion(other) { return this._costForInclusion(other.x0, other.y0, other.x1, other.y1); } /** * * @param {number} x0 * @param {number} y0 * @param {number} x1 * @param {number} y1 * @returns {number} */ _costForInclusion(x0, y0, x1, y1) { let x = 0; let y = 0; // const _x0 = this.x0; const _y0 = this.y0; const _x1 = this.x1; const _y1 = this.y1; // if (_x0 > x0) { x += _x0 - x0; } if (_x1 < x1) { x += x1 - _x1; } if (_y0 > y0) { y += _y0 - y0; } if (_y1 < y1) { y += y1 - _y1; } const dx = _x1 - _x0; const dy = _y1 - _y0; return x * dy + y * dx; } /** * * @returns {number} */ computeArea() { const x0 = this.x0; const y0 = this.y0; const x1 = this.x1; const y1 = this.y1; return aabb2_compute_area(x0, y0, x1, y1); } /** * * @return {number} */ computeSurfaceArea() { const x0 = this.x0; const y0 = this.y0; const x1 = this.x1; const y1 = this.y1; const dx = x1 - x0; const dy = y1 - y0; return 2 * (dx + dy); } /** * * @param {number} x * @param {number} y * @returns {boolean} */ containsPoint(x, y) { return x >= this.x0 && x <= this.x1 && y >= this.y0 && y <= this.y1; } /** * * @param {AABB2} other */ expandToFit(other) { this._expandToFit(other.x0, other.y0, other.x1, other.y1); } /** * * @param {Vector2} [result] */ getCenter(result = new Vector2()) { result.set(this.centerX, this.centerY); return result; } /** * retrieve midpoint of AABB along X axis * @deprecated use {@link centerX} instead * @returns {number} */ midX() { return this.centerX; } /** * retrieve midpoint of AABB along Y axis * @deprecated use {@link centerY} instead * @returns {number} */ midY() { return this.centerY; } /** * midpoint along X axis * @return {number} */ get centerX() { return (this.x0 + this.x1) * 0.5; } /** * midpoint along Y axis * @return {number} */ get centerY() { return (this.y0 + this.y1) * 0.5 } /** * * @returns {number} */ getWidth() { return this.x1 - this.x0; } /** * * @return {number} */ get width() { return this.getWidth(); } /** * * @returns {number} */ getHeight() { return this.y1 - this.y0; } /** * * @return {number} */ get height() { return this.getHeight(); } /** * * @param {Number} x0 * @param {Number} y0 * @param {Number} x1 * @param {Number} y1 * returns {AABB2} this */ set(x0, y0, x1, y1) { assert.isNumber(x0, `x0`); assert.isNumber(y0, `y0`); assert.isNumber(x1, `x1`); assert.isNumber(y1, `y1`); assert.notNaN(x0, `x0`); assert.notNaN(y0, `y0`); assert.notNaN(x1, `x1`); assert.notNaN(y1, `y1`); this.x0 = x0; this.y0 = y0; this.x1 = x1; this.y1 = y1; return this; } /** * * @param {number} x * @param {number} y */ setPosition(x, y) { const width = this.getWidth(); const height = this.getHeight(); this.set(x, y, x + width, y + height); } /** * Relative displacement of the AABB by given vector described by {@param deltaX} and {@param deltaY} * @param {number} deltaX * @param {number} deltaY */ move(deltaX, deltaY) { this.set( this.x0 + deltaX, this.y0 + deltaY, this.x1 + deltaX, this.y1 + deltaY ); } /** * * @returns {AABB2} */ clone() { return new AABB2(this.x0, this.y0, this.x1, this.y1); } /** * * @param {AABB2} other * @returns {AABB2} this */ copy(other) { return this.set(other.x0, other.y0, other.x1, other.y1); } /** * * @param {AABB2} other * @returns {boolean} */ equals(other) { return this.x0 === other.x0 && this.y0 === other.y0 && this.x1 === other.x1 && this.y1 === other.y1; } /** * Clamps AABB to specified region * @param {number} x0 * @param {number} y0 * @param {number} x1 * @param {number} y1 */ clamp(x0, y0, x1, y1) { this.x0 = clamp(this.x0, x0, x1); this.y0 = clamp(this.y0, y0, y1); this.x1 = clamp(this.x1, x0, x1); this.y1 = clamp(this.y1, y0, y1); } /** * Set bounds without having to maintain constraint that x0 >= x1 and y0 >= y1 * @param {number} x0 * @param {number} y0 * @param {number} x1 * @param {number} y1 */ setBoundsUnordered(x0, y0, x1, y1) { let _x0, _y0, _x1, _y1; if (x0 < x1) { _x0 = x0; _x1 = x1; } else { _x0 = x1; _x1 = x0; } if (y0 < y1) { _y0 = y0; _y1 = y1; } else { _y0 = y1; _y1 = y0; } this.set(_x0, _y0, _x1, _y1); } /** * returns {AABB2} */ setNegativelyInfiniteBounds() { return this.set(Number.POSITIVE_INFINITY, Number.POSITIVE_INFINITY, Number.NEGATIVE_INFINITY, Number.NEGATIVE_INFINITY); } toString() { return `AABB2{x0:${this.x0}, y0:${this.y0}, x1:${this.x1}, y1:${this.y1}}`; } toJSON() { return { x0: this.x0, y0: this.y0, x1: this.x1, y1: this.y1 }; } fromJSON(json) { this.set(json.x0, json.y0, json.x1, json.y1); } /** * @param {number[]|Float32Array} target * @param {number} offset * @returns {number[]|Float32Array} */ toArray(target=[], offset=0){ target[offset] = this.x0; target[offset+1] = this.y0; target[offset+2] = this.x1; target[offset+3] = this.y1; return target; } } /** * * @param {AABB2} b0 * @param {AABB2} b1 * @param {Vector2} p0 resulting line segment point * @param {Vector2} p1 resulting line segment point * @returns {boolean} false if no intersection line found */ function computeLineBetweenTwoBoxes(b0, b1, p0, p1) { //compute box centers const c0 = new Vector2(b0.centerX, b0.centerY); const c1 = new Vector2(b1.centerX, b1.centerY); const i0 = b0.lineIntersectionPoint(c0, c1, p0); if (!i0) { // console.error("No intersection point: ", b0, c0, c1); return false; } const i1 = b1.lineIntersectionPoint(c0, c1, p1); if (!i1) { // console.error("No intersection point: ", b1, c0, c1); return false; } return true; } AABB2.computeLineBetweenTwoBoxes = computeLineBetweenTwoBoxes; /** * @readonly * @type {AABB2} */ AABB2.zero = Object.freeze(new AABB2(0, 0, 0, 0)); /** * @readonly * @type {AABB2} */ AABB2.unit = Object.freeze(new AABB2(0, 0, 1, 1)); export default AABB2;