diff --git a/src/main/java/pythagoras/f/AbstractDimension.java b/src/main/java/pythagoras/f/AbstractDimension.java index 06e81cd..ee1d88e 100644 --- a/src/main/java/pythagoras/f/AbstractDimension.java +++ b/src/main/java/pythagoras/f/AbstractDimension.java @@ -33,6 +33,6 @@ public abstract class AbstractDimension implements IDimension @Override public String toString () { - return Geometry.dimenToString(getWidth(), getHeight()); + return Dimensions.dimenToString(getWidth(), getHeight()); } } diff --git a/src/main/java/pythagoras/f/AbstractLine.java b/src/main/java/pythagoras/f/AbstractLine.java index 4ea8b42..37a0693 100644 --- a/src/main/java/pythagoras/f/AbstractLine.java +++ b/src/main/java/pythagoras/f/AbstractLine.java @@ -3,6 +3,8 @@ package pythagoras.f; +import java.util.NoSuchElementException; + /** * Provides most of the implementation of {@link ILine}, obtaining only the start and end points * from the derived class. @@ -45,42 +47,42 @@ public abstract class AbstractLine implements ILine @Override // from interface ILine public float pointLineDistSq (float px, float py) { - return Geometry.pointLineDistSq(px, py, getX1(), getY1(), getX2(), getY2()); + return Lines.pointLineDistSq(px, py, getX1(), getY1(), getX2(), getY2()); } @Override // from interface ILine public float pointLineDistSq (IPoint p) { - return Geometry.pointLineDistSq(p.getX(), p.getY(), getX1(), getY1(), getX2(), getY2()); + return Lines.pointLineDistSq(p.getX(), p.getY(), getX1(), getY1(), getX2(), getY2()); } @Override // from interface ILine public float pointLineDist (float px, float py) { - return Geometry.pointLineDist(px, py, getX1(), getY1(), getX2(), getY2()); + return Lines.pointLineDist(px, py, getX1(), getY1(), getX2(), getY2()); } @Override // from interface ILine public float pointLineDist (IPoint p) { - return Geometry.pointLineDist(p.getX(), p.getY(), getX1(), getY1(), getX2(), getY2()); + return Lines.pointLineDist(p.getX(), p.getY(), getX1(), getY1(), getX2(), getY2()); } @Override // from interface ILine public float pointSegDistSq (float px, float py) { - return Geometry.pointSegDistSq(px, py, getX1(), getY1(), getX2(), getY2()); + return Lines.pointSegDistSq(px, py, getX1(), getY1(), getX2(), getY2()); } @Override // from interface ILine public float pointSegDistSq (IPoint p) { - return Geometry.pointSegDistSq(p.getX(), p.getY(), getX1(), getY1(), getX2(), getY2()); + return Lines.pointSegDistSq(p.getX(), p.getY(), getX1(), getY1(), getX2(), getY2()); } @Override // from interface ILine public float pointSegDist (float px, float py) { - return Geometry.pointSegDist(px, py, getX1(), getY1(), getX2(), getY2()); + return Lines.pointSegDist(px, py, getX1(), getY1(), getX2(), getY2()); } @Override // from interface ILine public float pointSegDist (IPoint p) { - return Geometry.pointSegDist(p.getX(), p.getY(), getX1(), getY1(), getX2(), getY2()); + return Lines.pointSegDist(p.getX(), p.getY(), getX1(), getY1(), getX2(), getY2()); } @Override // from interface ILine @@ -115,7 +117,7 @@ public abstract class AbstractLine implements ILine @Override // from interface IShape public boolean intersects (float rx, float ry, float rw, float rh) { - return Geometry.lineIntersectsRect(getX1(), getY1(), getX2(), getY2(), rx, ry, rw, rh); + return Lines.lineIntersectsRect(getX1(), getY1(), getX2(), getY2(), rx, ry, rw, rh); } @Override // from interface IShape @@ -167,4 +169,62 @@ public abstract class AbstractLine implements ILine } return new Rectangle(rx, ry, rw, rh); } + + @Override // from interface IShape + public PathIterator getPathIterator (AffineTransform at) { + return new Iterator(this, at); + } + + @Override // from interface IShape + public PathIterator getPathIterator (AffineTransform at, float flatness) { + return new Iterator(this, at); + } + + /** An iterator over an {@link ILine}. */ + protected static class Iterator implements PathIterator + { + private float x1, y1, x2, y2; + private AffineTransform t; + private int index; + + Iterator (ILine l, AffineTransform at) { + this.x1 = l.getX1(); + this.y1 = l.getY1(); + this.x2 = l.getX2(); + this.y2 = l.getY2(); + this.t = at; + } + + @Override public int getWindingRule () { + return WIND_NON_ZERO; + } + + @Override public boolean isDone () { + return index > 1; + } + + @Override public void next () { + index++; + } + + @Override public int currentSegment (float[] coords) { + if (isDone()) { + throw new NoSuchElementException("Iterator out of bounds"); + } + int type; + if (index == 0) { + type = SEG_MOVETO; + coords[0] = x1; + coords[1] = y1; + } else { + type = SEG_LINETO; + coords[0] = x2; + coords[1] = y2; + } + if (t != null) { + t.transform(coords, 0, coords, 0, 1); + } + return type; + } + } } diff --git a/src/main/java/pythagoras/f/AbstractPoint.java b/src/main/java/pythagoras/f/AbstractPoint.java index 347ff58..cca5676 100644 --- a/src/main/java/pythagoras/f/AbstractPoint.java +++ b/src/main/java/pythagoras/f/AbstractPoint.java @@ -11,22 +11,22 @@ public abstract class AbstractPoint implements IPoint { @Override // from interface IPoint public float distanceSq (float px, float py) { - return Geometry.distanceSq(getX(), getY(), px, py); + return Points.distanceSq(getX(), getY(), px, py); } @Override // from interface IPoint public float distanceSq (IPoint p) { - return Geometry.distanceSq(getX(), getY(), p.getX(), p.getY()); + return Points.distanceSq(getX(), getY(), p.getX(), p.getY()); } @Override // from interface IPoint public float distance (float px, float py) { - return Geometry.distance(getX(), getY(), px, py); + return Points.distance(getX(), getY(), px, py); } @Override // from interface IPoint public float distance (IPoint p) { - return Geometry.distance(getX(), getY(), p.getX(), p.getY()); + return Points.distance(getX(), getY(), p.getX(), p.getY()); } @Override // from interface IPoint @@ -53,6 +53,6 @@ public abstract class AbstractPoint implements IPoint @Override public String toString () { - return Geometry.pointToString(getX(), getY()); + return Points.pointToString(getX(), getY()); } } diff --git a/src/main/java/pythagoras/f/AbstractRectangle.java b/src/main/java/pythagoras/f/AbstractRectangle.java index 0cc5e29..813c111 100644 --- a/src/main/java/pythagoras/f/AbstractRectangle.java +++ b/src/main/java/pythagoras/f/AbstractRectangle.java @@ -3,6 +3,8 @@ package pythagoras.f; +import java.util.NoSuchElementException; + /** * Provides most of the implementation of {@link IRectangle}, obtaining only the location and * dimensions from the derived class. @@ -66,7 +68,7 @@ public abstract class AbstractRectangle extends RectangularShape implements IRec @Override // from interface IRectangle public boolean intersectsLine (float x1, float y1, float x2, float y2) { - return Geometry.lineIntersectsRect(x1, y1, x2, y2, getX(), getY(), getWidth(), getHeight()); + return Lines.lineIntersectsRect(x1, y1, x2, y2, getX(), getY(), getWidth(), getHeight()); } @Override // from interface IRectangle @@ -135,6 +137,16 @@ public abstract class AbstractRectangle extends RectangularShape implements IRec return (rx + rw > x1) && (rx < x2) && (ry + rh > y1) && (ry < y2); } + @Override // from interface IShape + public PathIterator getPathIterator (AffineTransform t) { + return new Iterator(this, t); + } + + @Override // from interface IShape + public PathIterator getPathIterator (AffineTransform t, float flatness) { + return new Iterator(this, t); + } + @Override // from RectangularShape public IRectangle frame () { return this; @@ -166,7 +178,79 @@ public abstract class AbstractRectangle extends RectangularShape implements IRec @Override // from Object public String toString () { - return Geometry.dimenToString(getWidth(), getHeight()) + - Geometry.pointToString(getX(), getY()); + return Dimensions.dimenToString(getWidth(), getHeight()) + + Points.pointToString(getX(), getY()); + } + + /** An iterator over an {@link IRectangle}. */ + protected static class Iterator implements PathIterator + { + private float x, y, width, height; + private AffineTransform t; + + /** The current segmenet index. */ + private int index; + + Iterator (IRectangle r, AffineTransform at) { + this.x = r.getX(); + this.y = r.getY(); + this.width = r.getWidth(); + this.height = r.getHeight(); + this.t = at; + if (width < 0f || height < 0f) { + index = 6; + } + } + + @Override public int getWindingRule () { + return WIND_NON_ZERO; + } + + @Override public boolean isDone () { + return index > 5; + } + + @Override public void next () { + index++; + } + + @Override public int currentSegment (float[] coords) { + if (isDone()) { + throw new NoSuchElementException("Iterator out of bounds"); + } + if (index == 5) { + return SEG_CLOSE; + } + int type; + if (index == 0) { + type = SEG_MOVETO; + coords[0] = x; + coords[1] = y; + } else { + type = SEG_LINETO; + switch (index) { + case 1: + coords[0] = x + width; + coords[1] = y; + break; + case 2: + coords[0] = x + width; + coords[1] = y + height; + break; + case 3: + coords[0] = x; + coords[1] = y + height; + break; + case 4: + coords[0] = x; + coords[1] = y; + break; + } + } + if (t != null) { + t.transform(coords, 0, coords, 0, 1); + } + return type; + } } } diff --git a/src/main/java/pythagoras/f/AffineTransform.java b/src/main/java/pythagoras/f/AffineTransform.java new file mode 100644 index 0000000..5827cb5 --- /dev/null +++ b/src/main/java/pythagoras/f/AffineTransform.java @@ -0,0 +1,483 @@ +// +// Pythagoras - a collection of geometry classes +// http://github.com/samskivert/pythagoras + +package pythagoras.f; + +import java.io.Serializable; + +/** + * Represents a 2D affine transform, which performs a linear mapping that preserves the + * straightness and parallelness of lines. + * + * @see http://download.oracle.com/javase/6/docs/api/java/awt/geom/AffineTransform.html + */ +public class AffineTransform implements Cloneable, Serializable +{ + public static final int TYPE_IDENTITY = 0; + public static final int TYPE_TRANSLATION = 1; + public static final int TYPE_UNIFORM_SCALE = 2; + public static final int TYPE_GENERAL_SCALE = 4; + public static final int TYPE_QUADRANT_ROTATION = 8; + public static final int TYPE_GENERAL_ROTATION = 16; + public static final int TYPE_GENERAL_TRANSFORM = 32; + public static final int TYPE_FLIP = 64; + public static final int TYPE_MASK_SCALE = TYPE_UNIFORM_SCALE | TYPE_GENERAL_SCALE; + public static final int TYPE_MASK_ROTATION = TYPE_QUADRANT_ROTATION | TYPE_GENERAL_ROTATION; + + public AffineTransform () { + this.type = TYPE_IDENTITY; + this.m00 = this.m11 = 1f; + this.m10 = this.m01 = this.m02 = this.m12 = 0; + } + + public AffineTransform (AffineTransform t) { + this.type = t.type; + this.m00 = t.m00; + this.m10 = t.m10; + this.m01 = t.m01; + this.m11 = t.m11; + this.m02 = t.m02; + this.m12 = t.m12; + } + + public AffineTransform (float m00, float m10, float m01, float m11, float m02, float m12) { + this.type = TYPE_UNKNOWN; + this.m00 = m00; + this.m10 = m10; + this.m01 = m01; + this.m11 = m11; + this.m02 = m02; + this.m12 = m12; + } + + public AffineTransform (float[] matrix) { + this.type = TYPE_UNKNOWN; + m00 = matrix[0]; + m10 = matrix[1]; + m01 = matrix[2]; + m11 = matrix[3]; + if (matrix.length > 4) { + m02 = matrix[4]; + m12 = matrix[5]; + } + } + + /* + * Method returns type of affine transformation. + * + * Transform matrix is m00 m01 m02 m10 m11 m12 + * + * According analytic geometry new basis vectors are (m00, m01) and (m10, + * m11), translation vector is (m02, m12). Original basis vectors are (1, 0) + * and (0, 1). Type transformations classification: TYPE_IDENTITY - new + * basis equals original one and zero translation TYPE_TRANSLATION - + * translation vector isn't zero TYPE_UNIFORM_SCALE - vectors length of new + * basis equals TYPE_GENERAL_SCALE - vectors length of new basis doesn't + * equal TYPE_FLIP - new basis vector orientation differ from original one + * TYPE_QUADRANT_ROTATION - new basis is rotated by 90, 180, 270, or 360 + * degrees TYPE_GENERAL_ROTATION - new basis is rotated by arbitrary angle + * TYPE_GENERAL_TRANSFORM - transformation can't be inversed + */ + public int getType () { + if (type != TYPE_UNKNOWN) { + return type; + } + + int type = 0; + + if (m00 * m01 + m10 * m11 != 0) { + type |= TYPE_GENERAL_TRANSFORM; + return type; + } + + if (m02 != 0 || m12 != 0) { + type |= TYPE_TRANSLATION; + } else if (m00 == 1f && m11 == 1f && m01 == 0 && m10 == 0) { + type = TYPE_IDENTITY; + return type; + } + + if (m00 * m11 - m01 * m10 < 0) { + type |= TYPE_FLIP; + } + + float dx = m00 * m00 + m10 * m10; + float dy = m01 * m01 + m11 * m11; + if (dx != dy) { + type |= TYPE_GENERAL_SCALE; + } else if (dx != 1f) { + type |= TYPE_UNIFORM_SCALE; + } + + if ((m00 == 0 && m11 == 0) || (m10 == 0 && m01 == 0 && (m00 < 0 || m11 < 0))) { + type |= TYPE_QUADRANT_ROTATION; + } else if (m01 != 0 || m10 != 0) { + type |= TYPE_GENERAL_ROTATION; + } + + return type; + } + + public float getScaleX () { + return m00; + } + + public float getScaleY () { + return m11; + } + + public float getShearX () { + return m01; + } + + public float getShearY () { + return m10; + } + + public float getTranslateX () { + return m02; + } + + public float getTranslateY () { + return m12; + } + + public boolean isIdentity () { + return getType() == TYPE_IDENTITY; + } + + public void getMatrix (float[] matrix) { + matrix[0] = m00; + matrix[1] = m10; + matrix[2] = m01; + matrix[3] = m11; + if (matrix.length > 4) { + matrix[4] = m02; + matrix[5] = m12; + } + } + + public float getDeterminant () { + return m00 * m11 - m01 * m10; + } + + public void setTransform (float m00, float m10, float m01, float m11, float m02, float m12) { + this.type = TYPE_UNKNOWN; + this.m00 = m00; + this.m10 = m10; + this.m01 = m01; + this.m11 = m11; + this.m02 = m02; + this.m12 = m12; + } + + public void setTransform (AffineTransform t) { + type = t.type; + setTransform(t.m00, t.m10, t.m01, t.m11, t.m02, t.m12); + } + + public void setToIdentity () { + type = TYPE_IDENTITY; + m00 = m11 = 1f; + m10 = m01 = m02 = m12 = 0; + } + + public void setToTranslation (float mx, float my) { + m00 = m11 = 1f; + m01 = m10 = 0; + m02 = mx; + m12 = my; + if (mx == 0 && my == 0) { + type = TYPE_IDENTITY; + } else { + type = TYPE_TRANSLATION; + } + } + + public void setToScale (float scx, float scy) { + m00 = scx; + m11 = scy; + m10 = m01 = m02 = m12 = 0; + if (scx != 1f || scy != 1f) { + type = TYPE_UNKNOWN; + } else { + type = TYPE_IDENTITY; + } + } + + public void setToShear (float shx, float shy) { + m00 = m11 = 1f; + m02 = m12 = 0; + m01 = shx; + m10 = shy; + if (shx != 0 || shy != 0) { + type = TYPE_UNKNOWN; + } else { + type = TYPE_IDENTITY; + } + } + + public void setToRotation (float angle) { + float sin = (float)Math.sin(angle); + float cos = (float)Math.cos(angle); + if (Math.abs(cos) < ZERO) { + cos = 0; + sin = sin > 0 ? 1f : -1f; + } else if (Math.abs(sin) < ZERO) { + sin = 0; + cos = cos > 0 ? 1f : -1f; + } + m00 = m11 = cos; + m01 = -sin; + m10 = sin; + m02 = m12 = 0; + type = TYPE_UNKNOWN; + } + + public void setToRotation (float angle, float px, float py) { + setToRotation(angle); + m02 = px * (1f - m00) + py * m10; + m12 = py * (1f - m00) - px * m10; + type = TYPE_UNKNOWN; + } + + public static AffineTransform getTranslateInstance (float mx, float my) { + AffineTransform t = new AffineTransform(); + t.setToTranslation(mx, my); + return t; + } + + public static AffineTransform getScaleInstance (float scx, float scY) { + AffineTransform t = new AffineTransform(); + t.setToScale(scx, scY); + return t; + } + + public static AffineTransform getShearInstance (float shx, float shy) { + AffineTransform m = new AffineTransform(); + m.setToShear(shx, shy); + return m; + } + + public static AffineTransform getRotateInstance (float angle) { + AffineTransform t = new AffineTransform(); + t.setToRotation(angle); + return t; + } + + public static AffineTransform getRotateInstance (float angle, float x, float y) { + AffineTransform t = new AffineTransform(); + t.setToRotation(angle, x, y); + return t; + } + + public void translate (float mx, float my) { + concatenate(AffineTransform.getTranslateInstance(mx, my)); + } + + public void scale (float scx, float scy) { + concatenate(AffineTransform.getScaleInstance(scx, scy)); + } + + public void shear (float shx, float shy) { + concatenate(AffineTransform.getShearInstance(shx, shy)); + } + + public void rotate (float angle) { + concatenate(AffineTransform.getRotateInstance(angle)); + } + + public void rotate (float angle, float px, float py) { + concatenate(AffineTransform.getRotateInstance(angle, px, py)); + } + + /** + * Multiply matrix of two AffineTransform objects + * + * @param t1 + * - the AffineTransform object is a multiplicand + * @param t2 + * - the AffineTransform object is a multiplier + * @return an AffineTransform object that is a result of t1 multiplied by + * matrix t2. + */ + AffineTransform multiply (AffineTransform t1, AffineTransform t2) { + return new AffineTransform(t1.m00 * t2.m00 + t1.m10 * t2.m01, // m00 + t1.m00 * t2.m10 + t1.m10 * t2.m11, // m01 + t1.m01 * t2.m00 + t1.m11 * t2.m01, // m10 + t1.m01 * t2.m10 + t1.m11 * t2.m11, // m11 + t1.m02 * t2.m00 + t1.m12 * t2.m01 + t2.m02, // m02 + t1.m02 * t2.m10 + t1.m12 * t2.m11 + t2.m12);// m12 + } + + public void concatenate (AffineTransform t) { + setTransform(multiply(t, this)); + } + + public void preConcatenate (AffineTransform t) { + setTransform(multiply(this, t)); + } + + public AffineTransform createInverse () throws NoninvertibleTransformException { + float det = getDeterminant(); + if (Math.abs(det) < ZERO) { + throw new NoninvertibleTransformException("Determinant is zero"); + } + return new AffineTransform(m11 / det, // m00 + -m10 / det, // m10 + -m01 / det, // m01 + m00 / det, // m11 + (m01 * m12 - m11 * m02) / det, // m02 + (m10 * m02 - m00 * m12) / det // m12 + ); + } + + public Point transform (IPoint src, Point dst) { + if (dst == null) { + dst = new Point(); + } + + float x = src.getX(), y = src.getY(); + dst.setLocation(x * m00 + y * m01 + m02, x * m10 + y * m11 + m12); + return dst; + } + + public void transform (IPoint[] src, int srcOff, Point[] dst, int dstOff, int length) { + while (--length >= 0) { + IPoint srcPoint = src[srcOff++]; + float x = srcPoint.getX(); + float y = srcPoint.getY(); + Point dstPoint = dst[dstOff]; + if (dstPoint == null) { + dstPoint = new Point(); + } + dstPoint.setLocation(x * m00 + y * m01 + m02, x * m10 + y * m11 + m12); + dst[dstOff++] = dstPoint; + } + } + + public void transform (float[] src, int srcOff, float[] dst, int dstOff, int length) { + int step = 2; + if (src == dst && srcOff < dstOff && dstOff < srcOff + length * 2) { + srcOff = srcOff + length * 2 - 2; + dstOff = dstOff + length * 2 - 2; + step = -2; + } + while (--length >= 0) { + float x = src[srcOff + 0]; + float y = src[srcOff + 1]; + dst[dstOff + 0] = (x * m00 + y * m01 + m02); + dst[dstOff + 1] = (x * m10 + y * m11 + m12); + srcOff += step; + dstOff += step; + } + } + + public Point deltaTransform (IPoint src, Point dst) { + if (dst == null) { + dst = new Point(); + } + float x = src.getX(), y = src.getY(); + dst.setLocation(x * m00 + y * m01, x * m10 + y * m11); + return dst; + } + + public void deltaTransform (float[] src, int srcOff, float[] dst, int dstOff, int length) { + while (--length >= 0) { + float x = src[srcOff++], y = src[srcOff++]; + dst[dstOff++] = x * m00 + y * m01; + dst[dstOff++] = x * m10 + y * m11; + } + } + + public Point inverseTransform (IPoint src, Point dst) throws NoninvertibleTransformException { + float det = getDeterminant(); + if (Math.abs(det) < ZERO) { + throw new NoninvertibleTransformException("Determinant is zero"); + } + if (dst == null) { + dst = new Point(); + } + float x = src.getX() - m02, y = src.getY() - m12; + dst.setLocation((x * m11 - y * m01) / det, (y * m00 - x * m10) / det); + return dst; + } + + public void inverseTransform (float[] src, int srcOff, float[] dst, int dstOff, int length) + throws NoninvertibleTransformException { + float det = getDeterminant(); + if (Math.abs(det) < ZERO) { + throw new NoninvertibleTransformException("Determinant is zero"); + } + while (--length >= 0) { + float x = src[srcOff++] - m02, y = src[srcOff++] - m12; + dst[dstOff++] = (x * m11 - y * m01) / det; + dst[dstOff++] = (y * m00 - x * m10) / det; + } + } + + public IShape createTransformedShape (IShape src) { + if (src == null) { + return null; + } + if (src instanceof Path) { + return ((Path)src).createTransformedShape(this); + } + PathIterator path = src.getPathIterator(this); + Path dst = new Path(path.getWindingRule()); + dst.append(path, false); + return dst; + } + + @Override + public String toString () { + return getClass().getName() + + "[[" + m00 + ", " + m01 + ", " + m02 + "], [" + m10 + ", " + m11 + ", " + m12 + "]]"; + } + + @Override + public AffineTransform clone () { + try { + return (AffineTransform)super.clone(); + } catch (CloneNotSupportedException e) { + throw new InternalError(); + } + } + + @Override + public int hashCode () { + return Float.floatToIntBits(m00) ^ Float.floatToIntBits(m01) ^ Float.floatToIntBits(m02) ^ + Float.floatToIntBits(m10) ^ Float.floatToIntBits(m11) ^ Float.floatToIntBits(m12); + } + + @Override + public boolean equals (Object obj) { + if (obj == this) { + return true; + } + if (obj instanceof AffineTransform) { + AffineTransform t = (AffineTransform)obj; + return m00 == t.m00 && m01 == t.m01 && m02 == t.m02 && + m10 == t.m10 && m11 == t.m11 && m12 == t.m12; + } + return false; + } + + // the values of transformation matrix + private float m00; + private float m10; + private float m01; + private float m11; + private float m02; + private float m12; + + /** The transformation {@code type}. */ + private transient int type; + + /** An initial type value. */ + private static final int TYPE_UNKNOWN = -1; + + /** The min value equivalent to zero. An absolute value < ZERO is considered to be zero. */ + private static final float ZERO = 1E-10f; +} diff --git a/src/main/java/pythagoras/f/Crossing.java b/src/main/java/pythagoras/f/Crossing.java new file mode 100644 index 0000000..3d117d0 --- /dev/null +++ b/src/main/java/pythagoras/f/Crossing.java @@ -0,0 +1,864 @@ +// +// Pythagoras - a collection of geometry classes +// http://github.com/samskivert/pythagoras + +package pythagoras.f; + +class Crossing +{ + /** Return value indicating that a crossing was found. */ + public static final int CROSSING = 255; + + /** Return value indicating the crossing result is unknown. */ + public static final int UNKNOWN = 254; + + /** + * Solves quadratic equation + * + * @param eqn the coefficients of the equation + * @param res the roots of the equation + * @return a number of roots + */ + public static int solveQuad (float eqn[], float res[]) { + float a = eqn[2]; + float b = eqn[1]; + float c = eqn[0]; + int rc = 0; + if (a == 0f) { + if (b == 0f) { + return -1; + } + res[rc++] = -c / b; + } else { + float d = b * b - 4f * a * c; + // d < 0f + if (d < 0f) { + return 0; + } + d = (float)Math.sqrt(d); + res[rc++] = (-b + d) / (a * 2f); + // d != 0f + if (d != 0f) { + res[rc++] = (-b - d) / (a * 2f); + } + } + return fixRoots(res, rc); + } + + /** + * Solves cubic equation + * + * @param eqn the coefficients of the equation + * @param res the roots of the equation + * @return a number of roots + */ + public static int solveCubic (float eqn[], float res[]) { + float d = eqn[3]; + if (d == 0) { + return solveQuad(eqn, res); + } + float a = eqn[2] / d; + float b = eqn[1] / d; + float c = eqn[0] / d; + int rc = 0; + + float Q = (a * a - 3f * b) / 9f; + float R = (2f * a * a * a - 9f * a * b + 27f * c) / 54f; + float Q3 = Q * Q * Q; + float R2 = R * R; + float n = -a / 3f; + + if (R2 < Q3) { + float t = (float)Math.acos(R / Math.sqrt(Q3)) / 3f; + float p = 2f * (float)Math.PI / 3f; + float m = -2f * (float)Math.sqrt(Q); + res[rc++] = m * (float)Math.cos(t) + n; + res[rc++] = m * (float)Math.cos(t + p) + n; + res[rc++] = m * (float)Math.cos(t - p) + n; + } else { + // Debug.println("R2 >= Q3 (" + R2 + "/" + Q3 + ")"); + float A = (float)Math.pow(Math.abs(R) + Math.sqrt(R2 - Q3), 1f / 3f); + if (R > 0f) { + A = -A; + } + // if (A == 0f) { + if (-ROOT_DELTA < A && A < ROOT_DELTA) { + res[rc++] = n; + } else { + float B = Q / A; + res[rc++] = A + B + n; + // if (R2 == Q3) { + float delta = R2 - Q3; + if (-ROOT_DELTA < delta && delta < ROOT_DELTA) { + res[rc++] = -(A + B) / 2f + n; + } + } + + } + return fixRoots(res, rc); + } + + /** + * Excludes double roots. Roots are double if they lies enough close with each other. + * + * @param res the roots + * @param rc the roots count + * @return new roots count + */ + protected static int fixRoots (float res[], int rc) { + int tc = 0; + for (int i = 0; i < rc; i++) { + out: { + for (int j = i + 1; j < rc; j++) { + if (isZero(res[i] - res[j])) { + break out; + } + } + res[tc++] = res[i]; + } + } + return tc; + } + + /** + * QuadCurve class provides basic functionality to find curve crossing and calculating bounds + */ + public static class QuadCurve + { + float ax, ay, bx, by; + float Ax, Ay, Bx, By; + + public QuadCurve (float x1, float y1, float cx, float cy, float x2, float y2) { + ax = x2 - x1; + ay = y2 - y1; + bx = cx - x1; + by = cy - y1; + + Bx = bx + bx; // Bx = 2f * bx + Ax = ax - Bx; // Ax = ax - 2f * bx + + By = by + by; // By = 2f * by + Ay = ay - By; // Ay = ay - 2f * by + } + + public int cross (float res[], int rc, float py1, float py2) { + int cross = 0; + + for (int i = 0; i < rc; i++) { + float t = res[i]; + + // CURVE-OUTSIDE + if (t < -DELTA || t > 1 + DELTA) { + continue; + } + // CURVE-START + if (t < DELTA) { + if (py1 < 0f && (bx != 0f ? bx : ax - bx) < 0f) { + cross--; + } + continue; + } + // CURVE-END + if (t > 1 - DELTA) { + if (py1 < ay && (ax != bx ? ax - bx : bx) > 0f) { + cross++; + } + continue; + } + // CURVE-INSIDE + float ry = t * (t * Ay + By); + // ry = t * t * Ay + t * By + if (ry > py2) { + float rxt = t * Ax + bx; + // rxt = 2f * t * Ax + Bx = 2f * t * Ax + 2f * bx + if (rxt > -DELTA && rxt < DELTA) { + continue; + } + cross += rxt > 0f ? 1 : -1; + } + } // for + + return cross; + } + + public int solvePoint (float res[], float px) { + float eqn[] = { -px, Bx, Ax }; + return solveQuad(eqn, res); + } + + public int solveExtreme (float res[]) { + int rc = 0; + if (Ax != 0f) { + res[rc++] = -Bx / (Ax + Ax); + } + if (Ay != 0f) { + res[rc++] = -By / (Ay + Ay); + } + return rc; + } + + public int addBound (float bound[], int bc, float res[], int rc, float minX, float maxX, + boolean changeId, int id) { + for (int i = 0; i < rc; i++) { + float t = res[i]; + if (t > -DELTA && t < 1 + DELTA) { + float rx = t * (t * Ax + Bx); + if (minX <= rx && rx <= maxX) { + bound[bc++] = t; + bound[bc++] = rx; + bound[bc++] = t * (t * Ay + By); + bound[bc++] = id; + if (changeId) { + id++; + } + } + } + } + return bc; + } + } + + /** + * CubicCurve class provides basic functionality to find curve crossing and calculating bounds + */ + public static class CubicCurve + { + float ax, ay, bx, by, cx, cy; + float Ax, Ay, Bx, By, Cx, Cy; + float Ax3, Bx2; + + public CubicCurve (float x1, float y1, float cx1, float cy1, float cx2, float cy2, + float x2, float y2) { + ax = x2 - x1; + ay = y2 - y1; + bx = cx1 - x1; + by = cy1 - y1; + cx = cx2 - x1; + cy = cy2 - y1; + + Cx = bx + bx + bx; // Cx = 3f * bx + Bx = cx + cx + cx - Cx - Cx; // Bx = 3f * cx - 6f * bx + Ax = ax - Bx - Cx; // Ax = ax - 3f * cx + 3f * bx + + Cy = by + by + by; // Cy = 3f * by + By = cy + cy + cy - Cy - Cy; // By = 3f * cy - 6f * by + Ay = ay - By - Cy; // Ay = ay - 3f * cy + 3f * by + + Ax3 = Ax + Ax + Ax; + Bx2 = Bx + Bx; + } + + public int cross (float res[], int rc, float py1, float py2) { + int cross = 0; + for (int i = 0; i < rc; i++) { + float t = res[i]; + + // CURVE-OUTSIDE + if (t < -DELTA || t > 1 + DELTA) { + continue; + } + // CURVE-START + if (t < DELTA) { + if (py1 < 0f && (bx != 0f ? bx : (cx != bx ? cx - bx : ax - cx)) < 0f) { + cross--; + } + continue; + } + // CURVE-END + if (t > 1 - DELTA) { + if (py1 < ay && (ax != cx ? ax - cx : (cx != bx ? cx - bx : bx)) > 0f) { + cross++; + } + continue; + } + // CURVE-INSIDE + float ry = t * (t * (t * Ay + By) + Cy); + // ry = t * t * t * Ay + t * t * By + t * Cy + if (ry > py2) { + float rxt = t * (t * Ax3 + Bx2) + Cx; + // rxt = 3f * t * t * Ax + 2f * t * Bx + Cx + if (rxt > -DELTA && rxt < DELTA) { + rxt = t * (Ax3 + Ax3) + Bx2; + // rxt = 6f * t * Ax + 2f * Bx + if (rxt < -DELTA || rxt > DELTA) { + // Inflection point + continue; + } + rxt = ax; + } + cross += rxt > 0f ? 1 : -1; + } + } // for + + return cross; + } + + public int solvePoint (float res[], float px) { + float eqn[] = { -px, Cx, Bx, Ax }; + return solveCubic(eqn, res); + } + + public int solveExtremeX (float res[]) { + float eqn[] = { Cx, Bx2, Ax3 }; + return solveQuad(eqn, res); + } + + public int solveExtremeY (float res[]) { + float eqn[] = { Cy, By + By, Ay + Ay + Ay }; + return solveQuad(eqn, res); + } + + public int addBound (float bound[], int bc, float res[], int rc, float minX, float maxX, + boolean changeId, int id) { + for (int i = 0; i < rc; i++) { + float t = res[i]; + if (t > -DELTA && t < 1 + DELTA) { + float rx = t * (t * (t * Ax + Bx) + Cx); + if (minX <= rx && rx <= maxX) { + bound[bc++] = t; + bound[bc++] = rx; + bound[bc++] = t * (t * (t * Ay + By) + Cy); + bound[bc++] = id; + if (changeId) { + id++; + } + } + } + } + return bc; + } + } + + /** + * Returns how many times ray from point (x,y) cross line. + */ + public static int crossLine (float x1, float y1, float x2, float y2, float x, float y) { + // LEFT/RIGHT/UP/EMPTY + if ((x < x1 && x < x2) || (x > x1 && x > x2) || (y > y1 && y > y2) || (x1 == x2)) { + return 0; + } + + // DOWN + if (y < y1 && y < y2) { + } else { + // INSIDE + if ((y2 - y1) * (x - x1) / (x2 - x1) <= y - y1) { + // INSIDE-UP + return 0; + } + } + + // START + if (x == x1) { + return x1 < x2 ? 0 : -1; + } + + // END + if (x == x2) { + return x1 < x2 ? 1 : 0; + } + + // INSIDE-DOWN + return x1 < x2 ? 1 : -1; + } + + /** + * Returns how many times ray from point (x,y) cross quard curve + */ + public static int crossQuad (float x1, float y1, float cx, float cy, float x2, float y2, + float x, float y) { + // LEFT/RIGHT/UP/EMPTY + if ((x < x1 && x < cx && x < x2) || (x > x1 && x > cx && x > x2) + || (y > y1 && y > cy && y > y2) || (x1 == cx && cx == x2)) { + return 0; + } + + // DOWN + if (y < y1 && y < cy && y < y2 && x != x1 && x != x2) { + if (x1 < x2) { + return x1 < x && x < x2 ? 1 : 0; + } + return x2 < x && x < x1 ? -1 : 0; + } + + // INSIDE + QuadCurve c = new QuadCurve(x1, y1, cx, cy, x2, y2); + float px = x - x1, py = y - y1; + float[] res = new float[3]; + int rc = c.solvePoint(res, px); + return c.cross(res, rc, py, py); + } + + /** + * Returns how many times ray from point (x,y) cross cubic curve + */ + public static int crossCubic (float x1, float y1, float cx1, float cy1, float cx2, + float cy2, float x2, float y2, float x, float y) { + // LEFT/RIGHT/UP/EMPTY + if ((x < x1 && x < cx1 && x < cx2 && x < x2) || (x > x1 && x > cx1 && x > cx2 && x > x2) + || (y > y1 && y > cy1 && y > cy2 && y > y2) + || (x1 == cx1 && cx1 == cx2 && cx2 == x2)) { + return 0; + } + + // DOWN + if (y < y1 && y < cy1 && y < cy2 && y < y2 && x != x1 && x != x2) { + if (x1 < x2) { + return x1 < x && x < x2 ? 1 : 0; + } + return x2 < x && x < x1 ? -1 : 0; + } + + // INSIDE + CubicCurve c = new CubicCurve(x1, y1, cx1, cy1, cx2, cy2, x2, y2); + float px = x - x1, py = y - y1; + float[] res = new float[3]; + int rc = c.solvePoint(res, px); + return c.cross(res, rc, py, py); + } + + /** + * Returns how many times ray from point (x,y) cross path + */ + public static int crossPath (PathIterator p, float x, float y) { + int cross = 0; + float mx, my, cx, cy; + mx = my = cx = cy = 0f; + float[] coords = new float[6]; + + while (!p.isDone()) { + switch (p.currentSegment(coords)) { + case PathIterator.SEG_MOVETO: + if (cx != mx || cy != my) { + cross += crossLine(cx, cy, mx, my, x, y); + } + mx = cx = coords[0]; + my = cy = coords[1]; + break; + case PathIterator.SEG_LINETO: + cross += crossLine(cx, cy, cx = coords[0], cy = coords[1], x, y); + break; + case PathIterator.SEG_QUADTO: + cross += crossQuad(cx, cy, coords[0], coords[1], cx = coords[2], cy = coords[3], x, + y); + break; + case PathIterator.SEG_CUBICTO: + cross += crossCubic(cx, cy, coords[0], coords[1], coords[2], coords[3], + cx = coords[4], cy = coords[5], x, y); + break; + case PathIterator.SEG_CLOSE: + if (cy != my || cx != mx) { + cross += crossLine(cx, cy, cx = mx, cy = my, x, y); + } + break; + } + + // checks if the point (x,y) is the vertex of shape with PathIterator p + if (x == cx && y == cy) { + cross = 0; + cy = my; + break; + } + p.next(); + } + if (cy != my) { + cross += crossLine(cx, cy, mx, my, x, y); + } + return cross; + } + + /** + * Returns how many times a ray from point (x,y) crosses a shape. + */ + public static int crossShape (IShape s, float x, float y) { + if (!s.bounds().contains(x, y)) { + return 0; + } + return crossPath(s.getPathIterator(null), x, y); + } + + /** + * Returns true if value is close enough to zero. + */ + public static boolean isZero (float val) { + return -DELTA < val && val < DELTA; + } + + /** + * Returns how many times rectangle stripe cross line or the are intersect + */ + public static int intersectLine (float x1, float y1, float x2, float y2, float rx1, + float ry1, float rx2, float ry2) { + // LEFT/RIGHT/UP + if ((rx2 < x1 && rx2 < x2) || (rx1 > x1 && rx1 > x2) || (ry1 > y1 && ry1 > y2)) { + return 0; + } + + // DOWN + if (ry2 < y1 && ry2 < y2) { + + } else { + // INSIDE + if (x1 == x2) { + return CROSSING; + } + + // Build bound + float bx1, bx2; + if (x1 < x2) { + bx1 = x1 < rx1 ? rx1 : x1; + bx2 = x2 < rx2 ? x2 : rx2; + } else { + bx1 = x2 < rx1 ? rx1 : x2; + bx2 = x1 < rx2 ? x1 : rx2; + } + float k = (y2 - y1) / (x2 - x1); + float by1 = k * (bx1 - x1) + y1; + float by2 = k * (bx2 - x1) + y1; + + // BOUND-UP + if (by1 < ry1 && by2 < ry1) { + return 0; + } + + // BOUND-DOWN + if (by1 > ry2 && by2 > ry2) { + } else { + return CROSSING; + } + } + + // EMPTY + if (x1 == x2) { + return 0; + } + + // CURVE-START + if (rx1 == x1) { + return x1 < x2 ? 0 : -1; + } + + // CURVE-END + if (rx1 == x2) { + return x1 < x2 ? 1 : 0; + } + + if (x1 < x2) { + return x1 < rx1 && rx1 < x2 ? 1 : 0; + } + return x2 < rx1 && rx1 < x1 ? -1 : 0; + } + + /** + * Returns how many times rectangle stripe cross quad curve or the are + * intersect + */ + public static int intersectQuad (float x1, float y1, float cx, float cy, float x2, + float y2, float rx1, float ry1, float rx2, float ry2) { + // LEFT/RIGHT/UP ------------------------------------------------------ + if ((rx2 < x1 && rx2 < cx && rx2 < x2) || (rx1 > x1 && rx1 > cx && rx1 > x2) || + (ry1 > y1 && ry1 > cy && ry1 > y2)) { + return 0; + } + + // DOWN --------------------------------------------------------------- + if (ry2 < y1 && ry2 < cy && ry2 < y2 && rx1 != x1 && rx1 != x2) { + if (x1 < x2) { + return x1 < rx1 && rx1 < x2 ? 1 : 0; + } + return x2 < rx1 && rx1 < x1 ? -1 : 0; + } + + // INSIDE ------------------------------------------------------------- + QuadCurve c = new QuadCurve(x1, y1, cx, cy, x2, y2); + float px1 = rx1 - x1; + float py1 = ry1 - y1; + float px2 = rx2 - x1; + float py2 = ry2 - y1; + + float res1[] = new float[3]; + float res2[] = new float[3]; + int rc1 = c.solvePoint(res1, px1); + int rc2 = c.solvePoint(res2, px2); + + // INSIDE-LEFT/RIGHT + if (rc1 == 0 && rc2 == 0) { + return 0; + } + + // Build bound -------------------------------------------------------- + float minX = px1 - DELTA; + float maxX = px2 + DELTA; + float bound[] = new float[28]; + int bc = 0; + // Add roots + bc = c.addBound(bound, bc, res1, rc1, minX, maxX, false, 0); + bc = c.addBound(bound, bc, res2, rc2, minX, maxX, false, 1); + // Add extremal points + rc2 = c.solveExtreme(res2); + bc = c.addBound(bound, bc, res2, rc2, minX, maxX, true, 2); + // Add start and end + if (rx1 < x1 && x1 < rx2) { + bound[bc++] = 0f; + bound[bc++] = 0f; + bound[bc++] = 0f; + bound[bc++] = 4; + } + if (rx1 < x2 && x2 < rx2) { + bound[bc++] = 1f; + bound[bc++] = c.ax; + bound[bc++] = c.ay; + bound[bc++] = 5; + } + // End build bound ---------------------------------------------------- + + int cross = crossBound(bound, bc, py1, py2); + if (cross != UNKNOWN) { + return cross; + } + return c.cross(res1, rc1, py1, py2); + } + + /** + * Returns how many times rectangle stripe cross cubic curve or the are + * intersect + */ + public static int intersectCubic (float x1, float y1, float cx1, float cy1, + float cx2, float cy2, float x2, float y2, + float rx1, float ry1, float rx2, float ry2) { + // LEFT/RIGHT/UP + if ((rx2 < x1 && rx2 < cx1 && rx2 < cx2 && rx2 < x2) + || (rx1 > x1 && rx1 > cx1 && rx1 > cx2 && rx1 > x2) + || (ry1 > y1 && ry1 > cy1 && ry1 > cy2 && ry1 > y2)) { + return 0; + } + + // DOWN + if (ry2 < y1 && ry2 < cy1 && ry2 < cy2 && ry2 < y2 && rx1 != x1 && rx1 != x2) { + if (x1 < x2) { + return x1 < rx1 && rx1 < x2 ? 1 : 0; + } + return x2 < rx1 && rx1 < x1 ? -1 : 0; + } + + // INSIDE + CubicCurve c = new CubicCurve(x1, y1, cx1, cy1, cx2, cy2, x2, y2); + float px1 = rx1 - x1; + float py1 = ry1 - y1; + float px2 = rx2 - x1; + float py2 = ry2 - y1; + + float res1[] = new float[3]; + float res2[] = new float[3]; + int rc1 = c.solvePoint(res1, px1); + int rc2 = c.solvePoint(res2, px2); + + // LEFT/RIGHT + if (rc1 == 0 && rc2 == 0) { + return 0; + } + + float minX = px1 - DELTA; + float maxX = px2 + DELTA; + + // Build bound -------------------------------------------------------- + float bound[] = new float[40]; + int bc = 0; + // Add roots + bc = c.addBound(bound, bc, res1, rc1, minX, maxX, false, 0); + bc = c.addBound(bound, bc, res2, rc2, minX, maxX, false, 1); + // Add extremal points + rc2 = c.solveExtremeX(res2); + bc = c.addBound(bound, bc, res2, rc2, minX, maxX, true, 2); + rc2 = c.solveExtremeY(res2); + bc = c.addBound(bound, bc, res2, rc2, minX, maxX, true, 4); + // Add start and end + if (rx1 < x1 && x1 < rx2) { + bound[bc++] = 0f; + bound[bc++] = 0f; + bound[bc++] = 0f; + bound[bc++] = 6; + } + if (rx1 < x2 && x2 < rx2) { + bound[bc++] = 1f; + bound[bc++] = c.ax; + bound[bc++] = c.ay; + bound[bc++] = 7; + } + // End build bound ---------------------------------------------------- + + int cross = crossBound(bound, bc, py1, py2); + if (cross != UNKNOWN) { + return cross; + } + return c.cross(res1, rc1, py1, py2); + } + + /** + * Returns how many times rectangle stripe cross path or the are intersect + */ + public static int intersectPath (PathIterator p, float x, float y, float w, float h) { + int cross = 0; + int count; + float mx, my, cx, cy; + mx = my = cx = cy = 0f; + float coords[] = new float[6]; + + float rx1 = x; + float ry1 = y; + float rx2 = x + w; + float ry2 = y + h; + + while (!p.isDone()) { + count = 0; + switch (p.currentSegment(coords)) { + case PathIterator.SEG_MOVETO: + if (cx != mx || cy != my) { + count = intersectLine(cx, cy, mx, my, rx1, ry1, rx2, ry2); + } + mx = cx = coords[0]; + my = cy = coords[1]; + break; + case PathIterator.SEG_LINETO: + count = intersectLine(cx, cy, cx = coords[0], cy = coords[1], rx1, ry1, rx2, ry2); + break; + case PathIterator.SEG_QUADTO: + count = intersectQuad(cx, cy, coords[0], coords[1], cx = coords[2], cy = coords[3], + rx1, ry1, rx2, ry2); + break; + case PathIterator.SEG_CUBICTO: + count = intersectCubic(cx, cy, coords[0], coords[1], coords[2], coords[3], + cx = coords[4], cy = coords[5], rx1, ry1, rx2, ry2); + break; + case PathIterator.SEG_CLOSE: + if (cy != my || cx != mx) { + count = intersectLine(cx, cy, mx, my, rx1, ry1, rx2, ry2); + } + cx = mx; + cy = my; + break; + } + if (count == CROSSING) { + return CROSSING; + } + cross += count; + p.next(); + } + if (cy != my) { + count = intersectLine(cx, cy, mx, my, rx1, ry1, rx2, ry2); + if (count == CROSSING) { + return CROSSING; + } + cross += count; + } + return cross; + } + + /** + * Returns how many times rectangle stripe cross shape or the are intersect + */ + public static int intersectShape (IShape s, float x, float y, float w, float h) { + if (!s.bounds().intersects(x, y, w, h)) { + return 0; + } + return intersectPath(s.getPathIterator(null), x, y, w, h); + } + + /** + * Returns true if cross count correspond inside location for non zero path + * rule + */ + public static boolean isInsideNonZero (int cross) { + return cross != 0; + } + + /** + * Returns true if cross count correspond inside location for even-odd path + * rule + */ + public static boolean isInsideEvenOdd (int cross) { + return (cross & 1) != 0; + } + + /** + * Sorts a bound array. + */ + protected static void sortBound (float[] bound, int bc) { + for (int i = 0; i < bc - 4; i += 4) { + int k = i; + for (int j = i + 4; j < bc; j += 4) { + if (bound[k] > bound[j]) { + k = j; + } + } + if (k != i) { + float tmp = bound[i]; + bound[i] = bound[k]; + bound[k] = tmp; + tmp = bound[i + 1]; + bound[i + 1] = bound[k + 1]; + bound[k + 1] = tmp; + tmp = bound[i + 2]; + bound[i + 2] = bound[k + 2]; + bound[k + 2] = tmp; + tmp = bound[i + 3]; + bound[i + 3] = bound[k + 3]; + bound[k + 3] = tmp; + } + } + } + + /** + * Returns whether bounds intersect a rectangle or not. + */ + protected static int crossBound (float bound[], int bc, float py1, float py2) { + // LEFT/RIGHT + if (bc == 0) { + return 0; + } + + // Check Y coordinate + int up = 0; + int down = 0; + for (int i = 2; i < bc; i += 4) { + if (bound[i] < py1) { + up++; + continue; + } + if (bound[i] > py2) { + down++; + continue; + } + return CROSSING; + } + + // UP + if (down == 0) { + return 0; + } + + if (up != 0) { + // bc >= 2 + sortBound(bound, bc); + boolean sign = bound[2] > py2; + for (int i = 6; i < bc; i += 4) { + boolean sign2 = bound[i] > py2; + if (sign != sign2 && bound[i + 1] != bound[i - 3]) { + return CROSSING; + } + sign = sign2; + } + } + return UNKNOWN; + } + + /** + * Allowable tolerance for bounds comparison + */ + protected static final float DELTA = 1E-5f; + + /** + * If roots have distance less then ROOT_DELTA they are double + */ + protected static final float ROOT_DELTA = 1E-10f; +} diff --git a/src/main/java/pythagoras/f/Dimensions.java b/src/main/java/pythagoras/f/Dimensions.java new file mode 100644 index 0000000..7a54218 --- /dev/null +++ b/src/main/java/pythagoras/f/Dimensions.java @@ -0,0 +1,17 @@ +// +// $Id$ + +package pythagoras.f; + +/** + * Dimension-related utility methods. + */ +public class Dimensions +{ + /** + * Returns a string describing the supplied dimension, of the form widthxheight. + */ + public static String dimenToString (float width, float height) { + return width + "x" + height; + } +} diff --git a/src/main/java/pythagoras/f/FlatteningPathIterator.java b/src/main/java/pythagoras/f/FlatteningPathIterator.java new file mode 100644 index 0000000..5950c37 --- /dev/null +++ b/src/main/java/pythagoras/f/FlatteningPathIterator.java @@ -0,0 +1,257 @@ +// +// Pythagoras - a collection of geometry classes +// http://github.com/samskivert/pythagoras + +package pythagoras.f; + +import java.util.NoSuchElementException; + +public class FlatteningPathIterator implements PathIterator +{ + /** + * The default points buffer size + */ + private static final int BUFFER_SIZE = 16; + + /** + * The default curve subdivision limit + */ + private static final int BUFFER_LIMIT = 16; + + /** + * The points buffer capacity + */ + private static final int BUFFER_CAPACITY = 16; + + /** + * The type of current segment to be flat + */ + int bufType; + + /** + * The curve subdivision limit + */ + int bufLimit; + + /** + * The current points buffer size + */ + int bufSize; + + /** + * The inner cursor position in points buffer + */ + int bufIndex; + + /** + * The current subdivision count + */ + int bufSubdiv; + + /** + * The points buffer + */ + float buf[]; + + /** + * The indicator of empty points buffer + */ + boolean bufEmpty = true; + + /** + * The source PathIterator + */ + PathIterator p; + + /** + * The flatness of new path + */ + float flatness; + + /** + * The square of flatness + */ + float flatness2; + + /** + * The x coordinate of previous path segment + */ + float px; + + /** + * The y coordinate of previous path segment + */ + float py; + + /** + * The tamporary buffer for getting points from PathIterator + */ + float coords[] = new float[6]; + + public FlatteningPathIterator (PathIterator path, float flatness) { + this(path, flatness, BUFFER_LIMIT); + } + + public FlatteningPathIterator (PathIterator path, float flatness, int limit) { + if (flatness < 0) { + throw new IllegalArgumentException("Flatness is less then zero"); + } + if (limit < 0) { + throw new IllegalArgumentException("Limit is less then zero"); + } + if (path == null) { + throw new NullPointerException("Path is null"); + } + this.p = path; + this.flatness = flatness; + this.flatness2 = flatness * flatness; + this.bufLimit = limit; + this.bufSize = Math.min(bufLimit, BUFFER_SIZE); + this.buf = new float[bufSize]; + this.bufIndex = bufSize; + } + + public float getFlatness () { + return flatness; + } + + public int getRecursionLimit () { + return bufLimit; + } + + public int getWindingRule () { + return p.getWindingRule(); + } + + public boolean isDone () { + return bufEmpty && p.isDone(); + } + + /** + * Calculates flat path points for current segment of the source shape. + * + * Line segment is flat by itself. Flatness of quad and cubic curves + * evaluated by getFlatnessSq() method. Curves subdivided until current + * flatness is bigger than user defined and subdivision limit isn't + * exhausted. Single source segment translated to series of buffer points. + * The less flatness the bigger serries. Every currentSegment() call extract + * one point from the buffer. When series completed evaluate() takes next + * source shape segment. + */ + void evaluate () { + if (bufEmpty) { + bufType = p.currentSegment(coords); + } + + switch (bufType) { + case SEG_MOVETO: + case SEG_LINETO: + px = coords[0]; + py = coords[1]; + break; + case SEG_QUADTO: + if (bufEmpty) { + bufIndex -= 6; + buf[bufIndex + 0] = px; + buf[bufIndex + 1] = py; + System.arraycopy(coords, 0, buf, bufIndex + 2, 4); + bufSubdiv = 0; + } + + while (bufSubdiv < bufLimit) { + if (QuadCurve2D.getFlatnessSq(buf, bufIndex) < flatness2) { + break; + } + + // Realloc buffer + if (bufIndex <= 4) { + float tmp[] = new float[bufSize + BUFFER_CAPACITY]; + System.arraycopy(buf, bufIndex, tmp, bufIndex + BUFFER_CAPACITY, bufSize + - bufIndex); + buf = tmp; + bufSize += BUFFER_CAPACITY; + bufIndex += BUFFER_CAPACITY; + } + + QuadCurve2D.subdivide(buf, bufIndex, buf, bufIndex - 4, buf, bufIndex); + + bufIndex -= 4; + bufSubdiv++; + } + + bufIndex += 4; + px = buf[bufIndex]; + py = buf[bufIndex + 1]; + + bufEmpty = (bufIndex == bufSize - 2); + if (bufEmpty) { + bufIndex = bufSize; + bufType = SEG_LINETO; + } + break; + case SEG_CUBICTO: + if (bufEmpty) { + bufIndex -= 8; + buf[bufIndex + 0] = px; + buf[bufIndex + 1] = py; + System.arraycopy(coords, 0, buf, bufIndex + 2, 6); + bufSubdiv = 0; + } + + while (bufSubdiv < bufLimit) { + if (CubicCurve2D.getFlatnessSq(buf, bufIndex) < flatness2) { + break; + } + + // Realloc buffer + if (bufIndex <= 6) { + float tmp[] = new float[bufSize + BUFFER_CAPACITY]; + System.arraycopy(buf, bufIndex, tmp, bufIndex + BUFFER_CAPACITY, bufSize + - bufIndex); + buf = tmp; + bufSize += BUFFER_CAPACITY; + bufIndex += BUFFER_CAPACITY; + } + + CubicCurve2D.subdivide(buf, bufIndex, buf, bufIndex - 6, buf, bufIndex); + + bufIndex -= 6; + bufSubdiv++; + } + + bufIndex += 6; + px = buf[bufIndex]; + py = buf[bufIndex + 1]; + + bufEmpty = (bufIndex == bufSize - 2); + if (bufEmpty) { + bufIndex = bufSize; + bufType = SEG_LINETO; + } + break; + } + + } + + public void next () { + if (bufEmpty) { + p.next(); + } + } + + public int currentSegment (float[] coords) { + if (isDone()) { + throw new NoSuchElementException("Iterator out of bounds"); + } + evaluate(); + int type = bufType; + if (type != SEG_CLOSE) { + coords[0] = px; + coords[1] = py; + if (type != SEG_MOVETO) { + type = SEG_LINETO; + } + } + return type; + } +} diff --git a/src/main/java/pythagoras/f/IShape.java b/src/main/java/pythagoras/f/IShape.java index 0f9dd9f..7511606 100644 --- a/src/main/java/pythagoras/f/IShape.java +++ b/src/main/java/pythagoras/f/IShape.java @@ -56,20 +56,20 @@ public interface IShape */ Rectangle getBounds (); - // /** - // * Returns an iterator over the path described by this shape. - // * - // * @param at if supplied, the points in the path are transformed using this. - // */ - // PathIterator getPathIterator (AffineTransform at); + /** + * Returns an iterator over the path described by this shape. + * + * @param at if supplied, the points in the path are transformed using this. + */ + PathIterator getPathIterator (AffineTransform at); - // /** - // * Returns an iterator over the path described by this shape. - // * - // * @param at if supplied, the points in the path are transformed using this. - // * @param flatness when approximating curved segments with lines, this controls the maximum - // * distance the lines are allowed to deviate from the approximated curve, thus a higher - // * flatness value generally allows for a path with fewer segments. - // */ - // PathIterator getPathIterator (AffineTransform at, float flatness); + /** + * Returns an iterator over the path described by this shape. + * + * @param at if supplied, the points in the path are transformed using this. + * @param flatness when approximating curved segments with lines, this controls the maximum + * distance the lines are allowed to deviate from the approximated curve, thus a higher + * flatness value generally allows for a path with fewer segments. + */ + PathIterator getPathIterator (AffineTransform at, float flatness); } diff --git a/src/main/java/pythagoras/f/IllegalPathStateException.java b/src/main/java/pythagoras/f/IllegalPathStateException.java new file mode 100644 index 0000000..43ace32 --- /dev/null +++ b/src/main/java/pythagoras/f/IllegalPathStateException.java @@ -0,0 +1,20 @@ +// +// Pythagoras - a collection of geometry classes +// http://github.com/samskivert/pythagoras + +package pythagoras.f; + +/** + * An exception thrown if an operation is performed on a {@link Path} that is in an illegal state + * with respect to the particular operation being performed. For example, appending a segment to a + * path without an initial moveto. + */ +public class IllegalPathStateException extends RuntimeException +{ + public IllegalPathStateException () { + } + + public IllegalPathStateException (String s) { + super(s); + } +} diff --git a/src/main/java/pythagoras/f/Line.java b/src/main/java/pythagoras/f/Line.java index fd6e553..c8f5107 100644 --- a/src/main/java/pythagoras/f/Line.java +++ b/src/main/java/pythagoras/f/Line.java @@ -10,9 +10,16 @@ import java.io.Serializable; */ public class Line extends AbstractLine implements Serializable { + /** The x-coordinate of the start of this line segment. */ public float x1; + + /** The y-coordinate of the start of this line segment. */ public float y1; + + /** The x-coordinate of the end of this line segment. */ public float x2; + + /** The y-coordinate of the end of this line segment. */ public float y2; /** diff --git a/src/main/java/pythagoras/f/Geometry.java b/src/main/java/pythagoras/f/Lines.java similarity index 74% rename from src/main/java/pythagoras/f/Geometry.java rename to src/main/java/pythagoras/f/Lines.java index 61dd4ec..2d7662b 100644 --- a/src/main/java/pythagoras/f/Geometry.java +++ b/src/main/java/pythagoras/f/Lines.java @@ -1,30 +1,13 @@ // -// Pythagoras - a collection of geometry classes -// http://github.com/samskivert/pythagoras +// $Id$ package pythagoras.f; /** - * Contains geometry routines that don't fit more nicely into specialized classes. + * Line-related utility methods. */ -public class Geometry +public class Lines { - /** - * Returns the squared Euclidian distance between the specified two points. - */ - public static float distanceSq (float x1, float y1, float x2, float y2) { - x2 -= x1; - y2 -= y1; - return x2 * x2 + y2 * y2; - } - - /** - * Returns the Euclidian distance between the specified two points. - */ - public static float distance (float x1, float y1, float x2, float y2) { - return (float)Math.sqrt(distanceSq(x1, y1, x2, y2)); - } - /** * Returns true if the specified two line segments intersect. */ @@ -137,24 +120,4 @@ public class Geometry public static float pointSegDist (float px, float py, float x1, float y1, float x2, float y2) { return (float)Math.sqrt(pointSegDistSq(px, py, x1, y1, x2, y2)); } - - /** - * Returns a string describing the supplied point, of the form +x+y, - * +x-y, -x-y, etc. - */ - public static String pointToString (float x, float y) { - StringBuilder buf = new StringBuilder(); - if (x >= 0) buf.append("+"); - buf.append(x); - if (y >= 0) buf.append("+"); - buf.append(y); - return buf.toString(); - } - - /** - * Returns a string describing the supplied dimension, of the form widthxheight. - */ - public static String dimenToString (float width, float height) { - return width + "x" + height; - } } diff --git a/src/main/java/pythagoras/f/NoninvertibleTransformException.java b/src/main/java/pythagoras/f/NoninvertibleTransformException.java new file mode 100644 index 0000000..0286276 --- /dev/null +++ b/src/main/java/pythagoras/f/NoninvertibleTransformException.java @@ -0,0 +1,12 @@ +// +// Pythagoras - a collection of geometry classes +// http://github.com/samskivert/pythagoras + +package pythagoras.f; + +public class NoninvertibleTransformException extends java.lang.Exception +{ + public NoninvertibleTransformException (String s) { + super(s); + } +} diff --git a/src/main/java/pythagoras/f/Path.java b/src/main/java/pythagoras/f/Path.java new file mode 100644 index 0000000..1751fcd --- /dev/null +++ b/src/main/java/pythagoras/f/Path.java @@ -0,0 +1,378 @@ +// +// Pythagoras - a collection of geometry classes +// http://github.com/samskivert/pythagoras + +package pythagoras.f; + +import java.util.NoSuchElementException; + +/** + * Represents a path constructed from lines and curves and which can contain subpaths. + */ +public final class Path implements IShape, Cloneable +{ + /** Specifies the even/odd rule for determining the interior of a path. */ + public static final int WIND_EVEN_ODD = PathIterator.WIND_EVEN_ODD; + + /** Specifies the non-zero rule for determining the interior of a path. */ + public static final int WIND_NON_ZERO = PathIterator.WIND_NON_ZERO; + + public Path () { + this(WIND_NON_ZERO, BUFFER_SIZE); + } + + public Path (int rule) { + this(rule, BUFFER_SIZE); + } + + public Path (int rule, int initialCapacity) { + setWindingRule(rule); + types = new byte[initialCapacity]; + points = new float[initialCapacity * 2]; + } + + public Path (IShape shape) { + this(WIND_NON_ZERO, BUFFER_SIZE); + PathIterator p = shape.getPathIterator(null); + setWindingRule(p.getWindingRule()); + append(p, false); + } + + public void setWindingRule (int rule) { + if (rule != WIND_EVEN_ODD && rule != WIND_NON_ZERO) { + throw new IllegalArgumentException("Invalid winding rule value"); + } + this.rule = rule; + } + + public int getWindingRule () { + return rule; + } + + public void moveTo (float x, float y) { + if (typeSize > 0 && types[typeSize - 1] == PathIterator.SEG_MOVETO) { + points[pointSize - 2] = x; + points[pointSize - 1] = y; + } else { + checkBuf(2, false); + types[typeSize++] = PathIterator.SEG_MOVETO; + points[pointSize++] = x; + points[pointSize++] = y; + } + } + + public void lineTo (float x, float y) { + checkBuf(2, true); + types[typeSize++] = PathIterator.SEG_LINETO; + points[pointSize++] = x; + points[pointSize++] = y; + } + + public void quadTo (float x1, float y1, float x2, float y2) { + checkBuf(4, true); + types[typeSize++] = PathIterator.SEG_QUADTO; + points[pointSize++] = x1; + points[pointSize++] = y1; + points[pointSize++] = x2; + points[pointSize++] = y2; + } + + public void curveTo (float x1, float y1, float x2, float y2, float x3, float y3) { + checkBuf(6, true); + types[typeSize++] = PathIterator.SEG_CUBICTO; + points[pointSize++] = x1; + points[pointSize++] = y1; + points[pointSize++] = x2; + points[pointSize++] = y2; + points[pointSize++] = x3; + points[pointSize++] = y3; + } + + public void closePath () { + if (typeSize == 0 || types[typeSize - 1] != PathIterator.SEG_CLOSE) { + checkBuf(0, true); + types[typeSize++] = PathIterator.SEG_CLOSE; + } + } + + public void append (IShape shape, boolean connect) { + PathIterator p = shape.getPathIterator(null); + append(p, connect); + } + + public void append (PathIterator path, boolean connect) { + while (!path.isDone()) { + float coords[] = new float[6]; + switch (path.currentSegment(coords)) { + case PathIterator.SEG_MOVETO: + if (!connect || typeSize == 0) { + moveTo(coords[0], coords[1]); + } else if (types[typeSize - 1] != PathIterator.SEG_CLOSE && + points[pointSize - 2] == coords[0] && + points[pointSize - 1] == coords[1]) { + // we're already here + } else { + lineTo(coords[0], coords[1]); + } + break; + case PathIterator.SEG_LINETO: + lineTo(coords[0], coords[1]); + break; + case PathIterator.SEG_QUADTO: + quadTo(coords[0], coords[1], coords[2], coords[3]); + break; + case PathIterator.SEG_CUBICTO: + curveTo(coords[0], coords[1], coords[2], coords[3], coords[4], coords[5]); + break; + case PathIterator.SEG_CLOSE: + closePath(); + break; + } + path.next(); + connect = false; + } + } + + public Point getCurrentPoint () { + if (typeSize == 0) { + return null; + } + int j = pointSize - 2; + if (types[typeSize - 1] == PathIterator.SEG_CLOSE) { + for (int i = typeSize - 2; i > 0; i--) { + int type = types[i]; + if (type == PathIterator.SEG_MOVETO) { + break; + } + j -= pointShift[type]; + } + } + return new Point(points[j], points[j + 1]); + } + + public void reset () { + typeSize = 0; + pointSize = 0; + } + + public void transform (AffineTransform t) { + t.transform(points, 0, points, 0, pointSize / 2); + } + + public IShape createTransformedShape (AffineTransform t) { + Path p = clone(); + if (t != null) { + p.transform(t); + } + return p; + } + + /** + * {@inheritDoc} + * + * Note: this method violates the contract in that the bounds returned will + * not reflect subsequent changes to this path. Doing so is prohibitively expensive. + */ + @Override public IRectangle bounds () { + return getBounds(); + } + + @Override // from interface IShape + public Rectangle getBounds () { + float rx1, ry1, rx2, ry2; + if (pointSize == 0) { + rx1 = ry1 = rx2 = ry2 = 0f; + } else { + int i = pointSize - 1; + ry1 = ry2 = points[i--]; + rx1 = rx2 = points[i--]; + while (i > 0) { + float y = points[i--]; + float x = points[i--]; + if (x < rx1) { + rx1 = x; + } else if (x > rx2) { + rx2 = x; + } + if (y < ry1) { + ry1 = y; + } else if (y > ry2) { + ry2 = y; + } + } + } + return new Rectangle(rx1, ry1, rx2 - rx1, ry2 - ry1); + } + + @Override // from interface IShape + public boolean isEmpty () { + // TODO: will this be insanely difficult to do correctly? + return getBounds().isEmpty(); + } + + @Override // from interface IShape + public boolean contains (float px, float py) { + return isInside(Crossing.crossShape(this, px, py)); + } + + @Override // from interface IShape + public boolean contains (float rx, float ry, float rw, float rh) { + int cross = Crossing.intersectShape(this, rx, ry, rw, rh); + return cross != Crossing.CROSSING && isInside(cross); + } + + @Override // from interface IShape + public boolean intersects (float rx, float ry, float rw, float rh) { + int cross = Crossing.intersectShape(this, rx, ry, rw, rh); + return cross == Crossing.CROSSING || isInside(cross); + } + + @Override // from interface IShape + public boolean contains (IPoint p) { + return contains(p.getX(), p.getY()); + } + + @Override // from interface IShape + public boolean contains (IRectangle r) { + return contains(r.getX(), r.getY(), r.getWidth(), r.getHeight()); + } + + @Override // from interface IShape + public boolean intersects (IRectangle r) { + return intersects(r.getX(), r.getY(), r.getWidth(), r.getHeight()); + } + + @Override // from interface IShape + public PathIterator getPathIterator (AffineTransform t) { + return new Iterator(this, t); + } + + @Override // from interface IShape + public PathIterator getPathIterator (AffineTransform t, float flatness) { + return new FlatteningPathIterator(getPathIterator(t), flatness); + } + + @Override + public Path clone () { + try { + Path p = (Path)super.clone(); + p.types = types.clone(); + p.points = points.clone(); + return p; + } catch (CloneNotSupportedException e) { + throw new InternalError(); + } + } + + /** + * Checks points and types buffer size to add pointCount points. If necessary realloc buffers + * to enlarge size. + * + * @param pointCount the point count to be added in buffer + */ + protected void checkBuf (int pointCount, boolean checkMove) { + if (checkMove && typeSize == 0) { + throw new IllegalPathStateException("First segment must be a SEG_MOVETO"); + } + if (typeSize == types.length) { + byte tmp[] = new byte[typeSize + BUFFER_CAPACITY]; + System.arraycopy(types, 0, tmp, 0, typeSize); + types = tmp; + } + if (pointSize + pointCount > points.length) { + float tmp[] = new float[pointSize + Math.max(BUFFER_CAPACITY * 2, pointCount)]; + System.arraycopy(points, 0, tmp, 0, pointSize); + points = tmp; + } + } + + /** + * Checks cross count according to path rule to define is it point inside shape or not. + * + * @param cross the point cross count. + * @return true if point is inside path, or false otherwise. + */ + protected boolean isInside (int cross) { + return (rule == WIND_NON_ZERO) ? Crossing.isInsideNonZero(cross) : + Crossing.isInsideEvenOdd(cross); + } + + /** An iterator over a {@link Path}. */ + protected static class Iterator implements PathIterator + { + /** The current cursor position in types buffer. */ + private int typeIndex; + + /** The current cursor position in points buffer. */ + private int pointIndex; + + /** The source Path object. */ + private Path p; + + /** The path iterator transformation. */ + private AffineTransform t; + + Iterator (Path path) { + this(path, null); + } + + Iterator (Path path, AffineTransform at) { + this.p = path; + this.t = at; + } + + @Override public int getWindingRule () { + return p.getWindingRule(); + } + + @Override public boolean isDone () { + return typeIndex >= p.typeSize; + } + + @Override public void next () { + typeIndex++; + } + + @Override public int currentSegment (float[] coords) { + if (isDone()) { + throw new NoSuchElementException("Iterator out of bounds"); + } + int type = p.types[typeIndex]; + int count = Path.pointShift[type]; + System.arraycopy(p.points, pointIndex, coords, 0, count); + if (t != null) { + t.transform(coords, 0, coords, 0, count / 2); + } + pointIndex += count; + return type; + } + } + + /** The point's types buffer. */ + protected byte[] types; + + /** The points buffer. */ + protected float[] points; + + /** The point's type buffer size. */ + protected int typeSize; + + /** The points buffer size. */ + protected int pointSize; + + /* The path rule. */ + protected int rule; + + /** The space required in points buffer for different segmenet types. */ + protected static int pointShift[] = { 2, // MOVETO + 2, // LINETO + 4, // QUADTO + 6, // CUBICTO + 0 }; // CLOSE + + /** The default initial buffer size. */ + protected static final int BUFFER_SIZE = 10; + + /** The amount by which to expand the buffer capacity. */ + protected static final int BUFFER_CAPACITY = 10; +} diff --git a/src/main/java/pythagoras/f/PathIterator.java b/src/main/java/pythagoras/f/PathIterator.java new file mode 100644 index 0000000..370cd7c --- /dev/null +++ b/src/main/java/pythagoras/f/PathIterator.java @@ -0,0 +1,61 @@ +// +// Pythagoras - a collection of geometry classes +// http://github.com/samskivert/pythagoras + +package pythagoras.f; + +/** + * Used to return the boundary of a {@link IShape}, one segment at a time. + */ +public interface PathIterator +{ + /** Specifies the even/odd rule for determining the interior of a path. */ + int WIND_EVEN_ODD = 0; + + /** Specifies the non-zero rule for determining the interior of a path. */ + int WIND_NON_ZERO = 1; + + /** Indicates the starting location for a new subpath. */ + int SEG_MOVETO = 0; + + /** Indicates the end point of a line to be drawn from the most recently specified point. */ + int SEG_LINETO = 1; + + /** Indicates a pair of points that specify a quadratic parametric curve to be drawn from the + * most recently specified point. */ + int SEG_QUADTO = 2; + + /** Indicates a pair of points that specify a cubic parametric curve to be drawn from the most + * recently specified point. */ + int SEG_CUBICTO = 3; + + /** Indicates that the preceding subpath should be closed by appending a line segment back to + * the point corresponding to the most recent {@link #SEG_MOVETO}. */ + int SEG_CLOSE = 4; + + /** + * Returns the winding rule used to determine the interior of this path. + */ + int getWindingRule (); + + /** + * Returns true if this path has no additional segments. + */ + boolean isDone (); + + /** + * Advances this path to the next segment. + */ + void next (); + + /** + * Returns the coordinates and type of the current path segment. The number of points stored in + * {@code coords} differs by path segment type: 0 - {@link #SEG_CLOSE}, 1 - {@link + * #SEG_MOVETO}, {@link #SEG_LINETO}, 2 - {@link #SEG_QUADTO}, 3 - {@link #SEG_CUBICTO}. + * + * @param coords a buffer into which the current coordinates will be copied. It must be of + * length 6. Each point is stored as a pair of x,y coordinates. + * @return the path segment type, e.g. {@link #SEG_MOVETO}. + */ + int currentSegment (float[] coords); +} diff --git a/src/main/java/pythagoras/f/Points.java b/src/main/java/pythagoras/f/Points.java new file mode 100644 index 0000000..621cc3e --- /dev/null +++ b/src/main/java/pythagoras/f/Points.java @@ -0,0 +1,39 @@ +// +// $Id$ + +package pythagoras.f; + +/** + * Point-related utility methods. + */ +public class Points +{ + /** + * Returns the squared Euclidian distance between the specified two points. + */ + public static float distanceSq (float x1, float y1, float x2, float y2) { + x2 -= x1; + y2 -= y1; + return x2 * x2 + y2 * y2; + } + + /** + * Returns the Euclidian distance between the specified two points. + */ + public static float distance (float x1, float y1, float x2, float y2) { + return (float)Math.sqrt(distanceSq(x1, y1, x2, y2)); + } + + /** + * Returns a string describing the supplied point, of the form +x+y, + * +x-y, -x-y, etc. + */ + public static String pointToString (float x, float y) { + StringBuilder buf = new StringBuilder(); + if (x >= 0) buf.append("+"); + buf.append(x); + if (y >= 0) buf.append("+"); + buf.append(y); + return buf.toString(); + } +}