From b18e918e13bd2916e524213aac89d955219b7869 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Galland?= Date: Fri, 20 May 2016 15:01:14 +0200 Subject: [PATCH] [math] Add "arcTo" to Path2D. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit see #38 Signed-off-by: Stéphane Galland --- .../arakhne/afc/math/geometry/d2/Path2D.java | 115 ++++++- .../afc/math/geometry/d2/afp/Path2afp.java | 314 +++++++++++++++++- .../afc/math/geometry/d2/ai/Path2ai.java | 308 ++++++++++++++++- .../afc/math/geometry/d2/d/Path2d.java | 12 +- .../afc/math/geometry/d2/dfx/Path2dfx.java | 21 ++ .../afc/math/geometry/d2/i/Path2i.java | 15 +- .../afc/math/geometry/d2/ifx/Path2ifx.java | 21 ++ .../math/geometry/d2/doc-files/arcto0.png | Bin 0 -> 26592 bytes .../math/geometry/d2/doc-files/arcto1.png | Bin 0 -> 15182 bytes .../geometry/d2/afp/AbstractPath2afpTest.java | 144 +++++++- .../d2/afp/AbstractShape2afpTest.java | 67 +++- .../geometry/d2/ai/AbstractPath2aiTest.java | 152 ++++++++- .../geometry/d2/ai/AbstractShape2aiTest.java | 65 ++++ 13 files changed, 1211 insertions(+), 23 deletions(-) create mode 100644 core/math/src/main/javadoc/org/arakhne/math/geometry/d2/doc-files/arcto0.png create mode 100644 core/math/src/main/javadoc/org/arakhne/math/geometry/d2/doc-files/arcto1.png diff --git a/core/math/src/main/java/org/arakhne/afc/math/geometry/d2/Path2D.java b/core/math/src/main/java/org/arakhne/afc/math/geometry/d2/Path2D.java index d2b1e02da..a56cae3a7 100644 --- a/core/math/src/main/java/org/arakhne/afc/math/geometry/d2/Path2D.java +++ b/core/math/src/main/java/org/arakhne/afc/math/geometry/d2/Path2D.java @@ -170,7 +170,106 @@ public interface Path2D< */ void curveTo(Point2D ctrl1, Point2D ctrl2, Point2D to); - /** + /** + * Adds a section of an shallow ellipse to the current path. + * The ellipse from which a quadrant is taken is the ellipse that would be + * inscribed in a parallelogram defined by 3 points, + * The current point which is considered to be the midpoint of the edge + * leading into the corner of the ellipse where the ellipse grazes it, + * {@code (ctrlx, ctrly)} which is considered to be the location of the + * corner of the parallelogram in which the ellipse is inscribed, + * and {@code (tox, toy)} which is considered to be the midpoint of the + * edge leading away from the corner of the oval where the oval grazes it. + * + *

+ * + *

Only the portion of the ellipse from {@code tfrom} to {@code tto} + * will be included where {@code 0f} represents the point where the + * ellipse grazes the leading edge, {@code 1f} represents the point where + * the oval grazes the trailing edge, and {@code 0.5f} represents the + * point on the oval closest to the control point. + * The two values must satisfy the relation + * {@code (0 <= tfrom <= tto <= 1)}. + * + *

