javafx.scene.transform.Rotate Maven / Gradle / Ivy
/*
* Copyright (c) 2011, 2018, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation. Oracle designates this
* particular file as subject to the "Classpath" exception as provided
* by Oracle in the LICENSE file that accompanied this code.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
package javafx.scene.transform;
import javafx.beans.property.DoubleProperty;
import javafx.beans.property.DoublePropertyBase;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.ObjectPropertyBase;
import javafx.geometry.Point3D;
import com.sun.javafx.geom.transform.Affine3D;
import com.sun.javafx.geom.transform.BaseTransform;
import javafx.geometry.Point2D;
/**
* This class represents an {@code Affine} object that rotates coordinates
* around an anchor point. This operation is equivalent to translating the
* coordinates so that the anchor point is at the origin (S1), then rotating them
* about the new origin (S2), and finally translating so that the
* intermediate origin is restored to the coordinates of the original
* anchor point (S3).
*
* The matrix representing the rotation transformation around an axis {@code (x,y,z)}
* by an angle {@code t} is as follows:
*
* [ cos(t) -sin(t) 0 x-x*cos(t)+y*sin(t) ]
* [ sin(t) cos(t) 0 y-x*sin(t)-y*cos(t) ]
* [ 0 0 1 z ]
*
*
* For example, to rotate a text 30 degrees around the Z-axis at
* anchor point of (50,30):
*
{@code
* Text text = new Text("This is a test");
* text.setX(10);
* text.setY(50);
* text.setFont(new Font(20));
*
* text.getTransforms().add(new Rotate(30, 50, 30));
* }
*
* @since JavaFX 2.0
*/
public class Rotate extends Transform {
/**
* Specifies the X-axis as the axis of rotation.
*/
public static final Point3D X_AXIS = new Point3D(1,0,0);
/**
* Specifies the Y-axis as the axis of rotation.
*/
public static final Point3D Y_AXIS = new Point3D(0,1,0);
/**
* Specifies the Z-axis as the axis of rotation.
*/
public static final Point3D Z_AXIS = new Point3D(0,0,1);
/**
* Avoids lot of repeated computation.
* @see #MatrixCache
*/
private MatrixCache cache;
/**
* Avoids lot of repeated computation.
* @see #MatrixCache
*/
private MatrixCache inverseCache;
/**
* Creates a default Rotate transform (identity).
*/
public Rotate() {
}
/**
* Creates a two-dimensional Rotate transform.
* The pivot point is set to (0,0)
* @param angle the angle of rotation measured in degrees
*/
public Rotate(double angle) {
setAngle(angle);
}
/**
* Creates a three-dimensional Rotate transform.
* The pivot point is set to (0,0,0)
* @param angle the angle of rotation measured in degrees
* @param axis the axis of rotation
*/
public Rotate(double angle, Point3D axis) {
setAngle(angle);
setAxis(axis);
}
/**
* Creates a two-dimensional Rotate transform with pivot.
* @param angle the angle of rotation measured in degrees
* @param pivotX the X coordinate of the rotation pivot point
* @param pivotY the Y coordinate of the rotation pivot point
*/
public Rotate(double angle, double pivotX, double pivotY) {
setAngle(angle);
setPivotX(pivotX);
setPivotY(pivotY);
}
/**
* Creates a simple Rotate transform with three-dimensional pivot.
* @param angle the angle of rotation measured in degrees
* @param pivotX the X coordinate of the rotation pivot point
* @param pivotY the Y coordinate of the rotation pivot point
* @param pivotZ the Z coordinate of the rotation pivot point
*/
public Rotate(double angle, double pivotX, double pivotY, double pivotZ) {
this(angle, pivotX, pivotY);
setPivotZ(pivotZ);
}
/**
* Creates a three-dimensional Rotate transform with pivot.
* @param angle the angle of rotation measured in degrees
* @param pivotX the X coordinate of the rotation pivot point
* @param pivotY the Y coordinate of the rotation pivot point
* @param pivotZ the Z coordinate of the rotation pivot point
* @param axis the axis of rotation
*/
public Rotate(double angle, double pivotX, double pivotY, double pivotZ, Point3D axis) {
this(angle, pivotX, pivotY);
setPivotZ(pivotZ);
setAxis(axis);
}
/**
* Defines the angle of rotation measured in degrees.
*/
private DoubleProperty angle;
public final void setAngle(double value) {
angleProperty().set(value);
}
public final double getAngle() {
return angle == null ? 0.0 : angle.get();
}
public final DoubleProperty angleProperty() {
if (angle == null) {
angle = new DoublePropertyBase() {
@Override
public void invalidated() {
transformChanged();
}
@Override
public Object getBean() {
return Rotate.this;
}
@Override
public String getName() {
return "angle";
}
};
}
return angle;
}
/**
* Defines the X coordinate of the rotation pivot point.
*
* @defaultValue 0.0
*/
private DoubleProperty pivotX;
public final void setPivotX(double value) {
pivotXProperty().set(value);
}
public final double getPivotX() {
return pivotX == null ? 0.0 : pivotX.get();
}
public final DoubleProperty pivotXProperty() {
if (pivotX == null) {
pivotX = new DoublePropertyBase() {
@Override
public void invalidated() {
transformChanged();
}
@Override
public Object getBean() {
return Rotate.this;
}
@Override
public String getName() {
return "pivotX";
}
};
}
return pivotX;
}
/**
* Defines the Y coordinate of the rotation pivot point.
*
* @defaultValue 0.0
*/
private DoubleProperty pivotY;
public final void setPivotY(double value) {
pivotYProperty().set(value);
}
public final double getPivotY() {
return pivotY == null ? 0.0 : pivotY.get();
}
public final DoubleProperty pivotYProperty() {
if (pivotY == null) {
pivotY = new DoublePropertyBase() {
@Override
public void invalidated() {
transformChanged();
}
@Override
public Object getBean() {
return Rotate.this;
}
@Override
public String getName() {
return "pivotY";
}
};
}
return pivotY;
}
/**
* Defines the Z coordinate of the rotation pivot point.
*
* @defaultValue 0.0
*/
private DoubleProperty pivotZ;
public final void setPivotZ(double value) {
pivotZProperty().set(value);
}
public final double getPivotZ() {
return pivotZ == null ? 0.0 : pivotZ.get();
}
public final DoubleProperty pivotZProperty() {
if (pivotZ == null) {
pivotZ = new DoublePropertyBase() {
@Override
public void invalidated() {
transformChanged();
}
@Override
public Object getBean() {
return Rotate.this;
}
@Override
public String getName() {
return "pivotZ";
}
};
}
return pivotZ;
}
/**
* Defines the axis of rotation at the pivot point.
*/
private ObjectProperty axis;
public final void setAxis(Point3D value) {
axisProperty().set(value);
}
public final Point3D getAxis() {
return axis == null ? Z_AXIS : axis.get();
}
public final ObjectProperty axisProperty() {
if (axis == null) {
axis = new ObjectPropertyBase(Z_AXIS) {
@Override
public void invalidated() {
transformChanged();
}
@Override
public Object getBean() {
return Rotate.this;
}
@Override
public String getName() {
return "axis";
}
};
}
return axis;
}
/* *************************************************************************
* *
* Element getters *
* *
**************************************************************************/
@Override
public double getMxx() {
updateCache();
return cache.mxx;
}
@Override
public double getMxy() {
updateCache();
return cache.mxy;
}
@Override
public double getMxz() {
updateCache();
return cache.mxz;
}
@Override
public double getTx() {
updateCache();
return cache.tx;
}
@Override
public double getMyx() {
updateCache();
return cache.myx;
}
@Override
public double getMyy() {
updateCache();
return cache.myy;
}
@Override
public double getMyz() {
updateCache();
return cache.myz;
}
@Override
public double getTy() {
updateCache();
return cache.ty;
}
@Override
public double getMzx() {
updateCache();
return cache.mzx;
}
@Override
public double getMzy() {
updateCache();
return cache.mzy;
}
@Override
public double getMzz() {
updateCache();
return cache.mzz;
}
@Override
public double getTz() {
updateCache();
return cache.tz;
}
/* *************************************************************************
* *
* State getters *
* *
**************************************************************************/
@Override
boolean computeIs2D() {
final Point3D a = getAxis();
return (a.getX() == 0.0 && a.getY() == 0.0) || getAngle() == 0;
}
@Override
boolean computeIsIdentity() {
if (getAngle() == 0.0) {
return true;
}
final Point3D a = getAxis();
return a.getX() == 0 && a.getY() == 0 && a.getZ() == 0.0;
}
/* *************************************************************************
* *
* Array getters *
* *
**************************************************************************/
@Override
void fill2DArray(double[] array) {
updateCache();
array[0] = cache.mxx;
array[1] = cache.mxy;
array[2] = cache.tx;
array[3] = cache.myx;
array[4] = cache.myy;
array[5] = cache.ty;
}
@Override
void fill3DArray(double[] array) {
updateCache();
array[0] = cache.mxx;
array[1] = cache.mxy;
array[2] = cache.mxz;
array[3] = cache.tx;
array[4] = cache.myx;
array[5] = cache.myy;
array[6] = cache.myz;
array[7] = cache.ty;
array[8] = cache.mzx;
array[9] = cache.mzy;
array[10] = cache.mzz;
array[11] = cache.tz;
return;
}
/* *************************************************************************
* *
* Transform creators *
* *
**************************************************************************/
@Override
public Transform createConcatenation(Transform transform) {
if (transform instanceof Rotate) {
Rotate r = (Rotate) transform;
final double px = getPivotX();
final double py = getPivotY();
final double pz = getPivotZ();
if ((r.getAxis() == getAxis() ||
r.getAxis().normalize().equals(getAxis().normalize())) &&
px == r.getPivotX() &&
py == r.getPivotY() &&
pz == r.getPivotZ()) {
return new Rotate(getAngle() + r.getAngle(), px, py, pz, getAxis());
}
}
if (transform instanceof Affine) {
Affine a = (Affine) transform.clone();
a.prepend(this);
return a;
}
return super.createConcatenation(transform);
}
@Override
public Transform createInverse() throws NonInvertibleTransformException {
return new Rotate(-getAngle(), getPivotX(), getPivotY(), getPivotZ(),
getAxis());
}
@Override
public Rotate clone() {
return new Rotate(getAngle(), getPivotX(), getPivotY(), getPivotZ(),
getAxis());
}
/* *************************************************************************
* *
* Transform, Inverse Transform *
* *
**************************************************************************/
@Override
public Point2D transform(double x, double y) {
ensureCanTransform2DPoint();
updateCache();
return new Point2D(
cache.mxx * x + cache.mxy * y + cache.tx,
cache.myx * x + cache.myy * y + cache.ty);
}
@Override
public Point3D transform(double x, double y, double z) {
updateCache();
return new Point3D(
cache.mxx * x + cache.mxy * y + cache.mxz * z + cache.tx,
cache.myx * x + cache.myy * y + cache.myz * z + cache.ty,
cache.mzx * x + cache.mzy * y + cache.mzz * z + cache.tz);
}
@Override
void transform2DPointsImpl(double[] srcPts, int srcOff,
double[] dstPts, int dstOff, int numPts) {
updateCache();
while (--numPts >= 0) {
final double x = srcPts[srcOff++];
final double y = srcPts[srcOff++];
dstPts[dstOff++] = cache.mxx * x + cache.mxy * y + cache.tx;
dstPts[dstOff++] = cache.myx * x + cache.myy * y + cache.ty;
}
}
@Override
void transform3DPointsImpl(double[] srcPts, int srcOff,
double[] dstPts, int dstOff, int numPts) {
updateCache();
while (--numPts >= 0) {
final double x = srcPts[srcOff++];
final double y = srcPts[srcOff++];
final double z = srcPts[srcOff++];
dstPts[dstOff++] = cache.mxx * x + cache.mxy * y + cache.mxz * z + cache.tx;
dstPts[dstOff++] = cache.myx * x + cache.myy * y + cache.myz * z + cache.ty;
dstPts[dstOff++] = cache.mzx * x + cache.mzy * y + cache.mzz * z + cache.tz;
}
}
@Override
public Point2D deltaTransform(double x, double y) {
ensureCanTransform2DPoint();
updateCache();
return new Point2D(
cache.mxx * x + cache.mxy * y,
cache.myx * x + cache.myy * y);
}
@Override
public Point3D deltaTransform(double x, double y, double z) {
updateCache();
return new Point3D(
cache.mxx * x + cache.mxy * y + cache.mxz * z,
cache.myx * x + cache.myy * y + cache.myz * z,
cache.mzx * x + cache.mzy * y + cache.mzz * z);
}
@Override
public Point2D inverseTransform(double x, double y) {
ensureCanTransform2DPoint();
updateInverseCache();
return new Point2D(
inverseCache.mxx * x + inverseCache.mxy * y + inverseCache.tx,
inverseCache.myx * x + inverseCache.myy * y + inverseCache.ty);
}
@Override
public Point3D inverseTransform(double x, double y, double z) {
updateInverseCache();
return new Point3D(
inverseCache.mxx * x + inverseCache.mxy * y + inverseCache.mxz * z
+ inverseCache.tx,
inverseCache.myx * x + inverseCache.myy * y + inverseCache.myz * z
+ inverseCache.ty,
inverseCache.mzx * x + inverseCache.mzy * y + inverseCache.mzz * z
+ inverseCache.tz);
}
@Override
void inverseTransform2DPointsImpl(double[] srcPts, int srcOff,
double[] dstPts, int dstOff, int numPts) {
updateInverseCache();
while (--numPts >= 0) {
final double x = srcPts[srcOff++];
final double y = srcPts[srcOff++];
dstPts[dstOff++] = inverseCache.mxx * x + inverseCache.mxy * y
+ inverseCache.tx;
dstPts[dstOff++] = inverseCache.myx * x + inverseCache.myy * y
+ inverseCache.ty;
}
}
@Override
void inverseTransform3DPointsImpl(double[] srcPts, int srcOff,
double[] dstPts, int dstOff, int numPts) {
updateInverseCache();
while (--numPts >= 0) {
final double x = srcPts[srcOff++];
final double y = srcPts[srcOff++];
final double z = srcPts[srcOff++];
dstPts[dstOff++] = inverseCache.mxx * x + inverseCache.mxy * y
+ inverseCache.mxz * z + inverseCache.tx;
dstPts[dstOff++] = inverseCache.myx * x + inverseCache.myy * y
+ inverseCache.myz * z + inverseCache.ty;
dstPts[dstOff++] = inverseCache.mzx * x + inverseCache.mzy * y
+ inverseCache.mzz * z + inverseCache.tz;
}
}
@Override
public Point2D inverseDeltaTransform(double x, double y) {
ensureCanTransform2DPoint();
updateInverseCache();
return new Point2D(
inverseCache.mxx * x + inverseCache.mxy * y,
inverseCache.myx * x + inverseCache.myy * y);
}
@Override
public Point3D inverseDeltaTransform(double x, double y, double z) {
updateInverseCache();
return new Point3D(
inverseCache.mxx * x + inverseCache.mxy * y + inverseCache.mxz * z,
inverseCache.myx * x + inverseCache.myy * y + inverseCache.myz * z,
inverseCache.mzx * x + inverseCache.mzy * y + inverseCache.mzz * z);
}
/* *************************************************************************
* *
* Other API *
* *
**************************************************************************/
/**
* Returns a string representation of this {@code Rotate} object.
* @return a string representation of this {@code Rotate} object.
*/
@Override
public String toString() {
final StringBuilder sb = new StringBuilder("Rotate [");
sb.append("angle=").append(getAngle());
sb.append(", pivotX=").append(getPivotX());
sb.append(", pivotY=").append(getPivotY());
sb.append(", pivotZ=").append(getPivotZ());
sb.append(", axis=").append(getAxis());
return sb.append("]").toString();
}
/* *************************************************************************
* *
* Internal implementation stuff *
* *
**************************************************************************/
@Override
void apply(final Affine3D trans) {
double localPivotX = getPivotX();
double localPivotY = getPivotY();
double localPivotZ = getPivotZ();
double localAngle = getAngle();
if (localPivotX != 0 || localPivotY != 0 || localPivotZ != 0) {
trans.translate(localPivotX, localPivotY, localPivotZ);
trans.rotate(Math.toRadians(localAngle),
getAxis().getX(),getAxis().getY(), getAxis().getZ());
trans.translate(-localPivotX, -localPivotY, -localPivotZ);
} else {
trans.rotate(Math.toRadians(localAngle),
getAxis().getX(), getAxis().getY(), getAxis().getZ());
}
}
@Override
BaseTransform derive(BaseTransform trans) {
if (isIdentity()) {
return trans;
}
double localPivotX = getPivotX();
double localPivotY = getPivotY();
double localPivotZ = getPivotZ();
double localAngle = getAngle();
if (localPivotX != 0 || localPivotY != 0 || localPivotZ != 0) {
trans = trans.deriveWithTranslation(localPivotX, localPivotY, localPivotZ);
trans = trans.deriveWithRotation(Math.toRadians(localAngle),
getAxis().getX(),getAxis().getY(), getAxis().getZ());
return trans.deriveWithTranslation(-localPivotX, -localPivotY, -localPivotZ);
} else {
return trans.deriveWithRotation(Math.toRadians(localAngle),
getAxis().getX(), getAxis().getY(), getAxis().getZ());
}
}
@Override
void validate() {
getAxis();
getAngle();
getPivotX();
getPivotY();
getPivotZ();
}
@Override
protected void transformChanged() {
if (cache != null) {
cache.invalidate();
}
super.transformChanged();
}
@Override
void appendTo(Affine a) {
a.appendRotation(getAngle(), getPivotX(), getPivotY(), getPivotZ(),
getAxis());
}
@Override
void prependTo(Affine a) {
a.prependRotation(getAngle(), getPivotX(), getPivotY(), getPivotZ(),
getAxis());
}
/**
* Updates the matrix cache
*/
private void updateCache() {
if (cache == null) {
cache = new MatrixCache();
}
if (!cache.valid) {
cache.update(getAngle(), getAxis(),
getPivotX(), getPivotY(), getPivotZ());
}
}
/**
* Updates the inverse matrix cache
*/
private void updateInverseCache() {
if (inverseCache == null) {
inverseCache = new MatrixCache();
}
if (!inverseCache.valid) {
inverseCache.update(-getAngle(), getAxis(),
getPivotX(), getPivotY(), getPivotZ());
}
}
/**
* Matrix cache. Computing single transformation matrix elements for
* a general rotation is quite expensive. Also each of those partial
* computations need some common operations to be made (compute sin
* and cos, normalize axis). Therefore with the direct element computations
* if all the getters for the elements are called to get the matrix,
* the result is slow.
*
* If a matrix element is asked for, we can reasonably anticipate that
* some other elements will be asked for as well. So when any element
* needs to be computed, we compute the entire matrix, cache it,
* and use the stored values until the transform changes.
*/
private static class MatrixCache {
boolean valid = false;
boolean is3D = false;
double mxx, mxy, mxz, tx,
myx, myy, myz, ty,
mzx, mzy, mzz, tz;
public MatrixCache() {
// to have the 3D part right when using 2D-only
mzz = 1.0;
}
public void update(double angle, Point3D axis,
double px, double py, double pz) {
final double rads = Math.toRadians(angle);
final double sin = Math.sin(rads);
final double cos = Math.cos(rads);
if (axis == Z_AXIS ||
(axis.getX() == 0.0 &&
axis.getY() == 0.0 &&
axis.getZ() > 0.0)) {
// 2D case
mxx = cos;
mxy = -sin;
tx = px * (1 - cos) + py * sin;
myx = sin;
myy = cos;
ty = py * (1 - cos) - px * sin;
if (is3D) {
// Was 3D, needs to set the 3D values
mxz = 0.0;
myz = 0.0;
mzx = 0.0;
mzy = 0.0;
mzz = 1.0;
tz = 0.0;
is3D = false;
}
valid = true;
return;
}
// 3D case
is3D = true;
double axisX, axisY, axisZ;
if (axis == X_AXIS || axis == Y_AXIS || axis == Z_AXIS) {
axisX = axis.getX();
axisY = axis.getY();
axisZ = axis.getZ();
} else {
// normalize
final double mag = Math.sqrt(axis.getX() * axis.getX() +
axis.getY() * axis.getY() + axis.getZ() * axis.getZ());
if (mag == 0.0) {
mxx = 1; mxy = 0; mxz = 0; tx = 0;
myx = 0; myy = 1; myz = 0; ty = 0;
mzx = 0; mzy = 0; mzz = 1; tz = 0;
valid = true;
return;
} else {
axisX = axis.getX() / mag;
axisY = axis.getY() / mag;
axisZ = axis.getZ() / mag;
}
}
mxx = cos + axisX * axisX * (1 - cos);
mxy = axisX * axisY * (1 - cos) - axisZ * sin;
mxz = axisX * axisZ * (1 - cos) + axisY * sin;
tx = px * (1 - mxx) - py * mxy - pz * mxz;
myx = axisY * axisX * (1 - cos) + axisZ * sin;
myy = cos + axisY * axisY * (1 - cos);
myz = axisY * axisZ * (1 - cos) - axisX * sin;
ty = py * (1 - myy) - px * myx - pz * myz;
mzx = axisZ * axisX * (1 - cos) - axisY * sin;
mzy = axisZ * axisY * (1 - cos) + axisX * sin;
mzz = cos + axisZ * axisZ * (1 - cos);
tz = pz * (1 - mzz) - px * mzx - py * mzy;
valid = true;
}
public void invalidate() {
valid = false;
}
}
}