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