Title: [186976] trunk
Revision
186976
Author
[email protected]
Date
2015-07-17 16:55:59 -0700 (Fri, 17 Jul 2015)

Log Message

Improve rect shrink-wrapping algorithm
https://bugs.webkit.org/show_bug.cgi?id=147037
<rdar://problem/21643094>

Reviewed by Simon Fraser.

* platform/graphics/FloatPoint.h:
(WebCore::areEssentiallyEqual):
Added; implementation is the same as FloatSize's.

* platform/graphics/PathUtilities.cpp:
(WebCore::FloatPointGraph::FloatPointGraph):
(WebCore::FloatPointGraph::~FloatPointGraph):
(WebCore::FloatPointGraph::Node::Node):
(WebCore::FloatPointGraph::Node::nextPoints):
(WebCore::FloatPointGraph::Node::addNextPoint):
(WebCore::FloatPointGraph::Node::isVisited):
(WebCore::FloatPointGraph::Node::visit):
(WebCore::FloatPointGraph::Node::reset):
(WebCore::FloatPointGraph::reset):
(WebCore::FloatPointGraph::findOrCreateNode):
(WebCore::findLineSegmentIntersection):
(WebCore::addIntersectionPoints):
(WebCore::walkGraphAndExtractPolygon):
(WebCore::findUnvisitedPolygonStartPoint):
(WebCore::unitePolygons):
(WebCore::edgesForRect):
(WebCore::PathUtilities::pathWithShrinkWrappedRects):
(WebCore::addShrinkWrapRightCorner): Deleted.
(WebCore::addShrinkWrapLeftCorner): Deleted.
(WebCore::addShrinkWrappedPathForRects): Deleted.
(WebCore::rectsIntersectOrTouch): Deleted.
(WebCore::findSetContainingRect): Deleted.
Add a new implementation of shrink-wrap, which is significantly more
generic than the old one, which assumed a top-down progression of rects.

This version uses polygon intersection to find the path around the
set of rects, and then follows said path and adds appropriately-sized
arcs for the corners.

The polygon intersection algorithm first finds all the intersection points
between all of the rects, then builds a graph of edges outward from one point.
It then traverses the graph, choosing at each point the next edge which
has not been visited and has the greatest interior angle, recording the polygon as it goes.

If at the end of the traversal we have not returned to the initial node,
we give up on shrink-wrapping and just use a bounding box around the rects.

If any of the original rects have not been visited at all, we repeat the traversal
starting with that rect, making an additional polygon (since we removed completely contained
rects before we started, having not visited the rect at all means that it's not connected
to the others).

Once we have a set of united polygons, we follow each one, determining the ideal (always
equal in width and height, never more than half the length of either edge, so that we always
have a smooth curve) arc radius and projecting it onto the edge, and then
adding an arc between the end of the previous path and beginning of the next.

Because the shrink-wrap algorithm is fairly expensive, if there are more than 20 rects,
we fall back to a bounding box. Given the current use cases, this is more than enough
rects, but can certainly be adjusted in the future if needed.

* testing/Internals.cpp:
(WebCore::Internals::pathWithShrinkWrappedRects):
* testing/Internals.h:
* testing/Internals.idl:
Add a radius parameter.

* fast/shrink-wrap/rect-shrink-wrap-expected.png:
* fast/shrink-wrap/rect-shrink-wrap.html:
Add a radius parameter to testRects, defaulting to 8.

Add an offset parameter to testRects, making it easier to slide
the rect sets around.

Add some more test cases.

Modified Paths

Diff

Modified: trunk/LayoutTests/ChangeLog (186975 => 186976)


--- trunk/LayoutTests/ChangeLog	2015-07-17 23:47:47 UTC (rev 186975)
+++ trunk/LayoutTests/ChangeLog	2015-07-17 23:55:59 UTC (rev 186976)
@@ -1,3 +1,20 @@
+2015-07-17  Tim Horton  <[email protected]>
+
+        Improve rect shrink-wrapping algorithm
+        https://bugs.webkit.org/show_bug.cgi?id=147037
+        <rdar://problem/21643094>
+
+        Reviewed by Simon Fraser.
+
+        * fast/shrink-wrap/rect-shrink-wrap-expected.png:
+        * fast/shrink-wrap/rect-shrink-wrap.html:
+        Add a radius parameter to testRects, defaulting to 8.
+
+        Add an offset parameter to testRects, making it easier to slide
+        the rect sets around.
+
+        Add some more test cases.
+
 2015-07-17  Nan Wang  <[email protected]>
 
         AX: iframe within table cell is inaccessible to VoiceOver

Modified: trunk/LayoutTests/fast/shrink-wrap/rect-shrink-wrap-expected.png


(Binary files differ)

Modified: trunk/LayoutTests/fast/shrink-wrap/rect-shrink-wrap.html (186975 => 186976)


--- trunk/LayoutTests/fast/shrink-wrap/rect-shrink-wrap.html	2015-07-17 23:47:47 UTC (rev 186975)
+++ trunk/LayoutTests/fast/shrink-wrap/rect-shrink-wrap.html	2015-07-17 23:55:59 UTC (rev 186976)
@@ -1,20 +1,27 @@
 <script>
 
