mirror of
https://github.com/juce-framework/JUCE.git
synced 2026-01-09 23:34:20 +00:00
RectangleList: Avoid infinite loops due to numerical precision issues
This commit is contained in:
parent
92573ca1ad
commit
d10c5c36e3
43 changed files with 376 additions and 73 deletions
|
|
@ -212,111 +212,95 @@ public:
|
|||
*/
|
||||
void subtract (const RectangleType rect)
|
||||
{
|
||||
using PointType = Point<ValueType>;
|
||||
|
||||
const auto numRects = rects.size();
|
||||
|
||||
if (numRects == 0)
|
||||
return;
|
||||
|
||||
const auto x1 = rect.getX();
|
||||
const auto y1 = rect.getY();
|
||||
const auto x2 = x1 + rect.getWidth();
|
||||
const auto y2 = y1 + rect.getHeight();
|
||||
struct AABB
|
||||
{
|
||||
AABB() = default;
|
||||
AABB (const RectangleType& r) : tl (r.getTopLeft()), br (r.getBottomRight()) {}
|
||||
AABB (PointType a, PointType b) : tl (a), br (b) {}
|
||||
operator RectangleType() const { return RectangleType { tl, br }; }
|
||||
|
||||
bool completelyOutside (const AABB& other) const
|
||||
{
|
||||
return other.br.x <= tl.x || br.x <= other.tl.x || other.br.y <= tl.y || br.y <= other.tl.y;
|
||||
}
|
||||
|
||||
PointType tl, br;
|
||||
};
|
||||
|
||||
Array<AABB> aabbs;
|
||||
aabbs.resize (rects.size());
|
||||
std::copy (rects.begin(), rects.end(), aabbs.begin());
|
||||
const AABB aabb { rect };
|
||||
|
||||
for (int i = numRects; --i >= 0;)
|
||||
{
|
||||
auto& r = rects.getReference (i);
|
||||
auto& rRef = aabbs.getReference (i);;
|
||||
const auto r = rRef;
|
||||
|
||||
const auto rx1 = r.getX();
|
||||
const auto ry1 = r.getY();
|
||||
const auto rx2 = rx1 + r.getWidth();
|
||||
const auto ry2 = ry1 + r.getHeight();
|
||||
if (r.completelyOutside (aabb))
|
||||
continue;
|
||||
|
||||
const auto isNotEqual = [&] (const RectangleType newRect)
|
||||
if (r.tl.x < aabb.tl.x && aabb.tl.x < r.br.x)
|
||||
{
|
||||
// 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<ValueType>)
|
||||
if (aabb.tl.y <= r.tl.y && r.br.y <= aabb.br.y && r.br.x <= aabb.br.x)
|
||||
{
|
||||
return newRect != r;
|
||||
rRef.br.x = aabb.tl.x;
|
||||
}
|
||||
else
|
||||
{
|
||||
ignoreUnused (newRect);
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
if (! (rx1 < x2 && x1 < rx2 && ry1 < y2 && y1 < ry2))
|
||||
continue;
|
||||
|
||||
if (rx1 < x1 && x1 < rx2)
|
||||
{
|
||||
if (y1 <= ry1 && ry2 <= y2 && rx2 <= x2)
|
||||
{
|
||||
r.setWidth (x1 - rx1);
|
||||
}
|
||||
else if (const RectangleType newRect (rx1, ry1, x1 - rx1, ry2 - ry1); isNotEqual (newRect))
|
||||
{
|
||||
r.setX (x1);
|
||||
r.setWidth (rx2 - x1);
|
||||
|
||||
rects.insert (++i, newRect);
|
||||
rRef.tl.x = aabb.tl.x;
|
||||
aabbs.insert (++i, { r.tl, { aabb.tl.x, r.br.y } });
|
||||
++i;
|
||||
}
|
||||
}
|
||||
else if (rx1 < x2 && x2 < rx2)
|
||||
else if (r.tl.x < aabb.br.x && aabb.br.x < r.br.x)
|
||||
{
|
||||
r.setX (x2);
|
||||
r.setWidth (rx2 - x2);
|
||||
rRef.tl.x = aabb.br.x;
|
||||
|
||||
if (ry1 < y1 || y2 < ry2 || rx1 < x1)
|
||||
if (r.tl.y < aabb.tl.y || aabb.br.y < r.br.y || r.tl.x < aabb.tl.x)
|
||||
{
|
||||
if (const RectangleType newRect (rx1, ry1, x2 - rx1, ry2 - ry1); isNotEqual (newRect))
|
||||
{
|
||||
rects.insert (++i, newRect);
|
||||
++i;
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (ry1 < y1 && y1 < ry2)
|
||||
{
|
||||
if (x1 <= rx1 && rx2 <= x2 && ry2 <= y2)
|
||||
{
|
||||
r.setHeight (y1 - ry1);
|
||||
}
|
||||
else if (const RectangleType newRect (rx1, ry1, rx2 - rx1, y1 - ry1); isNotEqual (newRect))
|
||||
{
|
||||
r.setY (y1);
|
||||
r.setHeight (ry2 - y1);
|
||||
|
||||
rects.insert (++i, newRect);
|
||||
aabbs.insert (++i, { r.tl, { aabb.br.x, r.br.y } });
|
||||
++i;
|
||||
}
|
||||
}
|
||||
else if (ry1 < y2 && y2 < ry2)
|
||||
else if (r.tl.y < aabb.tl.y && aabb.tl.y < r.br.y)
|
||||
{
|
||||
r.setY (y2);
|
||||
r.setHeight (ry2 - y2);
|
||||
|
||||
if (rx1 < x1 || x2 < rx2 || ry1 < y1)
|
||||
if (aabb.tl.x <= r.tl.x && r.br.x <= aabb.br.x && r.br.y <= aabb.br.y)
|
||||
{
|
||||
if (const RectangleType newRect (rx1, ry1, rx2 - rx1, y2 - ry1); isNotEqual (newRect))
|
||||
{
|
||||
rects.insert (++i, newRect);
|
||||
++i;
|
||||
}
|
||||
rRef.br.y = aabb.tl.y;
|
||||
}
|
||||
else
|
||||
{
|
||||
rRef.tl.y = aabb.tl.y;
|
||||
aabbs.insert (++i, { r.tl, { r.br.x, aabb.tl.y } });
|
||||
++i;
|
||||
}
|
||||
}
|
||||
else if (r.tl.y < aabb.br.y && aabb.br.y < r.br.y)
|
||||
{
|
||||
rRef.tl.y = aabb.br.y;
|
||||
|
||||
if (r.tl.x < aabb.tl.x || aabb.br.x < r.br.x || r.tl.y < aabb.tl.y)
|
||||
{
|
||||
aabbs.insert (++i, { r.tl, { r.br.x, aabb.br.y } });
|
||||
++i;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
rects.remove (i);
|
||||
aabbs.remove (i);
|
||||
}
|
||||
}
|
||||
|
||||
rects.resize (aabbs.size());
|
||||
std::copy (aabbs.begin(), aabbs.end(), rects.begin());
|
||||
}
|
||||
|
||||
/** Removes all areas in another RectangleList from this one.
|
||||
|
|
|
|||
202
modules/juce_graphics/geometry/juce_RectangleList_test.cpp
Normal file
202
modules/juce_graphics/geometry/juce_RectangleList_test.cpp
Normal file
|
|
@ -0,0 +1,202 @@
|
|||
/*
|
||||
==============================================================================
|
||||
|
||||
This file is part of the JUCE framework.
|
||||
Copyright (c) Raw Material Software Limited
|
||||
|
||||
JUCE is an open source framework subject to commercial or open source
|
||||
licensing.
|
||||
|
||||
By downloading, installing, or using the JUCE framework, or combining the
|
||||
JUCE framework with any other source code, object code, content or any other
|
||||
copyrightable work, you agree to the terms of the JUCE End User Licence
|
||||
Agreement, and all incorporated terms including the JUCE Privacy Policy and
|
||||
the JUCE Website Terms of Service, as applicable, which will bind you. If you
|
||||
do not agree to the terms of these agreements, we will not license the JUCE
|
||||
framework to you, and you must discontinue the installation or download
|
||||
process and cease use of the JUCE framework.
|
||||
|
||||
JUCE End User Licence Agreement: https://juce.com/legal/juce-8-licence/
|
||||
JUCE Privacy Policy: https://juce.com/juce-privacy-policy
|
||||
JUCE Website Terms of Service: https://juce.com/juce-website-terms-of-service/
|
||||
|
||||
Or:
|
||||
|
||||
You may also use this code under the terms of the AGPLv3:
|
||||
https://www.gnu.org/licenses/agpl-3.0.en.html
|
||||
|
||||
THE JUCE FRAMEWORK IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL
|
||||
WARRANTIES, WHETHER EXPRESSED OR IMPLIED, INCLUDING WARRANTY OF
|
||||
MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE, ARE DISCLAIMED.
|
||||
|
||||
==============================================================================
|
||||
*/
|
||||
|
||||
namespace juce
|
||||
{
|
||||
|
||||
class RectangleListTests : public UnitTest
|
||||
{
|
||||
public:
|
||||
RectangleListTests() : UnitTest ("RectangleList", UnitTestCategories::graphics) {}
|
||||
|
||||
void runTest() override
|
||||
{
|
||||
beginTest ("Avoid infinite loops when adding rectangles");
|
||||
{
|
||||
const Rectangle<float> rectanglesA[]
|
||||
{
|
||||
{ { -9.15555f, 5.06667f }, { 12.f, 5.15556f } },
|
||||
{ { -9.11111f, 5.11111f }, { 12.f, 5.2f } },
|
||||
};
|
||||
|
||||
RectangleList<float> a;
|
||||
|
||||
for (const auto& rect : rectanglesA)
|
||||
a.add (rect);
|
||||
|
||||
const std::set<Rectangle<float>> expectedA { { { -9.15555f, 5.06666994f }, { 12.0f, 5.15556f } },
|
||||
{ { -9.11111f, 5.15556f }, { 12.0f, 5.2f } } };
|
||||
|
||||
for (const auto& r : expectedA)
|
||||
expect (std::find (a.begin(), a.end(), r) != a.end());
|
||||
|
||||
const Rectangle<float> rectanglesB[]
|
||||
{
|
||||
{ 565.15887451171875, 777.1043701171875, 454, 212 },
|
||||
{ -1368.379150390625, 175.8321533203125, 2241.439453125, 782.1121826171875 },
|
||||
};
|
||||
|
||||
RectangleList<float> b;
|
||||
|
||||
for (const auto& rect : rectanglesB)
|
||||
b.add (rect);
|
||||
|
||||
const std::set<Rectangle<float>> expectedB { { { 565.15887451171875, 777.1043701171875 }, { 1019.15887451171875, 989.1043701171875 } },
|
||||
{ { 565.15887451171875, 175.8321533203125 }, { 873.060302734375, 777.1043701171875 } },
|
||||
{ { -1368.379150390625, 175.8321533203125 }, { 565.158935546875, 957.9443359375 } } };
|
||||
|
||||
for (const auto& r : expectedB)
|
||||
expect (std::find (b.begin(), b.end(), r) != b.end());
|
||||
}
|
||||
|
||||
beginTest ("Subtracting overlapping empty rect subdivides existing rects");
|
||||
{
|
||||
RectangleList<float> list;
|
||||
list.add ({ 10, 10, 80, 80 });
|
||||
list.subtract ({ 50, 50, 0, 0 });
|
||||
|
||||
// The overlapping rect gets subdivided on the X axis at the location of the empty rect.
|
||||
const std::set<Rectangle<float>> expected { { 10, 10, 40, 80 },
|
||||
{ 50, 10, 40, 80 } };
|
||||
|
||||
for (const auto& r : expected)
|
||||
expect (std::find (list.begin(), list.end(), r) != list.end());
|
||||
}
|
||||
|
||||
beginTest ("Subtracting non-overlapping rects has no effect");
|
||||
{
|
||||
RectangleList<float> list;
|
||||
list.add ({ 10, 10, 80, 80 });
|
||||
|
||||
list.subtract ({ 0, 0, 5, 5 });
|
||||
list.subtract ({ 0, 95, 5, 5 });
|
||||
list.subtract ({ 95, 0, 5, 5 });
|
||||
list.subtract ({ 95, 95, 5, 5 });
|
||||
|
||||
expect (list.getNumRectangles() == 1);
|
||||
expect (*list.begin() == Rectangle<float> { 10, 10, 80, 80 });
|
||||
}
|
||||
|
||||
beginTest ("Subtracting from corner produces two rects");
|
||||
{
|
||||
RectangleList<float> list;
|
||||
list.add ({ 10, 10, 80, 80 });
|
||||
|
||||
list.subtract ({ 0, 0, 50, 50 });
|
||||
|
||||
const std::set<Rectangle<float>> expected { { 50, 10, 40, 80 },
|
||||
{ 10, 50, 40, 40 } };
|
||||
|
||||
for (const auto& r : expected)
|
||||
expect (std::find (list.begin(), list.end(), r) != list.end());
|
||||
}
|
||||
|
||||
beginTest ("Subtracting from entire edge shrinks existing rect");
|
||||
{
|
||||
RectangleList<float> list;
|
||||
list.add ({ 10, 10, 80, 80 });
|
||||
|
||||
list.subtract ({ 0, 0, 30, 100 });
|
||||
list.subtract ({ 30, 50, 60, 40 });
|
||||
|
||||
const std::set<Rectangle<float>> expected { { 30, 10, 60, 40 } };
|
||||
|
||||
for (const auto& r : expected)
|
||||
expect (std::find (list.begin(), list.end(), r) != list.end());
|
||||
}
|
||||
|
||||
beginTest ("Subtracting a notch from a vertical edge produces new rects");
|
||||
{
|
||||
RectangleList<float> list;
|
||||
list.add ({ 10, 10, 80, 80 });
|
||||
list.subtract ({ 10, 20, 10, 60 });
|
||||
|
||||
const std::set<Rectangle<float>> expected { { 20, 10, 70, 80 },
|
||||
{ 10, 80, 10, 10 },
|
||||
{ 10, 10, 10, 10} };
|
||||
|
||||
for (auto r : list)
|
||||
expect (std::find (list.begin(), list.end(), r) != list.end());
|
||||
}
|
||||
|
||||
beginTest ("Subtracting a notch from a horizontal edge produces new rects");
|
||||
{
|
||||
RectangleList<float> list;
|
||||
list.add ({ 10, 10, 80, 80 });
|
||||
list.subtract ({ 20, 10, 60, 10 });
|
||||
|
||||
const std::set<Rectangle<float>> expected { { 80, 10, 10, 80 },
|
||||
{ 20, 20, 60, 70 },
|
||||
{ 10, 10, 10, 80 } };
|
||||
|
||||
for (auto r : list)
|
||||
expect (std::find (list.begin(), list.end(), r) != list.end());
|
||||
}
|
||||
|
||||
beginTest ("Subtracting a hole from the centre of a rect produces new rects");
|
||||
{
|
||||
RectangleList<float> list;
|
||||
list.add ({ 10, 10, 80, 80 });
|
||||
list.subtract ({ 20, 20, 60, 60 });
|
||||
|
||||
const std::set<Rectangle<float>> expected { { 10, 10, 10, 80 },
|
||||
{ 20, 10, 60, 10 },
|
||||
{ 20, 80, 60, 10 },
|
||||
{ 80, 10, 10, 80 } };
|
||||
|
||||
for (auto r : list)
|
||||
expect (std::find (list.begin(), list.end(), r) != list.end());
|
||||
}
|
||||
|
||||
beginTest ("Subtracting a rect from itself produces an empty list");
|
||||
{
|
||||
RectangleList<float> list;
|
||||
list.add ({ 10, 10, 80, 80 });
|
||||
list.subtract ({ 10, 10, 80, 80 });
|
||||
expect (list.isEmpty());
|
||||
}
|
||||
|
||||
beginTest ("Subtracting a larger rect from a rect list produces an empty list");
|
||||
{
|
||||
RectangleList<float> list;
|
||||
list.add ({ 10, 10, 80, 80 });
|
||||
list.subtract ({ 0, 0, 100, 100 });
|
||||
expect (list.isEmpty());
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
static RectangleListTests rectangleListTests;
|
||||
|
||||
} // namespace juce
|
||||
|
|
@ -209,6 +209,7 @@ extern "C"
|
|||
#if JUCE_UNIT_TESTS
|
||||
#include "geometry/juce_Parallelogram_test.cpp"
|
||||
#include "geometry/juce_Rectangle_test.cpp"
|
||||
#include "geometry/juce_RectangleList_test.cpp"
|
||||
#endif
|
||||
|
||||
#if JUCE_USE_FREETYPE
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue