UNPKG

2d-quadtree

Version:

Quadtree for games and simulations

726 lines (556 loc) 20.3 kB
<!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8"> <title>JSDoc: Source: quadtree.js</title> <script src="scripts/prettify/prettify.js"> </script> <script src="scripts/prettify/lang-css.js"> </script> <!--[if lt IE 9]> <script src="//html5shiv.googlecode.com/svn/trunk/html5.js"></script> <![endif]--> <link type="text/css" rel="stylesheet" href="styles/prettify-tomorrow.css"> <link type="text/css" rel="stylesheet" href="styles/jsdoc-default.css"> </head> <body> <div id="main"> <h1 class="page-title">Source: quadtree.js</h1> <section> <article> <pre class="prettyprint source linenums"><code>'use strict'; var _ = require('underscore'); var DEFAULT_MAX_CHILDREN = 4, DEFAULT_DEPTH = 4, DEFAULT_WIDTH = 1000, DEFAULT_HEIGHT = 1000, NORTH_WEST = 1, NORTH_EAST = 2, SOUTH_WEST = 4, SOUTH_EAST = 8; /** * Rectangles inserted into the quadtree are extended with this object literal */ var rectPrototype = { /** * Moves the rectangle in the quadtree to a new position (x, y) * @method * @param {Number} x The x coordinate as defined by the quadtree coordinate system * @param {Number} y The y coordinate as defined by the quadtree coordinate system */ 'move': function (x, y) { this.x = x; this.y = y; if (this.parent.orphans.indexOf(this) !== -1 || !isWithinBounds(this.parent, this)) { this.parent.remove(this); this.parent.insert(this); } }, /** * Returns an array of the rectangles within the quadtree that intersect with this rectangle * @method * @return {Array} The rectangles that intersect with this rectangle */ 'getCollisions': function () { return this.parent.getCollisions(this); }, /** * Comepletely removes this rectangle from the quadtree * @method */ 'remove': function () { this.parent.remove(this); } }; /** * Quadtree contstructor function. Use to initialize the Quadtree. Also, whenever the quadtree splits, * this constructor is used to initialize the new nodes of the quadtree. * @constructor * @param {Object} options The only options of concern to you: width, height, maxChildren, depth */ function Quadtree (options) { options = options || {}; this.maxChildren = options.maxChildren || DEFAULT_MAX_CHILDREN; this.depth = options.depth || DEFAULT_DEPTH; this.height = options.height || DEFAULT_HEIGHT; this.width = options.width || DEFAULT_WIDTH; this.halfHeight = this.height / 2; this.halfWidth = this.width / 2; this.x = options.x || 0; this.y = options.y || 0; this.parent = options.parent || null; this.children = []; this.orphans = []; this.isLeaf = true; this.quadrant = options.quadrant || (NORTH_WEST + NORTH_EAST + SOUTH_WEST + SOUTH_EAST); } /** * * Inserts an object into the quadTree * @param {Object} An arbitrary object with rectangle properties (x, y, width, height) */ Quadtree.prototype.insert = function (object) { var numberOfChildren = this.children.length; if (!hasRectProps(object)) { throw 'Inserting an object into the Quadtree requires a height, width, x, and y property'; } if (!object.move) { _.extend(object, rectPrototype); } if (!isWithinBounds(this, object)) { if (this.parent) { this.parent.insert(object); return; } else { forceObjectWithinBounds(object, this); } } object.parent = this; object.quadrant = undefined; // This quadTree does not contain quadTrees if (this.isLeaf) { this.children.push(object); setQuadrant(object, this); if (this.children.length > this.maxChildren &amp;&amp; this.depth) { this.divide(); return; } // This quadTree contains quadTrees // We should check if the object we are inserting can be completely contained within // one of these quadTrees. If it can't, it must be an orphan. } else { for (var i = 0; i &lt; numberOfChildren; i++) { if (isWithinBounds(this.children[i], object)) { this.children[i].insert(object); return; } } // Object does not fit within any of the sub-quadTrees. It's an orphan. setQuadrant(object, this); this.orphans.push(object); } }; /** * Removes the object and potentially collapses the quadTree * @method remove * @param {Object} Item that was inserted into the quadTree */ Quadtree.prototype.remove = function (object) { var parent = object.parent, children = parent.children, orphans = parent.orphans, newParent = parent; if (_.contains(children, object)) { children.splice(children.indexOf(object), 1); } else if (_.contains(orphans, object)) { orphans.splice(orphans.indexOf(object), 1); } else { throw 'Object not found in quadTree when attempting to remove'; } while (newParent.parent) { newParent = newParent.parent; } object.parent = newParent; parent.collapse(); }; /** * Partitions the quadTree into 4 equal sized quadTrees. * It also re-inserts all of the children that the leaf contained. */ Quadtree.prototype.divide = function () { var children = this.children, quarterWidth = this.width / 4, quarterHeight = this.height / 4, options = { 'depth' : this.depth - 1, 'width' : this.width / 2, 'height': this.height / 2, 'parent': this }; this.isLeaf = false; this.children = [ new Quadtree(_.extend(options, { 'x' : this.x - quarterWidth, 'y' : this.y - quarterHeight, 'quadrant': NORTH_WEST })), new Quadtree(_.extend(options, { 'x' : this.x + quarterWidth, 'y' : this.y - quarterHeight, 'quadrant': NORTH_EAST })), new Quadtree(_.extend(options, { 'x' : this.x - quarterWidth, 'y' : this.y + quarterHeight, 'quadrant': SOUTH_WEST })), new Quadtree(_.extend(options, { 'x' : this.x + quarterWidth, 'y' : this.y + quarterHeight, 'quadrant': SOUTH_EAST })) ]; for (var i = 0, l = children.length; i &lt; l; i++) { this.insert(children[i]); } }; /** * Collapses the quadTree */ Quadtree.prototype.collapse = function () { if (this.parent) { if (this !== this.parent.children[0] &amp;&amp; this !== this.parent.children[1] &amp;&amp; this !== this.parent.children[2] &amp;&amp; this !== this.parent.children[3]) { throw 'This was a bug that was fixed, but I am paranoid this will get hit so I left it...'; } } if (this.parent &amp;&amp; this.parent.canCollapse()) { this.parent.collapse(); return; } if (this.canCollapse() &amp;&amp; !this.isLeaf) { var allChildrenAndOrphans = this.getOrphansAndChildren(); this.children = []; this.orphans = []; this.isLeaf = true; for (var i = 0; i &lt; allChildrenAndOrphans.length; i++) { this.insert(allChildrenAndOrphans[i]); } } }; /** * Helper method that determines if the quadtree should collapse */ Quadtree.prototype.canCollapse = function () { return this.getOrphanAndChildCount() &lt;= this.maxChildren; } /** * getOrphanCount returns the number of orphans in the quadTree * @return {Array} number of orphans in the quadTree */ Quadtree.prototype.getOrphanCount = function () { var numberOfOrphans = this.orphans.length, numberOfChildren = this.children.length, count = numberOfOrphans; if (this.isLeaf) { if (count !== 0) { throw 'Why does this leaf have orphans?!'; } return count; // should be 0. } else { for (var i = 0; i &lt; numberOfChildren; i++) { count += this.children[i].getOrphanCount(); } } return count; }; /** * Returns the number of children in the quadTree * @return {Number} The number of children in the quadTree */ Quadtree.prototype.getChildCount = function () { var count = 0, numberOfChildren = this.children.length; if (!this.isLeaf) { for (var i = 0; i &lt; numberOfChildren; i++) { count += this.children[i].getChildCount(); } } else { count += numberOfChildren; } return count; }; /** * getOrphanAndChildCount returns all rectangles that have been inserted into the quadtree * @return {Number} The number of all inserted objects in the quadtree */ Quadtree.prototype.getOrphanAndChildCount = function () { return this.getOrphanCount() + this.getChildCount(); }; /** * getOrphans return all the orphans of the quadTree * @return {Array} all the orphans of the quadTree */ Quadtree.prototype.getOrphans = function () { var orphans = []; if (!this.isLeaf) { orphans = this.orphans; for (var i = 0; i &lt; this.children.length; i++) { orphans = orphans.concat(this.children[i].getOrphans()); } } return orphans; }; /** * getChildren returns an array of all the children of the quadTree * @return {Array} all the children of the quadTree */ Quadtree.prototype.getChildren = function () { var children = []; if (this.isLeaf) { return this.children; } else { for (var i = 0; i &lt; this.children.length; i++) { children = children.concat(this.children[i].getChildren()); } } return children; }; /** * getOrphansAndChildren returns an array of all the children and orphans of the quadTree * @return {Array} all the children and orphans of the quadTree */ Quadtree.prototype.getOrphansAndChildren = function () { return this.getChildren().concat(this.getOrphans()); }; /** * getQuadtreeCount returns the number of divisions within the quadtree. * @return {Number} The number of divisions within the quadtree. */ Quadtree.prototype.getQuadtreeCount = function () { var count = this.children.length; if (this.isLeaf) { return 0; } for (var i = 0; i &lt; this.children.length; i++) { count += this.children[i].getQuadtreeCount(); } return count; }; Quadtree.prototype.getEntireQuadtreesOrphansAndChildren = function () { var originalParent = this; while (originalParent.parent) { originalParent = originalParent.parent; } return originalParent.getOrphansAndChildren(); } Quadtree.prototype.getParentOrphanComparisons = function () { var comparisonList = [], orphans = this.parent &amp;&amp; this.parent.orphans; if (!orphans) { return comparisonList; } for (var i = 0; i &lt; orphans.length; i++) { if ((orphans[i].quadrant &amp; this.quadrant)) { comparisonList.push(orphans[i]); } } return comparisonList.concat(this.parent.getParentOrphanComparisons()); }; Quadtree.prototype.getCollisions = function (rect) { if (!hasRectProps(rect)) { throw 'Collsion must be a rect'; } return getCollisions(this.getComparisons(rect), rect); }; // This might be an area to optimized. A rectangle that is an orphans of the parent-most quadtree // that overlaps all quadrants will be the same as a brute force collision detector. Quadtree.prototype.getOrphansAndChildrenInQuadrants = function (rect) { var orphansAndChildren = [], quadrant = rect.quadrant; if (quadrant &amp; NORTH_WEST) { orphansAndChildren = orphansAndChildren.concat(this.children[0].getOrphansAndChildren()); } if (quadrant &amp; NORTH_EAST) { orphansAndChildren = orphansAndChildren.concat(this.children[1].getOrphansAndChildren()); } if (quadrant &amp; SOUTH_WEST) { orphansAndChildren = orphansAndChildren.concat(this.children[2].getOrphansAndChildren()); } if (quadrant &amp; SOUTH_EAST) { orphansAndChildren = orphansAndChildren.concat(this.children[3].getOrphansAndChildren()); } return orphansAndChildren; }; Quadtree.prototype.getComparisons = function (rect) { if (!hasRectProps(rect)) { throw 'Collsion must be a rect'; } if (!rect.quadrant) { throw 'Rect does not have a quadrant property'; } var comparisonList = rect.parent.isLeaf ? rect.parent.getChildren() : rect.parent.getOrphansAndChildrenInQuadrants(rect), directOrphans = rect.parent.orphans, parentOrphanComparisons = rect.parent.getParentOrphanComparisons(rect); for (var i = 0; i &lt; directOrphans.length; i++) { if ((directOrphans[i].quadrant &amp; rect.quadrant)) { comparisonList.push(directOrphans[i]); } } comparisonList = comparisonList.concat(parentOrphanComparisons); if (_.contains(comparisonList, rect)) { comparisonList.splice(comparisonList.indexOf(rect), 1); } return comparisonList; }; Quadtree.prototype.getBruteForceCollisions = function (rect) { if (!hasRectProps(rect)) { throw 'Collsion must be a rect'; } var comparisonList, currentQuadTree = this; while (currentQuadTree.parent) { currentQuadTree = currentQuadTree.parent; } comparisonList = currentQuadTree.getOrphansAndChildren(); if (_.contains(comparisonList, rect)) { comparisonList.splice(comparisonList.indexOf(rect), 1); } return getCollisions(comparisonList, rect); }; // Helper functions /** * setQuadrant sets the overlapping quadrants (quadtrees) given an object * @param {Object} object A rectangle that is inserted in the quadtree * @param {Object} quadtree A quadtree */ function setQuadrant (object, quadtree) { if (quadtree.isLeaf) { if (quadtree.parent) { object.quadrant = (isIntersecting(quadtree.parent.children[0], object) * NORTH_WEST) + (isIntersecting(quadtree.parent.children[1], object) * NORTH_EAST) + (isIntersecting(quadtree.parent.children[2], object) * SOUTH_WEST) + (isIntersecting(quadtree.parent.children[3], object) * SOUTH_EAST); } else { object.quadrant = 15; } } else { object.quadrant = (isIntersecting(quadtree.children[0], object) * NORTH_WEST) + (isIntersecting(quadtree.children[1], object) * NORTH_EAST) + (isIntersecting(quadtree.children[2], object) * SOUTH_WEST) + (isIntersecting(quadtree.children[3], object) * SOUTH_EAST); } } /** * [hasRectProps determines if the object has the necessary properties to be considered a rectangle] * @param {Object} object [The object questioned for rect props] * @return {Boolean} [True if it is a rectangle] */ function hasRectProps (object) { return typeof object.width !== 'undefined' &amp;&amp; object.height !== 'undefined' &amp;&amp; object.x !== 'undefined' &amp;&amp; object.y !== 'undefined'; } /** * [getBounds returns the bounds of a rectangle] * @param {Object} r [x, y, width, height] * @return {Object} [left, right, top, bottom] */ function getBounds (r) { return { 'left' : r.x - r.width / 2, 'right' : r.x + r.width / 2, 'top' : r.y - r.height / 2, 'bottom': r.y + r.height / 2 }; } /** * [isWithinBounds retuns true if rect2 is completely within rect1] * @param {Object} r1 [x, y, width, height] * @param {Object} r2 [x, y, width, height] * @return {Boolean} [true if rect2 is completely within rect1] */ function isWithinBounds (r1, r2) { var r1Bounds = getBounds(r1), r2Bounds = getBounds(r2); return (r2Bounds.left >= r1Bounds.left &amp;&amp; r2Bounds.right &lt;= r1Bounds.right &amp;&amp; r2Bounds.top >= r1Bounds.top &amp;&amp; r2Bounds.bottom &lt;= r1Bounds.bottom); } /** * [isIntersecting returns true if two rectangles intersect] * @param {Object} r1 [rectangle] * @param {Object} r2 [rectangle] * @return {Boolean} [True if two rectangles isIntersecting] * @diagram * * * * * * * * * * r2 * * * * * * * * * * * * * * * * * r1 * * * * * * * * * * * * * * * * r2 * * * * * * * * * * * * * * * * r1 * * * * * * * * * */ function isIntersecting (r1, r2) { if (r1.radius &amp;&amp; r2.radius) { return isIntersectingCircles(r1, r2); } else { return isIntersectingSquares(r1, r2); } } function isIntersectingSquares (r1, r2) { var r1Bounds = getBounds(r1), r2Bounds = getBounds(r2); return (r1Bounds.left &lt; r2Bounds.right &amp;&amp; r1Bounds.right > r2Bounds.left &amp;&amp; r1Bounds.top &lt; r2Bounds.bottom &amp;&amp; r1Bounds.bottom > r2Bounds.top); } function isIntersectingCircles (c1, c2) { var dx = c1.x - c2.x, dy = c1.y - c2.y, distance = Math.sqrt(dx * dx + dy * dy); return distance &lt; c1.radius + c2.radius; } function getCollisions (comparisonList, rect) { var collisionList = []; for (var i = 0; i &lt; comparisonList.length; i++) { if (isIntersecting(comparisonList[i], rect)) { collisionList.push(comparisonList[i]); } } return collisionList; } /** * [forceObjectWithinBounds forces the inserted object into the quadtree bounds. * This makes the quadtree behave like pac-man when he goes into the opening on * the side of the map] * @param {Object} object [This is the parent-most quadtree] * @param {Object} rect [The inserted rectangle] */ function forceObjectWithinBounds (object, rect) { var objectBounds = getBounds(object), containerBounds = getBounds(rect), isTooFarLeft = objectBounds.left &lt; containerBounds.left, isTooFarRight = objectBounds.left > containerBounds.right, isTooFarAbove = objectBounds.top &lt; containerBounds.top, isTooFarBelow = objectBounds.top > containerBounds.bottom; if (isTooFarLeft) { while (object.x &lt; containerBounds.left) { object.x = containerBounds.right + object.x + rect.halfWidth; } } if (isTooFarRight) { while (object.x > containerBounds.right) { object.x = containerBounds.left + object.x - rect.halfWidth; } } if (isTooFarAbove) { while (object.y &lt; containerBounds.top) { object.y = containerBounds.bottom + object.y + rect.halfHeight; } } if (isTooFarBelow) { while (object.y > containerBounds.bottom) { object.y = containerBounds.top + object.y - rect.halfHeight; } } } module.exports = Quadtree;</code></pre> </article> </section> </div> <nav> <h2><a href="index.html">Home</a></h2><h3>Classes</h3><ul><li><a href="Quadtree.html">Quadtree</a></li></ul><h3>Global</h3><ul><li><a href="global.html#forceObjectWithinBounds">forceObjectWithinBounds</a></li><li><a href="global.html#getBounds">getBounds</a></li><li><a href="global.html#hasRectProps">hasRectProps</a></li><li><a href="global.html#isIntersecting">isIntersecting</a></li><li><a href="global.html#isWithinBounds">isWithinBounds</a></li><li><a href="global.html#rectPrototype">rectPrototype</a></li><li><a href="global.html#remove">remove</a></li><li><a href="global.html#setQuadrant">setQuadrant</a></li></ul> </nav> <br class="clear"> <footer> Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 3.3.2</a> on Tue Aug 04 2015 14:09:02 GMT-0400 (Eastern Daylight Time) </footer> <script> prettyPrint(); </script> <script src="scripts/linenumber.js"> </script> </body> </html>