@woosh/meep-engine
Version:
Pure JavaScript game engine. Fully featured and production ready.
687 lines (585 loc) • 14.8 kB
JavaScript
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;