jdk-24/test/jdk/java/awt/geom/Path2D/UnitTest.java

1468 lines
58 KiB
Java

/*
* Copyright (c) 2006, 2018, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
/*
* @test
* @bug 4172661 8176501
* @summary Tests all public methods of Path2D classes on all 3 variants
* Path2D.Float, Path2D.Double, and GeneralPath.
* REMIND: Note that the hit testing tests will fail
* occasionally due to precision bugs in the various hit
* testing methods in the geometry classes.
* (Failure rates vary from 1 per 100 runs to 1 per thousands).
* See bug 6396047 to track progress on these failures.
*/
import java.awt.Rectangle;
import java.awt.Shape;
import java.awt.geom.AffineTransform;
import java.awt.geom.Arc2D;
import java.awt.geom.Area;
import java.awt.geom.CubicCurve2D;
import java.awt.geom.Ellipse2D;
import java.awt.geom.FlatteningPathIterator;
import java.awt.geom.GeneralPath;
import java.awt.geom.Line2D;
import java.awt.geom.Path2D;
import java.awt.geom.PathIterator;
import java.awt.geom.Point2D;
import java.awt.geom.QuadCurve2D;
import java.awt.geom.Rectangle2D;
import java.awt.geom.RoundRectangle2D;
import java.util.Random;
import java.util.NoSuchElementException;
public class UnitTest {
public static boolean verbose;
public static final int WIND_NON_ZERO = PathIterator.WIND_NON_ZERO;
public static final int WIND_EVEN_ODD = PathIterator.WIND_EVEN_ODD;
public static int CoordsForType[] = { 2, 2, 4, 6, 0 };
public static AffineTransform TxIdentity = new AffineTransform();
public static AffineTransform TxComplex = makeAT();
public static Shape TestShapes[];
public static SampleShape ShortSampleNonZero;
public static SampleShape ShortSampleEvenOdd;
public static SampleShape LongSampleNonZero;
public static SampleShape LongSampleEvenOdd;
public static Shape EmptyShapeNonZero =
new EmptyShape(WIND_NON_ZERO);
public static Shape EmptyShapeEvenOdd =
new EmptyShape(WIND_EVEN_ODD);
// Note: We pick a shape that is not anywhere near any of
// our test shapes so that the Path2D does not try to collapse
// out the connecting segment - an optimization that is too
// difficult to account for in the AppendedShape code.
public static Shape AppendShape = new Arc2D.Double(1000, 1000, 40, 40,
Math.PI/4, Math.PI,
Arc2D.CHORD);
public static AffineTransform makeAT() {
AffineTransform at = new AffineTransform();
at.scale(0.66, 0.23);
at.rotate(Math.toRadians(35.0));
at.shear(0.78, 1.32);
return at;
}
Random random;
public UnitTest(long randomSeed) {
this.random = new Random(randomSeed);
TestShapes = new Shape[] {
EmptyShapeNonZero,
EmptyShapeEvenOdd,
new Line2D.Double(),
new Line2D.Double(rpc(), rpc(), rpc(), rpc()),
new Line2D.Double(rnc(), rnc(), rnc(), rnc()),
new Rectangle2D.Double(),
new Rectangle2D.Double(rpc(), rpc(), -1, -1),
new Rectangle2D.Double(rpc(), rpc(), rd(), rd()),
new Rectangle2D.Double(rnc(), rnc(), rd(), rd()),
new Ellipse2D.Double(),
new Ellipse2D.Double(rpc(), rpc(), -1, -1),
new Ellipse2D.Double(rpc(), rpc(), rd(), rd()),
new Ellipse2D.Double(rnc(), rnc(), rd(), rd()),
new Arc2D.Double(Arc2D.OPEN),
new Arc2D.Double(Arc2D.CHORD),
new Arc2D.Double(Arc2D.PIE),
new Arc2D.Double(rpc(), rpc(), -1, -1, rt(), rt(), Arc2D.OPEN),
new Arc2D.Double(rpc(), rpc(), -1, -1, rt(), rt(), Arc2D.CHORD),
new Arc2D.Double(rpc(), rpc(), -1, -1, rt(), rt(), Arc2D.PIE),
new Arc2D.Double(rpc(), rpc(), rd(), rd(), rt(), rt(), Arc2D.OPEN),
new Arc2D.Double(rpc(), rpc(), rd(), rd(), rt(), rt(), Arc2D.CHORD),
new Arc2D.Double(rpc(), rpc(), rd(), rd(), rt(), rt(), Arc2D.PIE),
new Arc2D.Double(rnc(), rnc(), rd(), rd(), rt(), rt(), Arc2D.OPEN),
new Arc2D.Double(rnc(), rnc(), rd(), rd(), rt(), rt(), Arc2D.CHORD),
new Arc2D.Double(rnc(), rnc(), rd(), rd(), rt(), rt(), Arc2D.PIE),
new RoundRectangle2D.Double(),
new RoundRectangle2D.Double(rpc(), rpc(), -1, -1, ra(), ra()),
new RoundRectangle2D.Double(rpc(), rpc(), rd(), rd(), ra(), ra()),
new RoundRectangle2D.Double(rnc(), rnc(), rd(), rd(), ra(), ra()),
new QuadCurve2D.Double(),
new QuadCurve2D.Double(rpc(), rpc(), rpc(), rpc(), rpc(), rpc()),
new QuadCurve2D.Double(rnc(), rnc(), rnc(), rnc(), rnc(), rnc()),
new CubicCurve2D.Double(),
new CubicCurve2D.Double(rpc(), rpc(), rpc(), rpc(),
rpc(), rpc(), rpc(), rpc()),
new CubicCurve2D.Double(rnc(), rnc(), rnc(), rnc(),
rnc(), rnc(), rnc(), rnc()),
makeGeneralPath(WIND_NON_ZERO, 1.0),
makeGeneralPath(WIND_EVEN_ODD, 1.0),
makeGeneralPath(WIND_NON_ZERO, -1.0),
makeGeneralPath(WIND_EVEN_ODD, -1.0),
makeJDK8176501(),
// this shape has a special property: some coefficients to the t^3 term
// are *nearly* zero. And analytically they should be zero, but machine
// error prevented it. In these cases cubic polynomials should degenerate
// into quadratic polynomials, but because the coefficient is not exactly
// zero that may not always be handled correctly:
AffineTransform.getRotateInstance(Math.PI / 4).createTransformedShape(
new Ellipse2D.Float(0, 0, 100, 100))
};
int types[] = new int[100];
int i = 0;
types[i++] = PathIterator.SEG_MOVETO;
types[i++] = PathIterator.SEG_LINETO;
types[i++] = PathIterator.SEG_QUADTO;
types[i++] = PathIterator.SEG_CUBICTO;
types[i++] = PathIterator.SEG_CLOSE;
int shortlen = i;
int prevt = types[i-1];
while (i < types.length) {
int t;
do {
t = (int) (random.nextDouble() * 5);
} while (t == prevt &&
(t == PathIterator.SEG_MOVETO ||
t == PathIterator.SEG_CLOSE));
types[i++] = t;
prevt = t;
}
int numcoords = 0;
int numshortcoords = 0;
for (i = 0; i < types.length; i++) {
if (i == shortlen) {
numshortcoords = numcoords;
}
numcoords += CoordsForType[types[i]];
}
double coords[] = new double[numcoords];
for (i = 0; i < coords.length; i++) {
coords[i] = rpc();
}
ShortSampleNonZero = new SampleShape(WIND_NON_ZERO,
types, coords,
shortlen, numshortcoords);
ShortSampleEvenOdd = new SampleShape(WIND_EVEN_ODD,
types, coords,
shortlen, numshortcoords);
LongSampleNonZero = new SampleShape(WIND_NON_ZERO,
types, coords,
types.length, numcoords);
LongSampleEvenOdd = new SampleShape(WIND_EVEN_ODD,
types, coords,
types.length, numcoords);
}
public GeneralPath makeGeneralPath(int windingrule, double sign) {
GeneralPath gp = new GeneralPath(windingrule);
gp.moveTo((float) (sign * rpc()), (float) (sign * rpc()));
gp.lineTo((float) (sign * rpc()), (float) (sign * rpc()));
gp.quadTo((float) (sign * rpc()), (float) (sign * rpc()),
(float) (sign * rpc()), (float) (sign * rpc()));
gp.curveTo((float) (sign * rpc()), (float) (sign * rpc()),
(float) (sign * rpc()), (float) (sign * rpc()),
(float) (sign * rpc()), (float) (sign * rpc()));
gp.closePath();
return gp;
}
/**
* JDK-8176501 focused on a shape whose bounds included a lot of dead space.
* This recreates that shape, and the unit test testGetBounds2D checks the
* accuracy of {@link Shape#getBounds2D()}
*/
public static Path2D makeJDK8176501() {
Path2D.Double path = new Path2D.Double();
path.moveTo(40, 140);
path.curveTo(40, 60, 160, 60, 160, 140);
path.curveTo(160, 220, 40, 220, 40, 140);
path.closePath();
return path;
}
// Due to odd issues with the sizes of errors when the values
// being manipulated are near zero, we try to avoid values
// near zero by ensuring that both the rpc (positive coords)
// stay away from zero and also by ensuring that the rpc+rd
// (positive coords + dimensions) stay away from zero. We
// also ensure that rnc+rd (negative coords + dimension) stay
// suitably negative without approaching zero.
// Random positive coordinate (10 -> 110)
// rpc + rd gives a total range of (30 -> 170)
public double rpc() {
return (random.nextDouble() * 100.0) + 10.0;
}
// Random negative coordinate (-200 -> -100)
// rnc + rd gives a total range of (-180 -> -40)
public double rnc() {
return (random.nextDouble() * 100.0) - 200.0;
}
// Random dimension (20 -> 60)
public double rd() {
return (random.nextDouble() * 40.0) + 20.0;
}
// Random arc width/height (0.1 -> 5.1)
public double ra() {
return (random.nextDouble() * 5.0) + 0.1;
}
// Random arc angle (theta) (PI/4 => 5PI/4)
public double rt() {
return (random.nextDouble() * Math.PI) + Math.PI/4;
}
public static int fltulpdiff(double v1, double v2) {
if (v1 == v2) {
return 0;
}
float vf1 = (float) v1;
float vf2 = (float) v2;
if (vf1 == vf2) {
return 0;
}
float diff = Math.abs(vf1-vf2);
//float ulp = Math.ulp((float) ((vf1 + vf2)/2f));
float ulp = Math.max(Math.ulp(vf1), Math.ulp(vf2));
if (verbose && diff > ulp) {
System.out.println("v1 = "+vf1+", ulp = "+Math.ulp(vf1));
System.out.println("v2 = "+vf2+", ulp = "+Math.ulp(vf2));
System.out.println((diff/ulp)+" ulps");
}
return (int) (diff/ulp);
}
public static int fltulpless(double v1, double v2) {
if (v1 >= v2) {
return 0;
}
float vf1 = (float) v1;
float vf2 = (float) v2;
if (vf1 >= vf2) {
return 0;
}
float diff = Math.abs(vf1-vf2);
//float ulp = Math.ulp((float) ((vf1 + vf2)/2f));
float ulp = Math.max(Math.ulp(vf1), Math.ulp(vf2));
if (verbose && diff > ulp) {
System.out.println("v1 = "+vf1+", ulp = "+Math.ulp(vf1));
System.out.println("v2 = "+vf2+", ulp = "+Math.ulp(vf2));
System.out.println((diff/ulp)+" ulps");
}
return (int) (diff/ulp);
}
public static int dblulpdiff(double v1, double v2) {
if (v1 == v2) {
return 0;
}
double diff = Math.abs(v1-v2);
//double ulp = Math.ulp((v1 + v2)/2.0);
double ulp = Math.max(Math.ulp(v1), Math.ulp(v2));
if (verbose && diff > ulp) {
System.out.println("v1 = "+v1+", ulp = "+Math.ulp(v1));
System.out.println("v2 = "+v2+", ulp = "+Math.ulp(v2));
System.out.println((diff/ulp)+" ulps");
}
return (int) (diff/ulp);
}
public static abstract class Creator {
public abstract Path2D makePath();
public abstract Path2D makePath(int windingrule);
public abstract Path2D makePath(int windingrule, int capacity);
public abstract Path2D makePath(Shape s);
public abstract Path2D makePath(Shape s, AffineTransform at);
public abstract boolean supportsFloatCompose();
public abstract int getRecommendedTxMaxUlp();
public abstract void compare(PathIterator testpi,
PathIterator refpi,
AffineTransform at,
int maxulp);
}
public static class FltCreator extends Creator {
public Path2D makePath() {
return new Path2D.Float();
}
public Path2D makePath(int windingrule) {
return new Path2D.Float(windingrule);
}
public Path2D makePath(int windingrule, int capacity) {
return new Path2D.Float(windingrule, capacity);
}
public Path2D makePath(Shape s) {
return new Path2D.Float(s);
}
public Path2D makePath(Shape s, AffineTransform at) {
return new Path2D.Float(s, at);
}
public boolean supportsFloatCompose() {
return true;
}
public int getRecommendedTxMaxUlp() {
return 5;
}
public void compare(PathIterator testpi,
PathIterator refpi,
AffineTransform at,
int maxulp)
{
if (testpi.getWindingRule() != refpi.getWindingRule()) {
throw new RuntimeException("wrong winding rule");
}
float testcoords[] = new float[6];
float refcoords[] = new float[6];
while (!testpi.isDone()) {
if (refpi.isDone()) {
throw new RuntimeException("too many segments");
}
int testtype = testpi.currentSegment(testcoords);
int reftype = refpi.currentSegment(refcoords);
if (testtype != reftype) {
throw new RuntimeException("different segment types");
}
if (at != null) {
at.transform(refcoords, 0, refcoords, 0,
CoordsForType[reftype]/2);
}
for (int i = 0; i < CoordsForType[testtype]; i++) {
int ulps = fltulpdiff(testcoords[i], refcoords[i]);
if (ulps > maxulp) {
throw new RuntimeException("coords are different: "+
testcoords[i]+" != "+
refcoords[i]+
" ("+ulps+" ulps)");
}
}
testpi.next();
refpi.next();
}
if (!refpi.isDone()) {
throw new RuntimeException("not enough segments");
}
}
}
public static class DblCreator extends Creator {
public Path2D makePath() {
return new Path2D.Double();
}
public Path2D makePath(int windingrule) {
return new Path2D.Double(windingrule);
}
public Path2D makePath(int windingrule, int capacity) {
return new Path2D.Double(windingrule, capacity);
}
public Path2D makePath(Shape s) {
return new Path2D.Double(s);
}
public Path2D makePath(Shape s, AffineTransform at) {
return new Path2D.Double(s, at);
}
public boolean supportsFloatCompose() {
return false;
}
public int getRecommendedTxMaxUlp() {
return 3;
}
public void compare(PathIterator testpi,
PathIterator refpi,
AffineTransform at,
int maxulp)
{
if (testpi.getWindingRule() != refpi.getWindingRule()) {
throw new RuntimeException("wrong winding rule");
}
double testcoords[] = new double[6];
double refcoords[] = new double[6];
while (!testpi.isDone()) {
if (refpi.isDone()) {
throw new RuntimeException("too many segments");
}
int testtype = testpi.currentSegment(testcoords);
int reftype = refpi.currentSegment(refcoords);
if (testtype != reftype) {
throw new RuntimeException("different segment types");
}
if (at != null) {
at.transform(refcoords, 0, refcoords, 0,
CoordsForType[reftype]/2);
}
for (int i = 0; i < CoordsForType[testtype]; i++) {
int ulps = dblulpdiff(testcoords[i], refcoords[i]);
if (ulps > maxulp) {
throw new RuntimeException("coords are different: "+
testcoords[i]+" != "+
refcoords[i]+
" ("+ulps+" ulps)");
}
}
testpi.next();
refpi.next();
}
if (!refpi.isDone()) {
throw new RuntimeException("not enough segments");
}
}
}
public static class GPCreator extends FltCreator {
public Path2D makePath() {
return new GeneralPath();
}
public Path2D makePath(int windingrule) {
return new GeneralPath(windingrule);
}
public Path2D makePath(int windingrule, int capacity) {
return new GeneralPath(windingrule, capacity);
}
public Path2D makePath(Shape s) {
return new GeneralPath(s);
}
public Path2D makePath(Shape s, AffineTransform at) {
GeneralPath gp = new GeneralPath();
PathIterator pi = s.getPathIterator(at);
gp.setWindingRule(pi.getWindingRule());
gp.append(pi, false);
return gp;
}
public boolean supportsFloatCompose() {
return true;
}
}
public static class EmptyShape implements Shape {
private int windingrule;
public EmptyShape(int windingrule) {
this.windingrule = windingrule;
}
public Rectangle getBounds() {
return new Rectangle();
}
public Rectangle2D getBounds2D() {
return new Rectangle();
}
public boolean contains(double x, double y) {
return false;
}
public boolean contains(Point2D p) {
return false;
}
public boolean intersects(double x, double y, double w, double h) {
return false;
}
public boolean intersects(Rectangle2D r) {
return false;
}
public boolean contains(double x, double y, double w, double h) {
return false;
}
public boolean contains(Rectangle2D r) {
return false;
}
public PathIterator getPathIterator(AffineTransform at) {
return new PathIterator() {
public int getWindingRule() {
return windingrule;
}
public boolean isDone() {
return true;
}
public void next() {}
public int currentSegment(float[] coords) {
throw new NoSuchElementException();
}
public int currentSegment(double[] coords) {
throw new NoSuchElementException();
}
};
}
public PathIterator getPathIterator(AffineTransform at,
double flatness)
{
return getPathIterator(at);
}
}
public static class SampleShape implements Shape {
int windingrule;
int theTypes[];
double theCoords[];
int numTypes;
int numCoords;
public SampleShape(int windingrule,
int types[], double coords[],
int numtypes, int numcoords)
{
this.windingrule = windingrule;
this.theTypes = types;
this.theCoords = coords;
this.numTypes = numtypes;
this.numCoords = numcoords;
}
private Shape testshape;
public Shape getTestShape() {
if (testshape == null) {
testshape = new Area(this);
}
return testshape;
}
public Rectangle getBounds() {
return getBounds2D().getBounds();
}
public Rectangle2D getBounds2D() {
return getTestShape().getBounds2D();
}
public boolean contains(double x, double y) {
return getTestShape().contains(x, y);
}
public boolean contains(Point2D p) {
return getTestShape().contains(p);
}
public boolean intersects(double x, double y, double w, double h) {
return getTestShape().intersects(x, y, w, h);
}
public boolean intersects(Rectangle2D r) {
return getTestShape().intersects(r);
}
public boolean contains(double x, double y, double w, double h) {
return getTestShape().contains(x, y, w, h);
}
public boolean contains(Rectangle2D r) {
return getTestShape().contains(r);
}
public PathIterator getPathIterator(final AffineTransform at) {
return new PathIterator() {
int tindex;
int cindex;
public int getWindingRule() {
return windingrule;
}
public boolean isDone() {
return (tindex >= numTypes);
}
public void next() {
cindex += CoordsForType[theTypes[tindex]];
tindex++;
}
public int currentSegment(float[] coords) {
int t = theTypes[tindex];
int n = CoordsForType[t];
if (n > 0) {
// Cast to float first, then transform
// to match accuracy of float paths
for (int i = 0; i < n; i++) {
coords[i] = (float) theCoords[cindex+i];
}
if (at != null) {
at.transform(coords, 0, coords, 0, n/2);
}
}
return t;
}
public int currentSegment(double[] coords) {
int t = theTypes[tindex];
int n = CoordsForType[t];
if (n > 0) {
if (at == null) {
System.arraycopy(theCoords, cindex,
coords, 0, n);
} else {
at.transform(theCoords, cindex,
coords, 0, n/2);
}
}
return t;
}
};
}
public PathIterator getPathIterator(AffineTransform at,
double flatness)
{
return new FlatteningPathIterator(getPathIterator(at), flatness);
}
public String toString() {
Rectangle2D r2d = getBounds2D();
double xmin = r2d.getMinX();
double ymin = r2d.getMinY();
double xmax = r2d.getMaxX();
double ymax = r2d.getMaxY();
return ("SampleShape["+
(windingrule == WIND_NON_ZERO
? "NonZero"
: "EvenOdd")+
", nsegments = "+numTypes+
", ncoords = "+numCoords+
", bounds["+(r2d.getMinX()+", "+r2d.getMinY()+", "+
r2d.getMaxX()+", "+r2d.getMaxY())+"]"+
"]");
}
public Path2D makeFloatPath(Creator c) {
Path2D.Float p2df = (Path2D.Float) c.makePath(windingrule);
int ci = 0;
for (int i = 0; i < numTypes; i++) {
int t = theTypes[i];
switch (t) {
case PathIterator.SEG_MOVETO:
p2df.moveTo((float) theCoords[ci++],
(float) theCoords[ci++]);
break;
case PathIterator.SEG_LINETO:
p2df.lineTo((float) theCoords[ci++],
(float) theCoords[ci++]);
break;
case PathIterator.SEG_QUADTO:
p2df.quadTo((float) theCoords[ci++],
(float) theCoords[ci++],
(float) theCoords[ci++],
(float) theCoords[ci++]);
break;
case PathIterator.SEG_CUBICTO:
p2df.curveTo((float) theCoords[ci++],
(float) theCoords[ci++],
(float) theCoords[ci++],
(float) theCoords[ci++],
(float) theCoords[ci++],
(float) theCoords[ci++]);
break;
case PathIterator.SEG_CLOSE:
p2df.closePath();
break;
default:
throw new InternalError("unrecognized path type: "+t);
}
if (t != PathIterator.SEG_CLOSE) {
Point2D curpnt = p2df.getCurrentPoint();
if (((float) curpnt.getX()) != ((float) theCoords[ci-2]) ||
((float) curpnt.getY()) != ((float) theCoords[ci-1]))
{
throw new RuntimeException("currentpoint failed");
}
}
}
if (ci != numCoords) {
throw new InternalError("numcoords did not match");
}
return p2df;
}
public Path2D makeDoublePath(Creator c) {
Path2D p2d = c.makePath(windingrule);
int ci = 0;
for (int i = 0; i < numTypes; i++) {
int t = theTypes[i];
switch (t) {
case PathIterator.SEG_MOVETO:
p2d.moveTo(theCoords[ci++], theCoords[ci++]);
break;
case PathIterator.SEG_LINETO:
p2d.lineTo(theCoords[ci++], theCoords[ci++]);
break;
case PathIterator.SEG_QUADTO:
p2d.quadTo(theCoords[ci++], theCoords[ci++],
theCoords[ci++], theCoords[ci++]);
break;
case PathIterator.SEG_CUBICTO:
p2d.curveTo(theCoords[ci++], theCoords[ci++],
theCoords[ci++], theCoords[ci++],
theCoords[ci++], theCoords[ci++]);
break;
case PathIterator.SEG_CLOSE:
p2d.closePath();
break;
default:
throw new InternalError("unrecognized path type: "+t);
}
if (t != PathIterator.SEG_CLOSE) {
Point2D curpnt = p2d.getCurrentPoint();
if (((float) curpnt.getX()) != ((float) theCoords[ci-2]) ||
((float) curpnt.getY()) != ((float) theCoords[ci-1]))
{
throw new RuntimeException("currentpoint failed");
}
}
}
if (ci != numCoords) {
throw new InternalError("numcoords did not match");
}
return p2d;
}
}
public static class AppendedShape implements Shape {
Shape s1;
Shape s2;
boolean connect;
public AppendedShape(Shape s1, Shape s2, boolean connect) {
this.s1 = s1;
this.s2 = s2;
this.connect = connect;
}
public Rectangle getBounds() {
return getBounds2D().getBounds();
}
public Rectangle2D getBounds2D() {
return s1.getBounds2D().createUnion(s2.getBounds2D());
}
private Shape testshape;
private Shape getTestShape() {
if (testshape == null) {
testshape = new GeneralPath(this);
}
return testshape;
}
public boolean contains(double x, double y) {
return getTestShape().contains(x, y);
}
public boolean contains(Point2D p) {
return getTestShape().contains(p);
}
public boolean intersects(double x, double y, double w, double h) {
return getTestShape().intersects(x, y, w, h);
}
public boolean intersects(Rectangle2D r) {
return getTestShape().intersects(r);
}
public boolean contains(double x, double y, double w, double h) {
return getTestShape().contains(x, y, w, h);
}
public boolean contains(Rectangle2D r) {
return getTestShape().contains(r);
}
public PathIterator getPathIterator(final AffineTransform at) {
return new AppendingPathIterator(s1, s2, connect, at);
}
public PathIterator getPathIterator(AffineTransform at,
double flatness)
{
return new FlatteningPathIterator(getPathIterator(at), flatness);
}
public static class AppendingPathIterator implements PathIterator {
AffineTransform at;
PathIterator pi;
Shape swaiting;
int windingrule;
boolean connectrequested;
boolean canconnect;
boolean converttoline;
public AppendingPathIterator(Shape s1, Shape s2,
boolean connect,
AffineTransform at)
{
this.at = at;
this.pi = s1.getPathIterator(at);
this.swaiting = s2;
this.windingrule = pi.getWindingRule();
this.connectrequested = connect;
if (pi.isDone()) {
chain();
}
}
public void chain() {
if (swaiting != null) {
pi = swaiting.getPathIterator(at);
swaiting = null;
converttoline = (connectrequested && canconnect);
}
}
public int getWindingRule() {
return windingrule;
}
public boolean isDone() {
return (pi.isDone());
}
public void next() {
converttoline = false;
pi.next();
if (pi.isDone()) {
chain();
}
canconnect = true;
}
public int currentSegment(float[] coords) {
int type = pi.currentSegment(coords);
if (converttoline) {
type = SEG_LINETO;
}
return type;
}
public int currentSegment(double[] coords) {
int type = pi.currentSegment(coords);
if (converttoline) {
type = SEG_LINETO;
}
return type;
}
}
}
public static void checkEmpty(Path2D p2d, int windingrule) {
checkEmpty2(p2d, windingrule);
p2d.setWindingRule(PathIterator.WIND_NON_ZERO);
checkEmpty2(p2d, PathIterator.WIND_NON_ZERO);
p2d.setWindingRule(PathIterator.WIND_EVEN_ODD);
checkEmpty2(p2d, PathIterator.WIND_EVEN_ODD);
}
public static void checkEmpty2(Path2D p2d, int windingrule) {
if (p2d.getWindingRule() != windingrule) {
throw new RuntimeException("wrong winding rule in Path2D");
}
PathIterator pi = p2d.getPathIterator(null);
if (pi.getWindingRule() != windingrule) {
throw new RuntimeException("wrong winding rule in iterator");
}
if (!pi.isDone()) {
throw new RuntimeException("path not empty");
}
}
public static void compare(Creator c, Path2D p2d, Shape ref, int maxulp) {
compare(c, p2d, (Shape) p2d.clone(), null, 0);
compare(c, p2d, ref, null, 0);
compare(c, p2d, ref, TxIdentity, 0);
p2d.transform(TxIdentity);
compare(c, p2d, ref, null, 0);
compare(c, p2d, ref, TxIdentity, 0);
Shape s2 = p2d.createTransformedShape(TxIdentity);
compare(c, s2, ref, null, 0);
compare(c, s2, ref, TxIdentity, 0);
s2 = p2d.createTransformedShape(TxComplex);
compare(c, s2, ref, TxComplex, maxulp);
p2d.transform(TxComplex);
compare(c, p2d, (Shape) p2d.clone(), null, 0);
compare(c, p2d, ref, TxComplex, maxulp);
}
public static void compare(Creator c,
Shape p2d, Shape s,
AffineTransform at, int maxulp)
{
c.compare(p2d.getPathIterator(null), s.getPathIterator(at),
null, maxulp);
c.compare(p2d.getPathIterator(null), s.getPathIterator(null),
at, maxulp);
}
public static void checkBounds(Shape stest, Shape sref) {
checkBounds(stest.getBounds2D(), sref.getBounds2D(),
"2D bounds too small");
/*
checkBounds(stest.getBounds(), sref.getBounds(),
"int bounds too small");
*/
checkBounds(stest.getBounds(), stest.getBounds2D(),
"int bounds too small for 2D bounds");
}
public static void checkBounds(Rectangle2D tBounds,
Rectangle2D rBounds,
String faildesc)
{
if (rBounds.isEmpty()) {
if (!tBounds.isEmpty()) {
throw new RuntimeException("bounds not empty");
}
return;
} else if (tBounds.isEmpty()) {
throw new RuntimeException("bounds empty");
}
double rxmin = rBounds.getMinX();
double rymin = rBounds.getMinY();
double rxmax = rBounds.getMaxX();
double rymax = rBounds.getMaxY();
double txmin = tBounds.getMinX();
double tymin = tBounds.getMinY();
double txmax = tBounds.getMaxX();
double tymax = tBounds.getMaxY();
if (txmin > rxmin || tymin > rymin ||
txmax < rxmax || tymax < rymax)
{
if (verbose) System.out.println("test bounds = "+tBounds);
if (verbose) System.out.println("ref bounds = "+rBounds);
// Allow fudge room of a couple of single precision ulps
double ltxmin = txmin - 5 * Math.max(Math.ulp((float) rxmin),
Math.ulp((float) txmin));
double ltymin = tymin - 5 * Math.max(Math.ulp((float) rymin),
Math.ulp((float) tymin));
double ltxmax = txmax + 5 * Math.max(Math.ulp((float) rxmax),
Math.ulp((float) txmax));
double ltymax = tymax + 5 * Math.max(Math.ulp((float) rymax),
Math.ulp((float) tymax));
if (ltxmin > rxmin || ltymin > rymin ||
ltxmax < rxmax || ltymax < rymax)
{
if (!verbose) System.out.println("test bounds = "+tBounds);
if (!verbose) System.out.println("ref bounds = "+rBounds);
System.out.println("xmin: "+
txmin+" + "+fltulpless(txmin, rxmin)+" = "+
rxmin+" + "+fltulpless(rxmin, txmin));
System.out.println("ymin: "+
tymin+" + "+fltulpless(tymin, rymin)+" = "+
rymin+" + "+fltulpless(rymin, tymin));
System.out.println("xmax: "+
txmax+" + "+fltulpless(txmax, rxmax)+" = "+
rxmax+" + "+fltulpless(rxmax, txmax));
System.out.println("ymax: "+
tymax+" + "+fltulpless(tymax, rymax)+" = "+
rymax+" + "+fltulpless(rymax, tymax));
System.out.println("flt tbounds = ["+
((float) txmin)+", "+((float) tymin)+", "+
((float) txmax)+", "+((float) tymax)+"]");
System.out.println("flt rbounds = ["+
((float) rxmin)+", "+((float) rymin)+", "+
((float) rxmax)+", "+((float) rymax)+"]");
System.out.println("xmin ulp = "+fltulpless(rxmin, txmin));
System.out.println("ymin ulp = "+fltulpless(rymin, tymin));
System.out.println("xmax ulp = "+fltulpless(txmax, rxmax));
System.out.println("ymax ulp = "+fltulpless(tymax, rymax));
throw new RuntimeException(faildesc);
}
}
}
public void checkHits(Shape stest, Shape sref) {
for (int i = 0; i < 10; i++) {
double px = random.nextDouble() * 500 - 250;
double py = random.nextDouble() * 500 - 250;
Point2D pnt = new Point2D.Double(px, py);
double rw = random.nextDouble()*10+0.4;
double rh = random.nextDouble()*10+0.4;
double rx = px - rw/2;
double ry = py - rh/2;
Rectangle2D rect = new Rectangle2D.Double(rx, ry, rw, rh);
Rectangle2D empty = new Rectangle2D.Double(rx, ry, 0, 0);
if (!rect.contains(pnt)) {
throw new InternalError("test point not inside test rect!");
}
if (stest.contains(rx, ry, 0, 0)) {
throw new RuntimeException("contains 0x0 rect");
}
if (stest.contains(empty)) {
throw new RuntimeException("contains empty rect");
}
if (stest.intersects(rx, ry, 0, 0)) {
throw new RuntimeException("intersects 0x0 rect");
}
if (stest.intersects(empty)) {
throw new RuntimeException("intersects empty rect");
}
boolean tContainsXY = stest.contains(px, py);
boolean tContainsPnt = stest.contains(pnt);
boolean tContainsXYWH = stest.contains(rx, ry, rw, rh);
boolean tContainsRect = stest.contains(rect);
boolean tIntersectsXYWH = stest.intersects(rx, ry, rw, rh);
boolean tIntersectsRect = stest.intersects(rect);
if (tContainsXY != tContainsPnt) {
throw new RuntimeException("contains(x,y) != "+
"contains(pnt)");
}
if (tContainsXYWH != tContainsRect) {
throw new RuntimeException("contains(x,y,w,h) != "+
"contains(rect)");
}
if (tIntersectsXYWH != tIntersectsRect) {
throw new RuntimeException("intersects(x,y,w,h) != "+
"intersects(rect)");
}
boolean uContainsXY =
Path2D.contains(stest.getPathIterator(null), px, py);
boolean uContainsXYWH =
Path2D.contains(stest.getPathIterator(null), rx, ry, rw, rh);
boolean uIntersectsXYWH =
Path2D.intersects(stest.getPathIterator(null), rx, ry, rw, rh);
if (tContainsXY != uContainsXY) {
throw new RuntimeException("contains(x,y) "+
"does not match utility");
}
if (tContainsXYWH != uContainsXYWH) {
throw new RuntimeException("contains(x,y,w,h) "+
"does not match utility");
}
if (tIntersectsXYWH != uIntersectsXYWH) {
throw new RuntimeException("intersects(x,y,w,h) "+
"does not match utility");
}
// Make rect slightly smaller to be more conservative for rContains
double srx = rx + 0.1;
double sry = ry + 0.1;
double srw = rw - 0.2;
double srh = rh - 0.2;
Rectangle2D srect = new Rectangle2D.Double(srx, sry, srw, srh);
// Make rect slightly larger to be more liberal for rIntersects
double lrx = rx - 0.1;
double lry = ry - 0.1;
double lrw = rw + 0.2;
double lrh = rh + 0.2;
Rectangle2D lrect = new Rectangle2D.Double(lrx, lry, lrw, lrh);
if (srect.isEmpty()) {
throw new InternalError("smaller rect too small (empty)");
}
if (!lrect.contains(rect)) {
throw new InternalError("test rect not inside larger rect!");
}
if (!rect.contains(srect)) {
throw new InternalError("smaller rect not inside test rect!");
}
boolean rContainsSmaller;
boolean rIntersectsLarger;
boolean rContainsPnt;
if (sref instanceof SampleShape ||
sref instanceof QuadCurve2D ||
sref instanceof CubicCurve2D)
{
// REMIND
// Some of the source shapes are not proving reliable
// enough to do reference verification of the hit
// testing results.
// Quad/CubicCurve2D have spaghetti test methods that could
// very likely contain some bugs. They return a conflicting
// answer in maybe 1 out of 20,000 tests.
// Area causes a conflicting answer maybe 1 out of
// 100 to 1000 runs and it infinite loops maybe 1
// out of 10,000 runs or so.
// So, we use some conservative "safe" answers for
// these shapes and avoid their hit testing methods.
rContainsSmaller = tContainsRect;
rIntersectsLarger = tIntersectsRect;
rContainsPnt = tContainsPnt;
} else {
rContainsSmaller = sref.contains(srect);
rIntersectsLarger = sref.intersects(lrect);
rContainsPnt = sref.contains(px, py);
}
if (tIntersectsRect) {
if (tContainsRect) {
if (!tContainsPnt) {
System.out.println("reference shape = "+sref);
System.out.println("pnt = "+pnt);
System.out.println("rect = "+rect);
System.out.println("tbounds = "+stest.getBounds2D());
throw new RuntimeException("test contains rect, "+
"but not center point");
}
}
// Note: (tContainsPnt || tContainsRect) is same as
// tContainsPnt because of the test above...
if (tContainsPnt) {
if (!rIntersectsLarger) {
System.out.println("reference shape = "+sref);
System.out.println("pnt = "+pnt);
System.out.println("rect = "+rect);
System.out.println("lrect = "+lrect);
System.out.println("tbounds = "+stest.getBounds2D());
System.out.println("rbounds = "+sref.getBounds2D());
throw new RuntimeException("test claims containment, "+
"but no ref intersection");
}
}
} else {
if (tContainsRect) {
throw new RuntimeException("test contains rect, "+
"with no intersection");
}
if (tContainsPnt) {
System.out.println("reference shape = "+sref);
System.out.println("rect = "+rect);
throw new RuntimeException("test contains point, "+
"with no intersection");
}
if (rContainsPnt || rContainsSmaller) {
System.out.println("pnt = "+pnt);
System.out.println("rect = "+rect);
System.out.println("srect = "+lrect);
throw new RuntimeException("test did not intersect, "+
"but ref claims containment");
}
}
}
}
public void test(Creator c) {
testConstructors(c);
testPathConstruction(c);
testAppend(c);
testBounds(c);
testHits(c);
}
public static void testConstructors(Creator c) {
checkEmpty(c.makePath(), WIND_NON_ZERO);
checkEmpty(c.makePath(WIND_NON_ZERO), WIND_NON_ZERO);
checkEmpty(c.makePath(WIND_EVEN_ODD), WIND_EVEN_ODD);
checkEmpty(c.makePath(EmptyShapeNonZero), WIND_NON_ZERO);
checkEmpty(c.makePath(EmptyShapeNonZero, null), WIND_NON_ZERO);
checkEmpty(c.makePath(EmptyShapeNonZero, TxIdentity), WIND_NON_ZERO);
checkEmpty(c.makePath(EmptyShapeEvenOdd), WIND_EVEN_ODD);
checkEmpty(c.makePath(EmptyShapeEvenOdd, null), WIND_EVEN_ODD);
checkEmpty(c.makePath(EmptyShapeEvenOdd, TxIdentity), WIND_EVEN_ODD);
try {
c.makePath(null);
throw new RuntimeException(c+" allowed null Shape in constructor");
} catch (NullPointerException npe) {
// passes
}
try {
c.makePath(null, TxIdentity);
throw new RuntimeException(c+" allowed null Shape in constructor");
} catch (NullPointerException npe) {
// passes
}
for (int i = 0; i < TestShapes.length; i++) {
Shape sref = TestShapes[i];
if (verbose) System.out.println("construct testing "+sref);
compare(c, c.makePath(sref), sref, null, 0);
compare(c, c.makePath(sref), sref, TxIdentity, 0);
compare(c, c.makePath(sref, null), sref, null, 0);
compare(c, c.makePath(sref, null), sref, TxIdentity, 0);
compare(c, c.makePath(sref, TxIdentity), sref, null, 0);
compare(c, c.makePath(sref, TxIdentity), sref, TxIdentity, 0);
compare(c, c.makePath(sref, TxComplex), sref, TxComplex,
c.getRecommendedTxMaxUlp());
}
}
public static void testPathConstruction(Creator c) {
testPathConstruction(c, LongSampleNonZero);
testPathConstruction(c, LongSampleEvenOdd);
}
public static void testPathConstruction(Creator c, SampleShape ref) {
if (c.supportsFloatCompose()) {
compare(c, ref.makeFloatPath(c), ref, c.getRecommendedTxMaxUlp());
}
compare(c, ref.makeDoublePath(c), ref, c.getRecommendedTxMaxUlp());
}
public static void testAppend(Creator c) {
for (int i = 0; i < TestShapes.length; i++) {
Shape sref = TestShapes[i];
if (verbose) System.out.println("append testing "+sref);
PathIterator spi = sref.getPathIterator(null);
Path2D stest = c.makePath(spi.getWindingRule());
stest.append(spi, false);
compare(c, stest, sref, null, 0);
stest.reset();
stest.append(sref, false);
compare(c, stest, sref, null, 0);
stest.reset();
stest.append(sref.getPathIterator(TxComplex), false);
compare(c, stest, sref, TxComplex, 0);
// multiple shape append testing...
if (sref.getBounds2D().isEmpty()) {
// If the first shape is empty, then we really
// are not testing multiple appended shapes,
// we are just testing appending the AppendShape
// to a null path over and over.
// Also note that some empty shapes will spit out
// a single useless SEG_MOVETO that has no affect
// on the outcome, but it makes duplicating the
// behavior that Path2D has in that case difficult
// when the AppenedShape utility class has to
// iterate the exact same segments. So, we will
// just ignore all empty shapes here.
continue;
}
stest.reset();
stest.append(sref, false);
stest.append(AppendShape, false);
compare(c, stest,
new AppendedShape(sref, AppendShape, false),
null, 0);
stest.reset();
stest.append(sref, false);
stest.append(AppendShape, true);
compare(c, stest,
new AppendedShape(sref, AppendShape, true),
null, 0);
stest.reset();
stest.append(sref.getPathIterator(null), false);
stest.append(AppendShape.getPathIterator(null), false);
compare(c, stest,
new AppendedShape(sref, AppendShape, false),
null, 0);
stest.reset();
stest.append(sref.getPathIterator(null), false);
stest.append(AppendShape.getPathIterator(null), true);
compare(c, stest,
new AppendedShape(sref, AppendShape, true),
null, 0);
stest.reset();
stest.append(sref.getPathIterator(TxComplex), false);
stest.append(AppendShape.getPathIterator(TxComplex), false);
compare(c, stest,
new AppendedShape(sref, AppendShape, false),
TxComplex, 0);
stest.reset();
stest.append(sref.getPathIterator(TxComplex), false);
stest.append(AppendShape.getPathIterator(TxComplex), true);
compare(c, stest,
new AppendedShape(sref, AppendShape, true),
TxComplex, 0);
}
}
public static void testBounds(Creator c) {
for (int i = 0; i < TestShapes.length; i++) {
Shape sref = TestShapes[i];
if (verbose) System.out.println("bounds testing "+sref);
Shape stest = c.makePath(sref);
checkBounds(c.makePath(sref), sref);
testGetBounds2D(stest);
}
testBounds(c, ShortSampleNonZero);
testBounds(c, ShortSampleEvenOdd);
testBounds(c, LongSampleNonZero);
testBounds(c, LongSampleEvenOdd);
}
public static void testBounds(Creator c, SampleShape ref) {
if (verbose) System.out.println("bounds testing "+ref);
if (c.supportsFloatCompose()) {
checkBounds(ref.makeFloatPath(c), ref);
}
checkBounds(ref.makeDoublePath(c), ref);
}
/**
* Make sure the {@link Shape#getBounds2D()} returns a Rectangle2D that tightly fits the
* shape data. It shouldn't contain lots of dead space (see JDK 8176501), and it shouldn't
* leave out any shape path. This test relies on the accuracy of
* {@link Shape#intersects(double, double, double, double)}
*/
public static void testGetBounds2D(Shape shape) {
// first: make sure the shape is actually close to the perimeter of shape.getBounds2D().
// this is the crux of JDK 8176501:
Rectangle2D r = shape.getBounds2D();
if (r.getWidth() == 0 || r.getHeight() == 0) {
// this can happen for completely empty paths, which are part of our
// edge test cases in this class.
return;
}
if (verbose) System.out.println("testGetBounds2D "+shape+", "+r);
double xminInterior = r.getMinX() + .000001;
double yminInterior = r.getMinY() + .000001;
double xmaxInterior = r.getMaxX() - .000001;
double ymaxInterior = r.getMaxY() - .000001;
Rectangle2D topStrip = new Rectangle2D.Double(r.getMinX(), r.getMinY(), r.getWidth(), yminInterior - r.getMinY());
Rectangle2D leftStrip = new Rectangle2D.Double(r.getMinX(), r.getMinY(), xminInterior - r.getMinX(), r.getHeight());
Rectangle2D bottomStrip = new Rectangle2D.Double(r.getMinX(), ymaxInterior, r.getWidth(), r.getMaxY() - ymaxInterior);
Rectangle2D rightStrip = new Rectangle2D.Double(xmaxInterior, r.getMinY(), r.getMaxX() - xmaxInterior, r.getHeight());
if (!shape.intersects(topStrip)) {
if (verbose)
System.out.println("topStrip = "+topStrip);
throw new RuntimeException("the shape must intersect the top strip of its bounds");
}
if (!shape.intersects(leftStrip)) {
if (verbose)
System.out.println("leftStrip = " + leftStrip);
throw new RuntimeException("the shape must intersect the left strip of its bounds");
}
if (!shape.intersects(bottomStrip)) {
if (verbose)
System.out.println("bottomStrip = " + bottomStrip);
throw new RuntimeException("the shape must intersect the bottom strip of its bounds");
}
if (!shape.intersects(rightStrip)) {
if (verbose)
System.out.println("rightStrip = " + rightStrip);
throw new RuntimeException("the shape must intersect the right strip of bounds");
}
// Similarly: make sure our shape doesn't exist OUTSIDE of r, either. To my knowledge this has never
// been a problem, but if it did happen this would be an even more serious breach of contract than
// the former case.
double xminExterior = r.getMinX() - .000001;
double yminExterior = r.getMinY() - .000001;
double xmaxExterior = r.getMaxX() + .000001;
double ymaxExterior = r.getMaxY() + .000001;
// k is simply meant to mean "a large number, functionally similar to infinity for this test"
double k = 10000.0;
leftStrip = new Rectangle2D.Double(xminExterior - k, -k, k, 3 * k);
rightStrip = new Rectangle2D.Double(xmaxExterior, -k, k, 3 * k);
topStrip = new Rectangle2D.Double(-k, yminExterior - k, 3 * k, k);
bottomStrip = new Rectangle2D.Double(-k, ymaxExterior, 3 * k, k);
if (shape.intersects(leftStrip)) {
if (verbose)
System.out.println("leftStrip = " + leftStrip);
throw new RuntimeException("the shape must not intersect anything to the left of its bounds");
}
if (shape.intersects(rightStrip)) {
if (verbose)
System.out.println("rightStrip = " + rightStrip);
throw new RuntimeException("the shape must not intersect anything to the right of its bounds");
}
if (shape.intersects(topStrip)) {
if (verbose)
System.out.println("topStrip = " + topStrip);
throw new RuntimeException("the shape must not intersect anything above its bounds");
}
if (shape.intersects(bottomStrip)) {
if (verbose)
System.out.println("bottomStrip = " + bottomStrip);
throw new RuntimeException("the shape must not intersect anything below its bounds");
}
}
public void testHits(Creator c) {
for (int i = 0; i < TestShapes.length; i++) {
Shape sref = TestShapes[i];
if (verbose) System.out.println("hit testing "+sref);
Shape stest = c.makePath(sref);
checkHits(c.makePath(sref), sref);
}
testHits(c, ShortSampleNonZero);
testHits(c, ShortSampleEvenOdd);
// These take too long to construct the Area for reference testing
//testHits(c, LongSampleNonZero);
//testHits(c, LongSampleEvenOdd);
}
public void testHits(Creator c, SampleShape ref) {
if (verbose) System.out.println("hit testing "+ref);
if (c.supportsFloatCompose()) {
checkHits(ref.makeFloatPath(c), ref);
}
checkHits(ref.makeDoublePath(c), ref);
}
public static void main(String argv[]) {
// as specific failures come up we can add them to this array to make sure they're frequently tested:
long[] previousBugSeeds = new long[] {
// these are all failures related to JDK-8176501
4603421469924484958L,
4596019360892069260L,
4604586530476373958L,
4603766396818608126L
};
verbose = (argv.length > 1);
for (long seed : previousBugSeeds) {
test("", seed);
}
int limit = (argv.length > 0) ? 10000 : 1;
for (int i = 0; i < limit; i++) {
long seed = Double.doubleToLongBits(Math.random());
test("loop #" + (i + 1), seed);
}
}
private static void test(String logPrefix, long seed) {
String msg = "seed = " + seed;
if (!logPrefix.isEmpty())
msg = logPrefix + ", " + msg;
System.out.println(msg);
UnitTest t = new UnitTest(seed);
t.test(new GPCreator());
t.test(new FltCreator());
t.test(new DblCreator());
}
}