changeset 5871:5e17e3810011

Fix RT-28691: Region corners wrong when corner radii overflow width or height Reviewed by: Felipe, David Grieve
author flar <James.Graham@oracle.com>
date Mon, 02 Dec 2013 14:54:15 -0800
parents d84e05a1e95b
children 1957002886ae
files modules/graphics/src/main/java/com/sun/javafx/sg/prism/NGRegion.java modules/graphics/src/main/java/javafx/scene/layout/Region.java
diffstat 2 files changed, 349 insertions(+), 175 deletions(-) [+]
line wrap: on
line diff
--- a/modules/graphics/src/main/java/com/sun/javafx/sg/prism/NGRegion.java	Mon Dec 02 14:39:01 2013 -0800
+++ b/modules/graphics/src/main/java/com/sun/javafx/sg/prism/NGRegion.java	Mon Dec 02 14:54:15 2013 -0800
@@ -128,6 +128,18 @@
     private Border border = Border.EMPTY;
 
     /**
+     * The normalized list of CornerRadii have been precomputed at the FX layer to
+     * properly account for percentages, insets and radii scaling to prevent
+     * the radii from overflowing the dimensions of the region.
+     * The List objects are shared with the FX layer and are therefore
+     * unmodifiable.  If the normalized list is null then it means that all
+     * of the raw radii in the list were already absolute and non-overflowing
+     * and so the originals can be used from the arrays of strokes and fills.
+     */
+    private List<CornerRadii> normalizedFillCorners;
+    private List<CornerRadii> normalizedStrokeCorners;
+
+    /**
      * The shape of the region. Usually this will be null (except for things like check box
      * checks, scroll bar down arrows / up arrows, etc). If this is not null, it determines
      * the shape of the region to draw. If it is null, then the assumed shape of the region is
@@ -278,6 +290,38 @@
     }
 
     /**
+     * Called by the Region when any parameters are changed.
+     * It is only technically needed when a parameter that affects the size
+     * of any percentage or overflowing corner radii is changed, but since
+     * the data is not processed here in NGRegion, it is set on every update
+     * of the peers for any reason.
+     * A null value means that the raw radii in the BorderStroke objects
+     * themselves were already absolute and non-overflowing.
+     * 
+     * @param normalizedStrokeCorners a precomputed copy of the radii in the
+     *        BorderStroke objects that are not percentages and do not overflow
+     */
+    public void updateStrokeCorners(List<CornerRadii> normalizedStrokeCorners) {
+        this.normalizedStrokeCorners = normalizedStrokeCorners;
+    }
+
+    /**
+     * Returns the normalized (non-percentage, non-overflowing) radii for the
+     * selected index into the BorderStroke objects.
+     * If a List was synchronized from the Region object, the value from that
+     * List, otherwise the raw radii are fetched from the indicated BorderStroke
+     * object.
+     * 
+     * @param index the index of the BorderStroke object being processed
+     * @return the normalized radii for the indicated BorderStroke object
+     */
+    private CornerRadii getNormalizedStrokeRadii(int index) {
+        return (normalizedStrokeCorners == null
+                ? border.getStrokes().get(index).getRadii()
+                : normalizedStrokeCorners.get(index));
+    }
+
+    /**
      * Called by the Region when the Background has changed. The Region *must* only call
      * this method if the background object has actually changed, or excessive work may be done.
      *
@@ -336,6 +380,38 @@
     }
 
     /**
+     * Called by the Region when any parameters are changed.
+     * It is only technically needed when a parameter that affects the size
+     * of any percentage or overflowing corner radii is changed, but since
+     * the data is not processed here in NGRegion, it is set on every update
+     * of the peers for any reason.
+     * A null value means that the raw radii in the BackgroundFill objects
+     * themselves were already absolute and non-overflowing.
+     * 
+     * @param normalizedStrokeCorners a precomputed copy of the radii in the
+     *        BackgroundFill objects that are not percentages and do not overflow
+     */
+    public void updateFillCorners(List<CornerRadii> normalizedFillCorners) {
+        this.normalizedFillCorners = normalizedFillCorners;
+    }
+
+    /**
+     * Returns the normalized (non-percentage, non-overflowing) radii for the
+     * selected index into the BackgroundFill objects.
+     * If a List was synchronized from the Region object, the value from that
+     * List, otherwise the raw radii are fetched from the indicated BackgroundFill
+     * object.
+     * 
+     * @param index the index of the BackgroundFill object being processed
+     * @return the normalized radii for the indicated BackgroundFill object
+     */
+    private CornerRadii getNormalizedFillRadii(int index) {
+        return (normalizedFillCorners == null
+                ? background.getFills().get(index).getRadii()
+                : normalizedFillCorners.get(index));
+    }
+
+    /**
      * Called by the Region whenever it knows that the opaque insets have changed. The
      * Region <strong>must</strong> make sure that these opaque insets include the opaque
      * inset information from the Border and Background as well, the NGRegion will not
@@ -1009,7 +1085,7 @@
                 // Could optimize this such that if paint is transparent then we go no further.
                 final Paint paint = getPlatformPaint(fill.getFill());
                 g.setPaint(paint);
-                final CornerRadii radii = fill.getRadii();
+                final CornerRadii radii = getNormalizedFillRadii(i);
                 // This is a workaround for RT-28435 so we use path rasterizer for small radius's We are
                 // keeping old rendering. We do not apply workaround when using Caspian or Embedded
                 if (radii.isUniform() &&
@@ -1022,9 +1098,6 @@
                         // The edges are square, so we can do a simple fill rect
                         g.fillRect(l, t, w, h);
                     } else {
-                        // Fix the horizontal and vertical radii if they are percentage based
-                        if (radii.isTopLeftHorizontalRadiusAsPercentage()) tlhr = tlhr * width;
-                        if (radii.isTopLeftVerticalRadiusAsPercentage()) tlvr = tlvr * height;
                         // The edges are rounded, so we need to compute the arc width and arc height
                         // and fill a round rect
                         float arcWidth = tlhr + tlhr;
@@ -1045,7 +1118,7 @@
                     // TODO document the issue number which will give us a fast path for rendering
                     // non-uniform corners, and that we want to implement that instead of createPath2
                     // below in such cases. (RT-26979)
-                    g.fill(createPath(width, height, t, l, b, r, normalize(radii)));
+                    g.fill(createPath(width, height, t, l, b, r, radii));
                 }
             }
         }
@@ -1057,7 +1130,7 @@
         for (int i = 0, max = strokes.size(); i < max; i++) {
             final BorderStroke stroke = strokes.get(i);
             final BorderWidths widths = stroke.getWidths();
-            final CornerRadii radii = normalize(stroke.getRadii());
+            final CornerRadii radii = getNormalizedStrokeRadii(i);
             final Insets insets = stroke.getInsets();
 
             final javafx.scene.paint.Paint topStroke = stroke.getTopStroke();
@@ -1357,7 +1430,7 @@
     }
 
     /**
-     * Visits each of the background fills and takes their raddi into account to determine the insets.
+     * Visits each of the background fills and takes their radii into account to determine the insets.
      * The backgroundInsets variable is cleared whenever the fills change, or whenever the size of the
      * region has changed (because if the size of the region changed and a radius is percentage based
      * then we need to recompute the insets).
@@ -1372,7 +1445,7 @@
             // (well, only deadly to a shape if it turns out to be a writable image).
             final BackgroundFill fill = fills.get(i);
             final Insets insets = fill.getInsets();
-            final CornerRadii radii = normalize(fill.getRadii());
+            final CornerRadii radii = getNormalizedFillRadii(i);
             top = (float) Math.max(top, insets.getTop() + Math.max(radii.getTopLeftVerticalRadius(), radii.getTopRightVerticalRadius()));
             right = (float) Math.max(right, insets.getRight() + Math.max(radii.getTopRightHorizontalRadius(), radii.getBottomRightHorizontalRadius()));
             bottom = (float) Math.max(bottom, insets.getBottom() + Math.max(radii.getBottomRightVerticalRadius(), radii.getBottomLeftVerticalRadius()));
@@ -1396,18 +1469,6 @@
         return (d - (int)d) == 0 ? (int) d : (int) (d + 1);
     }
 
-    private CornerRadii normalize(CornerRadii radii) {
-        final double tlvr = radii.isTopLeftVerticalRadiusAsPercentage() ? height * radii.getTopLeftVerticalRadius() : radii.getTopLeftVerticalRadius();
-        final double tlhr = radii.isTopLeftHorizontalRadiusAsPercentage() ? width * radii.getTopLeftHorizontalRadius() : radii.getTopLeftHorizontalRadius();
-        final double trvr = radii.isTopRightVerticalRadiusAsPercentage() ? height * radii.getTopRightVerticalRadius() : radii.getTopRightVerticalRadius();
-        final double trhr = radii.isTopRightHorizontalRadiusAsPercentage() ? width * radii.getTopRightHorizontalRadius() : radii.getTopRightHorizontalRadius();
-        final double brvr = radii.isBottomRightVerticalRadiusAsPercentage() ? height * radii.getBottomRightVerticalRadius() : radii.getBottomRightVerticalRadius();
-        final double brhr = radii.isBottomRightHorizontalRadiusAsPercentage() ? width * radii.getBottomRightHorizontalRadius() : radii.getBottomRightHorizontalRadius();
-        final double blvr = radii.isBottomLeftVerticalRadiusAsPercentage() ? height * radii.getBottomLeftVerticalRadius() : radii.getBottomLeftVerticalRadius();
-        final double blhr = radii.isBottomLeftHorizontalRadiusAsPercentage() ? width * radii.getBottomLeftHorizontalRadius() : radii.getBottomLeftHorizontalRadius();
-        return new CornerRadii(tlhr, tlvr, trvr, trhr, brhr, brvr, blvr, blhr, false, false, false, false, false, false, false, false);
-    }
-
     /**
      * Creates a Prism BasicStroke based on the stroke style, width, and line length.
      *
@@ -1541,52 +1602,79 @@
         g.setPaint(sbFill);
     }
 
-    // If we generate the coordinates for the "start point, corner, end point"
-    // triplets for each corner arc on the border going clockwise from the
-    // upper left, we get the following pattern (X, Y == corner coords):
-    //
-    // 0 - Top Left:      X + 0, Y + R,      X, Y,      X + R, Y + 0
-    // 1 - Top Right:     X - R, Y + 0,      X, Y,      X + 0, Y + R
-    // 2 - Bottom Right:  X + 0, Y - R,      X, Y,      X - R, Y + 0
-    // 3 - Bottom Left:   X + R, Y + 0,      X, Y,      X + 0, Y - R
-    //
-    // The start and end points are just the corner coordinate + {-R, 0, +R}.
-    // If we view these four lines as the following line with appropriate
-    // values for A, B, C, D:
-    //
-    //     General form:  X + A, Y + B,      X, Y,      X + C, Y + D
-    //
-    // We see that C == B and D == -A in every case so we really only have
-    // 2 constants and the following reduced general form:
-    //
-    //     Reduced form:  X + A, Y + B,      X, Y,      X + B, Y - A
-    //
-    // You might note that these values are actually related to the sin
-    // and cos of 90 degree angles and the relationship between (A,B) and (C,D)
-    // is just that of a 90 degree rotation.  We can thus use the following
-    // trigonometric "quick lookup" array and the relationships:
-    //
-    // 1. cos(quadrant) == sin(quadrant + 1)
-    // 2. dx,dy for the end point
-    //      == dx,dy for the start point + 90 degrees
-    //      == dy,-dx
-    //
-    // Note that the array goes through 6 quadrants to allow us to look
-    // 2 quadrants past a complete circle.  We need to go one quadrant past
-    // so that we can compute cos(4th quadrant) == sin(5th quadrant) and we
-    // also need to allow one more quadrant because the makeRoundedEdge
-    // method always computes 2 consecutive rounded corners at a time.
-    private static final float SIN_VALUES[] = { 1f, 0f, -1f, 0f, 1f, 0f};
-
-    private void doCorner(Path2D path, float x, float y, float r, int quadrant) {
-        if (r > 0) {
-            float dx = r * SIN_VALUES[quadrant + 1]; // cos(quadrant)
-            float dy = r * SIN_VALUES[quadrant];
-            path.appendOvalQuadrant(x + dx, y + dy, x, y, x + dy, y - dx, 0f, 1f,
-                                    (quadrant == 0)
+    /**
+     * Inserts geometry into the specified Path2D object for the specified
+     * corner of a general rounded rectangle.
+     * 
+     * The corner drawn is specified by the quadrant parameter, whose least
+     * significant 2 bits specify the following corners and the associated
+     * start, corner, and end points (which are always drawn clockwise):
+     * 
+     * 0 - Top Left:      X + 0 , Y + VR,      X, Y,      X + HR, Y + 0
+     * 1 - Top Right:     X - HR, Y + 0 ,      X, Y,      X + 0 , Y + VR
+     * 2 - Bottom Right:  X + 0 , Y - VR,      X, Y,      X - HR, Y + 0
+     * 3 - Bottom Left:   X + HR, Y + 0 ,      X, Y,      X + 0 , Y - VR
+     * 
+     * The associated horizontal and vertical radii are fetched from the
+     * indicated CornerRadii object which is assumed to be absolute (not
+     * percentage based) and already scaled so that no pair of radii are
+     * larger than the indicated width/height of the rounded rectangle being
+     * expressed.
+     * 
+     * The tstart and tend parameters specify what portion of the rounded
+     * corner should be drawn with 0f => 1f being the entire rounded corner.
+     * 
+     * The newPath parameter indicates whether the path should reach the
+     * starting point with a moveTo() command or a lineTo() segment.
+     * 
+     * @param path
+     * @param radii
+     * @param x
+     * @param y
+     * @param quadrant
+     * @param tstart
+     * @param tend
+     * @param newPath 
+     */
+    private void doCorner(Path2D path, CornerRadii radii,
+                          float x, float y, int quadrant,
+                          float tstart, float tend, boolean newPath)
+    {
+        float dx0, dy0, dx1, dy1;
+        float hr, vr;
+        switch (quadrant & 0x3) {
+            case 0:
+                hr = (float) radii.getTopLeftHorizontalRadius();
+                vr = (float) radii.getTopLeftVerticalRadius();
+                // 0 - Top Left:      X + 0 , Y + VR,      X, Y,      X + HR, Y + 0
+                dx0 =  0f;  dy0 =  vr;    dx1 =  hr;  dy1 =  0f;
+                break;
+            case 1:
+                hr = (float) radii.getTopRightHorizontalRadius();
+                vr = (float) radii.getTopRightVerticalRadius();
+                // 1 - Top Right:     X - HR, Y + 0 ,      X, Y,      X + 0 , Y + VR
+                dx0 = -hr;  dy0 =  0f;    dx1 =  0f;  dy1 =  vr;
+                break;
+            case 2:
+                hr = (float) radii.getBottomRightHorizontalRadius();
+                vr = (float) radii.getBottomRightVerticalRadius();
+                // 2 - Bottom Right:  X + 0 , Y - VR,      X, Y,      X - HR, Y + 0
+                dx0 =  0f;  dy0 = -vr;    dx1 = -hr;  dy1 = 0f;
+                break;
+            case 3:
+                hr = (float) radii.getBottomLeftHorizontalRadius();
+                vr = (float) radii.getBottomLeftVerticalRadius();
+                // 3 - Bottom Left:   X + HR, Y + 0 ,      X, Y,      X + 0 , Y - VR
+                dx0 =  hr;  dy0 =  0f;    dx1 =  0f;  dy1 = -vr;
+                break;
+            default: return; // Can never happen
+        }
+        if (hr > 0 && vr > 0) {
+            path.appendOvalQuadrant(x + dx0, y + dy0, x, y, x + dx1, y + dy1, tstart, tend,
+                                    (newPath)
                                         ? Path2D.CornerPrefix.MOVE_THEN_CORNER
                                         : Path2D.CornerPrefix.LINE_THEN_CORNER);
-        } else if (quadrant == 0) {
+        } else if (newPath) {
             path.moveTo(x, y);
         } else {
             path.lineTo(x, y);
@@ -1600,47 +1688,22 @@
     private Path2D createPath(float width, float height, float t, float l, float bo, float ro, CornerRadii radii) {
         float r = width - ro;
         float b = height - bo;
-        // TODO have to teach this method how to handle vertical radii (RT-26941)
-        float tlr = (float) radii.getTopLeftHorizontalRadius();
-        float trr = (float) radii.getTopRightHorizontalRadius();
-        float blr = (float) radii.getBottomLeftHorizontalRadius();
-        float brr = (float) radii.getBottomRightHorizontalRadius();
-        float ratio = getReducingRatio(r - l, b - t, tlr, trr, blr, brr);
-        if (ratio < 1.0f) {
-            tlr *= ratio;
-            trr *= ratio;
-            blr *= ratio;
-            brr *= ratio;
-        }
         Path2D path = new Path2D();
-        doCorner(path, l, t, tlr, 0);
-        doCorner(path, r, t, trr, 1);
-        doCorner(path, r, b, brr, 2);
-        doCorner(path, l, b, blr, 3);
+        doCorner(path, radii, l, t, 0, 0f, 1f, true);
+        doCorner(path, radii, r, t, 1, 0f, 1f, false);
+        doCorner(path, radii, r, b, 2, 0f, 1f, false);
+        doCorner(path, radii, l, b, 3, 0f, 1f, false);
         path.closePath();
         return path;
     }
 
-    private Path2D makeRoundedEdge(float x0, float y0, float x1, float y1,
-                                   float r0, float r1, int quadrant)
+    private Path2D makeRoundedEdge(CornerRadii radii,
+                                   float x0, float y0, float x1, float y1,
+                                   int quadrant)
     {
         Path2D path = new Path2D();
-        if (r0 > 0) {
-            float dx = r0 * SIN_VALUES[quadrant + 1];  // cos(quadrant)
-            float dy = r0 * SIN_VALUES[quadrant];
-            path.appendOvalQuadrant(x0 + dx, y0 + dy, x0, y0, x0 + dy, y0 - dx,
-                                    0.5f, 1f, Path2D.CornerPrefix.MOVE_THEN_CORNER);
-        } else {
-            path.moveTo(x0, y0);
-        }
-        if (r1 > 0) {
-            float dx = r1 * SIN_VALUES[quadrant + 2];  // cos(quadrant + 1)
-            float dy = r1 * SIN_VALUES[quadrant + 1];
-            path.appendOvalQuadrant(x1 + dx, y1 + dy, x1, y1, x1 + dy, y1 - dx,
-                                    0f, 0.5f, Path2D.CornerPrefix.LINE_THEN_CORNER);
-        } else {
-            path.lineTo(x1, y1);
-        }
+        doCorner(path, radii, x0, y0, quadrant,   0.5f, 1.0f, true);
+        doCorner(path, radii, x1, y1, quadrant+1, 0.0f, 0.5f, false);
         return path;
     }
 
@@ -1651,25 +1714,13 @@
      */
     private Path2D[] createPaths(float t, float l, float bo, float ro, CornerRadii radii)
     {
-        // TODO have to teach how to handle the other 4 radii (RT-26941)
-        float tlr = (float) radii.getTopLeftHorizontalRadius(),
-            trr = (float) radii.getTopRightHorizontalRadius(),
-            blr = (float) radii.getBottomLeftHorizontalRadius(),
-            brr = (float) radii.getBottomRightHorizontalRadius();
         float r = width - ro;
         float b = height - bo;
-        float ratio = getReducingRatio(r - l, b - t, tlr, trr, blr, brr);
-        if (ratio < 1.0f) {
-            tlr *= ratio;
-            trr *= ratio;
-            blr *= ratio;
-            brr *= ratio;
-        }
         return new Path2D[] {
-            makeRoundedEdge(l, t, r, t, tlr, trr, 0), // top
-            makeRoundedEdge(r, t, r, b, trr, brr, 1), // right
-            makeRoundedEdge(r, b, l, b, brr, blr, 2), // bottom
-            makeRoundedEdge(l, b, l, t, blr, tlr, 3), // left
+            makeRoundedEdge(radii, l, t, r, t, 0), // top
+            makeRoundedEdge(radii, r, t, r, b, 1), // right
+            makeRoundedEdge(radii, r, b, l, b, 2), // bottom
+            makeRoundedEdge(radii, l, b, l, t, 3), // left
         };
     }
 
@@ -1924,26 +1975,4 @@
             texture.unlock();
         }
     }
