/**
* The `Matter.Bodies` module contains factory methods for creating rigid body models
* with commonly used body configurations (such as rectangles, circles and other polygons).
*
* See the included usage [examples](https://github.com/liabru/matter-js/tree/master/examples).
*
* @class Bodies
*/
// TODO: true circle bodies
var Bodies = {};
module.exports = Bodies;
var Vertices = require('../geometry/Vertices');
var Common = require('../core/Common');
var Body = require('../body/Body');
var Bounds = require('../geometry/Bounds');
var Vector = require('../geometry/Vector');
(function() {
/**
* Creates a new rigid body model with a rectangle hull.
* 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 rectangle
* @param {number} x
* @param {number} y
* @param {number} width
* @param {number} height
* @param {object} [options]
* @return {body} A new rectangle body
*/
Bodies.rectangle = function(x, y, width, height, options) {
options = options || {};
var rectangle = {
label: 'Rectangle Body',
position: { x: x, y: y },
vertices: Vertices.fromPath('L 0 0 L ' + width + ' 0 L ' + width + ' ' + height + ' L 0 ' + height)
};
if (options.chamfer) {
var chamfer = options.chamfer;
rectangle.vertices = Vertices.chamfer(rectangle.vertices, chamfer.radius,
chamfer.quality, chamfer.qualityMin, chamfer.qualityMax);
delete options.chamfer;
}
return Body.create(Common.extend({}, rectangle, options));
};
/**
* Creates a new rigid body model with a trapezoid hull.
* The `slope` is parameterised as a fraction of `width` and must be < 1 to form a valid trapezoid.
* 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 trapezoid
* @param {number} x
* @param {number} y
* @param {number} width
* @param {number} height
* @param {number} slope Must be a number < 1.
* @param {object} [options]
* @return {body} A new trapezoid body
*/
Bodies.trapezoid = function(x, y, width, height, slope, options) {
options = options || {};
if (slope >= 1) {
Common.warn('Bodies.trapezoid: slope parameter must be < 1.');
}
slope *= 0.5;
var roof = (1 - (slope * 2)) * width;
var x1 = width * slope,
x2 = x1 + roof,
x3 = x2 + x1,
verticesPath;
if (slope < 0.5) {
verticesPath = 'L 0 0 L ' + x1 + ' ' + (-height) + ' L ' + x2 + ' ' + (-height) + ' L ' + x3 + ' 0';
} else {
verticesPath = 'L 0 0 L ' + x2 + ' ' + (-height) + ' L ' + x3 + ' 0';
}
var trapezoid = {
label: 'Trapezoid Body',
position: { x: x, y: y },
vertices: Vertices.fromPath(verticesPath)
};
if (options.chamfer) {
var chamfer = options.chamfer;
trapezoid.vertices = Vertices.chamfer(trapezoid.vertices, chamfer.radius,
chamfer.quality, chamfer.qualityMin, chamfer.qualityMax);
delete options.chamfer;
}
return Body.create(Common.extend({}, trapezoid, options));
};
/**
* Creates a new rigid body model with a circle hull.
* 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 circle
* @param {number} x
* @param {number} y
* @param {number} radius
* @param {object} [options]
* @param {number} [maxSides]
* @return {body} A new circle body
*/
Bodies.circle = function(x, y, radius, options, maxSides) {
options = options || {};
var circle = {
label: 'Circle Body',
circleRadius: radius
};
// approximate circles with polygons until true circles implemented in SAT
maxSides = maxSides || 25;
var sides = Math.ceil(Math.max(10, Math.min(maxSides, radius)));
// optimisation: always use even number of sides (half the number of unique axes)
if (sides % 2 === 1)
sides += 1;
return Bodies.polygon(x, y, sides, radius, Common.extend({}, circle, options));
};
/**
* Creates a new rigid body model with a regular polygon hull with the given number of sides.
* 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 polygon
* @param {number} x
* @param {number} y
* @param {number} sides
* @param {number} radius
* @param {object} [options]
* @return {body} A new regular polygon body
*/
Bodies.polygon = function(x, y, sides, radius, options) {
options = options || {};
if (sides < 3)
return Bodies.circle(x, y, radius, options);
var theta = 2 * Math.PI / sides,
path = '',
offset = theta * 0.5;
for (var i = 0; i < sides; i += 1) {
var angle = offset + (i * theta),
xx = Math.cos(angle) * radius,
yy = Math.sin(angle) * radius;
path += 'L ' + xx.toFixed(3) + ' ' + yy.toFixed(3) + ' ';
}
var polygon = {
label: 'Polygon Body',
position: { x: x, y: y },
vertices: Vertices.fromPath(path)
};
if (options.chamfer) {
var chamfer = options.chamfer;
polygon.vertices = Vertices.chamfer(polygon.vertices, chamfer.radius,
chamfer.quality, chamfer.qualityMin, chamfer.qualityMax);
delete options.chamfer;
}
return Body.create(Common.extend({}, polygon, options));
};
/**
* Utility to create a compound body based on set(s) of vertices.
*
* _Note:_ To optionally enable automatic concave vertices decomposition the [poly-decomp](https://github.com/schteppe/poly-decomp.js)
* package must be first installed and provided see `Common.setDecomp`, otherwise the convex hull of each vertex set will be used.
*
* The resulting vertices are reorientated about their centre of mass,
* and offset such that `body.position` corresponds to this point.
*
* The resulting offset may be found if needed by subtracting `body.bounds` from the original input bounds.
* To later move the centre of mass see `Body.setCentre`.
*
* Note that automatic conconcave decomposition results are not always optimal.
* For best results, simplify the input vertices as much as possible first.
* By default this function applies some addtional simplification to help.
*
* Some outputs may also require further manual processing afterwards to be robust.
* In particular some parts may need to be overlapped to avoid collision gaps.
* Thin parts and sharp points should be avoided or removed where possible.
*
* The options parameter object specifies any `Matter.Body` 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 {array} vertexSets One or more arrays of vertex points e.g. `[[{ x: 0, y: 0 }...], ...]`.
* @param {object} [options] The body options.
* @param {bool} [flagInternal=false] Optionally marks internal edges with `isInternal`.
* @param {number} [removeCollinear=0.01] Threshold when simplifying vertices along the same edge.
* @param {number} [minimumArea=10] Threshold when removing small parts.
* @param {number} [removeDuplicatePoints=0.01] Threshold when simplifying nearby vertices.
* @return {body}
*/
Bodies.fromVertices = function(x, y, vertexSets, options, flagInternal, removeCollinear, minimumArea, removeDuplicatePoints) {
var decomp = Common.getDecomp(),
canDecomp,
body,
parts,
isConvex,
isConcave,
vertices,
i,
j,
k,
v,
z;
// check decomp is as expected
canDecomp = Boolean(decomp && decomp.quickDecomp);
options = options || {};
parts = [];
flagInternal = typeof flagInternal !== 'undefined' ? flagInternal : false;
removeCollinear = typeof removeCollinear !== 'undefined' ? removeCollinear : 0.01;
minimumArea = typeof minimumArea !== 'undefined' ? minimumArea : 10;
removeDuplicatePoints = typeof removeDuplicatePoints !== 'undefined' ? removeDuplicatePoints : 0.01;
// ensure vertexSets is an array of arrays
if (!Common.isArray(vertexSets[0])) {
vertexSets = [vertexSets];
}
for (v = 0; v < vertexSets.length; v += 1) {
vertices = vertexSets[v];
isConvex = Vertices.isConvex(vertices);
isConcave = !isConvex;
if (isConcave && !canDecomp) {
Common.warnOnce(
'Bodies.fromVertices: Install the \'poly-decomp\' library and use Common.setDecomp or provide \'decomp\' as a global to decompose concave vertices.'
);
}
if (isConvex || !canDecomp) {
if (isConvex) {
vertices = Vertices.clockwiseSort(vertices);
} else {
// fallback to convex hull when decomposition is not possible
vertices = Vertices.hull(vertices);
}
parts.push({
position: { x: x, y: y },
vertices: vertices
});
} else {
// initialise a decomposition
var concave = vertices.map(function(vertex) {
return [vertex.x, vertex.y];
});
// vertices are concave and simple, we can decompose into parts
decomp.makeCCW(concave);
if (removeCollinear !== false)
decomp.removeCollinearPoints(concave, removeCollinear);
if (removeDuplicatePoints !== false && decomp.removeDuplicatePoints)
decomp.removeDuplicatePoints(concave, removeDuplicatePoints);
// use the quick decomposition algorithm (Bayazit)
var decomposed = decomp.quickDecomp(concave);
// for each decomposed chunk
for (i = 0; i < decomposed.length; i++) {
var chunk = decomposed[i];
// convert vertices into the correct structure
var chunkVertices = chunk.map(function(vertices) {
return {
x: vertices[0],
y: vertices[1]
};
});
// skip small chunks
if (minimumArea > 0 && Vertices.area(chunkVertices) < minimumArea)
continue;
// create a compound part
parts.push({
position: Vertices.centre(chunkVertices),
vertices: chunkVertices
});
}
}
}
// create body parts
for (i = 0; i < parts.length; i++) {
parts[i] = Body.create(Common.extend(parts[i], options));
}
// flag internal edges (coincident part edges)
if (flagInternal) {
var coincident_max_dist = 5;
for (i = 0; i < parts.length; i++) {
var partA = parts[i];
for (j = i + 1; j < parts.length; j++) {
var partB = parts[j];
if (Bounds.overlaps(partA.bounds, partB.bounds)) {
var pav = partA.vertices,
pbv = partB.vertices;
// iterate vertices of both parts
for (k = 0; k < partA.vertices.length; k++) {
for (z = 0; z < partB.vertices.length; z++) {
// find distances between the vertices
var da = Vector.magnitudeSquared(Vector.sub(pav[(k + 1) % pav.length], pbv[z])),
db = Vector.magnitudeSquared(Vector.sub(pav[k], pbv[(z + 1) % pbv.length]));
// if both vertices are very close, consider the edge concident (internal)
if (da < coincident_max_dist && db < coincident_max_dist) {
pav[k].isInternal = true;
pbv[z].isInternal = true;
}
}
}
}
}
}
}
if (parts.length > 1) {
// create the parent body to be returned, that contains generated compound parts
body = Body.create(Common.extend({ parts: parts.slice(0) }, options));
// offset such that body.position is at the centre off mass
Body.setPosition(body, { x: x, y: y });
return body;
} else {
return parts[0];
}
};
})();