
org.dyn4j.geometry.HalfEllipse Maven / Gradle / Ivy
/*
* Copyright (c) 2010-2016 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 dyn4j 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 OWNER 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 org.dyn4j.DataContainer;
import org.dyn4j.resources.Messages;
/**
* Implementation of an Half-Ellipse {@link Convex} {@link Shape}.
*
* A half ellipse must have a width and height greater than zero and the height parameter is the height of the half.
*
* This shape is only supported by the GJK collision detection algorithm.
*
* An UnsupportedOperationException
is thrown when this shape is used with SAT. If you are using
* or are planning on using the SAT collision detection algorithm, you can use the
* {@link Geometry#createPolygonalHalfEllipse(int, double, double)} method to create a half ellipse
* {@link Polygon} approximation. Another option is to use the GJK or your own collision detection
* algorithm for this shape only and use SAT on others.
* @author William Bittle
* @version 3.2.3
* @since 3.1.7
*/
public class HalfEllipse extends AbstractShape implements Convex, Shape, Transformable, DataContainer {
/**
* The half ellipse inertia constant.
* @see Elliptical Half
*/
private static final double INERTIA_CONSTANT = Math.PI / 8.0 - 8.0 / (9.0 * Math.PI);
/** The ellipse width */
final double width;
/** The ellipse height */
final double height;
/** The half-width */
final double halfWidth;
/** A local vector to */
public final Vector2 localXAxis;
/** The ellipse center */
final Vector2 ellipseCenter;
/** The vertices of the bottom */
final Vector2[] vertices;
/**
* Validated constructor.
*
* This creates an axis-aligned half ellipse fitting inside a rectangle
* of the given width and height.
* @param valid always true or this constructor would not be called
* @param width the width
* @param center the center
* @param vertices the vertices
*/
private HalfEllipse(boolean valid, double width, double height, Vector2 center, Vector2[] vertices) {
super(center, center.distance(vertices[1]));
// set width and height
this.width = width;
this.height = height;
// compute the major and minor axis lengths
// (the x,y radii)
this.halfWidth = width * 0.5;
// set the ellipse center
this.ellipseCenter = new Vector2();
// since we create ellipses as axis aligned we set the local x axis
// to the world space x axis
this.localXAxis = new Vector2(1.0, 0.0);
// setup the vertices
this.vertices = vertices;
}
/**
* Minimal constructor.
*
* This creates an axis-aligned half ellipse fitting inside a rectangle
* of the given width and height.
* @param width the width
* @param height the height of the half
* @throws IllegalArgumentException if either the width or height is less than or equal to zero
*/
public HalfEllipse(double width, double height) {
this(
validate(width, height),
width,
height,
new Vector2(0, (4.0 * height) / (3.0 * Math.PI)),
new Vector2[] {
// the left point
new Vector2(-width * 0.5, 0),
// the right point
new Vector2( width * 0.5, 0)
});
}
/**
* Validates the constructor input returning true if valid or throwing an exception if invalid.
* @param width the bounding rectangle width
* @param height the bounding rectangle height
* @return boolean true
* @throws IllegalArgumentException if either the width or height is less than or equal to zero
*/
private static final boolean validate(double width, double height) {
// validate the width and height
if (width <= 0.0) throw new IllegalArgumentException(Messages.getString("geometry.halfEllipse.invalidWidth"));
if (height <= 0.0) throw new IllegalArgumentException(Messages.getString("geometry.halfEllipse.invalidHeight"));
return true;
}
/* (non-Javadoc)
* @see org.dyn4j.geometry.Wound#toString()
*/
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
sb.append("HalfEllipse[").append(super.toString())
.append("|Width=").append(this.width)
.append("|Height=").append(this.height)
.append("]");
return sb.toString();
}
/**
* {@inheritDoc}
*
* This method is not supported by this shape.
* @throws UnsupportedOperationException when called
*/
@Override
public Vector2[] getAxes(Vector2[] foci, Transform transform) {
// this shape is not supported by SAT
throw new UnsupportedOperationException(Messages.getString("geometry.halfEllipse.satNotSupported"));
}
/**
* {@inheritDoc}
*
* This method is not supported by this shape.
* @throws UnsupportedOperationException when called
*/
@Override
public Vector2[] getFoci(Transform transform) {
// this shape is not supported by SAT
throw new UnsupportedOperationException(Messages.getString("geometry.halfEllipse.satNotSupported"));
}
/* (non-Javadoc)
* @see org.dyn4j.geometry.Convex#getFarthestPoint(org.dyn4j.geometry.Vector2, org.dyn4j.geometry.Transform)
*/
@Override
public Vector2 getFarthestPoint(Vector2 vector, Transform transform) {
// convert the world space vector(n) to local space
Vector2 localAxis = transform.getInverseTransformedR(vector);
// include local rotation
double r = this.getRotation();
// invert the local rotation
localAxis.rotate(-r);
// an ellipse is a circle with a non-uniform scaling transformation applied
// so we can achieve that by scaling the input axis by the major and minor
// axis lengths
localAxis.x *= this.halfWidth;
localAxis.y *= this.height;
// then normalize it
localAxis.normalize();
Vector2 p = null;
if (localAxis.y <= 0 && localAxis.x >= 0) {
return transform.getTransformed(this.vertices[1]);
} else if (localAxis.y <= 0 && localAxis.x <= 0) {
return transform.getTransformed(this.vertices[0]);
} else {
// add the radius along the vector to the center to get the farthest point
p = new Vector2(localAxis.x * this.halfWidth, localAxis.y * this.height);
}
// include local rotation
// invert the local rotation
p.rotate(r);
p.add(this.ellipseCenter);
// then finally convert back into world space coordinates
transform.transform(p);
return p;
}
/* (non-Javadoc)
* @see org.dyn4j.geometry.Convex#getFarthestFeature(org.dyn4j.geometry.Vector2, org.dyn4j.geometry.Transform)
*/
@Override
public Feature getFarthestFeature(Vector2 vector, Transform transform) {
Vector2 localAxis = transform.getInverseTransformedR(vector);
if (localAxis.getAngleBetween(this.localXAxis) < 0) {
// then its the farthest point
Vector2 point = this.getFarthestPoint(vector, transform);
return new PointFeature(point);
} else {
// return the full bottom side
return Segment.getFarthestFeature(this.vertices[0], this.vertices[1], vector, transform);
}
}
/* (non-Javadoc)
* @see org.dyn4j.geometry.Shape#project(org.dyn4j.geometry.Vector2, org.dyn4j.geometry.Transform)
*/
@Override
public Interval project(Vector2 vector, Transform transform) {
// get the world space farthest point
Vector2 p1 = this.getFarthestPoint(vector, transform);
Vector2 p2 = this.getFarthestPoint(vector.getNegative(), transform);
// project the point onto the axis
double d1 = p1.dot(vector);
double d2 = p2.dot(vector);
// get the interval along the axis
return new Interval(d2, d1);
}
/* (non-Javadoc)
* @see org.dyn4j.geometry.Shape#createAABB(org.dyn4j.geometry.Transform)
*/
@Override
public AABB createAABB(Transform transform) {
Interval x = this.project(Vector2.X_AXIS, transform);
Interval y = this.project(Vector2.Y_AXIS, transform);
return new AABB(x.getMin(), y.getMin(), x.getMax(), y.getMax());
}
/* (non-Javadoc)
* @see org.dyn4j.geometry.Shape#createMass(double)
*/
@Override
public Mass createMass(double density) {
double area = Math.PI * this.halfWidth * this.height;
double m = area * density * 0.5;
// moment of inertia given by: http://www.efunda.com/math/areas/ellipticalhalf.cfm
double I = m * (this.halfWidth * this.halfWidth + this.height * this.height) * INERTIA_CONSTANT;
return new Mass(this.center, m, I);
}
/* (non-Javadoc)
* @see org.dyn4j.geometry.Shape#getRadius(org.dyn4j.geometry.Vector2)
*/
@Override
public double getRadius(Vector2 center) {
// annoyingly, finding the radius of a rotated/translated ellipse
// about another point is the same as finding the farthest point
// from an arbitrary point. The solution to this is a quartic function
// that has no analytic solution, so we are stuck with root finding.
// Thankfully, this method shouldn't be called that often, in fact
// it should only be called when the user modifies the shapes on a body.
// in the half ellipse case, if the point is on the side of the flat edge
// then we do the ellipse code, else we can just return the farthest of the
// two points that make up the flat side
if (Segment.getLocation(center, this.vertices[0], this.vertices[1]) > 0) {
return Geometry.getRotationRadius(center, vertices);
} else {
// we need to translate/rotate the point so that this ellipse is
// considered centered at the origin with it's semi-major axis aligned
// with the x-axis and its semi-minor axis aligned with the y-axis
Vector2 p = center.difference(this.ellipseCenter).rotate(-this.getRotation());
// get the farthest point.
Vector2 fp = Ellipse.getFarthestPoint(this.halfWidth, this.height, p);
// get the distance between the two points. The distance will be the
// same if we translate/rotate the points back to the real position
// and rotation, so don't bother
return p.distance(fp);
}
}
/* (non-Javadoc)
* @see org.dyn4j.geometry.Shape#contains(org.dyn4j.geometry.Vector2, org.dyn4j.geometry.Transform)
*/
@Override
public boolean contains(Vector2 point, Transform transform) {
// equation of an ellipse:
// (x - h)^2/a^2 + (y - k)^2/b^2 = 1
// for a point to be inside the ellipse, we can plug in
// the point into this equation and verify that the value
// is less than or equal to one
// get the world space point into local coordinates
Vector2 localPoint = transform.getInverseTransformed(point);
// account for local rotation
double r = this.getRotation();
localPoint.rotate(-r, this.ellipseCenter.x, this.ellipseCenter.y);
// translate into local coordinates
double x = (localPoint.x - this.ellipseCenter.x);
double y = (localPoint.y - this.ellipseCenter.y);
// for half ellipse we have an early out
if (y < 0) return false;
double x2 = x * x;
double y2 = y * y;
double a2 = this.halfWidth * this.halfWidth;
double b2 = this.height * this.height;
double value = x2 / a2 + y2 / b2;
if (value <= 1.0) {
return true;
}
return false;
}
/* (non-Javadoc)
* @see org.dyn4j.geometry.AbstractShape#rotate(double, double, double)
*/
@Override
public void rotate(double theta, double x, double y) {
super.rotate(theta, x, y);
// rotate the local axis as well
this.localXAxis.rotate(theta);
// rotate the vertices
for (int i = 0; i < this.vertices.length; i++) {
this.vertices[i].rotate(theta, x, y);
}
// rotate the ellipse center
this.ellipseCenter.rotate(theta, x, y);
}
/* (non-Javadoc)
* @see org.dyn4j.geometry.AbstractShape#translate(double, double)
*/
@Override
public void translate(double x, double y) {
// translate the centroid
super.translate(x, y);
// translate the pie vertices
for (int i = 0; i < this.vertices.length; i++) {
this.vertices[i].add(x, y);
}
// translate the ellipse center
this.ellipseCenter.add(x, y);
}
/**
* Returns the rotation about the local center in radians.
* @return double the rotation in radians
*/
public double getRotation() {
return Vector2.X_AXIS.getAngleBetween(this.localXAxis);
}
/**
* Returns the width.
* @return double
*/
public double getWidth() {
return this.width;
}
/**
* Returns the height.
* @return double
*/
public double getHeight() {
return this.height;
}
/**
* Returns the half width.
* @return double
*/
public double getHalfWidth() {
return this.halfWidth;
}
/**
* Returns the center of the ellipse.
* @return {@link Vector2}
*/
public Vector2 getEllipseCenter() {
return this.ellipseCenter;
}
}