Home Reference Source

src/Box2.js

import { Sphere } from "./Sphere.js";
import { Vector2 } from "./Vector2.js";

/**
 * A vector.
 *
 * @type {Vector2}
 * @private
 */

const v = new Vector2();

/**
 * A 2D box.
 */

export class Box2 {

	/**
	 * Constructs a new box.
	 *
	 * @param {Vector2} [min] - The lower bounds.
	 * @param {Vector2} [max] - The upper bounds.
	 */

	constructor(
		min = new Vector2(Infinity, Infinity),
		max = new Vector2(-Infinity, -Infinity)
	) {

		/**
		 * The lower bounds.
		 *
		 * @type {Vector2}
		 */

		this.min = min;

		/**
		 * The upper bounds.
		 *
		 * @type {Vector2}
		 */

		this.max = max;

	}

	/**
	 * Sets the values of this box.
	 *
	 * @param {Vector2} min - The lower bounds.
	 * @param {Vector2} max - The upper bounds.
	 * @return {Box2} This box.
	 */

	set(min, max) {

		this.min.copy(min);
		this.max.copy(max);

		return this;

	}

	/**
	 * Copies the values of a given box.
	 *
	 * @param {Box2} b - A box.
	 * @return {Box2} This box.
	 */

	copy(b) {

		this.min.copy(b.min);
		this.max.copy(b.max);

		return this;

	}

	/**
	 * Clones this box.
	 *
	 * @return {Box2} A clone of this box.
	 */

	clone() {

		return new this.constructor().copy(this);

	}

	/**
	 * Makes this box empty.
	 *
	 * The lower bounds are set to infinity and the upper bounds to negative
	 * infinity to create an infinitely small box.
	 *
	 * @return {Box2} This box.
	 */

	makeEmpty() {

		this.min.x = this.min.y = Infinity;
		this.max.x = this.max.y = -Infinity;

		return this;

	}

	/**
	 * Indicates whether this box is truly empty.
	 *
	 * This is a more robust check for emptiness since the volume can get positive
	 * with two negative axes.
	 *
	 * @return {Box2} This box.
	 */

	isEmpty() {

		return (
			this.max.x < this.min.x ||
			this.max.y < this.min.y
		);

	}

	/**
	 * Computes the center of this box.
	 *
	 * @param {Vector2} [target] - A target vector. If none is provided, a new one will be created.
	 * @return {Vector2} A vector that describes the center of this box.
	 */

	getCenter(target = new Vector2()) {

		return !this.isEmpty() ?
			target.addVectors(this.min, this.max).multiplyScalar(0.5) :
			target.set(0, 0);

	}

	/**
	 * Computes the size of this box.
	 *
	 * @param {Vector2} [target] - A target vector. If none is provided, a new one will be created.
	 * @return {Vector2} A vector that describes the size of this box.
	 */

	getSize(target = new Vector2()) {

		return !this.isEmpty() ?
			target.subVectors(this.max, this.min) :
			target.set(0, 0);

	}

	/**
	 * Computes the bounding sphere of this box.
	 *
	 * @param {Sphere} [target] - A target sphere. If none is provided, a new one will be created.
	 * @return {Sphere} The bounding sphere of this box.
	 */

	getBoundingSphere(target = new Sphere()) {

		this.getCenter(target.center);

		target.radius = this.getSize(v).length() * 0.5;

		return target;

	}

	/**
	 * Expands this box by the given point.
	 *
	 * @param {Vector2} p - A point.
	 * @return {Box2} This box.
	 */

	expandByPoint(p) {

		this.min.min(p);
		this.max.max(p);

		return this;

	}

	/**
	 * Expands this box by the given vector.
	 *
	 * @param {Vector2} v - A vector.
	 * @return {Box2} This box.
	 */

	expandByVector(v) {

		this.min.sub(v);
		this.max.add(v);

		return this;

	}

	/**
	 * Expands this box by the given scalar.
	 *
	 * @param {Number} s - A scalar.
	 * @return {Box2} This box.
	 */

	expandByScalar(s) {

		this.min.addScalar(-s);
		this.max.addScalar(s);

		return this;

	}

