8176501: Method Shape.getBounds2D() incorrectly includes Bezier control points in bounding box

Reviewed-by: prr
This commit is contained in:
jeremy 2022-04-27 18:37:42 +00:00 committed by Phil Race
parent 05dac5a23e
commit 8a16842b4e
6 changed files with 599 additions and 119 deletions
src/java.desktop/share/classes
test/jdk/java/awt/geom/Path2D

@ -311,23 +311,6 @@ public abstract class CubicCurve2D implements Shape, Cloneable {
this.y2 = y2;
}
/**
* {@inheritDoc}
* @since 1.2
*/
public Rectangle2D getBounds2D() {
float left = Math.min(Math.min(x1, x2),
Math.min(ctrlx1, ctrlx2));
float top = Math.min(Math.min(y1, y2),
Math.min(ctrly1, ctrly2));
float right = Math.max(Math.max(x1, x2),
Math.max(ctrlx1, ctrlx2));
float bottom = Math.max(Math.max(y1, y2),
Math.max(ctrly1, ctrly2));
return new Rectangle2D.Float(left, top,
right - left, bottom - top);
}
/**
* Use serialVersionUID from JDK 1.6 for interoperability.
*/
@ -558,23 +541,6 @@ public abstract class CubicCurve2D implements Shape, Cloneable {
this.y2 = y2;
}
/**
* {@inheritDoc}
* @since 1.2
*/
public Rectangle2D getBounds2D() {
double left = Math.min(Math.min(x1, x2),
Math.min(ctrlx1, ctrlx2));
double top = Math.min(Math.min(y1, y2),
Math.min(ctrly1, ctrly2));
double right = Math.max(Math.max(x1, x2),
Math.max(ctrlx1, ctrlx2));
double bottom = Math.max(Math.max(y1, y2),
Math.max(ctrly1, ctrly2));
return new Rectangle2D.Double(left, top,
right - left, bottom - top);
}
/**
* Use serialVersionUID from JDK 1.6 for interoperability.
*/
@ -1509,6 +1475,15 @@ public abstract class CubicCurve2D implements Shape, Cloneable {
return contains(r.getX(), r.getY(), r.getWidth(), r.getHeight());
}
/**
* {@inheritDoc}
* @since 1.2
*/
public Rectangle2D getBounds2D() {
return Path2D.getBounds2D(getPathIterator(null));
}
/**
* {@inheritDoc}
* @since 1.2

@ -802,23 +802,7 @@ public abstract sealed class Path2D implements Shape, Cloneable
* @since 1.6
*/
public final synchronized Rectangle2D getBounds2D() {
float x1, y1, x2, y2;
int i = numCoords;
if (i > 0) {
y1 = y2 = floatCoords[--i];
x1 = x2 = floatCoords[--i];
while (i > 0) {
float y = floatCoords[--i];
float x = floatCoords[--i];
if (x < x1) x1 = x;
if (y < y1) y1 = y;
if (x > x2) x2 = x;
if (y > y2) y2 = y;
}
} else {
x1 = y1 = x2 = y2 = 0.0f;
}
return new Rectangle2D.Float(x1, y1, x2 - x1, y2 - y1);
return getBounds2D(getPathIterator(null));
}
/**
@ -1594,23 +1578,7 @@ public abstract sealed class Path2D implements Shape, Cloneable
* @since 1.6
*/
public final synchronized Rectangle2D getBounds2D() {
double x1, y1, x2, y2;
int i = numCoords;
if (i > 0) {
y1 = y2 = doubleCoords[--i];
x1 = x2 = doubleCoords[--i];
while (i > 0) {
double y = doubleCoords[--i];
double x = doubleCoords[--i];
if (x < x1) x1 = x;
if (y < y1) y1 = y;
if (x > x2) x2 = x;
if (y > y2) y2 = y;
}
} else {
x1 = y1 = x2 = y2 = 0.0;
}
return new Rectangle2D.Double(x1, y1, x2 - x1, y2 - y1);
return getBounds2D(getPathIterator(null));
}
/**
@ -2131,6 +2099,88 @@ public abstract sealed class Path2D implements Shape, Cloneable
return getBounds2D().getBounds();
}
/**
* Returns a high precision bounding box of the specified PathIterator.
* <p>
* This method provides a basic facility for implementors of the {@link Shape} interface to
* implement support for the {@link Shape#getBounds2D()} method.
* </p>
*
* @param pi the specified {@code PathIterator}
* @return an instance of {@code Rectangle2D} that is a high-precision bounding box of the
* {@code PathIterator}.
* @see Shape#getBounds2D()
*/
static Rectangle2D getBounds2D(final PathIterator pi) {
final double[] coeff = new double[4];
final double[] deriv_coeff = new double[3];
final double[] coords = new double[6];
// bounds are stored as {leftX, rightX, topY, bottomY}
double[] bounds = null;
double lastX = 0.0;
double lastY = 0.0;
double endX = 0.0;
double endY = 0.0;
for (; !pi.isDone(); pi.next()) {
final int type = pi.currentSegment(coords);
switch (type) {
case PathIterator.SEG_MOVETO:
if (bounds == null) {
bounds = new double[] { coords[0], coords[0], coords[1], coords[1] };
}
endX = coords[0];
endY = coords[1];
break;
case PathIterator.SEG_LINETO:
endX = coords[0];
endY = coords[1];
break;
case PathIterator.SEG_QUADTO:
endX = coords[2];
endY = coords[3];
break;
case PathIterator.SEG_CUBICTO:
endX = coords[4];
endY = coords[5];
break;
case PathIterator.SEG_CLOSE:
default:
continue;
}
if (endX < bounds[0]) bounds[0] = endX;
if (endX > bounds[1]) bounds[1] = endX;
if (endY < bounds[2]) bounds[2] = endY;
if (endY > bounds[3]) bounds[3] = endY;
switch (type) {
case PathIterator.SEG_QUADTO:
Curve.accumulateExtremaBoundsForQuad(bounds, 0, lastX, coords[0], coords[2], coeff, deriv_coeff);
Curve.accumulateExtremaBoundsForQuad(bounds, 2, lastY, coords[1], coords[3], coeff, deriv_coeff);
break;
case PathIterator.SEG_CUBICTO:
Curve.accumulateExtremaBoundsForCubic(bounds, 0, lastX, coords[0], coords[2], coords[4], coeff, deriv_coeff);
Curve.accumulateExtremaBoundsForCubic(bounds, 2, lastY, coords[1], coords[3], coords[5], coeff, deriv_coeff);
break;
default:
break;
}
lastX = endX;
lastY = endY;
}
if (bounds != null) {
return new Rectangle2D.Double(bounds[0], bounds[2], bounds[1] - bounds[0], bounds[3] - bounds[2]);
}
// there's room to debate what should happen here, but historically we return a zeroed
// out rectangle here. So for backwards compatibility let's keep doing that:
return new Rectangle2D.Double();
}
/**
* Tests if the specified coordinates are inside the closed
* boundary of the specified {@link PathIterator}.

@ -238,19 +238,6 @@ public abstract class QuadCurve2D implements Shape, Cloneable {
this.y2 = y2;
}
/**
* {@inheritDoc}
* @since 1.2
*/
public Rectangle2D getBounds2D() {
float left = Math.min(Math.min(x1, x2), ctrlx);
float top = Math.min(Math.min(y1, y2), ctrly);
float right = Math.max(Math.max(x1, x2), ctrlx);
float bottom = Math.max(Math.max(y1, y2), ctrly);
return new Rectangle2D.Float(left, top,
right - left, bottom - top);
}
/**
* Use serialVersionUID from JDK 1.6 for interoperability.
*/
@ -428,19 +415,6 @@ public abstract class QuadCurve2D implements Shape, Cloneable {
this.y2 = y2;
}
/**
* {@inheritDoc}
* @since 1.2
*/
public Rectangle2D getBounds2D() {
double left = Math.min(Math.min(x1, x2), ctrlx);
double top = Math.min(Math.min(y1, y2), ctrly);
double right = Math.max(Math.max(x1, x2), ctrlx);
double bottom = Math.max(Math.max(y1, y2), ctrly);
return new Rectangle2D.Double(left, top,
right - left, bottom - top);
}
/**
* Use serialVersionUID from JDK 1.6 for interoperability.
*/
@ -1335,6 +1309,14 @@ public abstract class QuadCurve2D implements Shape, Cloneable {
return contains(r.getX(), r.getY(), r.getWidth(), r.getHeight());
}
/**
* {@inheritDoc}
* @since 1.2
*/
public Rectangle2D getBounds2D() {
return Path2D.getBounds2D(getPathIterator(null));
}
/**
* {@inheritDoc}
* @since 1.2

@ -712,6 +712,126 @@ public abstract class Curve {
return crossings;
}
/**
* Accumulate the quadratic extrema into the pre-existing bounding array.
* <p>
* This method focuses on one dimension at a time, so to get both the x and y
* dimensions you'll need to call this method twice.
* </p>
* <p>
* Whenever we have to examine cubic or quadratic extrema that change our bounding
* box: we run the risk of machine error that may produce a box that is slightly
* too small. But the contract of {@link Shape#getBounds2D()} says we should err
* on the side of being too large. So to address this: we'll apply a margin based
* on the upper limit of numerical error caused by the polynomial evaluation (horner
* scheme).
* </p>
*
* @param bounds the bounds to update, which are expressed as: { minX, maxX }
* @param boundsOffset the index in boundsof the minimum value
* @param x1 the starting value of the bezier curve where t = 0.0
* @param ctrlX the control value of the bezier curve
* @param x2 the ending value of the bezier curve where t = 1.0
* @param coeff an array of at least 3 elements that will be overwritten and reused
* @param deriv_coeff an array of at least 2 elements that will be overwritten and reused
*/
public static void accumulateExtremaBoundsForQuad(double[] bounds, int boundsOffset, double x1, double ctrlX, double x2, double[] coeff, double[] deriv_coeff) {
if (ctrlX < bounds[boundsOffset] ||
ctrlX > bounds[boundsOffset + 1]) {
final double dx21 = ctrlX - x1;
coeff[2] = (x2 - ctrlX) - dx21; // A = P3 - P0 - 2 P2
coeff[1] = 2.0 * dx21; // B = 2 (P2 - P1)
coeff[0] = x1; // C = P1
deriv_coeff[0] = coeff[1];
deriv_coeff[1] = 2.0 * coeff[2];
final double t = -deriv_coeff[0] / deriv_coeff[1];
if (t > 0.0 && t < 1.0) {
final double v = coeff[0] + t * (coeff[1] + t * coeff[2]);
// error condition = sum ( abs (coeff) ):
final double margin = Math.ulp(Math.abs(coeff[0])
+ Math.abs(coeff[1]) + Math.abs(coeff[2]));
if (v - margin < bounds[boundsOffset]) {
bounds[boundsOffset] = v - margin;
}
if (v + margin > bounds[boundsOffset + 1]) {
bounds[boundsOffset + 1] = v + margin;
}
}
}
}
/**
* Accumulate the cubic extrema into the pre-existing bounding array.
* <p>
* This method focuses on one dimension at a time, so to get both the x and y
* dimensions you'll need to call this method twice.
* </p>
* <p>
* Whenever we have to examine cubic or quadratic extrema that change our bounding
* box: we run the risk of machine error that may produce a box that is slightly
* too small. But the contract of {@link Shape#getBounds2D()} says we should err
* on the side of being too large. So to address this: we'll apply a margin based
* on the upper limit of numerical error caused by the polynomial evaluation (horner
* scheme).
* </p>
*
* @param bounds the bounds to update, which are expressed as: { minX, maxX }
* @param boundsOffset the index in boundsof the minimum value
* @param x1 the starting value of the bezier curve where t = 0.0
* @param ctrlX1 the first control value of the bezier curve
* @param ctrlX1 the second control value of the bezier curve
* @param x2 the ending value of the bezier curve where t = 1.0
* @param coeff an array of at least 3 elements that will be overwritten and reused
* @param deriv_coeff an array of at least 2 elements that will be overwritten and reused
*/
public static void accumulateExtremaBoundsForCubic(double[] bounds, int boundsOffset, double x1, double ctrlX1, double ctrlX2, double x2, double[] coeff, double[] deriv_coeff) {
if (ctrlX1 < bounds[boundsOffset] ||
ctrlX1 > bounds[boundsOffset + 1] ||
ctrlX2 < bounds[boundsOffset] ||
ctrlX2 > bounds[boundsOffset + 1]) {
final double dx32 = 3.0 * (ctrlX2 - ctrlX1);
final double dx21 = 3.0 * (ctrlX1 - x1);
coeff[3] = (x2 - x1) - dx32; // A = P3 - P0 - 3 (P2 - P1) = (P3 - P0) + 3 (P1 - P2)
coeff[2] = (dx32 - dx21); // B = 3 (P2 - P1) - 3(P1 - P0) = 3 (P2 + P0) - 6 P1
coeff[1] = dx21; // C = 3 (P1 - P0)
coeff[0] = x1; // D = P0
deriv_coeff[0] = coeff[1];
deriv_coeff[1] = 2.0 * coeff[2];
deriv_coeff[2] = 3.0 * coeff[3];
// reuse this array, give it a new name for readability:
final double[] tExtrema = deriv_coeff;
// solveQuadratic should be improved to get correct t extrema (1 ulp):
final int tExtremaCount = QuadCurve2D.solveQuadratic(deriv_coeff, tExtrema);
if (tExtremaCount > 0) {
// error condition = sum ( abs (coeff) ):
final double margin = Math.ulp(Math.abs(coeff[0])
+ Math.abs(coeff[1]) + Math.abs(coeff[2])
+ Math.abs(coeff[3]));
for (int i = 0; i < tExtremaCount; i++) {
final double t = tExtrema[i];
if (t > 0.0 && t < 1.0) {
final double v = coeff[0] + t * (coeff[1] + t * (coeff[2] + t * coeff[3]));
if (v - margin < bounds[boundsOffset]) {
bounds[boundsOffset] = v - margin;
}
if (v + margin > bounds[boundsOffset + 1]) {
bounds[boundsOffset + 1] = v + margin;
}
}
}
}
}
}
public Curve(int direction) {
this.direction = direction;
}

