windowmanager
Version:
A framework to manage multiple dockable, HTML windows
533 lines (462 loc) • 18.6 kB
JavaScript
// TODO: Utilize ES6 features (like for loops)
import Vector from './Vector';
import CollisionMesh from './CollisionMesh';
// Utility functions:
function minAbs(min, ...vals) {
let minAbs = Math.abs(min);
for (const val of vals) {
let argAbs = Math.abs(val);
if (argAbs < minAbs) {
min = val;
minAbs = argAbs;
}
}
return {
min: min,
abs: minAbs
};
}
/**
* A BoundingBox class.
*/
class BoundingBox {
/**
* @param {Number} left - The left position of x-axis.
* @param {Number} top - The top position of y-axis.
* @param {Number} right - The right position of x-axis.
* @param {Number} bottom - The bottom position of y-axis.
*/
constructor(left, top, right, bottom) {
let obj = left;
if (obj && obj.constructor !== Number) {
if (obj.getBoundingBox) { obj = obj.getBoundingBox(); }
// new BoundingBox(obj)
this.left = obj.left;
this.top = obj.top;
this.right = obj.right;
this.bottom = obj.bottom;
} else {
// new BoundingBox(left, top, right, bottom)
this.left = left;
this.top = top;
this.right = right;
this.bottom = bottom;
}
}
/**
* Clone the current boundingbox to a new object.
* @returns {BoundingBox} A clone of this instance
*/
clone() {
return new BoundingBox(this);
}
/**
* Checks if any property on `this` is NaN.
* @returns {Boolean}
*/
isNaN() {
return isNaN(this.left) || isNaN(this.top) || isNaN(this.right) || isNaN(this.bottom);
}
/**
* Returns the width of `this`.
* @returns {Number} width
*/
getWidth() {
return Math.abs(this.right - this.left);
}
/**
* Returns the height of `this`.
* @returns {Number} height
*/
getHeight() {
return Math.abs(this.bottom - this.top);
}
/**
* Returns the size of `this`.
* @returns {Vector} size
*/
getSize() {
return new Vector(this.getWidth(), this.getHeight());
}
/**
* Returns the area of `this`.
* @returns {Number} area
*/
getArea() {
return this.getWidth() * this.getHeight();
}
/**
* Returns the position of `this`.
* @returns {Vector} position
*/
getPosition() {
return new Vector(this.left, this.top);
}
/**
* Resolve this object down to a {@link BoundingBox} instance.
* Since this instance is already a boundingbox, it returns itself.
* @returns {BoundingBox} self
*/
getBoundingBox() {
// We have this method, so any prototype in this script will return their bounding box,
// and if they are one it will return itself.
// This simplifies code, and prevents having to do a ton of checks.
return this;
}
/**
* Returns a {@link CollisionMesh} instance version of this boundingbox similar to:<br>
* ```javascript
* new CollisionMesh(BoundingBox)
* ```
* @returns {CollisionMesh}
*/
getCollisionMesh() {
return new CollisionMesh(this);
}
/**
* Returns the center position of `this`.
* @returns {Vector} position
*/
getCenterPosition() {
return new Vector(this.left + this.getWidth() / 2, this.top + this.getHeight() / 2);
}
/**
* Returns `this` subtract `other`.
* @param {BoundingBox}
* @returns {Vector} position
*/
difference(other) {
if (!other) { throw new Error("difference requires argument 'other'"); }
other = other.getBoundingBox();
return new BoundingBox(this.left - other.left, this.top - other.top,
this.right - other.right, this.bottom - other.bottom);
}
/**
* Returns a position, which if `this` is set to, `this` will be centered on `other`.
* @param {BoundingBox}
* @returns {Vector} position
*/
getCenteredOnPosition(other) {
if (!other) { throw new Error("getCenteredOnPosition requires argument 'other'"); }
other = other.getBoundingBox();
return other.getCenterPosition().subtract(this.getCenterPosition().subtract(this.getPosition()));
}
/**
* Returns the intersection between `this` and `other`.
* This will return a {@link Vector} if they only intersect at a point.
* This will return a {@link BoundingBox} if they intersect over an area or line.
* This will return a undefined if they do not intersect.
* @param {BoundingBox}
* @returns {Vector|BoundingBox|undefined} intersection object
*/
getIntersection(other) {
if (!other) { throw new Error("getIntersection requires argument 'other'"); }
other = other.getBoundingBox();
let left = Math.max(this.left, other.left);
let top = Math.max(this.top, other.top);
let right = Math.min(this.right, other.right);
let bottom = Math.min(this.bottom, other.bottom);
if (left === right && top === bottom) {
return new Vector(left, top);
} else if (left <= right && top <= bottom) {
return new BoundingBox(left, top, right, bottom);
}
}
/**
* Returns the squared distance between `this` and `other`.
* @param {Vector}
* @returns {Number} squared distance
*/
getDistanceSquaredToPoint(other) {
other = other.getVector();
let cLeft = (other.left <= this.left ? this.left : (other.left >= this.right ? this.right : other.left));
let cTop = (other.top <= this.top ? this.top : (other.top >= this.bottom ? this.bottom : other.top));
let cPos = new Vector(cLeft, cTop);
return cPos.distanceSquared(other);
}
/**
* Returns the distance between `this` and `other`.
* @param {Vector}
* @returns {Number} distance
*/
getDistanceToPoint(other) {
return Math.sqrt(this.getDistanceSquaredToPoint(other));
}
/**
* Sets `this`'s properties to `other`'s properties.
* @param {BoundingBox}
* @returns {BoundingBox} self
*/
set(other) {
if (!other) { throw new Error("set requires argument 'other'"); }
other = other.getBoundingBox();
this.left = other.left;
this.top = other.top;
this.right = other.right;
this.bottom = other.bottom;
return this;
}
/**
* Move `this` to position at `left` and/or `top`.
* @param {Number} [left=null]
* @param {Number} [top=null]
* @returns {BoundingBox} self
*/
moveTo(left, top) {
if (left && left.constructor === Number) {
this.right = left + (this.right - this.left);
this.left = left;
}
if (top && top.constructor === Number) {
this.bottom = top + (this.bottom - this.top);
this.top = top;
}
return this;
}
/**
* Move `this` relatively to position by `deltaLeft` and/or `deltaTop`.
* @param {Number} [deltaLeft=null]
* @param {Number} [deltaTop=null]
* @returns {BoundingBox} self
*/
moveBy(deltaLeft, deltaTop) {
if (deltaLeft && deltaLeft.constructor === Number) {
this.left += deltaLeft;
this.right += deltaLeft;
}
if (deltaTop && deltaTop.constructor === Number) {
this.top += deltaTop;
this.bottom += deltaTop;
}
return this;
}
/**
* Resize `this` to size `width` and/or `height`, anchored at `anchor`.
* @param {Number} [width=null]
* @param {Number} [height=null]
* @param {String} [anchor='top-left'] supports "top-left", "top-right", "bottom-left", or "bottom-right"
* @returns {BoundingBox} self
*/
resizeTo(width, height, anchor) {
// NOTE: anchor supports "top-left", "top-right", "bottom-left", or "bottom-right". By default it is "top-left".
// NOTE: anchor also supports being passed as a position. Allowing the resize anchor to be anywhere other than
// the predefined strings.
let curSize = this.getSize();
let newSize = new Vector(width || curSize.left, height || curSize.top);
anchor = anchor || 'top-left';
if (typeof anchor === 'string' || anchor instanceof String) {
let anchorStr = anchor;
anchor = this.getPosition();
if (anchorStr.indexOf('right') >= 0) { anchor.left += curSize.left; }
if (anchorStr.indexOf('bottom') >= 0) { anchor.top += curSize.top; }
}
this.left += (anchor.left - this.left) * (curSize.left - newSize.left) / curSize.left;
this.right += (anchor.left - this.right) * (curSize.left - newSize.left) / curSize.left;
this.top += (anchor.top - this.top) * (curSize.top - newSize.top) / curSize.top;
this.bottom += (anchor.top - this.bottom) * (curSize.top - newSize.top) / curSize.top;
return this;
}
/**
* Determines if `this` encapsulates `other`.
* @param {BoundingBox}
* @returns {Boolean}
*/
isContains(other) {
if (!other) { throw new Error("isContains requires argument 'other'"); }
other = other.getBoundingBox();
return other.left >= this.left && other.right <= this.right &&
other.top >= this.top && other.bottom <= this.bottom;
}
/**
* Determines if `this` encapsulates at least one of `others`.
* @param {BoundingBox[]}
* @returns {Boolean}
*/
someContains(others) {
if (!others) { throw new Error("someContains requires argument 'others'"); }
if (others.constructor !== Array) { throw new Error("someContains requires argument 'others' of type Array"); }
for (let index = 0; index < others.length; index += 1) {
if (this.isContains(others[index])) { return true; }
}
return false;
}
/**
* Determines if `this` touches an edge of `other`, but does not intersect area.
* @param {BoundingBox}
* @returns {Boolean}
*/
isTouching(other) {
if (!other) { throw new Error("isTouching requires argument 'other'"); }
other = other.getBoundingBox();
return ((this.top <= other.bottom && this.bottom >= other.top) &&
(this.left === other.right || this.right === other.left)) ||
((this.left <= other.right && this.right >= other.left) &&
(this.top === other.bottom || this.bottom === other.top));
}
/**
* If `this` touches one of `others`, but does not intersect area, then this returns the `this` edge name.
* @param {BoundingBox[]}
* @returns {String|undefined} edge name
*/
getEdgeTouching(others) {
if (!others) { throw new Error("getEdgeTouching requires argument 'others'"); }
if (others.constructor !== Array) { others = [others]; }
for (let index = 0; index < others.length; index += 1) {
let other = others[index].getBoundingBox();
if (this.top <= other.bottom && this.bottom >= other.top) {
if (this.left === other.right) { return 'left'; }
if (this.right === other.left) { return 'right'; }
}
if (this.left <= other.right && this.right >= other.left) {
if (this.top === other.bottom) { return 'top'; }
if (this.bottom === other.top) { return 'bottom'; }
}
}
}
/**
* If `this` touches one of `others`, but does not intersect area, then this returns the `other` edge name.
* @param {BoundingBox[]}
* @returns {String|undefined} edge name
*/
getOtherEdgeTouching(others) {
if (!others) { throw new Error("getOtherEdgeTouching requires argument 'others'"); }
if (others.constructor !== Array) { others = [others]; }
for (let index = 0; index < others.length; index += 1) {
let other = others[index].getBoundingBox();
if (this.top <= other.bottom && this.bottom >= other.top) {
if (this.left === other.right) { return 'right'; }
if (this.right === other.left) { return 'left'; }
}
if (this.left <= other.right && this.right >= other.left) {
if (this.top === other.bottom) { return 'bottom'; }
if (this.bottom === other.top) { return 'top'; }
}
}
}
/**
* Determines which edges of `this` is closest to `other`, returns all edges in sorted order by distance.
* @param {BoundingBox}
* @returns {String[]} edge names sorted from closest to furthest
*/
getEdgeClosestOrder(other) {
if (!other) { throw new Error("getEdgeClosest requires argument 'other'"); }
other = other.getBoundingBox();
let centerPos = this.getCenterPosition();
let dis = [];
dis.push({
'edge': 'left',
dis: other.getDistanceSquaredToPoint(this.left, centerPos.top)
});
dis.push({
'edge': 'top',
dis: other.getDistanceSquaredToPoint(centerPos.left, this.top)
});
dis.push({
'edge': 'right',
dis: other.getDistanceSquaredToPoint(this.right, centerPos.top)
});
dis.push({
'edge': 'bottom',
dis: other.getDistanceSquaredToPoint(centerPos.left, this.bottom)
});
dis.sort(function (a, b) {
return a.dis - b.dis;
});
return dis.map(function (dis) { return dis.edge; });
}
/**
* Determines which `this` edge is closest to `other`.
* @param {BoundingBox}
* @returns {String} edge name
*/
getEdgeClosest(other) {
let edges = this.getEdgeClosestOrder(other);
return edges[0];
}
/**
* Returns a vector representing the delta position to add to `this` to snap to `other`.<br>
* Note: `snapDelta` may contain `NaN` for `left` or `right`
* @param {BoundingBox}
* @param {Number} [snapDistance=5] max distance to move `this`
* @returns {Vector} snapDelta
*/
getSnapDelta(other, snapDistance) {
if (!other) { throw new Error("getSnapDelta requires argument 'other'"); }
other = other.getBoundingBox();
snapDistance = snapDistance || 5;
let snapDelta = new Vector(NaN, NaN);
if (this.top <= other.bottom && this.bottom >= other.top) {
// Handle x-snap:
const leftRightDis = minAbs(other.left - this.right, other.right - this.left);
if (leftRightDis.abs <= snapDistance) { // this.LeftRightSnapTo(other)
snapDelta.left = leftRightDis.min;
// Handle y-subsnap:
const topBottomDis = minAbs(other.top - this.top, other.bottom - this.bottom);
if (topBottomDis.abs <= snapDistance) { // this.TopBottomSubSnapTo(other)
snapDelta.top = topBottomDis.min;
}
}
} else if (this.left <= other.right && this.right >= other.left) {
// Handle y-snap:
const topBottomDis = minAbs(other.top - this.bottom, other.bottom - this.top);
if (topBottomDis.abs <= snapDistance) { // this.TopBottomSnapTo(other)
snapDelta.top = topBottomDis.min;
// Handle x-subsnap:
const leftRightDis = minAbs(other.left - this.left, other.right - this.right);
if (leftRightDis.abs <= snapDistance) { // this.LeftRightSubSnapTo(other)
snapDelta.left = leftRightDis.min;
}
}
}
return snapDelta;
}
/**
* Determines if `this` touches an edge of one of `others`, but does not intersect area.
* @param {BoundingBox[]}
* @returns {Boolean}
*/
someTouching(others) {
if (!others) { throw new Error("someTouching requires argument 'others'"); }
if (others.constructor !== Array) { throw new Error("someTouching requires argument 'others' of type Array"); }
for (let index = 0; index < others.length; index += 1) {
if (this.isTouching(others[index])) { return true; }
}
return false;
}
/**
* Determines if `this` intersects an area of `others`, not an edge.
* @param {BoundingBox}
* @returns {Boolean}
*/
isColliding(other) {
if (!other) { throw new Error("isColliding requires argument 'other'"); }
other = other.getBoundingBox();
return this.left < other.right && this.right > other.left && this.top < other.bottom && this.bottom > other.top;
}
/**
* Determines if `this` intersects an area of one of `others`, not an edge.
* @param {BoundingBox[]}
* @returns {Boolean}
*/
someColliding(others) {
if (!others) { throw new Error("someColliding requires argument 'others'"); }
if (others.constructor !== Array) { throw new Error("someColliding requires argument 'others' of type Array"); }
for (let index = 0; index < others.length; index += 1) {
if (this.isColliding(others[index])) { return true; }
}
return false;
}
/**
* Returns which of `other` that `this` intersects an area of, not an edge.
* @param {BoundingBox[]}
* @returns {BoundingBox|undefined}
*/
getColliding(others) {
if (!others) { throw new Error("getColliding requires argument 'others'"); }
if (others.constructor !== Array) { throw new Error("getColliding requires argument 'others' of type Array"); }
for (let index = 0; index < others.length; index += 1) {
if (this.isColliding(others[index])) { return others[index]; }
}
}
}
export default BoundingBox;