diff --git a/src/main/java/pythagoras/f/AbstractArc.java b/src/main/java/pythagoras/f/AbstractArc.java new file mode 100644 index 0000000..d75befb --- /dev/null +++ b/src/main/java/pythagoras/f/AbstractArc.java @@ -0,0 +1,356 @@ +// +// $Id$ + +package pythagoras.f; + +import java.util.NoSuchElementException; + +/** + * Provides most of the implementation of {@link IArc}, obtaining only the frame and other metrics + * from the derived class. + */ +public abstract class AbstractArc extends RectangularShape implements IArc +{ + @Override // from interface IArc + public Point getStartPoint () { + return getStartPoint(new Point()); + } + + @Override // from interface IArc + public Point getStartPoint (Point target) { + float a = (float)Math.toRadians(getAngleStart()); + target.setLocation(getX() + (1f + (float)Math.cos(a)) * getWidth() / 2f, + getY() + (1f - (float)Math.sin(a)) * getHeight() / 2f); + return target; + } + + @Override // from interface IArc + public Point getEndPoint () { + return getEndPoint(new Point()); + } + + @Override // from interface IArc + public Point getEndPoint (Point target) { + float a = (float)Math.toRadians(getAngleStart() + getAngleExtent()); + target.setLocation(getX() + (1f + (float)Math.cos(a)) * getWidth() / 2f, + getY() + (1f - (float)Math.sin(a)) * getHeight() / 2f); + return target; + } + + @Override // from interface IArc + public boolean containsAngle (float angle) { + float extent = getAngleExtent(); + if (extent >= 360f) { + return true; + } + angle = getNormAngle(angle); + float a1 = getNormAngle(getAngleStart()); + float a2 = a1 + extent; + if (a2 > 360f) { + return angle >= a1 || angle <= a2 - 360f; + } + if (a2 < 0f) { + return angle >= a2 + 360f || angle <= a1; + } + return (extent > 0f) ? a1 <= angle && angle <= a2 : a2 <= angle && angle <= a1; + } + + @Override // from interface IArc + public Arc clone () { + return new Arc(getX(), getY(), getWidth(), getHeight(), getAngleStart(), getAngleExtent(), + getArcType()); + } + + @Override // from RectangularShape + public boolean isEmpty () { + return getArcType() == OPEN || super.isEmpty(); + } + + @Override // from RectangularShape + public boolean contains (float px, float py) { + // normalize point + float nx = (px - getX()) / getWidth() - 0.5f; + float ny = (py - getY()) / getHeight() - 0.5f; + if ((nx * nx + ny * ny) > 0.25) { + return false; + } + + float extent = getAngleExtent(); + float absExtent = Math.abs(extent); + if (absExtent >= 360f) { + return true; + } + + boolean containsAngle = containsAngle((float)Math.toDegrees(-Math.atan2(ny, nx))); + if (getArcType() == PIE) { + return containsAngle; + } + if (absExtent <= 180f && !containsAngle) { + return false; + } + + Line l = new Line(getStartPoint(), getEndPoint()); + int ccw1 = l.relativeCCW(px, py); + int ccw2 = l.relativeCCW(getCenterX(), getCenterY()); + return ccw1 == 0 || ccw2 == 0 || ((ccw1 + ccw2) == 0 ^ absExtent > 180f); + } + + @Override // from RectangularShape + public boolean contains (float rx, float ry, float rw, float rh) { + if (!(contains(rx, ry) && contains(rx + rw, ry) && + contains(rx + rw, ry + rh) && contains(rx, ry + rh))) { + return false; + } + + float absExtent = Math.abs(getAngleExtent()); + if (getArcType() != PIE || absExtent <= 180f || absExtent >= 360f) { + return true; + } + + Rectangle r = new Rectangle(rx, ry, rw, rh); + float cx = getCenterX(), cy = getCenterY(); + if (r.contains(cx, cy)) { + return false; + } + + Point p1 = getStartPoint(), p2 = getEndPoint(); + return !r.intersectsLine(cx, cy, p1.getX(), p1.getY()) && + !r.intersectsLine(cx, cy, p2.getX(), p2.getY()); + } + + @Override // from RectangularShape + public boolean intersects (float rx, float ry, float rw, float rh) { + if (isEmpty() || rw <= 0f || rh <= 0f) { + return false; + } + + // check: does arc contain rectangle's points + if (contains(rx, ry) || contains(rx + rw, ry) || + contains(rx, ry + rh) || contains(rx + rw, ry + rh)) { + return true; + } + + float cx = getCenterX(), cy = getCenterY(); + Point p1 = getStartPoint(), p2 = getEndPoint(); + + // check: does rectangle contain arc's points + Rectangle r = new Rectangle(rx, ry, rw, rh); + if (r.contains(p1) || r.contains(p2) || (getArcType() == PIE && r.contains(cx, cy))) { + return true; + } + + if (getArcType() == PIE) { + if (r.intersectsLine(p1.getX(), p1.getY(), cx, cy) || + r.intersectsLine(p2.getX(), p2.getY(), cx, cy)) { + return true; + } + } else { + if (r.intersectsLine(p1.getX(), p1.getY(), p2.getX(), p2.getY())) { + return true; + } + } + + // nearest rectangle point + float nx = cx < rx ? rx : (cx > rx + rw ? rx + rw : cx); + float ny = cy < ry ? ry : (cy > ry + rh ? ry + rh : cy); + return contains(nx, ny); + } + + @Override // from RectangularShape + public Rectangle getBounds (Rectangle target) { + if (isEmpty()) { + target.setBounds(getX(), getY(), getWidth(), getHeight()); + return target; + } + + float rx1 = getX(); + float ry1 = getY(); + float rx2 = rx1 + getWidth(); + float ry2 = ry1 + getHeight(); + + Point p1 = getStartPoint(), p2 = getEndPoint(); + + float bx1 = containsAngle(180f) ? rx1 : Math.min(p1.getX(), p2.getX()); + float by1 = containsAngle(90f) ? ry1 : Math.min(p1.getY(), p2.getY()); + float bx2 = containsAngle(0f) ? rx2 : Math.max(p1.getX(), p2.getX()); + float by2 = containsAngle(270f) ? ry2 : Math.max(p1.getY(), p2.getY()); + + if (getArcType() == PIE) { + float cx = getCenterX(); + float cy = getCenterY(); + bx1 = Math.min(bx1, cx); + by1 = Math.min(by1, cy); + bx2 = Math.max(bx2, cx); + by2 = Math.max(by2, cy); + } + target.setBounds(bx1, by1, bx2 - bx1, by2 - by1); + return target; + } + + @Override // from interface IShape + public PathIterator getPathIterator (AffineTransform at) { + return new Iterator(this, at); + } + + /** Returns a normalized angle (bound between 0 and 360 degrees). */ + protected float getNormAngle (float angle) { + return angle - (float)Math.floor(angle / 360f) * 360f; + } + + /** An iterator over an {@link IArc}. */ + protected static class Iterator implements PathIterator + { + /** The x coordinate of left-upper corner of the arc rectangle bounds */ + private float x; + + /** The y coordinate of left-upper corner of the arc rectangle bounds */ + private float y; + + /** The width of the arc rectangle bounds */ + private float width; + + /** The height of the arc rectangle bounds */ + private float height; + + /** The start angle of the arc in degrees */ + private float angle; + + /** The angle extent in degrees */ + private float extent; + + /** The closure type of the arc */ + private int type; + + /** The path iterator transformation */ + private AffineTransform t; + + /** The current segmenet index */ + private int index; + + /** The number of arc segments the source arc subdivided to be approximated by Bezier + * curves. Depends on extent value. */ + private int arcCount; + + /** The number of line segments. Depends on closure type. */ + private int lineCount; + + /** The step to calculate next arc subdivision point */ + private float step; + + /** The tempopary value of cosinus of the current angle */ + private float cos; + + /** The tempopary value of sinus of the current angle */ + private float sin; + + /** The coefficient to calculate control points of Bezier curves */ + private float k; + + /** The tempopary value of x coordinate of the Bezier curve control vector */ + private float kx; + + /** The tempopary value of y coordinate of the Bezier curve control vector */ + private float ky; + + /** The x coordinate of the first path point (MOVE_TO) */ + private float mx; + + /** The y coordinate of the first path point (MOVE_TO) */ + private float my; + + Iterator (IArc a, AffineTransform t) { + this.width = a.getWidth() / 2f; + this.height = a.getHeight() / 2f; + this.x = a.getX() + width; + this.y = a.getY() + height; + this.angle = -(float)Math.toRadians(a.getAngleStart()); + this.extent = -a.getAngleExtent(); + this.type = a.getArcType(); + this.t = t; + + if (width < 0 || height < 0) { + arcCount = 0; + lineCount = 0; + index = 1; + return; + } + + if (Math.abs(extent) >= 360f) { + arcCount = 4; + k = 4f / 3f * ((float)Math.sqrt(2f) - 1f); + step = (float)Math.PI / 2f; + if (extent < 0f) { + step = -step; + k = -k; + } + } else { + arcCount = (int)Math.rint(Math.abs(extent) / 90f); + step = (float)Math.toRadians(extent / arcCount); + k = 4f / 3f * (1f - (float)Math.cos(step / 2f)) / (float)Math.sin(step / 2f); + } + + lineCount = 0; + if (type == CHORD) { + lineCount++; + } else if (type == PIE) { + lineCount += 2; + } + } + + @Override public int getWindingRule () { + return WIND_NON_ZERO; + } + + @Override public boolean isDone () { + return index > arcCount + lineCount; + } + + @Override public void next () { + index++; + } + + @Override public int currentSegment (float[] coords) { + if (isDone()) { + throw new NoSuchElementException("Iterator out of bounds"); + } + int type; + int count; + if (index == 0) { + type = SEG_MOVETO; + count = 1; + cos = (float)Math.cos(angle); + sin = (float)Math.sin(angle); + kx = k * width * sin; + ky = k * height * cos; + coords[0] = mx = x + cos * width; + coords[1] = my = y + sin * height; + } else if (index <= arcCount) { + type = SEG_CUBICTO; + count = 3; + coords[0] = mx - kx; + coords[1] = my + ky; + angle += step; + cos = (float)Math.cos(angle); + sin = (float)Math.sin(angle); + kx = k * width * sin; + ky = k * height * cos; + coords[4] = mx = x + cos * width; + coords[5] = my = y + sin * height; + coords[2] = mx + kx; + coords[3] = my - ky; + } else if (index == arcCount + lineCount) { + type = SEG_CLOSE; + count = 0; + } else { + type = SEG_LINETO; + count = 1; + coords[0] = x; + coords[1] = y; + } + if (t != null) { + t.transform(coords, 0, coords, 0, count); + } + return type; + } + } +} diff --git a/src/main/java/pythagoras/f/Arc.java b/src/main/java/pythagoras/f/Arc.java new file mode 100644 index 0000000..2ec1f46 --- /dev/null +++ b/src/main/java/pythagoras/f/Arc.java @@ -0,0 +1,239 @@ +// +// $Id$ + +package pythagoras.f; + +import java.io.Serializable; + +/** + * Represents an arc defined by a framing rectangle, start angle, angular extend, and closure type. + */ +public class Arc extends AbstractArc implements Serializable +{ + /** The x-coordinate of this arc's framing rectangle. */ + public float x; + + /** The y-coordinate of this arc's framing rectangle. */ + public float y; + + /** The width of this arc's framing rectangle. */ + public float width; + + /** The height of this arc's framing rectangle. */ + public float height; + + /** The starting angle of this arc. */ + public float start; + + /** The angular extent of this arc. */ + public float extent; + + /** + * Creates an open arc with frame (0x0+0+0) and zero angles. + */ + public Arc () { + this(OPEN); + } + + /** + * Creates an arc of the specified type with frame (0x0+0+0) and zero angles. + */ + public Arc (int type) { + setArcType(type); + } + + /** + * Creates an arc of the specified type with the specified framing rectangle, starting angle + * and angular extent. + */ + public Arc (float x, float y, float width, float height, float start, float extent, int type) { + setArc(x, y, width, height, start, extent, type); + } + + /** + * Creates an arc of the specified type with the supplied framing rectangle, starting angle and + * angular extent. + */ + public Arc (IRectangle bounds, float start, float extent, int type) { + setArc(bounds.getX(), bounds.getY(), bounds.getWidth(), bounds.getHeight(), + start, extent, type); + } + + @Override // from interface IArc + public int getArcType () { + return type; + } + + @Override // from interface IArc + public float getX () { + return x; + } + + @Override // from interface IArc + public float getY () { + return y; + } + + @Override // from interface IArc + public float getWidth () { + return width; + } + + @Override // from interface IArc + public float getHeight () { + return height; + } + + @Override // from interface IArc + public float getAngleStart () { + return start; + } + + @Override // from interface IArc + public float getAngleExtent () { + return extent; + } + + /** + * Sets the type of this arc to the specified value. + */ + public void setArcType (int type) { + if (type != OPEN && type != CHORD && type != PIE) { + throw new IllegalArgumentException("Invalid Arc type: " + type); + } + this.type = type; + } + + /** + * Sets the starting angle of this arc to the specified value. + */ + public void setAngleStart (float start) { + this.start = start; + } + + /** + * Sets the angular extent of this arc to the specified value. + */ + public void setAngleExtent (float extent) { + this.extent = extent; + } + + /** + * Sets the location, size, angular extents, and closure type of this arc to the specified + * values. + */ + public void setArc (float x, float y, float width, float height, + float start, float extent, int type) { + setArcType(type); + this.x = x; + this.y = y; + this.width = width; + this.height = height; + this.start = start; + this.extent = extent; + } + + /** + * Sets the location, size, angular extents, and closure type of this arc to the specified + * values. + */ + public void setArc (IPoint point, IDimension size, float start, float extent, int type) { + setArc(point.getX(), point.getY(), size.getWidth(), size.getHeight(), start, extent, type); + } + + /** + * Sets the location, size, angular extents, and closure type of this arc to the specified + * values. + */ + public void setArc (IRectangle rect, float start, float extent, int type) { + setArc(rect.getX(), rect.getY(), rect.getWidth(), rect.getHeight(), start, extent, type); + } + + /** + * Sets the location, size, angular extents, and closure type of this arc to the same values as + * the supplied arc. + */ + public void setArc (IArc arc) { + setArc(arc.getX(), arc.getY(), arc.getWidth(), arc.getHeight(), arc.getAngleStart(), + arc.getAngleExtent(), arc.getArcType()); + } + + /** + * Sets the location, size, angular extents, and closure type of this arc based on the + * specified values. + */ + public void setArcByCenter (float x, float y, float radius, + float start, float extent, int type) { + setArc(x - radius, y - radius, radius * 2f, radius * 2f, start, extent, type); + } + + /** + * Sets the location, size, angular extents, and closure type of this arc based on the + * specified values. + */ + public void setArcByTangent (IPoint p1, IPoint p2, IPoint p3, float radius) { + // use simple geometric calculations of arc center, radius and angles by tangents + float a1 = -(float)Math.atan2(p1.getY() - p2.getY(), p1.getX() - p2.getX()); + float a2 = -(float)Math.atan2(p3.getY() - p2.getY(), p3.getX() - p2.getX()); + float am = (a1 + a2) / 2f; + float ah = a1 - am; + float d = radius / Math.abs((float)Math.sin(ah)); + float x = p2.getX() + d * (float)Math.cos(am); + float y = p2.getY() - d * (float)Math.sin(am); + ah = ah >= 0f ? (float)Math.PI * 1.5f - ah : (float)Math.PI * 0.5f - ah; + a1 = getNormAngle((float)Math.toDegrees(am - ah)); + a2 = getNormAngle((float)Math.toDegrees(am + ah)); + float delta = a2 - a1; + if (delta <= 0f) { + delta += 360f; + } + setArcByCenter(x, y, radius, a1, delta, type); + } + + /** + * Sets the starting angle of this arc to the angle defined by the supplied point relative to + * the center of this arc. + */ + public void setAngleStart (IPoint point) { + float angle = (float)Math.atan2(point.getY() - getCenterY(), point.getX() - getCenterX()); + setAngleStart(getNormAngle(-(float)Math.toDegrees(angle))); + } + + /** + * Sets the starting angle and angular extent of this arc using two sets of coordinates. The + * first set of coordinates is used to determine the angle of the starting point relative to + * the arc's center. The second set of coordinates is used to determine the angle of the end + * point relative to the arc's center. The arc will always be non-empty and extend + * counterclockwise from the first point around to the second point. + */ + public void setAngles (float x1, float y1, float x2, float y2) { + float cx = getCenterX(); + float cy = getCenterY(); + float a1 = getNormAngle(-(float)Math.toDegrees(Math.atan2(y1 - cy, x1 - cx))); + float a2 = getNormAngle(-(float)Math.toDegrees(Math.atan2(y2 - cy, x2 - cx))); + a2 -= a1; + if (a2 <= 0f) { + a2 += 360f; + } + setAngleStart(a1); + setAngleExtent(a2); + } + + /** + * Sets the starting angle and angular extent of this arc using two sets of coordinates. The + * first set of coordinates is used to determine the angle of the starting point relative to + * the arc's center. The second set of coordinates is used to determine the angle of the end + * point relative to the arc's center. The arc will always be non-empty and extend + * counterclockwise from the first point around to the second point. + */ + public void setAngles (IPoint p1, IPoint p2) { + setAngles(p1.getX(), p1.getY(), p2.getX(), p2.getY()); + } + + @Override // from RectangularShape + public void setFrame (float x, float y, float width, float height) { + setArc(x, y, width, height, getAngleStart(), getAngleExtent(), type); + } + + private int type; +} diff --git a/src/main/java/pythagoras/f/IArc.java b/src/main/java/pythagoras/f/IArc.java new file mode 100644 index 0000000..45aa389 --- /dev/null +++ b/src/main/java/pythagoras/f/IArc.java @@ -0,0 +1,55 @@ +// +// $Id$ + +package pythagoras.f; + +/** + * Provides read-only access to an {@link Arc}. + */ +public interface IArc extends IRectangularShape, Cloneable +{ + /** An arc type indicating a simple, unconnected curve. */ + int OPEN = 0; + + /** An arc type indicating a closed curve, connected by a straight line from the starting to + * the ending point of the arc. */ + int CHORD = 1; + + /** An arc type indicating a closed curve, connected by a line from the starting point of the + * arc to the center of the circle defining the arc, and another straight line from that center + * to the ending point of the arc. */ + int PIE = 2; + + /** Returns the type of this arc: {@link #OPEN}, etc. */ + int getArcType (); + + /** Returns the starting angle of this arc. */ + float getAngleStart (); + + /** Returns the angular extent of this arc. */ + float getAngleExtent (); + + /** Returns the intersection of the ray from the center (defined by the starting angle) and the + * elliptical boundary of the arc. */ + Point getStartPoint (); + + /** Writes the intersection of the ray from the center (defined by the starting angle) and the + * elliptical boundary of the arc into {@code target}. + * @return the supplied point. */ + Point getStartPoint (Point target); + + /** Returns the intersection of the ray from the center (defined by the starting angle plus the + * angular extent of the arc) and the elliptical boundary of the arc. */ + Point getEndPoint (); + + /** Writes the intersection of the ray from the center (defined by the starting angle plus the + * angular extent of the arc) and the elliptical boundary of the arc into {@code target}. + * @return the supplied point. */ + Point getEndPoint (Point target); + + /** Returns whether the specified angle is within the angular extents of this arc. */ + boolean containsAngle (float angle); + + /** Returns a mutable copy of this arc. */ + Arc clone (); +}