diff --git a/build.xml b/build.xml index 057d526..65a0932 100644 --- a/build.xml +++ b/build.xml @@ -14,8 +14,9 @@ + - + @@ -30,7 +31,6 @@ - @@ -51,10 +50,10 @@ - + - diff --git a/pom.xml b/pom.xml index 88caf8e..e8193e7 100644 --- a/pom.xml +++ b/pom.xml @@ -43,6 +43,12 @@ + + junit + junit + 4.8.1 + test + diff --git a/src/main/java/pythagoras/f/AbstractArc.java b/src/main/java/pythagoras/f/AbstractArc.java index bed858e..f6efee0 100644 --- a/src/main/java/pythagoras/f/AbstractArc.java +++ b/src/main/java/pythagoras/f/AbstractArc.java @@ -20,9 +20,8 @@ public abstract class AbstractArc extends RectangularShape implements IArc @Override // from interface IArc public Point getStartPoint (Point target) { float a = FloatMath.toRadians(getAngleStart()); - target.setLocation(getX() + (1f + FloatMath.cos(a)) * getWidth() / 2f, - getY() + (1f - FloatMath.sin(a)) * getHeight() / 2f); - return target; + return target.set(getX() + (1f + FloatMath.cos(a)) * getWidth() / 2f, + getY() + (1f - FloatMath.sin(a)) * getHeight() / 2f); } @Override // from interface IArc @@ -33,9 +32,8 @@ public abstract class AbstractArc extends RectangularShape implements IArc @Override // from interface IArc public Point getEndPoint (Point target) { float a = FloatMath.toRadians(getAngleStart() + getAngleExtent()); - target.setLocation(getX() + (1f + FloatMath.cos(a)) * getWidth() / 2f, - getY() + (1f - FloatMath.sin(a)) * getHeight() / 2f); - return target; + return target.set(getX() + (1f + FloatMath.cos(a)) * getWidth() / 2f, + getY() + (1f - FloatMath.sin(a)) * getHeight() / 2f); } @Override // from interface IArc @@ -189,7 +187,7 @@ public abstract class AbstractArc extends RectangularShape implements IArc } @Override // from interface IShape - public PathIterator getPathIterator (AffineTransform at) { + public PathIterator getPathIterator (Transform at) { return new Iterator(this, at); } @@ -223,7 +221,7 @@ public abstract class AbstractArc extends RectangularShape implements IArc private int type; /** The path iterator transformation */ - private AffineTransform t; + private Transform t; /** The current segment index */ private int index; @@ -259,7 +257,7 @@ public abstract class AbstractArc extends RectangularShape implements IArc /** The y coordinate of the first path point (MOVE_TO) */ private float my; - Iterator (IArc a, AffineTransform t) { + Iterator (IArc a, Transform t) { this.width = a.getWidth() / 2f; this.height = a.getHeight() / 2f; this.x = a.getX() + width; diff --git a/src/main/java/pythagoras/f/AbstractCubicCurve.java b/src/main/java/pythagoras/f/AbstractCubicCurve.java index a0b3526..c9c870f 100644 --- a/src/main/java/pythagoras/f/AbstractCubicCurve.java +++ b/src/main/java/pythagoras/f/AbstractCubicCurve.java @@ -111,12 +111,12 @@ public abstract class AbstractCubicCurve implements ICubicCurve } @Override // from interface IShape - public PathIterator getPathIterator (AffineTransform t) { + public PathIterator getPathIterator (Transform t) { return new Iterator(this, t); } @Override // from interface IShape - public PathIterator getPathIterator (AffineTransform at, float flatness) { + public PathIterator getPathIterator (Transform at, float flatness) { return new FlatteningPathIterator(getPathIterator(at), flatness); } @@ -124,10 +124,10 @@ public abstract class AbstractCubicCurve implements ICubicCurve protected static class Iterator implements PathIterator { private ICubicCurve c; - private AffineTransform t; + private Transform t; private int index; - Iterator (ICubicCurve c, AffineTransform t) { + Iterator (ICubicCurve c, Transform t) { this.c = c; this.t = t; } diff --git a/src/main/java/pythagoras/f/AbstractEllipse.java b/src/main/java/pythagoras/f/AbstractEllipse.java index a7870fa..070a0e7 100644 --- a/src/main/java/pythagoras/f/AbstractEllipse.java +++ b/src/main/java/pythagoras/f/AbstractEllipse.java @@ -44,7 +44,7 @@ public abstract class AbstractEllipse extends RectangularShape implements IEllip } @Override // from interface IShape - public PathIterator getPathIterator (AffineTransform at) { + public PathIterator getPathIterator (Transform at) { return new Iterator(this, at); } @@ -52,10 +52,10 @@ public abstract class AbstractEllipse extends RectangularShape implements IEllip protected static class Iterator implements PathIterator { private final float x, y, width, height; - private final AffineTransform t; + private final Transform t; private int index; - Iterator (IEllipse e, AffineTransform t) { + Iterator (IEllipse e, Transform t) { this.x = e.getX(); this.y = e.getY(); this.width = e.getWidth(); diff --git a/src/main/java/pythagoras/f/AbstractLine.java b/src/main/java/pythagoras/f/AbstractLine.java index d0553dd..6a9fbbb 100644 --- a/src/main/java/pythagoras/f/AbstractLine.java +++ b/src/main/java/pythagoras/f/AbstractLine.java @@ -19,8 +19,7 @@ public abstract class AbstractLine implements ILine @Override // from interface ILine public Point getP1 (Point target) { - target.setLocation(getX1(), getY1()); - return target; + return target.set(getX1(), getY1()); } @Override // from interface ILine @@ -30,8 +29,7 @@ public abstract class AbstractLine implements ILine @Override // from interface ILine public Point getP2 (Point target) { - target.setLocation(getX2(), getY2()); - return target; + return target.set(getX2(), getY2()); } @Override // from interface ILine @@ -152,12 +150,12 @@ public abstract class AbstractLine implements ILine } @Override // from interface IShape - public PathIterator getPathIterator (AffineTransform at) { + public PathIterator getPathIterator (Transform at) { return new Iterator(this, at); } @Override // from interface IShape - public PathIterator getPathIterator (AffineTransform at, float flatness) { + public PathIterator getPathIterator (Transform at, float flatness) { return new Iterator(this, at); } @@ -165,10 +163,10 @@ public abstract class AbstractLine implements ILine protected static class Iterator implements PathIterator { private float x1, y1, x2, y2; - private AffineTransform t; + private Transform t; private int index; - Iterator (ILine l, AffineTransform at) { + Iterator (ILine l, Transform at) { this.x1 = l.getX1(); this.y1 = l.getY1(); this.x2 = l.getX2(); diff --git a/src/main/java/pythagoras/f/AbstractPoint.java b/src/main/java/pythagoras/f/AbstractPoint.java index 5066483..f18a4fa 100644 --- a/src/main/java/pythagoras/f/AbstractPoint.java +++ b/src/main/java/pythagoras/f/AbstractPoint.java @@ -12,27 +12,59 @@ import pythagoras.util.Platform; */ public abstract class AbstractPoint implements IPoint { - @Override // from interface IPoint + @Override // from IPoint public float distanceSq (float px, float py) { return Points.distanceSq(getX(), getY(), px, py); } - @Override // from interface IPoint + @Override // from IPoint public float distanceSq (IPoint p) { return Points.distanceSq(getX(), getY(), p.getX(), p.getY()); } - @Override // from interface IPoint + @Override // from IPoint public float distance (float px, float py) { return Points.distance(getX(), getY(), px, py); } - @Override // from interface IPoint + @Override // from IPoint public float distance (IPoint p) { return Points.distance(getX(), getY(), p.getX(), p.getY()); } - @Override // from interface IPoint + @Override // from IPoint + public Point mult (float s) { + return mult(s, new Point()); + } + + @Override // from IPoint + public Point mult (float s, Point result) { + return result.set(getX() * s, getY() * s); + } + + @Override // from IPoint + public Point add (float x, float y) { + return new Point(getX() + x, getY() + y); + } + + @Override // from IPoint + public Point add (float x, float y, Point result) { + return result.set(getX() + x, getY() + y); + } + + @Override // from IPoint + public Point rotate (float angle) { + return rotate(angle, new Point()); + } + + @Override // from IPoint + public Point rotate (float angle, Point result) { + float x = getX(), y = getY(); + float sina = FloatMath.sin(angle), cosa = FloatMath.cos(angle); + return result.set(x*cosa - y*sina, x*sina + y*cosa); + } + + @Override // from IPoint public Point clone () { return new Point(this); } diff --git a/src/main/java/pythagoras/f/AbstractQuadCurve.java b/src/main/java/pythagoras/f/AbstractQuadCurve.java index 1649eb5..865da78 100644 --- a/src/main/java/pythagoras/f/AbstractQuadCurve.java +++ b/src/main/java/pythagoras/f/AbstractQuadCurve.java @@ -102,12 +102,12 @@ public abstract class AbstractQuadCurve implements IQuadCurve } @Override // from interface IShape - public PathIterator getPathIterator (AffineTransform t) { + public PathIterator getPathIterator (Transform t) { return new Iterator(this, t); } @Override // from interface IShape - public PathIterator getPathIterator (AffineTransform t, float flatness) { + public PathIterator getPathIterator (Transform t, float flatness) { return new FlatteningPathIterator(getPathIterator(t), flatness); } @@ -115,10 +115,10 @@ public abstract class AbstractQuadCurve implements IQuadCurve protected static class Iterator implements PathIterator { private IQuadCurve c; - private AffineTransform t; + private Transform t; private int index; - Iterator (IQuadCurve q, AffineTransform t) { + Iterator (IQuadCurve q, Transform t) { this.c = q; this.t = t; } diff --git a/src/main/java/pythagoras/f/AbstractRectangle.java b/src/main/java/pythagoras/f/AbstractRectangle.java index 6234c9b..3cf4c40 100644 --- a/src/main/java/pythagoras/f/AbstractRectangle.java +++ b/src/main/java/pythagoras/f/AbstractRectangle.java @@ -21,8 +21,7 @@ public abstract class AbstractRectangle extends RectangularShape implements IRec @Override // from interface IRectangle public Point getLocation (Point target) { - target.setLocation(getX(), getY()); - return target; + return target.set(getX(), getY()); } @Override // from interface IRectangle @@ -129,12 +128,12 @@ public abstract class AbstractRectangle extends RectangularShape implements IRec } @Override // from interface IShape - public PathIterator getPathIterator (AffineTransform t) { + public PathIterator getPathIterator (Transform t) { return new Iterator(this, t); } @Override // from interface IShape - public PathIterator getPathIterator (AffineTransform t, float flatness) { + public PathIterator getPathIterator (Transform t, float flatness) { return new Iterator(this, t); } @@ -167,12 +166,12 @@ public abstract class AbstractRectangle extends RectangularShape implements IRec protected static class Iterator implements PathIterator { private float x, y, width, height; - private AffineTransform t; + private Transform t; /** The current segment index. */ private int index; - Iterator (IRectangle r, AffineTransform at) { + Iterator (IRectangle r, Transform at) { this.x = r.getX(); this.y = r.getY(); this.width = r.getWidth(); diff --git a/src/main/java/pythagoras/f/AbstractRoundRectangle.java b/src/main/java/pythagoras/f/AbstractRoundRectangle.java index 2562e49..0e534c5 100644 --- a/src/main/java/pythagoras/f/AbstractRoundRectangle.java +++ b/src/main/java/pythagoras/f/AbstractRoundRectangle.java @@ -75,7 +75,7 @@ public abstract class AbstractRoundRectangle extends RectangularShape implements } @Override // from interface IShape - public PathIterator getPathIterator (AffineTransform at) { + public PathIterator getPathIterator (Transform at) { return new Iterator(this, at); } @@ -83,10 +83,10 @@ public abstract class AbstractRoundRectangle extends RectangularShape implements protected static class Iterator implements PathIterator { private final float x, y, width, height, aw, ah; - private final AffineTransform t; + private final Transform t; private int index; - Iterator (IRoundRectangle rr, AffineTransform at) { + Iterator (IRoundRectangle rr, Transform at) { this.x = rr.getX(); this.y = rr.getY(); this.width = rr.getWidth(); diff --git a/src/main/java/pythagoras/f/AbstractTransform.java b/src/main/java/pythagoras/f/AbstractTransform.java new file mode 100644 index 0000000..19cf17f --- /dev/null +++ b/src/main/java/pythagoras/f/AbstractTransform.java @@ -0,0 +1,73 @@ +// +// Pythagoras - a collection of geometry classes +// http://github.com/samskivert/pythagoras + +package pythagoras.f; + +/** + * Implements some code shared by the various {@link Transform} implementations. + */ +public abstract class AbstractTransform implements Transform +{ + @Override // from Transform + public Vector getScale () { + return new Vector(getScaleX(), getScaleY()); + } + + @Override // from Transform + public Vector getTranslation () { + return new Vector(getTx(), getTy()); + } + + @Override // from Transform + public Transform setUniformScale (float scale) { + throw new UnsupportedOperationException(); + } + + @Override // from Transform + public Transform setScale (float scaleX, float scaleY) { + setScaleX(scaleX); + setScaleY(scaleY); + return this; + } + + @Override // from Transform + public Transform setScaleX (float scaleX) { + throw new UnsupportedOperationException(); + } + + @Override // from Transform + public Transform setScaleY (float scaleY) { + throw new UnsupportedOperationException(); + } + + @Override // from Transform + public Transform setRotation (float angle) { + throw new UnsupportedOperationException(); + } + + @Override // from Transform + public Transform setTranslation (float tx, float ty) { + setTx(tx); + setTy(ty); + return this; + } + + @Override // from Transform + public Transform setTx (float tx) { + throw new UnsupportedOperationException(); + } + + @Override // from Transform + public Transform setTy (float ty) { + throw new UnsupportedOperationException(); + } + + @Override // from Transform + public Transform setTransform (float m00, float m01, float m10, float m11, float tx, float ty) { + throw new UnsupportedOperationException(); + } + + @Override // from Transform + public abstract Transform clone (); +} diff --git a/src/main/java/pythagoras/f/AffineTransform.java b/src/main/java/pythagoras/f/AffineTransform.java index d46609c..287c8d8 100644 --- a/src/main/java/pythagoras/f/AffineTransform.java +++ b/src/main/java/pythagoras/f/AffineTransform.java @@ -4,317 +4,144 @@ package pythagoras.f; -import java.io.Serializable; - import pythagoras.util.NoninvertibleTransformException; -import pythagoras.util.Platform; /** - * 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 + * Implements an affine (3x2 matrix) transform. The transformation matrix has the form: + *
{@code
+ * [ m00, m10, 0 ]
+ * [ m01, m11, 0 ]
+ * [  tx,  ty, 1 ]
+ * }
*/ -public class AffineTransform implements Cloneable, Serializable +public class AffineTransform extends AbstractTransform { - 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; + /** Identifies the affine transform in {@link #generality}. */ + public static final int GENERALITY = 4; - /** - * Returns a transform that performs the specified translation. - */ - public static AffineTransform getTranslateInstance (float tx, float ty) { - AffineTransform t = new AffineTransform(); - t.setToTranslation(tx, ty); - return t; - } + /** The scale, rotation and shear components of this transform. */ + public float m00, m01, m10, m11; - /** - * Returns a transform that performs the specified scale. - */ - public static AffineTransform getScaleInstance (float scx, float scY) { - AffineTransform t = new AffineTransform(); - t.setToScale(scx, scY); - return t; - } + /** The translation components of this transform. */ + public float tx, ty; - /** - * Returns a transform that performs the specified shear. - */ - public static AffineTransform getShearInstance (float shx, float shy) { - AffineTransform m = new AffineTransform(); - m.setToShear(shx, shy); - return m; - } - - /** - * Returns a transform that performs the specified rotation. - */ - public static AffineTransform getRotateInstance (float angle) { - AffineTransform t = new AffineTransform(); - t.setToRotation(angle); - return t; - } - - /** - * Returns a transform that performs the specified rotation. - */ - public static AffineTransform getRotateInstance (float angle, float x, float y) { - AffineTransform t = new AffineTransform(); - t.setToRotation(angle, x, y); - return t; - } - - /** - * Constructs an identity transform. - */ + /** Creates an affine transform configured with the identity transform. */ public AffineTransform () { - setToIdentity(); + this(1, 0, 0, 1, 0, 0); } - /** - * Constructs a transform that is a copy of the supplied transform. - */ - public AffineTransform (AffineTransform t) { - setTransform(t); + /** Creates an affine transform from the supplied scale, rotation and translation. */ + public AffineTransform (float scale, float angle, float tx, float ty) { + this(scale, scale, angle, tx, ty); } - /** - * Constructs a transform with the specified transformation matrix. - */ - public AffineTransform (float m00, float m10, float m01, float m11, float m02, float m12) { - setTransform(m00, m10, m01, m11, m02, m12); + /** Creates an affine transform from the supplied scale, rotation and translation. */ + public AffineTransform (float scaleX, float scaleY, float angle, float tx, float ty) { + float sina = FloatMath.sin(angle), cosa = FloatMath.cos(angle); + this.m00 = cosa * scaleX; this.m01 = -sina * scaleX; + this.m10 = sina * scaleY; this.m11 = cosa * scaleY; + this.tx = tx; this.ty = ty; } - /** - * Constructs a transform with the specified transformation matrix. - * - * @param matrix either {@code [m00, m10, m01, m11]} or {@code [m00, m10, m01, m11, m02, 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]; - } + /** Creates an affine transform with the specified transform matrix. */ + public AffineTransform (float m00, float m01, float m10, float m11, float tx, float ty) { + this.m00 = m00; this.m01 = m01; + this.m10 = m10; this.m11 = m11; + this.tx = tx; this.ty = ty; } - /** - * Returns the type of this affine transform, which is a bitwise-or of the type flags - * ({@link #TYPE_TRANSLATION}, etc.). - */ - 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; + @Override // from Transform + public float getUniformScale () { + // the square root of the signed area of the parallelogram spanned by the axis vectors + float cp = m00*m11 - m01*m10; + return (cp < 0f) ? -FloatMath.sqrt(-cp) : FloatMath.sqrt(cp); } - /** - * Returns the x-component of the scale vector. - */ + @Override // from Transform public float getScaleX () { - return m00; + return FloatMath.sqrt(m00*m00 + m01*m01); } - /** - * Returns the y-component of the scale vector. - */ + @Override // from Transform public float getScaleY () { - return m11; + return FloatMath.sqrt(m10*m10 + m11*m11); } - /** - * Returns the x-component of the shear vector. - */ - public float getShearX () { - return m01; - } + @Override // from Transform + public float getRotation () { + // use the iterative polar decomposition algorithm described by Ken Shoemake: + // http://www.cs.wisc.edu/graphics/Courses/838-s2002/Papers/polar-decomp.pdf - /** - * Returns the y-component of the shear vector. - */ - public float getShearY () { - return m10; - } + // start with the contents of the upper 2x2 portion of the matrix + float n00 = m00, n10 = m10; + float n01 = m01, n11 = m11; + for (int ii = 0; ii < 10; ii++) { + // store the results of the previous iteration + float o00 = n00, o10 = n10; + float o01 = n01, o11 = n11; - /** - * Returns the x-component of the translation vector. - */ - public float getTranslateX () { - return m02; - } + // compute average of the matrix with its inverse transpose + float det = o00*o11 - o10*o01; + if (Math.abs(det) == 0f) { + // determinant is zero; matrix is not invertible + throw new NoninvertibleTransformException(this.toString()); + } + float hrdet = 0.5f / det; + n00 = +o11 * hrdet + o00*0.5f; + n10 = -o01 * hrdet + o10*0.5f; - /** - * Returns the y-component of the translation vector. - */ - public float getTranslateY () { - return m12; - } + n01 = -o10 * hrdet + o01*0.5f; + n11 = +o00 * hrdet + o11*0.5f; - /** - * Returns true if this transform is the identity. - */ - public boolean isIdentity () { - return getType() == TYPE_IDENTITY; - } - - /** - * Fills in the supplied matrix with this transform's values. - * - * @param matrix either a length-4 or length-6 array. - */ - 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; + // compute the difference; if it's small enough, we're done + float d00 = n00 - o00, d10 = n10 - o10; + float d01 = n01 - o01, d11 = n11 - o11; + if (d00*d00 + d10*d10 + d01*d01 + d11*d11 < FloatMath.EPSILON) { + break; + } } + // now that we have a nice orthogonal matrix, we can extract the rotation + return FloatMath.atan2(n01, n00); } - /** - * Returns the determinant of this - * matrix. - */ - public float getDeterminant () { - return m00 * m11 - m01 * m10; + @Override // from Transform + public float getTx () { + return this.tx; } - /** - * Sets this transform's values. - */ - 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; + @Override // from Transform + public float getTy () { + return this.tx; } - /** - * Sets this transform's values to be equal to those of the supplied transform. - */ - public void setTransform (AffineTransform t) { - setTransform(t.m00, t.m10, t.m01, t.m11, t.m02, t.m12); - type = t.type; + @Override // from Transform + public Transform setUniformScale (float scale) { + return setScale(scale, scale); } - /** - * Sets this transform to the identity transform. Any existing transform values are - * overwritten. - */ - public void setToIdentity () { - type = TYPE_IDENTITY; - m00 = m11 = 1f; - m10 = m01 = m02 = m12 = 0; + @Override // from Transform + public Transform setScaleX (float scaleX) { + // normalize the scale to 1, then re-apply + float osx = getScaleX(); + m00 /= osx; m01 /= osx; + m00 *= scaleX; m01 *= scaleX; + return this; } - /** - * Sets this transform to a simple translation using the supplied values. Any existing - * transform values are overwritten. - */ - public void setToTranslation (float tx, float ty) { - m00 = m11 = 1f; - m01 = m10 = 0; - m02 = tx; - m12 = ty; - if (tx == 0 && ty == 0) { - type = TYPE_IDENTITY; - } else { - type = TYPE_TRANSLATION; - } + @Override // from Transform + public Transform setScaleY (float scaleY) { + // normalize the scale to 1, then re-apply + float osy = getScaleY(); + m10 /= osy; m11 /= osy; + m10 *= scaleY; m11 *= scaleY; + return this; } - /** - * Sets this transform to a simple scale using the supplied values. Any existing transform - * values are overwritten. - */ - 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; - } - } - - /** - * Sets this transform to a simple shear using the supplied values. Any existing transform - * values are overwritten. - */ - 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; - } - } - - /** - * Sets this transform to a simple rotation using the supplied values. Any existing transform - * values are overwritten. - * - * @param angle the angle of rotation (in radians). - */ - public void setToRotation (float angle) { - float sin = FloatMath.sin(angle); - float cos = FloatMath.cos(angle); + @Override // from Transform + public Transform setRotation (float angle) { + // extract the scale, then reapply rotation and scale together + float sx = getScaleX(), sy = getScaleY(); + float sin = FloatMath.sin(angle), cos = FloatMath.cos(angle); if (Math.abs(cos) < ZERO) { cos = 0; sin = sin > 0 ? 1f : -1f; @@ -322,329 +149,160 @@ public class AffineTransform implements Cloneable, Serializable sin = 0; cos = cos > 0 ? 1f : -1f; } - m00 = m11 = cos; - m01 = -sin; - m10 = sin; - m02 = m12 = 0; - type = TYPE_UNKNOWN; + m00 = cos * sx; m01 = -sin * sx; + m10 = sin * sy; m11 = cos * sy; + return this; } - /** - * Sets this transform to a simple rotation using the supplied values. Any existing transform - * values are overwritten. - * - * @param angle the angle of rotation (in radians). - * @param px the x-coordinate of the point around which to rotate. - * @param py the y-coordinate of the point around which to rotate. - */ - 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; + @Override // from Transform + public Transform setTranslation (float tx, float ty) { + this.tx = tx; + this.ty = ty; + return this; } - /** - * Concatenates the specified translation to this transform. - */ - public void translate (float tx, float ty) { - concatenate(getTranslateInstance(tx, ty)); + @Override // from Transform + public Transform setTx (float tx) { + this.tx = tx; + return this; } - /** - * Concatenates the specified scale to this transform. - */ - public void scale (float scx, float scy) { - concatenate(getScaleInstance(scx, scy)); + @Override // from Transform + public Transform setTy (float ty) { + this.ty = ty; + return this; } - /** - * Concatenates the specified shear to this transform. - */ - public void shear (float shx, float shy) { - concatenate(getShearInstance(shx, shy)); + @Override // from Transform + public Transform setTransform (float m00, float m01, float m10, float m11, float tx, float ty) { + this.m00 = m00; + this.m01 = m01; + this.m10 = m10; + this.m11 = m11; + this.tx = tx; + this.ty = ty; + return this; } - /** - * Concatenates the specified rotation to this transform. - */ - public void rotate (float angle) { - concatenate(getRotateInstance(angle)); - } - - /** - * Concatenates the specified rotation to this transform. - */ - public void rotate (float angle, float px, float py) { - concatenate(getRotateInstance(angle, px, py)); - } - - /** - * Concatenates the specified transform to this transform. - */ - public void concatenate (AffineTransform t) { - multiply(t, this, this); - } - - /** - * Pre-concatenates the specified transform to this transform. - */ - public void preConcatenate (AffineTransform t) { - multiply(this, t, this); - } - - /** - * Computes the inverse of this transform and stores it in the supplied target. - * - * @return the supplied target. - * @throws NoninvertibleTransformException if this transform cannot be inverted. - */ - public AffineTransform createInverse (AffineTransform target) - throws NoninvertibleTransformException { - float det = getDeterminant(); - if (Math.abs(det) < ZERO) { - throw new NoninvertibleTransformException("Determinant is zero"); + @Override // from Transform + public Transform invert () { + // compute the determinant, storing the subdeterminants for later use + float det = m00*m11 - m10*m01; + if (Math.abs(det) == 0f) { + // determinant is zero; matrix is not invertible + throw new NoninvertibleTransformException(this.toString()); } - target.setTransform(m11 / det, // m00 - -m10 / det, // m10 - -m01 / det, // m01 - m00 / det, // m11 - (m01 * m12 - m11 * m02) / det, // m02 - (m10 * m02 - m00 * m12) / det); // m12 - return target; + float rdet = 1f / det; + return new AffineTransform( + +m11 * rdet, -m01 * rdet, + -m10 * rdet, +m00 * rdet, + (m01*ty - m11*tx) * rdet, (m10*tx - m00*ty) * rdet); } - /** - * Computes and returns the inverse of this transform. - * - * @return the supplied target. - * @throws NoninvertibleTransformException if this transform cannot be inverted. - */ - public AffineTransform createInverse () throws NoninvertibleTransformException { - return createInverse(new AffineTransform()); + @Override // from Transform + public Transform concatenate (Transform other) { + if (generality() < other.generality()) { + return other.preConcatenate(this); + } + return multiply((other instanceof AffineTransform) ? + (AffineTransform)other : new AffineTransform(other)); } - /** - * Transforms the supplied point using this transform's matrix. - * - * @param src the point to be transformed. - * @param dst the point in which to store the transformed values, if null a new instance will - * be created. May be {@code src}. - * @return the supplied (or created) destination point. - */ - public Point transform (IPoint src, Point dst) { - if (dst == null) { - dst = new Point(); + @Override // from Transform + public Transform preConcatenate (Transform other) { + if (generality() < other.generality()) { + return other.concatenate(this); + } + return ((other instanceof AffineTransform) ? + (AffineTransform)other : new AffineTransform(other)).multiply(this); + } + + @Override // from Transform + public Transform lerp (Transform other, float t) { + if (generality() < other.generality()) { + return other.lerp(this, -t); // TODO: is this correct? } - float x = src.getX(), y = src.getY(); - dst.setLocation(x * m00 + y * m01 + m02, x * m10 + y * m11 + m12); - return dst; + AffineTransform ot = (other instanceof AffineTransform) ? + (AffineTransform)other : new AffineTransform(other); + return new AffineTransform( + m00 + t*(ot.m00 - m00), m01 + t*(ot.m01 - m01), + m10 + t*(ot.m10 - m10), m11 + t*(ot.m11 - m11), + tx + t*(ot.tx - tx ), ty + t*(ot.ty - ty )); } - /** - * Transforms the supplied points using this transform's matrix. - * - * @param src the points to be transformed. - * @param srcOff the offset into the {@code src} array at which to start. - * @param dst the points into which to store the transformed points. May be {@code src}. - * @param dstOff the offset into the {@code dst} array at which to start. - * @param length the number of points to transform. - */ - 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; + @Override // from Transform + public Point transform (IPoint p, Point into) { + float x = p.getX(), y = p.getY(); + return into.set(m00*x + m01*y + tx, m10*x + m11*y + ty); + } + + @Override // from Transform + public void transform (IPoint[] src, int srcOff, Point[] dst, int dstOff, int count) { + for (int ii = 0; ii < count; ii++) { + transform(src[srcOff++], dst[dstOff++]); } } - /** - * Transforms the supplied points using this transform's matrix. - * - * @param src the points to be transformed (as {@code [x, y, x, y, ...]}). - * @param srcOff the offset into the {@code src} array at which to start. - * @param dst the points into which to store the transformed points. May be {@code src}. - * @param dstOff the offset into the {@code dst} array at which to start. - * @param length the number of points to transform. - */ - 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; - } - } - - /** - * Transforms the supplied relative distance vector (ignores the translation component). - * - * @param src the point to be transformed. - * @param dst the point in which to store the transformed values, if null a new instance will - * be created. May be {@code src}. - * @return the supplied (or created) destination point. - */ - 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; - } - - /** - * Transforms the supplied relative distance vectors using this transform's matrix (ignores the - * translation component). - * - * @param src the points to be transformed (as {@code [x, y, x, y, ...]}). - * @param srcOff the offset into the {@code src} array at which to start. - * @param dst the points into which to store the transformed points. May be {@code src}. - * @param dstOff the offset into the {@code dst} array at which to start. - * @param length the number of points to transform. - */ - public void deltaTransform (float[] src, int srcOff, float[] dst, int dstOff, int length) { - while (--length >= 0) { + @Override // from Transform + public void transform (float[] src, int srcOff, float[] dst, int dstOff, int count) { + for (int ii = 0; ii < count; ii++) { float x = src[srcOff++], y = src[srcOff++]; - dst[dstOff++] = x * m00 + y * m01; - dst[dstOff++] = x * m10 + y * m11; + dst[dstOff++] = m00*x + m01*y + tx; + dst[dstOff++] = m10*x + m11*y + ty; } } - /** - * Transforms the supplied point using the inverse of this transform's matrix. - * - * @param src the point to be transformed. - * @param dst the point in which to store the transformed values, if null a new instance will - * be created. May be {@code src}. - * @return the supplied (or created) destination point. - */ - 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; + @Override // from Transform + public Point inverseTransform (IPoint p, Point into) { + return invert().transform(p, into); // TODO } - /** - * Transforms the supplied points using the inverse of this transform's matrix. - * - * @param src the points to be transformed (as {@code [x, y, x, y, ...]}). - * @param srcOff the offset into the {@code src} array at which to start. - * @param dst the points into which to store the transformed points. May be {@code src}. - * @param dstOff the offset into the {@code dst} array at which to start. - * @param length the number of points to transform. - */ - 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; - } + @Override // from Transform + public Vector transform (IVector v, Vector into) { + float x = v.getX(), y = v.getY(); + return into.set(m00*x + m01*y, m10*x + m11*y); } - /** - * Creates and returns a new shape that is the supplied shape transformed by this transform's - * matrix. - */ - 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 // from Transform + public Vector inverseTransform (IVector v, Vector into) { + return invert().transform(v, into); // TODO + } + + @Override // from Transform + public Transform clone () { + return new AffineTransform(m00, m01, m10, m11, tx, ty); + } + + @Override // from Transform + public int generality () { + return GENERALITY; } @Override public String toString () { - return getClass().getName() + - "[[" + m00 + ", " + m01 + ", " + m02 + "], [" + m10 + ", " + m11 + ", " + m12 + "]]"; + return "affine [" + m00 + " " + m01 + " " + m10 + " " + m11 + " " + getTranslation() + "]"; } - // @Override // can't declare @Override due to GWT - public AffineTransform clone () { - return new AffineTransform(this); + // we don't publicize this because it might encourage someone to do something stupid like + // create a new AffineTransform from another AffineTransform using this instead of clone() + protected AffineTransform (Transform other) { + this(other.getScaleX(), other.getScaleY(), other.getRotation(), + other.getTx(), other.getTy()); } - @Override - public int hashCode () { - return Platform.hashCode(m00) ^ Platform.hashCode(m01) ^ Platform.hashCode(m02) ^ - Platform.hashCode(m10) ^ Platform.hashCode(m11) ^ Platform.hashCode(m12); + protected AffineTransform multiply (AffineTransform other) { + return multiply(other.m00, other.m01, other.m10, other.m11, other.tx, other.ty); } - @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; + protected AffineTransform multiply (float m00, float m01, float m10, float m11, + float tx, float ty) { + return new AffineTransform( + this.m00 * m00 + this.m01 * m10, this.m00 * m01 + this.m01 * m11, + this.m10 * m00 + this.m11 * m10, this.m10 * m01 + this.m11 * m11, + this.m00 * tx + this.m01 * ty + this.tx, this.m10 * tx + this.m11 * ty + this.ty); } - /** - * Multiplies two transforms, storing the result in the target transform. - * @return the supplied target transform. - */ - protected static AffineTransform multiply (AffineTransform t1, AffineTransform t2, - AffineTransform into) { - into.setTransform(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 - return into; - } - - // 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; + private static final float ZERO = 1E-7f; } diff --git a/src/main/java/pythagoras/f/Area.java b/src/main/java/pythagoras/f/Area.java index b3ec4cd..6b751a9 100644 --- a/src/main/java/pythagoras/f/Area.java +++ b/src/main/java/pythagoras/f/Area.java @@ -112,15 +112,15 @@ public class Area implements IShape, Cloneable /** * Transforms this area with the supplied transform. */ - public void transform (AffineTransform t) { - copy(new Area(t.createTransformedShape(this)), this); + public void transform (Transform t) { + copy(new Area(Transforms.createTransformedShape(t, this)), this); } /** * Creates a new area equal to this area transformed by the supplied transform. */ - public Area createTransformedArea (AffineTransform t) { - return new Area(t.createTransformedShape(this)); + public Area createTransformedArea (Transform t) { + return new Area(Transforms.createTransformedShape(t, this)); } /** @@ -258,12 +258,12 @@ public class Area implements IShape, Cloneable } @Override // from interface IShape - public PathIterator getPathIterator (AffineTransform t) { + public PathIterator getPathIterator (Transform t) { return new AreaPathIterator(t); } @Override // from interface IShape - public PathIterator getPathIterator (AffineTransform t, float flatness) { + public PathIterator getPathIterator (Transform t, float flatness) { return new FlatteningPathIterator(getPathIterator(t), flatness); } @@ -1159,11 +1159,11 @@ public class Area implements IShape, Cloneable // the internal class implements PathIterator private class AreaPathIterator implements PathIterator { - private final AffineTransform transform; + private final Transform transform; private int curRuleIndex = 0; private int curCoordIndex = 0; - AreaPathIterator (AffineTransform t) { + AreaPathIterator (Transform t) { this.transform = t; } diff --git a/src/main/java/pythagoras/f/FloatMath.java b/src/main/java/pythagoras/f/FloatMath.java index 428d72a..2c47ae3 100644 --- a/src/main/java/pythagoras/f/FloatMath.java +++ b/src/main/java/pythagoras/f/FloatMath.java @@ -12,17 +12,20 @@ public class FloatMath /** The ratio of a circle's circumference to its diameter. */ public static final float PI = (float)Math.PI; - /** The circle constant, tau (τ) http://tauday.com/ */ - public static final float TWO_PI = (float)(Math.PI * 2); + /** The circle constant, Tau (τ) http://tauday.com/ */ + public static final float TAU = (float)(Math.PI * 2); + + /** Twice Pi. */ + public static final float TWO_PI = TAU; /** Pi times one half. */ - public static final float HALF_PI = PI * 0.5f; + public static final float HALF_PI = (float)(Math.PI * 0.5); /** The base value of the natural logarithm. */ public static final float E = (float)Math.E; /** A small number. */ - public static final float EPSILON = 0.000001f; + public static final float EPSILON = 0.00001f; /** * Computes and returns the sine of the given angle. diff --git a/src/main/java/pythagoras/f/IPoint.java b/src/main/java/pythagoras/f/IPoint.java index c34bd86..7d3a369 100644 --- a/src/main/java/pythagoras/f/IPoint.java +++ b/src/main/java/pythagoras/f/IPoint.java @@ -27,6 +27,31 @@ public interface IPoint extends Cloneable /** Returns the Euclidian distance between this point and the supplied point. */ float distance (IPoint p); + /** Multiplies this point by a scale factor. + * @return a new point containing the result. */ + Point mult (float s); + + /** Multiplies this point by a scale factor and places the result in the supplied object. + * @return a reference to the result, for chaining. */ + Point mult (float s, Point result); + + /** Translates this point by the specified offset. + * @return a new point containing the result. */ + Point add (float x, float y); + + /** Translates this point by the specified offset and stores the result in the object provided. + * @return a reference to the result, for chaining. */ + Point add (float x, float y, Point result); + + /** Rotates this point around the origin by the specified angle. + * @return a new point containing the result. */ + Point rotate (float angle); + + /** Rotates this point around the origin by the specified angle, storing the result in the + * point provided. + * @return a reference to the result point, for chaining. */ + Point rotate (float angle, Point result); + /** Returns a mutable copy of this point. */ Point clone (); } diff --git a/src/main/java/pythagoras/f/IShape.java b/src/main/java/pythagoras/f/IShape.java index 108cabe..9a3f5e9 100644 --- a/src/main/java/pythagoras/f/IShape.java +++ b/src/main/java/pythagoras/f/IShape.java @@ -42,7 +42,7 @@ public interface IShape * * @param at if supplied, the points in the path are transformed using this. */ - PathIterator getPathIterator (AffineTransform at); + PathIterator getPathIterator (Transform at); /** * Returns an iterator over the path described by this shape. @@ -52,5 +52,5 @@ public interface IShape * 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); + PathIterator getPathIterator (Transform at, float flatness); } diff --git a/src/main/java/pythagoras/f/IdentityTransform.java b/src/main/java/pythagoras/f/IdentityTransform.java new file mode 100644 index 0000000..d7e1f22 --- /dev/null +++ b/src/main/java/pythagoras/f/IdentityTransform.java @@ -0,0 +1,113 @@ +// +// Pythagoras - a collection of geometry classes +// http://github.com/samskivert/pythagoras + +package pythagoras.f; + +/** + * Implements the identity transform. + */ +public class IdentityTransform extends AbstractTransform +{ + /** Identifies the identity transform in {@link #generality}. */ + public static final int GENERALITY = 0; + + @Override // from Transform + public float getUniformScale () { + return 1; + } + + @Override // from Transform + public float getScaleX () { + return 1; + } + + @Override // from Transform + public float getScaleY () { + return 1; + } + + @Override // from Transform + public float getRotation () { + return 0; + } + + @Override // from Transform + public float getTx () { + return 0; + } + + @Override // from Transform + public float getTy () { + return 0; + } + + @Override // from Transform + public Transform invert () { + return this; + } + + @Override // from Transform + public Transform concatenate (Transform other) { + return other; + } + + @Override // from Transform + public Transform preConcatenate (Transform other) { + return other; + } + + @Override // from Transform + public Transform lerp (Transform other, float t) { + throw new UnsupportedOperationException(); // TODO + } + + @Override // from Transform + public Point transform (IPoint p, Point into) { + return into.set(p); + } + + @Override // from Transform + public void transform (IPoint[] src, int srcOff, Point[] dst, int dstOff, int count) { + for (int ii = 0; ii < count; ii++) { + transform(src[srcOff++], dst[dstOff++]); + } + } + + @Override // from Transform + public void transform (float[] src, int srcOff, float[] dst, int dstOff, int count) { + for (int ii = 0; ii < count; ii++) { + dst[dstOff++] = src[srcOff++]; + } + } + + @Override // from Transform + public Point inverseTransform (IPoint p, Point into) { + return into.set(p); + } + + @Override // from Transform + public Vector transform (IVector v, Vector into) { + return into.set(v); + } + + @Override // from Transform + public Vector inverseTransform (IVector v, Vector into) { + return into.set(v); + } + + @Override // from Transform + public Transform clone () { + return this; + } + + @Override // from Transform + public int generality () { + return GENERALITY; + } + + @Override + public String toString () { + return "ident"; + } +} diff --git a/src/main/java/pythagoras/f/NonUniformTransform.java b/src/main/java/pythagoras/f/NonUniformTransform.java new file mode 100644 index 0000000..4cef0ba --- /dev/null +++ b/src/main/java/pythagoras/f/NonUniformTransform.java @@ -0,0 +1,213 @@ +// +// Pythagoras - a collection of geometry classes +// http://github.com/samskivert/pythagoras + +package pythagoras.f; + +/** + * Implements a uniform (translation, rotation, scaleX, scaleY) transform. + */ +public class NonUniformTransform extends AbstractTransform +{ + /** Identifies the uniform transform in {@link #generality}. */ + public static final int GENERALITY = 3; + + /** The scale components of this transform. */ + public float scaleX, scaleY; + + /** The rotation component of this transform (in radians). */ + public float rotation; + + /** The translation components of this transform. */ + public float tx, ty; + + /** Creates a uniform transform with zero translation and rotation, and unit scale. */ + public NonUniformTransform () { + this.scaleX = this.scaleY = 1; + } + + /** Creates a uniform transform with the specified translation, rotation and scale. */ + public NonUniformTransform (float scaleX, float scaleY, float rotation, float tx, float ty) { + setScale(scaleX, scaleY); + setRotation(rotation); + setTranslation(tx, ty); + } + + @Override // from Transform + public float getUniformScale () { + return (scaleX + scaleY) / 2; // TODO: is this sane + } + + @Override // from Transform + public float getScaleX () { + return scaleX; + } + + @Override // from Transform + public float getScaleY () { + return scaleY; + } + + @Override // from Transform + public float getRotation () { + return rotation; + } + + @Override // from Transform + public float getTx () { + return tx; + } + + @Override // from Transform + public float getTy () { + return ty; + } + + @Override // from Transform + public Transform setUniformScale (float scale) { + setScaleX(scale); + setScaleY(scale); + return this; + } + + @Override // from Transform + public Transform setScaleX (float scaleX) { + if (scaleX == 0) throw new IllegalArgumentException("Scale (x) must not be zero."); + this.scaleX = scaleX; + return this; + } + + @Override // from Transform + public Transform setScaleY (float scaleY) { + if (scaleY == 0) throw new IllegalArgumentException("Scale (y) must not be zero."); + this.scaleY = scaleY; + return this; + } + + @Override // from Transform + public Transform setRotation (float angle) { + this.rotation = angle; + return this; + } + + @Override // from Transform + public Transform setTx (float tx) { + this.tx = tx; + return this; + } + + @Override // from Transform + public Transform setTy (float ty) { + this.ty = ty; + return this; + } + + @Override // from Transform + public Transform invert () { + Vector iscale = new Vector(1f / scaleX, 1f / scaleY); + Vector t = new Vector(tx, ty).negateLocal().rotateLocal(-rotation).multLocal(iscale); + return new NonUniformTransform(iscale.x, iscale.y, -rotation, t.x, t.y); + } + + @Override // from Transform + public Transform concatenate (Transform other) { + if (generality() < other.generality()) { + return other.preConcatenate(this); + } + + float otx = other.getTx(), oty = other.getTy(); + float sina = FloatMath.sin(rotation), cosa = FloatMath.cos(rotation); + float ntx = (otx*cosa - oty*sina) * scaleX + getTx(); + float nty = (otx*sina + oty*cosa) * scaleY + getTy(); + + float nrotation = FloatMath.normalizeAngle(rotation + other.getRotation()); + float nscaleX = scaleX * other.getScaleX(); + float nscaleY = scaleY * other.getScaleY(); + return new NonUniformTransform(nscaleX, nscaleY, nrotation, ntx, nty); + } + + @Override // from Transform + public Transform preConcatenate (Transform other) { + if (generality() < other.generality()) { + return other.concatenate(this); + } + + float tx = getTx(), ty = getTy(); + float sina = FloatMath.sin(other.getRotation()), cosa = FloatMath.cos(other.getRotation()); + float ntx = (tx*cosa - ty*sina) * other.getScaleX() + other.getTx(); + float nty = (tx*sina + ty*cosa) * other.getScaleY() + other.getTy(); + float nrotation = FloatMath.normalizeAngle(other.getRotation() + rotation); + float nscaleX = other.getScaleX() * scaleX; + float nscaleY = other.getScaleY() * scaleY; + return new NonUniformTransform(nscaleX, nscaleY, nrotation, ntx, nty); + } + + @Override // from Transform + public Transform lerp (Transform other, float t) { + if (generality() < other.generality()) { + return other.lerp(this, -t); // TODO: is this correct? + } + + Vector nt = getTranslation().lerpLocal(other.getTranslation(), t); + float nrotation = FloatMath.lerpa(rotation, other.getRotation(), t); + float nscaleX = FloatMath.lerp(scaleX, other.getScaleX(), t); + float nscaleY = FloatMath.lerp(scaleY, other.getScaleY(), t); + return new NonUniformTransform(nscaleX, nscaleY, nrotation, nt.x, nt.y); + } + + @Override // from Transform + public Point transform (IPoint p, Point into) { + return Points.transform(p.getX(), p.getY(), scaleX, scaleY, rotation, tx, ty, into); + } + + @Override // from Transform + public void transform (IPoint[] src, int srcOff, Point[] dst, int dstOff, int count) { + float sina = FloatMath.sin(rotation), cosa = FloatMath.cos(rotation); + for (int ii = 0; ii < count; ii++) { + IPoint s = src[srcOff++]; + Points.transform(s.getX(), s.getY(), scaleX, scaleY, sina, cosa, tx, ty, dst[dstOff++]); + } + } + + @Override // from Transform + public void transform (float[] src, int srcOff, float[] dst, int dstOff, int count) { + Point p = new Point(); + float sina = FloatMath.sin(rotation), cosa = FloatMath.cos(rotation); + for (int ii = 0; ii < count; ii++) { + Points.transform(src[srcOff++], src[srcOff++], scaleX, scaleY, sina, cosa, tx, ty, p); + dst[dstOff++] = p.x; + dst[dstOff++] = p.y; + } + } + + @Override // from Transform + public Point inverseTransform (IPoint p, Point into) { + return Points.inverseTransform(p.getX(), p.getY(), scaleX, scaleY, rotation, tx, ty, into); + } + + @Override // from Transform + public Vector transform (IVector v, Vector into) { + return Vectors.transform(v.getX(), v.getY(), scaleX, scaleY, rotation, into); + } + + @Override // from Transform + public Vector inverseTransform (IVector v, Vector into) { + return Vectors.inverseTransform(v.getX(), v.getY(), scaleX, scaleY, rotation, into); + } + + @Override // from Transform + public Transform clone () { + return new NonUniformTransform(scaleX, scaleY, rotation, tx, ty); + } + + @Override // from Transform + public int generality () { + return GENERALITY; + } + + @Override + public String toString () { + return "nonunif [scale=" + getScale() + ", rot=" + rotation + + ", trans=" + getTranslation() + "]"; + } +} diff --git a/src/main/java/pythagoras/f/Path.java b/src/main/java/pythagoras/f/Path.java index 56f86b2..68f212c 100644 --- a/src/main/java/pythagoras/f/Path.java +++ b/src/main/java/pythagoras/f/Path.java @@ -157,11 +157,11 @@ public final class Path implements IShape, Cloneable pointSize = 0; } - public void transform (AffineTransform t) { + public void transform (Transform t) { t.transform(points, 0, points, 0, pointSize / 2); } - public IShape createTransformedShape (AffineTransform t) { + public IShape createTransformedShape (Transform t) { Path p = clone(); if (t != null) { p.transform(t); @@ -241,12 +241,12 @@ public final class Path implements IShape, Cloneable } @Override // from interface IShape - public PathIterator getPathIterator (AffineTransform t) { + public PathIterator getPathIterator (Transform t) { return new Iterator(this, t); } @Override // from interface IShape - public PathIterator getPathIterator (AffineTransform t, float flatness) { + public PathIterator getPathIterator (Transform t, float flatness) { return new FlatteningPathIterator(getPathIterator(t), flatness); } @@ -309,13 +309,13 @@ public final class Path implements IShape, Cloneable private Path p; /** The path iterator transformation. */ - private AffineTransform t; + private Transform t; Iterator (Path path) { this(path, null); } - Iterator (Path path, AffineTransform at) { + Iterator (Path path, Transform at) { this.p = path; this.t = at; } diff --git a/src/main/java/pythagoras/f/Point.java b/src/main/java/pythagoras/f/Point.java index 25a9092..9cc3998 100644 --- a/src/main/java/pythagoras/f/Point.java +++ b/src/main/java/pythagoras/f/Point.java @@ -27,44 +27,46 @@ public class Point extends AbstractPoint implements Serializable * Constructs a point at the specified coordinates. */ public Point (float x, float y) { - setLocation(x, y); + set(x, y); } /** * Constructs a point with coordinates equal to the supplied point. */ public Point (IPoint p) { - setLocation(p.getX(), p.getY()); + set(p.getX(), p.getY()); } - /** - * Sets the coordinates of this point to be equal to those of the supplied point. - */ - public void setLocation (IPoint p) { - setLocation(p.getX(), p.getY()); + /** Sets the coordinates of this point to be equal to those of the supplied point. + * @return a reference to this this, for chaining. */ + public Point set (IPoint p) { + return set(p.getX(), p.getY()); } - /** - * Sets the coordinates of this point to the supplied values. - */ - public void setLocation (float x, float y) { + /** Sets the coordinates of this point to the supplied values. + * @return a reference to this this, for chaining. */ + public Point set (float x, float y) { this.x = x; this.y = y; + return this; } - /** - * A synonym for {@link #setLocation}. - */ - public void move (float x, float y) { - setLocation(x, y); + /** Multiplies this point by a scale factor. + * @return a a reference to this point, for chaining. */ + public Point multLocal (float s) { + return mult(s, this); } - /** - * Translates this point by the specified offset. - */ - public void translate (float dx, float dy) { - x += dx; - y += dy; + /** Translates this point by the specified offset. + * @return a reference to this point, for chaining. */ + public Point addLocal (float dx, float dy) { + return add(dx, dy, this); + } + + /** Rotates this point in-place by the specified angle. + * @return a reference to this point, for chaining. */ + public Point rotateLocal (float angle) { + return rotate(angle, this); } @Override // from interface IPoint diff --git a/src/main/java/pythagoras/f/Points.java b/src/main/java/pythagoras/f/Points.java index e393ae4..ab95ab6 100644 --- a/src/main/java/pythagoras/f/Points.java +++ b/src/main/java/pythagoras/f/Points.java @@ -25,6 +25,32 @@ public class Points return (float)Math.sqrt(distanceSq(x1, y1, x2, y2)); } + /** Transforms a point as specified, storing the result in the point provided. + * @return a reference to the result point, for chaining. */ + public static Point transform (float x, float y, float sx, float sy, float rotation, + float tx, float ty, Point result) { + return transform(x, y, sx, sy, FloatMath.sin(rotation), FloatMath.cos(rotation), tx, ty, + result); + } + + /** Transforms a point as specified, storing the result in the point provided. + * @return a reference to the result point, for chaining. */ + public static Point transform (float x, float y, float sx, float sy, float sina, float cosa, + float tx, float ty, Point result) { + return result.set((x*cosa - y*sina) * sx + tx, (x*sina + y*cosa) * sy + ty); + } + + /** Inverse transforms a point as specified, storing the result in the point provided. + * @return a reference to the result point, for chaining. */ + public static Point inverseTransform (float x, float y, float sx, float sy, float rotation, + float tx, float ty, Point result) { + x -= tx; y -= ty; // untranslate + float sinnega = FloatMath.sin(-rotation), cosnega = FloatMath.cos(-rotation); + float nx = (x * cosnega - y * sinnega); // unrotate + float ny = (x * sinnega + y * cosnega); + return result.set(nx / sx, ny / sy); // unscale + } + /** * Returns a string describing the supplied point, of the form +x+y, * +x-y, -x-y, etc. diff --git a/src/main/java/pythagoras/f/RectangularShape.java b/src/main/java/pythagoras/f/RectangularShape.java index 2ea57e2..c47c4aa 100644 --- a/src/main/java/pythagoras/f/RectangularShape.java +++ b/src/main/java/pythagoras/f/RectangularShape.java @@ -170,7 +170,7 @@ public abstract class RectangularShape implements IRectangularShape } @Override // from interface IShape - public PathIterator getPathIterator (AffineTransform t, float flatness) { + public PathIterator getPathIterator (Transform t, float flatness) { return new FlatteningPathIterator(getPathIterator(t), flatness); } } diff --git a/src/main/java/pythagoras/f/RigidTransform.java b/src/main/java/pythagoras/f/RigidTransform.java new file mode 100644 index 0000000..4da8d5f --- /dev/null +++ b/src/main/java/pythagoras/f/RigidTransform.java @@ -0,0 +1,172 @@ +// +// Pythagoras - a collection of geometry classes +// http://github.com/samskivert/pythagoras + +package pythagoras.f; + +/** + * Implements a rigid body (translation, rotation) transform. + */ +public class RigidTransform extends AbstractTransform +{ + /** Identifies the rigid body transform in {@link #generality}. */ + public static final int GENERALITY = 1; + + /** The rotation component of this transform (in radians). */ + public float rotation; + + /** The translation components of this transform. */ + public float tx, ty; + + /** Creates a rigid body transform with zero translation and rotation. */ + public RigidTransform () { + } + + /** Creates a rigid body transform with the specified translation and rotation. */ + public RigidTransform (float rotation, float tx, float ty) { + setRotation(rotation); + setTranslation(tx, ty); + } + + @Override // from Transform + public float getUniformScale () { + return 1; + } + + @Override // from Transform + public float getScaleX () { + return 1; + } + + @Override // from Transform + public float getScaleY () { + return 1; + } + + @Override // from Transform + public float getRotation () { + return rotation; + } + + @Override // from Transform + public float getTx () { + return tx; + } + + @Override // from Transform + public float getTy () { + return ty; + } + + @Override // from Transform + public Transform setRotation (float angle) { + this.rotation = angle; + return this; + } + + @Override // from Transform + public Transform setTx (float tx) { + this.tx = tx; + return this; + } + + @Override // from Transform + public Transform setTy (float ty) { + this.ty = ty; + return this; + } + + @Override // from Transform + public Transform invert () { + Vector t = getTranslation().negateLocal().rotateLocal(-rotation); + return new RigidTransform(-rotation, t.x, t.y); + } + + @Override // from Transform + public Transform concatenate (Transform other) { + if (generality() < other.generality()) { + return other.preConcatenate(this); + } + + Vector nt = other.getTranslation(); + nt.rotateAndAdd(rotation, getTranslation(), nt); + float nrotation = FloatMath.normalizeAngle(rotation + other.getRotation()); + return new RigidTransform(nrotation, nt.x, nt.y); + } + + @Override // from Transform + public Transform preConcatenate (Transform other) { + if (generality() < other.generality()) { + return other.concatenate(this); + } + + Vector nt = getTranslation(); + nt.rotateAndAdd(other.getRotation(), other.getTranslation(), nt); + float nrotation = FloatMath.normalizeAngle(other.getRotation() + rotation); + return new RigidTransform(nrotation, nt.x, nt.y); + } + + @Override // from Transform + public Transform lerp (Transform other, float t) { + if (generality() < other.generality()) { + return other.lerp(this, -t); // TODO: is this correct? + } + Vector nt = getTranslation().lerpLocal(other.getTranslation(), t); + return new RigidTransform(FloatMath.lerpa(rotation, other.getRotation(), t), nt.x, nt.y); + } + + @Override // from Transform + public Point transform (IPoint p, Point into) { + return Points.transform(p.getX(), p.getY(), 1, 1, rotation, tx, ty, into); + } + + @Override // from Transform + public void transform (IPoint[] src, int srcOff, Point[] dst, int dstOff, int count) { + float sina = FloatMath.sin(rotation), cosa = FloatMath.cos(rotation); + for (int ii = 0; ii < count; ii++) { + IPoint s = src[srcOff++]; + Points.transform(s.getX(), s.getY(), 1, 1, sina, cosa, tx, ty, dst[dstOff++]); + } + } + + @Override // from Transform + public void transform (float[] src, int srcOff, float[] dst, int dstOff, int count) { + Point p = new Point(); + float sina = FloatMath.sin(rotation), cosa = FloatMath.cos(rotation); + for (int ii = 0; ii < count; ii++) { + Points.transform(src[srcOff++], src[srcOff++], 1, 1, sina, cosa, tx, ty, p); + dst[dstOff++] = p.x; + dst[dstOff++] = p.y; + } + } + + @Override // from Transform + public Point inverseTransform (IPoint p, Point into) { + return Points.inverseTransform(p.getX(), p.getY(), 1, 1, rotation, tx, ty, into); + } + + @Override // from Transform + public Vector transform (IVector v, Vector into) { + return v.rotate(rotation, into); + } + + @Override // from Transform + public Vector inverseTransform (IVector v, Vector into) { + return v.rotate(-rotation, into); + } + + @Override // from Transform + public Transform clone () { + return new RigidTransform(rotation, tx, ty); + } + + @Override // from Transform + public int generality () { + return GENERALITY; + } + + @Override + public String toString () { + return "rigid [rot=" + rotation + ", trans=" + getTranslation() + "]"; + } +} diff --git a/src/main/java/pythagoras/f/Transform.java b/src/main/java/pythagoras/f/Transform.java index b2bd6f1..88d27b9 100644 --- a/src/main/java/pythagoras/f/Transform.java +++ b/src/main/java/pythagoras/f/Transform.java @@ -6,52 +6,32 @@ package pythagoras.f; /** * Represents a geometric transform. Specialized implementations exist for identity, rigid body, - * uniform, affine and general transforms. + * uniform, non-uniform, and affine transforms. */ public interface Transform { - /** Sets the translation component of this transform. - * @throws UnsupportedOperationException if the transform is not rigid body or greater. */ - void setTranslation (float tx, float ty); + /** Returns the uniform scale applied by this transform. The uniform scale will be approximated + * for non-uniform transforms. */ + float getUniformScale (); - /** Sets the x-component of this transform's translation. - * @throws UnsupportedOperationException if the transform is not rigid body or greater. */ - void setTx (float tx); + /** Returns the scale vector for this transform. */ + Vector getScale (); - /** Sets the y-component of this transform's translation. - * @throws UnsupportedOperationException if the transform is not rigid body or greater. */ - void setTy (float ty); + /** Returns the x-component of the scale applied by this transform. Note that this will be + * extracted and therefore approximate for affine transforms. */ + float getScaleX (); - /** Sets the rotation component of this transform. - * @throws UnsupportedOperationException if the transform is not rigid body or greater. */ - void setRotation (float angle); + /** Returns the y-component of the scale applied by this transform. Note that this will be + * extracted and therefore approximate for affine transforms. */ + float getScaleY (); - /** Sets the uniform scale of this transform. - * @throws UnsupportedOperationException if the transform is not uniform or greater. */ - void setScale (float scale); + /** Returns the rotation applied by this transform. Note that the rotation is extracted and + * therefore approximate for affine transforms. + * @throws NoninvertibleTransformException if the transform is not invertible. */ + float getRotation (); - /** Sets the x and y scale of this transform. - * @throws UnsupportedOperationException if the transform is not affine or greater. */ - void setScale (float scaleX, float scaleY); - - /** Sets the x scale of this transform. - * @throws UnsupportedOperationException if the transform is not affine or greater. */ - void setScaleX (float scaleX); - - /** Sets the y scale of this transform. - * @throws UnsupportedOperationException if the transform is not affine or greater. */ - void setScaleY (float scaleY); - - /** Sets the affine transform matrix. - * @throws UnsupportedOperationException if the transform is not affine or greater. */ - void setTransform (float m00, float m01, float m10, float m11, - float tx, float ty); - - /** Sets the general transform matrix. - * @throws UnsupportedOperationException if the transform is not general. */ - void setTransform (float m00, float m01, float m02, - float m10, float m11, float m12, - float m20, float m21, float m22); + /** Returns the translation vector for this transform. */ + Vector getTranslation (); /** Returns the x-coordinate of the translation component. */ float getTx (); @@ -59,45 +39,114 @@ public interface Transform /** Returns the y-coordinate of the translation component. */ float getTy (); - /** Returns the rotation applied by this transform. Note that the rotation is extracted and - * therefore approximate for affine and general transforms. */ - float getRotation (); // will be extracted from affine+ + /** Sets the uniform scale of this transform. + * @return this instance, for chaining. + * @throws IllegalArgumentException if the supplied scale is zero. + * @throws UnsupportedOperationException if the transform is not uniform or greater. */ + Transform setUniformScale (float scale); - /** Returns the uniform scale applied by this transform. Note that the uniform scale will be - * approximated for non-uniform transforms (affine and general). */ - float getScale (); // will be extracted/approximated for affine+ + /** Sets the x and y scale of this transform. + * @return this instance, for chaining. + * @throws IllegalArgumentException if either supplied scale is zero. + * @throws UnsupportedOperationException if the transform is not affine or greater. */ + Transform setScale (float scaleX, float scaleY); - /** Returns the x-component of the scale applied by this transform. */ - float getScaleX (); // will be extracted from affine+ + /** Sets the x scale of this transform. + * @return this instance, for chaining. + * @throws IllegalArgumentException if the supplied scale is zero. + * @throws UnsupportedOperationException if the transform is not affine or greater. */ + Transform setScaleX (float scaleX); - /** Returns the y-component of the scale applied by this transform. */ - float getScaleY (); // will be extracted from affine+ + /** Sets the y scale of this transform. + * @return this instance, for chaining. + * @throws IllegalArgumentException if the supplied scale is zero. + * @throws UnsupportedOperationException if the transform is not affine or greater. */ + Transform setScaleY (float scaleY); - /** Returns the inverse of this transform. + /** Sets the rotation component of this transform. + * @return this instance, for chaining. + * @throws UnsupportedOperationException if the transform is not rigid body or greater. */ + Transform setRotation (float angle); + + /** Sets the translation component of this transform. + * @return this instance, for chaining. + * @throws UnsupportedOperationException if the transform is not rigid body or greater. */ + Transform setTranslation (float tx, float ty); + + /** Sets the x-component of this transform's translation. + * @return this instance, for chaining. + * @throws UnsupportedOperationException if the transform is not rigid body or greater. */ + Transform setTx (float tx); + + /** Sets the y-component of this transform's translation. + * @return this instance, for chaining. + * @throws UnsupportedOperationException if the transform is not rigid body or greater. */ + Transform setTy (float ty); + + /** Sets the affine transform matrix. + * @return this instance, for chaining. + * @throws UnsupportedOperationException if the transform is not affine or greater. */ + Transform setTransform (float m00, float m01, float m10, float m11, + float tx, float ty); + + /** Returns a new transform that represents the inverse of this transform. * @throws NoninvertibleTransformException if the transform is not invertible. */ Transform invert (); - /** Composes this transform with the supplied transform (i.e. {@code this x other}). */ - Transform compose (Transform other); + /** Returns a new transform comprised of the concatenation of {@code other} to this transform + * (i.e. {@code this x other}). */ + Transform concatenate (Transform other); - /** Returns the linear interpolation between this transform and the specified other. */ + /** Returns a new transform comprised of the concatenation of this transform to {@code other} + * (i.e. {@code other x this}). */ + Transform preConcatenate (Transform other); + + /** Returns a new transform comprised of the linear interpolation between this transform and + * the specified other. */ Transform lerp (Transform other, float t); - /** Transforms the supplied point, writing the result into {@code into}, which may reference - * the same object as {@code p}. */ - void transform (IPoint p, Point into); + /** Transforms the supplied point, writing the result into {@code into}. + * @param into a point into which to store the result, may be the same object as {@code p}. + * @return {@code into} for chaining. */ + Point transform (IPoint p, Point into); - /** Inverse transforms the supplied point, writing the result into {@code into}, which may - * reference the same object as {@code p}. + /** Transforms the supplied points. + * @param src the points to be transformed. + * @param srcOff the offset into the {@code src} array at which to start. + * @param dst the points into which to store the transformed points. May be {@code src}. + * @param dstOff the offset into the {@code dst} array at which to start. + * @param count the number of points to transform. */ + void transform (IPoint[] src, int srcOff, Point[] dst, int dstOff, int count); + + /** Transforms the supplied points. + * @param src the points to be transformed (as {@code [x, y, x, y, ...]}). + * @param srcOff the offset into the {@code src} array at which to start. + * @param dst the points into which to store the transformed points. May be {@code src}. + * @param dstOff the offset into the {@code dst} array at which to start. + * @param count the number of points to transform. */ + void transform (float[] src, int srcOff, float[] dst, int dstOff, int count); + + /** Inverse transforms the supplied point, writing the result into {@code into}. + * @param into a point into which to store the result, may be the same object as {@code p}. + * @return {@code into}, for chaining. * @throws NoninvertibleTransformException if the transform is not invertible. */ - void inverseTransform (IPoint p, Point into); + Point inverseTransform (IPoint p, Point into); - /** Transforms the supplied vector, writing the result into {@code into}, which may reference - * the same object as {@code v}. */ - void transform (IVector v, Vector into); + /** Transforms the supplied vector, writing the result into {@code into}. + * @param into a vector into which to store the result, may be the same object as {@code v}. + * @return {@code into}, for chaining. */ + Vector transform (IVector v, Vector into); - /** Inverse transforms the supplied vector, writing the result into {@code into}, which may - * reference the same object as {@code v}. + /** Inverse transforms the supplied vector, writing the result into {@code into}. + * @param into a vector into which to store the result, may be the same object as {@code v}. + * @return {@code into}, for chaining. * @throws NoninvertibleTransformException if the transform is not invertible. */ - void inverseTransform (IVector v, Vector into); + Vector inverseTransform (IVector v, Vector into); + + /** Returns a clone of this transform. */ + Transform clone (); + + /** Returns an integer that increases monotonically with the generality of the transform + * implementation. Used internally when combining transforms. */ + int generality (); } diff --git a/src/main/java/pythagoras/f/Transforms.java b/src/main/java/pythagoras/f/Transforms.java new file mode 100644 index 0000000..cc74298 --- /dev/null +++ b/src/main/java/pythagoras/f/Transforms.java @@ -0,0 +1,28 @@ +// +// Pythagoras - a collection of geometry classes +// http://github.com/samskivert/pythagoras + +package pythagoras.f; + +/** + * {@link Transform} related utility methods. + */ +public class Transforms +{ + /** + * Creates and returns a new shape that is the supplied shape transformed by this transform's + * matrix. + */ + public static IShape createTransformedShape (Transform t, IShape src) { + if (src == null) { + return null; + } + if (src instanceof Path) { + return ((Path)src).createTransformedShape(t); + } + PathIterator path = src.getPathIterator(t); + Path dst = new Path(path.getWindingRule()); + dst.append(path, false); + return dst; + } +} diff --git a/src/main/java/pythagoras/f/UniformTransform.java b/src/main/java/pythagoras/f/UniformTransform.java new file mode 100644 index 0000000..5e6cd81 --- /dev/null +++ b/src/main/java/pythagoras/f/UniformTransform.java @@ -0,0 +1,192 @@ +// +// Pythagoras - a collection of geometry classes +// http://github.com/samskivert/pythagoras + +package pythagoras.f; + +/** + * Implements a uniform (translation, rotation, scale) transform. + */ +public class UniformTransform extends AbstractTransform +{ + /** Identifies the uniform transform in {@link #generality}. */ + public static final int GENERALITY = 2; + + /** The uniform scale component of this transform. */ + public float scale; + + /** The rotation component of this transform (in radians). */ + public float rotation; + + /** The translation components of this transform. */ + public float tx, ty; + + /** Creates a uniform transform with zero translation and rotation, and unit scale. */ + public UniformTransform () { + setUniformScale(1); + } + + /** Creates a uniform transform with the specified translation, rotation and scale. */ + public UniformTransform (float scale, float rotation, float tx, float ty) { + setUniformScale(scale); + setRotation(rotation); + setTranslation(tx, ty); + } + + @Override // from Transform + public float getUniformScale () { + return scale; + } + + @Override // from Transform + public float getScaleX () { + return scale; + } + + @Override // from Transform + public float getScaleY () { + return scale; + } + + @Override // from Transform + public float getRotation () { + return rotation; + } + + @Override // from Transform + public float getTx () { + return tx; + } + + @Override // from Transform + public float getTy () { + return ty; + } + + @Override // from Transform + public Transform setUniformScale (float scale) { + if (scale == 0) throw new IllegalArgumentException("Scale must be non-zero."); + this.scale = scale; + return this; + } + + @Override // from Transform + public Transform setRotation (float angle) { + this.rotation = angle; + return this; + } + + @Override // from Transform + public Transform setTx (float tx) { + this.tx = tx; + return this; + } + + @Override // from Transform + public Transform setTy (float ty) { + this.ty = ty; + return this; + } + + @Override // from Transform + public Transform invert () { + float nscale = 1f / scale, nrotation = -rotation; + Vector t = getTranslation().negateLocal().rotateLocal(nrotation).multLocal(nscale); + return new UniformTransform(nscale, nrotation, t.x, t.y); + } + + @Override // from Transform + public Transform concatenate (Transform other) { + if (generality() < other.generality()) { + return other.preConcatenate(this); + } + + Vector nt = other.getTranslation(); + nt.rotateScaleAndAdd(rotation, scale, getTranslation(), nt); + float nrotation = FloatMath.normalizeAngle(rotation + other.getRotation()); + float nscale = scale * other.getUniformScale(); + return new UniformTransform(nscale, nrotation, nt.x, nt.y); + } + + @Override // from Transform + public Transform preConcatenate (Transform other) { + if (generality() < other.generality()) { + return other.concatenate(this); + } + + Vector nt = getTranslation(); + nt.rotateScaleAndAdd(other.getRotation(), other.getUniformScale(), + other.getTranslation(), nt); + float nrotation = FloatMath.normalizeAngle(other.getRotation() + rotation); + float nscale = other.getUniformScale() * scale; + return new UniformTransform(nscale, nrotation, nt.x, nt.y); + } + + @Override // from Transform + public Transform lerp (Transform other, float t) { + if (generality() < other.generality()) { + return other.lerp(this, -t); // TODO: is this correct? + } + + Vector nt = getTranslation().lerpLocal(other.getTranslation(), t); + float nrotation = FloatMath.lerpa(rotation, other.getRotation(), t); + float nscale = FloatMath.lerp(scale, other.getUniformScale(), t); + return new UniformTransform(nscale, nrotation, nt.x, nt.y); + } + + @Override // from Transform + public Point transform (IPoint p, Point into) { + return Points.transform(p.getX(), p.getY(), scale, scale, rotation, tx, ty, into); + } + + @Override // from Transform + public void transform (IPoint[] src, int srcOff, Point[] dst, int dstOff, int count) { + float sina = FloatMath.sin(rotation), cosa = FloatMath.cos(rotation); + for (int ii = 0; ii < count; ii++) { + IPoint p = src[srcOff++]; + Points.transform(p.getX(), p.getY(), scale, scale, sina, cosa, tx, ty, dst[dstOff++]); + } + } + + @Override // from Transform + public void transform (float[] src, int srcOff, float[] dst, int dstOff, int count) { + Point p = new Point(); + float sina = FloatMath.sin(rotation), cosa = FloatMath.cos(rotation); + for (int ii = 0; ii < count; ii++) { + Points.transform(src[srcOff++], src[srcOff++], scale, scale, sina, cosa, tx, ty, p); + dst[dstOff++] = p.x; + dst[dstOff++] = p.y; + } + } + + @Override // from Transform + public Point inverseTransform (IPoint p, Point into) { + return Points.inverseTransform(p.getX(), p.getY(), scale, scale, rotation, tx, ty, into); + } + + @Override // from Transform + public Vector transform (IVector v, Vector into) { + return Vectors.transform(v.getX(), v.getY(), scale, scale, rotation, into); + } + + @Override // from Transform + public Vector inverseTransform (IVector v, Vector into) { + return Vectors.inverseTransform(v.getX(), v.getY(), scale, scale, rotation, into); + } + + @Override // from Transform + public Transform clone () { + return new UniformTransform(scale, rotation, tx, ty); + } + + @Override // from Transform + public int generality () { + return GENERALITY; + } + + @Override + public String toString () { + return "uniform [scale=" + scale + ", rot=" + rotation + + ", trans=" + getTranslation() + "]"; + } +} diff --git a/src/main/java/pythagoras/f/Vector.java b/src/main/java/pythagoras/f/Vector.java index 43751f7..ff6ac9c 100644 --- a/src/main/java/pythagoras/f/Vector.java +++ b/src/main/java/pythagoras/f/Vector.java @@ -29,16 +29,6 @@ public class Vector extends AbstractVector public Vector () { } - @Override // from AbstractVector - public float getX () { - return x; - } - - @Override // from AbstractVector - public float getY () { - return y; - } - /** Negates this vector in-place. * @return a reference to this vector, for chaining. */ public Vector negateLocal () { @@ -118,4 +108,14 @@ public class Vector extends AbstractVector this.y = y; return this; } + + @Override // from AbstractVector + public float getX () { + return x; + } + + @Override // from AbstractVector + public float getY () { + return y; + } } diff --git a/src/main/java/pythagoras/f/Vectors.java b/src/main/java/pythagoras/f/Vectors.java index 7f1dfc8..403d8b1 100644 --- a/src/main/java/pythagoras/f/Vectors.java +++ b/src/main/java/pythagoras/f/Vectors.java @@ -25,6 +25,50 @@ public class Vectors /** A vector containing the maximum floating point value for all components. */ public static final IVector MAX_VALUE = new Vector(Float.MAX_VALUE, Float.MAX_VALUE); + /** + * Returns the magnitude of the specified vector. + */ + public static final float length (float x, float y) { + return FloatMath.sqrt(lengthSq(x, y)); + } + + /** + * Returns the square of the magnitude of the specified vector. + */ + public static final float lengthSq (float x, float y) { + return (x*x + y*y); + } + + /** + * Transforms a point as specified, storing the result in the point provided. + * @return a reference to the result vector, for chaining. + */ + public static Vector transform (float x, float y, float sx, float sy, float rotation, + Vector result) { + return transform(x, y, sx, sy, FloatMath.sin(rotation), FloatMath.cos(rotation), result); + } + + /** + * Transforms a vector as specified, storing the result in the vector provided. + * @return a reference to the result vector, for chaining. + */ + public static Vector transform (float x, float y, float sx, float sy, float sina, float cosa, + Vector result) { + return result.set((x*cosa - y*sina) * sx, (x*sina + y*cosa) * sy); + } + + /** + * Inverse transforms a point as specified, storing the result in the point provided. + * @return a reference to the result vector, for chaining. + */ + public static Vector inverseTransform (float x, float y, float sx, float sy, float rotation, + Vector result) { + float sinnega = FloatMath.sin(-rotation), cosnega = FloatMath.cos(-rotation); + float nx = (x * cosnega - y * sinnega); // unrotate + float ny = (x * sinnega + y * cosnega); + return result.set(nx / sx, ny / sy); // unscale + } + /** * Returns a string describing the supplied vector, of the form +x+y, * +x-y, -x-y, etc. diff --git a/src/main/java/pythagoras/util/NoninvertibleTransformException.java b/src/main/java/pythagoras/util/NoninvertibleTransformException.java index 1835ef1..d7e6a19 100644 --- a/src/main/java/pythagoras/util/NoninvertibleTransformException.java +++ b/src/main/java/pythagoras/util/NoninvertibleTransformException.java @@ -5,7 +5,7 @@ package pythagoras.util; /** - * An exception thrown by {@code AffineTransform} when a request for an inverse transform cannot be + * An exception thrown by {@code Transform} when a request for an inverse transform cannot be * satisfied. */ public class NoninvertibleTransformException extends java.lang.RuntimeException diff --git a/src/test/java/pythagoras/f/TransformTest.java b/src/test/java/pythagoras/f/TransformTest.java new file mode 100644 index 0000000..343ab85 --- /dev/null +++ b/src/test/java/pythagoras/f/TransformTest.java @@ -0,0 +1,390 @@ +// +// Pythagoras - a collection of geometry classes +// http://github.com/samskivert/pythagoras + +package pythagoras.f; + +import org.junit.*; +import static org.junit.Assert.*; + +/** + * Tests the various transform implementations. + */ +public class TransformTest +{ + @Test public void testTranslate () { + for (Transform proto : createTransforms()) { + if (proto.generality() < RigidTransform.GENERALITY) continue; + for (Point trans : TRANS) { + Transform t = proto.clone(); + t.setTranslation(trans.x, trans.y); + for (Point point : POINTS) { + test(t, point, point.add(trans.x, trans.y)); + } + for (Vector vec : VECTORS) { + test(t, vec, vec); + } + } + } + } + + @Test public void testRotate () { + for (Transform proto : createTransforms()) { + if (proto.generality() < RigidTransform.GENERALITY) continue; + for (float angle : ANGLES) { + Transform t = proto.clone(); + t.setRotation(angle); + for (Point point : POINTS) { + test(t, point, point.rotate(angle)); + } + for (Vector vector : VECTORS) { + test(t, vector, vector.rotate(angle)); + } + } + } + } + + @Test public void testScale () { + for (Transform proto : createTransforms()) { + if (proto.generality() < UniformTransform.GENERALITY) continue; + for (Point point : POINTS) { + for (float scale : SCALES) { + Transform t = proto.clone(); + t.setUniformScale(scale); + test(t, point, point.mult(scale)); + } + } + } + } + + @Test public void testTranslateRotate () { + for (Transform proto : createTransforms()) { + if (proto.generality() < RigidTransform.GENERALITY) continue; + for (Point trans : TRANS) { + Transform t1 = proto.clone(); + t1.setTranslation(trans.x, trans.y); + for (float angle : ANGLES) { + Transform t2 = proto.clone(); + t2.setRotation(angle); + + Transform tpost = t2.concatenate(t1); + Transform tpre = t1.preConcatenate(t2); + for (Point point : POINTS) { + Point expect = point.add(trans.x, trans.y).rotateLocal(angle); + test(tpost, point, expect); + test(tpre, point, expect); + } + for (Vector vector : VECTORS) { + Vector expect = vector.rotate(angle); + test(tpost, vector, expect); + test(tpre, vector, expect); + } + } + } + } + } + + @Test public void testRotateTranslate () { + for (Transform proto : createTransforms()) { + if (proto.generality() < RigidTransform.GENERALITY) continue; + for (float angle : ANGLES) { + Transform t1 = proto.clone(); + t1.setRotation(angle); + for (Point trans : TRANS) { + Transform t2 = proto.clone(); + t2.setTranslation(trans.x, trans.y); + + // test that a single transform rotates then translates + Transform t = proto.clone(); + t.setRotation(angle); + t.setTranslation(trans.x, trans.y); + + // test explicitly via concatenation + Transform tpost = t2.concatenate(t1); + Transform tpre = t1.preConcatenate(t2); + for (Point point : POINTS) { + Point expect = point.rotate(angle).addLocal(trans.x, trans.y); + test(t, point, expect); + test(tpost, point, expect); + test(tpre, point, expect); + } + for (Vector vector : VECTORS) { + Vector expect = vector.rotate(angle); + test(t, vector, expect); + test(tpost, vector, expect); + test(tpre, vector, expect); + } + } + } + } + } + + @Test public void testTranslateScale () { + for (Transform proto : createTransforms()) { + if (proto.generality() < UniformTransform.GENERALITY) continue; + for (Point trans : TRANS) { + Transform t1 = proto.clone(); + t1.setTranslation(trans.x, trans.y); + for (float scale : SCALES) { + Transform t2 = proto.clone(); + t2.setUniformScale(scale); + + Transform tpost = t2.concatenate(t1); + Transform tpre = t1.preConcatenate(t2); + for (Point point : POINTS) { + Point expect = point.add(trans.x, trans.y).multLocal(scale); + test(tpost, point, expect); + test(tpre, point, expect); + } + for (Vector vector : VECTORS) { + Vector expect = vector.mult(scale); + test(tpost, vector, expect); + test(tpre, vector, expect); + } + } + } + } + } + + @Test public void testScaleTranslate () { + for (Transform proto : createTransforms()) { + if (proto.generality() < UniformTransform.GENERALITY) continue; + for (float scale : SCALES) { + Transform t1 = proto.clone(); + t1.setUniformScale(scale); + for (Point trans : TRANS) { + Transform t2 = proto.clone(); + t2.setTranslation(trans.x, trans.y); + + // test that a single transform scales then translates + Transform t = proto.clone(); + t.setUniformScale(scale); + t.setTranslation(trans.x, trans.y); + + // test explicitly via concatenation + Transform tpost = t2.concatenate(t1); + Transform tpre = t1.preConcatenate(t2); + for (Point point : POINTS) { + Point expect = point.mult(scale).addLocal(trans.x, trans.y); + test(t, point, expect); + test(tpost, point, expect); + test(tpre, point, expect); + } + for (Vector vector : VECTORS) { + Vector expect = vector.mult(scale); + test(t, vector, expect); + test(tpost, vector, expect); + test(tpre, vector, expect); + } + } + } + } + } + + @Test public void testRotateScale () { + for (Transform proto : createTransforms()) { + if (proto.generality() < UniformTransform.GENERALITY) continue; + for (float angle : ANGLES) { + Transform t1 = proto.clone(); + t1.setRotation(angle); + for (float scale : SCALES) { + Transform t2 = proto.clone(); + t2.setUniformScale(scale); + + Transform tpost = t2.concatenate(t1); + Transform tpre = t1.preConcatenate(t2); + for (Point point : POINTS) { + Point expect = point.rotate(angle).multLocal(scale); + test(tpost, point, expect); + test(tpre, point, expect); + } + for (Vector vector : VECTORS) { + Vector expect = vector.rotate(angle).multLocal(scale); + test(tpost, vector, expect); + test(tpre, vector, expect); + } + } + } + } + } + + @Test public void testScaleRotate () { + for (Transform proto : createTransforms()) { + if (proto.generality() < UniformTransform.GENERALITY) continue; + for (float scale : SCALES) { + Transform t1 = proto.clone(); + t1.setUniformScale(scale); + for (float angle : ANGLES) { + Transform t2 = proto.clone(); + t2.setRotation(angle); + + // test explicitly via concatenation + Transform tpost = t2.concatenate(t1); + Transform tpre = t1.preConcatenate(t2); + for (Point point : POINTS) { + Point expect = point.mult(scale).rotateLocal(angle); + test(tpost, point, expect); + test(tpre, point, expect); + } + for (Vector vector : VECTORS) { + Vector expect = vector.mult(scale).rotateLocal(angle); + test(tpost, vector, expect); + test(tpre, vector, expect); + } + + // if we have an affine transform, we cannot set the scale and then set the + // rotation, because setting the rotation will first extract the scale and then + // reapply it, losing the sign of the scale in the process + if (proto.generality() >= AffineTransform.GENERALITY) continue; + + // test that a single transform scales then rotates + Transform t = proto.clone(); + t.setUniformScale(scale); + t.setRotation(angle); + for (Point point : POINTS) { + Point expect = point.mult(scale).rotateLocal(angle); + test(t, point, expect); + } + for (Vector vector : VECTORS) { + Vector expect = vector.mult(scale).rotateLocal(angle); + test(t, vector, expect); + } + } + } + } + } + + @Test public void testScaleRotateTranslate () { + for (Transform proto : createTransforms()) { + if (proto.generality() < UniformTransform.GENERALITY) continue; + for (float scale : SCALES) { + Transform t1 = proto.clone(); + t1.setUniformScale(scale); + for (float angle : ANGLES) { + Transform t2 = proto.clone(); + t2.setRotation(angle); + for (Point trans : TRANS) { + Transform t3 = proto.clone(); + t3.setTranslation(trans.x, trans.y); + + // test explicitly via concatenation + Transform tpost = t3.concatenate(t2).concatenate(t1); + Transform tpre = t1.preConcatenate(t2.preConcatenate(t3)); + for (Point point : POINTS) { + Point expect = point.mult(scale).rotateLocal(angle). + addLocal(trans.x, trans.y); + test(tpost, point, expect); + test(tpre, point, expect); + } + for (Vector vector : VECTORS) { + Vector expect = vector.mult(scale).rotateLocal(angle); + test(tpost, vector, expect); + test(tpre, vector, expect); + } + + // if we have an affine transform, we cannot set the scale and then set the + // rotation, because setting the rotation will first extract the scale and + // then reapply it, losing the sign of the scale in the process + if (proto.generality() >= AffineTransform.GENERALITY) continue; + + // test that a single transform scales, rotates, then translates + Transform t = proto.clone(); + t.setUniformScale(scale); + t.setRotation(angle); + t.setTranslation(trans.x, trans.y); + for (Point point : POINTS) { + Point expect = point.mult(scale).rotateLocal(angle). + addLocal(trans.x, trans.y); + test(t, point, expect); + } + for (Vector vector : VECTORS) { + Vector expect = vector.mult(scale).rotateLocal(angle); + test(t, vector, expect); + } + } + } + } + } + } + + protected void test (Transform t, Point p, Point expect) { + Point orig = new Point(p); + String desc = t + " @ " + p; + + // test single point transform and inverse transform + Point tp = t.transform(p, new Point()); + Point itp = t.inverseTransform(tp, new Point()); + assertEquals(desc, orig, p); + assertPointsEqual(desc, expect, tp); + assertPointsEqual(desc, p, itp); + + // test multipoint transform + Point[] ps = new Point[] { null, p, null }; + Point[] tps = new Point[] { null, new Point(), null }; + t.transform(ps, 1, tps, 1, 1); + assertEquals(desc, orig, p); + assertEquals(desc, null, tps[0]); + assertPointsEqual(desc, expect, tps[1]); + assertEquals(desc, null, tps[2]); + } + + protected void assertPointsEqual (String desc, Point p1, Point p2) { + assertEquals(desc + " = " + p1, p1.x, p2.x, FloatMath.EPSILON); + assertEquals(desc + " = " + p1, p1.y, p2.y, FloatMath.EPSILON); + } + + protected void test (Transform t, Vector v, Vector expect) { + Vector orig = new Vector(v); + String desc = t + " @ " + v; + + // test vector transform and inverse transform + Vector tv = t.transform(v, new Vector()); + Vector itv = t.inverseTransform(tv, new Vector()); + assertEquals(desc, orig, v); + assertVectorsEqual(desc, expect, tv); + assertVectorsEqual(desc, v, itv); + } + + protected void assertVectorsEqual (String desc, Vector v1, Vector v2) { + assertEquals(desc + " = " + v1, v1.x, v2.x, FloatMath.EPSILON); + assertEquals(desc + " = " + v1, v1.y, v2.y, FloatMath.EPSILON); + } + + protected Transform[] createTransforms () { + return new Transform[] { + new IdentityTransform(), + new RigidTransform(), + new UniformTransform(), + new NonUniformTransform(), + new AffineTransform(), + }; + } + + protected static final Point[] POINTS = { + new Point(0, 0), new Point(FloatMath.TAU, FloatMath.E), + new Point(1, 0), new Point(0, 1), new Point(-1, 0), new Point(0, -1), + new Point(1, 1), new Point(-1, 1), new Point(-1, -1), new Point(1, -1) + }; + protected static final Vector[] VECTORS = { + new Vector(0, 0), new Vector(FloatMath.TAU, FloatMath.E), + new Vector(1, 0), new Vector(0, 1), new Vector(-1, 0), new Vector(0, -1), + new Vector(1, 1), new Vector(-1, 1), new Vector(-1, -1), new Vector(1, -1) + }; + protected static final float[] ANGLES = { + 0, FloatMath.PI/2, FloatMath.PI, FloatMath.PI*3/2, + -FloatMath.PI/2, -FloatMath.PI, -FloatMath.PI*3/2 + }; + protected static final float[] SCALES = { 0.5f, 1, 1.5f, -0.5f, -1, -1.5f }; + + protected static final float[] DXS = { -25, 0, 25 }; + protected static final float[] DYS = { -25, 0, 25 }; + protected static final Point[] TRANS = new Point[DXS.length * DYS.length]; + static { + int ii = 0; + for (float dx : DXS) { + for (float dy : DYS) { + TRANS[ii++] = new Point(dx, dy); + } + } + } +}