8176501: Method Shape.getBounds2D() incorrectly includes Bezier control points in bounding box
Reviewed-by: prr
This commit is contained in:
parent
05dac5a23e
commit
8a16842b4e
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;
|
||||
}
|
||||
|
263
test/jdk/java/awt/geom/Path2D/GetBounds2DPrecisionTest.java
Normal file
263
test/jdk/java/awt/geom/Path2D/GetBounds2DPrecisionTest.java
Normal file
@ -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];
|
||||
|
Loading…
x
Reference in New Issue
Block a user