From 3d45f9eaa09abae6d8ae4b3fd6d8e69696ce1225 Mon Sep 17 00:00:00 2001 From: Jim Riecken Date: Sat, 17 Feb 2018 10:59:52 -0800 Subject: [PATCH] Add getCentroid method to Polygon. Fixes #50 --- SAT.js | 149 +++++++++++++++++++++++++++++---------------------- package.json | 4 +- test/test.js | 58 +++++++++++++++----- 3 files changed, 132 insertions(+), 79 deletions(-) diff --git a/SAT.js b/SAT.js index 164b3e9..f7a9a84 100644 --- a/SAT.js +++ b/SAT.js @@ -1,14 +1,14 @@ -// Version 0.6.0 - Copyright 2012 - 2016 - Jim Riecken +// Version 0.6.0 - Copyright 2012 - 2018 - Jim Riecken // // 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.6.0 - Copyright 2012 - 2016 - Jim Riecken - released under the MIT License. https://github.com/jriecken/sat-js */ +/** @preserve SAT.js - Version 0.6.0 - Copyright 2012 - 2018 - Jim Riecken - 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, +/*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: @@ -45,7 +45,7 @@ // 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 @@ -112,7 +112,7 @@ this['y'] = -this['y']; return this; }; - + // Normalize this vector. (make it have length of `1`) /** @@ -126,7 +126,7 @@ } return this; }; - + // Add another vector to this one. /** * @param {Vector} other The other Vector. @@ -137,7 +137,7 @@ this['y'] += other['y']; return this; }; - + // Subtract another vector from this one. /** * @param {Vector} other The other Vector. @@ -148,7 +148,7 @@ this['y'] -= other['y']; return this; }; - + // Scale this vector. An independant scaling factor can be provided // for each axis, or a single scaling factor that will scale both `x` and `y`. /** @@ -160,9 +160,9 @@ Vector.prototype['scale'] = Vector.prototype.scale = function(x,y) { this['x'] *= x; this['y'] *= y || x; - return this; + return this; }; - + // Project this vector on to another vector. /** * @param {Vector} other The vector to project onto. @@ -174,7 +174,7 @@ 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. /** @@ -187,7 +187,7 @@ this['y'] = amt * other['y']; return this; }; - + // Reflect this vector on an arbitrary axis. /** * @param {Vector} axis The vector representing the axis. @@ -201,7 +201,7 @@ 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. /** @@ -216,7 +216,7 @@ this['y'] -= y; return this; }; - + // Get the dot product of this vector and another. /** * @param {Vector} other The vector to dot this one against. @@ -225,7 +225,7 @@ 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. @@ -233,7 +233,7 @@ Vector.prototype['len2'] = Vector.prototype.len2 = function() { return this.dot(this); }; - + // Get the length of this vector. /** * @return {number} The length of this vector. @@ -241,7 +241,7 @@ Vector.prototype['len'] = Vector.prototype.len = function() { return Math.sqrt(this.len2()); }; - + // ## Circle // // Represents a circle with a position and a radius. @@ -259,7 +259,7 @@ this['r'] = r || 0; } SAT['Circle'] = Circle; - + // Compute the axis-aligned bounding box (AABB) of this Circle. // // Note: Returns a _new_ `Polygon` each time you call this. @@ -298,7 +298,7 @@ this.setPoints(points || []); } SAT['Polygon'] = Polygon; - + // Set the points of the polygon. // // Note: The points are counter-clockwise *with respect to the coordinate system*. @@ -385,8 +385,8 @@ var points = this['points']; var len = points.length; for (var i = 0; i < len; i++) { - points[i].x += x; - points[i].y += y; + points[i]["x"] += x; + points[i]["y"] += y; } this._recalc(); return this; @@ -418,8 +418,8 @@ var i; for (i = 0; i < len; i++) { var calcPoint = calcPoints[i].copy(points[i]); - calcPoint.x += offset.x; - calcPoint.y += offset.y; + calcPoint["x"] += offset["x"]; + calcPoint["y"] += offset["y"]; if (angle !== 0) { calcPoint.rotate(angle); } @@ -433,8 +433,8 @@ } return this; }; - - + + // Compute the axis-aligned bounding box. Any current state // (translations/rotations) will be applied before constructing the AABB. // @@ -466,7 +466,30 @@ } return new Box(this["pos"].clone().add(new Vector(xMin, yMin)), xMax - xMin, yMax - yMin).toPolygon(); }; - + + // Compute the centroid (geometric center) of the polygon + // + // See https://en.wikipedia.org/wiki/Centroid#Centroid_of_a_polygon + 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 hae 2*area + cx = cx / ar; + cy = cy / ar; + return new Vector(cx, cy); + }; + // ## Box // @@ -498,11 +521,11 @@ 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(), new Vector(w, 0), new Vector(w,h), new Vector(0,h) ]); }; - + // ## Response // // An object representing the result of an intersection. Contains: @@ -513,7 +536,7 @@ // - Whether the first object is entirely inside the second, and vice versa. /** * @constructor - */ + */ function Response() { this['a'] = null; this['b'] = null; @@ -545,7 +568,7 @@ */ 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. /** @@ -590,7 +613,7 @@ } result[0] = min; result[1] = max; } - + // Check whether two convex polygons are separated by the specified // axis (must be a unit vector). /** @@ -620,8 +643,8 @@ 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_VECTORS.push(offsetV); + T_ARRAYS.push(rangeA); T_ARRAYS.push(rangeB); return true; } @@ -632,7 +655,7 @@ 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]) { + if (rangeA[1] < rangeB[1]) { overlap = rangeA[1] - rangeB[0]; response['bInA'] = false; // B is fully inside A. Pick the shortest way out. @@ -645,7 +668,7 @@ } else { response['bInA'] = false; // B ends before A ends. We have to push A out of B - if (rangeA[1] > rangeB[1]) { + if (rangeA[1] > rangeB[1]) { overlap = rangeA[0] - rangeB[1]; response['aInB'] = false; // A is fully inside B. Pick the shortest way out. @@ -663,15 +686,15 @@ if (overlap < 0) { response['overlapN'].reverse(); } - } + } } - T_VECTORS.push(offsetV); - T_ARRAYS.push(rangeA); + 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)` // @@ -710,7 +733,7 @@ * @const */ var RIGHT_VORONOI_REGION = 1; - + // ## Collision Tests // Check if a point is inside a circle. @@ -752,7 +775,7 @@ * @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. + * @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 @@ -767,7 +790,7 @@ return false; } // They intersect. If we're calculating a response, calculate the overlap. - if (response) { + if (response) { var dist = Math.sqrt(distanceSq); response['a'] = a; response['b'] = b; @@ -781,7 +804,7 @@ return true; } SAT['testCircleCircle'] = testCircleCircle; - + // Check if a polygon and a circle collide. /** * @param {Polygon} polygon The polygon. @@ -799,26 +822,26 @@ 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: @@ -833,9 +856,9 @@ var dist = point.len(); if (dist > radius) { // No intersection - T_VECTORS.push(circlePos); + T_VECTORS.push(circlePos); T_VECTORS.push(edge); - T_VECTORS.push(point); + T_VECTORS.push(point); T_VECTORS.push(point2); return false; } else if (response) { @@ -858,10 +881,10 @@ var dist = point.len(); if (dist > radius) { // No intersection - T_VECTORS.push(circlePos); - T_VECTORS.push(edge); + T_VECTORS.push(circlePos); + T_VECTORS.push(edge); T_VECTORS.push(point); - return false; + return false; } else if (response) { // It intersects, calculate the overlap. response['bInA'] = false; @@ -874,15 +897,15 @@ // 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 + // 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(circlePos); + T_VECTORS.push(normal); T_VECTORS.push(point); return false; } else if (response) { @@ -896,28 +919,28 @@ } } } - - // If this is the smallest overlap we've seen, keep it. + + // 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(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 @@ -946,7 +969,7 @@ return result; } SAT['testCirclePolygon'] = testCirclePolygon; - + // Checks whether polygons collide. /** * @param {Polygon} a The first polygon. diff --git a/package.json b/package.json index 29a30e8..5738ecb 100644 --- a/package.json +++ b/package.json @@ -19,8 +19,8 @@ "bugs": { "url": "http://github.com/jriecken/sat-js/issues" }, - "scripts":{ - "test": "mocha" + "scripts": { + "test": "mocha" }, "main": "SAT.js", "devDependencies": { diff --git a/test/test.js b/test/test.js index e603c78..8f1f969 100644 --- a/test/test.js +++ b/test/test.js @@ -1,8 +1,36 @@ var SAT = require('..'); var assert = require('assert'); -describe("collision of", function(){ - it("two circles", function(){ +describe("Polygon.getCentroid", function() { + it("should calculate the correct value for a square", function() { + var V = SAT.Vector; + var P = SAT.Polygon; + + // A square + var polygon = new P(new V(0,0), [ + new V(0,0), new V(40,0), new V(40,40), new V(0,40) + ]); + var c = polygon.getCentroid(); + assert( c.x === 20 ); + assert( c.y === 20 ); + }); + + it("should calculate the correct value for a triangle", function() { + var V = SAT.Vector; + var P = SAT.Polygon; + + // A triangle + var polygon = new P(new V(0,0), [ + new V(0,0), new V(100,0), new V(50,99) + ]); + var c = polygon.getCentroid(); + assert( c.x === 50 ); + assert( c.y === 33 ); + }); +}); + +describe("Collision", function() { + it("testCircleCircle", function() { var V = SAT.Vector; var C = SAT.Circle; @@ -16,7 +44,7 @@ describe("collision of", function(){ assert( response.overlapV.x == 10 && response.overlapV.y === 0); }); - it("circle and polygon", function(){ + it("testPolygonCircle", function() { var V = SAT.Vector; var C = SAT.Circle; @@ -25,7 +53,7 @@ describe("collision of", function(){ var circle = new C(new V(50,50), 20); // A square var polygon = new P(new V(0,0), [ - new V(0,0), new V(40,0), new V(40,40), new V(0,40) + new V(0,0), new V(40,0), new V(40,40), new V(0,40) ]); var response = new SAT.Response(); var collided = SAT.testPolygonCircle(polygon, circle, response); @@ -38,17 +66,17 @@ describe("collision of", function(){ ); }); - it("polygon and polygon", function(){ + it("testPolygonPolygon", function() { var V = SAT.Vector; var P = SAT.Polygon; // A square var polygon1 = new P(new V(0,0), [ - new V(0,0), new V(40,0), new V(40,40), new V(0,40) + new V(0,0), new V(40,0), new V(40,40), new V(0,40) ]); // A triangle var polygon2 = new P(new V(30,0), [ - new V(0,0), new V(30, 0), new V(0, 30) + new V(0,0), new V(30, 0), new V(0, 30) ]); var response = new SAT.Response(); var collided = SAT.testPolygonPolygon(polygon1, polygon2, response); @@ -59,8 +87,8 @@ describe("collision of", function(){ }); }); -describe("No collision between", function(){ - it("two boxes", function(){ +describe("No collision", function() { + it("testPolygonPolygon", function(){ var V = SAT.Vector; var B = SAT.Box; @@ -70,8 +98,8 @@ describe("No collision between", function(){ }); }); -describe("Hit testing", function(){ - it("a circle", function(){ +describe("Point testing", function() { + it("pointInCircle", function(){ var V = SAT.Vector; var C = SAT.Circle; @@ -80,18 +108,20 @@ describe("Hit testing", function(){ assert(!SAT.pointInCircle(new V(0,0), circle)); // false assert(SAT.pointInCircle(new V(110,110), circle)); // true }); - it("a polygon", function(){ + + it("pointInPolygon", function() { var V = SAT.Vector; var C = SAT.Circle; var P = SAT.Polygon; var triangle = new P(new V(30,0), [ - new V(0,0), new V(30, 0), new V(0, 30) + new V(0,0), new V(30, 0), new V(0, 30) ]); assert(!SAT.pointInPolygon(new V(0,0), triangle)); // false assert(SAT.pointInPolygon(new V(35, 5), triangle)); // true }); - it("a small polygon", function () { + + it("pointInPolygon (small)", function () { var V = SAT.Vector; var C = SAT.Circle; var P = SAT.Polygon;