Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion CPP/Clipper2Lib/include/clipper2/clipper.engine.h
Original file line number Diff line number Diff line change
Expand Up @@ -226,7 +226,7 @@ namespace Clipper2Lib {
inline void UpdateEdgeIntoAEL(Active *e);
void IntersectEdges(Active &e1, Active &e2, const Point64& pt);
inline void DeleteFromAEL(Active &e);
inline void AdjustCurrXAndCopyToSEL(const int64_t top_y);
inline void AdjustCurrXAndCopyToSEL(const int64_t top_y, bool& is_curr_x_sorted);
void DoIntersections(const int64_t top_y);
void AddNewIntersectNode(Active &e1, Active &e2, const int64_t top_y);
bool BuildIntersectList(const int64_t top_y);
Expand Down
14 changes: 12 additions & 2 deletions CPP/Clipper2Lib/src/clipper.engine.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2099,10 +2099,14 @@ namespace Clipper2Lib {
}


inline void ClipperBase::AdjustCurrXAndCopyToSEL(const int64_t top_y)
inline void ClipperBase::AdjustCurrXAndCopyToSEL(const int64_t top_y,
bool& is_curr_x_sorted)
{
Active* e = actives_;
sel_ = e;
// detect whether projected edges retain their AEL order
is_curr_x_sorted = true;
int64_t prev_x = INT64_MIN;
while (e)
{
e->prev_in_sel = e->prev_in_ael;
Expand All @@ -2111,6 +2115,8 @@ namespace Clipper2Lib {
// it is safe to ignore 'joined' edges here because
// if necessary they will be split in IntersectEdges()
e->curr_x = TopX(*e, top_y);
if (e->curr_x < prev_x) is_curr_x_sorted = false;
prev_x = e->curr_x;
e = e->next_in_ael;
}
}
Expand Down Expand Up @@ -2382,7 +2388,11 @@ namespace Clipper2Lib {

//Calculate edge positions at the top of the current scanbeam, and from this
//we will determine the intersections required to reach these new positions.
AdjustCurrXAndCopyToSEL(top_y);
//When edges are already sorted on curr_x at the top of the scanbeam, no
//edges have crossed inside it, so there are no intersections to process.
bool is_curr_x_sorted;
AdjustCurrXAndCopyToSEL(top_y, is_curr_x_sorted);
if (is_curr_x_sorted) return false;
//Find all edge intersections in the current scanbeam using a stable merge
//sort that ensures only adjacent edges are intersecting. Intersect info is
//stored in FIntersectList ready to be processed in ProcessIntersectList.
Expand Down
13 changes: 9 additions & 4 deletions CPP/Clipper2Lib/src/clipper.offset.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -372,9 +372,11 @@ void ClipperOffset::OffsetPoint(Group& group, const Path64& path, size_t j, size
void ClipperOffset::OffsetPolygon(Group& group, const Path64& path)
{
path_out.clear();
// reserve a conservative baseline; joins may add additional points
path_out.reserve(path.size());
for (Path64::size_type j = 0, k = path.size() - 1; j < path.size(); k = j, ++j)
OffsetPoint(group, path, j, k);
solution->emplace_back(path_out);
OffsetPoint(group, path, j, k);
solution->emplace_back(std::move(path_out));
}

void ClipperOffset::OffsetOpenJoined(Group& group, const Path64& path)
Expand All @@ -394,6 +396,9 @@ void ClipperOffset::OffsetOpenJoined(Group& group, const Path64& path)

void ClipperOffset::OffsetOpenPath(Group& group, const Path64& path)
{
// open paths offset both sides but joins and caps may add more points
path_out.reserve(path.size() * 2);

// do the line start cap
if (deltaCallback64_) group_delta_ = deltaCallback64_(path, norms, 0, 0);

Expand Down Expand Up @@ -449,7 +454,7 @@ void ClipperOffset::OffsetOpenPath(Group& group, const Path64& path)

for (size_t j = highI -1, k = highI; j > 0; k = j, --j)
OffsetPoint(group, path, j, k);
solution->emplace_back(path_out);
solution->emplace_back(std::move(path_out));
}

void ClipperOffset::DoGroupOffset(Group& group)
Expand Down Expand Up @@ -523,7 +528,7 @@ void ClipperOffset::DoGroupOffset(Group& group)
#endif
}

solution->emplace_back(path_out);
solution->emplace_back(std::move(path_out));
continue;
} // end of offsetting a single point

Expand Down
69 changes: 69 additions & 0 deletions CPP/Tests/TestOffsets.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -698,3 +698,72 @@ TEST(Clipper2Tests, TestOffsets13) // see #965
const auto area2 = std::abs(Area(solution2));
EXPECT_EQ(area2, 122);
}

TEST(Clipper2Tests, TestOffsetMultipleRoundPolygons)
{
const Paths64 subjects = {
MakePath({0, 0, 10, 0, 10, 10, 0, 10}),
MakePath({30, 0, 40, 0, 40, 10, 30, 10})
};
ClipperOffset offset;
offset.AddPaths(subjects, JoinType::Round, EndType::Polygon);
Paths64 solution;

offset.Execute(2, solution);

ASSERT_EQ(solution.size(), 2);
EXPECT_EQ(GetBounds(solution), Rect64(-2, -2, 42, 12));
EXPECT_GT(std::abs(Area(solution)), 200);
}

TEST(Clipper2Tests, TestOffsetOpenJoinedPath)
{
const Paths64 subject = {
MakePath({0, 0, 20, 0, 20, 20, 0, 20})
};
ClipperOffset offset;
offset.AddPaths(subject, JoinType::Round, EndType::Joined);
Paths64 solution;

offset.Execute(2, solution);

ASSERT_FALSE(solution.empty());
EXPECT_EQ(GetBounds(solution), Rect64(-2, -2, 22, 22));
EXPECT_GT(std::abs(Area(solution)), 0);
}

TEST(Clipper2Tests, TestOffsetMultipleOpenPathsWithRoundCaps)
{
const Paths64 subjects = {
MakePath({0, 0, 20, 0}),
MakePath({0, 20, 20, 20})
};
ClipperOffset offset;
offset.AddPaths(subjects, JoinType::Round, EndType::Round);
Paths64 solution;

offset.Execute(2, solution);

ASSERT_EQ(solution.size(), 2);
EXPECT_EQ(GetBounds(solution), Rect64(-2, -2, 22, 22));
for (const auto& path : solution)
EXPECT_GT(path.size(), 4);
}

TEST(Clipper2Tests, TestOffsetSinglePointRoundAndSquare)
{
const Paths64 subject = {{{0, 0}}};
const Paths64 round_solution = InflatePaths(
subject, 10, JoinType::Round, EndType::Polygon);
const Paths64 square_solution = InflatePaths(
subject, 10, JoinType::Square, EndType::Polygon);

ASSERT_EQ(round_solution.size(), 1);
EXPECT_GT(round_solution[0].size(), 4);
EXPECT_EQ(GetBounds(round_solution), Rect64(-10, -10, 10, 10));

ASSERT_EQ(square_solution.size(), 1);
EXPECT_EQ(square_solution[0].size(), 4);
EXPECT_EQ(GetBounds(square_solution), Rect64(-10, -10, 10, 10));
EXPECT_EQ(std::abs(Area(square_solution)), 400);
}
40 changes: 39 additions & 1 deletion CPP/Tests/TestPolygons.cpp
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
#include <gtest/gtest.h>
#include <algorithm>
#include "clipper2/clipper.h"
#include "ClipFileLoad.h"
inline Clipper2Lib::PathD MakeRandomPath(int width, int height, unsigned vertCnt)
Expand Down Expand Up @@ -122,4 +123,41 @@ TEST(Clipper2Tests, TestCollinearOnMacOs) //#777
ASSERT_EQ(solution.size(), 1);
EXPECT_EQ(solution[0].size(), 3);
EXPECT_EQ(IsPositive(subject[0]), IsPositive(solution[0]));
}
}

