UNPKG

check2d

Version:

Polygons, Ellipses, Circles, Boxes, Lines, Points. Ray-Casting, offsets, rotation, scaling, padding, groups.

1,453 lines (1,291 loc) 123 kB
var SAT$1 = {exports: {}}; var SAT = SAT$1.exports; var hasRequiredSAT; function requireSAT () { if (hasRequiredSAT) return SAT$1.exports; hasRequiredSAT = 1; (function (module, exports) { // Version 0.9.0 - Copyright 2012 - 2021 - Jim Riecken <jimr@jimr.ca> // // Released under the MIT License - https://github.com/jriecken/sat-js // // A simple library for determining intersections of circles and // polygons using the Separating Axis Theorem. /** @preserve SAT.js - Version 0.9.0 - Copyright 2012 - 2021 - Jim Riecken <jimr@jimr.ca> - released under the MIT License. https://github.com/jriecken/sat-js */ /*global define: false, module: false*/ /*jshint shadow:true, sub:true, forin:true, noarg:true, noempty:true, eqeqeq:true, bitwise:true, strict:true, undef:true, curly:true, browser:true */ // Create a UMD wrapper for SAT. Works in: // // - Plain browser via global SAT variable // - AMD loader (like require.js) // - Node.js // // The quoted properties all over the place are used so that the Closure Compiler // does not mangle the exposed API in advanced mode. /** * @param {*} root - The global scope * @param {Function} factory - Factory that creates SAT module */ (function (root, factory) { { module['exports'] = factory(); } }(SAT, function () { var SAT = {}; // // ## Vector // // Represents a vector in two dimensions with `x` and `y` properties. // Create a new Vector, optionally passing in the `x` and `y` coordinates. If // a coordinate is not specified, it will be set to `0` /** * @param {?number=} x The x position. * @param {?number=} y The y position. * @constructor */ function Vector(x, y) { this['x'] = x || 0; this['y'] = y || 0; } SAT['Vector'] = Vector; // Alias `Vector` as `V` SAT['V'] = Vector; // Copy the values of another Vector into this one. /** * @param {Vector} other The other Vector. * @return {Vector} This for chaining. */ Vector.prototype['copy'] = Vector.prototype.copy = function (other) { this['x'] = other['x']; this['y'] = other['y']; return this; }; // Create a new vector with the same coordinates as this on. /** * @return {Vector} The new cloned vector */ Vector.prototype['clone'] = Vector.prototype.clone = function () { return new Vector(this['x'], this['y']); }; // Change this vector to be perpendicular to what it was before. (Effectively // roatates it 90 degrees in a clockwise direction) /** * @return {Vector} This for chaining. */ Vector.prototype['perp'] = Vector.prototype.perp = function () { var x = this['x']; this['x'] = this['y']; this['y'] = -x; return this; }; // Rotate this vector (counter-clockwise) by the specified angle (in radians). /** * @param {number} angle The angle to rotate (in radians) * @return {Vector} This for chaining. */ Vector.prototype['rotate'] = Vector.prototype.rotate = function (angle) { var x = this['x']; var y = this['y']; this['x'] = x * Math.cos(angle) - y * Math.sin(angle); this['y'] = x * Math.sin(angle) + y * Math.cos(angle); return this; }; // Reverse this vector. /** * @return {Vector} This for chaining. */ Vector.prototype['reverse'] = Vector.prototype.reverse = function () { this['x'] = -this['x']; this['y'] = -this['y']; return this; }; // Normalize this vector. (make it have length of `1`) /** * @return {Vector} This for chaining. */ Vector.prototype['normalize'] = Vector.prototype.normalize = function () { var d = this.len(); if (d > 0) { this['x'] = this['x'] / d; this['y'] = this['y'] / d; } return this; }; // Add another vector to this one. /** * @param {Vector} other The other Vector. * @return {Vector} This for chaining. */ Vector.prototype['add'] = Vector.prototype.add = function (other) { this['x'] += other['x']; this['y'] += other['y']; return this; }; // Subtract another vector from this one. /** * @param {Vector} other The other Vector. * @return {Vector} This for chaiing. */ Vector.prototype['sub'] = Vector.prototype.sub = function (other) { this['x'] -= other['x']; this['y'] -= other['y']; return this; }; // Scale this vector. An independent scaling factor can be provided // for each axis, or a single scaling factor that will scale both `x` and `y`. /** * @param {number} x The scaling factor in the x direction. * @param {?number=} y The scaling factor in the y direction. If this * is not specified, the x scaling factor will be used. * @return {Vector} This for chaining. */ Vector.prototype['scale'] = Vector.prototype.scale = function (x, y) { this['x'] *= x; this['y'] *= typeof y != 'undefined' ? y : x; return this; }; // Project this vector on to another vector. /** * @param {Vector} other The vector to project onto. * @return {Vector} This for chaining. */ Vector.prototype['project'] = Vector.prototype.project = function (other) { var amt = this.dot(other) / other.len2(); this['x'] = amt * other['x']; this['y'] = amt * other['y']; return this; }; // Project this vector onto a vector of unit length. This is slightly more efficient // than `project` when dealing with unit vectors. /** * @param {Vector} other The unit vector to project onto. * @return {Vector} This for chaining. */ Vector.prototype['projectN'] = Vector.prototype.projectN = function (other) { var amt = this.dot(other); this['x'] = amt * other['x']; this['y'] = amt * other['y']; return this; }; // Reflect this vector on an arbitrary axis. /** * @param {Vector} axis The vector representing the axis. * @return {Vector} This for chaining. */ Vector.prototype['reflect'] = Vector.prototype.reflect = function (axis) { var x = this['x']; var y = this['y']; this.project(axis).scale(2); this['x'] -= x; this['y'] -= y; return this; }; // Reflect this vector on an arbitrary axis (represented by a unit vector). This is // slightly more efficient than `reflect` when dealing with an axis that is a unit vector. /** * @param {Vector} axis The unit vector representing the axis. * @return {Vector} This for chaining. */ Vector.prototype['reflectN'] = Vector.prototype.reflectN = function (axis) { var x = this['x']; var y = this['y']; this.projectN(axis).scale(2); this['x'] -= x; this['y'] -= y; return this; }; // Get the dot product of this vector and another. /** * @param {Vector} other The vector to dot this one against. * @return {number} The dot product. */ Vector.prototype['dot'] = Vector.prototype.dot = function (other) { return this['x'] * other['x'] + this['y'] * other['y']; }; // Get the squared length of this vector. /** * @return {number} The length^2 of this vector. */ Vector.prototype['len2'] = Vector.prototype.len2 = function () { return this.dot(this); }; // Get the length of this vector. /** * @return {number} The length of this vector. */ Vector.prototype['len'] = Vector.prototype.len = function () { return Math.sqrt(this.len2()); }; // ## Circle // // Represents a circle with a position and a radius. // Create a new circle, optionally passing in a position and/or radius. If no position // is given, the circle will be at `(0,0)`. If no radius is provided, the circle will // have a radius of `0`. /** * @param {Vector=} pos A vector representing the position of the center of the circle * @param {?number=} r The radius of the circle * @constructor */ function Circle(pos, r) { this['pos'] = pos || new Vector(); this['r'] = r || 0; this['offset'] = new Vector(); } SAT['Circle'] = Circle; // Compute the axis-aligned bounding box (AABB) of this Circle. // // Note: Returns a _new_ `Box` each time you call this. /** * @return {Polygon} The AABB */ Circle.prototype['getAABBAsBox'] = Circle.prototype.getAABBAsBox = function () { var r = this['r']; var corner = this['pos'].clone().add(this['offset']).sub(new Vector(r, r)); return new Box(corner, r * 2, r * 2); }; // Compute the axis-aligned bounding box (AABB) of this Circle. // // Note: Returns a _new_ `Polygon` each time you call this. /** * @return {Polygon} The AABB */ Circle.prototype['getAABB'] = Circle.prototype.getAABB = function () { return this.getAABBAsBox().toPolygon(); }; // Set the current offset to apply to the radius. /** * @param {Vector} offset The new offset vector. * @return {Circle} This for chaining. */ Circle.prototype['setOffset'] = Circle.prototype.setOffset = function (offset) { this['offset'] = offset; return this; }; // ## Polygon // // Represents a *convex* polygon with any number of points (specified in counter-clockwise order) // // Note: Do _not_ manually change the `points`, `angle`, or `offset` properties. Use the // provided setters. Otherwise the calculated properties will not be updated correctly. // // `pos` can be changed directly. // Create a new polygon, passing in a position vector, and an array of points (represented // by vectors relative to the position vector). If no position is passed in, the position // of the polygon will be `(0,0)`. /** * @param {Vector=} pos A vector representing the origin of the polygon. (all other * points are relative to this one) * @param {Array<Vector>=} points An array of vectors representing the points in the polygon, * in counter-clockwise order. * @constructor */ function Polygon(pos, points) { this['pos'] = pos || new Vector(); this['angle'] = 0; this['offset'] = new Vector(); this.setPoints(points || []); } SAT['Polygon'] = Polygon; // Set the points of the polygon. Any consecutive duplicate points will be combined. // // Note: The points are counter-clockwise *with respect to the coordinate system*. // If you directly draw the points on a screen that has the origin at the top-left corner // it will _appear_ visually that the points are being specified clockwise. This is just // because of the inversion of the Y-axis when being displayed. /** * @param {Array<Vector>=} points An array of vectors representing the points in the polygon, * in counter-clockwise order. * @return {Polygon} This for chaining. */ Polygon.prototype['setPoints'] = Polygon.prototype.setPoints = function (points) { // Only re-allocate if this is a new polygon or the number of points has changed. var lengthChanged = !this['points'] || this['points'].length !== points.length; if (lengthChanged) { var i; var calcPoints = this['calcPoints'] = []; var edges = this['edges'] = []; var normals = this['normals'] = []; // Allocate the vector arrays for the calculated properties for (i = 0; i < points.length; i++) { // Remove consecutive duplicate points var p1 = points[i]; var p2 = i < points.length - 1 ? points[i + 1] : points[0]; if (p1 !== p2 && p1.x === p2.x && p1.y === p2.y) { points.splice(i, 1); i -= 1; continue; } calcPoints.push(new Vector()); edges.push(new Vector()); normals.push(new Vector()); } } this['points'] = points; this._recalc(); return this; }; // Set the current rotation angle of the polygon. /** * @param {number} angle The current rotation angle (in radians). * @return {Polygon} This for chaining. */ Polygon.prototype['setAngle'] = Polygon.prototype.setAngle = function (angle) { this['angle'] = angle; this._recalc(); return this; }; // Set the current offset to apply to the `points` before applying the `angle` rotation. /** * @param {Vector} offset The new offset vector. * @return {Polygon} This for chaining. */ Polygon.prototype['setOffset'] = Polygon.prototype.setOffset = function (offset) { this['offset'] = offset; this._recalc(); return this; }; // Rotates this polygon counter-clockwise around the origin of *its local coordinate system* (i.e. `pos`). // // Note: This changes the **original** points (so any `angle` will be applied on top of this rotation). /** * @param {number} angle The angle to rotate (in radians) * @return {Polygon} This for chaining. */ Polygon.prototype['rotate'] = Polygon.prototype.rotate = function (angle) { var points = this['points']; var len = points.length; for (var i = 0; i < len; i++) { points[i].rotate(angle); } this._recalc(); return this; }; // Translates the points of this polygon by a specified amount relative to the origin of *its own coordinate // system* (i.e. `pos`). // // This is most useful to change the "center point" of a polygon. If you just want to move the whole polygon, change // the coordinates of `pos`. // // Note: This changes the **original** points (so any `offset` will be applied on top of this translation) /** * @param {number} x The horizontal amount to translate. * @param {number} y The vertical amount to translate. * @return {Polygon} This for chaining. */ Polygon.prototype['translate'] = Polygon.prototype.translate = function (x, y) { var points = this['points']; var len = points.length; for (var i = 0; i < len; i++) { points[i]['x'] += x; points[i]['y'] += y; } this._recalc(); return this; }; // Computes the calculated collision polygon. Applies the `angle` and `offset` to the original points then recalculates the // edges and normals of the collision polygon. /** * @return {Polygon} This for chaining. */ Polygon.prototype._recalc = function () { // Calculated points - this is what is used for underlying collisions and takes into account // the angle/offset set on the polygon. var calcPoints = this['calcPoints']; // The edges here are the direction of the `n`th edge of the polygon, relative to // the `n`th point. If you want to draw a given edge from the edge value, you must // first translate to the position of the starting point. var edges = this['edges']; // The normals here are the direction of the normal for the `n`th edge of the polygon, relative // to the position of the `n`th point. If you want to draw an edge normal, you must first // translate to the position of the starting point. var normals = this['normals']; // Copy the original points array and apply the offset/angle var points = this['points']; var offset = this['offset']; var angle = this['angle']; var len = points.length; var i; for (i = 0; i < len; i++) { var calcPoint = calcPoints[i].copy(points[i]); calcPoint['x'] += offset['x']; calcPoint['y'] += offset['y']; if (angle !== 0) { calcPoint.rotate(angle); } } // Calculate the edges/normals for (i = 0; i < len; i++) { var p1 = calcPoints[i]; var p2 = i < len - 1 ? calcPoints[i + 1] : calcPoints[0]; var e = edges[i].copy(p2).sub(p1); normals[i].copy(e).perp().normalize(); } return this; }; // Compute the axis-aligned bounding box. Any current state // (translations/rotations) will be applied before constructing the AABB. // // Note: Returns a _new_ `Box` each time you call this. /** * @return {Polygon} The AABB */ Polygon.prototype['getAABBAsBox'] = Polygon.prototype.getAABBAsBox = function () { var points = this['calcPoints']; var len = points.length; var xMin = points[0]['x']; var yMin = points[0]['y']; var xMax = points[0]['x']; var yMax = points[0]['y']; for (var i = 1; i < len; i++) { var point = points[i]; if (point['x'] < xMin) { xMin = point['x']; } else if (point['x'] > xMax) { xMax = point['x']; } if (point['y'] < yMin) { yMin = point['y']; } else if (point['y'] > yMax) { yMax = point['y']; } } return new Box(this['pos'].clone().add(new Vector(xMin, yMin)), xMax - xMin, yMax - yMin); }; // Compute the axis-aligned bounding box. Any current state // (translations/rotations) will be applied before constructing the AABB. // // Note: Returns a _new_ `Polygon` each time you call this. /** * @return {Polygon} The AABB */ Polygon.prototype['getAABB'] = Polygon.prototype.getAABB = function () { return this.getAABBAsBox().toPolygon(); }; // Compute the centroid (geometric center) of the polygon. Any current state // (translations/rotations) will be applied before computing the centroid. // // See https://en.wikipedia.org/wiki/Centroid#Centroid_of_a_polygon // // Note: Returns a _new_ `Vector` each time you call this. /** * @return {Vector} A Vector that contains the coordinates of the Centroid. */ Polygon.prototype['getCentroid'] = Polygon.prototype.getCentroid = function () { var points = this['calcPoints']; var len = points.length; var cx = 0; var cy = 0; var ar = 0; for (var i = 0; i < len; i++) { var p1 = points[i]; var p2 = i === len - 1 ? points[0] : points[i + 1]; // Loop around if last point var a = p1['x'] * p2['y'] - p2['x'] * p1['y']; cx += (p1['x'] + p2['x']) * a; cy += (p1['y'] + p2['y']) * a; ar += a; } ar = ar * 3; // we want 1 / 6 the area and we currently have 2*area cx = cx / ar; cy = cy / ar; return new Vector(cx, cy); }; // ## Box // // Represents an axis-aligned box, with a width and height. // Create a new box, with the specified position, width, and height. If no position // is given, the position will be `(0,0)`. If no width or height are given, they will // be set to `0`. /** * @param {Vector=} pos A vector representing the bottom-left of the box (i.e. the smallest x and smallest y value). * @param {?number=} w The width of the box. * @param {?number=} h The height of the box. * @constructor */ function Box(pos, w, h) { this['pos'] = pos || new Vector(); this['w'] = w || 0; this['h'] = h || 0; } SAT['Box'] = Box; // Returns a polygon whose edges are the same as this box. /** * @return {Polygon} A new Polygon that represents this box. */ Box.prototype['toPolygon'] = Box.prototype.toPolygon = function () { var pos = this['pos']; var w = this['w']; var h = this['h']; return new Polygon(new Vector(pos['x'], pos['y']), [ new Vector(), new Vector(w, 0), new Vector(w, h), new Vector(0, h) ]); }; // ## Response // // An object representing the result of an intersection. Contains: // - The two objects participating in the intersection // - The vector representing the minimum change necessary to extract the first object // from the second one (as well as a unit vector in that direction and the magnitude // of the overlap) // - Whether the first object is entirely inside the second, and vice versa. /** * @constructor */ function Response() { this['a'] = null; this['b'] = null; this['overlapN'] = new Vector(); this['overlapV'] = new Vector(); this.clear(); } SAT['Response'] = Response; // Set some values of the response back to their defaults. Call this between tests if // you are going to reuse a single Response object for multiple intersection tests (recommented // as it will avoid allcating extra memory) /** * @return {Response} This for chaining */ Response.prototype['clear'] = Response.prototype.clear = function () { this['aInB'] = true; this['bInA'] = true; this['overlap'] = Number.MAX_VALUE; return this; }; // ## Object Pools // A pool of `Vector` objects that are used in calculations to avoid // allocating memory. /** * @type {Array<Vector>} */ var T_VECTORS = []; for (var i = 0; i < 10; i++) { T_VECTORS.push(new Vector()); } // A pool of arrays of numbers used in calculations to avoid allocating // memory. /** * @type {Array<Array<number>>} */ var T_ARRAYS = []; for (var i = 0; i < 5; i++) { T_ARRAYS.push([]); } // Temporary response used for polygon hit detection. /** * @type {Response} */ var T_RESPONSE = new Response(); // Tiny "point" polygon used for polygon hit detection. /** * @type {Polygon} */ var TEST_POINT = new Box(new Vector(), 0.000001, 0.000001).toPolygon(); // ## Helper Functions // Flattens the specified array of points onto a unit vector axis, // resulting in a one dimensional range of the minimum and // maximum value on that axis. /** * @param {Array<Vector>} points The points to flatten. * @param {Vector} normal The unit vector axis to flatten on. * @param {Array<number>} result An array. After calling this function, * result[0] will be the minimum value, * result[1] will be the maximum value. */ function flattenPointsOn(points, normal, result) { var min = Number.MAX_VALUE; var max = -Number.MAX_VALUE; var len = points.length; for (var i = 0; i < len; i++) { // The magnitude of the projection of the point onto the normal var dot = points[i].dot(normal); if (dot < min) { min = dot; } if (dot > max) { max = dot; } } result[0] = min; result[1] = max; } // Check whether two convex polygons are separated by the specified // axis (must be a unit vector). /** * @param {Vector} aPos The position of the first polygon. * @param {Vector} bPos The position of the second polygon. * @param {Array<Vector>} aPoints The points in the first polygon. * @param {Array<Vector>} bPoints The points in the second polygon. * @param {Vector} axis The axis (unit sized) to test against. The points of both polygons * will be projected onto this axis. * @param {Response=} response A Response object (optional) which will be populated * if the axis is not a separating axis. * @return {boolean} true if it is a separating axis, false otherwise. If false, * and a response is passed in, information about how much overlap and * the direction of the overlap will be populated. */ function isSeparatingAxis(aPos, bPos, aPoints, bPoints, axis, response) { var rangeA = T_ARRAYS.pop(); var rangeB = T_ARRAYS.pop(); // The magnitude of the offset between the two polygons var offsetV = T_VECTORS.pop().copy(bPos).sub(aPos); var projectedOffset = offsetV.dot(axis); // Project the polygons onto the axis. flattenPointsOn(aPoints, axis, rangeA); flattenPointsOn(bPoints, axis, rangeB); // Move B's range to its position relative to A. rangeB[0] += projectedOffset; rangeB[1] += projectedOffset; // Check if there is a gap. If there is, this is a separating axis and we can stop if (rangeA[0] > rangeB[1] || rangeB[0] > rangeA[1]) { T_VECTORS.push(offsetV); T_ARRAYS.push(rangeA); T_ARRAYS.push(rangeB); return true; } // This is not a separating axis. If we're calculating a response, calculate the overlap. if (response) { var overlap = 0; // A starts further left than B if (rangeA[0] < rangeB[0]) { response['aInB'] = false; // A ends before B does. We have to pull A out of B if (rangeA[1] < rangeB[1]) { overlap = rangeA[1] - rangeB[0]; response['bInA'] = false; // B is fully inside A. Pick the shortest way out. } else { var option1 = rangeA[1] - rangeB[0]; var option2 = rangeB[1] - rangeA[0]; overlap = option1 < option2 ? option1 : -option2; } // B starts further left than A } else { response['bInA'] = false; // B ends before A ends. We have to push A out of B if (rangeA[1] > rangeB[1]) { overlap = rangeA[0] - rangeB[1]; response['aInB'] = false; // A is fully inside B. Pick the shortest way out. } else { var option1 = rangeA[1] - rangeB[0]; var option2 = rangeB[1] - rangeA[0]; overlap = option1 < option2 ? option1 : -option2; } } // If this is the smallest amount of overlap we've seen so far, set it as the minimum overlap. var absOverlap = Math.abs(overlap); if (absOverlap < response['overlap']) { response['overlap'] = absOverlap; response['overlapN'].copy(axis); if (overlap < 0) { response['overlapN'].reverse(); } } } T_VECTORS.push(offsetV); T_ARRAYS.push(rangeA); T_ARRAYS.push(rangeB); return false; } SAT['isSeparatingAxis'] = isSeparatingAxis; // Calculates which Voronoi region a point is on a line segment. // It is assumed that both the line and the point are relative to `(0,0)` // // | (0) | // (-1) [S]--------------[E] (1) // | (0) | /** * @param {Vector} line The line segment. * @param {Vector} point The point. * @return {number} LEFT_VORONOI_REGION (-1) if it is the left region, * MIDDLE_VORONOI_REGION (0) if it is the middle region, * RIGHT_VORONOI_REGION (1) if it is the right region. */ function voronoiRegion(line, point) { var len2 = line.len2(); var dp = point.dot(line); // If the point is beyond the start of the line, it is in the // left voronoi region. if (dp < 0) { return LEFT_VORONOI_REGION; } // If the point is beyond the end of the line, it is in the // right voronoi region. else if (dp > len2) { return RIGHT_VORONOI_REGION; } // Otherwise, it's in the middle one. else { return MIDDLE_VORONOI_REGION; } } // Constants for Voronoi regions /** * @const */ var LEFT_VORONOI_REGION = -1; /** * @const */ var MIDDLE_VORONOI_REGION = 0; /** * @const */ var RIGHT_VORONOI_REGION = 1; // ## Collision Tests // Check if a point is inside a circle. /** * @param {Vector} p The point to test. * @param {Circle} c The circle to test. * @return {boolean} true if the point is inside the circle, false if it is not. */ function pointInCircle(p, c) { var differenceV = T_VECTORS.pop().copy(p).sub(c['pos']).sub(c['offset']); var radiusSq = c['r'] * c['r']; var distanceSq = differenceV.len2(); T_VECTORS.push(differenceV); // If the distance between is smaller than the radius then the point is inside the circle. return distanceSq <= radiusSq; } SAT['pointInCircle'] = pointInCircle; // Check if a point is inside a convex polygon. /** * @param {Vector} p The point to test. * @param {Polygon} poly The polygon to test. * @return {boolean} true if the point is inside the polygon, false if it is not. */ function pointInPolygon(p, poly) { TEST_POINT['pos'].copy(p); T_RESPONSE.clear(); var result = testPolygonPolygon(TEST_POINT, poly, T_RESPONSE); if (result) { result = T_RESPONSE['aInB']; } return result; } SAT['pointInPolygon'] = pointInPolygon; // Check if two circles collide. /** * @param {Circle} a The first circle. * @param {Circle} b The second circle. * @param {Response=} response Response object (optional) that will be populated if * the circles intersect. * @return {boolean} true if the circles intersect, false if they don't. */ function testCircleCircle(a, b, response) { // Check if the distance between the centers of the two // circles is greater than their combined radius. var differenceV = T_VECTORS.pop().copy(b['pos']).add(b['offset']).sub(a['pos']).sub(a['offset']); var totalRadius = a['r'] + b['r']; var totalRadiusSq = totalRadius * totalRadius; var distanceSq = differenceV.len2(); // If the distance is bigger than the combined radius, they don't intersect. if (distanceSq > totalRadiusSq) { T_VECTORS.push(differenceV); return false; } // They intersect. If we're calculating a response, calculate the overlap. if (response) { var dist = Math.sqrt(distanceSq); response['a'] = a; response['b'] = b; response['overlap'] = totalRadius - dist; response['overlapN'].copy(differenceV.normalize()); response['overlapV'].copy(differenceV).scale(response['overlap']); response['aInB'] = a['r'] <= b['r'] && dist <= b['r'] - a['r']; response['bInA'] = b['r'] <= a['r'] && dist <= a['r'] - b['r']; } T_VECTORS.push(differenceV); return true; } SAT['testCircleCircle'] = testCircleCircle; // Check if a polygon and a circle collide. /** * @param {Polygon} polygon The polygon. * @param {Circle} circle The circle. * @param {Response=} response Response object (optional) that will be populated if * they interset. * @return {boolean} true if they intersect, false if they don't. */ function testPolygonCircle(polygon, circle, response) { // Get the position of the circle relative to the polygon. var circlePos = T_VECTORS.pop().copy(circle['pos']).add(circle['offset']).sub(polygon['pos']); var radius = circle['r']; var radius2 = radius * radius; var points = polygon['calcPoints']; var len = points.length; var edge = T_VECTORS.pop(); var point = T_VECTORS.pop(); // For each edge in the polygon: for (var i = 0; i < len; i++) { var next = i === len - 1 ? 0 : i + 1; var prev = i === 0 ? len - 1 : i - 1; var overlap = 0; var overlapN = null; // Get the edge. edge.copy(polygon['edges'][i]); // Calculate the center of the circle relative to the starting point of the edge. point.copy(circlePos).sub(points[i]); // If the distance between the center of the circle and the point // is bigger than the radius, the polygon is definitely not fully in // the circle. if (response && point.len2() > radius2) { response['aInB'] = false; } // Calculate which Voronoi region the center of the circle is in. var region = voronoiRegion(edge, point); // If it's the left region: if (region === LEFT_VORONOI_REGION) { // We need to make sure we're in the RIGHT_VORONOI_REGION of the previous edge. edge.copy(polygon['edges'][prev]); // Calculate the center of the circle relative the starting point of the previous edge var point2 = T_VECTORS.pop().copy(circlePos).sub(points[prev]); region = voronoiRegion(edge, point2); if (region === RIGHT_VORONOI_REGION) { // It's in the region we want. Check if the circle intersects the point. var dist = point.len(); if (dist > radius) { // No intersection T_VECTORS.push(circlePos); T_VECTORS.push(edge); T_VECTORS.push(point); T_VECTORS.push(point2); return false; } else if (response) { // It intersects, calculate the overlap. response['bInA'] = false; overlapN = point.normalize(); overlap = radius - dist; } } T_VECTORS.push(point2); // If it's the right region: } else if (region === RIGHT_VORONOI_REGION) { // We need to make sure we're in the left region on the next edge edge.copy(polygon['edges'][next]); // Calculate the center of the circle relative to the starting point of the next edge. point.copy(circlePos).sub(points[next]); region = voronoiRegion(edge, point); if (region === LEFT_VORONOI_REGION) { // It's in the region we want. Check if the circle intersects the point. var dist = point.len(); if (dist > radius) { // No intersection T_VECTORS.push(circlePos); T_VECTORS.push(edge); T_VECTORS.push(point); return false; } else if (response) { // It intersects, calculate the overlap. response['bInA'] = false; overlapN = point.normalize(); overlap = radius - dist; } } // Otherwise, it's the middle region: } else { // Need to check if the circle is intersecting the edge, // Change the edge into its "edge normal". var normal = edge.perp().normalize(); // Find the perpendicular distance between the center of the // circle and the edge. var dist = point.dot(normal); var distAbs = Math.abs(dist); // If the circle is on the outside of the edge, there is no intersection. if (dist > 0 && distAbs > radius) { // No intersection T_VECTORS.push(circlePos); T_VECTORS.push(normal); T_VECTORS.push(point); return false; } else if (response) { // It intersects, calculate the overlap. overlapN = normal; overlap = radius - dist; // If the center of the circle is on the outside of the edge, or part of the // circle is on the outside, the circle is not fully inside the polygon. if (dist >= 0 || overlap < 2 * radius) { response['bInA'] = false; } } } // If this is the smallest overlap we've seen, keep it. // (overlapN may be null if the circle was in the wrong Voronoi region). if (overlapN && response && Math.abs(overlap) < Math.abs(response['overlap'])) { response['overlap'] = overlap; response['overlapN'].copy(overlapN); } } // Calculate the final overlap vector - based on the smallest overlap. if (response) { response['a'] = polygon; response['b'] = circle; response['overlapV'].copy(response['overlapN']).scale(response['overlap']); } T_VECTORS.push(circlePos); T_VECTORS.push(edge); T_VECTORS.push(point); return true; } SAT['testPolygonCircle'] = testPolygonCircle; // Check if a circle and a polygon collide. // // **NOTE:** This is slightly less efficient than polygonCircle as it just // runs polygonCircle and reverses everything at the end. /** * @param {Circle} circle The circle. * @param {Polygon} polygon The polygon. * @param {Response=} response Response object (optional) that will be populated if * they interset. * @return {boolean} true if they intersect, false if they don't. */ function testCirclePolygon(circle, polygon, response) { // Test the polygon against the circle. var result = testPolygonCircle(polygon, circle, response); if (result && response) { // Swap A and B in the response. var a = response['a']; var aInB = response['aInB']; response['overlapN'].reverse(); response['overlapV'].reverse(); response['a'] = response['b']; response['b'] = a; response['aInB'] = response['bInA']; response['bInA'] = aInB; } return result; } SAT['testCirclePolygon'] = testCirclePolygon; // Checks whether polygons collide. /** * @param {Polygon} a The first polygon. * @param {Polygon} b The second polygon. * @param {Response=} response Response object (optional) that will be populated if * they interset. * @return {boolean} true if they intersect, false if they don't. */ function testPolygonPolygon(a, b, response) { var aPoints = a['calcPoints']; var aLen = aPoints.length; var bPoints = b['calcPoints']; var bLen = bPoints.length; // If any of the edge normals of A is a separating axis, no intersection. for (var i = 0; i < aLen; i++) { if (isSeparatingAxis(a['pos'], b['pos'], aPoints, bPoints, a['normals'][i], response)) { return false; } } // If any of the edge normals of B is a separating axis, no intersection. for (var i = 0; i < bLen; i++) { if (isSeparatingAxis(a['pos'], b['pos'], aPoints, bPoints, b['normals'][i], response)) { return false; } } // Since none of the edge normals of A or B are a separating axis, there is an intersection // and we've already calculated the smallest overlap (in isSeparatingAxis). Calculate the // final overlap vector. if (response) { response['a'] = a; response['b'] = b; response['overlapV'].copy(response['overlapN']).scale(response['overlap']); } return true; } SAT['testPolygonPolygon'] = testPolygonPolygon; return SAT; })); } (SAT$1)); return SAT$1.exports; } var SATExports = requireSAT(); /** * Rearranges items so that all items in the [left, k] are the smallest. * The k-th element will have the (k - left + 1)-th smallest value in [left, right]. * * @template T * @param {T[]} arr the array to partially sort (in place) * @param {number} k middle index for partial sorting (as defined above) * @param {number} [left=0] left index of the range to sort * @param {number} [right=arr.length-1] right index * @param {(a: T, b: T) => number} [compare = (a, b) => a - b] compare function */ function quickselect(arr, k, left = 0, right = arr.length - 1, compare = defaultCompare) { while (right > left) { if (right - left > 600) { const n = right - left + 1; const m = k - left + 1; const z = Math.log(n); const s = 0.5 * Math.exp(2 * z / 3); const sd = 0.5 * Math.sqrt(z * s * (n - s) / n) * (m - n / 2 < 0 ? -1 : 1); const newLeft = Math.max(left, Math.floor(k - m * s / n + sd)); const newRight = Math.min(right, Math.floor(k + (n - m) * s / n + sd)); quickselect(arr, k, newLeft, newRight, compare); } const t = arr[k]; let i = left; /** @type {number} */ let j = right; swap(arr, left, k); if (compare(arr[right], t) > 0) swap(arr, left, right); while (i < j) { swap(arr, i, j); i++; j--; while (compare(arr[i], t) < 0) i++; while (compare(arr[j], t) > 0) j--; } if (compare(arr[left], t) === 0) swap(arr, left, j); else { j++; swap(arr, j, right); } if (j <= k) left = j + 1; if (k <= j) right = j - 1; } } /** * @template T * @param {T[]} arr * @param {number} i * @param {number} j */ function swap(arr, i, j) { const tmp = arr[i]; arr[i] = arr[j]; arr[j] = tmp; } /** * @template T * @param {T} a * @param {T} b * @returns {number} */ function defaultCompare(a, b) { return a < b ? -1 : a > b ? 1 : 0; } class RBush { constructor(maxEntries = 9) { // max entries in a node is 9 by default; min node fill is 40% for best performance this._maxEntries = Math.max(4, maxEntries); this._minEntries = Math.max(2, Math.ceil(this._maxEntries * 0.4)); this.clear(); } all() { return this._all(this.data, []); } search(bbox) { let node = this.data; const result = []; if (!intersects(bbox, node)) return result; const toBBox = this.toBBox; const nodesToSearch = []; while (node) { for (let i = 0; i < node.children.length; i++) { const child = node.children[i]; const childBBox = node.leaf ? toBBox(child) : child; if (intersects(bbox, childBBox)) { if (node.leaf) result.push(child); else if (contains(bbox, childBBox)) this._all(child, result); else nodesToSearch.push(child); } } node = nodesToSearch.pop(); } return result; } collides(bbox) { let node = this.data; if (!intersects(bbox, node)) return false; const nodesToSearch = []; while (node) { for (let i = 0; i < node.children.length; i++) { const child = node.children[i]; const childBBox = node.leaf ? this.toBBox(child) : child; if (intersects(bbox, childBBox)) { if (node.leaf || contains(bbox, childBBox)) return true; nodesToSearch.push(child); } } node = nodesToSearch.pop(); } return false; } load(data) { if (!(data && data.length)) return this; if (data.length < this._minEntries) { for (let i = 0; i < data.length; i++) { this.insert(data[i]); } return this; } // recursively build the tree with the given data from scratch using OMT algorithm let node = this._build(data.slice(), 0, data.length - 1, 0); if (!this.data.children.length) { // save as is if tree is empty this.data = node; } else if (this.data.height === node.height) { // split root if trees have the same height this._splitRoot(this.data, node); } else { if (this.data.height < node.height) { // swap trees if inserted one is bigger const tmpNode = this.data; this.data = node; node = tmpNode; } // insert the small tree into the large tree at appropriate level this._insert(node, this.data.height - node.height - 1, true); } return this; } insert(item) { if (item) this._insert(item, this.data.height - 1); return this; } clear() { this.data = createNode([]); return this; } remove(item, equalsFn) { if (!item) return this; let node = this.data; const bbox = this.toBBox(item); const path = []; const indexes = []; let i, parent, goingUp; // depth-first iterative tree traversal while (node || path.length) { if (!node) { // go up node = path.pop(); parent = path[path.length - 1]; i = indexes.pop(); goingUp = true; } if (node.leaf) { // check current node const index = findItem(item, node.children, equalsFn); if (index !== -1) { // item found, remove the item and condense tree upwards node.children.splice(index, 1); path.push(node); this._condense(path); return this; } } if (!goingUp && !node.leaf && contains(node, bbox)) { // go down path.push(node); indexes.push(i); i = 0; parent = node; node = node.children[0]; } else if (parent) { // go right i++; node = parent.children[i]; goingUp = false; } else node = null; // nothing found } return this; } toBBox(item) { return item; } compareMinX(a, b) { return a.minX - b.minX; } compareMinY(a, b) { return a.minY - b.minY; } toJSON() { return this.data; } fromJSON(data) { this.data = data; return this; } _all(node, result) { const nodesToSearch = []; while (node) { if (node.leaf) result.push(...node.children); else nodesToSearch.push(...node.children); node = nodesToSearch.pop(); } return result; } _build(items, left, right, height) { const N = right - left + 1; let M = this._maxEntries; let node; if (N <= M) { // reached leaf level; return leaf node = createNode(items.slice(left, right + 1)); calcBBox(node, this.toBBox); return node; } if (!height) { // target height of the bulk-loaded tree height = Math.ceil(Math.log(N) / Math.log(M)); // target number of root entries to maximize storage utilization M = Math.ceil(N / Math.pow(M, height - 1)); } node = createNode([]); node.leaf = false; node.height = height; // split the items into M mostly square tiles const N2 = Math.ceil(N / M); const N1 = N2 * Math.ceil(Math.sqrt(M)); multiSelect(items, left, right, N1, this.compareMinX); for (let i = left; i <= right; i += N1) { const right2 = Math.min(i + N1 - 1, right); multiSelect(items, i, right2, N2, this.compareMinY); for (let j = i; j <= right2; j += N2) { const right3 = Math.min(j + N2 - 1, right2); // pack each entry recursively node.children.push(this._build(items, j, right3, height - 1)); } } calcBBox(node, this.toBBox); return node; } _chooseSubtree(bbox, node, level, path) { while (true) { path.push(node); if (node.leaf || path.length - 1 === level) break; let minArea = Infinity; let minEnlargement = Infinity; let targetNode; for (let i = 0; i < node.children.length; i++) { const child = node.children[i]; const area = bboxArea(child); const enlargement = enlargedArea(bbox, child) - area; // choose entry with the least area enlargement if (enlargement < minEnlargement) { minEnlargement = enlargement; minArea = area < minArea ? area : minArea; targetNode = child; } else if (enlargement === minEnlargement) { // otherwise choose one with the smallest area if (area < minArea) { minArea = area; targetNode = child; } } } node = targetNode || node.children[0]; } return node; } _insert(item, level, isNode) { const bbox = isNode ? item : this.toBBox(item); const insertPath = []; // find the best node for accommodating the item, saving all nodes along the path too const node = this._chooseSubtree(bbox, this.data, level, insertPath); // put the item into the node node.children.push(item); extend(node, bbox); // split on node overflow; propagate upwards if necessary while (level >= 0) { if (insertPath[level].children.length > this._maxEntries) { this._split(insertPath, level); level--; } else break; } // adjust bboxes along the insertion path this._adjustParentBBoxes(bbox, insertPath, level); } // split overflowed node into two _split(insertPath, level) { const node = insertPath[level]; const M = node.children.length; const m = this._minEntries; this._chooseSplitAxis(node, m, M); const splitIndex = this._chooseSplitIndex(node, m, M); const newNode = createNode(node.children.splice(splitIndex, node.children.length - splitIndex)); newNode.height = node.height; newNode.leaf = node.leaf; calcBBox(node, this.toBBox); calcBBox(newNode, this.toBBox); if (level) insertPath[level - 1].children.push(newNode); else this._splitRoot(node, newNode); } _splitRo