	/**
	 * Defines this box by the given points.
	 *
	 * @param {Vector2[]} points - The points.
	 * @return {Box2} This box.
	 */

	setFromPoints(points) {

		let i, l;

		this.min.set(0, 0);
		this.max.set(0, 0);

		for(i = 0, l = points.length; i < l; ++i) {

			this.expandByPoint(points[i]);

		}

		return this;

	}

	/**
	 * Defines this box by the given center and size.
	 *
	 * @param {Vector2} center - The center.
	 * @param {Number} size - The size.
	 * @return {Box2} This box.
	 */

	setFromCenterAndSize(center, size) {

		const halfSize = v.copy(size).multiplyScalar(0.5);

		this.min.copy(center).sub(halfSize);
		this.max.copy(center).add(halfSize);

		return this;

	}

	/**
	 * Clamps the given point to the boundaries of this box.
	 *
	 * @param {Vector2} point - A point.
	 * @param {Vector2} [target] - A target vector. If none is provided, a new one will be created.
	 * @return {Vector2} The clamped point.
	 */

	clampPoint(point, target = new Vector2()) {

		return target.copy(point).clamp(this.min, this.max);

	}

	/**
	 * Calculates the distance from this box to the given point.
	 *
	 * @param {Vector2} p - A point.
	 * @return {Number} The distance.
	 */

	distanceToPoint(p) {

		const clampedPoint = v.copy(p).clamp(this.min, this.max);

		return clampedPoint.sub(p).length();

	}

	/**
	 * Translates this box.
	 *
	 * @param {Vector2} offset - The offset.
	 * @return {Box2} This box.
	 */

	translate(offset) {

		this.min.add(offset);
		this.max.add(offset);

		return this;

	}

	/**
	 * Intersects this box with the given one.
	 *
	 * @param {Box2} b - A box.
	 * @return {Box2} This box.
	 */

	intersect(b) {

		this.min.max(b.min);
		this.max.min(b.max);

		/* Ensure that if there is no overlap, the result is fully empty to prevent
		subsequent intersections to erroneously return valid values. */
		if(this.isEmpty()) {

			this.makeEmpty();

		}

		return this;

	}

	/**
	 * Expands this box by combining it with the given one.
	 *
	 * @param {Box2} b - A box.
	 * @return {Box2} This box.
	 */

	union(b) {

		this.min.min(b.min);
		this.max.max(b.max);

		return this;

	}

	/**
	 * Checks if the given point lies inside this box.
	 *
	 * @param {Vector2} p - A point.
	 * @return {Boolean} Whether this box contains the point.
	 */

	containsPoint(p) {

		const min = this.min;
		const max = this.max;

		return (
			p.x >= min.x &&
			p.y >= min.y &&
			p.x <= max.x &&
			p.y <= max.y
		);

	}

	/**
	 * Checks if the given box lies inside this box.
	 *
	 * @param {Box2} b - A box.
	 * @return {Boolean} Whether this box contains the given one.
	 */

	containsBox(b) {

		const tMin = this.min;
		const tMax = this.max;
		const bMin = b.min;
		const bMax = b.max;

		return (
			tMin.x <= bMin.x && bMax.x <= tMax.x &&
			tMin.y <= bMin.y && bMax.y <= tMax.y
		);

	}

	/**
	 * Checks if this box intersects the given one.
	 *
	 * @param {Box2} b - A box.
	 * @return {Boolean} Whether the boxes intersect.
	 */

	intersectsBox(b) {

		const tMin = this.min;
		const tMax = this.max;
		const bMin = b.min;
		const bMax = b.max;

		return (
			bMax.x >= tMin.x &&
			bMax.y >= tMin.y &&
			bMin.x <= tMax.x &&
			bMin.y <= tMax.y
		);

	}

	/**
	 * Checks if this box equals the given one.
	 *
	 * @param {Box2} b - A box.
	 * @return {Boolean} Whether this box equals the given one.
	 */

	equals(b) {

		return (b.min.equals(this.min) && b.max.equals(this.max));

	}

}