-function testRects(rects) {
+function testRects(offset, rects, radius) {
     if (!window.internals)
         document.write("This test must be run in a test runner.")
 
+    if (radius == undefined)
+        radius = 8;
+
     var concatRects = [];
     for (var i in rects)
         Array.prototype.push.apply(concatRects, rects[i]);
 
     var path = undefined;
     if (window.internals)
-        path = window.internals.pathWithShrinkWrappedRects(concatRects);
+        path = window.internals.pathWithShrinkWrappedRects(concatRects, radius);
 
     var canvas = document.getElementById("shrink");
     var ctx = canvas.getContext("2d");
 
+    ctx.save();
+
+    ctx.translate.apply(ctx, offset);
+
     ctx.fillStyle = "rgba(0,0,0,0.2)";
 
     for (var i in rects)
@@ -29,118 +36,203 @@
     ctx.lineWidth = 3;
     if (path)
         ctx.stroke(path);
+
+    ctx.restore();
 }
 
 window._onload_ = function () {
     // Right and left aligned, touching:
 
-    testRects([
-        [20, 20, 50, 20],
-        [20, 40, 35, 20],
-        [20, 60, 20, 20]]);
+    testRects([20, 20], [
+        [0, 0, 50, 20],
+        [0, 20, 35, 20],
+        [0, 40, 20, 20]]);
 
-    testRects([
-        [20, 90, 20, 20],
-        [20, 110, 35, 20],
-        [20, 130, 50, 20]]);
+    testRects([20, 90], [
+        [0, 0, 20, 20],
+        [0, 20, 35, 20],
+        [0, 40, 50, 20]]);
 
-    testRects([
-        [80, 20, 50, 20],
-        [95, 40, 35, 20],
-        [110, 60, 20, 20]]);
+    testRects([80, 20], [
+        [0, 0, 50, 20],
+        [15, 20, 35, 20],
+        [30, 40, 20, 20]]);
 
-    testRects([
-        [110, 90, 20, 20],
-        [95, 110, 35, 20],
-        [80, 130, 50, 20]]);
+    testRects([80, 90], [
+        [30, 0, 20, 20],
+        [15, 20, 35, 20],
+        [0, 40, 50, 20]]);
 
     // Center aligned, touching:
 
-    testRects([
-        [170, 20, 100, 40],
-        [190, 60, 60, 40],
-        [205, 100, 30, 40]]);
+    testRects([170, 20], [
+        [0, 0, 100, 40],
+        [20, 40, 60, 40],
+        [35, 80, 30, 40]]);
 
-    testRects([
-        [305, 20, 30, 40],
-        [290, 60, 60, 40],
-        [270, 100, 100, 40]]);
+    testRects([270, 20], [
+        [35, 0, 30, 40],
+        [20, 40, 60, 40],
+        [0, 80, 100, 40]]);
 
-    testRects([
-        [370, 20, 100, 40],
-        [405, 60, 30, 40],
-        [390, 100, 60, 40]]);
+    testRects([370, 20], [
+        [0, 0, 100, 40],
+        [35, 40, 30, 40],
+        [20, 80, 60, 40]]);
 
     // Other:
 
-    testRects([
-        [20, 200, 40, 40],
-        [40, 220, 40, 40],
-        [60, 240, 40, 40]]);
+    testRects([20, 200], [
+        [0, 0, 40, 40],
+        [20, 20, 40, 40],
+        [40, 40, 40, 40]]);
 
-    testRects([
-        [120, 200, 40, 40],
-        [120, 240, 40, 40],
-        [120, 280, 40, 40]]);
+    testRects([120, 200], [
+        [0, 40, 40, 40],
+        [20, 20, 40, 40],
+        [40, 0, 40, 40]]);
 
+    testRects([220, 200], [
+        [0, 0, 40, 40],
+        [0, 40, 40, 40],
+        [0, 80, 40, 40]]);
+
+    testRects([200, 350], [
+        [0, 0, 100, 50],
+        [0, 25, 50, 50]]);
+
     // Non-touching:
 
-    testRects([
-        [180, 200, 40, 60],
-        [180, 280, 40, 40]]);
+    testRects([20, 300], [
+        [0, 0, 40, 60],
+        [0, 80, 40, 40]]);
 
     // Combination of touching and non-touching:
 
-    testRects([
-        [280, 200, 30, 40],
-        [280, 280, 50, 40],
-        [340, 200, 40, 40],
-        [360, 240, 80, 40],
-        [380, 280, 40, 40],
-        [430, 200, 40, 20],
-        [450, 215, 40, 20],
-        [470, 230, 40, 20]]);
+    testRects([280, 200], [
+        [0, 0, 30, 40],
+        [0, 80, 50, 40],
+        [60, 0, 40, 40],
+        [80, 40, 80, 40],
+        [100, 80, 40, 40],
+        [150, 0, 40, 20],
+        [170, 15, 40, 20],
+        [190, 30, 40, 20]]);
 
-    // Incorrectly sorted:
+    // Enclosing:
 
-    testRects([
-        [20, 380, 40, 40],
-        [40, 360, 40, 40],
-        [60, 340, 40, 40]]);
+    testRects([100, 300], [
+        [0, 0, 50, 50],
+        [10, 10, 20, 20]]);
 
-    // Broken:
+    testRects([100, 370], [
+        [0, 0, 50, 50],
+        [10, 10, 20, 20],
+        [20, 20, 20, 20]]);
 
-    testRects([
-        [600+100, 90, 20, 20],
-        [600+95, 110, 35, 20],
-        [600+80, 130, 50, 20]]);
+    // Harder (widths less than the radius, horizontally arranged, etc.):
 
-    testRects([
-        [230+340, 20, 40, 40],
-        [230+360, 60, 65, 40],
-        [230+380, 100, 40, 40]]);
+    testRects([500, 20], [
+        [0, 0, 40, 40],
+        [20, 40, 65, 40],
+        [40, 80, 40, 40]]);
 
-    // These should fallback to a rounded bounding rect:
+    testRects([600, 20], [
+        [20, 0, 20, 20],
+        [15, 20, 35, 20],
+        [0, 40, 50, 20]]);
 
-    testRects([
-        [600+100, 190, 20, 20],
-        [600+95, 210, 35, 20],
-        [600+80, 210, 50, 20]]);
+    testRects([650, 100], [
+        [0, 0, 40, 40],
+        [40, 0, 40, 40],
+        [80, 0, 40, 40]]);
 
-    testRects([
-        [600+0, 250, 40, 40],
-        [600+40, 250, 40, 40],
-        [600+80, 250, 40, 40]]);
+    testRects([700, 20], [
+        [20, 0, 20, 20],
+        [15, 20, 35, 20],
+        [0, 20, 50, 20]]);
 
-    testRects([
-        [600, 300, 20, 40],
-        [600+20, 320, 20, 40],
-        [600, 340, 20, 40]]);
+    testRects([600, 200], [
+        [0, 0, 20, 40],
+        [20, 20, 20, 40],
+        [40, 0, 20, 40]]);
 
-    testRects([
-        [700, 300, 20, 40],
-        [700+20, 320, 20, 40],
-        [700+40, 300, 20, 40]]);
+    testRects([700, 200], [
+        [0, 0, 20, 40],
+        [20, 25, 20, 40],
+        [0, 50, 20, 40]]);
+
+    // Huge radius:
+
+    testRects([20, 450], [
+        [0, 0, 50, 20],
+        [0, 20, 35, 20],
+        [0, 40, 20, 20]], 100);
+
+    testRects([20, 520], [
+        [0, 0, 20, 20],
+        [0, 20, 35, 20],
+        [0, 40, 50, 20]], 100);
+
+    testRects([80, 450], [
+        [0, 0, 50, 20],
+        [15, 20, 35, 20],
+        [30, 40, 20, 20]], 100);
+
+    testRects([80, 520], [
+        [30, 0, 20, 20],
+        [15, 20, 35, 20],
+        [0, 40, 50, 20]], 100);
+
+    testRects([170, 450], [
+        [0, 0, 100, 40],
+        [20, 40, 60, 40],
+        [35, 80, 30, 40]], 100);
+
+    testRects([270, 450], [
+        [35, 0, 30, 40],
+        [20, 40, 60, 40],
+        [0, 80, 100, 40]], 100);
+
+    testRects([370, 450], [
+        [0, 0, 100, 40],
+        [35, 40, 30, 40],
+        [20, 80, 60, 40]], 100);
+
+    testRects([750, 500], [
+        [0, 0, 20, 40],
+        [20, 20, 20, 40],
+        [0, 40, 20, 40]]);
+
+    // Holes:
+
+    testRects([400, 300], [
+        [30, 0, 40, 40],
+        [60, 40, 40, 40],
+        [30, 80, 40, 40],
+        [0, 40, 40, 40]]);
+
+    // Lines with overlap:
+
+    testRects([520, 450], [
+        [0, 0, 50, 20],
+        [0, 15, 35, 20],
+        [0, 30, 20, 20]]);
+
+    testRects([520, 520], [
+        [0, 0, 20, 20],
+        [0, 15, 35, 20],
+        [0, 30, 50, 20]]);
+
+    testRects([580, 450], [
+        [0, 0, 50, 20],
+        [15, 15, 35, 20],
+        [30, 30, 20, 20]]);
+
+    testRects([580, 520], [
+        [30, 0, 20, 20],
+        [15, 15, 35, 20],
+        [0, 30, 50, 20]]);
 }
 
 </script>

Modified: trunk/Source/WebCore/ChangeLog (186975 => 186976)


--- trunk/Source/WebCore/ChangeLog	2015-07-17 23:47:47 UTC (rev 186975)
+++ trunk/Source/WebCore/ChangeLog	2015-07-17 23:55:59 UTC (rev 186976)
@@ -1,3 +1,73 @@
+2015-07-17  Tim Horton  <[email protected]>
+
+        Improve rect shrink-wrapping algorithm
+        https://bugs.webkit.org/show_bug.cgi?id=147037
+        <rdar://problem/21643094>
+
+        Reviewed by Simon Fraser.
+
+        * platform/graphics/FloatPoint.h:
+        (WebCore::areEssentiallyEqual):
+        Added; implementation is the same as FloatSize's.
+
+        * platform/graphics/PathUtilities.cpp:
+        (WebCore::FloatPointGraph::FloatPointGraph):
+        (WebCore::FloatPointGraph::~FloatPointGraph):
+        (WebCore::FloatPointGraph::Node::Node):
+        (WebCore::FloatPointGraph::Node::nextPoints):
+        (WebCore::FloatPointGraph::Node::addNextPoint):
+        (WebCore::FloatPointGraph::Node::isVisited):
+        (WebCore::FloatPointGraph::Node::visit):
+        (WebCore::FloatPointGraph::Node::reset):
+        (WebCore::FloatPointGraph::reset):
+        (WebCore::FloatPointGraph::findOrCreateNode):
+        (WebCore::findLineSegmentIntersection):
+        (WebCore::addIntersectionPoints):
+        (WebCore::walkGraphAndExtractPolygon):
+        (WebCore::findUnvisitedPolygonStartPoint):
+        (WebCore::unitePolygons):
+        (WebCore::edgesForRect):
+        (WebCore::PathUtilities::pathWithShrinkWrappedRects):
+        (WebCore::addShrinkWrapRightCorner): Deleted.
+        (WebCore::addShrinkWrapLeftCorner): Deleted.
+        (WebCore::addShrinkWrappedPathForRects): Deleted.
+        (WebCore::rectsIntersectOrTouch): Deleted.
+        (WebCore::findSetContainingRect): Deleted.
+        Add a new implementation of shrink-wrap, which is significantly more
+        generic than the old one, which assumed a top-down progression of rects.
+
+        This version uses polygon intersection to find the path around the
+        set of rects, and then follows said path and adds appropriately-sized
+        arcs for the corners.
+
+        The polygon intersection algorithm first finds all the intersection points
+        between all of the rects, then builds a graph of edges outward from one point.
+        It then traverses the graph, choosing at each point the next edge which
+        has not been visited and has the greatest interior angle, recording the polygon as it goes.
+
+        If at the end of the traversal we have not returned to the initial node,
+        we give up on shrink-wrapping and just use a bounding box around the rects.
+
+        If any of the original rects have not been visited at all, we repeat the traversal
+        starting with that rect, making an additional polygon (since we removed completely contained
+        rects before we started, having not visited the rect at all means that it's not connected
+        to the others).
+
+        Once we have a set of united polygons, we follow each one, determining the ideal (always
+        equal in width and height, never more than half the length of either edge, so that we always
+        have a smooth curve) arc radius and projecting it onto the edge, and then
+        adding an arc between the end of the previous path and beginning of the next.
+
+        Because the shrink-wrap algorithm is fairly expensive, if there are more than 20 rects,
+        we fall back to a bounding box. Given the current use cases, this is more than enough
+        rects, but can certainly be adjusted in the future if needed.
+
+        * testing/Internals.cpp:
+        (WebCore::Internals::pathWithShrinkWrappedRects):
+        * testing/Internals.h:
+        * testing/Internals.idl:
+        Add a radius parameter.
+
 2015-07-17  Nan Wang  <[email protected]>
 
         AX: iframe within table cell is inaccessible to VoiceOver

Modified: trunk/Source/WebCore/platform/graphics/FloatPoint.h (186975 => 186976)


--- trunk/Source/WebCore/platform/graphics/FloatPoint.h	2015-07-17 23:47:47 UTC (rev 186975)
+++ trunk/Source/WebCore/platform/graphics/FloatPoint.h	2015-07-17 23:55:59 UTC (rev 186976)
@@ -254,6 +254,11 @@
     return FloatPoint(a.width(), a.height());
 }
 
