On Fri, 12 Nov 2021 05:47:12 GMT, Jeremy <d...@openjdk.java.net> wrote:

>> This removes code that relied on consulting the Bezier control points to 
>> calculate the Rectangle2D bounding box. Instead it's pretty straight-forward 
>> to convert the Bezier control points into the x & y parametric equations. At 
>> their most complex these equations are cubic polynomials, so calculating 
>> their extrema is just a matter of applying the quadratic formula to 
>> calculate their extrema. (Or in path segments that are 
>> quadratic/linear/constant: we do even less work.)
>> 
>> The bug writeup indicated they wanted Path2D#getBounds2D() to be more 
>> accurate/concise. They didn't explicitly say they wanted CubicCurve2D and 
>> QuadCurve2D to become more accurate too. But a preexisting unit test failed 
>> when Path2D#getBounds2D() was updated and those other classes weren't. At 
>> this point I considered either:
>> A. Updating CubicCurve2D and QuadCurve2D to use the new more accurate 
>> getBounds2D() or
>> B. Updating the unit test to forgive the discrepancy.
>> 
>> I chose A. Which might technically be seen as scope creep, but it feels like 
>> a more holistic/better approach.
>> 
>> Other shapes in java.awt.geom should not require updating, because they 
>> already identify concise bounds.
>> 
>> This also includes a new unit test (in Path2D/UnitTest.java) that fails 
>> without the changes in this commit.
>
> Jeremy has updated the pull request incrementally with three additional 
> commits since the last revision:
> 
>  - 8176501: Method Shape.getBounds2D() incorrectly includes Bezier control 
> points in bounding box
>    
>    This adds a new unit test that calculates a high-precision bounding box 
> (using BigDecimals), and then makes sure our double-based logic contains that 
> high-precision bounds.
>    
>    This restores getBounds2D() to its original contract: it should only ever 
> be *larger* than the actual bounds -- it should never be smaller.
>    
>    Also we want to only apply this margin (aka "padding") when we deal with 
> polynomial-based extrema. We should never apply it to line-based polygons. 
> For ex: a Path2D that represents an int-based rectangle should return the 
> same bounds as before 8176501 was addressed.
>    
>    This test currently only addresses very small cubic curves.
>    
>    I experimented with very large cubic & quadratic curves, but I didn't come 
> up with a unit test that failed before and after this commit. Adding unit 
> tests for large curve segments is a possible area of improvement.
>  - 8176501: Method Shape.getBounds2D() incorrectly includes Bezier control 
> points in bounding box
>    
>    Addressing code review comments: given current code structure we don't 
> need separate data structures for x and y equations.
>  - 8176501: Method Shape.getBounds2D() incorrectly includes Bezier control 
> points in bounding box
>    
>    Removing accidental leftover code. This should have been removed in a 
> recent previous commit. The preceding code already defines these values.

src/java.desktop/share/classes/java/awt/geom/Path2D.java line 2124:

> 2122:         // a box that is slightly too small. But the contract of this 
> method
> 2123:         // says we should err on the side of being too large.
> 2124:         // So to address this: we take the difference between the 
> control