-
-    private float getReducingRatio(float w, float h,
-                                   float tlr, float trr,
-                                   float blr, float brr)
-    {
-        float ratio = 1.0f;
-        // working clockwise TRBL
-        if (tlr + trr > w) { // top radii
-            ratio = Math.min(ratio, w / (tlr + trr));
-        }
-        if (trr + brr > h) { // right radii
-            ratio = Math.min(ratio, h / (trr + brr));
-        }
-        if (brr + blr > w) { // bottom radii
-            ratio = Math.min(ratio, w / (brr + blr));
-        }
-        if (blr + tlr > h) { // left radii
-            ratio = Math.min(ratio, h / (blr + tlr));
-        }
-        return ratio;
-    }
-
 }
--- a/modules/graphics/src/main/java/javafx/scene/layout/Region.java	Mon Dec 02 14:39:01 2013 -0800
+++ b/modules/graphics/src/main/java/javafx/scene/layout/Region.java	Mon Dec 02 14:54:15 2013 -0800
@@ -55,6 +55,7 @@
 import javafx.util.Callback;
 import java.util.ArrayList;
 import java.util.Collections;
+import java.util.Arrays;
 import java.util.List;
 import java.util.function.Function;
 import com.sun.javafx.Logging;
@@ -66,12 +67,9 @@
 import com.sun.javafx.css.converters.SizeConverter;
 import com.sun.javafx.geom.BaseBounds;
 import com.sun.javafx.geom.PickRay;
