Hit testing is incorrect in some cases with perspective transforms
authorshawnsingh@chromium.org <shawnsingh@chromium.org@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Wed, 9 May 2012 17:57:50 +0000 (17:57 +0000)
committershawnsingh@chromium.org <shawnsingh@chromium.org@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Wed, 9 May 2012 17:57:50 +0000 (17:57 +0000)
https://bugs.webkit.org/show_bug.cgi?id=79136

Reviewed by Simon Fraser.

Source/WebCore:

Tests: transforms/3d/hit-testing/coplanar-with-camera.html
       transforms/3d/hit-testing/perspective-clipped.html

* platform/graphics/transforms/TransformationMatrix.cpp:
(WebCore::TransformationMatrix::projectPoint): Fix a
divide-by-zero error so that values do not become Inf or Nan. Also
fix an overflow error by using a large, but not-too-large constant
to represent infinity.

(WebCore::TransformationMatrix::projectQuad): Fix an error where
incorrect quads were being returned. Incorrect quads can occur
when projectPoint clamped==true after returning.

LayoutTests:

* transforms/3d/hit-testing/coplanar-with-camera-expected.txt: Added.
* transforms/3d/hit-testing/coplanar-with-camera.html: Added.
* transforms/3d/hit-testing/perspective-clipped-expected.txt: Added.
* transforms/3d/hit-testing/perspective-clipped.html: Added.

git-svn-id: https://svn.webkit.org/repository/webkit/trunk@116543 268f45cc-cd09-0410-ab3c-d52691b4dbfc

LayoutTests/ChangeLog
LayoutTests/transforms/3d/hit-testing/coplanar-with-camera-expected.txt [new file with mode: 0644]
LayoutTests/transforms/3d/hit-testing/coplanar-with-camera.html [new file with mode: 0644]
LayoutTests/transforms/3d/hit-testing/perspective-clipped-expected.txt [new file with mode: 0644]
LayoutTests/transforms/3d/hit-testing/perspective-clipped.html [new file with mode: 0644]
Source/WebCore/ChangeLog
Source/WebCore/platform/graphics/transforms/TransformationMatrix.cpp

index 252136f..4fb6fea 100644 (file)
@@ -1,3 +1,15 @@
+2012-05-03  Shawn Singh  <shawnsingh@chromium.org>
+
+        Hit testing is incorrect in some cases with perspective transforms
+        https://bugs.webkit.org/show_bug.cgi?id=79136
+
+        Reviewed by Simon Fraser.
+
+        * transforms/3d/hit-testing/coplanar-with-camera-expected.txt: Added.
+        * transforms/3d/hit-testing/coplanar-with-camera.html: Added.
+        * transforms/3d/hit-testing/perspective-clipped-expected.txt: Added.
+        * transforms/3d/hit-testing/perspective-clipped.html: Added.
+
 2012-05-09  Dominik Röttsches  <dominik.rottsches@intel.com>
 
         Textarea-placeholder-wrapping.html may fail because of scrollbars
