/**
* The `Matter.Vertices` module contains methods for creating and manipulating sets of vertices.
* A set of vertices is an array of `Matter.Vector` with additional indexing properties inserted by `Vertices.create`.
* A `Matter.Body` maintains a set of vertices to represent the shape of the object (its convex hull).
*
* See the included usage [examples](https://github.com/liabru/matter-js/tree/master/examples).
*
* @class Vertices
*/
var Vertices = {};
module.exports = Vertices;
var Vector = require('../geometry/Vector');
var Common = require('../core/Common');
(function() {
/**
* Creates a new set of `Matter.Body` compatible vertices.
* The `points` argument accepts an array of `Matter.Vector` points orientated around the origin `(0, 0)`, for example:
*
* [{ x: 0, y: 0 }, { x: 25, y: 50 }, { x: 50, y: 0 }]
*
* The `Vertices.create` method returns a new array of vertices, which are similar to Matter.Vector objects,
* but with some additional references required for efficient collision detection routines.
*
* Vertices must be specified in clockwise order.
*
* Note that the `body` argument is not optional, a `Matter.Body` reference must be provided.
*
* @method create
* @param {vector[]} points
* @param {body} body
*/
Vertices.create = function(points, body) {
var vertices = [];
for (var i = 0; i < points.length; i++) {
var point = points[i],
vertex = {
x: point.x,
y: point.y,
index: i,
body: body,
isInternal: false
};
vertices.push(vertex);
}
return vertices;
};
/**
* Parses a string containing ordered x y pairs separated by spaces (and optionally commas),
* into a `Matter.Vertices` object for the given `Matter.Body`.
* For parsing SVG paths, see `Svg.pathToVertices`.
* @method fromPath
* @param {string} path
* @param {body} body
* @return {vertices} vertices
*/
Vertices.fromPath = function(path, body) {
var pathPattern = /L?\s*([-\d.e]+)[\s,]*([-\d.e]+)*/ig,
points = [];
path.replace(pathPattern, function(match, x, y) {
points.push({ x: parseFloat(x), y: parseFloat(y) });
});
return Vertices.create(points, body);
};
/**
* Returns the centre (centroid) of the set of vertices.
* @method centre
* @param {vertices} vertices
* @return {vector} The centre point
*/
Vertices.centre = function(vertices) {
var area = Vertices.area(vertices, true),
centre = { x: 0, y: 0 },
cross,
temp,
j;
for (var i = 0; i < vertices.length; i++) {
j = (i + 1) % vertices.length;
cross = Vector.cross(vertices[i], vertices[j]);
temp = Vector.mult(Vector.add(vertices[i], vertices[j]), cross);
centre = Vector.add(centre, temp);
}
return Vector.div(centre, 6 * area);
};
/**
* Returns the average (mean) of the set of vertices.
* @method mean
* @param {vertices} vertices
* @return {vector} The average point
*/
Vertices.mean = function(vertices) {
var average = { x: 0, y: 0 };
for (var i = 0; i < vertices.length; i++) {
average.x += vertices[i].x;
average.y += vertices[i].y;
}
return Vector.div(average, vertices.length);
};
/**
* Returns the area of the set of vertices.
* @method area
* @param {vertices} vertices
* @param {bool} signed
* @return {number} The area
*/
Vertices.area = function(vertices, signed) {
var area = 0,
j = vertices.length - 1;
for (var i = 0; i < vertices.length; i++) {
area += (vertices[j].x - vertices[i].x) * (vertices[j].y + vertices[i].y);
j = i;
}
if (signed)
return area / 2;
return Math.abs(area) / 2;
};
/**
* Returns the moment of inertia (second moment of area) of the set of vertices given the total mass.
* @method inertia
* @param {vertices} vertices
* @param {number} mass
* @return {number} The polygon's moment of inertia
*/
Vertices.inertia = function(vertices, mass) {
var numerator = 0,
denominator = 0,
v = vertices,
cross,
j;
// find the polygon's moment of inertia, using second moment of area
// from equations at http://www.physicsforums.com/showthread.php?t=25293
for (var n = 0; n < v.length; n++) {
j = (n + 1) % v.length;
cross = Math.abs(Vector.cross(v[j], v[n]));
numerator += cross * (Vector.dot(v[j], v[j]) + Vector.dot(v[j], v[n]) + Vector.dot(v[n], v[n]));
denominator += cross;
}
return (mass / 6) * (numerator / denominator);
};
/**
* Translates the set of vertices in-place.
* @method translate
* @param {vertices} vertices
* @param {vector} vector
* @param {number} scalar
*/
Vertices.translate = function(vertices, vector, scalar) {
scalar = typeof scalar !== 'undefined' ? scalar : 1;
var verticesLength = vertices.length,
translateX = vector.x * scalar,
translateY = vector.y * scalar,
i;
for (i = 0; i < verticesLength; i++) {
vertices[i].x += translateX;
vertices[i].y += translateY;
}
return vertices;
};
/**
* Rotates the set of vertices in-place.
* @method rotate
* @param {vertices} vertices
* @param {number} angle
* @param {vector} point
*/
Vertices.rotate = function(vertices, angle, point) {
if (angle === 0)
return;
var cos = Math.cos(angle),
sin = Math.sin(angle),
pointX = point.x,
pointY = point.y,
verticesLength = vertices.length,
vertex,
dx,
dy,
i;
for (i = 0; i < verticesLength; i++) {
vertex = vertices[i];
dx = vertex.x - pointX;
dy = vertex.y - pointY;
vertex.x = pointX + (dx * cos - dy * sin);
vertex.y = pointY + (dx * sin + dy * cos);
}
return vertices;
};
/**
* Returns `true` if the `point` is inside the set of `vertices`.
* @method contains
* @param {vertices} vertices
* @param {vector} point
* @return {boolean} True if the vertices contains point, otherwise false
*/
Vertices.contains = function(vertices, point) {
var pointX = point.x,
pointY = point.y,
verticesLength = vertices.length,
vertex = vertices[verticesLength - 1],
nextVertex;
for (var i = 0; i < verticesLength; i++) {
nextVertex = vertices[i];
if ((pointX - vertex.x) * (nextVertex.y - vertex.y)
+ (pointY - vertex.y) * (vertex.x - nextVertex.x) > 0) {
return false;
}
vertex = nextVertex;
}
return true;
};
/**
* Scales the vertices from a point (default is centre) in-place.
* @method scale
* @param {vertices} vertices
* @param {number} scaleX
* @param {number} scaleY
* @param {vector} point
*/
Vertices.scale = function(vertices, scaleX, scaleY, point) {
if (scaleX === 1 && scaleY === 1)
return vertices;
point = point || Vertices.centre(vertices);
var vertex,
delta;
for (var i = 0; i < vertices.length; i++) {
vertex = vertices[i];
delta = Vector.sub(vertex, point);
vertices[i].x = point.x + delta.x * scaleX;
vertices[i].y = point.y + delta.y * scaleY;
}
return vertices;
};
/**
* Chamfers a set of vertices by giving them rounded corners, returns a new set of vertices.
* The radius parameter is a single number or an array to specify the radius for each vertex.
* @method chamfer
* @param {vertices} vertices
* @param {number[]} radius
* @param {number} quality
* @param {number} qualityMin
* @param {number} qualityMax
*/
Vertices.chamfer = function(vertices, radius, quality, qualityMin, qualityMax) {
if (typeof radius === 'number') {
radius = [radius];
} else {
radius = radius || [8];
}
// quality defaults to -1, which is auto
quality = (typeof quality !== 'undefined') ? quality : -1;
qualityMin = qualityMin || 2;
qualityMax = qualityMax || 14;
var newVertices = [];
for (var i = 0; i < vertices.length; i++) {
var prevVertex = vertices[i - 1 >= 0 ? i - 1 : vertices.length - 1],
vertex = vertices[i],
nextVertex = vertices[(i + 1) % vertices.length],
currentRadius = radius[i < radius.length ? i : radius.length - 1];
if (currentRadius === 0) {
newVertices.push(vertex);
continue;
}
var prevNormal = Vector.normalise({
x: vertex.y - prevVertex.y,
y: prevVertex.x - vertex.x
});
var nextNormal = Vector.normalise({
x: nextVertex.y - vertex.y,
y: vertex.x - nextVertex.x
});
var diagonalRadius = Math.sqrt(2 * Math.pow(currentRadius, 2)),
radiusVector = Vector.mult(Common.clone(prevNormal), currentRadius),
midNormal = Vector.normalise(Vector.mult(Vector.add(prevNormal, nextNormal), 0.5)),
scaledVertex = Vector.sub(vertex, Vector.mult(midNormal, diagonalRadius));
var precision = quality;
if (quality === -1) {
// automatically decide precision
precision = Math.pow(currentRadius, 0.32) * 1.75;
}
precision = Common.clamp(precision, qualityMin, qualityMax);
// use an even value for precision, more likely to reduce axes by using symmetry
if (precision % 2 === 1)
precision += 1;
var alpha = Math.acos(Vector.dot(prevNormal, nextNormal)),
theta = alpha / precision;
for (var j = 0; j < precision; j++) {
newVertices.push(Vector.add(Vector.rotate(radiusVector, theta * j), scaledVertex));
}
}
return newVertices;
};
/**
* Sorts the input vertices into clockwise order in place.
* @method clockwiseSort
* @param {vertices} vertices
* @return {vertices} vertices
*/
Vertices.clockwiseSort = function(vertices) {
var centre = Vertices.mean(vertices);
vertices.sort(function(vertexA, 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/
// Copyright (c) Paul Bourke (use permitted)
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
* @param {vertices} vertices
* @return [vertex] vertices
*/
Vertices.hull = function(vertices) {
// http://geomalgorithms.com/a10-_hull-1.html
var upper = [],
lower = [],
vertex,
i;
// sort vertices on x-axis (y-axis for ties)
vertices = vertices.slice(0);
vertices.sort(function(vertexA, vertexB) {
var dx = vertexA.x - vertexB.x;
return dx !== 0 ? dx : vertexA.y - vertexB.y;
});
// build lower hull
for (i = 0; i < vertices.length; i += 1) {
vertex = vertices[i];
while (lower.length >= 2
&& Vector.cross3(lower[lower.length - 2], lower[lower.length - 1], vertex) <= 0) {
lower.pop();
}
lower.push(vertex);
}
// build upper hull
for (i = vertices.length - 1; i >= 0; i -= 1) {
vertex = vertices[i];
while (upper.length >= 2
&& Vector.cross3(upper[upper.length - 2], upper[upper.length - 1], vertex) <= 0) {
upper.pop();
}
upper.push(vertex);
}
// concatenation of the lower and upper hulls gives the convex hull
// omit last points because they are repeated at the beginning of the other list
upper.pop();
lower.pop();
return upper.concat(lower);
};
})();