-import com.sun.javafx.geom.Point2D;
 import com.sun.javafx.geom.RectBounds;
 import com.sun.javafx.geom.Vec2d;
-import com.sun.javafx.geom.transform.Affine2D;
 import com.sun.javafx.geom.transform.BaseTransform;
-import com.sun.javafx.geom.transform.NoninvertibleTransformException;
 import com.sun.javafx.scene.DirtyBits;
 import com.sun.javafx.scene.input.PickResultChooser;
 import com.sun.javafx.sg.prism.NGNode;
@@ -422,7 +420,6 @@
         return array;
     }
 
-
     /***************************************************************************
      *                                                                         *
      * Constructors                                                            *
@@ -616,6 +613,7 @@
 
                 // No matter what, the fill has changed, so we have to update it
                 impl_markDirty(DirtyBits.SHAPE_FILL);
+                cornersValid = false;
                 old = b;
             }
         }
@@ -672,6 +670,7 @@
 
                 // No matter what, the fill has changed, so we have to update it
                 impl_markDirty(DirtyBits.SHAPE_STROKE);
+                cornersValid = false;
                 old = b;
             }
         }
@@ -870,6 +869,7 @@
         // unnecessary bounds changes, however, is relatively high.
         if (value != _width) {
             _width = value;
+            cornersValid = false;
             boundingBox = null;
             impl_layoutBoundsChanged();
             impl_geomChanged();
@@ -923,6 +923,7 @@
     private void heightChanged(double value) {
         if (_height != value) {
             _height = value;
+            cornersValid = false;
             // It is possible that somebody sets the height of the region to a value which
             // it previously held. If this is the case, we want to avoid excessive layouts.
             // Note that I have biased this for layout over binding, because the heightProperty
@@ -2370,6 +2371,10 @@
         if (_shape != null) _shape.impl_syncPeer();
         NGRegion pg = impl_getPeer();
 
+        if (!cornersValid) {
+            validateCorners();
+        }
+
         final boolean sizeChanged = impl_isDirty(DirtyBits.NODE_GEOMETRY);
         if (sizeChanged) {
             pg.setSize((float)getWidth(), (float)getHeight());
@@ -2383,6 +2388,9 @@
             pg.updateShape(_shape, isScaleShape(), isCenterShape(), isCacheShape());
         }
 
+        // The normalized corners can always be updated since they require no
+        // processing at the NG layer.
+        pg.updateFillCorners(normalizedFillCorners);
         final boolean backgroundChanged = impl_isDirty(DirtyBits.SHAPE_FILL);
         final Background bg = getBackground();
         if (backgroundChanged) {
@@ -2395,6 +2403,9 @@
             pg.imagesUpdated();
         }
 
+        // The normalized corners can always be updated since they require no
+        // processing at the NG layer.
+        pg.updateStrokeCorners(normalizedStrokeCorners);
         if (impl_isDirty(DirtyBits.SHAPE_STROKE)) {
             pg.updateBorder(getBorder());
         }
@@ -2461,7 +2472,7 @@
     @Override public NGNode impl_createPeer() {
         return new NGRegion();
     }
-    
+
     /**
      * Transform x, y in local Region coordinates to local coordinates of scaled/centered shape and
      * check if the shape contains the coordinates.
@@ -2559,8 +2570,6 @@
 
         final double x2 = getWidth();
         final double y2 = getHeight();
-        // Figure out what the maximum possible radius value is.
-        final double maxRadius = Math.min(x2 / 2.0, y2 / 2.0);
 
         final Background background = getBackground();
         // First check the shape. Shape could be impacted by scaleShape & positionShape properties.
@@ -2590,7 +2599,7 @@
             final List<BackgroundFill> fills = background.getFills();
             for (int i = 0, max = fills.size(); i < max; i++) {
                 final BackgroundFill bgFill = fills.get(i);
-                if (contains(localX, localY, 0, 0, x2, y2, bgFill.getInsets(), bgFill.getRadii(), maxRadius)) {
+                if (contains(localX, localY, 0, 0, x2, y2, bgFill.getInsets(), getNormalizedFillCorner(i))) {
                     return true;
                 }
             }
@@ -2607,7 +2616,7 @@
             for (int i=0, max=strokes.size(); i<max; i++) {
                 final BorderStroke strokeBorder = strokes.get(i);
                 if (contains(localX, localY, 0, 0, x2, y2, strokeBorder.getWidths(), false, strokeBorder.getInsets(),
-                             strokeBorder.getRadii(), maxRadius)) {
+                             getNormalizedStrokeCorner(i))) {
                     return true;
                 }
             }
@@ -2617,7 +2626,7 @@
             for (int i = 0, max = images.size(); i < max; i++) {
                 final BorderImage borderImage = images.get(i);
                 if (contains(localX, localY, 0, 0, x2, y2, borderImage.getWidths(), borderImage.isFilled(),
-                             borderImage.getInsets(), CornerRadii.EMPTY, maxRadius)) {
+                             borderImage.getInsets(), CornerRadii.EMPTY)) {
                     return true;
                 }
             }
@@ -2646,20 +2655,20 @@
     private boolean contains(final double px, final double py,
                              final double x1, final double y1, final double x2, final double y2,
                              BorderWidths widths, boolean filled,
-                             final Insets insets, final CornerRadii rad, final double maxRadius) {
+                             final Insets insets, final CornerRadii rad) {
         if (filled) {
-            if (contains(px, py, x1, y1, x2, y2, insets, rad, maxRadius)) {
+            if (contains(px, py, x1, y1, x2, y2, insets, rad)) {
                 return true;
             }
         } else {
-            boolean insideOuterEdge = contains(px, py, x1, y1, x2, y2, insets, rad, maxRadius);
+            boolean insideOuterEdge = contains(px, py, x1, y1, x2, y2, insets, rad);
             if (insideOuterEdge) {
                 boolean outsideInnerEdge = !contains(px, py,
                     x1 + (widths.isLeftAsPercentage() ? getWidth() * widths.getLeft() : widths.getLeft()),
                     y1 + (widths.isTopAsPercentage() ? getHeight() * widths.getTop() : widths.getTop()),
                     x2 - (widths.isRightAsPercentage() ? getWidth() * widths.getRight() : widths.getRight()),
                     y2 - (widths.isBottomAsPercentage() ? getHeight() * widths.getBottom() : widths.getBottom()),
-                    insets, rad, maxRadius);
+                    insets, rad);
                 if (outsideInnerEdge) return true;
             }
         }
@@ -2683,7 +2692,7 @@
      */
     private boolean contains(final double px, final double py,
                              final double x1, final double y1, final double x2, final double y2,
-                             final Insets insets, CornerRadii rad, final double maxRadius) {
+                             final Insets insets, CornerRadii rad) {
         // These four values are the x0, y0, x1, y1 bounding box after
         // having taken into account the insets of this particular
         // background fill.
@@ -2692,27 +2701,26 @@
         final double rrx1 = x2 - insets.getRight();
         final double rry1 = y2 - insets.getBottom();
 
-        // Adjust based on whether it is % based radii
-        rad = normalize(rad);
+//        assert rad.hasPercentBasedRadii == false;
 
         // Check for trivial rejection - point is inside bounding rectangle
         if (px >= rrx0 && py >= rry0 && px <= rrx1 && py <= rry1) {
             // The point was within the index bounding box. Now we need to analyze the
             // corner radii to see if the point lies within the corners or not. If the
             // point is within a corner then we reject this one.
-            final double tlhr = Math.min(rad.getTopLeftHorizontalRadius(), maxRadius);
+            final double tlhr = rad.getTopLeftHorizontalRadius();
             if (rad.isUniform() && tlhr == 0) {
                 // This is a simple square! Since we know the point is already within
                 // the insets of this fill, we can simply return true.
                 return true;
             } else {
-                final double tlvr = Math.min(rad.getTopLeftVerticalRadius(), maxRadius);
-                final double trhr = Math.min(rad.getTopRightHorizontalRadius(), maxRadius);
-                final double trvr = Math.min(rad.getTopRightVerticalRadius(), maxRadius);
-                final double blhr = Math.min(rad.getBottomLeftHorizontalRadius(), maxRadius);
-                final double blvr = Math.min(rad.getBottomLeftVerticalRadius(), maxRadius);
-                final double brhr = Math.min(rad.getBottomRightHorizontalRadius(), maxRadius);
-                final double brvr = Math.min(rad.getBottomRightVerticalRadius(), maxRadius);
+                final double tlvr = rad.getTopLeftVerticalRadius();
+                final double trhr = rad.getTopRightHorizontalRadius();
+                final double trvr = rad.getTopRightVerticalRadius();
+                final double blhr = rad.getBottomLeftHorizontalRadius();
+                final double blvr = rad.getBottomLeftVerticalRadius();
+                final double brhr = rad.getBottomRightHorizontalRadius();
+                final double brvr = rad.getBottomRightVerticalRadius();
 
                 // The four corners can each be described as a quarter of an ellipse
                 double centerX, centerY, a, b;
@@ -2756,25 +2764,162 @@
         return false;
     }
 
+    /*
+     * The normalized corner radii are unmodifiable List objects shared between
+     * the NG layer and the FX layer.  As cached shadow copies of the objects
+     * in the BackgroundFill and BorderStroke objects they should be considered
+     * read-only and will only be updated by replacing the original objects
+     * when validation is needed.
+     */
+    private boolean cornersValid; // = false
+    private List<CornerRadii> normalizedFillCorners; // = null
+    private List<CornerRadii> normalizedStrokeCorners; // = null
+
     /**
-     * Direct copy of a method in NGRegion. If NGRegion were part of the core graphics module (coming!)
-     * then this method could be removed.
+     * Returns the normalized absolute radii for the indicated BackgroundFill,
+     * taking the current size of the region into account to eliminate any
+     * percentage-based measurements and to scale the radii to prevent
+     * overflowing the width or height.
+     * 
+     * @param i the index of the BackgroundFill whose radii will be normalized.
+     * @return the normalized (non-percentage, non-overflowing) radii
+     */
+    private CornerRadii getNormalizedFillCorner(int i) {
+        if (!cornersValid) {
+            validateCorners();
+        }
+        return (normalizedFillCorners == null
+                ? getBackground().getFills().get(i).getRadii()
+                : normalizedFillCorners.get(i));
+    }
+
+    /**
+     * Returns the normalized absolute radii for the indicated BorderStroke,
+     * taking the current size of the region into account to eliminate any
+     * percentage-based measurements and to scale the radii to prevent
+     * overflowing the width or height.
+     * 
+     * @param i the index of the BorderStroke whose radii will be normalized.
+     * @return the normalized (non-percentage, non-overflowing) radii
+     */
+    private CornerRadii getNormalizedStrokeCorner(int i) {
+        if (!cornersValid) {
+            validateCorners();
+        }
+        return (normalizedStrokeCorners == null
+                ? getBorder().getStrokes().get(i).getRadii()
+                : normalizedStrokeCorners.get(i));
+    }
+
+    /**
+     * This method validates all CornerRadii objects in both the set of
+     * BackgroundFills and BorderStrokes and saves the normalized values
+     * into the private fields above.
+     */
+    private void validateCorners() {
+        final double width = getWidth();
+        final double height = getHeight();
+        List<CornerRadii> newFillCorners = null;
+        List<CornerRadii> newStrokeCorners = null;
+        final Background background = getBackground();
+        final List<BackgroundFill> fills = background == null ? Collections.EMPTY_LIST : background.getFills();
+        for (int i = 0; i < fills.size(); i++) {
+            final BackgroundFill fill = fills.get(i);
+            final CornerRadii origRadii = fill.getRadii();
+            final Insets origInsets = fill.getInsets();
+            final CornerRadii newRadii = normalize(origRadii, origInsets, width, height);
+            if (origRadii != newRadii) {
+                if (newFillCorners == null) {
+                    newFillCorners = Arrays.asList(new CornerRadii[fills.size()]);
+                }
+                newFillCorners.set(i, newRadii);
+            }
+        }
+        final Border border = getBorder();
+        final List<BorderStroke> strokes = (border == null ? Collections.EMPTY_LIST : border.getStrokes());
+        for (int i = 0; i < strokes.size(); i++) {
+            final BorderStroke stroke = strokes.get(i);
+            final CornerRadii origRadii = stroke.getRadii();
+            final Insets origInsets = stroke.getInsets();
+            final CornerRadii newRadii = normalize(origRadii, origInsets, width, height);
+            if (origRadii != newRadii) {
+                if (newStrokeCorners == null) {
+                    newStrokeCorners = Arrays.asList(new CornerRadii[strokes.size()]);
+                }
+                newStrokeCorners.set(i, newRadii);
+            }
+        }
+        if (newFillCorners != null) {
+            for (int i = 0; i < fills.size(); i++) {
+                if (newFillCorners.get(i) == null) {
+                    newFillCorners.set(i, fills.get(i).getRadii());
+                }
+            }
+            newFillCorners = Collections.unmodifiableList(newFillCorners);
+        }
+        if (newStrokeCorners != null) {
+            for (int i = 0; i < strokes.size(); i++) {
+                if (newStrokeCorners.get(i) == null) {
+                    newStrokeCorners.set(i, strokes.get(i).getRadii());
+                }
+            }
+            newStrokeCorners = Collections.unmodifiableList(newStrokeCorners);
+        }
+        normalizedFillCorners = newFillCorners;
+        normalizedStrokeCorners = newStrokeCorners;
+        cornersValid = true;
+    }
+
+    /**
+     * Return a version of the radii that is not percentage based and is scaled to
+     * fit the indicated inset rectangle without overflow.
+     * This method may return the original CornerRadii if none of the radii
+     * values in the given object are percentages or require scaling.
      *
      * @param radii    The radii.
+     * @param insets   The insets for the associated background or stroke.
+     * @param width    The width of the region before insets are applied.
+     * @param height   The height of the region before insets are applied.
      * @return Normalized radii.
      */
-    private CornerRadii normalize(CornerRadii radii) {
-        final double width = getWidth();
-        final double height = getHeight();
-        final double tlvr = radii.isTopLeftVerticalRadiusAsPercentage() ? height * radii.getTopLeftVerticalRadius() : radii.getTopLeftVerticalRadius();
-        final double tlhr = radii.isTopLeftHorizontalRadiusAsPercentage() ? width * radii.getTopLeftHorizontalRadius() : radii.getTopLeftHorizontalRadius();
-        final double trvr = radii.isTopRightVerticalRadiusAsPercentage() ? height * radii.getTopRightVerticalRadius() : radii.getTopRightVerticalRadius();
-        final double trhr = radii.isTopRightHorizontalRadiusAsPercentage() ? width * radii.getTopRightHorizontalRadius() : radii.getTopRightHorizontalRadius();
-        final double brvr = radii.isBottomRightVerticalRadiusAsPercentage() ? height * radii.getBottomRightVerticalRadius() : radii.getBottomRightVerticalRadius();
-        final double brhr = radii.isBottomRightHorizontalRadiusAsPercentage() ? width * radii.getBottomRightHorizontalRadius() : radii.getBottomRightHorizontalRadius();
-        final double blvr = radii.isBottomLeftVerticalRadiusAsPercentage() ? height * radii.getBottomLeftVerticalRadius() : radii.getBottomLeftVerticalRadius();
-        final double blhr = radii.isBottomLeftHorizontalRadiusAsPercentage() ? width * radii.getBottomLeftHorizontalRadius() : radii.getBottomLeftHorizontalRadius();
-        return new CornerRadii(tlhr, tlvr, trvr, trhr, brhr, brvr, blvr, blhr, false, false, false, false, false, false, false, false);
+    private static CornerRadii normalize(CornerRadii radii, Insets insets, double width, double height) {
+        width  -= insets.getLeft() + insets.getRight();
+        height -= insets.getTop() + insets.getBottom();
+        if (width <= 0 || height <= 0) return CornerRadii.EMPTY;
+        double tlvr = radii.getTopLeftVerticalRadius();
+        double tlhr = radii.getTopLeftHorizontalRadius();
+        double trvr = radii.getTopRightVerticalRadius();
+        double trhr = radii.getTopRightHorizontalRadius();
+        double brvr = radii.getBottomRightVerticalRadius();
+        double brhr = radii.getBottomRightHorizontalRadius();
+        double blvr = radii.getBottomLeftVerticalRadius();
+        double blhr = radii.getBottomLeftHorizontalRadius();
+        if (radii.hasPercentBasedRadii) {
+            if (radii.isTopLeftVerticalRadiusAsPercentage())       tlvr *= height;
+            if (radii.isTopLeftHorizontalRadiusAsPercentage())     tlhr *= width;
+            if (radii.isTopRightVerticalRadiusAsPercentage())      trvr *= height;
+            if (radii.isTopRightHorizontalRadiusAsPercentage())    trhr *= width;
+            if (radii.isBottomRightVerticalRadiusAsPercentage())   brvr *= height;
+            if (radii.isBottomRightHorizontalRadiusAsPercentage()) brhr *= width;
+            if (radii.isBottomLeftVerticalRadiusAsPercentage())    blvr *= height;
+            if (radii.isBottomLeftHorizontalRadiusAsPercentage())  blhr *= width;
+        }
+        double scale = 1.0;
+        if (tlhr + trhr > width)  { scale = Math.min(scale, width  / (tlhr + trhr)); }
+        if (blhr + brhr > width)  { scale = Math.min(scale, width  / (blhr + brhr)); }
+        if (tlvr + blvr > height) { scale = Math.min(scale, height / (tlvr + blvr)); }
+        if (trvr + brvr > height) { scale = Math.min(scale, height / (trvr + brvr)); }
+        if (scale < 1.0) {
+            tlvr *= scale;  tlhr *= scale;
+            trvr *= scale;  trhr *= scale;
+            brvr *= scale;  brhr *= scale;
+            blvr *= scale;  blhr *= scale;
+        }
+        if (radii.hasPercentBasedRadii || scale < 1.0) {
+            return new CornerRadii(tlhr,  tlvr,  trvr,  trhr,  brhr,  brvr,  blvr,  blhr,
+                                   false, false, false, false, false, false, false, false);
+        }
+        return radii;
     }
 
     /**