diff --git a/LayoutTests/transforms/3d/hit-testing/coplanar-with-camera-expected.txt b/LayoutTests/transforms/3d/hit-testing/coplanar-with-camera-expected.txt
new file mode 100644 (file)
index 0000000..c20bd36
--- /dev/null
@@ -0,0 +1,9 @@
+The text on this element should be selectable. Hovering on this element should cause a highlight. Element at 98, 200 has id "background": PASS
+Element at 302, 200 has id "background": PASS
+Element at 200, 98 has id "background": PASS
+Element at 200, 302 has id "background": PASS
+Element at 101, 200 has id "layer": PASS
+Element at 299, 200 has id "layer": PASS
+Element at 200, 101 has id "layer": PASS
+Element at 200, 299 has id "layer": PASS
+
diff --git a/LayoutTests/transforms/3d/hit-testing/coplanar-with-camera.html b/LayoutTests/transforms/3d/hit-testing/coplanar-with-camera.html
new file mode 100644 (file)
index 0000000..a6311ad
--- /dev/null
@@ -0,0 +1,84 @@
+<html>
+  <!-- This test reproduces a divide-by-zero error that is hopefully fixed by
+       https://bugs.webkit.org/show_bug.cgi?id=79136. In that bug, a layer that gets
+       translated by z so that it is coplanar with the camera origin. As a result, when
+       trying to project a point from the container space to the local space, the
+       implementation had a divide-by-zero which made hit-testing results incorrect. -->
+
+<head>
+  <style type="text/css">
+    /* Marquee content. */
+    #camera {
+        position: absolute;
+        top: 100px;
+        left: 100px;
+        -webkit-perspective: 800px;
+    }
+
+    #container {
+        -webkit-transform-style: preserve-3d;
+        -webkit-transform: translateZ(800px)
+    }
+
+    #layer {
+        position: absolute;
+        width: 200px;
+        height: 200px;
+        background-color: green;
+
+        /* This should theoretically cancel out the container's transform, and hit-testing should work. */
+        -webkit-transform: translateZ(-800px);
+    }
+
+    #background {
+        position: absolute;
+        width: 400px;
+        height: 400px;
+        background-color: gray;
+   }
+
+    #layer:hover {
+        background-color: orange;
+    }
+
+    #results {
+        position: absolute;
+        top: 420px;
+        left: 20px;
+    }
+  </style>
+
+  <script src="resources/hit-test-utils.js"></script>
+  <script>
+    const hitTestData = [
+        { 'point': [98, 200], 'target' : 'background' },
+        { 'point': [302, 200], 'target' : 'background' },
+        { 'point': [200, 98], 'target' : 'background' },
+        { 'point': [200, 302], 'target' : 'background' },
+        { 'point': [101, 200], 'target' : 'layer' },
+        { 'point': [299, 200], 'target' : 'layer' },
+        { 'point': [200, 101], 'target' : 'layer' },
+        { 'point': [200, 299], 'target' : 'layer' },
+    ];
+
+    window.addEventListener('load', runTest, false);
+  </script>
+</head>
+
+<body>
+
+  <div id="background"></div>
+
+  <div id="camera">
+    <div id="container">
+      <div id="layer">
+        The text on this element should be selectable.
+        Hovering on this element should cause a highlight.
+      </div>
+    </div>
+  </div>
+
+  <div id="results"></div>
+
+</body>
+</html>
diff --git a/LayoutTests/transforms/3d/hit-testing/perspective-clipped-expected.txt b/LayoutTests/transforms/3d/hit-testing/perspective-clipped-expected.txt
new file mode 100644 (file)
index 0000000..7e00bed
--- /dev/null
@@ -0,0 +1,13 @@
+Element at 35, 100 has id "topLayer": PASS
+Element at 370, 100 has id "topLayer": PASS
+Element at 40, 40 has id "topLayer": PASS
+Element at 365, 40 has id "topLayer": PASS
+Element at 35, 40 has id "backgroundLayer": PASS
+Element at 370, 40 has id "backgroundLayer": PASS
+Element at 40, 340 has id "bottomLayer": PASS
+Element at 365, 340 has id "bottomLayer": PASS
+Element at 35, 260 has id "bottomLayer": PASS
+Element at 370, 280 has id "bottomLayer": PASS
+Element at 35, 340 has id "backgroundLayer": PASS
+Element at 370, 340 has id "backgroundLayer": PASS
+
diff --git a/LayoutTests/transforms/3d/hit-testing/perspective-clipped.html b/LayoutTests/transforms/3d/hit-testing/perspective-clipped.html
new file mode 100644 (file)
index 0000000..33e001e
--- /dev/null
@@ -0,0 +1,103 @@
+<html>
+  <!-- This test reproduces a perspective w < 0 error addressed in
+       https://bugs.webkit.org/show_bug.cgi?id=79136. In that bug, as a layer is being
+       transformed, it may get "clamped" by the homogeneous coordinate w < 0.  When
+       projecting individual points, this was handled correctly, but projecting quads was
+       ignoring this clamping, causing invalid quads to be generated, which ultimately did
+       not hit-test correctly.
+    -->
+<head>
+  <style type="text/css">
+    #container {
+        -webkit-perspective: 1000px;
+        -webkit-perspective-origin-x: 145px;
+        /* removing this property fixes the issue, but doesn't provide the desired rendering */
+        -webkit-perspective-origin-y: 189px;
+    }
+
+    #intermediate {
+        position: absolute;
+        left: 40px;
+        top: 189px;
+        -webkit-transform-style: preserve-3d;
+    }
+
+    #results {
+        position: absolute;
+        top: 420px;
+        left: 20px;
+    }
+
+    #backgroundLayer {
+        position: absolute;
+        width: 400px;
+        height: 400px;
+        background-color: gray;
+    }
+
+    .highlightOnHover:hover {
+        background-color: orange;
+    }
+
+    .rotatedUp {
+        -webkit-transform: rotateX(-240deg) translateZ(200px)
+    }
+
+    .rotatedDown {
+        -webkit-transform: rotateX(-300deg) translateZ(200px)
+    }
+
+    .green {
+        background-color: green;
+    }
+
+    .box {
+        position: absolute;
+        width: 300px;
+        height: 110px;
+    }
+
+  </style>
+  <script src="resources/hit-test-utils.js"></script>
+  <script>
+      const hitTestData = [
+        // Points near the corners of the top layer
+        { 'point': [35, 100], 'target' : 'topLayer' },
+        { 'point': [370, 100], 'target' : 'topLayer' },
+        { 'point': [40, 40], 'target' : 'topLayer' },
+        { 'point': [365, 40], 'target' : 'topLayer' },
+
+        // Points within the axis-aligned bounding box of the top layer, but not actually on the layer itself
+        { 'point': [35, 40], 'target' : 'backgroundLayer' },
+        { 'point': [370, 40], 'target' : 'backgroundLayer' },
+
+        // Points near the corners of the top layer
+        { 'point': [40, 340], 'target' : 'bottomLayer' },
+        { 'point': [365, 340], 'target' : 'bottomLayer' },
+        { 'point': [35, 260], 'target' : 'bottomLayer' },
+        { 'point': [370, 280], 'target' : 'bottomLayer' },
+
+        // Points within the axis-aligned bounding box of the bottom layer, but not actually on the layer itself
+        { 'point': [35, 340], 'target' : 'backgroundLayer' },
+        { 'point': [370, 340], 'target' : 'backgroundLayer' },
+      ];
+
+      window.addEventListener('load', runTest, false);
+  </script>
+</head>
+
+<body>
+
+  <div id="backgroundLayer"></div>
+
+  <div id="container">
+    <div id="intermediate" class="host" style="-webkit-transform: rotate3d(1, 0, 0, 270deg)">
+      <div id="topLayer" class="highlightOnHover rotatedUp green box" style=""></div>
+      <div id="bottomLayer" class="highlightOnHover rotatedDown green box" style=""></div>
+    </div>
+  </div>
+
+  <div id="results"></div>
+
+</body>
+</html>
index 8f169d0..c1cd79c 100644 (file)
@@ -1,3 +1,23 @@
+2012-05-03  Shawn Singh  <shawnsingh@chromium.org>
+
+        Hit testing is incorrect in some cases with perspective transforms
+        https://bugs.webkit.org/show_bug.cgi?id=79136
+
+        Reviewed by Simon Fraser.
+
+        Tests: transforms/3d/hit-testing/coplanar-with-camera.html
+               transforms/3d/hit-testing/perspective-clipped.html
+
+        * platform/graphics/transforms/TransformationMatrix.cpp:
+        (WebCore::TransformationMatrix::projectPoint): Fix a
+        divide-by-zero error so that values do not become Inf or Nan. Also
+        fix an overflow error by using a large, but not-too-large constant
+        to represent infinity.
+
+        (WebCore::TransformationMatrix::projectQuad): Fix an error where
+        incorrect quads were being returned. Incorrect quads can occur
+        when projectPoint clamped==true after returning.
+
 2012-05-09  Caio Marcelo de Oliveira Filho  <caio.oliveira@openbossa.org>
 
         Simplify CSSParser::parseSimpleLengthValue()
