From d7f3ae4fc169036c58a85db3c6f6ecd332491725 Mon Sep 17 00:00:00 2001 From: reuk Date: Thu, 1 Dec 2022 17:44:51 +0000 Subject: [PATCH] RectangleList: Avoid entering a death spiral when subtracting very close rectangles --- .../geometry/juce_RectangleList.h | 92 +++++++++++++------ 1 file changed, 62 insertions(+), 30 deletions(-) diff --git a/modules/juce_graphics/geometry/juce_RectangleList.h b/modules/juce_graphics/geometry/juce_RectangleList.h index 8e106cd40e..c05a698433 100644 --- a/modules/juce_graphics/geometry/juce_RectangleList.h +++ b/modules/juce_graphics/geometry/juce_RectangleList.h @@ -201,76 +201,108 @@ public: Any rectangles in the list which overlap this will be clipped and subdivided if necessary. */ - void subtract (RectangleType rect) + void subtract (const RectangleType rect) { if (auto numRects = rects.size()) { - auto x1 = rect.getX(); - auto y1 = rect.getY(); - auto x2 = x1 + rect.getWidth(); - auto y2 = y1 + rect.getHeight(); + const auto x1 = rect.getX(); + const auto y1 = rect.getY(); + const auto x2 = x1 + rect.getWidth(); + const auto y2 = y1 + rect.getHeight(); for (int i = numRects; --i >= 0;) { auto& r = rects.getReference (i); - auto rx1 = r.getX(); - auto ry1 = r.getY(); - auto rx2 = rx1 + r.getWidth(); - auto ry2 = ry1 + r.getHeight(); + const auto rx1 = r.getX(); + const auto ry1 = r.getY(); + const auto rx2 = rx1 + r.getWidth(); + const auto ry2 = ry1 + r.getHeight(); - if (! (x2 <= rx1 || x1 >= rx2 || y2 <= ry1 || y1 >= ry2)) + const auto isNotEqual = [&] (const RectangleType newRect) { - if (x1 > rx1 && x1 < rx2) + // When subtracting tiny slices from relatively large rectangles, the + // subtraction may have no effect (due to limited-precision floating point + // maths) and the original rectangle may remain unchanged. + // We check that any 'new' rectangle has different dimensions to the rectangle + // being tested before adding it to the rects array. + // Integer arithmetic is not susceptible to this problem, so there's no need + // for this additional equality check when working with integral rectangles. + if constexpr (std::is_floating_point_v) { - if (y1 <= ry1 && y2 >= ry2 && x2 >= rx2) + return newRect != r; + } + else + { + ignoreUnused (newRect); + return true; + } + }; + + if (rx1 < x2 && x1 < rx2 && ry1 < y2 && y1 < ry2) + { + if (rx1 < x1 && x1 < rx2) + { + if (y1 <= ry1 && ry2 <= y2 && rx2 <= x2) { r.setWidth (x1 - rx1); } else { - r.setX (x1); - r.setWidth (rx2 - x1); + if (const RectangleType newRect (rx1, ry1, x1 - rx1, ry2 - ry1); isNotEqual (newRect)) + { + r.setX (x1); + r.setWidth (rx2 - x1); - rects.insert (++i, RectangleType (rx1, ry1, x1 - rx1, ry2 - ry1)); - ++i; + rects.insert (++i, newRect); + ++i; + } } } - else if (x2 > rx1 && x2 < rx2) + else if (rx1 < x2 && x2 < rx2) { r.setX (x2); r.setWidth (rx2 - x2); - if (y1 > ry1 || y2 < ry2 || x1 > rx1) + if (ry1 < y1 || y2 < ry2 || rx1 < x1) { - rects.insert (++i, RectangleType (rx1, ry1, x2 - rx1, ry2 - ry1)); - ++i; + if (const RectangleType newRect (rx1, ry1, x2 - rx1, ry2 - ry1); isNotEqual (newRect)) + { + rects.insert (++i, newRect); + ++i; + } } } - else if (y1 > ry1 && y1 < ry2) + else if (ry1 < y1 && y1 < ry2) { - if (x1 <= rx1 && x2 >= rx2 && y2 >= ry2) + if (x1 <= rx1 && rx2 <= x2 && ry2 <= y2) { r.setHeight (y1 - ry1); } else { - r.setY (y1); - r.setHeight (ry2 - y1); + if (const RectangleType newRect (rx1, ry1, rx2 - rx1, y1 - ry1); isNotEqual (newRect)) + { + r.setY (y1); + r.setHeight (ry2 - y1); - rects.insert (++i, RectangleType (rx1, ry1, rx2 - rx1, y1 - ry1)); - ++i; + rects.insert (++i, newRect); + ++i; + } } } - else if (y2 > ry1 && y2 < ry2) + else if (ry1 < y2 && y2 < ry2) { r.setY (y2); r.setHeight (ry2 - y2); - if (x1 > rx1 || x2 < rx2 || y1 > ry1) + if (rx1 < x1 || x2 < rx2 || ry1 < y1) { - rects.insert (++i, RectangleType (rx1, ry1, rx2 - rx1, y2 - ry1)); - ++i; + if (const RectangleType newRect (rx1, ry1, rx2 - rx1, y2 - ry1); isNotEqual (newRect)) + { + rects.insert (++i, newRect); + ++i; + } } } else