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

263 lines
10 KiB
Java

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;
}
}