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