| /* | |
| * 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()); | |
| } | |
| } |