8a16842b4e
Reviewed-by: prr
263 lines
10 KiB
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;
|
|
}
|
|
} |