diff --git a/demo/dev.html b/demo/dev.html index c2c43a6..3edd648 100644 --- a/demo/dev.html +++ b/demo/dev.html @@ -6,6 +6,9 @@ + + + @@ -32,6 +35,7 @@ + diff --git a/demo/js/Demo.js b/demo/js/Demo.js index 4469942..3d2e7d6 100644 --- a/demo/js/Demo.js +++ b/demo/js/Demo.js @@ -130,8 +130,6 @@ }); World.add(_world, compound); - - _world.gravity.y = 1; var renderOptions = _engine.render.options; renderOptions.showCollisions = true; @@ -141,6 +139,26 @@ renderOptions.showConvexHulls = true; }; + Demo.concave = function() { + var _world = _engine.world; + + Demo.reset(); + + var vertices = Matter.Vertices.fromPath('354 89,336 118,310 145,302 227,322 271,375 292,490 289,539 271,540 233,549 133,526 100,552 36,601 63,633 122,628 227,594 304,505 340,426 340,327 330,265 294,246 242,246 181,256 133,283 81,346 44'); + //var vertices = Matter.Vertices.fromPath('164 171,232 233,213 302,273 241,342 305,316 231,364 170,309 188,281 117,240 182'); + + var concave = Bodies.fromVertices(200, 200, vertices); + World.add(_world, concave); + + var renderOptions = _engine.render.options; + renderOptions.showCollisions = true; + renderOptions.showBounds = true; + renderOptions.showAxes = true; + renderOptions.showPositions = true; + renderOptions.showConvexHulls = true; + //renderOptions.showVertexNumbers = true; + }; + Demo.slingshot = function() { var _world = _engine.world; diff --git a/demo/js/lib/decomp.js b/demo/js/lib/decomp.js new file mode 100644 index 0000000..f16bc45 --- /dev/null +++ b/demo/js/lib/decomp.js @@ -0,0 +1,670 @@ +!function(e){"object"==typeof exports?module.exports=e():"function"==typeof define&&define.amd?define(e):"undefined"!=typeof window?window.decomp=e():"undefined"!=typeof global?global.decomp=e():"undefined"!=typeof self&&(self.decomp=e())}(function(){var define,module,exports; +return (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);throw new Error("Cannot find module '"+o+"'")}var f=n[o]={exports:{}};t[o][0].call(f.exports,function(e){var n=t[o][1][e];return s(n?n:e)},f,f.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o=0 && s<=1 && t>=0 && t<=1); +}; + + +},{"./Scalar":4}],2:[function(require,module,exports){ +module.exports = Point; + +/** + * Point related functions + * @class Point + */ +function Point(){}; + +/** + * Get the area of a triangle spanned by the three given points. Note that the area will be negative if the points are not given in counter-clockwise order. + * @static + * @method area + * @param {Array} a + * @param {Array} b + * @param {Array} c + * @return {Number} + */ +Point.area = function(a,b,c){ + return (((b[0] - a[0])*(c[1] - a[1]))-((c[0] - a[0])*(b[1] - a[1]))); +}; + +Point.left = function(a,b,c){ + return Point.area(a,b,c) > 0; +}; + +Point.leftOn = function(a,b,c) { + return Point.area(a, b, c) >= 0; +}; + +Point.right = function(a,b,c) { + return Point.area(a, b, c) < 0; +}; + +Point.rightOn = function(a,b,c) { + return Point.area(a, b, c) <= 0; +}; + +var tmpPoint1 = [], + tmpPoint2 = []; + +/** + * Check if three points are collinear + * @method collinear + * @param {Array} a + * @param {Array} b + * @param {Array} c + * @param {Number} [thresholdAngle=0] Threshold angle to use when comparing the vectors. The function will return true if the angle between the resulting vectors is less than this value. Use zero for max precision. + * @return {Boolean} + */ +Point.collinear = function(a,b,c,thresholdAngle) { + if(!thresholdAngle) + return Point.area(a, b, c) == 0; + else { + var ab = tmpPoint1, + bc = tmpPoint2; + + ab[0] = b[0]-a[0]; + ab[1] = b[1]-a[1]; + bc[0] = c[0]-b[0]; + bc[1] = c[1]-b[1]; + + var dot = ab[0]*bc[0] + ab[1]*bc[1], + magA = Math.sqrt(ab[0]*ab[0] + ab[1]*ab[1]), + magB = Math.sqrt(bc[0]*bc[0] + bc[1]*bc[1]), + angle = Math.acos(dot/(magA*magB)); + return angle < thresholdAngle; + } +}; + +Point.sqdist = function(a,b){ + var dx = b[0] - a[0]; + var dy = b[1] - a[1]; + return dx * dx + dy * dy; +}; + +},{}],3:[function(require,module,exports){ +var Line = require("./Line") +, Point = require("./Point") +, Scalar = require("./Scalar") + +module.exports = Polygon; + +/** + * Polygon class. + * @class Polygon + * @constructor + */ +function Polygon(){ + + /** + * Vertices that this polygon consists of. An array of array of numbers, example: [[0,0],[1,0],..] + * @property vertices + * @type {Array} + */ + this.vertices = []; +} + +/** + * Get a vertex at position i. It does not matter if i is out of bounds, this function will just cycle. + * @method at + * @param {Number} i + * @return {Array} + */ +Polygon.prototype.at = function(i){ + var v = this.vertices, + s = v.length; + return v[i < 0 ? i % s + s : i % s]; +}; + +/** + * Get first vertex + * @method first + * @return {Array} + */ +Polygon.prototype.first = function(){ + return this.vertices[0]; +}; + +/** + * Get last vertex + * @method last + * @return {Array} + */ +Polygon.prototype.last = function(){ + return this.vertices[this.vertices.length-1]; +}; + +/** + * Clear the polygon data + * @method clear + * @return {Array} + */ +Polygon.prototype.clear = function(){ + this.vertices.length = 0; +}; + +/** + * Append points "from" to "to"-1 from an other polygon "poly" onto this one. + * @method append + * @param {Polygon} poly The polygon to get points from. + * @param {Number} from The vertex index in "poly". + * @param {Number} to The end vertex index in "poly". Note that this vertex is NOT included when appending. + * @return {Array} + */ +Polygon.prototype.append = function(poly,from,to){ + if(typeof(from) == "undefined") throw new Error("From is not given!"); + if(typeof(to) == "undefined") throw new Error("To is not given!"); + + if(to-1 < from) throw new Error("lol1"); + if(to > poly.vertices.length) throw new Error("lol2"); + if(from < 0) throw new Error("lol3"); + + for(var i=from; i v[br][0])) { + br = i; + } + } + + // reverse poly if clockwise + if (!Point.left(this.at(br - 1), this.at(br), this.at(br + 1))) { + this.reverse(); + } +}; + +/** + * Reverse the vertices in the polygon + * @method reverse + */ +Polygon.prototype.reverse = function(){ + var tmp = []; + for(var i=0, N=this.vertices.length; i!==N; i++){ + tmp.push(this.vertices.pop()); + } + this.vertices = tmp; +}; + +/** + * Check if a point in the polygon is a reflex point + * @method isReflex + * @param {Number} i + * @return {Boolean} + */ +Polygon.prototype.isReflex = function(i){ + return Point.right(this.at(i - 1), this.at(i), this.at(i + 1)); +}; + +var tmpLine1=[], + tmpLine2=[]; + +/** + * Check if two vertices in the polygon can see each other + * @method canSee + * @param {Number} a Vertex index 1 + * @param {Number} b Vertex index 2 + * @return {Boolean} + */ +Polygon.prototype.canSee = function(a,b) { + var p, dist, l1=tmpLine1, l2=tmpLine2; + + if (Point.leftOn(this.at(a + 1), this.at(a), this.at(b)) && Point.rightOn(this.at(a - 1), this.at(a), this.at(b))) { + return false; + } + dist = Point.sqdist(this.at(a), this.at(b)); + for (var i = 0; i !== this.vertices.length; ++i) { // for each edge + if ((i + 1) % this.vertices.length === a || i === a) // ignore incident edges + continue; + if (Point.leftOn(this.at(a), this.at(b), this.at(i + 1)) && Point.rightOn(this.at(a), this.at(b), this.at(i))) { // if diag intersects an edge + l1[0] = this.at(a); + l1[1] = this.at(b); + l2[0] = this.at(i); + l2[1] = this.at(i + 1); + p = Line.lineInt(l1,l2); + if (Point.sqdist(this.at(a), p) < dist) { // if edge is blocking visibility to b + return false; + } + } + } + + return true; +}; + +/** + * Copy the polygon from vertex i to vertex j. + * @method copy + * @param {Number} i + * @param {Number} j + * @param {Polygon} [targetPoly] Optional target polygon to save in. + * @return {Polygon} The resulting copy. + */ +Polygon.prototype.copy = function(i,j,targetPoly){ + var p = targetPoly || new Polygon(); + p.clear(); + if (i < j) { + // Insert all vertices from i to j + for(var k=i; k<=j; k++) + p.vertices.push(this.vertices[k]); + + } else { + + // Insert vertices 0 to j + for(var k=0; k<=j; k++) + p.vertices.push(this.vertices[k]); + + // Insert vertices i to end + for(var k=i; k 0) + return this.slice(edges); + else + return [this]; +}; + +/** + * Slices the polygon given one or more cut edges. If given one, this function will return two polygons (false on failure). If many, an array of polygons. + * @method slice + * @param {Array} cutEdges A list of edges, as returned by .getCutEdges() + * @return {Array} + */ +Polygon.prototype.slice = function(cutEdges){ + if(cutEdges.length == 0) return [this]; + if(cutEdges instanceof Array && cutEdges.length && cutEdges[0] instanceof Array && cutEdges[0].length==2 && cutEdges[0][0] instanceof Array){ + + var polys = [this]; + + for(var i=0; i maxlevel){ + console.warn("quickDecomp: max level ("+maxlevel+") reached."); + return result; + } + + for (var i = 0; i < this.vertices.length; ++i) { + if (poly.isReflex(i)) { + reflexVertices.push(poly.vertices[i]); + upperDist = lowerDist = Number.MAX_VALUE; + + + for (var j = 0; j < this.vertices.length; ++j) { + if (Point.left(poly.at(i - 1), poly.at(i), poly.at(j)) + && Point.rightOn(poly.at(i - 1), poly.at(i), poly.at(j - 1))) { // if line intersects with an edge + p = getIntersectionPoint(poly.at(i - 1), poly.at(i), poly.at(j), poly.at(j - 1)); // find the point of intersection + if (Point.right(poly.at(i + 1), poly.at(i), p)) { // make sure it's inside the poly + d = Point.sqdist(poly.vertices[i], p); + if (d < lowerDist) { // keep only the closest intersection + lowerDist = d; + lowerInt = p; + lowerIndex = j; + } + } + } + if (Point.left(poly.at(i + 1), poly.at(i), poly.at(j + 1)) + && Point.rightOn(poly.at(i + 1), poly.at(i), poly.at(j))) { + p = getIntersectionPoint(poly.at(i + 1), poly.at(i), poly.at(j), poly.at(j + 1)); + if (Point.left(poly.at(i - 1), poly.at(i), p)) { + d = Point.sqdist(poly.vertices[i], p); + if (d < upperDist) { + upperDist = d; + upperInt = p; + upperIndex = j; + } + } + } + } + + // if there are no vertices to connect to, choose a point in the middle + if (lowerIndex == (upperIndex + 1) % this.vertices.length) { + //console.log("Case 1: Vertex("+i+"), lowerIndex("+lowerIndex+"), upperIndex("+upperIndex+"), poly.size("+this.vertices.length+")"); + p[0] = (lowerInt[0] + upperInt[0]) / 2; + p[1] = (lowerInt[1] + upperInt[1]) / 2; + steinerPoints.push(p); + + if (i < upperIndex) { + //lowerPoly.insert(lowerPoly.end(), poly.begin() + i, poly.begin() + upperIndex + 1); + lowerPoly.append(poly, i, upperIndex+1); + lowerPoly.vertices.push(p); + upperPoly.vertices.push(p); + if (lowerIndex != 0){ + //upperPoly.insert(upperPoly.end(), poly.begin() + lowerIndex, poly.end()); + upperPoly.append(poly,lowerIndex,poly.vertices.length); + } + //upperPoly.insert(upperPoly.end(), poly.begin(), poly.begin() + i + 1); + upperPoly.append(poly,0,i+1); + } else { + if (i != 0){ + //lowerPoly.insert(lowerPoly.end(), poly.begin() + i, poly.end()); + lowerPoly.append(poly,i,poly.vertices.length); + } + //lowerPoly.insert(lowerPoly.end(), poly.begin(), poly.begin() + upperIndex + 1); + lowerPoly.append(poly,0,upperIndex+1); + lowerPoly.vertices.push(p); + upperPoly.vertices.push(p); + //upperPoly.insert(upperPoly.end(), poly.begin() + lowerIndex, poly.begin() + i + 1); + upperPoly.append(poly,lowerIndex,i+1); + } + } else { + // connect to the closest point within the triangle + //console.log("Case 2: Vertex("+i+"), closestIndex("+closestIndex+"), poly.size("+this.vertices.length+")\n"); + + if (lowerIndex > upperIndex) { + upperIndex += this.vertices.length; + } + closestDist = Number.MAX_VALUE; + + if(upperIndex < lowerIndex){ + return result; + } + + for (var j = lowerIndex; j <= upperIndex; ++j) { + if (Point.leftOn(poly.at(i - 1), poly.at(i), poly.at(j)) + && Point.rightOn(poly.at(i + 1), poly.at(i), poly.at(j))) { + d = Point.sqdist(poly.at(i), poly.at(j)); + if (d < closestDist) { + closestDist = d; + closestIndex = j % this.vertices.length; + } + } + } + + if (i < closestIndex) { + lowerPoly.append(poly,i,closestIndex+1); + if (closestIndex != 0){ + upperPoly.append(poly,closestIndex,v.length); + } + upperPoly.append(poly,0,i+1); + } else { + if (i != 0){ + lowerPoly.append(poly,i,v.length); + } + lowerPoly.append(poly,0,closestIndex+1); + upperPoly.append(poly,closestIndex,i+1); + } + } + + // solve smallest poly first + if (lowerPoly.vertices.length < upperPoly.vertices.length) { + lowerPoly.quickDecomp(result,reflexVertices,steinerPoints,delta,maxlevel,level); + upperPoly.quickDecomp(result,reflexVertices,steinerPoints,delta,maxlevel,level); + } else { + upperPoly.quickDecomp(result,reflexVertices,steinerPoints,delta,maxlevel,level); + lowerPoly.quickDecomp(result,reflexVertices,steinerPoints,delta,maxlevel,level); + } + + return result; + } + } + result.push(this); + + return result; +}; + +/** + * Remove collinear points in the polygon. + * @method removeCollinearPoints + * @param {Number} [precision] The threshold angle to use when determining whether two edges are collinear. Use zero for finest precision. + * @return {Number} The number of points removed + */ +Polygon.prototype.removeCollinearPoints = function(precision){ + var num = 0; + for(var i=this.vertices.length-1; this.vertices.length>3 && i>=0; --i){ + if(Point.collinear(this.at(i-1),this.at(i),this.at(i+1),precision)){ + // Remove the middle point + this.vertices.splice(i%this.vertices.length,1); + i--; // Jump one point forward. Otherwise we may get a chain removal + num++; + } + } + return num; +}; + +},{"./Line":1,"./Point":2,"./Scalar":4}],4:[function(require,module,exports){ +module.exports = Scalar; + +/** + * Scalar functions + * @class Scalar + */ +function Scalar(){} + +/** + * Check if two scalars are equal + * @static + * @method eq + * @param {Number} a + * @param {Number} b + * @param {Number} [precision] + * @return {Boolean} + */ +Scalar.eq = function(a,b,precision){ + precision = precision || 0; + return Math.abs(a-b) < precision; +}; + +},{}],5:[function(require,module,exports){ +module.exports = { + Polygon : require("./Polygon"), + Point : require("./Point"), +}; + +},{"./Point":2,"./Polygon":3}]},{},[5]) +(5) +}); +; \ No newline at end of file diff --git a/src/body/Body.js b/src/body/Body.js index 9f3db17..3092182 100644 --- a/src/body/Body.js +++ b/src/body/Body.js @@ -563,6 +563,9 @@ var Body = {}; * @return {} */ var _totalProperties = function(body) { + // https://ecourses.ou.edu/cgi-bin/ebook.cgi?doc=&topic=st&chap_sec=07.2&page=theory + // http://output.to/sideway/default.asp?qno=121100087 + var properties = { mass: 0, area: 0, @@ -576,10 +579,10 @@ var Body = {}; properties.mass += part.mass; properties.area += part.area; properties.inertia += part.inertia; - Vector.add(properties.centre, part.position, properties.centre); + properties.centre = Vector.add(properties.centre, Vector.mult(part.position, part.mass)); } - properties.centre = Vector.div(properties.centre, body.parts.length - 1); + properties.centre = Vector.div(properties.centre, properties.mass); return properties; }; diff --git a/src/factory/Bodies.js b/src/factory/Bodies.js index 25d2aa2..bc6813e 100644 --- a/src/factory/Bodies.js +++ b/src/factory/Bodies.js @@ -93,7 +93,7 @@ var Bodies = {}; * @param {number} y * @param {number} radius * @param {object} [options] - * @param {number} maxSides + * @param {number} [maxSides] * @return {body} A new circle body */ Bodies.circle = function(x, y, radius, options, maxSides) { @@ -161,4 +161,100 @@ var Bodies = {}; return Body.create(Common.extend({}, polygon, options)); }; + /** + * Creates a body using the supplied vertices. + * If the vertices are not convex, they will be decomposed if [poly-decomp.js](https://github.com/schteppe/poly-decomp.js) is available. + * If the vertices can not be decomposed, the function will use the convex hull. + * By default the decomposition will discard collinear edges (to improve performance). + * The options parameter is an object that specifies any properties you wish to override the defaults. + * See the properties section of the `Matter.Body` module for detailed information on what you can pass via the `options` object. + * @method fromVertices + * @param {number} x + * @param {number} y + * @param [vector] vertices + * @param {object} [options] + * @param {bool} [removeCollinear=true] + * @return {body} + */ + Bodies.fromVertices = function(x, y, vertices, options, removeCollinear) { + var canDecompose = true, + body, + i; + + options = options || {}; + removeCollinear = typeof removeCollinear !== 'undefined' ? removeCollinear : true; + + if (Vertices.isConvex(vertices)) { + // vertices are convex, so just create a body normally + body = { + position: { x: x, y: y }, + vertices: vertices + }; + + return Body.create(Common.extend({}, body, options)); + } + + // check for poly-decomp.js + if (!window.decomp) { + Common.log('Bodies.fromVertices: poly-decomp.js required. Could not decompose vertices. Fallback to convex hull.', 'warn'); + canDecompose = false; + } + + // initialise a decomposition + var concave = new decomp.Polygon(); + for (i = 0; i < vertices.length; i++) { + concave.vertices.push([vertices[i].x, vertices[i].y]); + } + + // check for complexity + if (!concave.isSimple()) { + Common.log('Bodies.fromVertices: Non-simple polygons are not supported. Could not decompose vertices. Fallback to convex hull.', 'warn'); + canDecompose = false; + } + + // try to decompose + if (canDecompose) { + // vertices are concave and simple, we can decompose into parts + concave.makeCCW(); + if (removeCollinear) + concave.removeCollinearPoints(0.001); + + var decomposed = concave.quickDecomp(), + parts = []; + + // for each decomposed chunk + for (i = 0; i < decomposed.length; i++) { + var chunk = decomposed[i], + chunkVertices = []; + + // convert vertices into the correct structure + for (var j = 0; j < chunk.vertices.length; j++) { + chunkVertices.push({ x: chunk.vertices[j][0], y: chunk.vertices[j][1] }); + } + + // create a compound part + parts.push( + Body.create({ + position: Vertices.centre(chunkVertices), + vertices: chunkVertices + }) + ); + } + + // create the parent body to be returned, that contains generated compound parts + body = Body.create(Common.extend({}, { parts: parts }, options)); + Body.setPosition(body, { x: x, y: y }); + + return body; + } else { + // fallback to convex hull when decomposition is not possible + body = { + position: { x: x, y: y }, + vertices: Vertices.hull(vertices) + }; + + return Body.create(Common.extend({}, body, options)); + } + }; + })(); diff --git a/src/geometry/Vertices.js b/src/geometry/Vertices.js index bfa7cf5..0c06a78 100644 --- a/src/geometry/Vertices.js +++ b/src/geometry/Vertices.js @@ -56,7 +56,7 @@ var Vertices = {}; * @return {vertices} vertices */ Vertices.fromPath = function(path, body) { - var pathPattern = /L\s*([\-\d\.]*)\s*([\-\d\.]*)/ig, + var pathPattern = /L?\s*([\-\d\.]+)\s*([\-\d\.]+)\s*,?/ig, points = []; path.replace(pathPattern, function(match, x, y) { @@ -330,15 +330,58 @@ var Vertices = {}; * @return {vertices} vertices */ Vertices.clockwiseSort = function(vertices) { - var mean = Vertices.mean(vertices); + var centre = Vertices.mean(vertices); vertices.sort(function(vertexA, vertexB) { - return Vector.angle(mean, vertexA) - Vector.angle(mean, vertexB); + return Vector.angle(centre, vertexA) - Vector.angle(centre, vertexB); }); return vertices; }; + /** + * Returns true if the vertices form a convex shape (vertices must be in clockwise order). + * @method isConvex + * @param {vertices} vertices + * @return {bool} `true` if the `vertices` are convex, `false` if not (or `null` if not computable). + */ + Vertices.isConvex = function(vertices) { + // http://paulbourke.net/geometry/polygonmesh/ + + var flag = 0, + n = vertices.length, + i, + j, + k, + z; + + if (n < 3) + return null; + + for (i = 0; i < n; i++) { + j = (i + 1) % n; + k = (i + 2) % n; + z = (vertices[j].x - vertices[i].x) * (vertices[k].y - vertices[j].y); + z -= (vertices[j].y - vertices[i].y) * (vertices[k].x - vertices[j].x); + + if (z < 0) { + flag |= 1; + } else if (z > 0) { + flag |= 2; + } + + if (flag === 3) { + return false; + } + } + + if (flag !== 0){ + return true; + } else { + return null; + } + }; + /** * Returns the convex hull of the input vertices as a new array of points. * @method hull