--- /dev/null 2015-04-01 20:20:30.714960462 +0300 +++ new/apps/samples/3DViewer/src/main/java/com/javafx/experiments/utils3d/geom/transform/Affine2D.java 2015-04-01 21:32:24.953401522 +0300 @@ -0,0 +1,1518 @@ +/* + * Copyright (c) 1996, 2015, Oracle and/or its affiliates. + * All rights reserved. Use is subject to license terms. + * + * This file is available and licensed under the following license: + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * - Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * - Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in + * the documentation and/or other materials provided with the distribution. + * - Neither the name of Oracle Corporation nor the names of its + * contributors may be used to endorse or promote products derived + * from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package com.javafx.experiments.utils3d.geom.transform; + +import com.javafx.experiments.utils3d.geom.Point2D; + +/** + * The Affine2D class represents a 2D affine transform + * that performs a linear mapping from 2D coordinates to other 2D + * coordinates that preserves the "straightness" and + * "parallelness" of lines. Affine transformations can be constructed + * using sequences of translations, scales, flips, rotations, and shears. + *

+ * Such a coordinate transformation can be represented by a 3 row by + * 3 column matrix with an implied last row of [ 0 0 1 ]. This matrix + * transforms source coordinates {@code (x,y)} into + * destination coordinates {@code (x',y')} by considering + * them to be a column vector and multiplying the coordinate vector + * by the matrix according to the following process: + *

+ *  [ x']   [  m00  m01  m02  ] [ x ]   [ m00x + m01y + m02 ]
+ *  [ y'] = [  m10  m11  m12  ] [ y ] = [ m10x + m11y + m12 ]
+ *  [ 1 ]   [   0    0    1   ] [ 1 ]   [         1         ]
+ * 
+ *

+ *

Handling 90-Degree Rotations

+ *

+ * In some variations of the rotate methods in the + * Affine2D class, a double-precision argument + * specifies the angle of rotation in radians. + * These methods have special handling for rotations of approximately + * 90 degrees (including multiples such as 180, 270, and 360 degrees), + * so that the common case of quadrant rotation is handled more + * efficiently. + * This special handling can cause angles very close to multiples of + * 90 degrees to be treated as if they were exact multiples of + * 90 degrees. + * For small multiples of 90 degrees the range of angles treated + * as a quadrant rotation is approximately 0.00000121 degrees wide. + * This section explains why such special care is needed and how + * it is implemented. + *

+ * Since 90 degrees is represented as PI/2 in radians, + * and since PI is a transcendental (and therefore irrational) number, + * it is not possible to exactly represent a multiple of 90 degrees as + * an exact double precision value measured in radians. + * As a result it is theoretically impossible to describe quadrant + * rotations (90, 180, 270 or 360 degrees) using these values. + * Double precision floating point values can get very close to + * non-zero multiples of PI/2 but never close enough + * for the sine or cosine to be exactly 0.0, 1.0 or -1.0. + * The implementations of Math.sin() and + * Math.cos() correspondingly never return 0.0 + * for any case other than Math.sin(0.0). + * These same implementations do, however, return exactly 1.0 and + * -1.0 for some range of numbers around each multiple of 90 + * degrees since the correct answer is so close to 1.0 or -1.0 that + * the double precision significand cannot represent the difference + * as accurately as it can for numbers that are near 0.0. + *

+ * The net result of these issues is that if the + * Math.sin() and Math.cos() methods + * are used to directly generate the values for the matrix modifications + * during these radian-based rotation operations then the resulting + * transform is never strictly classifiable as a quadrant rotation + * even for a simple case like rotate(Math.PI/2.0), + * due to minor variations in the matrix caused by the non-0.0 values + * obtained for the sine and cosine. + * If these transforms are not classified as quadrant rotations then + * subsequent code which attempts to optimize further operations based + * upon the type of the transform will be relegated to its most general + * implementation. + *

+ * Because quadrant rotations are fairly common, + * this class should handle these cases reasonably quickly, both in + * applying the rotations to the transform and in applying the resulting + * transform to the coordinates. + * To facilitate this optimal handling, the methods which take an angle + * of rotation measured in radians attempt to detect angles that are + * intended to be quadrant rotations and treat them as such. + * These methods therefore treat an angle theta as a quadrant + * rotation if either Math.sin(theta) or + * Math.cos(theta) returns exactly 1.0 or -1.0. + * As a rule of thumb, this property holds true for a range of + * approximately 0.0000000211 radians (or 0.00000121 degrees) around + * small multiples of Math.PI/2.0. + * + * @version 1.83, 05/05/07 + */ +public class Affine2D extends AffineBase { + private Affine2D(double mxx, double myx, + double mxy, double myy, + double mxt, double myt, + int state) + { + this.mxx = mxx; + this.myx = myx; + this.mxy = mxy; + this.myy = myy; + this.mxt = mxt; + this.myt = myt; + this.state = state; + this.type = TYPE_UNKNOWN; + } + + /** + * Constructs a new Affine2D representing the + * Identity transformation. + */ + public Affine2D() { + mxx = myy = 1.0; + // m01 = m10 = m02 = m12 = 0.0; /* Not needed. */ + // state = APPLY_IDENTITY; /* Not needed. */ + // type = TYPE_IDENTITY; /* Not needed. */ + } + + /** + * Constructs a new Affine2D that uses the same transform + * as the specified BaseTransform object. + * @param Tx the BaseTransform object to copy + */ + public Affine2D(BaseTransform Tx) { + setTransform(Tx); + } + + /** + * Constructs a new Affine2D from 6 floating point + * values representing the 6 specifiable entries of the 3x3 + * transformation matrix. + * + * @param mxx the X coordinate scaling element of the 3x3 matrix + * @param myx the Y coordinate shearing element of the 3x3 matrix + * @param mxy the X coordinate shearing element of the 3x3 matrix + * @param myy the Y coordinate scaling element of the 3x3 matrix + * @param mxt the X coordinate translation element of the 3x3 matrix + * @param myt the Y coordinate translation element of the 3x3 matrix + */ + public Affine2D(float mxx, float myx, + float mxy, float myy, + float mxt, float myt) + { + this.mxx = mxx; + this.myx = myx; + this.mxy = mxy; + this.myy = myy; + this.mxt = mxt; + this.myt = myt; + updateState2D(); + } + + /** + * Constructs a new Affine2D from 6 double + * precision values representing the 6 specifiable entries of the 3x3 + * transformation matrix. + * + * @param mxx the X coordinate scaling element of the 3x3 matrix + * @param myx the Y coordinate shearing element of the 3x3 matrix + * @param mxy the X coordinate shearing element of the 3x3 matrix + * @param myy the Y coordinate scaling element of the 3x3 matrix + * @param mxt the X coordinate translation element of the 3x3 matrix + * @param myt the Y coordinate translation element of the 3x3 matrix + */ + public Affine2D(double mxx, double myx, + double mxy, double myy, + double mxt, double myt) + { + this.mxx = mxx; + this.myx = myx; + this.mxy = mxy; + this.myy = myy; + this.mxt = mxt; + this.myt = myt; + updateState2D(); + } + + @Override + public Degree getDegree() { + return Degree.AFFINE_2D; + } + + @Override + protected void reset3Delements() { /* NOP for Affine2D */ } + + /** + * Concatenates this transform with a transform 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). + *

+ * This operation is equivalent to the following sequence of calls: + *

+     *     translate(anchorx, anchory);      // S3: final translation
+     *     rotate(theta);                    // S2: rotate around anchor
+     *     translate(-anchorx, -anchory);    // S1: translate anchor to origin
+     * 
+ * Rotating by a positive angle theta rotates points on the positive + * X axis toward the positive Y axis. + * Note also the discussion of + * Handling 90-Degree Rotations + * above. + * + * @param theta the angle of rotation measured in radians + * @param anchorx the X coordinate of the rotation anchor point + * @param anchory the Y coordinate of the rotation anchor point + */ + public void rotate(double theta, double anchorx, double anchory) { + // REMIND: Simple for now - optimize later + translate(anchorx, anchory); + rotate(theta); + translate(-anchorx, -anchory); + } + + /** + * Concatenates this transform with a transform that rotates + * coordinates according to a rotation vector. + * All coordinates rotate about the origin by the same amount. + * The amount of rotation is such that coordinates along the former + * positive X axis will subsequently align with the vector pointing + * from the origin to the specified vector coordinates. + * If both vecx and vecy are 0.0, + * no additional rotation is added to this transform. + * This operation is equivalent to calling: + *
+     *          rotate(Math.atan2(vecy, vecx));
+     * 
+ * + * @param vecx the X coordinate of the rotation vector + * @param vecy the Y coordinate of the rotation vector + */ + public void rotate(double vecx, double vecy) { + if (vecy == 0.0) { + if (vecx < 0.0) { + rotate180(); + } + // If vecx > 0.0 - no rotation + // If vecx == 0.0 - undefined rotation - treat as no rotation + } else if (vecx == 0.0) { + if (vecy > 0.0) { + rotate90(); + } else { // vecy must be < 0.0 + rotate270(); + } + } else { + double len = Math.sqrt(vecx * vecx + vecy * vecy); + double sin = vecy / len; + double cos = vecx / len; + double M0, M1; + M0 = mxx; + M1 = mxy; + mxx = cos * M0 + sin * M1; + mxy = -sin * M0 + cos * M1; + M0 = myx; + M1 = myy; + myx = cos * M0 + sin * M1; + myy = -sin * M0 + cos * M1; + updateState2D(); + } + } + + /** + * Concatenates this transform with a transform that rotates + * coordinates around an anchor point according to a rotation + * vector. + * All coordinates rotate about the specified anchor coordinates + * by the same amount. + * The amount of rotation is such that coordinates along the former + * positive X axis will subsequently align with the vector pointing + * from the origin to the specified vector coordinates. + * If both vecx and vecy are 0.0, + * the transform is not modified in any way. + * This method is equivalent to calling: + *
+     *     rotate(Math.atan2(vecy, vecx), anchorx, anchory);
+     * 
+ * + * @param vecx the X coordinate of the rotation vector + * @param vecy the Y coordinate of the rotation vector + * @param anchorx the X coordinate of the rotation anchor point + * @param anchory the Y coordinate of the rotation anchor point + */ + public void rotate(double vecx, double vecy, + double anchorx, double anchory) + { + // REMIND: Simple for now - optimize later + translate(anchorx, anchory); + rotate(vecx, vecy); + translate(-anchorx, -anchory); + } + + /** + * Concatenates this transform with a transform that rotates + * coordinates by the specified number of quadrants. + * This is equivalent to calling: + *
+     *     rotate(numquadrants * Math.PI / 2.0);
+     * 
+ * Rotating by a positive number of quadrants rotates points on + * the positive X axis toward the positive Y axis. + * @param numquadrants the number of 90 degree arcs to rotate by + */ + public void quadrantRotate(int numquadrants) { + switch (numquadrants & 3) { + case 0: + break; + case 1: + rotate90(); + break; + case 2: + rotate180(); + break; + case 3: + rotate270(); + break; + } + } + + /** + * Concatenates this transform with a transform that rotates + * coordinates by the specified number of quadrants around + * the specified anchor point. + * This method is equivalent to calling: + *
+     *     rotate(numquadrants * Math.PI / 2.0, anchorx, anchory);
+     * 
+ * Rotating by a positive number of quadrants rotates points on + * the positive X axis toward the positive Y axis. + * + * @param numquadrants the number of 90 degree arcs to rotate by + * @param anchorx the X coordinate of the rotation anchor point + * @param anchory the Y coordinate of the rotation anchor point + */ + public void quadrantRotate(int numquadrants, + double anchorx, double anchory) + { + switch (numquadrants & 3) { + case 0: + return; + case 1: + mxt += anchorx * (mxx - mxy) + anchory * (mxy + mxx); + myt += anchorx * (myx - myy) + anchory * (myy + myx); + rotate90(); + break; + case 2: + mxt += anchorx * (mxx + mxx) + anchory * (mxy + mxy); + myt += anchorx * (myx + myx) + anchory * (myy + myy); + rotate180(); + break; + case 3: + mxt += anchorx * (mxx + mxy) + anchory * (mxy - mxx); + myt += anchorx * (myx + myy) + anchory * (myy - myx); + rotate270(); + break; + } + if (mxt == 0.0 && myt == 0.0) { + state &= ~APPLY_TRANSLATE; + if (type != TYPE_UNKNOWN) { + type &= ~TYPE_TRANSLATION; + } + } else { + state |= APPLY_TRANSLATE; + type |= TYPE_TRANSLATION; + } + } + + /** + * Sets this transform to a translation transformation. + * The matrix representing this transform becomes: + *
+     *      [   1    0    tx  ]
+     *      [   0    1    ty  ]
+     *      [   0    0    1   ]
+     * 
+ * @param tx the distance by which coordinates are translated in the + * X axis direction + * @param ty the distance by which coordinates are translated in the + * Y axis direction + */ + public void setToTranslation(double tx, double ty) { + mxx = 1.0; + myx = 0.0; + mxy = 0.0; + myy = 1.0; + mxt = tx; + myt = ty; + if (tx != 0.0 || ty != 0.0) { + state = APPLY_TRANSLATE; + type = TYPE_TRANSLATION; + } else { + state = APPLY_IDENTITY; + type = TYPE_IDENTITY; + } + } + + /** + * Sets this transform to a rotation transformation. + * The matrix representing this transform becomes: + *
+     *      [   cos(theta)    -sin(theta)    0   ]
+     *      [   sin(theta)     cos(theta)    0   ]
+     *      [       0              0         1   ]
+     * 
+ * Rotating by a positive angle theta rotates points on the positive + * X axis toward the positive Y axis. + * Note also the discussion of + * Handling 90-Degree Rotations + * above. + * @param theta the angle of rotation measured in radians + */ + public void setToRotation(double theta) { + double sin = Math.sin(theta); + double cos; + if (sin == 1.0 || sin == -1.0) { + cos = 0.0; + state = APPLY_SHEAR; + type = TYPE_QUADRANT_ROTATION; + } else { + cos = Math.cos(theta); + if (cos == -1.0) { + sin = 0.0; + state = APPLY_SCALE; + type = TYPE_QUADRANT_ROTATION; + } else if (cos == 1.0) { + sin = 0.0; + state = APPLY_IDENTITY; + type = TYPE_IDENTITY; + } else { + state = APPLY_SHEAR | APPLY_SCALE; + type = TYPE_GENERAL_ROTATION; + } + } + mxx = cos; + myx = sin; + mxy = -sin; + myy = cos; + mxt = 0.0; + myt = 0.0; + } + + /** + * Sets this transform to a translated rotation transformation. + * 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). + *

+ * This operation is equivalent to the following sequence of calls: + *

+     *     setToTranslation(anchorx, anchory); // S3: final translation
+     *     rotate(theta);                      // S2: rotate around anchor
+     *     translate(-anchorx, -anchory);      // S1: translate anchor to origin
+     * 
+ * The matrix representing this transform becomes: + *
+     *      [   cos(theta)    -sin(theta)    x-x*cos+y*sin  ]
+     *      [   sin(theta)     cos(theta)    y-x*sin-y*cos  ]
+     *      [       0              0               1        ]
+     * 
+ * Rotating by a positive angle theta rotates points on the positive + * X axis toward the positive Y axis. + * Note also the discussion of + * Handling 90-Degree Rotations + * above. + * + * @param theta the angle of rotation measured in radians + * @param anchorx the X coordinate of the rotation anchor point + * @param anchory the Y coordinate of the rotation anchor point + */ + public void setToRotation(double theta, double anchorx, double anchory) { + setToRotation(theta); + double sin = myx; + double oneMinusCos = 1.0 - mxx; + mxt = anchorx * oneMinusCos + anchory * sin; + myt = anchory * oneMinusCos - anchorx * sin; + if (mxt != 0.0 || myt != 0.0) { + state |= APPLY_TRANSLATE; + type |= TYPE_TRANSLATION; + } + } + + /** + * Sets this transform to a rotation transformation that rotates + * coordinates according to a rotation vector. + * All coordinates rotate about the origin by the same amount. + * The amount of rotation is such that coordinates along the former + * positive X axis will subsequently align with the vector pointing + * from the origin to the specified vector coordinates. + * If both vecx and vecy are 0.0, + * the transform is set to an identity transform. + * This operation is equivalent to calling: + *
+     *     setToRotation(Math.atan2(vecy, vecx));
+     * 
+ * + * @param vecx the X coordinate of the rotation vector + * @param vecy the Y coordinate of the rotation vector + */ + public void setToRotation(double vecx, double vecy) { + double sin, cos; + if (vecy == 0) { + sin = 0.0; + if (vecx < 0.0) { + cos = -1.0; + state = APPLY_SCALE; + type = TYPE_QUADRANT_ROTATION; + } else { + cos = 1.0; + state = APPLY_IDENTITY; + type = TYPE_IDENTITY; + } + } else if (vecx == 0) { + cos = 0.0; + sin = (vecy > 0.0) ? 1.0 : -1.0; + state = APPLY_SHEAR; + type = TYPE_QUADRANT_ROTATION; + } else { + double len = Math.sqrt(vecx * vecx + vecy * vecy); + cos = vecx / len; + sin = vecy / len; + state = APPLY_SHEAR | APPLY_SCALE; + type = TYPE_GENERAL_ROTATION; + } + mxx = cos; + myx = sin; + mxy = -sin; + myy = cos; + mxt = 0.0; + myt = 0.0; + } + + /** + * Sets this transform to a rotation transformation that rotates + * coordinates around an anchor point according to a rotation + * vector. + * All coordinates rotate about the specified anchor coordinates + * by the same amount. + * The amount of rotation is such that coordinates along the former + * positive X axis will subsequently align with the vector pointing + * from the origin to the specified vector coordinates. + * If both vecx and vecy are 0.0, + * the transform is set to an identity transform. + * This operation is equivalent to calling: + *
+     *     setToTranslation(Math.atan2(vecy, vecx), anchorx, anchory);
+     * 
+ * + * @param vecx the X coordinate of the rotation vector + * @param vecy the Y coordinate of the rotation vector + * @param anchorx the X coordinate of the rotation anchor point + * @param anchory the Y coordinate of the rotation anchor point + */ + public void setToRotation(double vecx, double vecy, + double anchorx, double anchory) + { + setToRotation(vecx, vecy); + double sin = myx; + double oneMinusCos = 1.0 - mxx; + mxt = anchorx * oneMinusCos + anchory * sin; + myt = anchory * oneMinusCos - anchorx * sin; + if (mxt != 0.0 || myt != 0.0) { + state |= APPLY_TRANSLATE; + type |= TYPE_TRANSLATION; + } + } + + /** + * Sets this transform to a rotation transformation that rotates + * coordinates by the specified number of quadrants. + * This operation is equivalent to calling: + *
+     *     setToRotation(numquadrants * Math.PI / 2.0);
+     * 
+ * Rotating by a positive number of quadrants rotates points on + * the positive X axis toward the positive Y axis. + * @param numquadrants the number of 90 degree arcs to rotate by + */ + public void setToQuadrantRotation(int numquadrants) { + switch (numquadrants & 3) { + case 0: + mxx = 1.0; + myx = 0.0; + mxy = 0.0; + myy = 1.0; + mxt = 0.0; + myt = 0.0; + state = APPLY_IDENTITY; + type = TYPE_IDENTITY; + break; + case 1: + mxx = 0.0; + myx = 1.0; + mxy = -1.0; + myy = 0.0; + mxt = 0.0; + myt = 0.0; + state = APPLY_SHEAR; + type = TYPE_QUADRANT_ROTATION; + break; + case 2: + mxx = -1.0; + myx = 0.0; + mxy = 0.0; + myy = -1.0; + mxt = 0.0; + myt = 0.0; + state = APPLY_SCALE; + type = TYPE_QUADRANT_ROTATION; + break; + case 3: + mxx = 0.0; + myx = -1.0; + mxy = 1.0; + myy = 0.0; + mxt = 0.0; + myt = 0.0; + state = APPLY_SHEAR; + type = TYPE_QUADRANT_ROTATION; + break; + } + } + + /** + * Sets this transform to a translated rotation transformation + * that rotates coordinates by the specified number of quadrants + * around the specified anchor point. + * This operation is equivalent to calling: + *
+     *     setToRotation(numquadrants * Math.PI / 2.0, anchorx, anchory);
+     * 
+ * Rotating by a positive number of quadrants rotates points on + * the positive X axis toward the positive Y axis. + * + * @param numquadrants the number of 90 degree arcs to rotate by + * @param anchorx the X coordinate of the rotation anchor point + * @param anchory the Y coordinate of the rotation anchor point + */ + public void setToQuadrantRotation(int numquadrants, + double anchorx, double anchory) + { + switch (numquadrants & 3) { + case 0: + mxx = 1.0; + myx = 0.0; + mxy = 0.0; + myy = 1.0; + mxt = 0.0; + myt = 0.0; + state = APPLY_IDENTITY; + type = TYPE_IDENTITY; + break; + case 1: + mxx = 0.0; + myx = 1.0; + mxy = -1.0; + myy = 0.0; + mxt = anchorx + anchory; + myt = anchory - anchorx; + if (mxt == 0.0 && myt == 0.0) { + state = APPLY_SHEAR; + type = TYPE_QUADRANT_ROTATION; + } else { + state = APPLY_SHEAR | APPLY_TRANSLATE; + type = TYPE_QUADRANT_ROTATION | TYPE_TRANSLATION; + } + break; + case 2: + mxx = -1.0; + myx = 0.0; + mxy = 0.0; + myy = -1.0; + mxt = anchorx + anchorx; + myt = anchory + anchory; + if (mxt == 0.0 && myt == 0.0) { + state = APPLY_SCALE; + type = TYPE_QUADRANT_ROTATION; + } else { + state = APPLY_SCALE | APPLY_TRANSLATE; + type = TYPE_QUADRANT_ROTATION | TYPE_TRANSLATION; + } + break; + case 3: + mxx = 0.0; + myx = -1.0; + mxy = 1.0; + myy = 0.0; + mxt = anchorx - anchory; + myt = anchory + anchorx; + if (mxt == 0.0 && myt == 0.0) { + state = APPLY_SHEAR; + type = TYPE_QUADRANT_ROTATION; + } else { + state = APPLY_SHEAR | APPLY_TRANSLATE; + type = TYPE_QUADRANT_ROTATION | TYPE_TRANSLATION; + } + break; + } + } + + /** + * Sets this transform to a scaling transformation. + * The matrix representing this transform becomes: + *
+     *      [   sx   0    0   ]
+     *      [   0    sy   0   ]
+     *      [   0    0    1   ]
+     * 
+ * @param sx the factor by which coordinates are scaled along the + * X axis direction + * @param sy the factor by which coordinates are scaled along the + * Y axis direction + */ + public void setToScale(double sx, double sy) { + mxx = sx; + myx = 0.0; + mxy = 0.0; + myy = sy; + mxt = 0.0; + myt = 0.0; + if (sx != 1.0 || sy != 1.0) { + state = APPLY_SCALE; + type = TYPE_UNKNOWN; + } else { + state = APPLY_IDENTITY; + type = TYPE_IDENTITY; + } + } + + /** + * Sets this transform to a copy of the transform in the specified + * BaseTransform object. + * @param Tx the BaseTransform object from which to + * copy the transform + */ + public void setTransform(BaseTransform Tx) { + switch (Tx.getDegree()) { + case IDENTITY: + setToIdentity(); + break; + case TRANSLATE_2D: + setToTranslation(Tx.getMxt(), Tx.getMyt()); + break; + default: + if (Tx.getType() > TYPE_AFFINE2D_MASK) { + System.out.println(Tx+" is "+Tx.getType()); + System.out.print(" "+Tx.getMxx()); + System.out.print(", "+Tx.getMxy()); + System.out.print(", "+Tx.getMxz()); + System.out.print(", "+Tx.getMxt()); + System.out.println(); + System.out.print(" "+Tx.getMyx()); + System.out.print(", "+Tx.getMyy()); + System.out.print(", "+Tx.getMyz()); + System.out.print(", "+Tx.getMyt()); + System.out.println(); + System.out.print(" "+Tx.getMzx()); + System.out.print(", "+Tx.getMzy()); + System.out.print(", "+Tx.getMzz()); + System.out.print(", "+Tx.getMzt()); + System.out.println(); + // TODO: Should this be thrown before we modify anything? + // (RT-26801) + degreeError(Degree.AFFINE_2D); + } + /* No Break */ + case AFFINE_2D: + this.mxx = Tx.getMxx(); + this.myx = Tx.getMyx(); + this.mxy = Tx.getMxy(); + this.myy = Tx.getMyy(); + this.mxt = Tx.getMxt(); + this.myt = Tx.getMyt(); + if (Tx instanceof AffineBase) { + this.state = ((AffineBase) Tx).state; + this.type = ((AffineBase) Tx).type; + } else { + updateState2D(); + } + break; + } + } + + /** + * Concatenates a BaseTransform Tx to + * this Affine2D Cx + * in a less commonly used way such that Tx modifies the + * coordinate transformation relative to the absolute pixel + * space rather than relative to the existing user space. + * Cx is updated to perform the combined transformation. + * Transforming a point p by the updated transform Cx' is + * equivalent to first transforming p by the original transform + * Cx and then transforming the result by + * Tx like this: + * Cx'(p) = Tx(Cx(p)) + * In matrix notation, if this transform Cx + * is represented by the matrix [this] and Tx is + * represented by the matrix [Tx] then this method does the + * following: + *
+     *      [this] = [Tx] x [this]
+     * 
+ * @param Tx the BaseTransform object to be + * concatenated with this Affine2D object. + * @see #concatenate + */ + public void preConcatenate(BaseTransform Tx) { + switch (Tx.getDegree()) { + case IDENTITY: + return; + case TRANSLATE_2D: + translate(Tx.getMxt(), Tx.getMyt()); + return; + case AFFINE_2D: + break; + default: + degreeError(Degree.AFFINE_2D); + } + double M0, M1; + double Txx, Txy, Tyx, Tyy; + double Txt, Tyt; + int mystate = state; + Affine2D at = (Affine2D) Tx; + int txstate = at.state; + switch ((txstate << HI_SHIFT) | mystate) { + case (HI_IDENTITY | APPLY_IDENTITY): + case (HI_IDENTITY | APPLY_TRANSLATE): + case (HI_IDENTITY | APPLY_SCALE): + case (HI_IDENTITY | APPLY_SCALE | APPLY_TRANSLATE): + case (HI_IDENTITY | APPLY_SHEAR): + case (HI_IDENTITY | APPLY_SHEAR | APPLY_TRANSLATE): + case (HI_IDENTITY | APPLY_SHEAR | APPLY_SCALE): + case (HI_IDENTITY | APPLY_SHEAR | APPLY_SCALE | APPLY_TRANSLATE): + // Tx is IDENTITY... + return; + + case (HI_TRANSLATE | APPLY_IDENTITY): + case (HI_TRANSLATE | APPLY_SCALE): + case (HI_TRANSLATE | APPLY_SHEAR): + case (HI_TRANSLATE | APPLY_SHEAR | APPLY_SCALE): + // Tx is TRANSLATE, this has no TRANSLATE + mxt = at.mxt; + myt = at.myt; + state = mystate | APPLY_TRANSLATE; + type |= TYPE_TRANSLATION; + return; + + case (HI_TRANSLATE | APPLY_TRANSLATE): + case (HI_TRANSLATE | APPLY_SCALE | APPLY_TRANSLATE): + case (HI_TRANSLATE | APPLY_SHEAR | APPLY_TRANSLATE): + case (HI_TRANSLATE | APPLY_SHEAR | APPLY_SCALE | APPLY_TRANSLATE): + // Tx is TRANSLATE, this has one too + mxt = mxt + at.mxt; + myt = myt + at.myt; + return; + + case (HI_SCALE | APPLY_TRANSLATE): + case (HI_SCALE | APPLY_IDENTITY): + // Only these two existing states need a new state + state = mystate | APPLY_SCALE; + /* NOBREAK */ + case (HI_SCALE | APPLY_SHEAR | APPLY_SCALE | APPLY_TRANSLATE): + case (HI_SCALE | APPLY_SHEAR | APPLY_SCALE): + case (HI_SCALE | APPLY_SHEAR | APPLY_TRANSLATE): + case (HI_SCALE | APPLY_SHEAR): + case (HI_SCALE | APPLY_SCALE | APPLY_TRANSLATE): + case (HI_SCALE | APPLY_SCALE): + // Tx is SCALE, this is anything + Txx = at.mxx; + Tyy = at.myy; + if ((mystate & APPLY_SHEAR) != 0) { + mxy = mxy * Txx; + myx = myx * Tyy; + if ((mystate & APPLY_SCALE) != 0) { + mxx = mxx * Txx; + myy = myy * Tyy; + } + } else { + mxx = mxx * Txx; + myy = myy * Tyy; + } + if ((mystate & APPLY_TRANSLATE) != 0) { + mxt = mxt * Txx; + myt = myt * Tyy; + } + type = TYPE_UNKNOWN; + return; + case (HI_SHEAR | APPLY_SHEAR | APPLY_TRANSLATE): + case (HI_SHEAR | APPLY_SHEAR): + mystate = mystate | APPLY_SCALE; + /* NOBREAK */ + case (HI_SHEAR | APPLY_TRANSLATE): + case (HI_SHEAR | APPLY_IDENTITY): + case (HI_SHEAR | APPLY_SCALE | APPLY_TRANSLATE): + case (HI_SHEAR | APPLY_SCALE): + state = mystate ^ APPLY_SHEAR; + /* NOBREAK */ + case (HI_SHEAR | APPLY_SHEAR | APPLY_SCALE | APPLY_TRANSLATE): + case (HI_SHEAR | APPLY_SHEAR | APPLY_SCALE): + // Tx is SHEAR, this is anything + Txy = at.mxy; + Tyx = at.myx; + + M0 = mxx; + mxx = myx * Txy; + myx = M0 * Tyx; + + M0 = mxy; + mxy = myy * Txy; + myy = M0 * Tyx; + + M0 = mxt; + mxt = myt * Txy; + myt = M0 * Tyx; + type = TYPE_UNKNOWN; + return; + } + // If Tx has more than one attribute, it is not worth optimizing + // all of those cases... + Txx = at.mxx; Txy = at.mxy; Txt = at.mxt; + Tyx = at.myx; Tyy = at.myy; Tyt = at.myt; + switch (mystate) { + default: + stateError(); + /* NOTREACHED */ + case (APPLY_SHEAR | APPLY_SCALE | APPLY_TRANSLATE): + M0 = mxt; + M1 = myt; + Txt += M0 * Txx + M1 * Txy; + Tyt += M0 * Tyx + M1 * Tyy; + + /* NOBREAK */ + case (APPLY_SHEAR | APPLY_SCALE): + mxt = Txt; + myt = Tyt; + + M0 = mxx; + M1 = myx; + mxx = M0 * Txx + M1 * Txy; + myx = M0 * Tyx + M1 * Tyy; + + M0 = mxy; + M1 = myy; + mxy = M0 * Txx + M1 * Txy; + myy = M0 * Tyx + M1 * Tyy; + break; + + case (APPLY_SHEAR | APPLY_TRANSLATE): + M0 = mxt; + M1 = myt; + Txt += M0 * Txx + M1 * Txy; + Tyt += M0 * Tyx + M1 * Tyy; + + /* NOBREAK */ + case (APPLY_SHEAR): + mxt = Txt; + myt = Tyt; + + M0 = myx; + mxx = M0 * Txy; + myx = M0 * Tyy; + + M0 = mxy; + mxy = M0 * Txx; + myy = M0 * Tyx; + break; + + case (APPLY_SCALE | APPLY_TRANSLATE): + M0 = mxt; + M1 = myt; + Txt += M0 * Txx + M1 * Txy; + Tyt += M0 * Tyx + M1 * Tyy; + + /* NOBREAK */ + case (APPLY_SCALE): + mxt = Txt; + myt = Tyt; + + M0 = mxx; + mxx = M0 * Txx; + myx = M0 * Tyx; + + M0 = myy; + mxy = M0 * Txy; + myy = M0 * Tyy; + break; + + case (APPLY_TRANSLATE): + M0 = mxt; + M1 = myt; + Txt += M0 * Txx + M1 * Txy; + Tyt += M0 * Tyx + M1 * Tyy; + + /* NOBREAK */ + case (APPLY_IDENTITY): + mxt = Txt; + myt = Tyt; + + mxx = Txx; + myx = Tyx; + + mxy = Txy; + myy = Tyy; + + state = mystate | txstate; + type = TYPE_UNKNOWN; + return; + } + updateState2D(); + } + + /** + * Returns an Affine2D object representing the + * inverse transformation. + * The inverse transform Tx' of this transform Tx + * maps coordinates transformed by Tx back + * to their original coordinates. + * In other words, Tx'(Tx(p)) = p = Tx(Tx'(p)). + *

+ * If this transform maps all coordinates onto a point or a line + * then it will not have an inverse, since coordinates that do + * not lie on the destination point or line will not have an inverse + * mapping. + * The getDeterminant method can be used to determine if this + * transform has no inverse, in which case an exception will be + * thrown if the createInverse method is called. + * @return a new Affine2D object representing the + * inverse transformation. + * @see #getDeterminant + * @exception NoninvertibleTransformException + * if the matrix cannot be inverted. + */ + public Affine2D createInverse() + throws NoninvertibleTransformException + { + double det; + switch (state) { + default: + stateError(); + /* NOTREACHED */ + case (APPLY_SHEAR | APPLY_SCALE | APPLY_TRANSLATE): + det = mxx * myy - mxy * myx; + if (det == 0 || Math.abs(det) <= Double.MIN_VALUE) { + throw new NoninvertibleTransformException("Determinant is "+ + det); + } + return new Affine2D( myy / det, -myx / det, + -mxy / det, mxx / det, + (mxy * myt - myy * mxt) / det, + (myx * mxt - mxx * myt) / det, + (APPLY_SHEAR | + APPLY_SCALE | + APPLY_TRANSLATE)); + case (APPLY_SHEAR | APPLY_SCALE): + det = mxx * myy - mxy * myx; + if (det == 0 || Math.abs(det) <= Double.MIN_VALUE) { + throw new NoninvertibleTransformException("Determinant is "+ + det); + } + return new Affine2D( myy / det, -myx / det, + -mxy / det, mxx / det, + 0.0, 0.0, + (APPLY_SHEAR | APPLY_SCALE)); + case (APPLY_SHEAR | APPLY_TRANSLATE): + if (mxy == 0.0 || myx == 0.0) { + throw new NoninvertibleTransformException("Determinant is 0"); + } + return new Affine2D( 0.0, 1.0 / mxy, + 1.0 / myx, 0.0, + -myt / myx, -mxt / mxy, + (APPLY_SHEAR | APPLY_TRANSLATE)); + case (APPLY_SHEAR): + if (mxy == 0.0 || myx == 0.0) { + throw new NoninvertibleTransformException("Determinant is 0"); + } + return new Affine2D(0.0, 1.0 / mxy, + 1.0 / myx, 0.0, + 0.0, 0.0, + (APPLY_SHEAR)); + case (APPLY_SCALE | APPLY_TRANSLATE): + if (mxx == 0.0 || myy == 0.0) { + throw new NoninvertibleTransformException("Determinant is 0"); + } + return new Affine2D( 1.0 / mxx, 0.0, + 0.0, 1.0 / myy, + -mxt / mxx, -myt / myy, + (APPLY_SCALE | APPLY_TRANSLATE)); + case (APPLY_SCALE): + if (mxx == 0.0 || myy == 0.0) { + throw new NoninvertibleTransformException("Determinant is 0"); + } + return new Affine2D(1.0 / mxx, 0.0, + 0.0, 1.0 / myy, + 0.0, 0.0, + (APPLY_SCALE)); + case (APPLY_TRANSLATE): + return new Affine2D( 1.0, 0.0, + 0.0, 1.0, + -mxt, -myt, + (APPLY_TRANSLATE)); + case (APPLY_IDENTITY): + return new Affine2D(); + } + + /* NOTREACHED */ + } + + /** + * Transforms an array of point objects by this transform. + * If any element of the ptDst array is + * null, a new Point2D object is allocated + * and stored into that element before storing the results of the + * transformation. + *

+ * Note that this method does not take any precautions to + * avoid problems caused by storing results into Point2D + * objects that will be used as the source for calculations + * further down the source array. + * This method does guarantee that if a specified Point2D + * object is both the source and destination for the same single point + * transform operation then the results will not be stored until + * the calculations are complete to avoid storing the results on + * top of the operands. + * If, however, the destination Point2D object for one + * operation is the same object as the source Point2D + * object for another operation further down the source array then + * the original coordinates in that point are overwritten before + * they can be converted. + * @param ptSrc the array containing the source point objects + * @param ptDst the array into which the transform point objects are + * returned + * @param srcOff the offset to the first point object to be + * transformed in the source array + * @param dstOff the offset to the location of the first + * transformed point object that is stored in the destination array + * @param numPts the number of point objects to be transformed + */ + public void transform(Point2D[] ptSrc, int srcOff, + Point2D[] ptDst, int dstOff, + int numPts) { + int mystate = this.state; + while (--numPts >= 0) { + // Copy source coords into local variables in case src == dst + Point2D src = ptSrc[srcOff++]; + double x = src.x; + double y = src.y; + Point2D dst = ptDst[dstOff++]; + if (dst == null) { + dst = new Point2D(); + ptDst[dstOff - 1] = dst; + } + switch (mystate) { + default: + stateError(); + /* NOTREACHED */ + case (APPLY_SHEAR | APPLY_SCALE | APPLY_TRANSLATE): + dst.setLocation((float)(x * mxx + y * mxy + mxt), + (float)(x * myx + y * myy + myt)); + break; + case (APPLY_SHEAR | APPLY_SCALE): + dst.setLocation((float)(x * mxx + y * mxy), + (float)(x * myx + y * myy)); + break; + case (APPLY_SHEAR | APPLY_TRANSLATE): + dst.setLocation((float)(y * mxy + mxt), + (float)(x * myx + myt)); + break; + case (APPLY_SHEAR): + dst.setLocation((float)(y * mxy), (float)(x * myx)); + break; + case (APPLY_SCALE | APPLY_TRANSLATE): + dst.setLocation((float)(x * mxx + mxt), (float)(y * myy + myt)); + break; + case (APPLY_SCALE): + dst.setLocation((float)(x * mxx), (float)(y * myy)); + break; + case (APPLY_TRANSLATE): + dst.setLocation((float)(x + mxt), (float)(y + myt)); + break; + case (APPLY_IDENTITY): + dst.setLocation((float) x, (float) y); + break; + } + } + + /* NOTREACHED */ + } + + /** + * Transforms the relative distance vector specified by + * ptSrc and stores the result in ptDst. + * A relative distance vector is transformed without applying the + * translation components of the affine transformation matrix + * using the following equations: + *

+     *  [  x' ]   [  m00  m01 (m02) ] [  x  ]   [ m00x + m01y ]
+     *  [  y' ] = [  m10  m11 (m12) ] [  y  ] = [ m10x + m11y ]
+     *  [ (1) ]   [  (0)  (0) ( 1 ) ] [ (1) ]   [     (1)     ]
+     * 
+ * If ptDst is null, a new + * Point2D object is allocated and then the result of the + * transform is stored in this object. + * In either case, ptDst, which contains the + * transformed point, is returned for convenience. + * If ptSrc and ptDst are the same object, + * the input point is correctly overwritten with the transformed + * point. + * @param ptSrc the distance vector to be delta transformed + * @param ptDst the resulting transformed distance vector + * @return ptDst, which contains the result of the + * transformation. + */ + public Point2D deltaTransform(Point2D ptSrc, Point2D ptDst) { + if (ptDst == null) { + ptDst = new Point2D(); + } + // Copy source coords into local variables in case src == dst + double x = ptSrc.x; + double y = ptSrc.y; + switch (state) { + default: + stateError(); + /* NOTREACHED */ + case (APPLY_SHEAR | APPLY_SCALE | APPLY_TRANSLATE): + case (APPLY_SHEAR | APPLY_SCALE): + ptDst.setLocation((float)(x * mxx + y * mxy), (float)(x * myx + y * myy)); + return ptDst; + case (APPLY_SHEAR | APPLY_TRANSLATE): + case (APPLY_SHEAR): + ptDst.setLocation((float)(y * mxy), (float)(x * myx)); + return ptDst; + case (APPLY_SCALE | APPLY_TRANSLATE): + case (APPLY_SCALE): + ptDst.setLocation((float)(x * mxx), (float)(y * myy)); + return ptDst; + case (APPLY_TRANSLATE): + case (APPLY_IDENTITY): + ptDst.setLocation((float) x, (float) y); + return ptDst; + } + + /* NOTREACHED */ + } + + // Round values to sane precision for printing + // Note that Math.sin(Math.PI) has an error of about 10^-16 + private static double _matround(double matval) { + return Math.rint(matval * 1E15) / 1E15; + } + + /** + * Returns a String that represents the value of this + * {@link Object}. + * @return a String representing the value of this + * Object. + */ + @Override + public String toString() { + return ("Affine2D[[" + + _matround(mxx) + ", " + + _matround(mxy) + ", " + + _matround(mxt) + "], [" + + _matround(myx) + ", " + + _matround(myy) + ", " + + _matround(myt) + "]]"); + } + + @Override + public boolean is2D() { + return true; + } + + @Override + public void restoreTransform(double mxx, double myx, + double mxy, double myy, + double mxt, double myt) + { + setTransform(mxx, myx, mxy, myy, mxt, myt); + } + + @Override + public void restoreTransform(double mxx, double mxy, double mxz, double mxt, + double myx, double myy, double myz, double myt, + double mzx, double mzy, double mzz, double mzt) + { + if ( mxz != 0 || + myz != 0 || + mzx != 0 || mzy != 0 || mzz != 1 || mzt != 0.0) + { + degreeError(Degree.AFFINE_2D); + } + setTransform(mxx, myx, mxy, myy, mxt, myt); + } + + @Override + public BaseTransform deriveWithTranslation(double mxt, double myt) { + translate(mxt, myt); + return this; + } + + @Override + public BaseTransform deriveWithTranslation(double mxt, double myt, double mzt) { + if (mzt == 0.0) { + translate(mxt, myt); + return this; + } + Affine3D a = new Affine3D(this); + a.translate(mxt, myt, mzt); + return a; + } + + @Override + public BaseTransform deriveWithScale(double mxx, double myy, double mzz) { + if (mzz == 1.0) { + scale(mxx, myy); + return this; + } + Affine3D a = new Affine3D(this); + a.scale(mxx, myy, mzz); + return a; + + } + + @Override + public BaseTransform deriveWithRotation(double theta, + double axisX, double axisY, double axisZ) { + if (theta == 0.0) { + return this; + } + if (almostZero(axisX) && almostZero(axisY)) { + if (axisZ > 0) { + rotate(theta); + } else if (axisZ < 0) { + rotate(-theta); + } // else rotating about zero vector - NOP + return this; + } + Affine3D a = new Affine3D(this); + a.rotate(theta, axisX, axisY, axisZ); + return a; + } + + @Override + public BaseTransform deriveWithPreTranslation(double mxt, double myt) { + this.mxt += mxt; + this.myt += myt; + if (this.mxt != 0.0 || this.myt != 0.0) { + state |= APPLY_TRANSLATE; +// if (type != TYPE_UNKNOWN) { + type |= TYPE_TRANSLATION; +// } + } else { + state &= ~APPLY_TRANSLATE; + if (type != TYPE_UNKNOWN) { + type &= ~TYPE_TRANSLATION; + } + } + return this; + } + + @Override + public BaseTransform deriveWithConcatenation(double mxx, double myx, + double mxy, double myy, + double mxt, double myt) + { + // TODO: Simplify this (RT-26801) + BaseTransform tmpTx = getInstance(mxx, myx, + mxy, myy, + mxt, myt); + concatenate(tmpTx); + return this; + } + + @Override + public BaseTransform deriveWithConcatenation( + double mxx, double mxy, double mxz, double mxt, + double myx, double myy, double myz, double myt, + double mzx, double mzy, double mzz, double mzt) { + if ( mxz == 0.0 + && myz == 0.0 + && mzx == 0.0 && mzy == 0.0 && mzz == 1.0 && mzt == 0.0) { + concatenate(mxx, mxy, + mxt, myx, + myy, myt); + return this; + } + + Affine3D t3d = new Affine3D(this); + t3d.concatenate(mxx, mxy, mxz, mxt, + myx, myy, myz, myt, + mzx, mzy, mzz, mzt); + return t3d; + } + + @Override + public BaseTransform deriveWithConcatenation(BaseTransform tx) { + if (tx.is2D()) { + concatenate(tx); + return this; + } + Affine3D t3d = new Affine3D(this); + t3d.concatenate(tx); + return t3d; + } + + @Override + public BaseTransform deriveWithPreConcatenation(BaseTransform tx) { + if (tx.is2D()) { + preConcatenate(tx); + return this; + } + Affine3D t3d = new Affine3D(this); + t3d.preConcatenate(tx); + return t3d; + } + + @Override + public BaseTransform deriveWithNewTransform(BaseTransform tx) { + if (tx.is2D()) { + setTransform(tx); + return this; + } + return getInstance(tx); + } + + @Override + public BaseTransform copy() { + return new Affine2D(this); + } + + private static final long BASE_HASH; + static { + long bits = 0; + bits = bits * 31 + Double.doubleToLongBits(IDENTITY_TRANSFORM.getMzz()); + bits = bits * 31 + Double.doubleToLongBits(IDENTITY_TRANSFORM.getMzy()); + bits = bits * 31 + Double.doubleToLongBits(IDENTITY_TRANSFORM.getMzx()); + bits = bits * 31 + Double.doubleToLongBits(IDENTITY_TRANSFORM.getMyz()); + bits = bits * 31 + Double.doubleToLongBits(IDENTITY_TRANSFORM.getMxz()); + BASE_HASH = bits; + } + + /** + * Returns the hashcode for this transform. The base algorithm for + * computing the hashcode is defined by the implementation in + * the {@code BaseTransform} class. This implementation is just a + * faster way of computing the same value knowing which elements of + * the transform matrix are populated. + * @return a hash code for this transform. + */ + @Override + public int hashCode() { + if (isIdentity()) return 0; + long bits = BASE_HASH; + bits = bits * 31 + Double.doubleToLongBits(getMyy()); + bits = bits * 31 + Double.doubleToLongBits(getMyx()); + bits = bits * 31 + Double.doubleToLongBits(getMxy()); + bits = bits * 31 + Double.doubleToLongBits(getMxx()); + bits = bits * 31 + Double.doubleToLongBits(0.0); // mzt + bits = bits * 31 + Double.doubleToLongBits(getMyt()); + bits = bits * 31 + Double.doubleToLongBits(getMxt()); + return (((int) bits) ^ ((int) (bits >> 32))); + } + + /** + * Returns true if this Affine2D + * represents the same coordinate transform as the specified + * argument. + * @param obj the Object to test for equality with this + * Affine2D + * @return true if obj equals this + * Affine2D object; false otherwise. + */ + @Override + public boolean equals(Object obj) { + if (obj instanceof BaseTransform) { + BaseTransform a = (BaseTransform) obj; + return (a.getType() <= TYPE_AFFINE2D_MASK && + a.getMxx() == this.mxx && + a.getMxy() == this.mxy && + a.getMxt() == this.mxt && + a.getMyx() == this.myx && + a.getMyy() == this.myy && + a.getMyt() == this.myt); + } + return false; + } +}