index 24d960f..b91ca11 100644 (file)
@@ -551,6 +551,12 @@ FloatPoint TransformationMatrix::projectPoint(const FloatPoint& p, bool* clamped
     // d = -dot (Pn', R0) / dot (Pn', Rd)
     if (clamped)
         *clamped = false;
+
+    if (m33() == 0) {
+        // In this case, the projection plane is parallel to the ray we are trying to
+        // trace, and there is no well-defined value for the projection.
+        return FloatPoint();
+    }
     
     double x = p.x();
     double y = p.y();
@@ -562,8 +568,12 @@ FloatPoint TransformationMatrix::projectPoint(const FloatPoint& p, bool* clamped
 
     double w = x * m14() + y * m24() + z * m34() + m44();
     if (w <= 0) {
-        outX = copysign(numeric_limits<int>::max(), outX);
-        outY = copysign(numeric_limits<int>::max(), outY);
+        // Using int max causes overflow when other code uses the projected point. To
+        // represent infinity yet reduce the risk of overflow, we use a large but
+        // not-too-large number here when clamping.
+        const int kLargeNumber = 100000000;
+        outX = copysign(kLargeNumber, outX);
+        outY = copysign(kLargeNumber, outY);
         if (clamped)
             *clamped = true;
     } else if (w != 1) {
@@ -577,11 +587,22 @@ FloatPoint TransformationMatrix::projectPoint(const FloatPoint& p, bool* clamped
 FloatQuad TransformationMatrix::projectQuad(const FloatQuad& q) const
 {
     FloatQuad projectedQuad;
-    projectedQuad.setP1(projectPoint(q.p1()));
-    projectedQuad.setP2(projectPoint(q.p2()));
-    projectedQuad.setP3(projectPoint(q.p3()));
-    projectedQuad.setP4(projectPoint(q.p4()));
-    
+
+    bool clamped1 = false;
+    bool clamped2 = false;
+    bool clamped3 = false;
+    bool clamped4 = false;
+
+    projectedQuad.setP1(projectPoint(q.p1(), &clamped1));
+    projectedQuad.setP2(projectPoint(q.p2(), &clamped2));
+    projectedQuad.setP3(projectPoint(q.p3(), &clamped3));
+    projectedQuad.setP4(projectPoint(q.p4(), &clamped4));
+
+    // If all points on the quad had w < 0, then the entire quad would not be visible to the projected surface.
+    bool everythingWasClipped = clamped1 && clamped2 && clamped3 && clamped4;
+    if (everythingWasClipped)
+        return FloatQuad();
+
     return projectedQuad;
 }