This is my alternative proposal to use the polynomial error as base error 
(cubic case is more tricky as solveQuadratic is problematic too for huge 
curves):

        // So to address this: we take using the upper limit of numerical error
        // caused by the polynomial evaluation (horner scheme).

        for (; !pi.isDone(); pi.next()) {
            final int type = pi.currentSegment(coords);
            switch (type) {
                case PathIterator.SEG_MOVETO:
                    if (!started) {
                        started = true;
                        leftX = rightX = coords[0];
                        topY = bottomY = coords[1];
                    } else {
                        if (coords[0] < leftX) {
                            leftX = coords[0];
                        }
                        if (coords[0] > rightX) {
                            rightX = coords[0];
                        }
                        if (coords[1] < topY) {
                            topY = coords[1];
                        }
                        if (coords[1] > bottomY) {
                            bottomY = coords[1];
                        }
                    }
                    lastX = coords[0];
                    lastY = coords[1];
                    break;
                case PathIterator.SEG_LINETO:
                    if (coords[0] < leftX) {
                        leftX = coords[0];
                    }
                    if (coords[0] > rightX) {
                        rightX = coords[0];
                    }
                    if (coords[1] < topY) {
                        topY = coords[1];
                    }
                    if (coords[1] > bottomY) {
                        bottomY = coords[1];
                    }
                    lastX = coords[0];
                    lastY = coords[1];
                    break;
                case PathIterator.SEG_QUADTO:
                    if (coords[2] < leftX) {
                        leftX = coords[2];
                    }
                    if (coords[2] > rightX) {
                        rightX = coords[2];
                    }
                    if (coords[3] < topY) {
                        topY = coords[3];
                    }
                    if (coords[3] > bottomY) {
                        bottomY = coords[3];
                    }

                    if (coords[0] < leftX || coords[0] > rightX) {
                        final double dx21 = (coords[0] - lastX);
                        coeff[2] = (coords[2] - coords[0]) - dx21;  // A = P3 - 
P0 - 2 P2
                        coeff[1] = 2.0 * dx21;                      // B = 2 
(P2 - P1)
                        coeff[0] = lastX;                           // C = P1

                        deriv_coeff[0] = coeff[1];
                        deriv_coeff[1] = 2.0 * coeff[2];

                        double t = -deriv_coeff[0] / deriv_coeff[1];
                        if (t > 0.0 && t < 1.0) {
                            double x = 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 (x - margin < leftX) {
                                leftX = x - margin;
                            }
                            if (x + margin > rightX) {
                                rightX = x + margin;
                            }
                        }
                    }
                    if (coords[1] < topY || coords[1] > bottomY) {
                        final double dy21 = (coords[1] - lastY);
                        coeff[2] = (coords[3] - coords[1]) - dy21;
                        coeff[1] = 2.0 * dy21;
                        coeff[0] = lastY;

                        deriv_coeff[0] = coeff[1];
                        deriv_coeff[1] = 2.0 * coeff[2];

                        double t = -deriv_coeff[0] / deriv_coeff[1];
                        if (t > 0.0 && t < 1.0) {
                            double y = 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 (y - margin < topY) {
                                topY = y - margin;
                            }
                            if (y + margin > bottomY) {
                                bottomY = y + margin;
                            }
                        }
                    }
                    lastX = coords[2];
                    lastY = coords[3];
                    break;
                case PathIterator.SEG_CUBICTO:
                    if (coords[4] < leftX) {
                        leftX = coords[4];
                    }
                    if (coords[4] > rightX) {
                        rightX = coords[4];
                    }
                    if (coords[5] < topY) {
                        topY = coords[5];
                    }
                    if (coords[5] > bottomY) {
                        bottomY = coords[5];
                    }

                    if (coords[0] < leftX || coords[0] > rightX || coords[2] < 
leftX || coords[2] > rightX) {
                        final double dx32 = 3.0 * (coords[2] - coords[0]);
                        final double dx21 = 3.0 * (coords[0] - lastX);
                        coeff[3] = (coords[4] - lastX) - 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] = lastX;                       // D = P0

                        deriv_coeff[0] = coeff[1];
                        deriv_coeff[1] = 2.0 * coeff[2];
                        deriv_coeff[2] = 3.0 * coeff[3];

                        // 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) {
                                    double x = coeff[0] + t * (coeff[1] + t * 
(coeff[2] + t * coeff[3]));
                                    if (x - margin < leftX) {
                                        leftX = x - margin;
                                    }
                                    if (x + margin > rightX) {
                                        rightX = x + margin;
                                    }
                                }
                            }
                        }
                    }
                    if (coords[1] < topY || coords[1] > bottomY || coords[3] < 
topY || coords[3] > bottomY) {
                        final double dy32 = 3.0 * (coords[3] - coords[1]);
                        final double dy21 = 3.0 * (coords[1] - lastY);
                        coeff[3] = (coords[5] - lastY) - dy32;
                        coeff[2] = (dy32 - dy21);
                        coeff[1] = dy21;
                        coeff[0] = lastY;

                        deriv_coeff[0] = coeff[1];
                        deriv_coeff[1] = 2.0 * coeff[2];
                        deriv_coeff[2] = 3.0 * coeff[3];

                        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++) {
                                double t = tExtrema[i];
                                if (t > 0.0 && t < 1.0) {
                                    double y = coeff[0] + t * (coeff[1] + t * 
(coeff[2] + t * coeff[3]));
                                    if (y - margin < topY) {
                                        topY = y - margin;
                                    }
                                    if (y + margin > bottomY) {
                                        bottomY = y + margin;
                                    }
                                }
                            }
                        }
                    }
                    lastX = coords[4];
                    lastY = coords[5];
                    break;
                case PathIterator.SEG_CLOSE:
                default:
            }
        }

-------------

PR: https://git.openjdk.java.net/jdk/pull/6227

Reply via email to