From a7a44cb89a3457819c17d5ce2ba336fa1f7d4deb Mon Sep 17 00:00:00 2001 From: Noel Grandin Date: Fri, 20 Mar 2026 18:53:32 +0200 Subject: [PATCH] cool#15052 reduce time spent calculating selection outline when it gets complex the solvePolygonOperationOr() algorithm becomes rather expensive. But we can do much better in this case since we know we only have rectangles. Change-Id: I2fdfce7925e843e25f11890dac2c96ce21f3be67 Reviewed-on: https://gerrit.libreoffice.org/c/core/+/202261 Tested-by: Jenkins Reviewed-by: Noel Grandin --- .../source/polygon/b2dpolypolygoncutter.cxx | 129 ++++++++++++++++++ .../basegfx/polygon/b2dpolypolygoncutter.hxx | 4 + svx/source/sdr/overlay/overlayselection.cxx | 59 ++------ sw/source/core/crsr/overlayrangesoutline.cxx | 28 +--- sw/source/uibase/docvw/OverlayRanges.cxx | 28 +--- 5 files changed, 147 insertions(+), 101 deletions(-) diff --git a/basegfx/source/polygon/b2dpolypolygoncutter.cxx b/basegfx/source/polygon/b2dpolypolygoncutter.cxx index 3e43a99b04ff..464336f98c59 100644 --- a/basegfx/source/polygon/b2dpolypolygoncutter.cxx +++ b/basegfx/source/polygon/b2dpolypolygoncutter.cxx @@ -31,6 +31,7 @@ #include #include #include +#include #include #include @@ -1119,6 +1120,134 @@ namespace basegfx::utils return B2DPolyPolygon(); } + namespace + { + struct B2DPointCompare + { + bool operator()(const basegfx::B2DPoint& lhs, const basegfx::B2DPoint& rhs) const + { + return std::make_pair(lhs.getX(), lhs.getY()) < std::make_pair(rhs.getX(), rhs.getY()); + } + }; + + struct B2DPointCompareYThenX + { + bool operator()(const basegfx::B2DPoint& lhs, const basegfx::B2DPoint& rhs) const + { + return std::make_pair(lhs.getY(), lhs.getX()) < std::make_pair(rhs.getY(), rhs.getX()); + } + }; + + } + + // Combine rectangles geometrically to a single OR'ed polygon. + // Algorithm is from + // "Uniqueness of orthogonal connect-the-dots" Joseph O'Rourke 1988 + // The basic algorithm is: + // Sort points by lowest x, lowest y + // Go through each column and create edges between the vertices 2i and 2i + 1 in that column + // Sort points by lowest y, lowest x + // Go through each row and create edges between the vertices 2i and 2i + 1 in that row. + // + basegfx::B2DPolyPolygon combineRectanglesToPolyPolygon(const std::vector< basegfx::B2DRange >& rRectangles) + { + o3tl::sorted_vector sort_x; + for (auto const & rRect : rRectangles) + { + auto checkPoint = [&sort_x](double x, double y) + { + basegfx::B2DPoint pt(x, y); + auto it = sort_x.find(pt); + if (it != sort_x.end()) // Shared vertice, remove it. + sort_x.erase(it); + else + sort_x.insert(pt); + }; + checkPoint(rRect.getMinX(), rRect.getMinY()); + checkPoint(rRect.getMinX(), rRect.getMaxY()); + checkPoint(rRect.getMaxX(), rRect.getMinY()); + checkPoint(rRect.getMaxX(), rRect.getMaxY()); + } + + o3tl::sorted_vector sort_y; + for (auto const & i : sort_x) + sort_y.insert(i); + + std::map edges_h; + std::map edges_v; + + size_t i = 0; + while (i < sort_x.size()) + { + auto curr_y = sort_y[i].getY(); + while (i < sort_x.size() && sort_y[i].getY() == curr_y) + { + edges_h[sort_y[i]] = sort_y[i + 1]; + edges_h[sort_y[i + 1]] = sort_y[i]; + i += 2; + } + } + i = 0; + while (i < sort_x.size()) + { + auto curr_x = sort_x[i].getX(); + while (i < sort_x.size() && sort_x[i].getX() == curr_x) + { + edges_v[sort_x[i]] = sort_x[i + 1]; + edges_v[sort_x[i + 1]] = sort_x[i]; + i += 2; + } + } + + // Get all the polygons. + basegfx::B2DPolyPolygon aPolyPolygon; + std::vector> tmpPolygon; + while (!edges_h.empty()) + { + tmpPolygon.clear(); + // We can start with any point. + basegfx::B2DPoint pt = edges_h.begin()->first; + edges_h.erase(edges_h.begin()); + tmpPolygon.push_back({pt, false}); + for (;;) + { + auto [curr, e] = tmpPolygon.back(); + if (!e) + { + auto it = edges_v.find(curr); + auto next_vertex = it->second; + edges_v.erase(it); + tmpPolygon.push_back({next_vertex, true}); + } + else + { + auto it = edges_h.find(curr); + auto next_vertex = it->second; + edges_h.erase(it); + tmpPolygon.push_back({next_vertex, false}); + } + if (tmpPolygon.back() == tmpPolygon.front()) + { + // Closed polygon + break; + } + } + for (auto const & pair : tmpPolygon) + { + auto const & vertex = std::get<0>(pair); + edges_h.erase(vertex); + edges_v.erase(vertex); + } + basegfx::B2DPolygon aPoly; + aPoly.reserve(tmpPolygon.size()); + for (auto const & pair : tmpPolygon) + aPoly.append(std::get<0>(pair)); + aPolyPolygon.append(std::move(aPoly)); + } + + return aPolyPolygon; + } + } // end of namespace /* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/include/basegfx/polygon/b2dpolypolygoncutter.hxx b/include/basegfx/polygon/b2dpolypolygoncutter.hxx index 55dd29cf8e52..3936ce64b599 100644 --- a/include/basegfx/polygon/b2dpolypolygoncutter.hxx +++ b/include/basegfx/polygon/b2dpolypolygoncutter.hxx @@ -139,6 +139,10 @@ namespace basegfx::utils */ BASEGFX_DLLPUBLIC B2DPolyPolygon mergeToSinglePolyPolygon(const B2DPolyPolygonVector& rInput); + /** + * version of solvePolygonOperationOr optimised for combining rectangles. + */ + BASEGFX_DLLPUBLIC basegfx::B2DPolyPolygon combineRectanglesToPolyPolygon(const std::vector< basegfx::B2DRange >& rRectangles); } // end of namespace basegfx::utils /* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/svx/source/sdr/overlay/overlayselection.cxx b/svx/source/sdr/overlay/overlayselection.cxx index 6f9284696d51..5f6058a67b05 100644 --- a/svx/source/sdr/overlay/overlayselection.cxx +++ b/svx/source/sdr/overlay/overlayselection.cxx @@ -37,54 +37,19 @@ namespace sdr::overlay { - - // combine ranges geometrically to a single, ORed polygon - static basegfx::B2DPolyPolygon impCombineRangesToPolyPolygon(const std::vector< basegfx::B2DRange >& rRanges, bool bOffset, double fOffset) + static basegfx::B2DPolyPolygon impCombineRectanglesToPolyPolygon(const std::vector< basegfx::B2DRange >& rRectangles, bool bOffset, double fOffset) { - const sal_uInt32 nCount(rRanges.size()); - basegfx::B2DPolyPolygon aRetval; - - for(sal_uInt32 a(0); a < nCount; a++) + if (!bOffset) + return basegfx::utils::combineRectanglesToPolyPolygon(rRectangles); + std::vector< basegfx::B2DRange > aGrownRectangles; + aGrownRectangles.reserve(rRectangles.size()); + for (const auto & rInput : rRectangles) { - basegfx::B2DRange aRange(rRanges[a]); - if (bOffset) - aRange.grow(fOffset); - const basegfx::B2DPolygon aDiscretePolygon(basegfx::utils::createPolygonFromRect(aRange)); - - if(0 == a) - { - aRetval.append(aDiscretePolygon); - } - else - { - aRetval = basegfx::utils::solvePolygonOperationOr(aRetval, basegfx::B2DPolyPolygon(aDiscretePolygon)); - } + basegfx::B2DRange aRange(rInput); + aRange.grow(fOffset); + aGrownRectangles.push_back(aRange); } - - return aRetval; - } - - // tdf#161204 Creates a poly-polygon using white hairline to provide contrast - static basegfx::B2DPolyPolygon impCombineRangesToContrastPolyPolygon(const std::vector< basegfx::B2DRange >& rRanges) - { - const sal_uInt32 nCount(rRanges.size()); - basegfx::B2DPolyPolygon aRetval; - - for(sal_uInt32 a(0); a < nCount; a++) - { - const basegfx::B2DPolygon aDiscretePolygon(basegfx::utils::createPolygonFromRect(rRanges[a])); - - if(0 == a) - { - aRetval.append(aDiscretePolygon); - } - else - { - aRetval = basegfx::utils::solvePolygonOperationOr(aRetval, basegfx::B2DPolyPolygon(aDiscretePolygon)); - } - } - - return aRetval; + return basegfx::utils::combineRectanglesToPolyPolygon(aGrownRectangles); } // check if wanted type OverlayType::Transparent or OverlayType::Solid @@ -169,7 +134,7 @@ namespace sdr::overlay // tdf#161204 Outline with white color to provide contrast if (mbContrastOutline) { - basegfx::B2DPolyPolygon aContrastPolyPolygon(impCombineRangesToContrastPolyPolygon(getRanges())); + basegfx::B2DPolyPolygon aContrastPolyPolygon(basegfx::utils::combineRectanglesToPolyPolygon(getRanges())); const drawinglayer::primitive2d::Primitive2DReference aContrastSelectionOutline( new drawinglayer::primitive2d::PolyPolygonHairlinePrimitive2D( std::move(aContrastPolyPolygon), @@ -183,7 +148,7 @@ namespace sdr::overlay fOffset = getOverlayManager()->getOutputDevice().PixelToLogic(Size(1, 1)).getWidth(); // External outline using themed color - basegfx::B2DPolyPolygon aPolyPolygon(impCombineRangesToPolyPolygon(getRanges(), mbContrastOutline, fOffset)); + basegfx::B2DPolyPolygon aPolyPolygon(impCombineRectanglesToPolyPolygon(getRanges(), mbContrastOutline, fOffset)); const drawinglayer::primitive2d::Primitive2DReference aSelectionOutline( new drawinglayer::primitive2d::PolyPolygonHairlinePrimitive2D( std::move(aPolyPolygon), diff --git a/sw/source/core/crsr/overlayrangesoutline.cxx b/sw/source/core/crsr/overlayrangesoutline.cxx index 265509a055ab..53657ef04a07 100644 --- a/sw/source/core/crsr/overlayrangesoutline.cxx +++ b/sw/source/core/crsr/overlayrangesoutline.cxx @@ -24,32 +24,6 @@ #include #include -namespace -{ - // combine ranges geometrically to a single, ORed polygon - basegfx::B2DPolyPolygon impCombineRangesToPolyPolygon(const std::vector< basegfx::B2DRange >& rRanges) - { - const sal_uInt32 nCount(rRanges.size()); - basegfx::B2DPolyPolygon aRetval; - - for(sal_uInt32 a(0); a < nCount; a++) - { - const basegfx::B2DPolygon aDiscretePolygon(basegfx::utils::createPolygonFromRect(rRanges[a])); - - if(0 == a) - { - aRetval.append(aDiscretePolygon); - } - else - { - aRetval = basegfx::utils::solvePolygonOperationOr(aRetval, basegfx::B2DPolyPolygon(aDiscretePolygon)); - } - } - - return aRetval; - } -} - namespace sw::overlay { drawinglayer::primitive2d::Primitive2DContainer OverlayRangesOutline::createOverlayObjectPrimitive2DSequence() @@ -60,7 +34,7 @@ namespace sw::overlay if( nCount ) { const basegfx::BColor aRGBColor(getBaseColor().getBColor()); - basegfx::B2DPolyPolygon aPolyPolygon(impCombineRangesToPolyPolygon(getRanges())); + basegfx::B2DPolyPolygon aPolyPolygon(basegfx::utils::combineRectanglesToPolyPolygon(getRanges())); const drawinglayer::primitive2d::Primitive2DReference aOutline( new drawinglayer::primitive2d::PolyPolygonHairlinePrimitive2D( std::move(aPolyPolygon), diff --git a/sw/source/uibase/docvw/OverlayRanges.cxx b/sw/source/uibase/docvw/OverlayRanges.cxx index 3201848bad18..4cce50f5f8bb 100644 --- a/sw/source/uibase/docvw/OverlayRanges.cxx +++ b/sw/source/uibase/docvw/OverlayRanges.cxx @@ -31,32 +31,6 @@ #include #include -namespace -{ - // combine ranges geometrically to a single, ORed polygon - basegfx::B2DPolyPolygon impCombineRangesToPolyPolygon(const std::vector< basegfx::B2DRange >& rRanges) - { - const sal_uInt32 nCount(rRanges.size()); - basegfx::B2DPolyPolygon aRetval; - - for(sal_uInt32 a(0); a < nCount; a++) - { - const basegfx::B2DPolygon aDiscretePolygon(basegfx::utils::createPolygonFromRect(rRanges[a])); - - if(0 == a) - { - aRetval.append(aDiscretePolygon); - } - else - { - aRetval = basegfx::utils::solvePolygonOperationOr(aRetval, basegfx::B2DPolyPolygon(aDiscretePolygon)); - } - } - - return aRetval; - } -} - namespace sw::overlay { drawinglayer::primitive2d::Primitive2DContainer OverlayRanges::createOverlayObjectPrimitive2DSequence() @@ -84,7 +58,7 @@ namespace sw::overlay if ( mbShowSolidBorder ) { const basegfx::BColor aRGBColor(getBaseColor().getBColor()); - basegfx::B2DPolyPolygon aPolyPolygon(impCombineRangesToPolyPolygon(getRanges())); + basegfx::B2DPolyPolygon aPolyPolygon(basegfx::utils::combineRectanglesToPolyPolygon(getRanges())); const drawinglayer::primitive2d::Primitive2DReference aOutline( new drawinglayer::primitive2d::PolyPolygonHairlinePrimitive2D( std::move(aPolyPolygon),