TEST(Clipper2Tests, TestScanbeamWithSortedCurrX)
{
// These active vertical edges retain their left-to-right order at the top
// of the scanbeam, so no intersection sorting is needed.
const Clipper2Lib::Paths64 subject = {
Clipper2Lib::MakePath({0, 0, 10, 0, 10, 10, 0, 10}),
Clipper2Lib::MakePath({20, 0, 30, 0, 30, 10, 20, 10})
};

const Clipper2Lib::Paths64 solution = Clipper2Lib::Union(
subject, Clipper2Lib::FillRule::NonZero);

EXPECT_EQ(solution.size(), 2);
EXPECT_EQ(std::abs(Clipper2Lib::Area(solution)), 200);
}

TEST(Clipper2Tests, TestScanbeamWithUnsortedCurrX)
{
// The two diagonal edges exchange their left-to-right order inside the
// scanbeam. They must still be sorted and intersected at (5, 5).
const Clipper2Lib::Paths64 subject = {
Clipper2Lib::MakePath({0, 0, 10, 10, 0, 10, 10, 0})
};

const Clipper2Lib::Paths64 solution = Clipper2Lib::Union(
subject, Clipper2Lib::FillRule::EvenOdd);

ASSERT_EQ(solution.size(), 2);
EXPECT_EQ(std::abs(Clipper2Lib::Area(solution)), 50);
const Clipper2Lib::Point64 intersection(5, 5);
for (const auto& path : solution)
{
EXPECT_EQ(path.size(), 3);
EXPECT_NE(std::find(path.begin(), path.end(), intersection), path.end());
}
}
12 changes: 10 additions & 2 deletions CSharp/Clipper2Lib/Clipper.Engine.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1856,10 +1856,13 @@ private Active NewActive()
}

