org.dyn4j.geometry.Polygon Maven / Gradle / Ivy
/*
* Copyright (c) 2010-2022 William Bittle http://www.dyn4j.org/
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without modification, are permitted
* provided that the following conditions are met:
*
* * Redistributions of source code must retain the above copyright notice, this list of conditions
* and the following disclaimer.
* * Redistributions in binary form must reproduce the above copyright notice, this list of conditions
* and the following disclaimer in the documentation and/or other materials provided with the
* distribution.
* * Neither the name of the copyright holder nor the names of its contributors may be used to endorse or
* promote products derived from this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR
* IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
* FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER
* IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
* OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package org.dyn4j.geometry;
import java.util.Iterator;
import org.dyn4j.DataContainer;
import org.dyn4j.Epsilon;
import org.dyn4j.resources.Messages;
/**
* Implementation of an arbitrary polygon {@link Convex} {@link Shape}.
*
* A {@link Polygon} must have at least 3 vertices where one of which is not colinear with the other two.
* A {@link Polygon} must also be {@link Convex} and have counter-clockwise winding of points.
*
* A polygon cannot have coincident vertices.
* @author William Bittle
* @version 4.2.1
* @since 1.0.0
*/
public class Polygon extends AbstractShape implements Convex, Wound, Shape, Transformable, DataContainer {
/** The polygon vertices */
final Vector2[] vertices;
/** The polygon normals */
final Vector2[] normals;
/**
* Full constructor for sub classes.
* @param center the center
* @param radius the rotation radius
* @param vertices the vertices
* @param normals the normals
*/
Polygon(Vector2 center, double radius, Vector2[] vertices, Vector2[] normals) {
super(center, radius);
this.vertices = vertices;
this.normals = normals;
}
/**
* Validated constructor.
*
* Creates a new {@link Polygon} using the given vertices. The center of the polygon
* is calculated using an area weighted method.
* @param valid always true or this constructor would not be called
* @param vertices the polygon vertices
* @param center the center of the polygon
*/
private Polygon(boolean valid, Vector2[] vertices, Vector2 center) {
super(center, Geometry.getRotationRadius(center, vertices));
// set the vertices
this.vertices = vertices;
// create the normals
this.normals = Geometry.getCounterClockwiseEdgeNormals(vertices);
}
/**
* Full constructor.
*
* Creates a new {@link Polygon} using the given vertices. The center of the polygon
* is calculated using an area weighted method.
*
* A polygon must have 3 or more vertices, of which one is not colinear with the other two.
*
* A polygon must also be convex and have counter-clockwise winding.
* @param vertices the array of vertices
* @throws NullPointerException if vertices is null or contains a null element
* @throws IllegalArgumentException if vertices contains less than 3 points, contains coincident points, is not convex, or has clockwise winding
*/
public Polygon(Vector2... vertices) {
this(validate(vertices), vertices, Geometry.getAreaWeightedCenter(vertices));
}
/**
* Validates the constructor input returning true if valid or throwing an exception if invalid.
* @param vertices the array of vertices
* @return boolean true
* @throws NullPointerException if vertices is null or contains a null element
* @throws IllegalArgumentException if vertices contains less than 3 points, contains coincident points, is not convex, or has clockwise winding
*/
private static final boolean validate(Vector2... vertices) {
// check the vertex array
if (vertices == null) throw new NullPointerException(Messages.getString("geometry.polygon.nullArray"));
// get the size
int size = vertices.length;
// check the size
if (size < 3) throw new IllegalArgumentException(Messages.getString("geometry.polygon.lessThan3Vertices"));
// check for null vertices
for (int i = 0; i < size; i++) {
if (vertices[i] == null) throw new NullPointerException(Messages.getString("geometry.polygon.nullVertices"));
}
// check for convex
double area = 0.0;
double sign = 0.0;
for (int i = 0; i < size; i++) {
Vector2 p0 = (i - 1 < 0) ? vertices[size - 1] : vertices[i - 1];
Vector2 p1 = vertices[i];
Vector2 p2 = (i + 1 == size) ? vertices[0] : vertices[i + 1];
// check for coincident vertices
if (p1.equals(p2)) {
throw new IllegalArgumentException(Messages.getString("geometry.polygon.coincidentVertices"));
}
// check the cross product for CCW winding
double cross = p0.to(p1).cross(p1.to(p2));
double tsign = Math.signum(cross);
area += cross;
// check for colinear edges (for now its allowed)
if (Math.abs(cross) > Epsilon.E) {
// check for convexity
if (sign != 0.0 && tsign != sign) {
throw new IllegalArgumentException(Messages.getString("geometry.polygon.nonConvex"));
}
}
sign = tsign;
}
// don't allow degenerate polygons
if (Math.abs(area) <= Epsilon.E) {
throw new IllegalArgumentException(Messages.getString("geometry.polygon.zeroArea"));
}
// check for CCW
if (area < 0.0) {
throw new IllegalArgumentException(Messages.getString("geometry.polygon.invalidWinding"));
}
// if we've made it this far then continue;
return true;
}
/* (non-Javadoc)
* @see org.dyn4j.geometry.Wound#toString()
*/
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
sb.append("Polygon[").append(super.toString())
.append("|Vertices={");
for (int i = 0; i < this.vertices.length; i++) {
if (i != 0) sb.append(",");
sb.append(this.vertices[i]);
}
sb.append("}")
.append("]");
return sb.toString();
}
/* (non-Javadoc)
* @see org.dyn4j.geometry.Wound#getVertices()
*/
@Override
public Vector2[] getVertices() {
return this.vertices;
}
/* (non-Javadoc)
* @see org.dyn4j.geometry.Wound#getNormals()
*/
@Override
public Vector2[] getNormals() {
return this.normals;
}
/* (non-Javadoc)
* @see org.dyn4j.geometry.Wound#getVertexIterator()
*/
@Override
public Iterator getVertexIterator() {
return new WoundIterator(this.vertices);
}
/* (non-Javadoc)
* @see org.dyn4j.geometry.Wound#getNormalIterator()
*/
@Override
public Iterator getNormalIterator() {
return new WoundIterator(this.normals);
}
/* (non-Javadoc)
* @see org.dyn4j.geometry.Shape#getRadius(org.dyn4j.geometry.Vector2)
*/
@Override
public double getRadius(Vector2 center) {
return Geometry.getRotationRadius(center, this.vertices);
}
/* (non-Javadoc)
* @see org.dyn4j.geometry.Convex#getAxes(java.util.List, org.dyn4j.geometry.Transform)
*/
@Override
public Vector2[] getAxes(Vector2[] foci, Transform transform) {
// get the size of the foci list
int fociSize = foci != null ? foci.length : 0;
// get the number of vertices this polygon has
int size = this.vertices.length;
// the axes of a polygon are created from the normal of the edges
// plus the closest point to each focus
Vector2[] axes = new Vector2[size + fociSize];
int n = 0;
// loop over the edge normals and put them into world space
for (int i = 0; i < size; i++) {
// create references to the current points
Vector2 v = this.normals[i];
// transform it into world space and add it to the list
axes[n++] = transform.getTransformedR(v);
}
// loop over the focal points and find the closest
// points on the polygon to the focal points
for (int i = 0; i < fociSize; i++) {
// get the current focus
Vector2 f = foci[i];
// create a place for the closest point
Vector2 closest = transform.getTransformed(this.vertices[0]);
double d = f.distanceSquared(closest);
// find the minimum distance vertex
for (int j = 1; j < size; j++) {
// get the vertex
Vector2 p = this.vertices[j];
// transform it into world space
p = transform.getTransformed(p);
// get the squared distance to the focus
double dt = f.distanceSquared(p);
// compare with the last distance
if (dt < d) {
// if its closer then save it
closest = p;
d = dt;
}
}
// once we have found the closest point create
// a vector from the focal point to the point
Vector2 axis = f.to(closest);
// normalize it
axis.normalize();
// add it to the array
axes[n++] = axis;
}
// return all the axes
return axes;
}
/**
* {@inheritDoc}
*
* Not applicable to this shape. Always returns null.
* @return null
*/
@Override
public Vector2[] getFoci(Transform transform) {
return null;
}
/* (non-Javadoc)
* @see org.dyn4j.geometry.Shape#contains(org.dyn4j.geometry.Vector2, org.dyn4j.geometry.Transform, boolean)
*/
@Override
public boolean contains(Vector2 point, Transform transform, boolean inclusive) {
// if the polygon is convex then do a simple inside test
// if the the sign of the location of the point on the side of an edge (or line)
// is always the same and the polygon is convex then we know that the
// point lies inside the polygon
// This method doesn't care about vertex winding
// inverse transform the point to put it in local coordinates
Vector2 p = transform.getInverseTransformed(point);
// start from the pair (p1 = last, p2 = first) so there's no need to check in the loop for wrap-around of the i + 1 vertice
int size = this.vertices.length;
Vector2 p1 = this.vertices[size - 1];
Vector2 p2 = this.vertices[0];
// get the location of the point relative to the first two vertices
double last = Segment.getLocation(p, p1, p2);
// loop through the rest of the vertices
for (int i = 0; i < size - 1; i++) {
// p1 is now p2
p1 = p2;
// p2 is the next point
p2 = this.vertices[i + 1];
// check if they are equal (one of the vertices)
if (inclusive && (p.equals(p1) || p.equals(p2))) {
return true;
}
// do side of line test
double location = Segment.getLocation(p, p1, p2);
// check for on the edge
if (!inclusive && location == 0.0) {
return false;
}
// multiply the last location with this location
// if they are the same sign then the opertation will yield a positive result
// -x * -y = +xy, x * y = +xy, -x * y = -xy, x * -y = -xy
if (last * location < 0) {
// reminder: (-0.0 < 0.0) evaluates to false and not true
return false;
}
// update the last location, but only if it's not zero
// a location of zero indicates that the point lies ON the line
// through p1 and p2. We can ignore these values because the
// convexity requirement of the shape will ensure that if it's
// outside, a sign will change.
if (Math.abs(location) > Epsilon.E) {
last = location;
}
}
return true;
}
/* (non-Javadoc)
* @see org.dyn4j.geometry.AbstractShape#rotate(org.dyn4j.geometry.Rotation, double, double)
*/
@Override
public void rotate(Rotation rotation, double x, double y) {
super.rotate(rotation, x, y);
int size = this.vertices.length;
for (int i = 0; i < size; i++) {
this.vertices[i].rotate(rotation, x, y);
this.normals[i].rotate(rotation);
}
}
/* (non-Javadoc)
* @see org.dyn4j.geometry.AbstractShape#translate(double, double)
*/
@Override
public void translate(double x, double y) {
super.translate(x, y);
int size = this.vertices.length;
for (int i = 0; i < size; i++) {
this.vertices[i].add(x, y);
}
}
/* (non-Javadoc)
* @see org.dyn4j.geometry.Shape#project(org.dyn4j.geometry.Vector2, org.dyn4j.geometry.Transform)
*/
@Override
public Interval project(Vector2 vector, Transform transform) {
double v = 0.0;
// get the first point
Vector2 p = transform.getTransformed(this.vertices[0]);
// project the point onto the vector
double min = vector.dot(p);
double max = min;
// loop over the rest of the vertices
int size = this.vertices.length;
for(int i = 1; i < size; i++) {
// get the next point
p = transform.getTransformed(this.vertices[i]);
// project it onto the vector
v = vector.dot(p);
if (v < min) {
min = v;
} else if (v > max) {
max = v;
}
}
return new Interval(min, max);
}
/* (non-Javadoc)
* @see org.dyn4j.geometry.Convex#getFarthestFeature(org.dyn4j.geometry.Vector2, org.dyn4j.geometry.Transform)
*/
@Override
public EdgeFeature getFarthestFeature(Vector2 vector, Transform transform) {
// transform the normal into local space
Vector2 localn = transform.getInverseTransformedR(vector);
int index = getFarthestVertexIndex(localn);
int count = this.vertices.length;
Vector2 maximum = new Vector2(this.vertices[index]);
// once we have the point of maximum
// see which edge is most perpendicular
Vector2 leftN = this.normals[index == 0 ? count - 1 : index - 1];
Vector2 rightN = this.normals[index];
// create the maximum point for the feature (transform the maximum into world space)
transform.transform(maximum);
PointFeature vm = new PointFeature(maximum, index);
// is the left or right edge more perpendicular?
if (leftN.dot(localn) < rightN.dot(localn)) {
int l = (index == count - 1) ? 0 : index + 1;
Vector2 left = transform.getTransformed(this.vertices[l]);
PointFeature vl = new PointFeature(left, l);
// make sure the edge is the right winding
return new EdgeFeature(vm, vl, vm, maximum.to(left), index + 1);
} else {
int r = (index == 0) ? count - 1 : index - 1;
Vector2 right = transform.getTransformed(this.vertices[r]);
PointFeature vr = new PointFeature(right, r);
// make sure the edge is the right winding
return new EdgeFeature(vr, vm, vm, right.to(maximum), index);
}
}
/* (non-Javadoc)
* @see org.dyn4j.geometry.Convex#getFarthestPoint(org.dyn4j.geometry.Vector2, org.dyn4j.geometry.Transform)
*/
@Override
public Vector2 getFarthestPoint(Vector2 vector, Transform transform) {
// transform the normal into local space
Vector2 localn = transform.getInverseTransformedR(vector);
// find the index of the farthest point
int index = getFarthestVertexIndex(localn);
// transform the point into world space and return
return transform.getTransformed(this.vertices[index]);
}
/**
* Internal helper method that returns the index of the point that is
* farthest in direction of a vector.
*
* @param vector the direction
* @return the index of the farthest vertex in that direction
* @since 3.4.0
*/
int getFarthestVertexIndex(Vector2 vector) {
/*
* The sequence a(n) = vector.dot(vertices[n]) has a maximum, a minimum and is monotonic (though not strictly monotonic) between those extrema.
* All indices are considered in modular arithmetic. I choose the initial index to be 0.
*
* Based on that I follow this approach:
* We start from an initial index n0. We want to an adjacent to n0 index n1 for which a(n1) > a(n0).
* If no such index exists then n0 is the maximum. Else we start in direction of n1 (i.e. left or right of n0)
* and while a(n) increases we continue to that direction. When the next number of the sequence does not increases anymore
* we can stop and we have found max{a(n)}.
*
* Although the idea is simple we need to be careful with some edge cases and the correctness of the algorithm in all cases.
* Although the sequence is not strictly monotonic the absence of equalities is intentional and wields the correct answer (see below).
*
* The correctness of this method relies on some properties:
* 1) If n0 and n1 are two adjacent indices and a(n0) = a(n1) then a(n0) and a(n1) are either max{a(n)} or min{a(n)}.
* This holds for all convex polygons. This property can guarantee that if our initial index is n0 or n1 then it does not
* matter to which side (left or right) we start searching.
* 2) The polygon has no coincident vertices.
* This guarantees us that there are no adjacent n0, n1, n2 for which a(n0) = a(n1) = a(n2)
* and that only two adjacent n0, n1 can exist with a(n0) = a(n1). This is important because if
* those adjacent n0, n1, n2 existed the code below would always return the initial index, without knowing if
* it's a minimum or maximum. But since only two adjacent indices can exist with a(n0) = a(n1) the code below
* will always start searching in one direction and because of 1) this will give us the correct answer.
*/
// The initial starting index and the corresponding dot product
int maxIndex = 0;
int n = this.vertices.length;
double max = vector.dot(this.vertices[0]), candidateMax;
if (max < (candidateMax = vector.dot(this.vertices[1]))) {
// Search to the right
do {
max = candidateMax;
maxIndex++;
} while ((maxIndex + 1) < n && max < (candidateMax = vector.dot(this.vertices[maxIndex + 1])));
} else if (max < (candidateMax = vector.dot(this.vertices[n - 1]))) {
maxIndex = n; // n = 0 (mod n)
// Search to the left
do {
max = candidateMax;
maxIndex--;
} while (maxIndex > 0 && max <= (candidateMax = vector.dot(this.vertices[maxIndex - 1])));
// ,----------^^
// The equality here makes this algorithm produce the same results with the old when there exist adjacent vertices
// with the same a(n).
}
// else maxIndex = 0, because if neither of the above conditions is met, then the initial index is the maximum
return maxIndex;
}
/**
* Creates a {@link Mass} object using the geometric properties of
* this {@link Polygon} and the given density.
*
* A {@link Polygon}'s centroid must be computed by the area weighted method since the
* average method can be bias to one side if there are more points on that one
* side than another.
*
* Finding the area of a {@link Polygon} can be done by using the following
* summation:
*
0.5 * ∑(xi * yi + 1 - xi + 1 * yi)
* Finding the area weighted centroid can be done by using the following
* summation:
* 1 / (6 * A) * ∑(pi + pi + 1) * (xi * yi + 1 - xi + 1 * yi)
* Finding the inertia tensor can by done by using the following equation:
*
* ∑(pi + 1 x pi) * (pi2 + pi · pi + 1 + pi + 12)
* m / 6 * -------------------------------------------
* ∑(pi + 1 x pi)
*
* Where the mass is computed by:
* d * area
* @param density the density in kg/m2
* @return {@link Mass} the {@link Mass} of this {@link Polygon}
*/
@Override
public Mass createMass(double density) {
// can't use normal centroid calculation since it will be weighted towards sides
// that have larger distribution of points.
Vector2 center = new Vector2();
double area = 0.0;
double I = 0.0;
int n = this.vertices.length;
// get the average center
Vector2 ac = new Vector2();
for (int i = 0; i < n; i++) {
ac.add(this.vertices[i]);
}
ac.divide(n);
// loop through the vertices using two variables to avoid branches in the loop
for (int i1 = n - 1, i2 = 0; i2 < n; i1 = i2++) {
// get two vertices
Vector2 p1 = this.vertices[i1];
Vector2 p2 = this.vertices[i2];
// get the vector from the center to the point
p1 = p1.difference(ac);
p2 = p2.difference(ac);
// perform the cross product (yi * x(i+1) - y(i+1) * xi)
double D = p1.cross(p2);
// multiply by half
double triangleArea = 0.5 * D;
// add it to the total area
area += triangleArea;
// area weighted centroid
// (p1 + p2) * (D / 6)
// = (x1 + x2) * (yi * x(i+1) - y(i+1) * xi) / 6
// we will divide by the total area later
center.x += (p1.x + p2.x) * Geometry.INV_3 * triangleArea;
center.y += (p1.y + p2.y) * Geometry.INV_3 * triangleArea;
// (yi * x(i+1) - y(i+1) * xi) * (p2^2 + p2 . p1 + p1^2)
I += triangleArea * (p2.dot(p2) + p2.dot(p1) + p1.dot(p1));
// we will do the m / 6A = (d / 6) when we have the area summed up
}
// compute the mass
double m = density * area;
// finish the centroid calculation by dividing by the total area
// and adding in the average center
center.divide(area);
Vector2 c = center.sum(ac);
// finish the inertia tensor by dividing by the total area and multiplying by d / 6
I *= (density / 6.0);
// shift the axis of rotation to the area weighted center
// (center is the vector from the average center to the area weighted center since
// the average center is used as the origin)
I -= m * center.getMagnitudeSquared();
return new Mass(c, m, I);
}
/* (non-Javadoc)
* @see org.dyn4j.geometry.Shape#getArea()
*/
@Override
public double getArea() {
double area = 0.0;
int n = this.vertices.length;
// get the average center
Vector2 ac = new Vector2();
for (int i = 0; i < n; i++) {
ac.add(this.vertices[i]);
}
ac.divide(n);
// loop through the vertices using two variables to avoid branches in the loop
for (int i1 = n - 1, i2 = 0; i2 < n; i1 = i2++) {
// get two vertices
Vector2 p1 = this.vertices[i1];
Vector2 p2 = this.vertices[i2];
// get the vector from the center to the point
p1 = p1.difference(ac);
p2 = p2.difference(ac);
// perform the cross product (yi * x(i+1) - y(i+1) * xi)
double D = p1.cross(p2);
// multiply by half
double triangleArea = 0.5 * D;
// add it to the total area
area += triangleArea;
}
return area;
}
/* (non-Javadoc)
* @see org.dyn4j.geometry.Shape#computeAABB(org.dyn4j.geometry.Transform, org.dyn4j.geometry.AABB)
*/
@Override
public void computeAABB(Transform transform, AABB aabb) {
// get the first point
Vector2 p = transform.getTransformed(this.vertices[0]);
// initialize min and max values
double minX = p.x;
double maxX = p.x;
double minY = p.y;
double maxY = p.y;
// loop over the rest of the vertices
int size = this.vertices.length;
for(int i = 1; i < size; i++) {
// get the next point p = transform.getTransformed(this.vertices[i]);
double px = transform.getTransformedX(this.vertices[i]);
double py = transform.getTransformedY(this.vertices[i]);
// compare the x values
if (px < minX) {
minX = px;
} else if (px > maxX) {
maxX = px;
}
// compare the y values
if (py < minY) {
minY = py;
} else if (py > maxY) {
maxY = py;
}
}
aabb.minX = minX;
aabb.minY = minY;
aabb.maxX = maxX;
aabb.maxY = maxY;
}
}