+inline bool areEssentiallyEqual(const FloatPoint& a, const FloatPoint& b)
+{
+    return WTF::areEssentiallyEqual(a.x(), b.x()) && WTF::areEssentiallyEqual(a.y(), b.y());
 }
 
+}
+
 #endif

Modified: trunk/Source/WebCore/platform/graphics/PathUtilities.cpp (186975 => 186976)


--- trunk/Source/WebCore/platform/graphics/PathUtilities.cpp	2015-07-17 23:47:47 UTC (rev 186975)
+++ trunk/Source/WebCore/platform/graphics/PathUtilities.cpp	2015-07-17 23:55:59 UTC (rev 186976)
@@ -29,234 +29,305 @@
 
 #include "FloatPoint.h"
 #include "FloatRect.h"
+#include "GeometryUtilities.h"
 #include <math.h>
 
 namespace WebCore {
 
-static void addShrinkWrapRightCorner(Path& path, const FloatRect* fromRect, const FloatRect* toRect, float radius)
-{
-    FloatSize horizontalRadius(radius, 0);
-    FloatSize verticalRadius(0, radius);
+class FloatPointGraph {
+    WTF_MAKE_NONCOPYABLE(FloatPointGraph);
+public:
+    FloatPointGraph() { }
 
-    if (!fromRect) {
-        // For the first (top) rect:
+    class Node : public FloatPoint {
+        WTF_MAKE_NONCOPYABLE(Node);
+    public:
+        Node(FloatPoint point)
+            : FloatPoint(point)
+        { }
 
-        path.moveTo(toRect->minXMinYCorner() + horizontalRadius);
+        const Vector<Node*>& nextPoints() const { return m_nextPoints; }
+        void addNextPoint(Node* node)
+        {
+            if (!m_nextPoints.contains(node))
+                m_nextPoints.append(node);
+        }
 
-        // Across the top, towards the right.
-        path.addLineTo(toRect->maxXMinYCorner() - horizontalRadius);
+        bool isVisited() const { return m_visited; }
+        void visit() { m_visited = true; }
 
-        // Arc the top corner.
-        path.addArcTo(toRect->maxXMinYCorner(), toRect->maxXMinYCorner() + verticalRadius, radius);
-    } else if (!toRect) {
-        // For the last rect:
+        void reset() { m_visited = false; m_nextPoints.clear(); }
 
-        // Down the right.
-        path.addLineTo(fromRect->maxXMaxYCorner() - verticalRadius);
+    private:
+        Vector<Node*> m_nextPoints;
+        bool m_visited { false };
+    };
 
-        // Arc the bottom corner.
-        path.addArcTo(fromRect->maxXMaxYCorner(), fromRect->maxXMaxYCorner() - horizontalRadius, radius);
-    } else {
-        // For middle rects:
+    typedef std::pair<Node*, Node*> Edge;
+    typedef Vector<Edge> Polygon;
 
-        float rightEdgeDifference = toRect->maxX() - fromRect->maxX();
+    Node* findOrCreateNode(FloatPoint);
 
-        // Skip over rects with equal edges, because we can't make
-        // sensible curves between them.
-        if (fabsf(rightEdgeDifference) < std::numeric_limits<float>::epsilon())
-            return;
+    void reset()
+    {
+        for (auto& node : m_allNodes)
+            node->reset();
+    }
 
-        if (rightEdgeDifference < 0) {
-            float effectiveY = std::max(toRect->y(), fromRect->maxY());
-            FloatPoint toRectMaxXMinYCorner = FloatPoint(toRect->maxX(), effectiveY);
+private:
+    Vector<std::unique_ptr<Node>> m_allNodes;
+};
 
-            // Down the right.
-            path.addLineTo(FloatPoint(fromRect->maxX(), effectiveY) - verticalRadius);
+FloatPointGraph::Node* FloatPointGraph::findOrCreateNode(FloatPoint point)
+{
+    for (auto& testNode : m_allNodes) {
+        if (areEssentiallyEqual(*testNode, point))
+            return testNode.get();
+    }
 
-            // Arc the outer corner.
-            path.addArcTo(FloatPoint(fromRect->maxX(), effectiveY), FloatPoint(fromRect->maxX(), effectiveY) - horizontalRadius, radius);
+    m_allNodes.append(std::make_unique<FloatPointGraph::Node>(point));
+    return m_allNodes.last().get();
+}
 
-            // Across the bottom, towards the left.
-            path.addLineTo(toRectMaxXMinYCorner + horizontalRadius);
+static bool findLineSegmentIntersection(const FloatPointGraph::Edge& edgeA, const FloatPointGraph::Edge& edgeB, FloatPoint& intersectionPoint)
+{
+    if (!findIntersection(*edgeA.first, *edgeA.second, *edgeB.first, *edgeB.second, intersectionPoint))
+        return false;
 
-            // Arc the inner corner.
-            path.addArcTo(toRectMaxXMinYCorner, toRectMaxXMinYCorner + verticalRadius, radius);
-        } else {
-            float effectiveY = std::min(toRect->y(), fromRect->maxY());
-            FloatPoint toRectMaxXMinYCorner = FloatPoint(toRect->maxX(), effectiveY);
+    FloatPoint edgeAVec(*edgeA.second - *edgeA.first);
+    FloatPoint edgeBVec(*edgeB.second - *edgeB.first);
 
-            // Down the right.
-            path.addLineTo(FloatPoint(fromRect->maxX(), effectiveY) - verticalRadius);
+    float dotA = edgeAVec.dot(toFloatPoint(intersectionPoint - *edgeA.first));
+    if (dotA < 0 || dotA > edgeAVec.lengthSquared())
+        return false;
 
-            // Arc the inner corner.
-            path.addArcTo(FloatPoint(fromRect->maxX(), effectiveY), FloatPoint(fromRect->maxX(), effectiveY) + horizontalRadius, radius);
+    float dotB = edgeBVec.dot(toFloatPoint(intersectionPoint - *edgeB.first));
+    if (dotB < 0 || dotB > edgeBVec.lengthSquared())
+        return false;
 
-            // Across the bottom, towards the right.
-            path.addLineTo(toRectMaxXMinYCorner - horizontalRadius);
+    return true;
+}
 
-            // Arc the outer corner.
-            path.addArcTo(toRectMaxXMinYCorner, toRectMaxXMinYCorner + verticalRadius, radius);
+static bool addIntersectionPoints(Vector<FloatPointGraph::Polygon>& polys, FloatPointGraph& graph)
+{
+    bool foundAnyIntersections = false;
+
+    Vector<FloatPointGraph::Edge> allEdges;
+    for (auto& poly : polys)
+        allEdges.appendVector(poly);
+
+    for (const FloatPointGraph::Edge& edgeA : allEdges) {
+        Vector<FloatPointGraph::Node*> intersectionPoints({edgeA.first, edgeA.second});
+
+        for (const FloatPointGraph::Edge& edgeB : allEdges) {
+            if (&edgeA == &edgeB)
+                continue;
+
+            FloatPoint intersectionPoint;
+            if (!findLineSegmentIntersection(edgeA, edgeB, intersectionPoint))
+                continue;
+            foundAnyIntersections = true;
+            intersectionPoints.append(graph.findOrCreateNode(intersectionPoint));
         }
+
+        std::sort(intersectionPoints.begin(), intersectionPoints.end(), [edgeA] (FloatPointGraph::Node* a, FloatPointGraph::Node* b) {
+            return FloatPoint(*edgeA.first - *b).lengthSquared() > FloatPoint(*edgeA.first - *a).lengthSquared();
+        });
+
+        for (unsigned pointIndex = 1; pointIndex < intersectionPoints.size(); pointIndex++)
+            intersectionPoints[pointIndex - 1]->addNextPoint(intersectionPoints[pointIndex]);
     }
+
+    return foundAnyIntersections;
 }
 
-static void addShrinkWrapLeftCorner(Path& path, const FloatRect* fromRect, const FloatRect* toRect, float radius)
+static FloatPointGraph::Polygon walkGraphAndExtractPolygon(FloatPointGraph::Node* startNode)
 {
-    FloatSize horizontalRadius(radius, 0);
-    FloatSize verticalRadius(0, radius);
+    FloatPointGraph::Polygon outPoly;
 
-    if (!fromRect) {
-        // For the first (bottom) rect:
+    FloatPointGraph::Node* currentNode = startNode;
+    FloatPointGraph::Node* previousNode = startNode;
 
-        // Across the bottom, towards the left.
-        path.addLineTo(toRect->minXMaxYCorner() + horizontalRadius);
+    do {
+        currentNode->visit();
 
-        // Arc the bottom corner.
-        path.addArcTo(toRect->minXMaxYCorner(), toRect->minXMaxYCorner() - verticalRadius, radius);
+        FloatPoint currentVec(*previousNode - *currentNode);
+        currentVec.normalize();
 
-    } else if (!toRect) {
-        // For the last (top) rect:
+        // Walk the graph, at each node choosing the next non-visited
+        // point with the greatest internal angle.
+        FloatPointGraph::Node* nextNode = nullptr;
+        float nextNodeAngle = 0;
+        for (auto potentialNextNode : currentNode->nextPoints()) {
+            if (potentialNextNode == currentNode)
+                continue;
 
-        // Up the left.
-        path.addLineTo(fromRect->minXMinYCorner() + verticalRadius);
+            // If we can get back to the start, we should, ignoring the fact that we already visited it.
+            // Otherwise we'll head inside the shape.
+            if (potentialNextNode == startNode) {
+                nextNode = startNode;
+                break;
+            }
 
-        // Arc the top corner.
-        path.addArcTo(fromRect->minXMinYCorner(), fromRect->minXMinYCorner() + horizontalRadius, radius);
-    } else {
-        // For middle rects:
-        float leftEdgeDifference = fromRect->x() - toRect->x();
+            if (potentialNextNode->isVisited())
+                continue;
 
-        // Skip over rects with equal edges, because we can't make
-        // sensible curves between them.
-        if (fabsf(leftEdgeDifference) < std::numeric_limits<float>::epsilon())
-            return;
+            FloatPoint nextVec(*potentialNextNode - *currentNode);
+            nextVec.normalize();
 
-        if (leftEdgeDifference < 0) {
-            float effectiveY = std::min(toRect->maxY(), fromRect->y());
-            FloatPoint toRectMinXMaxYCorner = FloatPoint(toRect->x(), effectiveY);
+            float angle = acos(nextVec.dot(currentVec));
+            float crossZ = nextVec.x() * currentVec.y() - nextVec.y() * currentVec.x();
 
-            // Up the right.
-            path.addLineTo(FloatPoint(fromRect->x(), effectiveY) + verticalRadius);
+            if (crossZ < 0)
+                angle = (2 * M_PI) - angle;
 
-            // Arc the inner corner.
-            path.addArcTo(FloatPoint(fromRect->x(), effectiveY), FloatPoint(fromRect->x(), effectiveY) + horizontalRadius, radius);
+            if (!nextNode || angle > nextNodeAngle) {
+                nextNode = potentialNextNode;
+                nextNodeAngle = angle;
+            }
+        }
 
-            // Across the bottom, towards the right.
-            path.addLineTo(toRectMinXMaxYCorner - horizontalRadius);
+        // If we don't end up at a node adjacent to the starting node,
+        // something went wrong (there's probably a hole in the shape),
+        // so bail out. We'll use a bounding box instead.
+        if (!nextNode)
+            return FloatPointGraph::Polygon();
 
-            // Arc the outer corner.
-            path.addArcTo(toRectMinXMaxYCorner, toRectMinXMaxYCorner - verticalRadius, radius);
-        } else {
-            float effectiveY = std::max(toRect->maxY(), fromRect->y());
-            FloatPoint toRectMinXMaxYCorner = FloatPoint(toRect->x(), effectiveY);
+        outPoly.append(std::make_pair(currentNode, nextNode));
 
-            // Up the right.
-            path.addLineTo(FloatPoint(fromRect->x(), effectiveY) + verticalRadius);
+        previousNode = currentNode;
+        currentNode = nextNode;
+    } while (currentNode != startNode);
 
-            // Arc the outer corner.
-            path.addArcTo(FloatPoint(fromRect->x(), effectiveY), FloatPoint(fromRect->x(), effectiveY) - horizontalRadius, radius);
+    return outPoly;
+}
 
-            // Across the bottom, towards the left.
-            path.addLineTo(toRectMinXMaxYCorner + horizontalRadius);
+static FloatPointGraph::Node* findUnvisitedPolygonStartPoint(Vector<FloatPointGraph::Polygon>& polys)
+{
+    for (auto& poly : polys) {
+        for (auto& edge : poly) {
+            if (edge.first->isVisited() || edge.second->isVisited())
+                goto nextPolygon;
+        }
 
-            // Arc the inner corner.
-            path.addArcTo(toRectMinXMaxYCorner, toRectMinXMaxYCorner - verticalRadius, radius);
-        }
+        // FIXME: We should make sure we find an outside edge to start with.
+        return poly[0].first;
+    nextPolygon:
+        continue;
     }
+    return nullptr;
 }
 
-static void addShrinkWrappedPathForRects(Path& path, Vector<FloatRect>& rects, float radius)
+static Vector<FloatPointGraph::Polygon> unitePolygons(Vector<FloatPointGraph::Polygon>& polys, FloatPointGraph& graph)
 {
-    size_t rectCount = rects.size();
+    graph.reset();
 
-    std::sort(rects.begin(), rects.end(), [](FloatRect a, FloatRect b) { return b.y() > a.y(); });
+    // There are no intersections, so the polygons are disjoint (we already removed wholly-contained rects in an earlier step).
+    if (!addIntersectionPoints(polys, graph))
+        return polys;
 
-    for (size_t i = 0; i <= rectCount; ++i)
-        addShrinkWrapRightCorner(path, i ? &rects[i - 1] : nullptr, i < rectCount ? &rects[i] : nullptr, radius);
+    Vector<FloatPointGraph::Polygon> unitedPolygons;
 
-    for (size_t i = 0; i <= rectCount; ++i) {
-        size_t reverseIndex = rectCount - i;
-        addShrinkWrapLeftCorner(path, reverseIndex < rectCount ? &rects[reverseIndex] : nullptr, reverseIndex ? &rects[reverseIndex - 1] : nullptr, radius);
+    while (FloatPointGraph::Node* startNode = findUnvisitedPolygonStartPoint(polys)) {
+        FloatPointGraph::Polygon unitedPolygon = walkGraphAndExtractPolygon(startNode);
+        if (unitedPolygon.isEmpty())
+            return Vector<FloatPointGraph::Polygon>();
+        unitedPolygons.append(unitedPolygon);
     }
 
-    path.closeSubpath();
+    return unitedPolygons;
 }
 
-static bool rectsIntersectOrTouch(const FloatRect& a, const FloatRect& b)
+static FloatPointGraph::Polygon edgesForRect(FloatRect rect, FloatPointGraph& graph)
 {
-    return !a.isEmpty() && !b.isEmpty()
-        && a.x() <= b.maxX() && b.x() <= a.maxX()
-        && a.y() <= b.maxY() && b.y() <= a.maxY();
+    auto minMin = graph.findOrCreateNode(rect.minXMinYCorner());
+    auto minMax = graph.findOrCreateNode(rect.minXMaxYCorner());
+    auto maxMax = graph.findOrCreateNode(rect.maxXMaxYCorner());
+    auto maxMin = graph.findOrCreateNode(rect.maxXMinYCorner());
+
+    return FloatPointGraph::Polygon({
+        std::make_pair(minMin, maxMin),
+        std::make_pair(maxMin, maxMax),
+        std::make_pair(maxMax, minMax),
+        std::make_pair(minMax, minMin)
+    });
 }
 
-static Vector<FloatRect>* findSetContainingRect(Vector<Vector<FloatRect>>& sets, FloatRect rect)
+Path PathUtilities::pathWithShrinkWrappedRects(const Vector<FloatRect>& rects, float radius)
 {
-    for (auto& set : sets) {
-        if (set.contains(rect))
-            return &set;
+    Path path;
+
+    if (rects.isEmpty())
+        return path;
+
+    if (rects.size() > 20) {
+        path.addRoundedRect(unionRect(rects), FloatSize(radius, radius));
+        return path;
     }
 
-    return nullptr;
-}
+    Vector<FloatRect> sortedRects = rects;
 
-static Vector<Vector<FloatRect>> contiguousRectGroupsFromRects(const Vector<FloatRect>& rects)
-{
-    Vector<std::pair<FloatRect, FloatRect>> intersections;
-    Vector<FloatRect> soloRects = rects;
+    std::sort(sortedRects.begin(), sortedRects.end(), [](FloatRect a, FloatRect b) { return b.y() > a.y(); });
 
-    for (auto& rectA : rects) {
-        for (auto& rectB : rects) {
-            if (rectA == rectB)
+    FloatPointGraph graph;
+    Vector<FloatPointGraph::Polygon> rectPolygons;
+    rectPolygons.reserveInitialCapacity(sortedRects.size());
+
+    for (auto& rect : sortedRects) {
+        bool isContained = false;
+        for (auto& otherRect : sortedRects) {
+            if (&rect == &otherRect)
                 continue;
-
-            if (rectsIntersectOrTouch(rectA, rectB)) {
-                intersections.append(std::make_pair(rectA, rectB));
-                soloRects.removeAllMatching([rectA, rectB](FloatRect q) { return q == rectA || q == rectB; });
+            if (otherRect.contains(rect)) {
+                isContained = true;
+                break;
             }
         }
+
+        if (!isContained)
+            rectPolygons.append(edgesForRect(rect, graph));
     }
 
-    Vector<Vector<FloatRect>> rectSets;
+    Vector<FloatPointGraph::Polygon> polys = unitePolygons(rectPolygons, graph);
 
-    for (auto& intersectingPair : intersections) {
-        if (Vector<FloatRect>* rectContainingFirst = findSetContainingRect(rectSets, intersectingPair.first)) {
-            if (!rectContainingFirst->contains(intersectingPair.second))
-                rectContainingFirst->append(intersectingPair.second);
-            continue;
-        }
+    if (polys.isEmpty()) {
+        path.addRoundedRect(unionRect(sortedRects), FloatSize(radius, radius));
+        return path;
+    }
 
-        if (Vector<FloatRect>* rectContainingSecond = findSetContainingRect(rectSets, intersectingPair.second)) {
-            if (!rectContainingSecond->contains(intersectingPair.first))
-                rectContainingSecond->append(intersectingPair.first);
-            continue;
-        }
+    for (auto& poly : polys) {
+        for (unsigned i = 0; i < poly.size(); i++) {
+            FloatPointGraph::Edge& toEdge = poly[i];
+            // Connect the first edge to the last.
+            FloatPointGraph::Edge& fromEdge = (i > 0) ? poly[i - 1] : poly[poly.size() - 1];
 
-        // We didn't find a set including either of our rects, so start a new one.
-        rectSets.append(Vector<FloatRect>({intersectingPair.first, intersectingPair.second}));
+            FloatPoint fromEdgeVec = toFloatPoint(*fromEdge.second - *fromEdge.first);
+            FloatPoint toEdgeVec = toFloatPoint(*toEdge.second - *toEdge.first);
 
-        continue;
-    }
+            // Clamp the radius to no more than half the length of either adjacent edge,
+            // because we want a smooth curve and don't want unequal radii.
+            float clampedRadius = std::min(radius, fabsf(fromEdgeVec.x() ? fromEdgeVec.x() : fromEdgeVec.y()) / 2);
+            clampedRadius = std::min(clampedRadius, fabsf(toEdgeVec.x() ? toEdgeVec.x() : toEdgeVec.y()) / 2);
 
-    for (auto& rect : soloRects) {
-        ASSERT(!findSetContainingRect(rectSets, rect));
-        rectSets.append(Vector<FloatRect>({rect}));
-    }
+            FloatPoint fromEdgeNorm = fromEdgeVec;
+            fromEdgeNorm.normalize();
+            FloatPoint toEdgeNorm = toEdgeVec;
+            toEdgeNorm.normalize();
 
-    return rectSets;
-}
+            // Project the radius along the incoming and outgoing edge.
+            FloatSize fromOffset = clampedRadius * toFloatSize(fromEdgeNorm);
+            FloatSize toOffset = clampedRadius * toFloatSize(toEdgeNorm);
 
-Path PathUtilities::pathWithShrinkWrappedRects(const Vector<FloatRect>& rects, float radius)
-{
-    Path path;
+            if (!i)
+                path.moveTo(*fromEdge.second - fromOffset);
+            else
+                path.addLineTo(*fromEdge.second - fromOffset);
+            path.addArcTo(*fromEdge.second, *toEdge.first + toOffset, clampedRadius);
+        }
 
-    if (rects.isEmpty())
-        return path;
+        path.closeSubpath();
+    }
 
-    Vector<Vector<FloatRect>> rectSets = contiguousRectGroupsFromRects(rects);
-
-    for (auto& set : rectSets)
-        addShrinkWrappedPathForRects(path, set, radius);
-    
     return path;
 }
 

Modified: trunk/Source/WebCore/testing/Internals.cpp (186975 => 186976)


--- trunk/Source/WebCore/testing/Internals.cpp	2015-07-17 23:47:47 UTC (rev 186975)
+++ trunk/Source/WebCore/testing/Internals.cpp	2015-07-17 23:55:59 UTC (rev 186976)
@@ -2942,7 +2942,7 @@
     return testPreloadScannerViewportSupport(contextDocument());
 }
 
-PassRefPtr<DOMPath> Internals::pathWithShrinkWrappedRects(Vector<double> rectComponents, ExceptionCode& ec)
+PassRefPtr<DOMPath> Internals::pathWithShrinkWrappedRects(Vector<double> rectComponents, double radius, ExceptionCode& ec)
 {
     if (rectComponents.size() % 4) {
         ec = INVALID_ACCESS_ERR;
@@ -2961,8 +2961,7 @@
 
     rects.reverse();
 
-    // FIXME: radius should be a parameter instead of fixed as 8.
-    Path path = PathUtilities::pathWithShrinkWrappedRects(rects, 8);
+    Path path = PathUtilities::pathWithShrinkWrappedRects(rects, radius);
     return DOMPath::create(path);
 }
 

Modified: trunk/Source/WebCore/testing/Internals.h (186975 => 186976)


--- trunk/Source/WebCore/testing/Internals.h	2015-07-17 23:47:47 UTC (rev 186975)
+++ trunk/Source/WebCore/testing/Internals.h	2015-07-17 23:55:59 UTC (rev 186976)
@@ -417,7 +417,7 @@
     String scrollSnapOffsets(Element*, ExceptionCode&);
 #endif
 
-    PassRefPtr<DOMPath> pathWithShrinkWrappedRects(Vector<double>, ExceptionCode&);
+    PassRefPtr<DOMPath> pathWithShrinkWrappedRects(Vector<double> rectComponents, double radius, ExceptionCode&);
 
 private:
     explicit Internals(Document*);

Modified: trunk/Source/WebCore/testing/Internals.idl (186975 => 186976)


--- trunk/Source/WebCore/testing/Internals.idl	2015-07-17 23:47:47 UTC (rev 186975)
+++ trunk/Source/WebCore/testing/Internals.idl	2015-07-17 23:55:59 UTC (rev 186976)
@@ -378,5 +378,5 @@
     [RaisesException] DOMString scrollSnapOffsets(Element element);
 #endif
 
-    [RaisesException] DOMPath pathWithShrinkWrappedRects(sequence<double> rectComponents);
+    [RaisesException] DOMPath pathWithShrinkWrappedRects(sequence<double> rectComponents, double radius);
 };
_______________________________________________
webkit-changes mailing list
[email protected]
https://lists.webkit.org/mailman/listinfo/webkit-changes

Reply via email to