[MethodImpl(MethodImplOptions.AggressiveInlining)]
private void AdjustCurrXAndCopyToSEL(long topY)
private void AdjustCurrXAndCopyToSEL(long topY, out bool isCurrXSorted)
{
Active? ae = _actives;
_sel = ae;
// detect whether projected edges retain their AEL order
isCurrXSorted = true;
long prevX = long.MinValue;
while (ae != null)
{
ae.prevInSEL = ae.prevInAEL;
Expand All @@ -1868,6 +1871,8 @@ private void AdjustCurrXAndCopyToSEL(long topY)
// it is safe to ignore 'joined' edges here because
// if necessary they will be split in IntersectEdges()
ae.curX = TopX(ae, topY);
if (ae.curX < prevX) isCurrXSorted = false;
prevX = ae.curX;
// NB don't update ae.curr.Y yet (see AddNewIntersectNode)
ae = ae.nextInAEL;
}
Expand Down Expand Up @@ -1984,7 +1989,10 @@ private bool BuildIntersectList(long topY)

// Calculate edge positions at the top of the current scanbeam, and from this
// we will determine the intersections required to reach these new positions.
AdjustCurrXAndCopyToSEL(topY);
// When edges are already sorted on curX at the top of the scanbeam, no
// edges have crossed inside it, so there are no intersections to process.
AdjustCurrXAndCopyToSEL(topY, out bool isCurrXSorted);
if (isCurrXSorted) return false;

// Find all edge intersections in the current scanbeam using a stable merge
// sort that ensures only adjacent edges are intersecting. Intersect info is
Expand Down
6 changes: 4 additions & 2 deletions CSharp/Clipper2Lib/Clipper.Offset.cs
Original file line number Diff line number Diff line change
Expand Up @@ -581,7 +581,8 @@ private void OffsetPoint(Group group, Path64 path, int j, ref int k)
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private void OffsetPolygon(Group group, Path64 path)
{
pathOut = new Path64();
// reserve a conservative baseline; joins may add additional points
pathOut = new Path64(path.Count);
int cnt = path.Count, prev = cnt - 1;
for (int i = 0; i < cnt; i++)
OffsetPoint(group, path, i, ref prev);
Expand All @@ -599,7 +600,8 @@ private void OffsetOpenJoined(Group group, Path64 path)

private void OffsetOpenPath(Group group, Path64 path)
{
pathOut = new Path64();
// open paths offset both sides but joins and caps may add more points
pathOut = new Path64(path.Count * 2);
int highI = path.Count - 1;

if (DeltaCallback != null)
Expand Down
89 changes: 89 additions & 0 deletions CSharp/Tests/Tests1/Tests/TestOffset.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,5 +15,94 @@ public void TestOffsetEmpty()
ClipperOffset offset = new ClipperOffset();
offset.Execute(10, solution);
}

private static void AssertBounds(Paths64 paths,
long left, long top, long right, long bottom)
{
Rect64 bounds = Clipper.GetBounds(paths);
Assert.AreEqual(left, bounds.left);
Assert.AreEqual(top, bounds.top);
Assert.AreEqual(right, bounds.right);
Assert.AreEqual(bottom, bounds.bottom);
}

[TestMethod]
public void TestOffsetMultipleRoundPolygons()
{
Paths64 subjects = new()
{
Clipper.MakePath(new long[] { 0, 0, 10, 0, 10, 10, 0, 10 }),
Clipper.MakePath(new long[] { 30, 0, 40, 0, 40, 10, 30, 10 })
};
ClipperOffset offset = new();
offset.AddPaths(subjects, JoinType.Round, EndType.Polygon);
Paths64 solution = new();

offset.Execute(2, solution);

Assert.AreEqual(2, solution.Count);
AssertBounds(solution, -2, -2, 42, 12);
Assert.IsTrue(Math.Abs(Clipper.Area(solution)) > 200);
}

[TestMethod]
public void TestOffsetOpenJoinedPath()
{
Paths64 subject = new()
{
Clipper.MakePath(new long[] { 0, 0, 20, 0, 20, 20, 0, 20 })
};
ClipperOffset offset = new();
offset.AddPaths(subject, JoinType.Round, EndType.Joined);
Paths64 solution = new();

offset.Execute(2, solution);

Assert.IsTrue(solution.Count > 0);
AssertBounds(solution, -2, -2, 22, 22);
Assert.IsTrue(Math.Abs(Clipper.Area(solution)) > 0);
}

[TestMethod]
public void TestOffsetMultipleOpenPathsWithRoundCaps()
{
Paths64 subjects = new()
{
Clipper.MakePath(new long[] { 0, 0, 20, 0 }),
Clipper.MakePath(new long[] { 0, 20, 20, 20 })
};
ClipperOffset offset = new();
offset.AddPaths(subjects, JoinType.Round, EndType.Round);
Paths64 solution = new();

offset.Execute(2, solution);

Assert.AreEqual(2, solution.Count);
AssertBounds(solution, -2, -2, 22, 22);
foreach (Path64 path in solution)
Assert.IsTrue(path.Count > 4);
}

[TestMethod]
public void TestOffsetSinglePointRoundAndSquare()
{
Paths64 subject = new()
{
new Path64 { new Point64(0, 0) }
};
Paths64 roundSolution = Clipper.InflatePaths(
subject, 10, JoinType.Round, EndType.Round);
Paths64 squareSolution = Clipper.InflatePaths(
subject, 10, JoinType.Square, EndType.Square);

Assert.AreEqual(1, roundSolution.Count);
Assert.IsTrue(roundSolution[0].Count > 4);
AssertBounds(roundSolution, -10, -10, 10, 10);

Assert.AreEqual(1, squareSolution.Count);
Assert.AreEqual(4, squareSolution[0].Count);
AssertBounds(squareSolution, -10, -10, 10, 10);
Assert.AreEqual(400, Math.Abs(Clipper.Area(squareSolution)));
}
}
}
Loading