@ -0,0 +1,263 @@
import java.awt.*;
import java.awt.geom.*;
import java.math.*;
import java.util.*;
/*
* @test
* @bug 8176501
* @summary This tests thousands of shapes and makes sure a high-precision bounding box fits inside the
* results of Path2D.getBounds(PathIterator)
* @run main GetBounds2DPrecisionTest
*/
public class GetBounds2DPrecisionTest {
public static void main(String[] args) {
String msg1 = testSmallCubics();
if (msg1 != null) {
System.out.println("testSmallCubics: "+msg1);
} else {
System.out.println("testSmallCubics: passed");
}
if (msg1 != null)
throw new RuntimeException("One or more tests failed; see System.out output for details.");
}
/**
* @return a String describing the failure, or null if this test passed.
*/
private static String testSmallCubics() {
int failureCtr = 0;
for(int a = 0; a < 1000; a++) {
CubicCurve2D cubicCurve2D = createSmallCubic(a);
if (!test(a, cubicCurve2D, getHorizontalEdges(cubicCurve2D)))
failureCtr++;
}
if (failureCtr > 0)
return failureCtr+" tests failed; see System.out for details";
return null;
}
private static CubicCurve2D createSmallCubic(int trial) {
Random random = new Random(trial);
double cx1 = random.nextDouble() * 10 - 5;
double cy1 = random.nextDouble();
double cx2 = random.nextDouble() * 10 - 5;
double cy2 = random.nextDouble();
return new CubicCurve2D.Double(0, 0, cx1, cy1, cx2, cy2, 0, 1);
}
/**
* This returns true if the shape's getBounds2D() method returns a bounding box whose
* left & right edges matches or exceeds the horizontalEdges arguments.
*/
private static boolean test(int trial, Shape shape, BigDecimal[] horizontalEdges) {
Rectangle2D bounds_doublePrecision = shape.getBounds2D();
Rectangle2D bounds_bigDecimalPrecision = new Rectangle2D.Double(
horizontalEdges[0].doubleValue(),
bounds_doublePrecision.getY(),
horizontalEdges[1].subtract(horizontalEdges[0]).doubleValue(),
bounds_doublePrecision.getHeight() );
boolean pass = true;
if (bounds_doublePrecision.getMinX() > bounds_bigDecimalPrecision.getMinX()) {
pass = false;
String x1a = toUniformString(bounds_bigDecimalPrecision.getX());
String x1b = toComparisonString(x1a, toUniformString(bounds_doublePrecision.getX()));
System.out.println("Left expected:\t"+x1a);
System.out.println("Left observed:\t"+x1b);
}
if (bounds_doublePrecision.getMaxX() < bounds_bigDecimalPrecision.getMaxX()) {
pass = false;
String x2a = toUniformString(bounds_bigDecimalPrecision.getMaxX());
String x2b = toComparisonString(x2a, toUniformString(bounds_doublePrecision.getMaxX()));
System.out.println("Right expected:\t"+x2a);
System.out.println("Right observed:\t"+x2b);
}
if (!pass)
System.out.println("\ttrial "+trial +" failed ("+toString(shape)+")");
return pass;
}
/**
* Return the left and right edges in high precision
*/
private static BigDecimal[] getHorizontalEdges(CubicCurve2D curve) {
double cx1 = curve.getCtrlX1();
double cx2 = curve.getCtrlX2();
BigDecimal[] coeff = new BigDecimal[4];
BigDecimal[] deriv_coeff = new BigDecimal[3];
BigDecimal[] tExtrema = new BigDecimal[2];
// coeff[3] = -lastX + 3.0 * coords[0] - 3.0 * coords[2] + coords[4];
// coeff[2] = 3.0 * lastX - 6.0 * coords[0] + 3.0 * coords[2];
// coeff[1] = -3.0 * lastX + 3.0 * coords[0];
// coeff[0] = lastX;
coeff[3] = new BigDecimal(3).multiply(new BigDecimal(cx1)).add( new BigDecimal(-3).multiply(new BigDecimal(cx2)) );
coeff[2] = new BigDecimal(-6).multiply(new BigDecimal(cx1)).add(new BigDecimal(3).multiply(new BigDecimal(cx2)));
coeff[1] = new BigDecimal(3).multiply(new BigDecimal(cx1));
coeff[0] = BigDecimal.ZERO;
deriv_coeff[0] = coeff[1];
deriv_coeff[1] = new BigDecimal(2.0).multiply( coeff[2] );
deriv_coeff[2] = new BigDecimal(3.0).multiply( coeff[3] );
int tExtremaCount = solveQuadratic(deriv_coeff, tExtrema);
BigDecimal leftX = BigDecimal.ZERO;
BigDecimal rightX = BigDecimal.ZERO;
for (int i = 0; i < tExtremaCount; i++) {
BigDecimal t = tExtrema[i];
if (t.compareTo( BigDecimal.ZERO ) > 0 && t.compareTo(BigDecimal.ONE) < 0) {
BigDecimal x = coeff[0].add( t.multiply(coeff[1].add(t.multiply(coeff[2].add(t.multiply(coeff[3]))))) );
if (x.compareTo(leftX) < 0) leftX = x;
if (x.compareTo(rightX) > 0) rightX = x;
}
}
return new BigDecimal[] { leftX, rightX };
}
/**
* Return the left and right edges in high precision
*/
private static BigDecimal[] getHorizontalEdges(QuadCurve2D curve) {
double cx = curve.getCtrlX();
BigDecimal[] coeff = new BigDecimal[3];
BigDecimal[] deriv_coeff = new BigDecimal[2];
BigDecimal dx21 = new BigDecimal(cx).subtract(new BigDecimal(curve.getX1()));
coeff[2] = new BigDecimal(curve.getX2()).subtract(new BigDecimal(cx)).subtract(dx21); // A = P3 - P0 - 2 P2
coeff[1] = new BigDecimal(2.0).multiply(dx21); // B = 2 (P2 - P1)
coeff[0] = new BigDecimal(curve.getX1()); // C = P1
deriv_coeff[0] = coeff[1];
deriv_coeff[1] = new BigDecimal(2.0).multiply( coeff[2] );
BigDecimal leftX = BigDecimal.ZERO;
BigDecimal rightX = BigDecimal.ZERO;
if (!deriv_coeff[1].equals(BigDecimal.ZERO)) {
BigDecimal t = deriv_coeff[0].negate().divide(deriv_coeff[1], RoundingMode.HALF_EVEN);
if (t.compareTo( BigDecimal.ZERO ) > 0 && t.compareTo(BigDecimal.ONE) < 0) {
BigDecimal x = coeff[0].add( t.multiply(coeff[1].add(t.multiply(coeff[2]))) );
if (x.compareTo(leftX) < 0) leftX = x;
if (x.compareTo(rightX) > 0) rightX = x;
}
}
return new BigDecimal[] { leftX, rightX };
}
/**
* Convert a shape into SVG-ish notation for debugging/readability.
*/
private static String toString(Shape shape) {
StringBuilder returnValue = new StringBuilder();
PathIterator pi = shape.getPathIterator(null);
double[] coords = new double[6];
while(!pi.isDone()) {
int k = pi.currentSegment(coords);
if (k == PathIterator.SEG_MOVETO) {
returnValue.append("m "+coords[0]+" "+coords[1]+" ");
} else if (k == PathIterator.SEG_LINETO) {
returnValue.append("l "+coords[0]+" "+coords[1]+" ");
} else if (k == PathIterator.SEG_QUADTO) {
returnValue.append("q "+coords[0]+" "+coords[1]+" "+coords[2]+" "+coords[3]+" ");
} else if (k == PathIterator.SEG_CUBICTO) {
returnValue.append("c "+coords[0]+" "+coords[1]+" "+coords[2]+" "+coords[3]+" "+coords[4]+" "+coords[5]+" ");
} else if (k == PathIterator.SEG_CLOSE) {
returnValue.append("z");
}
pi.next();
}
return returnValue.toString();
}
private static String toUniformString(double value) {
BigDecimal decimal = new BigDecimal(value);
int DIGIT_COUNT = 40;
String str = decimal.toPlainString();
if (str.length() >= DIGIT_COUNT) {
str = str.substring(0,DIGIT_COUNT-1)+"";
}
while(str.length() < DIGIT_COUNT) {
str = str + " ";
}
return str;
}
private static String toComparisonString(String target, String observed) {
for(int a = 0; a<target.length(); a++) {
char ch1 = target.charAt(a);
char ch2 = observed.charAt(a);
if (ch1 != ch2) {
return observed.substring(0,a) + createCircleDigit(ch2)+observed.substring(a+1);
}
}
return observed;
}
/**
* Convert a digit 0-9 into a "circle digit". Really we just want any unobtrusive way to
* highlight a character.
*/
private static char createCircleDigit(char ch) {
if (ch >= '1' && ch <='9')
return (char)( ch - '1' + '\u2460');
if (ch == '0')
return '\u24ea';
return ch;
}
private static int solveQuadratic(BigDecimal[] eqn, BigDecimal[] res) {
BigDecimal a = eqn[2];
BigDecimal b = eqn[1];
BigDecimal c = eqn[0];
int roots = 0;
if (a.equals(BigDecimal.ZERO)) {
// The quadratic parabola has degenerated to a line.
if (b.equals(BigDecimal.ZERO)) {
// The line has degenerated to a constant.
return -1;
}
res[roots++] = c.negate().divide(b);
} else {
// From Numerical Recipes, 5.6, Quadratic and Cubic Equations
BigDecimal d = b.multiply(b).add(new BigDecimal(-4.0).multiply(a).multiply(c));
if (d.compareTo(BigDecimal.ZERO) < 0) {
// If d < 0.0, then there are no roots
return 0;
}
d = d.sqrt(MathContext.DECIMAL128);
// For accuracy, calculate one root using:
// (-b +/- d) / 2a
// and the other using:
// 2c / (-b +/- d)
// Choose the sign of the +/- so that b+d gets larger in magnitude
if (b.compareTo(BigDecimal.ZERO) < 0) {
d = d.negate();
}
BigDecimal q = b.add(d).divide(new BigDecimal(-2.0));
q = q.setScale(40, RoundingMode.HALF_EVEN);
// We already tested a for being 0 above
res[roots++] = q.divide(a, RoundingMode.HALF_EVEN);
if (!q.equals(BigDecimal.ZERO)) {
c = c.setScale(40, RoundingMode.HALF_EVEN);
res[roots++] = c.divide(q, RoundingMode.HALF_EVEN);
}
}
return roots;
}
}

