5848a60c01
Reviewed-by: prr
1468 lines
58 KiB
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());
|
|
}
|
|
}
|