If {@code tfrom} is not {@code 0f} then the caller would most likely + * want to use one of the arc {@code type} values that inserts a segment + * leading to the initial point. + * An initial {@link #moveTo(double, double)} or {@link #lineTo(double, double)} can be added to direct + * the path to the starting point of the ellipse section if + * {@link ArcType#MOVE_THEN_ARC} or + * {@link ArcType#LINE_THEN_ARC} are + * specified by the type argument. + * The {@code lineTo} path segment will only be added if the current point + * is not already at the indicated location to avoid spurious empty line + * segments. + * The type can be specified as + * {@link ArcType.CORNER_ONLY} if the current point + * on the path is known to be at the starting point of the ellipse section. + * + * @param ctrl the control point, i.e. the corner of the parallelogram in which the ellipse is inscribed. + * @param to the target point. + * @param tfrom the fraction of the ellipse section where the curve should start. + * @param tto the fraction of the ellipse section where the curve should end + * @param type the specification of what additional path segments should + * be appended to lead the current path to the starting point. + */ + void arcTo(Point2D ctrl, Point2D to, double tfrom, double tto, ArcType type); + + /** + * Adds a section of an shallow ellipse to the current path. + * + *

This function is equivalent to:


+     * this.arcTo(ctrl, to, 0.0, 1.0, ArcType.ARCONLY);
+     * 
+ * + * @param ctrl the control point, i.e. the corner of the parallelogram in which the ellipse is inscribed. + * @param to the target point. + */ + default void arcTo(Point2D ctrl, Point2D to) { + arcTo(ctrl, to, 0., 1., ArcType.ARC_ONLY); + } + + /** + * Adds a section of an shallow ellipse to the current path. + * The ellipse from which the portions are extracted follows the rules: + * + * + *

+ * + *

The method will do nothing if the destination point is the same as + * the current point. + * The method will draw a simple line segment to the destination point + * if either of the two radii are zero. + * + * @param to the target point. + * @param radii the X and Y radii of the tilted ellipse. + * @param xAxisRotation the angle of tilt of the ellipse. + * @param largeArcFlag true iff the path will sweep the long way around the ellipse. + * @param sweepFlag true iff the path will sweep clockwise around the ellipse. + * @see "http://www.w3.org/TR/SVG/paths.html#PathDataEllipticalArcCommands" + */ + void arcTo(Point2D to, Vector2D radii, double xAxisRotation, boolean largeArcFlag, boolean sweepFlag); + + /** * Closes the current subpath by drawing a straight line back to * the coordinates of the last {@code moveTo}. If the path is already * closed or if the previous coordinates are for a {@code moveTo} @@ -351,4 +450,18 @@ default double getLength() { @Pure boolean containsControlPoint(Point2D point); + /** Type of drawing to used when drawing an arc. + * + * @author $Author: sgalland$ + * @version $FullVersion$ + * @mavengroupid $GroupId$ + * @mavenartifactid $ArtifactId$ + * @since 13.0 + */ + enum ArcType { + ARC_ONLY, + MOVE_THEN_ARC, + LINE_THEN_ARC + } + } \ No newline at end of file diff --git a/core/math/src/main/java/org/arakhne/afc/math/geometry/d2/afp/Path2afp.java b/core/math/src/main/java/org/arakhne/afc/math/geometry/d2/afp/Path2afp.java index 67f6f8e73..1769b6116 100644 --- a/core/math/src/main/java/org/arakhne/afc/math/geometry/d2/afp/Path2afp.java +++ b/core/math/src/main/java/org/arakhne/afc/math/geometry/d2/afp/Path2afp.java @@ -34,6 +34,7 @@ import org.arakhne.afc.math.geometry.d2.Point2D; import org.arakhne.afc.math.geometry.d2.Transform2D; import org.arakhne.afc.math.geometry.d2.Vector2D; +import org.arakhne.afc.math.geometry.d2.afp.Circle2afp.AbstractCirclePathIterator; import org.eclipse.xtext.xbase.lib.Pure; /** Fonctional interface that represented a 2D path on a plane. @@ -1872,12 +1873,297 @@ void curveTo(double x1, double y1, default void curveTo(Point2D ctrl1, Point2D ctrl2, Point2D to) { assert (ctrl1 != null) : "First control point must be not null"; //$NON-NLS-1$ assert (ctrl2 != null) : "Second control point must be not null"; //$NON-NLS-1$ - assert (to != null) : "Taarget point must be not null"; //$NON-NLS-1$ + assert (to != null) : "Target point must be not null"; //$NON-NLS-1$ curveTo(ctrl1.getX(), ctrl1.getY(), ctrl2.getX(), ctrl2.getY(), to.getX(), to.getY()); - } - - @Pure + + /** + * Adds a section of an shallow ellipse to the current path. + * The ellipse from which a quadrant is taken is the ellipse that would be + * inscribed in a parallelogram defined by 3 points, + * The current point which is considered to be the midpoint of the edge + * leading into the corner of the ellipse where the ellipse grazes it, + * {@code (ctrlx, ctrly)} which is considered to be the location of the + * corner of the parallelogram in which the ellipse is inscribed, + * and {@code (tox, toy)} which is considered to be the midpoint of the + * edge leading away from the corner of the oval where the oval grazes it. + * + *

+ * + *

Only the portion of the ellipse from {@code tfrom} to {@code tto} + * will be included where {@code 0f} represents the point where the + * ellipse grazes the leading edge, {@code 1f} represents the point where + * the oval grazes the trailing edge, and {@code 0.5f} represents the + * point on the oval closest to the control point. + * The two values must satisfy the relation + * {@code (0 <= tfrom <= tto <= 1)}. + * + *

If {@code tfrom} is not {@code 0f} then the caller would most likely + * want to use one of the arc {@code type} values that inserts a segment + * leading to the initial point. + * An initial {@link #moveTo(double, double)} or {@link #lineTo(double, double)} can be added to direct + * the path to the starting point of the ellipse section if + * {@link ArcType#MOVE_THEN_ARC} or + * {@link ArcType#LINE_THEN_ARC} are + * specified by the type argument. + * The {@code lineTo} path segment will only be added if the current point + * is not already at the indicated location to avoid spurious empty line + * segments. + * The type can be specified as + * {@link ArcType.CORNER_ONLY} if the current point + * on the path is known to be at the starting point of the ellipse section. + * + * @param ctrlx the x coordinate of the control point, i.e. the corner of the parallelogram in which the ellipse is inscribed. + * @param ctrlx the y coordinate of the control point, i.e. the corner of the parallelogram in which the ellipse is inscribed. + * @param to the x coordinate of the target point. + * @param to the y coordinate of the target point. + * @param tfrom the fraction of the ellipse section where the curve should start. + * @param tto the fraction of the ellipse section where the curve should end + * @param type the specification of what additional path segments should + * be appended to lead the current path to the starting point. + */ + default void arcTo(double ctrlx, double ctrly, double tox, double toy, double tfrom, double tto, + ArcType type) { + // Copied from JavaFX Path2D + assert (tfrom >= 0.) : "tfrom must be positive or zero"; //$NON-NLS-1$ + assert (tto >= tfrom) : "tto must be greater than or equal to tfrom"; //$NON-NLS-1$ + assert (tto <= 1.) : "tto must be lower than 1"; //$NON-NLS-1$ + double currentx = getCurrentX(); + double currenty = getCurrentY(); + final double ocurrentx = currentx; + final double ocurrenty = currenty; + double cx0 = currentx + (ctrlx - currentx) * AbstractCirclePathIterator.CTRL_POINT_DISTANCE; + double cy0 = currenty + (ctrly - currenty) * AbstractCirclePathIterator.CTRL_POINT_DISTANCE; + double cx1 = tox + (ctrlx - tox) * AbstractCirclePathIterator.CTRL_POINT_DISTANCE; + double cy1 = toy + (ctrly - toy) * AbstractCirclePathIterator.CTRL_POINT_DISTANCE; + if (tto < 1.) { + double t = 1. - tto; + tox += (cx1 - tox) * t; + toy += (cy1 - toy) * t; + cx1 += (cx0 - cx1) * t; + cy1 += (cy0 - cy1) * t; + cx0 += (currentx - cx0) * t; + cy0 += (currenty - cy0) * t; + tox += (cx1 - tox) * t; + toy += (cy1 - toy) * t; + cx1 += (cx0 - cx1) * t; + cy1 += (cy0 - cy1) * t; + tox += (cx1 - tox) * t; + toy += (cy1 - toy) * t; + } + if (tfrom > 0.) { + if (tto < 1.) { + tfrom = tfrom / tto; + } + currentx += (cx0 - currentx) * tfrom; + currenty += (cy0 - currenty) * tfrom; + cx0 += (cx1 - cx0) * tfrom; + cy0 += (cy1 - cy0) * tfrom; + cx1 += (tox - cx1) * tfrom; + cy1 += (toy - cy1) * tfrom; + currentx += (cx0 - currentx) * tfrom; + currenty += (cy0 - currenty) * tfrom; + cx0 += (cx1 - cx0) * tfrom; + cy0 += (cy1 - cy0) * tfrom; + currentx += (cx0 - currentx) * tfrom; + currenty += (cy0 - currenty) * tfrom; + } + if (type == ArcType.MOVE_THEN_ARC) { + if (currentx != ocurrentx || currenty != ocurrenty) { + moveTo(currentx, currenty); + } + } else if (type == ArcType.LINE_THEN_ARC) { + if (currentx != ocurrentx || currenty != ocurrenty) { + lineTo(currentx, currenty); + } + } + if (tfrom == tto || + (currentx == cx0 && cx0 == cx1 && cx1 == tox && + currenty == cy0 && cy0 == cy1 && cy1 == toy)) { + if (type != ArcType.LINE_THEN_ARC) { + lineTo(tox, toy); + } + } else { + curveTo(cx0, cy0, cx1, cy1, tox, toy); + } + } + + @Override + default void arcTo(Point2D ctrl, Point2D to, double tfrom, double tto, + org.arakhne.afc.math.geometry.d2.Path2D.ArcType type) { + assert (ctrl != null) : "Control point must be not null"; //$NON-NLS-1$ + assert (to != null) : "Target point must be not null"; //$NON-NLS-1$ + arcTo(ctrl.getX(), ctrl.getY(), to.getX(), to.getY(), tfrom, tto, type); + } + + /** + * Adds a section of an shallow ellipse to the current path. + * + *

This function is equivalent to:


+     * this.arcTo(ctrl, to, 0.0, 1.0, ArcType.ARCONLY);
+     * 
+ * + * @param ctrlx the x coordinate of the control point, i.e. the corner of the parallelogram in which the ellipse is inscribed. + * @param ctrlx the y coordinate of the control point, i.e. the corner of the parallelogram in which the ellipse is inscribed. + * @param to the x coordinate of the target point. + * @param to the y coordinate of the target point. + */ + default void arcTo(double ctrlx, double ctrly, double tox, double toy) { + arcTo(ctrlx, ctrly, tox, toy, 0., 1., ArcType.ARC_ONLY); + } + + @Override + default void arcTo(Point2D to, Vector2D radii, double xAxisRotation, boolean largeArcFlag, boolean sweepFlag) { + assert (radii != null) : "Radii vector must be not null"; //$NON-NLS-1$ + assert (to != null) : "Target point must be not null"; //$NON-NLS-1$ + arcTo(to.getX(), to.getY(), radii.getX(), radii.getY(), xAxisRotation, largeArcFlag, sweepFlag); + } + + /** + * Adds a section of an shallow ellipse to the current path. + * The ellipse from which the portions are extracted follows the rules: + * + * + *

+ * + *

The method will do nothing if the destination point is the same as + * the current point. + * The method will draw a simple line segment to the destination point + * if either of the two radii are zero. + * + * @param tox the X coordinate of the target point. + * @param toy the Y coordinate of the target point. + * @param radiusx the X radius of the tilted ellipse. + * @param radiusy the Y radius of the tilted ellipse. + * @param xAxisRotation the angle of tilt of the ellipse. + * @param largeArcFlag true iff the path will sweep the long way around the ellipse. + * @param sweepFlag true iff the path will sweep clockwise around the ellipse. + * @see "http://www.w3.org/TR/SVG/paths.html#PathDataEllipticalArcCommands" + */ + default void arcTo(double tox, double toy, double radiusx, double radiusy, double xAxisRotation, boolean largeArcFlag, boolean sweepFlag) { + // Copied for JavaFX + assert (radiusx >= 0.) : "X radius must be positive or zero."; //$NON-NLS-1$ + assert (radiusy >= 0.) : "Y radius must be positive or zero."; //$NON-NLS-1$ + if (radiusx == 0. || radiusy == 0.) { + lineTo(tox, toy); + return; + } + double ocurrentx = getCurrentX(); + double ocurrenty = getCurrentY(); + double x1 = ocurrentx; + double y1 = ocurrenty; + double x2 = tox; + double y2 = toy; + if (x1 == x2 && y1 == y2) { + return; + } + double cosphi, sinphi; + if (xAxisRotation == 0.) { + cosphi = 1.; + sinphi = 0.; + } else { + cosphi = Math.cos(xAxisRotation); + sinphi = Math.sin(xAxisRotation); + } + double mx = (x1 + x2) / 2.; + double my = (y1 + y2) / 2.; + double relx1 = x1 - mx; + double rely1 = y1 - my; + double x1p = (cosphi * relx1 + sinphi * rely1) / radiusx; + double y1p = (cosphi * rely1 - sinphi * relx1) / radiusy; + double lenpsq = x1p * x1p + y1p * y1p; + if (lenpsq >= 1.) { + double xqpr = y1p * radiusx; + double yqpr = x1p * radiusy; + if (sweepFlag) { + xqpr = -xqpr; + } else { + yqpr = -yqpr; + } + double relxq = cosphi * xqpr - sinphi * yqpr; + double relyq = cosphi * yqpr + sinphi * xqpr; + double xq = mx + relxq; + double yq = my + relyq; + double xc = x1 + relxq; + double yc = y1 + relyq; + if (x1 != ocurrentx || y1 != ocurrenty) { + lineTo(x1, y1); + } + arcTo(xc, yc, xq, yq, 0, 1, ArcType.ARC_ONLY); + xc = x2 + relxq; + yc = y2 + relyq; + arcTo(xc, yc, x2, y2, 0, 1, ArcType.ARC_ONLY); + return; + } + double scalef = Math.sqrt((1. - lenpsq) / lenpsq); + double cxp = scalef * y1p; + double cyp = scalef * x1p; + if (largeArcFlag == sweepFlag) { + cxp = -cxp; + } else { + cyp = -cyp; + } + mx += (cosphi * cxp * radiusx - sinphi * cyp * radiusy); + my += (cosphi * cyp * radiusy + sinphi * cxp * radiusx); + double ux = x1p - cxp; + double uy = y1p - cyp; + double vx = -(x1p + cxp); + double vy = -(y1p + cyp); + boolean done = false; + double quadlen = 1.; + boolean wasclose = false; + do { + double xqp = uy; + double yqp = ux; + if (sweepFlag) { + xqp = -xqp; + } else { + yqp = -yqp; + } + if (xqp * vx + yqp * vy > 0.) { + double dot = ux * vx + uy * vy; + if (dot >= 0) { + quadlen = Math.acos(dot) / MathConstants.DEMI_PI; + done = true; + } + wasclose = true; + } else if (wasclose) { + break; + } + double relxq = (cosphi * xqp * radiusx - sinphi * yqp * radiusy); + double relyq = (cosphi * yqp * radiusy + sinphi * xqp * radiusx); + double xq = mx + relxq; + double yq = my + relyq; + double xc = x1 + relxq; + double yc = y1 + relyq; + arcTo(xc, yc, xq, yq, 0, quadlen, ArcType.ARC_ONLY); + x1 = xq; + y1 = yq; + ux = xqp; + uy = yqp; + } while (!done); + } + + @Pure @Override default double getDistanceSquared(Point2D p) { assert (p != null) : "Point must be not null"; //$NON-NLS-1$ @@ -2132,6 +2418,26 @@ default PathIterator2afp getPathIterator(double flatness) { default PathIterator2afp getPathIterator(Transform2D transform, double flatness) { return new FlatteningPathIterator<>(getPathIterator(transform), flatness, DEFAULT_FLATENING_LIMIT); } + + /** Replies the x coordinate of the last point in the path. + * + * @return the x coordinate of the last point in the path. + */ + @Pure + double getCurrentX(); + + /** Replies the y coordinate of the last point in the path. + * + * @return the y coordinate of the last point in the path. + */ + @Pure + double getCurrentY(); + + @Override + @Pure + default P getCurrentPoint() { + return getGeomFactory().newPoint(getCurrentX(), getCurrentY()); + } @Pure @Override diff --git a/core/math/src/main/java/org/arakhne/afc/math/geometry/d2/ai/Path2ai.java b/core/math/src/main/java/org/arakhne/afc/math/geometry/d2/ai/Path2ai.java index ed2507dcb..8dbad4d86 100644 --- a/core/math/src/main/java/org/arakhne/afc/math/geometry/d2/ai/Path2ai.java +++ b/core/math/src/main/java/org/arakhne/afc/math/geometry/d2/ai/Path2ai.java @@ -33,6 +33,7 @@ import org.arakhne.afc.math.geometry.d2.Point2D; import org.arakhne.afc.math.geometry.d2.Transform2D; import org.arakhne.afc.math.geometry.d2.Vector2D; +import org.arakhne.afc.math.geometry.d2.afp.Circle2afp.AbstractCirclePathIterator; import org.arakhne.afc.math.geometry.d2.afp.Segment2afp; import org.eclipse.xtext.xbase.lib.Pure; @@ -1356,7 +1357,292 @@ default void curveTo(Point2D ctrl1, Point2D ctrl2, Point2D to) curveTo(ctrl1.ix(), ctrl1.iy(), ctrl2.ix(), ctrl2.iy(), to.ix(), to.iy()); } - @Pure + /** + * Adds a section of an shallow ellipse to the current path. + * The ellipse from which a quadrant is taken is the ellipse that would be + * inscribed in a parallelogram defined by 3 points, + * The current point which is considered to be the midpoint of the edge + * leading into the corner of the ellipse where the ellipse grazes it, + * {@code (ctrlx, ctrly)} which is considered to be the location of the + * corner of the parallelogram in which the ellipse is inscribed, + * and {@code (tox, toy)} which is considered to be the midpoint of the + * edge leading away from the corner of the oval where the oval grazes it. + * + *

+ * + *

Only the portion of the ellipse from {@code tfrom} to {@code tto} + * will be included where {@code 0f} represents the point where the + * ellipse grazes the leading edge, {@code 1f} represents the point where + * the oval grazes the trailing edge, and {@code 0.5f} represents the + * point on the oval closest to the control point. + * The two values must satisfy the relation + * {@code (0 <= tfrom <= tto <= 1)}. + * + *

If {@code tfrom} is not {@code 0f} then the caller would most likely + * want to use one of the arc {@code type} values that inserts a segment + * leading to the initial point. + * An initial {@link #moveTo(double, double)} or {@link #lineTo(double, double)} can be added to direct + * the path to the starting point of the ellipse section if + * {@link ArcType#MOVE_THEN_ARC} or + * {@link ArcType#LINE_THEN_ARC} are + * specified by the type argument. + * The {@code lineTo} path segment will only be added if the current point + * is not already at the indicated location to avoid spurious empty line + * segments. + * The type can be specified as + * {@link ArcType.CORNER_ONLY} if the current point + * on the path is known to be at the starting point of the ellipse section. + * + * @param ctrlx the x coordinate of the control point, i.e. the corner of the parallelogram in which the ellipse is inscribed. + * @param ctrlx the y coordinate of the control point, i.e. the corner of the parallelogram in which the ellipse is inscribed. + * @param to the x coordinate of the target point. + * @param to the y coordinate of the target point. + * @param tfrom the fraction of the ellipse section where the curve should start. + * @param tto the fraction of the ellipse section where the curve should end + * @param type the specification of what additional path segments should + * be appended to lead the current path to the starting point. + */ + default void arcTo(int ctrlx, int ctrly, int tox, int toy, double tfrom, double tto, ArcType type) { + // Copied from JavaFX Path2D + assert (tfrom >= 0.) : "tfrom must be positive or zero"; //$NON-NLS-1$ + assert (tto >= tfrom) : "tto must be greater than or equal to tfrom"; //$NON-NLS-1$ + assert (tto <= 1.) : "tto must be lower than 1"; //$NON-NLS-1$ + int currentx = getCurrentX(); + int currenty = getCurrentY(); + final int ocurrentx = currentx; + final int ocurrenty = currenty; + double cx0 = currentx + (ctrlx - currentx) * AbstractCirclePathIterator.CTRL_POINT_DISTANCE; + double cy0 = currenty + (ctrly - currenty) * AbstractCirclePathIterator.CTRL_POINT_DISTANCE; + double cx1 = tox + (ctrlx - tox) * AbstractCirclePathIterator.CTRL_POINT_DISTANCE; + double cy1 = toy + (ctrly - toy) * AbstractCirclePathIterator.CTRL_POINT_DISTANCE; + if (tto < 1.) { + double t = 1. - tto; + tox += (cx1 - tox) * t; + toy += (cy1 - toy) * t; + cx1 += (cx0 - cx1) * t; + cy1 += (cy0 - cy1) * t; + cx0 += (currentx - cx0) * t; + cy0 += (currenty - cy0) * t; + tox += (cx1 - tox) * t; + toy += (cy1 - toy) * t; + cx1 += (cx0 - cx1) * t; + cy1 += (cy0 - cy1) * t; + tox += (cx1 - tox) * t; + toy += (cy1 - toy) * t; + } + if (tfrom > 0.) { + if (tto < 1.) { + tfrom = tfrom / tto; + } + currentx += (cx0 - currentx) * tfrom; + currenty += (cy0 - currenty) * tfrom; + cx0 += (cx1 - cx0) * tfrom; + cy0 += (cy1 - cy0) * tfrom; + cx1 += (tox - cx1) * tfrom; + cy1 += (toy - cy1) * tfrom; + currentx += (cx0 - currentx) * tfrom; + currenty += (cy0 - currenty) * tfrom; + cx0 += (cx1 - cx0) * tfrom; + cy0 += (cy1 - cy0) * tfrom; + currentx += (cx0 - currentx) * tfrom; + currenty += (cy0 - currenty) * tfrom; + } + if (type == ArcType.MOVE_THEN_ARC) { + if (currentx != ocurrentx || currenty != ocurrenty) { + moveTo(currentx, currenty); + } + } else if (type == ArcType.LINE_THEN_ARC) { + if (currentx != ocurrentx || currenty != ocurrenty) { + lineTo(currentx, currenty); + } + } + if (tfrom == tto || + (currentx == cx0 && cx0 == cx1 && cx1 == tox && + currenty == cy0 && cy0 == cy1 && cy1 == toy)) { + if (type != ArcType.LINE_THEN_ARC) { + lineTo(tox, toy); + } + } else { + curveTo((int) Math.round(cx0), (int) Math.round(cy0), (int) Math.round(cx1), (int) Math.round(cy1), tox, toy); + } + } + + @Override + default void arcTo(Point2D ctrl, Point2D to, double tfrom, double tto, + org.arakhne.afc.math.geometry.d2.Path2D.ArcType type) { + assert (ctrl != null) : "Control point must be not null"; //$NON-NLS-1$ + assert (to != null) : "Target point must be not null"; //$NON-NLS-1$ + arcTo(ctrl.ix(), ctrl.iy(), to.ix(), to.iy(), tfrom, tto, type); + } + + /** + * Adds a section of an shallow ellipse to the current path. + * + *

This function is equivalent to:


+     * this.arcTo(ctrlx, ctrly, tox, toy, 0.0, 1.0, ArcType.ARCONLY);
+     * 
+ * + * @param ctrlx the x coordinate of the control point, i.e. the corner of the parallelogram in which the ellipse is inscribed. + * @param ctrlx the y coordinate of the control point, i.e. the corner of the parallelogram in which the ellipse is inscribed. + * @param to the x coordinate of the target point. + * @param to the y coordinate of the target point. + */ + default void arcTo(int ctrlx, int ctrly, int tox, int toy) { + arcTo(ctrlx, ctrly, tox, toy, 0., 1., ArcType.ARC_ONLY); + } + + @Override + default void arcTo(Point2D to, Vector2D radii, double xAxisRotation, boolean largeArcFlag, boolean sweepFlag) { + assert (radii != null) : "Radii vector must be not null"; //$NON-NLS-1$ + assert (to != null) : "Target point must be not null"; //$NON-NLS-1$ + arcTo(to.ix(), to.iy(), radii.ix(), radii.iy(), xAxisRotation, largeArcFlag, sweepFlag); + } + + /** + * Adds a section of an shallow ellipse to the current path. + * The ellipse from which the portions are extracted follows the rules: + * + * + *

+ * + *

The method will do nothing if the destination point is the same as + * the current point. + * The method will draw a simple line segment to the destination point + * if either of the two radii are zero. + * + * @param tox the X coordinate of the target point. + * @param toy the Y coordinate of the target point. + * @param radiusx the X radius of the tilted ellipse. + * @param radiusy the Y radius of the tilted ellipse. + * @param xAxisRotation the angle of tilt of the ellipse. + * @param largeArcFlag true iff the path will sweep the long way around the ellipse. + * @param sweepFlag true iff the path will sweep clockwise around the ellipse. + * @see "http://www.w3.org/TR/SVG/paths.html#PathDataEllipticalArcCommands" + */ + default void arcTo(int tox, int toy, int radiusx, int radiusy, double xAxisRotation, boolean largeArcFlag, boolean sweepFlag) { + // Copied for JavaFX + assert (radiusx >= 0.) : "X radius must be positive or zero."; //$NON-NLS-1$ + assert (radiusy >= 0.) : "Y radius must be positive or zero."; //$NON-NLS-1$ + if (radiusx == 0. || radiusy == 0.) { + lineTo(tox, toy); + return; + } + int ocurrentx = getCurrentX(); + int ocurrenty = getCurrentY(); + int x1 = ocurrentx; + int y1 = ocurrenty; + int x2 = tox; + int y2 = toy; + if (x1 == x2 && y1 == y2) { + return; + } + double cosphi, sinphi; + if (xAxisRotation == 0.) { + cosphi = 1.; + sinphi = 0.; + } else { + cosphi = Math.cos(xAxisRotation); + sinphi = Math.sin(xAxisRotation); + } + double mx = (x1 + x2) / 2.; + double my = (y1 + y2) / 2.; + double relx1 = x1 - mx; + double rely1 = y1 - my; + double x1p = (cosphi * relx1 + sinphi * rely1) / radiusx; + double y1p = (cosphi * rely1 - sinphi * relx1) / radiusy; + double lenpsq = x1p * x1p + y1p * y1p; + if (lenpsq >= 1.) { + double xqpr = y1p * radiusx; + double yqpr = x1p * radiusy; + if (sweepFlag) { + xqpr = -xqpr; + } else { + yqpr = -yqpr; + } + double relxq = cosphi * xqpr - sinphi * yqpr; + double relyq = cosphi * yqpr + sinphi * xqpr; + int xq = (int) Math.round(mx + relxq); + int yq = (int) Math.round(my + relyq); + double xc = x1 + relxq; + double yc = y1 + relyq; + if (x1 != ocurrentx || y1 != ocurrenty) { + lineTo(x1, y1); + } + arcTo((int) Math.round(xc), (int) Math.round(yc), xq, yq, 0, 1, ArcType.ARC_ONLY); + xc = x2 + relxq; + yc = y2 + relyq; + arcTo((int) Math.round(xc), (int) Math.round(yc), x2, y2, 0, 1, ArcType.ARC_ONLY); + return; + } + double scalef = Math.sqrt((1. - lenpsq) / lenpsq); + double cxp = scalef * y1p; + double cyp = scalef * x1p; + if (largeArcFlag == sweepFlag) { + cxp = -cxp; + } else { + cyp = -cyp; + } + mx += (cosphi * cxp * radiusx - sinphi * cyp * radiusy); + my += (cosphi * cyp * radiusy + sinphi * cxp * radiusx); + double ux = x1p - cxp; + double uy = y1p - cyp; + double vx = -(x1p + cxp); + double vy = -(y1p + cyp); + boolean done = false; + double quadlen = 1.; + boolean wasclose = false; + do { + double xqp = uy; + double yqp = ux; + if (sweepFlag) { + xqp = -xqp; + } else { + yqp = -yqp; + } + if (xqp * vx + yqp * vy > 0.) { + double dot = ux * vx + uy * vy; + if (dot >= 0) { + quadlen = Math.acos(dot) / MathConstants.DEMI_PI; + done = true; + } + wasclose = true; + } else if (wasclose) { + break; + } + double relxq = (cosphi * xqp * radiusx - sinphi * yqp * radiusy); + double relyq = (cosphi * yqp * radiusy + sinphi * xqp * radiusx); + int xq = (int) Math.round(mx + relxq); + int yq = (int) Math.round(my + relyq); + double xc = x1 + relxq; + double yc = y1 + relyq; + arcTo((int) Math.round(xc), (int) Math.round(yc), xq, yq, 0, quadlen, ArcType.ARC_ONLY); + x1 = xq; + y1 = yq; + ux = xqp; + uy = yqp; + } while (!done); + } + + @Pure @Override default double getDistanceSquared(Point2D p) { assert (p != null) : "Point must not be null"; //$NON-NLS-1$ @@ -1435,6 +1721,26 @@ default double getLengthSquared() { return length; } + + @Pure + @Override + default P getCurrentPoint() { + return getGeomFactory().newPoint(getCurrentX(), getCurrentY()); + } + + /** Replies the x coordinate of the last point in the path. + * + * @return the x coordinate of the last point in the path. + */ + @Pure + int getCurrentX(); + + /** Replies the x coordinate of the last point in the path. + * + * @return the x coordinate of the last point in the path. + */ + @Pure + int getCurrentY(); /** Replies the coordinate at the given index. * The index is in [0;{@link #size()}*2). diff --git a/core/math/src/main/java/org/arakhne/afc/math/geometry/d2/d/Path2d.java b/core/math/src/main/java/org/arakhne/afc/math/geometry/d2/d/Path2d.java index e3adaf355..c1256efd9 100644 --- a/core/math/src/main/java/org/arakhne/afc/math/geometry/d2/d/Path2d.java +++ b/core/math/src/main/java/org/arakhne/afc/math/geometry/d2/d/Path2d.java @@ -550,10 +550,14 @@ public Point2d getPointAt(int index) { @Override @Pure - public Point2d getCurrentPoint() { - return getGeomFactory().newPoint( - this.coords[this.numCoords-2], - this.coords[this.numCoords-1]); + public double getCurrentX() { + return this.coords[this.numCoords - 2]; + } + + @Override + @Pure + public double getCurrentY() { + return this.coords[this.numCoords - 1]; } @Override diff --git a/core/math/src/main/java/org/arakhne/afc/math/geometry/d2/dfx/Path2dfx.java b/core/math/src/main/java/org/arakhne/afc/math/geometry/d2/dfx/Path2dfx.java index dfd3a2d87..962e6a508 100644 --- a/core/math/src/main/java/org/arakhne/afc/math/geometry/d2/dfx/Path2dfx.java +++ b/core/math/src/main/java/org/arakhne/afc/math/geometry/d2/dfx/Path2dfx.java @@ -593,7 +593,28 @@ public Point2dfx getPointAt(int index) { this.coords.get(baseIdx), this.coords.get(baseIdx + 1)); } + + @Pure + @Override + public double getCurrentX() { + if (this.coords == null) { + throw new IndexOutOfBoundsException(); + } + int baseIdx = this.coords.size() - 1; + return this.coords.get(baseIdx - 1); + } + @Pure + @Override + public double getCurrentY() { + if (this.coords == null) { + throw new IndexOutOfBoundsException(); + } + int baseIdx = this.coords.size() - 1; + return this.coords.get(baseIdx); + } + + @Pure @Override public Point2dfx getCurrentPoint() { if (this.coords == null) { diff --git a/core/math/src/main/java/org/arakhne/afc/math/geometry/d2/i/Path2i.java b/core/math/src/main/java/org/arakhne/afc/math/geometry/d2/i/Path2i.java index c168f3e14..b1df7bbd1 100644 --- a/core/math/src/main/java/org/arakhne/afc/math/geometry/d2/i/Path2i.java +++ b/core/math/src/main/java/org/arakhne/afc/math/geometry/d2/i/Path2i.java @@ -563,13 +563,18 @@ public Point2i getPointAt(int index) { this.coords[index*2], this.coords[index*2+1]); } - + @Override @Pure - public Point2i getCurrentPoint() { - return getGeomFactory().newPoint( - this.coords[this.numCoords-2], - this.coords[this.numCoords-1]); + public int getCurrentX() { + return this.coords[this.numCoords - 2]; + } + + @Override + @Pure + public int getCurrentY() { + // TODO Auto-generated method stub + return this.coords[this.numCoords - 1]; } @Override diff --git a/core/math/src/main/java/org/arakhne/afc/math/geometry/d2/ifx/Path2ifx.java b/core/math/src/main/java/org/arakhne/afc/math/geometry/d2/ifx/Path2ifx.java index 68590faa1..c53501c40 100644 --- a/core/math/src/main/java/org/arakhne/afc/math/geometry/d2/ifx/Path2ifx.java +++ b/core/math/src/main/java/org/arakhne/afc/math/geometry/d2/ifx/Path2ifx.java @@ -618,6 +618,27 @@ public Point2ifx getPointAt(int index) { } @Override + @Pure + public int getCurrentX() { + if (this.coords == null) { + throw new IndexOutOfBoundsException(); + } + int index = this.coords.size() - 2; + return this.coords.get(index); + } + + @Override + @Pure + public int getCurrentY() { + if (this.coords == null) { + throw new IndexOutOfBoundsException(); + } + int index = this.coords.size() - 1; + return this.coords.get(index); + } + + @Override + @Pure public Point2ifx getCurrentPoint() { if (this.coords == null) { throw new IndexOutOfBoundsException(); diff --git a/core/math/src/main/javadoc/org/arakhne/math/geometry/d2/doc-files/arcto0.png b/core/math/src/main/javadoc/org/arakhne/math/geometry/d2/doc-files/arcto0.png new file mode 100644 index 0000000000000000000000000000000000000000..e82d0d320661d18be45e65dc8295a3dae34616b6 GIT binary patch literal 26592 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 OA|WCpT>e4V|Nj6DbGJkQ literal 0 HcmV?d00001 diff --git a/core/math/src/test/java/org/arakhne/afc/math/geometry/d2/afp/AbstractPath2afpTest.java b/core/math/src/test/java/org/arakhne/afc/math/geometry/d2/afp/AbstractPath2afpTest.java index e0ff9cbf5..571b2cd6b 100644 --- a/core/math/src/test/java/org/arakhne/afc/math/geometry/d2/afp/AbstractPath2afpTest.java +++ b/core/math/src/test/java/org/arakhne/afc/math/geometry/d2/afp/AbstractPath2afpTest.java @@ -26,16 +26,18 @@ import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; +import java.io.File; +import java.io.IOException; import java.util.Collection; import java.util.Iterator; import org.arakhne.afc.math.MathConstants; import org.arakhne.afc.math.geometry.PathElementType; +import org.arakhne.afc.math.geometry.d2.Path2D.ArcType; import org.arakhne.afc.math.geometry.d2.Point2D; import org.arakhne.afc.math.geometry.d2.Shape2D; import org.arakhne.afc.math.geometry.d2.Transform2D; import org.arakhne.afc.math.geometry.d2.afp.Path2afp.CrossingComputationType; -import org.arakhne.afc.math.geometry.d2.ai.Path2ai; import org.arakhne.afc.math.geometry.d2.ai.PathIterator2ai; import org.junit.Test; @@ -647,7 +649,7 @@ public void addIterator_closeBefore() { } @Test(expected = IllegalStateException.class) - public void curveToIntIntIntIntIntInt_noMoveTo() { + public void curveToDoubleDoubleDoubleDoubleDoubleDouble_noMoveTo() { Path2afp tmpShape = createPath(); tmpShape.curveTo(15, 145, 50, 20, 0, 0); } @@ -681,6 +683,123 @@ public void curveToPoint2DPoint2DPoint2D() { assertElement(pi, PathElementType.CURVE_TO, 123.456, 456.789, 789.123, 159.753, 456.852, 963.789); assertNoElement(pi); } + +// @Test +// public void generateShapeBitmap() throws IOException { +// this.shape.arcTo(20, 10, 10, 10, 0, false, false); +// File filename = generateTestPicture(this.shape); +// System.out.println("Filename: " + filename); +// } + + @Test + public void arcToDoubleDoubleDoubleDoubleDoubleDoubleArcType_01_arcOnly() { + this.shape.arcTo(5, 5, 20, 10, 0, 1, ArcType.ARC_ONLY); + PathIterator2afp pi = this.shape.getPathIterator(); + assertElement(pi, PathElementType.MOVE_TO, 0, 0); + assertElement(pi, PathElementType.LINE_TO, 1, 1); + assertElement(pi, PathElementType.QUAD_TO, 3, 0, 4, 3); + assertElement(pi, PathElementType.CURVE_TO, 5, -1, 6, 5, 7, -5); + assertElement(pi, PathElementType.CURVE_TO, 5.89543, 0.52285, 11.71573, 7.23858, 20, 10); + assertNoElement(pi); + } + + @Test + public void arcToDoubleDoubleDoubleDoubleDoubleDoubleArcType_01_lineTo() { + this.shape.arcTo(5, 5, 20, 10, 0, 1, ArcType.LINE_THEN_ARC); + PathIterator2afp pi = this.shape.getPathIterator(); + assertElement(pi, PathElementType.MOVE_TO, 0, 0); + assertElement(pi, PathElementType.LINE_TO, 1, 1); + assertElement(pi, PathElementType.QUAD_TO, 3, 0, 4, 3); + assertElement(pi, PathElementType.CURVE_TO, 5, -1, 6, 5, 7, -5); + assertElement(pi, PathElementType.CURVE_TO, 5.89543, 0.52285, 11.71573, 7.23858, 20, 10); + assertNoElement(pi); + } + + @Test + public void arcToDoubleDoubleDoubleDoubleDoubleDoubleArcType_01_moveTo() { + this.shape.arcTo(5, 5, 20, 10, 0, 1, ArcType.MOVE_THEN_ARC); + PathIterator2afp pi = this.shape.getPathIterator(); + assertElement(pi, PathElementType.MOVE_TO, 0, 0); + assertElement(pi, PathElementType.LINE_TO, 1, 1); + assertElement(pi, PathElementType.QUAD_TO, 3, 0, 4, 3); + assertElement(pi, PathElementType.CURVE_TO, 5, -1, 6, 5, 7, -5); + assertElement(pi, PathElementType.CURVE_TO, 5.89543, 0.52285, 11.71573, 7.23858, 20, 10); + assertNoElement(pi); + } + + @Test + public void arcToDoubleDoubleDoubleDoubleDoubleDoubleArcType_0251_arcOnly() { + this.shape.arcTo(5, 5, 20, 10, .25, 1, ArcType.ARC_ONLY); + PathIterator2afp pi = this.shape.getPathIterator(); + assertElement(pi, PathElementType.MOVE_TO, 0, 0); + assertElement(pi, PathElementType.LINE_TO, 1, 1); + assertElement(pi, PathElementType.QUAD_TO, 3, 0, 4, 3); + assertElement(pi, PathElementType.CURVE_TO, 5, -1, 6, 5, 7, -5); + assertElement(pi, PathElementType.CURVE_TO, 8.95958, 3.63357, 13.7868, 7.92893, 20, 10); + assertNoElement(pi); + } + + @Test + public void arcToDoubleDoubleDoubleDoubleDoubleDoubleArcType_0251_lineTo() { + this.shape.arcTo(5, 5, 20, 10, .25, 1, ArcType.LINE_THEN_ARC); + PathIterator2afp pi = this.shape.getPathIterator(); + assertElement(pi, PathElementType.MOVE_TO, 0, 0); + assertElement(pi, PathElementType.LINE_TO, 1, 1); + assertElement(pi, PathElementType.QUAD_TO, 3, 0, 4, 3); + assertElement(pi, PathElementType.CURVE_TO, 5, -1, 6, 5, 7, -5); + assertElement(pi, PathElementType.LINE_TO, 7.40028, -0.71462); + assertElement(pi, PathElementType.CURVE_TO, 8.95958, 3.63357, 13.7868, 7.92893, 20, 10); + assertNoElement(pi); + } + + @Test + public void arcToDoubleDoubleDoubleDoubleDoubleDoubleArcType_0251_moveTo() { + this.shape.arcTo(5, 5, 20, 10, .25, 1, ArcType.MOVE_THEN_ARC); + PathIterator2afp pi = this.shape.getPathIterator(); + assertElement(pi, PathElementType.MOVE_TO, 0, 0); + assertElement(pi, PathElementType.LINE_TO, 1, 1); + assertElement(pi, PathElementType.QUAD_TO, 3, 0, 4, 3); + assertElement(pi, PathElementType.CURVE_TO, 5, -1, 6, 5, 7, -5); + assertElement(pi, PathElementType.MOVE_TO, 7.40028, -0.71462); + assertElement(pi, PathElementType.CURVE_TO, 8.95958, 3.63357, 13.7868, 7.92893, 20, 10); + assertNoElement(pi); + } + + @Test + public void arcToPoint2DPoint2DDoubleDoubleArcType_01_arcOnly() { + this.shape.arcTo(createPoint(5, 5), createPoint(20, 10), 0, 1, ArcType.ARC_ONLY); + PathIterator2afp pi = this.shape.getPathIterator(); + assertElement(pi, PathElementType.MOVE_TO, 0, 0); + assertElement(pi, PathElementType.LINE_TO, 1, 1); + assertElement(pi, PathElementType.QUAD_TO, 3, 0, 4, 3); + assertElement(pi, PathElementType.CURVE_TO, 5, -1, 6, 5, 7, -5); + assertElement(pi, PathElementType.CURVE_TO, 5.89543, 0.52285, 11.71573, 7.23858, 20, 10); + assertNoElement(pi); + } + + @Test + public void arcToDoubleDoubleDoubleDouble() { + this.shape.arcTo(5, 5, 20, 10); + PathIterator2afp pi = this.shape.getPathIterator(); + assertElement(pi, PathElementType.MOVE_TO, 0, 0); + assertElement(pi, PathElementType.LINE_TO, 1, 1); + assertElement(pi, PathElementType.QUAD_TO, 3, 0, 4, 3); + assertElement(pi, PathElementType.CURVE_TO, 5, -1, 6, 5, 7, -5); + assertElement(pi, PathElementType.CURVE_TO, 5.89543, 0.52285, 11.71573, 7.23858, 20, 10); + assertNoElement(pi); + } + + @Test + public void arcToPoint2DPoint2D() { + this.shape.arcTo(createPoint(5, 5), createPoint(20, 10)); + PathIterator2afp pi = this.shape.getPathIterator(); + assertElement(pi, PathElementType.MOVE_TO, 0, 0); + assertElement(pi, PathElementType.LINE_TO, 1, 1); + assertElement(pi, PathElementType.QUAD_TO, 3, 0, 4, 3); + assertElement(pi, PathElementType.CURVE_TO, 5, -1, 6, 5, 7, -5); + assertElement(pi, PathElementType.CURVE_TO, 5.89543, 0.52285, 11.71573, 7.23858, 20, 10); + assertNoElement(pi); + } @Test public void getCoordAt() { @@ -2327,5 +2446,26 @@ public void isPolyline() { this.shape.lineTo(7, 8); assertFalse(this.shape.isPolyline()); } + + @Test + public void getCurrentX() { + assertEpsilonEquals(7, this.shape.getCurrentX()); + this.shape.lineTo(154, 485); + assertEpsilonEquals(154, this.shape.getCurrentX()); + } + + @Test + public void getCurrentY() { + assertEpsilonEquals(-5, this.shape.getCurrentY()); + this.shape.lineTo(154, 485); + assertEpsilonEquals(485, this.shape.getCurrentY()); + } + + @Test + public void getCurrentPoint() { + assertFpPointEquals(7, -5, this.shape.getCurrentPoint()); + this.shape.lineTo(154, 485); + assertFpPointEquals(154, 485, this.shape.getCurrentPoint()); + } } \ No newline at end of file diff --git a/core/math/src/test/java/org/arakhne/afc/math/geometry/d2/afp/AbstractShape2afpTest.java b/core/math/src/test/java/org/arakhne/afc/math/geometry/d2/afp/AbstractShape2afpTest.java index 44a206063..ded1d0ee1 100644 --- a/core/math/src/test/java/org/arakhne/afc/math/geometry/d2/afp/AbstractShape2afpTest.java +++ b/core/math/src/test/java/org/arakhne/afc/math/geometry/d2/afp/AbstractShape2afpTest.java @@ -24,18 +24,23 @@ import static org.junit.Assert.assertNotNull; import static org.junit.Assert.fail; +import java.awt.Color; +import java.awt.Graphics2D; +import java.awt.geom.Line2D; +import java.awt.geom.Path2D; +import java.awt.image.BufferedImage; +import java.io.File; +import java.io.IOException; import java.util.Arrays; +import javax.imageio.ImageIO; + import org.arakhne.afc.math.AbstractMathTestCase; -import org.arakhne.afc.math.Unefficient; import org.arakhne.afc.math.geometry.PathElementType; import org.arakhne.afc.math.geometry.PathWindingRule; import org.arakhne.afc.math.geometry.coordinatesystem.CoordinateSystem2DTestRule; import org.arakhne.afc.math.geometry.d2.Point2D; -import org.arakhne.afc.math.geometry.d2.Shape2D; -import org.arakhne.afc.math.geometry.d2.Transform2D; import org.arakhne.afc.math.geometry.d2.Vector2D; -import org.eclipse.xtext.xbase.lib.Pure; import org.junit.After; import org.junit.Before; import org.junit.ComparisonFailure; @@ -375,4 +380,58 @@ public void getGeomFactory() { @Test public abstract void operator_upToPoint2D(); + /** Generate a bitmap containing the given Shape2D. + * + * @param shape. + * @return the filename + * @throws IOException Input/output exception + */ + public static File generateTestPicture(Shape2afp shape) throws IOException { + File filename = File.createTempFile("testShape", ".png"); + Rectangle2afp box = shape.toBoundingBox(); + PathIterator2afp iterator = shape.getPathIterator(); + Path2D path = new Path2D.Double( + iterator.getWindingRule() == PathWindingRule.NON_ZERO ? Path2D.WIND_NON_ZERO : Path2D.WIND_EVEN_ODD); + while (iterator.hasNext()) { + PathElement2afp element = iterator.next(); + double tox = (element.getToX() - box.getMinX()) * 2.; + double toy = (box.getMaxY() - (element.getToY() - box.getMinY())) * 2.; + switch (element.getType()) { + case LINE_TO: + path.lineTo(tox, toy); + break; + case MOVE_TO: + path.moveTo(tox, toy); + break; + case CLOSE: + path.closePath(); + break; + case CURVE_TO: + path.curveTo( + (element.getCtrlX1() - box.getMinX()) * 2., + (box.getMaxY() - (element.getCtrlY1() - box.getMinY())) * 2., + (element.getCtrlX2() - box.getMinX()) * 2., + (box.getMaxY() - (element.getCtrlY2() - box.getMinY())) * 2., + tox, toy); + break; + case QUAD_TO: + path.quadTo( + (element.getCtrlX1() - box.getMinX()) * 2., + (box.getMaxY() - (element.getCtrlY1() - box.getMinY())) * 2., + tox, toy); + break; + default: + } + } + BufferedImage image = new BufferedImage( + (int) Math.floor(box.getWidth() * 2.) + 1, + (int) Math.floor(box.getHeight() * 2.) + 1, + BufferedImage.TYPE_INT_ARGB); + Graphics2D g2d = (Graphics2D) image.getGraphics(); + g2d.setColor(Color.BLACK); + g2d.draw(path); + ImageIO.write(image, "png", filename); + return filename; + } + } \ No newline at end of file diff --git a/core/math/src/test/java/org/arakhne/afc/math/geometry/d2/ai/AbstractPath2aiTest.java b/core/math/src/test/java/org/arakhne/afc/math/geometry/d2/ai/AbstractPath2aiTest.java index e0681891c..f14e72241 100644 --- a/core/math/src/test/java/org/arakhne/afc/math/geometry/d2/ai/AbstractPath2aiTest.java +++ b/core/math/src/test/java/org/arakhne/afc/math/geometry/d2/ai/AbstractPath2aiTest.java @@ -28,6 +28,8 @@ import static org.junit.Assert.assertNotSame; import static org.junit.Assert.assertTrue; +import java.io.File; +import java.io.IOException; import java.util.Arrays; import java.util.Collection; import java.util.Iterator; @@ -35,13 +37,12 @@ import org.arakhne.afc.math.MathConstants; import org.arakhne.afc.math.geometry.PathElementType; import org.arakhne.afc.math.geometry.PathWindingRule; +import org.arakhne.afc.math.geometry.d2.Path2D.ArcType; import org.arakhne.afc.math.geometry.d2.Point2D; import org.arakhne.afc.math.geometry.d2.Shape2D; import org.arakhne.afc.math.geometry.d2.Transform2D; import org.junit.Test; -import javafx.beans.property.BooleanProperty; - @SuppressWarnings("all") public abstract class AbstractPath2aiTest, B extends Rectangle2ai> extends AbstractShape2aiTest { @@ -817,6 +818,27 @@ public void getClosestPointTo() { assertEquals(p.toString(), -2, p.iy()); } + @Test + public void getCurrentX() { + assertEquals(7, this.shape.getCurrentX()); + this.shape.lineTo(148, 752); + assertEquals(148, this.shape.getCurrentX()); + } + + @Test + public void getCurrentY() { + assertEquals(-5, this.shape.getCurrentY()); + this.shape.lineTo(148, 752); + assertEquals(752, this.shape.getCurrentY()); + } + + @Test + public void getCurrentPoint() { + assertIntPointEquals(7, -5, this.shape.getCurrentPoint()); + this.shape.lineTo(148, 752); + assertIntPointEquals(148, 752, this.shape.getCurrentPoint()); + } + @Test @Override public void getFarthestPointTo() { @@ -2205,4 +2227,130 @@ public void isPolyline() { assertFalse(this.shape.isPolyline()); } + @Test + public void generateShapeBitmap() throws IOException { + this.shape.arcTo(5, 5, 20, 10, 0, 1, ArcType.ARC_ONLY); + File filename = generateTestPicture(this.shape); + System.out.println("Filename: " + filename); + } + + @Test + public void arcToIntIntIntIntDoubleDoubleArcType_01_arcOnly() { + this.shape.arcTo(5, 5, 20, 10, 0, 1, ArcType.ARC_ONLY); + PathIterator2ai pi = this.shape.getPathIterator(); + assertElement(pi, PathElementType.MOVE_TO, 0, 0); + assertElement(pi, PathElementType.LINE_TO, 2, 2); + assertElement(pi, PathElementType.QUAD_TO, 3, 0, 4, 3); + assertElement(pi, PathElementType.CURVE_TO, 5, -1, 6, 5, 7, -5); + assertElement(pi, PathElementType.CLOSE, 0, 0); + assertElement(pi, PathElementType.CURVE_TO, 6, 1, 12, 7, 20, 10); + assertNoElement(pi); + } + + @Test + public void arcToIntIntIntIntDoubleDoubleArcType_01_lineTo() { + this.shape.arcTo(5, 5, 20, 10, 0, 1, ArcType.LINE_THEN_ARC); + PathIterator2ai pi = this.shape.getPathIterator(); + assertElement(pi, PathElementType.MOVE_TO, 0, 0); + assertElement(pi, PathElementType.LINE_TO, 2, 2); + assertElement(pi, PathElementType.QUAD_TO, 3, 0, 4, 3); + assertElement(pi, PathElementType.CURVE_TO, 5, -1, 6, 5, 7, -5); + assertElement(pi, PathElementType.CLOSE, 0, 0); + assertElement(pi, PathElementType.CURVE_TO, 6, 1, 12, 7, 20, 10); + assertNoElement(pi); + } + + @Test + public void arcToIntIntIntIntDoubleDoubleArcType_01_moveTo() { + this.shape.arcTo(5, 5, 20, 10, 0, 1, ArcType.MOVE_THEN_ARC); + PathIterator2ai pi = this.shape.getPathIterator(); + assertElement(pi, PathElementType.MOVE_TO, 0, 0); + assertElement(pi, PathElementType.LINE_TO, 2, 2); + assertElement(pi, PathElementType.QUAD_TO, 3, 0, 4, 3); + assertElement(pi, PathElementType.CURVE_TO, 5, -1, 6, 5, 7, -5); + assertElement(pi, PathElementType.CLOSE, 0, 0); + assertElement(pi, PathElementType.CURVE_TO, 6, 1, 12, 7, 20, 10); + assertNoElement(pi); + } + + @Test + public void arcToIntIntIntIntDoubleDoubleArcType_0251_arcOnly() { + this.shape.arcTo(5, 5, 20, 10, .25, 1, ArcType.ARC_ONLY); + PathIterator2ai pi = this.shape.getPathIterator(); + assertElement(pi, PathElementType.MOVE_TO, 0, 0); + assertElement(pi, PathElementType.LINE_TO, 2, 2); + assertElement(pi, PathElementType.QUAD_TO, 3, 0, 4, 3); + assertElement(pi, PathElementType.CURVE_TO, 5, -1, 6, 5, 7, -5); + assertElement(pi, PathElementType.CLOSE, 0, 0); + assertElement(pi, PathElementType.CURVE_TO, 9, 4, 14, 8, 20, 10); + assertNoElement(pi); + } + + @Test + public void arcToIntIntIntIntDoubleDoubleArcType_0251_lineTo() { + this.shape.arcTo(5, 5, 20, 10, .25, 1, ArcType.LINE_THEN_ARC); + PathIterator2ai pi = this.shape.getPathIterator(); + assertElement(pi, PathElementType.MOVE_TO, 0, 0); + assertElement(pi, PathElementType.LINE_TO, 2, 2); + assertElement(pi, PathElementType.QUAD_TO, 3, 0, 4, 3); + assertElement(pi, PathElementType.CURVE_TO, 5, -1, 6, 5, 7, -5); + assertElement(pi, PathElementType.CLOSE, 0, 0); + assertElement(pi, PathElementType.LINE_TO, 6, 0); + assertElement(pi, PathElementType.CURVE_TO, 9, 4, 14, 8, 20, 10); + assertNoElement(pi); + } + + @Test + public void arcToIntIntIntIntDoubleDoubleArcType_0251_moveTo() { + this.shape.arcTo(5, 5, 20, 10, .25, 1, ArcType.MOVE_THEN_ARC); + PathIterator2ai pi = this.shape.getPathIterator(); + assertElement(pi, PathElementType.MOVE_TO, 0, 0); + assertElement(pi, PathElementType.LINE_TO, 2, 2); + assertElement(pi, PathElementType.QUAD_TO, 3, 0, 4, 3); + assertElement(pi, PathElementType.CURVE_TO, 5, -1, 6, 5, 7, -5); + assertElement(pi, PathElementType.CLOSE, 0, 0); + assertElement(pi, PathElementType.MOVE_TO, 6, 0); + assertElement(pi, PathElementType.CURVE_TO, 9, 4, 14, 8, 20, 10); + assertNoElement(pi); + } + + @Test + public void arcToPoint2DPoint2DDoubleDoubleArcType_01_arcOnly() { + this.shape.arcTo(createPoint(5, 5), createPoint(20, 10), 0, 1, ArcType.ARC_ONLY); + PathIterator2ai pi = this.shape.getPathIterator(); + assertElement(pi, PathElementType.MOVE_TO, 0, 0); + assertElement(pi, PathElementType.LINE_TO, 2, 2); + assertElement(pi, PathElementType.QUAD_TO, 3, 0, 4, 3); + assertElement(pi, PathElementType.CURVE_TO, 5, -1, 6, 5, 7, -5); + assertElement(pi, PathElementType.CLOSE, 0, 0); + assertElement(pi, PathElementType.CURVE_TO, 6, 1, 12, 7, 20, 10); + assertNoElement(pi); + } + + @Test + public void arcToIntIntIntInt() { + this.shape.arcTo(5, 5, 20, 10); + PathIterator2ai pi = this.shape.getPathIterator(); + assertElement(pi, PathElementType.MOVE_TO, 0, 0); + assertElement(pi, PathElementType.LINE_TO, 2, 2); + assertElement(pi, PathElementType.QUAD_TO, 3, 0, 4, 3); + assertElement(pi, PathElementType.CURVE_TO, 5, -1, 6, 5, 7, -5); + assertElement(pi, PathElementType.CLOSE, 0, 0); + assertElement(pi, PathElementType.CURVE_TO, 6, 1, 12, 7, 20, 10); + assertNoElement(pi); + } + + @Test + public void arcToPoint2DPoint2D() { + this.shape.arcTo(createPoint(5, 5), createPoint(20, 10)); + PathIterator2ai pi = this.shape.getPathIterator(); + assertElement(pi, PathElementType.MOVE_TO, 0, 0); + assertElement(pi, PathElementType.LINE_TO, 2, 2); + assertElement(pi, PathElementType.QUAD_TO, 3, 0, 4, 3); + assertElement(pi, PathElementType.CURVE_TO, 5, -1, 6, 5, 7, -5); + assertElement(pi, PathElementType.CLOSE, 0, 0); + assertElement(pi, PathElementType.CURVE_TO, 6, 1, 12, 7, 20, 10); + assertNoElement(pi); + } + } \ No newline at end of file diff --git a/core/math/src/test/java/org/arakhne/afc/math/geometry/d2/ai/AbstractShape2aiTest.java b/core/math/src/test/java/org/arakhne/afc/math/geometry/d2/ai/AbstractShape2aiTest.java index 123ba067a..11286d0f3 100644 --- a/core/math/src/test/java/org/arakhne/afc/math/geometry/d2/ai/AbstractShape2aiTest.java +++ b/core/math/src/test/java/org/arakhne/afc/math/geometry/d2/ai/AbstractShape2aiTest.java @@ -24,14 +24,25 @@ import static org.junit.Assert.assertNotNull; import static org.junit.Assert.fail; +import java.awt.Color; +import java.awt.Graphics2D; +import java.awt.geom.Path2D; +import java.awt.image.BufferedImage; +import java.io.File; +import java.io.IOException; import java.util.Arrays; +import javax.imageio.ImageIO; + import org.arakhne.afc.math.AbstractMathTestCase; import org.arakhne.afc.math.geometry.PathElementType; import org.arakhne.afc.math.geometry.PathWindingRule; import org.arakhne.afc.math.geometry.coordinatesystem.CoordinateSystem2DTestRule; import org.arakhne.afc.math.geometry.d2.Point2D; import org.arakhne.afc.math.geometry.d2.Vector2D; +import org.arakhne.afc.math.geometry.d2.afp.PathElement2afp; +import org.arakhne.afc.math.geometry.d2.afp.Rectangle2afp; +import org.arakhne.afc.math.geometry.d2.afp.Shape2afp; import org.junit.After; import org.junit.Before; import org.junit.Rule; @@ -303,4 +314,58 @@ public void getGeomFactory() { @Test public abstract void operator_upToPoint2D(); + /** Generate a bitmap containing the given Shape2D. + * + * @param shape. + * @return the filename + * @throws IOException Input/output exception + */ + public static File generateTestPicture(Shape2ai shape) throws IOException { + File filename = File.createTempFile("testShape", ".png"); + Rectangle2ai box = shape.toBoundingBox(); + PathIterator2ai iterator = shape.getPathIterator(); + Path2D path = new Path2D.Double( + iterator.getWindingRule() == PathWindingRule.NON_ZERO ? Path2D.WIND_NON_ZERO : Path2D.WIND_EVEN_ODD); + while (iterator.hasNext()) { + PathElement2ai element = iterator.next(); + int tox = (element.getToX() - box.getMinX()) * 2; + int toy = (box.getMaxY() - (element.getToY() - box.getMinY())) * 2; + switch (element.getType()) { + case LINE_TO: + path.lineTo(tox, toy); + break; + case MOVE_TO: + path.moveTo(tox, toy); + break; + case CLOSE: + path.closePath(); + break; + case CURVE_TO: + path.curveTo( + (element.getCtrlX1() - box.getMinX()) * 2, + (box.getMaxY() - (element.getCtrlY1() - box.getMinY())) * 2, + (element.getCtrlX2() - box.getMinX()) * 2, + (box.getMaxY() - (element.getCtrlY2() - box.getMinY())) * 2, + tox, toy); + break; + case QUAD_TO: + path.quadTo( + (element.getCtrlX1() - box.getMinX()) * 2, + (box.getMaxY() - (element.getCtrlY1() - box.getMinY())) * 2, + tox, toy); + break; + default: + } + } + BufferedImage image = new BufferedImage( + box.getWidth() * 2, + box.getHeight(), + BufferedImage.TYPE_INT_ARGB); + Graphics2D g2d = (Graphics2D) image.getGraphics(); + g2d.setColor(Color.BLACK); + g2d.draw(path); + ImageIO.write(image, "png", filename); + return filename; + } + } \ No newline at end of file