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