@ -23,7 +23,7 @@
/*
* @test
* @bug 4172661
* @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
@ -132,6 +132,16 @@ public class UnitTest {
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];
@ -193,6 +203,20 @@ public class UnitTest {
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)
@ -538,33 +562,11 @@ public class UnitTest {
return testshape;
}
private Rectangle2D cachedBounds;
public Rectangle2D getCachedBounds2D() {
if (cachedBounds == null) {
double xmin, ymin, xmax, ymax;
int ci = 0;
xmin = xmax = theCoords[ci++];
ymin = ymax = theCoords[ci++];
while (ci < numCoords) {
double c = theCoords[ci++];
if (xmin > c) xmin = c;
if (xmax < c) xmax = c;
c = theCoords[ci++];
if (ymin > c) ymin = c;
if (ymax < c) ymax = c;
}
cachedBounds = new Rectangle2D.Double(xmin, ymin,
xmax - xmin,
ymax - ymin);
}
return cachedBounds;
}
public Rectangle getBounds() {
return getCachedBounds2D().getBounds();
return getBounds2D().getBounds();
}
public Rectangle2D getBounds2D() {
return getCachedBounds2D().getBounds2D();
return getTestShape().getBounds2D();
}
public boolean contains(double x, double y) {
return getTestShape().contains(x, y);
@ -1297,6 +1299,7 @@ public class UnitTest {
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);
@ -1312,6 +1315,93 @@ public class UnitTest {
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 static void testHits(Creator c) {
for (int i = 0; i < TestShapes.length; i++) {
Shape sref = TestShapes[i];