1 /*
   2  * Copyright (c) 2013, Oracle and/or its affiliates. All rights reserved.
   3  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
   4  *
   5  * This code is free software; you can redistribute it and/or modify it
   6  * under the terms of the GNU General Public License version 2 only, as
   7  * published by the Free Software Foundation.  Oracle designates this
   8  * particular file as subject to the "Classpath" exception as provided
   9  * by Oracle in the LICENSE file that accompanied this code.
  10  *
  11  * This code is distributed in the hope that it will be useful, but WITHOUT
  12  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
  13  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
  14  * version 2 for more details (a copy is included in the LICENSE file that
  15  * accompanied this code).
  16  *
  17  * You should have received a copy of the GNU General Public License version
  18  * 2 along with this work; if not, write to the Free Software Foundation,
  19  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
  20  *
  21  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
  22  * or visit www.oracle.com if you need additional information or have any
  23  * questions.
  24  */
  25 
  26 package test.com.sun.javafx.sg.prism;
  27 
  28 import com.sun.javafx.geom.DirtyRegionContainer;
  29 import com.sun.javafx.geom.RectBounds;
  30 import com.sun.javafx.geom.transform.BaseTransform;
  31 import com.sun.javafx.geom.transform.GeneralTransform3D;
  32 import com.sun.javafx.sg.prism.NGCircle;
  33 import com.sun.javafx.sg.prism.NGGroup;
  34 import com.sun.javafx.sg.prism.NGNode;
  35 import com.sun.javafx.sg.prism.NGNodeShim;
  36 import com.sun.javafx.sg.prism.NGRectangle;
  37 import com.sun.javafx.sg.prism.NodePath;
  38 import org.junit.Before;
  39 import org.junit.Ignore;
  40 import org.junit.Test;
  41 import static org.junit.Assert.assertSame;
  42 import static org.junit.Assert.assertTrue;
  43 
  44 /**
  45  * Tests for the computation of the render root of a graph
  46  */
  47 public class RenderRootTest extends NGTestBase {
  48     // NGNodes to test: NGRectangle, NGImageView, NGRegion, NGCircle, NGEllipse
  49     // Also thrown in 3D transforms and 2D transforms other than BASE_TRANSFORM
  50     // Structures to test: root, background, foreground
  51     //      - Foreground completely covers background.
  52     //      - Foreground partially overlaps background
  53     // Test each node has exactly the expected opaque region, given:
  54     //      - stroke
  55     //      - effect
  56     //      - clip (dirty region should only include the intersection of the clip & bounds)
  57     //      - transforms (transform onto a pixel boundary, or transform into a pixel crack)
  58     //      - fill
  59     //      - opaque image / transparent image
  60     //      - opacity
  61     //      - blend mode
  62     //      - x / y position on partial pixel boundaries
  63     //      - others?
  64 
  65     private NGRectangle rect;
  66     private NGGroup root;
  67 
  68     @Before
  69     public void setup() {
  70         rect = createRectangle(10, 10, 90, 90);
  71         root = createGroup(rect);
  72     }
  73 
  74     /**
  75      * Helper method to get the render root. We have to run both the markCullRegions and getRenderRoot methods
  76      * in order for getRenderRoot to return the correctly computed results.
  77      *
  78      * @param root The root node
  79      * @param dirtyX The x coordinate of the dirty region
  80      * @param dirtyY The y coordinate of the dirty region
  81      * @param dirtyWidth The width of the dirty region
  82      * @param dirtyHeight The height of the dirty region
  83      * @return The NodePath, or null if there are no path elements
  84      */
  85     private NodePath getRenderRoot(NGGroup root, int dirtyX, int dirtyY, int dirtyWidth, int dirtyHeight) {
  86         final DirtyRegionContainer drc = new DirtyRegionContainer(1);
  87         final RectBounds dirtyRegion = new RectBounds(dirtyX, dirtyY, dirtyX+dirtyWidth, dirtyY+dirtyHeight);
  88         drc.addDirtyRegion(dirtyRegion);
  89         final BaseTransform tx = BaseTransform.IDENTITY_TRANSFORM;
  90         final GeneralTransform3D pvTx = new GeneralTransform3D();
  91         NGNodeShim.markCullRegions(root, drc, -1, tx, pvTx);
  92         NodePath path = new NodePath();
  93         root.getRenderRoot(path, dirtyRegion, 0, tx, pvTx);
  94         return path;
  95     }
  96 
  97     /**
  98      * A quick note about how the NodePath works.
  99      * 
 100      * If it is empty, then it means that nothing
 101      * needs to be painted (maybe there were some dirty nodes, but they were completely
 102      * occluded, so we don't need to paint anything).
 103      * 
 104      * If it contains *only* the root node,
 105      * then either the root node itself is completely occluding the dirty region, or there was
 106      * no other render root to be found, so we have to paint the whole scene.
 107      * 
 108      * If it contains something more than just the root node, then it will be the path from
 109      * the root node down to the render root child.
 110      * 
 111      * This method takes the expected root (which may be null) and the rootPath (which can
 112      * never be null). If expectedRoot is null, rootPath must be empty. Otherwise,
 113      * expectedRoot must be the last item in rootPath.
 114      */
 115     private void assertRenderRoot(NGNode expectedRoot, NodePath rootPath) {
 116         if (expectedRoot == null) {
 117             assertTrue(rootPath.isEmpty());
 118         } else {
 119             // Get to the end
 120             while (rootPath.hasNext()) rootPath.next();
 121             assertSame(expectedRoot, rootPath.getCurrentNode());
 122         }
 123     }
 124     
 125     /**
 126      * Tests the case where the dirty region is completely within the opaque region.
 127      * The rect in this case is dirty.
 128      */
 129     @Test
 130     public void dirtyRegionWithinOpaqueRegion() {
 131         NodePath rootPath = getRenderRoot(root, 20, 20, 70, 70);
 132         assertRenderRoot(rect, rootPath);
 133     }
 134 
 135     /**
 136      * Tests the case where the dirty region is completely within the opaque region.
 137      * The rect in this case is clean.
 138      */
 139     @Test
 140     public void dirtyRegionWithinOpaqueRegion_Clean() {
 141         root.clearDirtyTree();
 142         NodePath rootPath = getRenderRoot(root, 20, 20, 70, 70);
 143         assertRenderRoot(null, rootPath);
 144     }
 145 
 146     /**
 147      * Tests the case where the dirty region exactly matches the opaque region.
 148      * The rect is dirty.
 149      */
 150     @Test
 151     public void dirtyRegionMatchesOpaqueRegion() {
 152         NodePath rootPath = getRenderRoot(root, 10, 10, 90, 90);
 153         assertRenderRoot(rect, rootPath);
 154     }
 155 
 156     /**
 157      * Tests the case where the dirty region exactly matches the opaque region.
 158      * The rect is clean.
 159      */
 160     @Test
 161     public void dirtyRegionMatchesOpaqueRegion_Clean() {
 162         root.clearDirtyTree();
 163         NodePath rootPath = getRenderRoot(root, 10, 10, 90, 90);
 164         assertRenderRoot(null, rootPath);
 165     }
 166 
 167     /**
 168      * Tests the case where the dirty region is within the opaque region, but shares the
 169      * same top edge. The rect is dirty.
 170      */
 171     @Test
 172     public void dirtyRegionWithinOpaqueRegionTouchesTop() {
 173         NodePath rootPath = getRenderRoot(root, 20, 10, 70, 70);
 174         assertRenderRoot(rect, rootPath);
 175     }
 176 
 177     /**
 178      * Tests the case where the dirty region is within the opaque region, but shares the
 179      * same top edge. The rect is clean.
 180      */
 181     @Test
 182     public void dirtyRegionWithinOpaqueRegionTouchesTop_Clean() {
 183         root.clearDirtyTree();
 184         NodePath rootPath = getRenderRoot(root, 20, 10, 70, 70);
 185         assertRenderRoot(null, rootPath);
 186     }
 187 
 188     /**
 189      * Tests the case where the dirty region is within the opaque region, but shares the
 190      * same right edge. The rect is dirty.
 191      */
 192     @Test
 193     public void dirtyRegionWithinOpaqueRegionTouchesRight() {
 194         NodePath rootPath = getRenderRoot(root, 20, 20, 80, 70);
 195         assertRenderRoot(rect, rootPath);
 196     }
 197     /**
 198      * Tests the case where the dirty region is within the opaque region, but shares the
 199      * same right edge. The rect is clean.
 200      */
 201     @Test
 202     public void dirtyRegionWithinOpaqueRegionTouchesRight_Clean() {
 203         root.clearDirtyTree();
 204         NodePath rootPath = getRenderRoot(root, 20, 20, 80, 70);
 205         assertRenderRoot(null, rootPath);
 206     }
 207 
 208     /**
 209      * Tests the case where the dirty region is within the opaque region, but shares the
 210      * same bottom edge. The rect is dirty.
 211      */
 212     @Test
 213     public void dirtyRegionWithinOpaqueRegionTouchesBottom() {
 214         NodePath rootPath = getRenderRoot(root, 20, 20, 70, 80);
 215         assertRenderRoot(rect, rootPath);
 216     }
 217 
 218     /**
 219      * Tests the case where the dirty region is within the opaque region, but shares the
 220      * same bottom edge. The rect is clean.
 221      */
 222     @Test
 223     public void dirtyRegionWithinOpaqueRegionTouchesBottom_Clean() {
 224         root.clearDirtyTree();
 225         NodePath rootPath = getRenderRoot(root, 20, 20, 70, 80);
 226         assertRenderRoot(null, rootPath);
 227     }
 228 
 229     /**
 230      * Tests the case where the dirty region is within the opaque region, but shares the
 231      * same left edge. The rect is dirty.
 232      */
 233     @Test
 234     public void dirtyRegionWithinOpaqueRegionTouchesLeft() {
 235         NodePath rootPath = getRenderRoot(root, 10, 20, 70, 70);
 236         assertRenderRoot(rect, rootPath);
 237     }
 238 
 239     /**
 240      * Tests the case where the dirty region is within the opaque region, but shares the
 241      * same left edge. The rect is clean.
 242      */
 243     @Test
 244     public void dirtyRegionWithinOpaqueRegionTouchesLeft_Clean() {
 245         root.clearDirtyTree();
 246         NodePath rootPath = getRenderRoot(root, 10, 20, 70, 70);
 247         assertRenderRoot(null, rootPath);
 248     }
 249 
 250     @Test
 251     public void opaqueRegionWithinDirtyRegion() {
 252         NodePath rootPath = getRenderRoot(root, 0, 0, 110, 110);
 253         assertRenderRoot(root, rootPath);
 254     }
 255 
 256     @Test
 257     public void opaqueRegionWithinDirtyRegion_Clean() {
 258         root.clearDirtyTree();
 259         NodePath rootPath = getRenderRoot(root, 0, 0, 110, 110);
 260         assertRenderRoot(root, rootPath);
 261     }
 262 
 263     @Test
 264     public void dirtyRegionIntersectsOpaqueRegionTop() {
 265         NodePath rootPath = getRenderRoot(root, 20, 0, 70, 30);
 266         assertRenderRoot(root, rootPath);
 267     }
 268 
 269     @Test
 270     public void dirtyRegionIntersectsOpaqueRegionTop_Clean() {
 271         root.clearDirtyTree();
 272         NodePath rootPath = getRenderRoot(root, 20, 0, 70, 30);
 273         assertRenderRoot(root, rootPath);
 274     }
 275 
 276     @Test
 277     public void dirtyRegionIntersectsOpaqueRegionRight() {
 278         NodePath rootPath = getRenderRoot(root, 90, 20, 30, 70);
 279         assertRenderRoot(root, rootPath);
 280     }
 281 
 282     @Test
 283     public void dirtyRegionIntersectsOpaqueRegionRight_Clean() {
 284         root.clearDirtyTree();
 285         NodePath rootPath = getRenderRoot(root, 90, 20, 30, 70);
 286         assertRenderRoot(root, rootPath);
 287     }
 288 
 289     @Test
 290     public void dirtyRegionIntersectsOpaqueRegionBottom() {
 291         NodePath rootPath = getRenderRoot(root, 20, 90, 70, 30);
 292         assertRenderRoot(root, rootPath);
 293     }
 294 
 295     @Test
 296     public void dirtyRegionIntersectsOpaqueRegionBottom_Clean() {
 297         root.clearDirtyTree();
 298         NodePath rootPath = getRenderRoot(root, 20, 90, 70, 30);
 299         assertRenderRoot(root, rootPath);
 300     }
 301 
 302     @Test
 303     public void dirtyRegionIntersectsOpaqueRegionLeft() {
 304         NodePath rootPath = getRenderRoot(root, 0, 20, 30, 70);
 305         assertRenderRoot(root, rootPath);
 306     }
 307 
 308     @Test
 309     public void dirtyRegionIntersectsOpaqueRegionLeft_Clean() {
 310         root.clearDirtyTree();
 311         NodePath rootPath = getRenderRoot(root, 0, 20, 30, 70);
 312         assertRenderRoot(root, rootPath);
 313     }
 314 
 315     @Test
 316     public void dirtyRegionCompletelyOutsideOfOpaqueRegion() {
 317         NodePath rootPath = getRenderRoot(root, 0, 0, 5, 5);
 318         assertRenderRoot(root, rootPath);
 319     }
 320 
 321     @Test
 322     public void dirtyRegionCompletelyOutsideOfOpaqueRegion_Clean() {
 323         root.clearDirtyTree();
 324         NodePath rootPath = getRenderRoot(root, 0, 0, 5, 5);
 325         assertRenderRoot(root, rootPath);
 326     }
 327 
 328     @Ignore("What is the right thing here? It seems that an empty dirty region should result in no rendering?")
 329     @Test
 330     public void emptyDirtyRegion() {
 331         NodePath rootPath = getRenderRoot(root, 0, 0, -1, -1);
 332         assertRenderRoot(root, rootPath);
 333     }
 334 
 335     @Ignore("Currently fails because isEmpty doesn't take into account width == 0, height == 0")
 336     @Test
 337     public void zeroSizeDirtyRegionWithinOpaqueRegion() {
 338         NodePath rootPath = getRenderRoot(root, 20, 20, 0, 0);
 339         assertRenderRoot(root, rootPath);
 340     }
 341 
 342     /**
 343      * Tests that a clip works. Note that I send the dirty region to be the same
 344      * size as what I expect the clip to be, so that the test will fail if the
 345      * dirty region ends up being larger than the computed clip.
 346      */
 347     @Test
 348     public void withRectangularClip() {
 349         NGRectangle clip = createRectangle(20, 20, 70, 70);
 350         rect.setClipNode(clip);
 351         NodePath rootPath = getRenderRoot(root, 20, 20, 70, 70);
 352         assertRenderRoot(rect, rootPath);
 353     }
 354 
 355     /**
 356      * The negative test, where the clip is smaller than the dirty region
 357      */
 358     @Test
 359     public void withRectangularClip_negative() {
 360         NGRectangle clip = createRectangle(20, 20, 70, 70);
 361         rect.setClipNode(clip);
 362         NodePath rootPath = getRenderRoot(root, 19, 20, 70, 70);
 363         assertRenderRoot(root, rootPath);
 364     }
 365 
 366     /**
 367      * Tests that a clip works when translated.
 368      */
 369     @Test
 370     public void withRectangularClipTranslated() {
 371         NGRectangle clip = createRectangle(20, 20, 70, 70);
 372         clip.setTransformMatrix(BaseTransform.getTranslateInstance(10, 10));
 373         rect.setClipNode(clip);
 374         NodePath rootPath = getRenderRoot(root, 30, 30, 70, 70);
 375         assertRenderRoot(rect, rootPath);
 376     }
 377 
 378     /**
 379      * Tests that a clip works when translated.
 380      */
 381     @Test
 382     public void withRectangularClipTranslated_negative() {
 383         NGRectangle clip = createRectangle(20, 20, 70, 70);
 384         clip.setTransformMatrix(BaseTransform.getTranslateInstance(10, 10));
 385         rect.setClipNode(clip);
 386         NodePath rootPath = getRenderRoot(root, 29, 30, 70, 70);
 387         assertRenderRoot(root, rootPath);
 388     }
 389 
 390     /**
 391      * Note, scales about origin, not center
 392      */
 393     @Test
 394     public void withRectangularClipScaled() {
 395         NGRectangle clip = createRectangle(20, 20, 70, 70);
 396         clip.setTransformMatrix(BaseTransform.getScaleInstance(.5, .5));
 397         rect.setClipNode(clip);
 398         NodePath rootPath = getRenderRoot(root, 10, 10, 35, 35);
 399         assertRenderRoot(rect, rootPath);
 400     }
 401 
 402     /**
 403      * Note, scales about origin, not center
 404      */
 405     @Test
 406     public void withRectangularClipScaled_negative() {
 407         NGRectangle clip = createRectangle(20, 20, 70, 70);
 408         clip.setTransformMatrix(BaseTransform.getScaleInstance(.5, .5));
 409         rect.setClipNode(clip);
 410         NodePath rootPath = getRenderRoot(root, 9, 10, 35, 35);
 411         assertRenderRoot(root, rootPath);
 412     }
 413 
 414     /**
 415      * We can now easily use ellipse and ellipse and images etc as clips
 416      * in addition to rect clips. Here I choose a dirty region that is
 417      * clearly in the center of the ellipse's area so as to pass the test.
 418      */
 419     @Test
 420     public void withCircleClip() {
 421         NGCircle clip = createCircle(50, 50, 45);
 422         rect.setClipNode(clip);
 423         NodePath rootPath = getRenderRoot(root, 40, 40, 20, 20);
 424         assertRenderRoot(rect, rootPath);
 425     }
 426 
 427     /**
 428      * Make the dirty area larger than the clip so as to fail.
 429      */
 430     @Test
 431     public void withCircleClip_negative() {
 432         NGCircle clip = createCircle(50, 50, 45);
 433         rect.setClipNode(clip);
 434         NodePath rootPath = getRenderRoot(root, 10, 10, 90, 90);
 435         assertRenderRoot(root, rootPath);
 436     }
 437 }