diff --git a/src/main/java/pythagoras/f/AbstractTransform.java b/src/main/java/pythagoras/f/AbstractTransform.java index 19cf17f..2f2f4ee 100644 --- a/src/main/java/pythagoras/f/AbstractTransform.java +++ b/src/main/java/pythagoras/f/AbstractTransform.java @@ -53,6 +53,50 @@ public abstract class AbstractTransform implements Transform return this; } + @Override // from Transform + public Transform uniformScale (float scale) { + throw new UnsupportedOperationException(); + } + + @Override // from Transform + public Transform scale (float scaleX, float scaleY) { + scaleX(scaleX); + scaleY(scaleY); + return this; + } + + @Override // from Transform + public Transform scaleX (float scaleX) { + throw new UnsupportedOperationException(); + } + + @Override // from Transform + public Transform scaleY (float scaleY) { + throw new UnsupportedOperationException(); + } + + @Override // from Transform + public Transform rotate (float angle) { + throw new UnsupportedOperationException(); + } + + @Override // from Transform + public Transform translate (float tx, float ty) { + translateX(tx); + translateY(ty); + return this; + } + + @Override // from Transform + public Transform translateX (float tx) { + throw new UnsupportedOperationException(); + } + + @Override // from Transform + public Transform translateY (float ty) { + throw new UnsupportedOperationException(); + } + @Override // from Transform public Transform setTx (float tx) { throw new UnsupportedOperationException(); diff --git a/src/main/java/pythagoras/f/AffineTransform.java b/src/main/java/pythagoras/f/AffineTransform.java index 287c8d8..db1e918 100644 --- a/src/main/java/pythagoras/f/AffineTransform.java +++ b/src/main/java/pythagoras/f/AffineTransform.java @@ -141,16 +141,9 @@ public class AffineTransform extends AbstractTransform 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; - } else if (Math.abs(sin) < ZERO) { - sin = 0; - cos = cos > 0 ? 1f : -1f; - } - m00 = cos * sx; m01 = -sin * sx; - m10 = sin * sy; m11 = cos * sy; + float sina = FloatMath.sin(angle), cosa = FloatMath.cos(angle); + m00 = cosa * sx; m01 = -sina * sx; + m10 = sina * sy; m11 = cosa * sy; return this; } @@ -184,6 +177,45 @@ public class AffineTransform extends AbstractTransform return this; } + @Override // from Transform + public Transform uniformScale (float scale) { + return scale(scale, scale); + } + + @Override // from Transform + public Transform scaleX (float scaleX) { + m00 *= scaleX; + m01 *= scaleX; + tx *= scaleX; + return this; + } + + @Override // from Transform + public Transform scaleY (float scaleY) { + m10 *= scaleY; + m11 *= scaleY; + ty *= scaleY; + return this; + } + + @Override // from Transform + public Transform rotate (float angle) { + float sina = FloatMath.sin(angle), cosa = FloatMath.cos(angle); + return multiply(cosa, -sina, sina, cosa, 0, 0, this, this); + } + + @Override // from Transform + public Transform translateX (float tx) { + this.tx += tx; + return this; + } + + @Override // from Transform + public Transform translateY (float ty) { + this.ty += ty; + return this; + } + @Override // from Transform public Transform invert () { // compute the determinant, storing the subdeterminants for later use @@ -204,8 +236,12 @@ public class AffineTransform extends AbstractTransform if (generality() < other.generality()) { return other.preConcatenate(this); } - return multiply((other instanceof AffineTransform) ? - (AffineTransform)other : new AffineTransform(other)); + if (other instanceof AffineTransform) { + return multiply(this, (AffineTransform)other, new AffineTransform()); + } else { + AffineTransform oaff = new AffineTransform(other); + return multiply(this, oaff, oaff); + } } @Override // from Transform @@ -213,8 +249,12 @@ public class AffineTransform extends AbstractTransform if (generality() < other.generality()) { return other.concatenate(this); } - return ((other instanceof AffineTransform) ? - (AffineTransform)other : new AffineTransform(other)).multiply(this); + if (other instanceof AffineTransform) { + return multiply((AffineTransform)other, this, new AffineTransform()); + } else { + AffineTransform oaff = new AffineTransform(other); + return multiply(oaff, this, oaff); + } } @Override // from Transform @@ -281,7 +321,8 @@ public class AffineTransform extends AbstractTransform @Override public String toString () { - return "affine [" + m00 + " " + m01 + " " + m10 + " " + m11 + " " + getTranslation() + "]"; + return "affine [" + FloatMath.toString(m00) + " " + FloatMath.toString(m01) + " " + + FloatMath.toString(m10) + " " + FloatMath.toString(m11) + " " + getTranslation() + "]"; } // we don't publicize this because it might encourage someone to do something stupid like @@ -291,18 +332,36 @@ public class AffineTransform extends AbstractTransform other.getTx(), other.getTy()); } - protected AffineTransform multiply (AffineTransform other) { - return multiply(other.m00, other.m01, other.m10, other.m11, other.tx, other.ty); + protected static AffineTransform multiply ( + AffineTransform a, AffineTransform b, AffineTransform into) { + return multiply(a.m00, a.m01, a.m10, a.m11, a.tx, a.ty, + b.m00, b.m01, b.m10, b.m11, b.tx, b.ty, into); } - 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); + protected static AffineTransform multiply ( + AffineTransform a, float m00, float m01, float m10, float m11, float tx, float ty, + AffineTransform into) { + return multiply(a.m00, a.m01, a.m10, a.m11, a.tx, a.ty, + m00, m01, m10, m11, tx, ty, into); } - /** The min value equivalent to zero. An absolute value < ZERO is considered to be zero. */ - private static final float ZERO = 1E-7f; + protected static AffineTransform multiply ( + float m00, float m01, float m10, float m11, float tx, float ty, + AffineTransform b, AffineTransform into) { + return multiply(m00, m01, m10, m11, tx, ty, + b.m00, b.m01, b.m10, b.m11, b.tx, b.ty, into); + } + + protected static AffineTransform multiply ( + float am00, float am01, float am10, float am11, float atx, float aty, + float bm00, float bm01, float bm10, float bm11, float btx, float bty, + AffineTransform into) { + into.m00 = am00 * bm00 + am01 * bm10; + into.m01 = am00 * bm01 + am01 * bm11; + into.m10 = am10 * bm00 + am11 * bm10; + into.m11 = am10 * bm01 + am11 * bm11; + into.tx = am00 * btx + am01 * bty + atx; + into.ty = am10 * btx + am11 * bty + aty; + return into; + } } diff --git a/src/main/java/pythagoras/f/FloatMath.java b/src/main/java/pythagoras/f/FloatMath.java index 2c47ae3..e6bae99 100644 --- a/src/main/java/pythagoras/f/FloatMath.java +++ b/src/main/java/pythagoras/f/FloatMath.java @@ -381,4 +381,15 @@ public class FloatMath { return (a > 0f ? PI : -PI) - a; } + + /** + * Returns ~0 if the value is very close to zero, the string value of the float otherwise. + */ + public static String toString (float value) + { + return Math.abs(value) < ZERO ? "~0" : String.valueOf(value); + } + + /** The min value equivalent to zero. An absolute value < ZERO is rendered as ~0. */ + private static final float ZERO = 1E-7f; } diff --git a/src/main/java/pythagoras/f/NonUniformTransform.java b/src/main/java/pythagoras/f/NonUniformTransform.java index 4663e99..95a5fcf 100644 --- a/src/main/java/pythagoras/f/NonUniformTransform.java +++ b/src/main/java/pythagoras/f/NonUniformTransform.java @@ -102,6 +102,51 @@ public class NonUniformTransform extends AbstractTransform return this; } + @Override // from Transform + public Transform uniformScale (float scale) { + return scale(scale, scale); + } + + @Override // from Transform + public Transform scaleX (float scaleX) { + if (scaleX == 0) throw new IllegalArgumentException("Scale (x) must not be zero."); + this.tx *= scaleX; + this.scaleX *= scaleX; + return this; + } + + @Override // from Transform + public Transform scaleY (float scaleY) { + if (scaleY == 0) throw new IllegalArgumentException("Scale (y) must not be zero."); + this.ty *= scaleX; + this.scaleY *= scaleY; + return this; + } + + @Override // from Transform + public Transform rotate (float angle) { + float otx = this.tx, oty = this.ty; + if (otx != 0 || oty != 0) { + float sina = FloatMath.sin(angle), cosa = FloatMath.cos(angle); + this.tx = otx*cosa - oty*sina; + this.ty = otx*sina + oty*cosa; + } + this.rotation += angle; + return this; + } + + @Override // from Transform + public Transform translateX (float tx) { + this.tx += tx; + return this; + } + + @Override // from Transform + public Transform translateY (float ty) { + this.ty += ty; + return this; + } + @Override // from Transform public Transform invert () { Vector iscale = new Vector(1f / scaleX, 1f / scaleY); diff --git a/src/main/java/pythagoras/f/Points.java b/src/main/java/pythagoras/f/Points.java index ab95ab6..d6697d0 100644 --- a/src/main/java/pythagoras/f/Points.java +++ b/src/main/java/pythagoras/f/Points.java @@ -58,9 +58,9 @@ public class Points public static String pointToString (float x, float y) { StringBuilder buf = new StringBuilder(); if (x >= 0) buf.append("+"); - buf.append(x); + buf.append(FloatMath.toString(x)); if (y >= 0) buf.append("+"); - buf.append(y); + buf.append(FloatMath.toString(y)); return buf.toString(); } } diff --git a/src/main/java/pythagoras/f/RigidTransform.java b/src/main/java/pythagoras/f/RigidTransform.java index 4da8d5f..9cba744 100644 --- a/src/main/java/pythagoras/f/RigidTransform.java +++ b/src/main/java/pythagoras/f/RigidTransform.java @@ -76,6 +76,30 @@ public class RigidTransform extends AbstractTransform return this; } + @Override // from Transform + public Transform rotate (float angle) { + float otx = this.tx, oty = this.ty; + if (otx != 0 || oty != 0) { + float sina = FloatMath.sin(angle), cosa = FloatMath.cos(angle); + this.tx = otx*cosa - oty*sina; + this.ty = otx*sina + oty*cosa; + } + this.rotation += angle; + return this; + } + + @Override // from Transform + public Transform translateX (float tx) { + this.tx += tx; + return this; + } + + @Override // from Transform + public Transform translateY (float ty) { + this.ty += ty; + return this; + } + @Override // from Transform public Transform invert () { Vector t = getTranslation().negateLocal().rotateLocal(-rotation); diff --git a/src/main/java/pythagoras/f/Transform.java b/src/main/java/pythagoras/f/Transform.java index 88d27b9..ac98645 100644 --- a/src/main/java/pythagoras/f/Transform.java +++ b/src/main/java/pythagoras/f/Transform.java @@ -48,19 +48,19 @@ public interface Transform /** 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. */ + * @throws UnsupportedOperationException if the transform is not non-uniform or greater. */ Transform setScale (float scaleX, float scaleY); /** 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. */ + * @throws UnsupportedOperationException if the transform is not non-uniform or greater. */ Transform setScaleX (float scaleX); /** 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. */ + * @throws UnsupportedOperationException if the transform is not non-uniform or greater. */ Transform setScaleY (float scaleY); /** Sets the rotation component of this transform. @@ -89,6 +89,50 @@ public interface Transform Transform setTransform (float m00, float m01, float m10, float m11, float tx, float ty); + /** Scales this transform in a uniform manner by the specified amount. + * @return this instance, for chaining. + * @throws IllegalArgumentException if the supplied scale is zero. + * @throws UnsupportedOperationException if the transform is not uniform or greater. */ + Transform uniformScale (float scale); + + /** Scales this transform by the specified amount in the x and y dimensions. + * @return this instance, for chaining. + * @throws IllegalArgumentException if either supplied scale is zero. + * @throws UnsupportedOperationException if the transform is not non-uniform or greater. */ + Transform scale (float scaleX, float scaleY); + + /** Scales this transform by the specified amount in the x dimension. + * @return this instance, for chaining. + * @throws IllegalArgumentException if the supplied scale is zero. + * @throws UnsupportedOperationException if the transform is not non-uniform or greater. */ + Transform scaleX (float scaleX); + + /** Scales this transform by the specified amount in the y dimension. + * @return this instance, for chaining. + * @throws IllegalArgumentException if the supplied scale is zero. + * @throws UnsupportedOperationException if the transform is not non-uniform or greater. */ + Transform scaleY (float scaleY); + + /** Rotates this transform. + * @return this instance, for chaining. + * @throws UnsupportedOperationException if the transform is not rigid body or greater. */ + Transform rotate (float angle); + + /** Translates this transform. + * @return this instance, for chaining. + * @throws UnsupportedOperationException if the transform is not rigid body or greater. */ + Transform translate (float tx, float ty); + + /** Translates this transform in the x dimension. + * @return this instance, for chaining. + * @throws UnsupportedOperationException if the transform is not rigid body or greater. */ + Transform translateX (float tx); + + /** Translates this transform in the y dimension. + * @return this instance, for chaining. + * @throws UnsupportedOperationException if the transform is not rigid body or greater. */ + Transform translateY (float ty); + /** Returns a new transform that represents the inverse of this transform. * @throws NoninvertibleTransformException if the transform is not invertible. */ Transform invert (); diff --git a/src/main/java/pythagoras/f/UniformTransform.java b/src/main/java/pythagoras/f/UniformTransform.java index 5e6cd81..45779cf 100644 --- a/src/main/java/pythagoras/f/UniformTransform.java +++ b/src/main/java/pythagoras/f/UniformTransform.java @@ -88,6 +88,39 @@ public class UniformTransform extends AbstractTransform return this; } + @Override // from Transform + public Transform uniformScale (float scale) { + if (scale == 0) throw new IllegalArgumentException("Scale must be non-zero."); + this.tx *= scale; + this.ty *= scale; + this.scale *= scale; + return this; + } + + @Override // from Transform + public Transform rotate (float angle) { + float otx = this.tx, oty = this.ty; + if (otx != 0 || oty != 0) { + float sina = FloatMath.sin(angle), cosa = FloatMath.cos(angle); + this.tx = otx*cosa - oty*sina; + this.ty = otx*sina + oty*cosa; + } + this.rotation += angle; + return this; + } + + @Override // from Transform + public Transform translateX (float tx) { + this.tx += tx; + return this; + } + + @Override // from Transform + public Transform translateY (float ty) { + this.ty += ty; + return this; + } + @Override // from Transform public Transform invert () { float nscale = 1f / scale, nrotation = -rotation; diff --git a/src/test/java/pythagoras/f/TransformTest.java b/src/test/java/pythagoras/f/TransformTest.java index 343ab85..57cf470 100644 --- a/src/test/java/pythagoras/f/TransformTest.java +++ b/src/test/java/pythagoras/f/TransformTest.java @@ -67,15 +67,18 @@ public class TransformTest Transform t2 = proto.clone(); t2.setRotation(angle); + Transform t = t1.clone().rotate(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(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); } @@ -129,15 +132,18 @@ public class TransformTest Transform t2 = proto.clone(); t2.setUniformScale(scale); + Transform t = t1.clone().uniformScale(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(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); } @@ -191,15 +197,18 @@ public class TransformTest Transform t2 = proto.clone(); t2.setUniformScale(scale); + Transform t = t1.clone().uniformScale(scale); Transform tpost = t2.concatenate(t1); Transform tpre = t1.preConcatenate(t2); for (Point point : POINTS) { Point expect = point.rotate(angle).multLocal(scale); + test(t, point, expect); test(tpost, point, expect); test(tpre, point, expect); } for (Vector vector : VECTORS) { Vector expect = vector.rotate(angle).multLocal(scale); + test(t, vector, expect); test(tpost, vector, expect); test(tpre, vector, expect); }