package org.newdawn.slick.geom; import java.io.Serializable; /** * The description of any 2D shape that can be transformed. The points provided approximate the intent * of the shape. * * @author Mark */ public abstract class Shape implements Serializable { /** The points representing this polygon. */ protected float points[]; /** Center point of the polygon. */ protected float center[]; /** The left most point of this shape. */ protected float x; /** The top most point of this shape. */ protected float y; /** The right most point of this shape */ protected float maxX; /** The bottom most point of this shape */ protected float maxY; /** The left most point of this shape. */ protected float minX; /** The top most point of this shape. */ protected float minY; /** Radius of a circle that can completely enclose this shape. */ protected float boundingCircleRadius; /** Flag to tell whether points need to be generated */ protected boolean pointsDirty; /** The triangles that define the shape */ protected Triangulator tris; /** True if the triangles need updating */ protected boolean trianglesDirty; /** * Shape constructor. * */ public Shape() { pointsDirty = true; } /** * Set the location of this shape * * @param x The x coordinate of the new location of the shape * @param y The y coordinate of the new location of the shape */ public void setLocation(float x, float y) { setX(x); setY(y); } /** * Apply a transformation and return a new shape. This will not alter the current shape but will * return the transformed shape. * * @param transform The transform to be applied * @return The transformed shape. */ public abstract Shape transform(Transform transform); /** * Subclasses implement this to create the points of the shape. * */ protected abstract void createPoints(); /** * Get the x location of the left side of this shape. * * @return The x location of the left side of this shape. */ public float getX() { return x; } /** * Set the x position of the left side this shape. * * @param x The new x position of the left side this shape. */ public void setX(float x) { if (x != this.x) { float dx = x - this.x; this.x = x; if ((points == null) || (center == null)) { checkPoints(); } // update the points in the special case for (int i=0;i<points.length/2;i++) { points[i*2] += dx; } center[0] += dx; x += dx; maxX += dx; minX += dx; trianglesDirty = true; } } /** * Set the y position of the top of this shape. * * @param y The new y position of the top of this shape. */ public void setY(float y) { if (y != this.y) { float dy = y - this.y; this.y = y; if ((points == null) || (center == null)) { checkPoints(); } // update the points in the special case for (int i=0;i<points.length/2;i++) { points[(i*2)+1] += dy; } center[1] += dy; y += dy; maxY += dy; minY += dy; trianglesDirty = true; } } /** * Get the y position of the top of this shape. * * @return The y position of the top of this shape. */ public float getY() { return y; } /** * Set the location of this shape * * @param loc The new location of the shape */ public void setLocation(Vector2f loc) { setX(loc.x); setY(loc.y); } /** * Get the x center of this shape. * * @return The x center of this shape. */ public float getCenterX() { checkPoints(); return center[0]; } /** * Set the x center of this shape. * * @param centerX The center point to set. */ public void setCenterX(float centerX) { if ((points == null) || (center == null)) { checkPoints(); } float xDiff = centerX - getCenterX(); setX(x + xDiff); } /** * Get the y center of this shape. * * @return The y center of this shape. */ public float getCenterY() { checkPoints(); return center[1]; } /** * Set the y center of this shape. * * @param centerY The center point to set. */ public void setCenterY(float centerY) { if ((points == null) || (center == null)) { checkPoints(); } float yDiff = centerY - getCenterY(); setY(y + yDiff); } /** * Get the right most point of this shape. * * @return The right most point of this shape. */ public float getMaxX() { checkPoints(); return maxX; } /** * Get the bottom most point of this shape. * * @return The bottom most point of this shape. */ public float getMaxY() { checkPoints(); return maxY; } /** * Get the left most point of this shape. * * @return The left most point of this shape. */ public float getMinX() { checkPoints(); return minX; } /** * Get the top most point of this shape. * * @return The top most point of this shape. */ public float getMinY() { checkPoints(); return minY; } /** * Get the radius of a circle that can completely enclose this shape. * * @return The radius of the circle. */ public float getBoundingCircleRadius() { checkPoints(); return boundingCircleRadius; } /** * Get the point closet to the center of all the points in this Shape * * @return The x,y coordinates of the center. */ public float[] getCenter() { checkPoints(); return center; } /** * Get the points that outline this shape. Use CW winding rule * * @return an array of x,y points */ public float[] getPoints() { checkPoints(); return points; } /** * Get the number of points in this polygon * * @return The number of points in this polygon */ public int getPointCount() { checkPoints(); return points.length / 2; } /** * Get a single point in this polygon * * @param index The index of the point to retrieve * @return The point's coordinates */ public float[] getPoint(int index) { checkPoints(); float result[] = new float[2]; result[0] = points[index * 2]; result[1] = points[index * 2 + 1]; return result; } /** * Get the combine normal of a given point * * @param index The index of the point whose normal should be retrieved * @return The combined normal of a given point */ public float[] getNormal(int index) { float[] current = getPoint(index); float[] prev = getPoint(index - 1 < 0 ? getPointCount() - 1 : index - 1); float[] next = getPoint(index + 1 >= getPointCount() ? 0 : index + 1); float[] t1 = getNormal(prev, current); float[] t2 = getNormal(current, next); if ((index == 0) && (!closed())) { return t2; } if ((index == getPointCount()-1) && (!closed())) { return t1; } float tx = (t1[0]+t2[0])/2; float ty = (t1[1]+t2[1])/2; float len = (float) Math.sqrt((tx*tx)+(ty*ty)); return new float[] {tx/len,ty/len}; } /** * Check if the shape passed is entirely contained within * this shape. * * @param other The other shape to test against this one * @return True if the other shape supplied is entirely contained * within this one. */ public boolean contains(Shape other) { if (other.intersects(this)) { return false; } for (int i=0;i<other.getPointCount();i++) { float[] pt = other.getPoint(i); if (!contains(pt[0], pt[1])) { return false; } } return true; } /** * Get the normal of the line between two points * * @param start The start point * @param end The end point * @return The normal of the line between the two points */ private float[] getNormal(float[] start, float[] end) { float dx = start[0] - end[0]; float dy = start[1] - end[1]; float len = (float) Math.sqrt((dx*dx)+(dy*dy)); dx /= len; dy /= len; return new float[] {-dy,dx}; } /** * Check if the given point is part of the path that * forms this shape * * @param x The x position of the point to check * @param y The y position of the point to check * @return True if the point is includes in the path of the polygon */ public boolean includes(float x, float y) { if (points.length == 0) { return false; } checkPoints(); Line testLine = new Line(0,0,0,0); Vector2f pt = new Vector2f(x,y); for (int i=0;i<points.length;i+=2) { int n = i+2; if (n >= points.length) { n = 0; } testLine.set(points[i], points[i+1], points[n], points[n+1]); if (testLine.on(pt)) { return true; } } return false; } /** * Get the index of a given point * * @param x The x coordinate of the point * @param y The y coordinate of the point * @return The index of the point or -1 if the point is not part of this shape path */ public int indexOf(float x, float y) { for (int i=0;i<points.length;i+=2) { if ((points[i] == x) && (points[i+1] == y)) { return i / 2; } } return -1; } /** * Check if this polygon contains the given point * * @param x The x position of the point to check * @param y The y position of the point to check * @return True if the point is contained in the polygon */ public boolean contains(float x, float y) { checkPoints(); if (points.length == 0) { return false; } boolean result = false; float xnew,ynew; float xold,yold; float x1,y1; float x2,y2; int npoints = points.length; xold=points[npoints - 2]; yold=points[npoints - 1]; for (int i=0;i < npoints;i+=2) { xnew = points[i]; ynew = points[i + 1]; if (xnew > xold) { x1 = xold; x2 = xnew; y1 = yold; y2 = ynew; } else { x1 = xnew; x2 = xold; y1 = ynew; y2 = yold; } if ((xnew < x) == (x <= xold) /* edge "open" at one end */ && ((double)y - (double)y1) * (x2 - x1) < ((double)y2 - (double)y1) * (x - x1)) { result = !result; } xold = xnew; yold = ynew; } return result; } /** * Check if this shape intersects with the shape provided. * * @param shape The shape to check if it intersects with this one. * @return True if the shapes do intersect, false otherwise. */ public boolean intersects(Shape shape) { /* * Intersection formula used: * (x4 - x3)(y1 - y3) - (y4 - y3)(x1 - x3) * UA = --------------------------------------- * (y4 - y3)(x2 - x1) - (x4 - x3)(y2 - y1) * * (x2 - x1)(y1 - y3) - (y2 - y1)(x1 - x3) * UB = --------------------------------------- * (y4 - y3)(x2 - x1) - (x4 - x3)(y2 - y1) * * if UA and UB are both between 0 and 1 then the lines intersect. * * Source: http://local.wasp.uwa.edu.au/~pbourke/geometry/lineline2d/ */ checkPoints(); boolean result = false; float points[] = getPoints(); // (x3, y3) and (x4, y4) float thatPoints[] = shape.getPoints(); // (x1, y1) and (x2, y2) int length = points.length; int thatLength = thatPoints.length; double unknownA; double unknownB; if (!closed()) { length -= 2; } if (!shape.closed()) { thatLength -= 2; } // x1 = thatPoints[j] // x2 = thatPoints[j + 2] // y1 = thatPoints[j + 1] // y2 = thatPoints[j + 3] // x3 = points[i] // x4 = points[i + 2] // y3 = points[i + 1] // y4 = points[i + 3] for(int i=0;i<length;i+=2) { int iNext = i+2; if (iNext >= points.length) { iNext = 0; } for(int j=0;j<thatLength;j+=2) { int jNext = j+2; if (jNext >= thatPoints.length) { jNext = 0; } unknownA = (((points[iNext] - points[i]) * (double) (thatPoints[j + 1] - points[i + 1])) - ((points[iNext+1] - points[i + 1]) * (thatPoints[j] - points[i]))) / (((points[iNext+1] - points[i + 1]) * (thatPoints[jNext] - thatPoints[j])) - ((points[iNext] - points[i]) * (thatPoints[jNext+1] - thatPoints[j + 1]))); unknownB = (((thatPoints[jNext] - thatPoints[j]) * (double) (thatPoints[j + 1] - points[i + 1])) - ((thatPoints[jNext+1] - thatPoints[j + 1]) * (thatPoints[j] - points[i]))) / (((points[iNext+1] - points[i + 1]) * (thatPoints[jNext] - thatPoints[j])) - ((points[iNext] - points[i]) * (thatPoints[jNext+1] - thatPoints[j + 1]))); if(unknownA >= 0 && unknownA <= 1 && unknownB >= 0 && unknownB <= 1) { result = true; break; } } if(result) { break; } } return result; } /** * Check if a particular location is a vertex of this polygon * * @param x The x coordinate to check * @param y The y coordinate to check * @return True if the cordinates supplied are a vertex of this polygon */ public boolean hasVertex(float x, float y) { if (points.length == 0) { return false; } checkPoints(); for (int i=0;i<points.length;i+=2) { if ((points[i] == x) && (points[i+1] == y)) { return true; } } return false; } /** * Get the center of this polygon. * */ protected void findCenter() { center = new float[]{0, 0}; int length = points.length; for(int i=0;i<length;i+=2) { center[0] += points[i]; center[1] += points[i + 1]; } center[0] /= (length / 2); center[1] /= (length / 2); } /** * Calculate the radius of a circle that can completely enclose this shape. * */ protected void calculateRadius() { boundingCircleRadius = 0; for(int i=0;i<points.length;i+=2) { float temp = ((points[i] - center[0]) * (points[i] - center[0])) + ((points[i + 1] - center[1]) * (points[i + 1] - center[1])); boundingCircleRadius = (boundingCircleRadius > temp) ? boundingCircleRadius : temp; } boundingCircleRadius = (float)Math.sqrt(boundingCircleRadius); } /** * Calculate the triangles that can fill this shape */ protected void calculateTriangles() { if (!trianglesDirty) { return; } if (points.length >= 6) { boolean clockwise = true; float area = 0; for (int i=0;i<(points.length/2)-1;i++) { float x1 = points[(i*2)]; float y1 = points[(i*2)+1]; float x2 = points[(i*2)+2]; float y2 = points[(i*2)+3]; area += (x1 * y2) - (y1 * x2); } area /= 2; clockwise = area > 0; tris = new NeatTriangulator(); for (int i=0;i<points.length;i+=2) { tris.addPolyPoint(points[i], points[i+1]); } tris.triangulate(); } trianglesDirty = false; } /** * Increase triangulation */ public void increaseTriangulation() { checkPoints(); calculateTriangles(); tris = new OverTriangulator(tris); } /** * The triangles that define the filled version of this shape * * @return The triangles that define the */ public Triangulator getTriangles() { checkPoints(); calculateTriangles(); return tris; } /** * Check the dirty flag and create points as necessary. */ protected final void checkPoints() { if (pointsDirty) { createPoints(); findCenter(); calculateRadius(); maxX = points[0]; maxY = points[1]; minX = points[0]; minY = points[1]; for (int i=0;i<points.length/2;i++) { maxX = Math.max(points[i*2],maxX); maxY = Math.max(points[(i*2)+1],maxY); minX = Math.min(points[i*2],minX); minY = Math.min(points[(i*2)+1],minY); } pointsDirty = false; trianglesDirty = true; } } /** * Cause all internal state to be generated and cached */ public void preCache() { checkPoints(); getTriangles(); } /** * True if this is a closed shape * * @return True if this is a closed shape */ public boolean closed() { return true; } /** * Prune any required points in this shape * * @return The new shape with points pruned */ public Shape prune() { Polygon result = new Polygon(); for (int i=0;i<getPointCount();i++) { int next = i+1 >= getPointCount() ? 0 : i+1; int prev = i-1 < 0 ? getPointCount() - 1 : i-1; float dx1 = getPoint(i)[0] - getPoint(prev)[0]; float dy1 = getPoint(i)[1] - getPoint(prev)[1]; float dx2 = getPoint(next)[0] - getPoint(i)[0]; float dy2 = getPoint(next)[1] - getPoint(i)[1]; float len1 = (float) Math.sqrt((dx1*dx1) + (dy1*dy1)); float len2 = (float) Math.sqrt((dx2*dx2) + (dy2*dy2)); dx1 /= len1; dy1 /= len1; dx2 /= len2; dy2 /= len2; if ((dx1 != dx2) || (dy1 != dy2)) { result.addPoint(getPoint(i)[0],getPoint(i)[1]); } } return result; } /** * Subtract the given shape from this one. Note that this method only deals * with edges, it will not create holes in polygons. * * @param other The other shape to subtract from this one * @return The newly created set of shapes resulting from the operation */ public Shape[] subtract(Shape other) { return new GeomUtil().subtract(this, other); } /** * Join this shape with another. * * @param other The other shape to join with this one * @return The newly created set of shapes resulting from the operation */ public Shape[] union(Shape other) { return new GeomUtil().union(this, other); } /** * Get the width of the shape * * @return The width of the shape */ public float getWidth() { return maxX - minX; } /** * Get the height of the shape * * @return The height of the shape */ public float getHeight() { return maxY - minY; } }