From 1057798727ecb14b1dc36275aaa27e51ab3d1feb Mon Sep 17 00:00:00 2001 From: angusj Date: Mon, 6 Feb 2023 15:45:15 +1000 Subject: [PATCH] Changed MakePath to expect only ordinal/arithmetic values (C++) (#398) Fixed InflatePaths to return unchanged paths when delta == 0 (#389) Minor tweak to detecting "touching" solution polygons Fixed minor bug in ClipperOffset (#392,#393) Tidied up error handling in C++ Added error_code param to ScalePath function (C++) Tweaks to C# CI testing (#388) Major rewrite of RectClip function (Delphi only so far) (#383) --- .../include/clipper2/clipper.core.h | 76 +- CPP/Clipper2Lib/include/clipper2/clipper.h | 156 +--- CPP/Clipper2Lib/src/clipper.engine.cpp | 22 +- CPP/Clipper2Lib/src/clipper.offset.cpp | 17 +- CPP/Examples/Inflate/Inflate.cpp | 20 +- CPP/Examples/UsingZ/UsingZ.cpp | 6 +- CPP/Tests/TestOrientation.cpp | 6 +- CPP/Tests/TestPolytreeIntersection.cpp | 4 +- CPP/Tests/TestPolytreeUnion.cpp | 4 +- CPP/Tests/TestRectClip.cpp | 19 +- CPP/Tests/TestTrimCollinear.cpp | 12 +- CPP/Utils/clipper.svg.utils.h | 6 +- .../Tests1/Tests/TestPolygons.cs | 63 +- CSharp/Clipper2Lib/Clipper.Engine.cs | 23 +- Delphi/Clipper2Lib/Clipper.Core.pas | 26 +- Delphi/Clipper2Lib/Clipper.Engine.pas | 18 +- Delphi/Clipper2Lib/Clipper.Offset.pas | 9 +- Delphi/Clipper2Lib/Clipper.RectClip.pas | 720 +++++++++++++++--- Delphi/Clipper2Lib/Clipper.pas | 121 +-- 19 files changed, 870 insertions(+), 458 deletions(-) diff --git a/CPP/Clipper2Lib/include/clipper2/clipper.core.h b/CPP/Clipper2Lib/include/clipper2/clipper.core.h index c7e8844b..6ae868d0 100644 --- a/CPP/Clipper2Lib/include/clipper2/clipper.core.h +++ b/CPP/Clipper2Lib/include/clipper2/clipper.core.h @@ -36,20 +36,45 @@ namespace Clipper2Lib "Precision exceeds the permitted range"; static const char* range_error = "Values exceed permitted range"; + static const char* scale_error = + "Invalid scale (either 0 or too large)"; + static const char* non_pair_error = + "There must be 2 values for each coordinate"; #endif // error codes (2^n) - const int precision_error_i = 1; // non-fatal - const int range_error_i = 2; + const int precision_error_i = 1; // non-fatal + const int scale_error_i = 2; // non-fatal + const int non_pair_error_i = 4; // non-fatal + const int range_error_i = 64; static const double PI = 3.141592653589793238; static const int64_t MAX_COORD = INT64_MAX >> 2; static const int64_t MIN_COORD = -MAX_COORD; static const int64_t INVALID = INT64_MAX; + const double max_coord = static_cast(MAX_COORD); + const double min_coord = static_cast(MIN_COORD); static const double MAX_DBL = (std::numeric_limits::max)(); static const double MIN_DBL = (std::numeric_limits::min)(); + static void DoError(int error_code) + { +#if __cpp_exceptions + switch (error_code) + { + case precision_error_i: + throw Clipper2Exception(precision_error); + case scale_error_i: + throw Clipper2Exception(scale_error); + case non_pair_error_i: + throw Clipper2Exception(non_pair_error); + case range_error_i: + throw Clipper2Exception(range_error); + } +#endif + } + //By far the most widely used filling rules for polygons are EvenOdd //and NonZero, sometimes called Alternate and Winding respectively. //https://en.wikipedia.org/wiki/Nonzero-rule @@ -384,9 +409,19 @@ namespace Clipper2Lib template - inline Path ScalePath(const Path& path, double scale_x, double scale_y) + inline Path ScalePath(const Path& path, + double scale_x, double scale_y, int& error_code) { Path result; + if (scale_x == 0 || scale_y == 0) + { + error_code |= scale_error_i; + DoError(scale_error_i); + // if no exception, treat as non-fatal error + if (scale_x == 0) scale_x = 1.0; + if (scale_y == 0) scale_y = 1.0; + } + result.reserve(path.size()); #ifdef USINGZ std::transform(path.begin(), path.end(), back_inserter(result), @@ -401,9 +436,10 @@ namespace Clipper2Lib } template - inline Path ScalePath(const Path& path, double scale) + inline Path ScalePath(const Path& path, + double scale, int& error_code) { - return ScalePath(path, scale, scale); + return ScalePath(path, scale, scale, error_code); } template @@ -416,27 +452,21 @@ namespace Clipper2Lib !std::numeric_limits::is_integer) { RectD r = Bounds(paths); - const double max_coord_d = static_cast(MAX_COORD); - const double min_coord_d = static_cast(MIN_COORD); - if ((r.left * scale_x) < min_coord_d || - (r.right * scale_x) > max_coord_d || - (r.top * scale_y) < min_coord_d || - (r.bottom * scale_y) > max_coord_d) + if ((r.left * scale_x) < min_coord || + (r.right * scale_x) > max_coord || + (r.top * scale_y) < min_coord || + (r.bottom * scale_y) > max_coord) { error_code |= range_error_i; -#if __cpp_exceptions - throw Clipper2Exception(range_error); -#else - // error_code = range_error_i; // compiler complains if here - return result; // empty -#endif + DoError(range_error_i); + return result; // empty path } } result.reserve(paths.size()); std::transform(paths.begin(), paths.end(), back_inserter(result), - [scale_x, scale_y](const auto& path) - { return ScalePath(path, scale_x, scale_y); }); + [=, &error_code](const auto& path) + { return ScalePath(path, scale_x, scale_y, error_code); }); return result; } @@ -578,11 +608,8 @@ namespace Clipper2Lib { if (precision >= -8 && precision <= 8) return; error_code |= precision_error_i; // non-fatal error -#if __cpp_exceptions - throw Clipper2Exception(precision_error); -#else + DoError(precision_error_i); // unless exceptions enabled precision = precision > 8 ? 8 : -8; -#endif } inline void CheckPrecision(int& precision) @@ -675,9 +702,6 @@ namespace Clipper2Lib return Area(poly) >= 0; } - static const double max_coord = static_cast(MAX_COORD); - static const double min_coord = static_cast(MIN_COORD); - inline int64_t CheckCastInt64(double val) { if ((val >= max_coord) || (val <= min_coord)) return INVALID; diff --git a/CPP/Clipper2Lib/include/clipper2/clipper.h b/CPP/Clipper2Lib/include/clipper2/clipper.h index b58e0e7f..69cbc7c9 100644 --- a/CPP/Clipper2Lib/include/clipper2/clipper.h +++ b/CPP/Clipper2Lib/include/clipper2/clipper.h @@ -11,6 +11,7 @@ #define CLIPPER_H #include +#include #include #include "clipper.core.h" @@ -134,6 +135,7 @@ namespace Clipper2Lib { inline Paths64 InflatePaths(const Paths64& paths, double delta, JoinType jt, EndType et, double miter_limit = 2.0) { + if (!delta) return paths; ClipperOffset clip_offset(miter_limit); clip_offset.AddPaths(paths, jt, et); return clip_offset.Execute(delta); @@ -144,6 +146,7 @@ namespace Clipper2Lib { { int error_code = 0; CheckPrecision(precision, error_code); + if (!delta) return paths; if (error_code) return PathsD(); const double scale = std::pow(10, precision); ClipperOffset clip_offset(miter_limit); @@ -401,94 +404,6 @@ namespace Clipper2Lib { return true; } - inline bool GetInt(std::string::const_iterator& iter, const - std::string::const_iterator& end_iter, int64_t& val) - { - val = 0; - bool is_neg = *iter == '-'; - if (is_neg) ++iter; - std::string::const_iterator start_iter = iter; - while (iter != end_iter && - ((*iter >= '0') && (*iter <= '9'))) - { - val = val * 10 + (static_cast(*iter++) - '0'); - } - if (is_neg) val = -val; - return (iter != start_iter); - } - - inline bool GetFloat(std::string::const_iterator& iter, const - std::string::const_iterator& end_iter, double& val) - { - val = 0; - bool is_neg = *iter == '-'; - if (is_neg) ++iter; - int dec_pos = 1; - const std::string::const_iterator start_iter = iter; - while (iter != end_iter && (*iter == '.' || - ((*iter >= '0') && (*iter <= '9')))) - { - if (*iter == '.') - { - if (dec_pos != 1) break; - dec_pos = 0; - ++iter; - continue; - } - if (dec_pos != 1) --dec_pos; - val = val * 10 + ((int64_t)(*iter++) - '0'); - } - if (iter == start_iter || dec_pos == 0) return false; - if (dec_pos < 0) - val *= std::pow(10, dec_pos); - if (is_neg) - val *= -1; - return true; - } - - inline void SkipWhiteSpace(std::string::const_iterator& iter, - const std::string::const_iterator& end_iter) - { - while (iter != end_iter && *iter <= ' ') ++iter; - } - - inline void SkipSpacesWithOptionalComma(std::string::const_iterator& iter, - const std::string::const_iterator& end_iter) - { - bool comma_seen = false; - while (iter != end_iter) - { - if (*iter == ' ') ++iter; - else if (*iter == ',') - { - if (comma_seen) return; // don't skip 2 commas! - comma_seen = true; - ++iter; - } - else return; - } - } - - inline bool has_one_match(const char c, char* chrs) - { - while (*chrs > 0 && c != *chrs) ++chrs; - if (!*chrs) return false; - *chrs = ' '; // only match once per char - return true; - } - - - inline void SkipUserDefinedChars(std::string::const_iterator& iter, - const std::string::const_iterator& end_iter, const std::string& skip_chars) - { - const size_t MAX_CHARS = 16; - char buff[MAX_CHARS] = {0}; - std::copy(skip_chars.cbegin(), skip_chars.cend(), &buff[0]); - while (iter != end_iter && - (*iter <= ' ' || has_one_match(*iter, buff))) ++iter; - return; - } - static void OutlinePolyPath(std::ostream& os, bool isHole, size_t count, const std::string& preamble) { @@ -588,39 +503,44 @@ namespace Clipper2Lib { return true; } - inline Path64 MakePath(const std::string& s) - { - const std::string skip_chars = " ,(){}[]"; - Path64 result; - std::string::const_iterator s_iter = s.cbegin(); - details::SkipUserDefinedChars(s_iter, s.cend(), skip_chars); - while (s_iter != s.cend()) - { - int64_t y = 0, x = 0; - if (!details::GetInt(s_iter, s.cend(), x)) break; - details::SkipSpacesWithOptionalComma(s_iter, s.cend()); - if (!details::GetInt(s_iter, s.cend(), y)) break; - result.push_back(Point64(x, y)); - details::SkipUserDefinedChars(s_iter, s.cend(), skip_chars); - } + template::value && + !std::is_same::value, bool + >::type = true> + inline Path64 MakePath(const T(&list)[N]) + { + if (N % 2 != 0) + DoError(non_pair_error_i); // non-fatal without exception handling + Path64 result; // else ignores unpaired value + result.reserve(N / 2); +#ifdef USINGZ + for (size_t i = 0; i < N; ++i) + result.emplace_back(Point64{ list[i], list[++i], 0 }); +#else + for (size_t i = 0; i < N; ++i) + result.emplace_back(Point64{ list[i], list[++i] }); +#endif return result; } - inline PathD MakePathD(const std::string& s) - { - const std::string skip_chars = " ,(){}[]"; - PathD result; - std::string::const_iterator s_iter = s.cbegin(); - details::SkipUserDefinedChars(s_iter, s.cend(), skip_chars); - while (s_iter != s.cend()) - { - double y = 0, x = 0; - if (!details::GetFloat(s_iter, s.cend(), x)) break; - details::SkipSpacesWithOptionalComma(s_iter, s.cend()); - if (!details::GetFloat(s_iter, s.cend(), y)) break; - result.push_back(PointD(x, y)); - details::SkipUserDefinedChars(s_iter, s.cend(), skip_chars); - } + template::value && + !std::is_same::value, bool + >::type = true> + inline PathD MakePathD(const T(&list)[N]) + { + if (N % 2 != 0) + DoError(non_pair_error_i); // non-fatal without exception handling + PathD result; // else ignores unpaired value + result.reserve(N / 2); +#ifdef USINGZ + for (size_t i = 0; i < N; ++i) + result.emplace_back(PointD{ list[i], list[++i], 0 }); +#else + for (size_t i = 0; i < N; ++i) + result.emplace_back(PointD{ list[i], list[++i] }); +#endif return result; } diff --git a/CPP/Clipper2Lib/src/clipper.engine.cpp b/CPP/Clipper2Lib/src/clipper.engine.cpp index a635ac60..dd93e766 100644 --- a/CPP/Clipper2Lib/src/clipper.engine.cpp +++ b/CPP/Clipper2Lib/src/clipper.engine.cpp @@ -1828,7 +1828,6 @@ namespace Clipper2Lib { return resultOp; } - inline void ClipperBase::DeleteFromAEL(Active& e) { Active* prev = e.prev_in_ael; @@ -2654,9 +2653,13 @@ namespace Clipper2Lib { IsOpen(*prev) || !IsHotEdge(*prev) || pt.y < e.top.y + 2 || pt.y < prev->top.y + 2) // avoid trivial joins return; - if (check_curr_x) prev->curr_x = TopX(*prev, pt.y); - if (e.curr_x != prev->curr_x || CrossProduct(e.top, pt, prev->top)) - return; + + if (check_curr_x) + { + if (DistanceFromLineSqrd(pt, prev->bot, prev->top) > 0.35) return; + } + else if (e.curr_x != prev->curr_x) return; + if (CrossProduct(e.top, pt, prev->top)) return; if (e.outrec->idx == prev->outrec->idx) AddLocalMaxPoly(*prev, e, pt); @@ -2677,10 +2680,13 @@ namespace Clipper2Lib { pt.y < e.top.y +2 || pt.y < next->top.y +2) // avoids trivial joins return; - if (check_curr_x) next->curr_x = TopX(*next, pt.y); - if (e.curr_x != next->curr_x || - CrossProduct(e.top, pt, next->top)) return; - + if (check_curr_x) + { + if (DistanceFromLineSqrd(pt, next->bot, next->top) > 0.35) return; + } + else if (e.curr_x != next->curr_x) return; + if (CrossProduct(e.top, pt, next->top)) return; + if (e.outrec->idx == next->outrec->idx) AddLocalMaxPoly(e, *next, pt); else if (e.outrec->idx < next->outrec->idx) diff --git a/CPP/Clipper2Lib/src/clipper.offset.cpp b/CPP/Clipper2Lib/src/clipper.offset.cpp index 870ee753..9dad3f64 100644 --- a/CPP/Clipper2Lib/src/clipper.offset.cpp +++ b/CPP/Clipper2Lib/src/clipper.offset.cpp @@ -275,9 +275,12 @@ void ClipperOffset::OffsetPoint(Group& group, { //almost no angle or concave group.path_.push_back(GetPerpendic(path[j], norms[k], group_delta_)); - // create a simple self-intersection that will be cleaned up later - if (!almostNoAngle) group.path_.push_back(path[j]); - group.path_.push_back(GetPerpendic(path[j], norms[j], group_delta_)); + if (!almostNoAngle) + { + // create a simple self-intersection that will be cleaned up later + //group.path_.push_back(path[j]); + group.path_.push_back(GetPerpendic(path[j], norms[j], group_delta_)); + } } else { @@ -381,12 +384,12 @@ void ClipperOffset::DoGroupOffset(Group& group, double delta) int idx = 0; GetBoundsAndLowestPolyIdx(group.paths_in_, r, idx); if (!IsSafeOffset(r, static_cast(std::ceil(delta)))) -#if __cpp_exceptions - throw Clipper2Exception(range_error); -#else + { + DoError(range_error_i); error_code_ |= range_error_i; return; -#endif + } + if (isClosedPaths) { double area = Area(group.paths_in_[idx]); diff --git a/CPP/Examples/Inflate/Inflate.cpp b/CPP/Examples/Inflate/Inflate.cpp index 57eb7b83..7a3734bd 100644 --- a/CPP/Examples/Inflate/Inflate.cpp +++ b/CPP/Examples/Inflate/Inflate.cpp @@ -14,8 +14,10 @@ void System(const std::string& filename); int main(int argc, char* argv[]) { - DoSimpleShapes(); + //DoSimpleShapes(); DoRabbit(); + + std::getchar(); } void DoSimpleShapes() @@ -25,7 +27,7 @@ void DoSimpleShapes() FillRule fr2 = FillRule::EvenOdd; SvgWriter svg2; - op1.push_back(MakePath("80,60, 20,20 180,20 180,80, 20,180 180,180")); + op1.push_back(MakePath({ 80,60, 20,20, 180,20, 180,80, 20,180, 180,180 })); op2 = InflatePaths(op1, 20, JoinType::Square, EndType::Butt); SvgAddOpenSubject(svg2, op1, fr2, false); SvgAddSolution(svg2, Paths64ToPathsD(op2), fr2, false); @@ -48,7 +50,7 @@ void DoSimpleShapes() //triangle offset - with large miter Paths64 p, pp; - p.push_back(MakePath("30, 150, 60, 350, 0, 350")); + p.push_back(MakePath({ 30, 150, 60, 350, 0, 350 })); pp.insert(pp.end(), p.begin(), p.end()); for (int i = 0; i < 5; ++i) @@ -61,7 +63,7 @@ void DoSimpleShapes() //rectangle offset - both squared and rounded p.clear(); - p.push_back(MakePath("100,30, 340,30, 340,230, 100,230")); + p.push_back(MakePath({ 100,30, 340,30, 340,230, 100,230 })); pp.insert(pp.end(), p.begin(), p.end()); //nb: using the ClipperOffest class directly here to control //different join types within the same offset operation @@ -91,11 +93,13 @@ void DoRabbit() while (p.size()) { - //nb: don't forget to scale the delta offset too! + // nb: don't forget to scale the delta offset too! p = InflatePaths(p, -2.5, jt, EndType::Polygon); - //RamerDouglasPeucker - not essential but - //speeds up the loop and also tidies up the result - p = RamerDouglasPeucker(p, 0.025); + // SimplifyPaths (or RamerDouglasPeucker) is not + // essential but is highly recommended because it + // speeds up the loop and also tidies up the result + p = SimplifyPaths(p, 0.25); // preferred over RDP() + //p = RamerDouglasPeucker(p, 0.25); solution.reserve(solution.size() + p.size()); copy(p.begin(), p.end(), back_inserter(solution)); } diff --git a/CPP/Examples/UsingZ/UsingZ.cpp b/CPP/Examples/UsingZ/UsingZ.cpp index 065f6812..7a0690ee 100644 --- a/CPP/Examples/UsingZ/UsingZ.cpp +++ b/CPP/Examples/UsingZ/UsingZ.cpp @@ -34,7 +34,7 @@ class MyClass { int main(int argc, char* argv[]) { - TestingZ_Int64(); + //TestingZ_Int64(); TestingZ_Double(); } @@ -45,7 +45,7 @@ void TestingZ_Int64() MyClass mc; Clipper64 c64; - subject.push_back(MakePath("100, 50, 10, 79, 65, 2, 65, 98, 10, 21 ")); + subject.push_back(MakePath({ 100, 50, 10, 79, 65, 2, 65, 98, 10, 21 })); c64.AddSubject(subject); c64.SetZCallback( std::bind(&MyClass::myZCB, mc, std::placeholders::_1, @@ -76,7 +76,7 @@ void TestingZ_Double() MyClass mc; ClipperD c; - subject.push_back(MakePathD("100, 50, 10, 79, 65, 2, 65, 98, 10, 21 ")); + subject.push_back(MakePathD({ 100, 50, 10, 79, 65, 2, 65, 98, 10, 21 })); c.AddSubject(subject); c.SetZCallback( std::bind(&MyClass::myZCBD, mc, std::placeholders::_1, diff --git a/CPP/Tests/TestOrientation.cpp b/CPP/Tests/TestOrientation.cpp index aff09bd3..65a38980 100644 --- a/CPP/Tests/TestOrientation.cpp +++ b/CPP/Tests/TestOrientation.cpp @@ -7,11 +7,11 @@ TEST(Clipper2Tests, TestNegativeOrientation) { Paths64 subjects, clips, solution; //also test MakePath using specified skip_chars (useful when pasting coords) - subjects.push_back(MakePath("(0,0), (0,100) (100,100) (100,0)")); - subjects.push_back(MakePath("(10,10) (10,110) (110,110) (110,10)")); + subjects.push_back(MakePath({ 0,0, 0,100, 100,100, 100,0 })); + subjects.push_back(MakePath({ 10,10, 10,110, 110,110, 110,10 })); EXPECT_FALSE(IsPositive(subjects[0])); EXPECT_FALSE(IsPositive(subjects[1])); - clips.push_back(MakePath("50,50 50,150 150,150 150,50")); + clips.push_back(MakePath({ 50,50, 50,150, 150,150, 150,50 })); EXPECT_FALSE(IsPositive(clips[0])); solution = Union(subjects, clips, FillRule::Negative); diff --git a/CPP/Tests/TestPolytreeIntersection.cpp b/CPP/Tests/TestPolytreeIntersection.cpp index 5b2e5fd3..58ff3f56 100644 --- a/CPP/Tests/TestPolytreeIntersection.cpp +++ b/CPP/Tests/TestPolytreeIntersection.cpp @@ -8,10 +8,10 @@ TEST(Clipper2Tests, TestPolyTreeIntersection) Clipper64 clipper; Paths64 subject; - subject.push_back( MakePath("0,0 0,5 5,5 5,0") ); + subject.push_back(MakePath({ 0,0, 0,5, 5,5, 5,0 })); clipper.AddSubject(subject); Paths64 clip; - clip.push_back( MakePath("1,1 1,6 6,6 6,1") ); + clip.push_back(MakePath({ 1,1, 1,6, 6,6, 6,1 })); clipper.AddClip (clip); PolyTree64 solution; diff --git a/CPP/Tests/TestPolytreeUnion.cpp b/CPP/Tests/TestPolytreeUnion.cpp index b8ebcd3a..ccda169d 100644 --- a/CPP/Tests/TestPolytreeUnion.cpp +++ b/CPP/Tests/TestPolytreeUnion.cpp @@ -6,8 +6,8 @@ using namespace Clipper2Lib; TEST(Clipper2Tests, TestPolytreeUnion) { Paths64 subject; - subject.push_back(MakePath("0,0 0,5 5,5 5,0")); - subject.push_back(MakePath("1,1 1,6 6,6 6,1")); + subject.push_back(MakePath({ 0,0, 0,5, 5,5, 5,0 })); + subject.push_back(MakePath({ 1,1, 1,6, 6,6, 6,1 })); Clipper64 clipper; clipper.AddSubject(subject); diff --git a/CPP/Tests/TestRectClip.cpp b/CPP/Tests/TestRectClip.cpp index ace3c000..2b5fda96 100644 --- a/CPP/Tests/TestRectClip.cpp +++ b/CPP/Tests/TestRectClip.cpp @@ -10,27 +10,27 @@ TEST(Clipper2Tests, TestRectClip) Rect64 rect = Rect64(100,100, 700, 500); clp.push_back(rect.AsPath()); - sub.push_back(MakePath("100,100, 700,100, 700,500, 100,500")); + sub.push_back(MakePath({ 100,100, 700,100, 700,500, 100,500 })); sol = RectClip(rect, sub); EXPECT_TRUE(Area(sol) == Area(sub)); sub.clear(); - sub.push_back(MakePath("110,110, 700,100, 700,500, 100,500")); + sub.push_back(MakePath({ 110,110, 700,100, 700,500, 100,500 })); sol = RectClip(rect, sub); EXPECT_TRUE(Area(sol) == Area(sub)); sub.clear(); - sub.push_back(MakePath("90,90, 700,100, 700,500, 100,500")); + sub.push_back(MakePath({ 90,90, 700,100, 700,500, 100,500 })); sol = RectClip(rect, sub); EXPECT_TRUE(Area(sol) == Area(clp)); sub.clear(); - sub.push_back(MakePath("90,90, 710,90, 710,510, 90,510")); + sub.push_back(MakePath({ 90,90, 710,90, 710,510, 90,510 })); sol = RectClip(rect, sub); EXPECT_TRUE(Area(sol) == Area(clp)); sub.clear(); - sub.push_back(MakePath("110,110, 690,110, 690,490, 110,490")); + sub.push_back(MakePath({ 110,110, 690,110, 690,490, 110,490 })); sol = RectClip(rect, sub); EXPECT_TRUE(Area(sol) == Area(sub)); @@ -38,22 +38,23 @@ TEST(Clipper2Tests, TestRectClip) clp.clear(); rect = Rect64(390, 290, 410, 310); clp.push_back(rect.AsPath()); - sub.push_back(MakePath("410,290 500,290 500,310 410,310")); + sub.push_back(MakePath({ 410,290, 500,290, 500,310, 410,310 })); sol = RectClip(rect, sub); EXPECT_TRUE(sol.empty()); sub.clear(); - sub.push_back(MakePath("430,290 470,330 390,330")); + sub.push_back(MakePath({ 430,290, 470,330, 390,330 })); sol = RectClip(rect, sub); EXPECT_TRUE(sol.empty()); sub.clear(); - sub.push_back(MakePath("450,290 480,330 450,330")); + sub.push_back(MakePath({ 450,290, 480,330, 450,330 })); sol = RectClip(rect, sub); EXPECT_TRUE(sol.empty()); sub.clear(); - sub.push_back(MakePath("208,66 366,112 402,303 234,332 233,262 243,140 215,126 40,172")); + sub.push_back(MakePath({ 208,66, 366,112, 402,303, + 234,332, 233,262, 243,140, 215,126, 40,172 })); rect = Rect64(237, 164, 322, 248); sol = RectClip(rect, sub); const auto solBounds = Bounds(sol); diff --git a/CPP/Tests/TestTrimCollinear.cpp b/CPP/Tests/TestTrimCollinear.cpp index b0b82384..94e5da0f 100644 --- a/CPP/Tests/TestTrimCollinear.cpp +++ b/CPP/Tests/TestTrimCollinear.cpp @@ -6,22 +6,22 @@ using namespace Clipper2Lib; TEST(Clipper2Tests, TestTrimCollinear) { Path64 input1 = - MakePath("10,10, 10,10, 50,10, 100,10, 100,100, 10,100, 10,10, 20,10"); + MakePath({ 10,10, 10,10, 50,10, 100,10, 100,100, 10,100, 10,10, 20,10 }); Path64 output1 = TrimCollinear(input1, false); EXPECT_EQ(output1.size(), 4); Path64 input2 = - MakePath("10,10, 10,10, 100,10, 100,100, 10,100, 10,10, 10,10"); + MakePath({ 10,10, 10,10, 100,10, 100,100, 10,100, 10,10, 10,10 }); Path64 output2 = TrimCollinear(input2, true); EXPECT_EQ(output2.size(), 5); - Path64 input3 = MakePath("10,10, 10,50, 10,10, 50,10,50,50, \ - 50,10, 70,10, 70,50, 70,10, 50,10, 100,10, 100,50, 100,10"); + Path64 input3 = MakePath({ 10,10, 10,50, 10,10, 50,10,50,50, + 50,10, 70,10, 70,50, 70,10, 50,10, 100,10, 100,50, 100,10 }); Path64 output3 = TrimCollinear(input3); EXPECT_EQ(output3.size(), 0); - Path64 input4 = MakePath("2,3, 3,4, 4,4, 4,5, 7,5, \ - 8,4, 8,3, 9,3, 8,3, 7,3, 6,3, 5,3, 4,3, 3,3, 2,3"); + Path64 input4 = MakePath({ 2,3, 3,4, 4,4, 4,5, 7,5, + 8,4, 8,3, 9,3, 8,3, 7,3, 6,3, 5,3, 4,3, 3,3, 2,3 }); Path64 output4a = TrimCollinear(input4); Path64 output4b = TrimCollinear(output4a); diff --git a/CPP/Utils/clipper.svg.utils.h b/CPP/Utils/clipper.svg.utils.h index 3831c58b..6d94ab47 100644 --- a/CPP/Utils/clipper.svg.utils.h +++ b/CPP/Utils/clipper.svg.utils.h @@ -96,13 +96,13 @@ namespace Clipper2Lib { } - inline void SvgAddSolution(SvgWriter& svg, const PathsD& path, FillRule fillrule, bool show_coords) + inline void SvgAddSolution(SvgWriter& svg, const PathsD &path, FillRule fillrule, bool show_coords) { svg.AddPaths(path, false, fillrule, solution_brush_clr, 0xFF003300, 1.2, show_coords); } - inline void SvgAddSolution(SvgWriter& svg, const Paths64& path, FillRule fillrule, bool show_coords) + inline void SvgAddSolution(SvgWriter& svg, const Paths64 &path, FillRule fillrule, bool show_coords) { svg.AddPaths(TransformPaths(path), false, fillrule, solution_brush_clr, 0xFF003300, 1.2, show_coords); @@ -129,7 +129,7 @@ namespace Clipper2Lib { int max_width = 0, int max_height = 0, int margin = 0) { if (FileExists(filename)) remove(filename.c_str()); - svg.SetCoordsStyle("Verdana", 0xFF0000AA, 9); + svg.SetCoordsStyle("Verdana", 0xFF0000AA, 7); svg.SaveToFile(filename, max_width, max_height, margin); } } diff --git a/CSharp/Clipper2Lib.Tests/Tests1/Tests/TestPolygons.cs b/CSharp/Clipper2Lib.Tests/Tests1/Tests/TestPolygons.cs index 6c715552..2a0a3535 100644 --- a/CSharp/Clipper2Lib.Tests/Tests1/Tests/TestPolygons.cs +++ b/CSharp/Clipper2Lib.Tests/Tests1/Tests/TestPolygons.cs @@ -43,36 +43,43 @@ public void TestClosedPaths() double areaDiffRatio = storedArea <= 0 ? 0 : (double) areaDiff / storedArea; // check polygon counts - if (storedCount <= 0) - ; // skip count - else if (IsInList(testNum, new int[] { 165, 166, 172, 173, 176, 177, 179 })) - Assert.IsTrue(countDiff <= 8); - else if (testNum >= 120) - Assert.IsTrue(countDiff <= 5); - else if (IsInList(testNum, new int[] { 27, 121, 126 })) - Assert.IsTrue(countDiff <= 2); - else if (IsInList(testNum, new int[] { 23, 37, 43, 45, 87, 102, 111, 118, 119 })) - Assert.IsTrue(countDiff <= 1); - else - Assert.IsTrue(countDiff == 0); + if (storedCount > 0) + { + if (IsInList(testNum, new int[] { 140, 150, 165, 166, 172, 173, 176, 177, 179 })) + { + Assert.IsTrue(countDiff <= 9); + } + else if (testNum >= 120) + { + Assert.IsTrue(countDiff <= 6); + } + else if (IsInList(testNum, new int[] { 27, 121, 126 })) + Assert.IsTrue(countDiff <= 2); + else if (IsInList(testNum, new int[] { 23, 37, 43, 45, 87, 102, 111, 118, 119 })) + Assert.IsTrue(countDiff <= 1); + else + Assert.IsTrue(countDiff == 0); + } // check polygon areas - if (storedArea <= 0) - ; // skip area - else if (IsInList(testNum, new int[] { 19, 22, 23, 24 })) - Assert.IsTrue(areaDiffRatio <= 0.5); - else if (testNum == 193) - Assert.IsTrue(areaDiffRatio <= 0.25); - else if (testNum == 63) - Assert.IsTrue(areaDiffRatio <= 0.1); - else if (testNum == 16) - Assert.IsTrue(areaDiffRatio <= 0.075); - else if (IsInList(testNum, new int[] { 15, 26 })) - Assert.IsTrue(areaDiffRatio <= 0.05); - else if (IsInList(testNum, new int[] { 52, 53, 54, 59, 60, 64, 117, 118, 119, 184 })) - Assert.IsTrue(areaDiffRatio <= 0.02); - else - Assert.IsTrue(areaDiffRatio <= 0.01); + if (storedArea > 0) + { + if (IsInList(testNum, new int[] { 19, 22, 23, 24 })) + Assert.IsTrue(areaDiffRatio <= 0.5); + else if (testNum == 193) + Assert.IsTrue(areaDiffRatio <= 0.25); + else if (testNum == 63) + Assert.IsTrue(areaDiffRatio <= 0.1); + else if (testNum == 16) + Assert.IsTrue(areaDiffRatio <= 0.075); + else if (IsInList(testNum, new int[] { 15, 26 })) + Assert.IsTrue(areaDiffRatio <= 0.05); + else if (IsInList(testNum, new int[] { 52, 53, 54, 59, 60, 64, 117, 118, 119, 184 })) + Assert.IsTrue(areaDiffRatio <= 0.02); + else + Assert.IsTrue(areaDiffRatio <= 0.01); + } + } //bottom of num loop } diff --git a/CSharp/Clipper2Lib/Clipper.Engine.cs b/CSharp/Clipper2Lib/Clipper.Engine.cs index b422bb3c..9855fa5e 100644 --- a/CSharp/Clipper2Lib/Clipper.Engine.cs +++ b/CSharp/Clipper2Lib/Clipper.Engine.cs @@ -1,6 +1,6 @@ /******************************************************************************* * Author : Angus Johnson * -* Date : 27 January 2023 * +* Date : 6 February 2023 * * Website : http://www.angusj.com * * Copyright : Angus Johnson 2010-2023 * * Purpose : This is the main polygon clipping module * @@ -14,7 +14,6 @@ using System.Collections; using System.Collections.Generic; using System.Runtime.CompilerServices; -using System.Runtime.ConstrainedExecution; using Clipper2Lib; namespace Clipper2Lib @@ -2356,10 +2355,13 @@ private void CheckJoinLeft(Active e, if (prev == null || IsOpen(e) || IsOpen(prev) || !IsHotEdge(e) || !IsHotEdge(prev) || pt.Y < e.top.Y + 2 || pt.Y < prev.top.Y + 2) return; - if (checkCurrX) prev.curX = TopX(prev, pt.Y); - if (e.curX != prev.curX || - InternalClipper.CrossProduct(e.top, pt, prev.top) != 0) - return; + + if (checkCurrX) + { + if (Clipper.PerpendicDistFromLineSqrd(pt, prev.bot, prev.top) > 0.25) return; + } + else if (e.curX != prev.curX) return; + if (InternalClipper.CrossProduct(e.top, pt, prev.top) != 0) return; if (e.outrec!.idx == prev.outrec!.idx) AddLocalMaxPoly(prev, e, pt); @@ -2381,9 +2383,12 @@ private void CheckJoinRight(Active e, pt.Y < e.top.Y + 2 || pt.Y < next.top.Y + 2) // avoids trivial joins return; - if (checkCurrX) next.curX = TopX(next, pt.Y); - if (e.curX != next.curX || - (InternalClipper.CrossProduct(e.top, pt, next.top) != 0)) + if (checkCurrX) + { + if (Clipper.PerpendicDistFromLineSqrd(pt, next.bot, next.top) > 0.25) return; + } + else if (e.curX != next.curX) return; + if (InternalClipper.CrossProduct(e.top, pt, next.top) != 0) return; if (e.outrec!.idx == next.outrec!.idx) diff --git a/Delphi/Clipper2Lib/Clipper.Core.pas b/Delphi/Clipper2Lib/Clipper.Core.pas index 116f26cc..606d8e87 100644 --- a/Delphi/Clipper2Lib/Clipper.Core.pas +++ b/Delphi/Clipper2Lib/Clipper.Core.pas @@ -2,7 +2,8 @@ (******************************************************************************* * Author : Angus Johnson * -* Date : 21 January 2023 * +* Date : 2 February 2023 * +* Website : http://www.angusj.com * * Copyright : Angus Johnson 2010-2023 * * Purpose : Core Clipper Library module * * Contains structures and functions used throughout the library * @@ -252,8 +253,10 @@ function ScalePathsD(const paths: TPathsD; scale: double): TPathsD; overload; function Path64(const pathD: TPathD): TPath64; function PathD(const path: TPath64): TPathD; -function Paths64(const pathsD: TPathsD): TPaths64; -function PathsD(const paths: TPaths64): TPathsD; +function Paths64(const path: TPath64): TPaths64; overload; +function Paths64(const pathsD: TPathsD): TPaths64; overload; +function PathsD(const paths: TPaths64): TPathsD; overload; +function PathsD(const path: TPathD): TPathsD; overload; function StripDuplicates(const path: TPath64; isClosedPath: Boolean = false): TPath64; function StripNearDuplicates(const path: TPathD; @@ -1021,6 +1024,13 @@ function PathD(const path: TPath64): TPathD; end; //------------------------------------------------------------------------------ +function Paths64(const path: TPath64): TPaths64; +begin + setLength(Result, 1); + Result[0] := path; +end; +//------------------------------------------------------------------------------ + function Paths64(const pathsD: TPathsD): TPaths64; var i, len: integer; @@ -1043,6 +1053,14 @@ function PathsD(const paths: TPaths64): TPathsD; end; //------------------------------------------------------------------------------ +function PathsD(const path: TPathD): TPathsD; +begin + setLength(Result, 1); + Result[0] := path; +end; +//------------------------------------------------------------------------------ + + function ReversePath(const path: TPath64): TPath64; var i, highI: Integer; @@ -1135,6 +1153,7 @@ procedure AppendPath(var paths: TPaths64; const extra: TPath64); var len: Integer; begin + if not Assigned(extra) then Exit; len := length(paths); SetLength(paths, len +1); paths[len] := extra; @@ -1145,6 +1164,7 @@ procedure AppendPath(var paths: TPathsD; const extra: TPathD); var len: Integer; begin + if not Assigned(extra) then Exit; len := length(paths); SetLength(paths, len +1); paths[len] := extra; diff --git a/Delphi/Clipper2Lib/Clipper.Engine.pas b/Delphi/Clipper2Lib/Clipper.Engine.pas index 4fa383e8..f2b662ef 100644 --- a/Delphi/Clipper2Lib/Clipper.Engine.pas +++ b/Delphi/Clipper2Lib/Clipper.Engine.pas @@ -2156,9 +2156,12 @@ procedure TClipperBase.CheckJoinLeft(e: PActive; if IsOpen(e) or not IsHotEdge(e) or not Assigned(prev) or IsOpen(prev) or not IsHotEdge(prev) or (pt.Y < e.top.Y +2) or (pt.Y < prev.top.Y +2) then Exit; - if checkCurrX then prev.currX := TopX(prev, pt.Y); - if (e.currX <> prev.currX) or - (CrossProduct(e.top, pt, prev.top) <> 0) then Exit; + if checkCurrX then + begin + if DistanceFromLineSqrd(pt, prev.bot, prev.top) > 0.25 then Exit + end else if (e.currX <> prev.currX) then Exit; + + if (CrossProduct(e.top, pt, prev.top) <> 0) then Exit; if (e.outrec.idx = prev.outrec.idx) then AddLocalMaxPoly(prev, e, pt) @@ -2181,10 +2184,13 @@ procedure TClipperBase.CheckJoinRight(e: PActive; IsOpen(next) or not IsHotEdge(next) or (pt.Y < e.top.Y +2) or (pt.Y < next.top.Y +2) then Exit; - if (checkCurrX) then next.currX := TopX(next, pt.Y); - if (e.currX <> next.currX) or - (CrossProduct(e.top, pt, next.top) <> 0) then Exit; + if (checkCurrX) then + begin + if DistanceFromLineSqrd(pt, next.bot, next.top) > 0.25 then Exit + end + else if (e.currX <> next.currX) then Exit; + if (CrossProduct(e.top, pt, next.top) <> 0) then Exit; if e.outrec.idx = next.outrec.idx then AddLocalMaxPoly(e, next, pt) else if e.outrec.idx < next.outrec.idx then diff --git a/Delphi/Clipper2Lib/Clipper.Offset.pas b/Delphi/Clipper2Lib/Clipper.Offset.pas index 74eff7b1..5ef894ac 100644 --- a/Delphi/Clipper2Lib/Clipper.Offset.pas +++ b/Delphi/Clipper2Lib/Clipper.Offset.pas @@ -681,9 +681,12 @@ procedure TClipperOffset.OffsetPoint(j: Integer; begin //almost no angle or concave AddPoint(GetPerpendic(fInPath[j], fNorms[k], fGrpDelta)); - // create a simple self-intersection that will be cleaned up later - if not almostNoAngle then AddPoint(fInPath[j]); - AddPoint(GetPerpendic(fInPath[j], fNorms[j], fGrpDelta)); + if not almostNoAngle then + begin + // create a simple self-intersection that will be cleaned up later + //AddPoint(fInPath[j]); // todo: check, as may not be needed + AddPoint(GetPerpendic(fInPath[j], fNorms[j], fGrpDelta)); + end; end else // convex offset begin diff --git a/Delphi/Clipper2Lib/Clipper.RectClip.pas b/Delphi/Clipper2Lib/Clipper.RectClip.pas index cb7bf101..1d6c7bfe 100644 --- a/Delphi/Clipper2Lib/Clipper.RectClip.pas +++ b/Delphi/Clipper2Lib/Clipper.RectClip.pas @@ -2,9 +2,9 @@ (******************************************************************************* * Author : Angus Johnson * -* Date : 9 November 2022 * +* Date : 6 February 2023 * * Website : http://www.angusj.com * -* Copyright : Angus Johnson 2010-2022 * +* Copyright : Angus Johnson 2010-2023 * * Purpose : FAST rectangular clipping * * License : http://www.boost.org/LICENSE_1_0.txt * *******************************************************************************) @@ -19,18 +19,36 @@ interface type TLocation = (locLeft, locTop, locRight, locBottom, locInside); + POutPt2 = ^TOutPt2; + POutPtArray = ^TOutPtArray; + TOutPtArray = array of POutPt2; + TOutPtArrayArray = array of TOutPtArray; + + TOutPt2 = record + ownerIdx: Cardinal; + edge: POutPtArray; + pt: TPoint64; + next: POutPt2; + prev: POutPt2; + end; + TRectClip = class protected - fResultCnt : integer; - fCapacity : integer; + fResults : TList; fRect : TRect64; fRectPath : TPath64; fRectMidPt : TPoint64; fFirstCrossLoc : TLocation; - fResult : TPath64; + fEdges : TOutPtArrayArray; fStartLocs : TList; - procedure Reset; - procedure Add(const pt: TPoint64); + function GetPrevOp: POutPt2; + {$IFDEF INLINING} inline; {$ENDIF} + function SafeDisposeOp(op: POutPt2): POutPt2; + {$IFDEF INLINING} inline; {$ENDIF} + procedure DisposeResults; + procedure CheckEdges; + procedure MergeEdges(idx: integer); + function Add(const pt: TPoint64): POutPt2; {$IFDEF INLINING} inline; {$ENDIF} procedure AddCorner(prev, curr: TLocation); overload; {$IFDEF INLINING} inline; {$ENDIF} @@ -38,21 +56,27 @@ TRectClip = class {$IFDEF INLINING} inline; {$ENDIF} procedure GetNextLocation(const path: TPath64; var loc: TLocation; var i: integer; highI: integer); + procedure ExecuteInternal(const path: TPath64); virtual; + function GetPath(resultIdx: integer): TPath64; public constructor Create(const rect: TRect64); destructor Destroy; override; - function Execute(const path: TPath64): TPath64; + //function Execute(const path: TPath64): TPaths64; + function Execute(const paths: TPaths64): TPaths64; end; TRectClipLines = class(TRectClip) private - function GetCurrentPath: TPath64; - public - function Execute(const path: TPath64): TPaths64; + //function GetCurrentPath: TPath64; + protected + procedure ExecuteInternal(const path: TPath64); override; end; implementation +type + PPath64 = ^TPath64; + //------------------------------------------------------------------------------ // Miscellaneous functions //------------------------------------------------------------------------------ @@ -225,6 +249,65 @@ function IsClockwise(prev, curr: TLocation; Result := CrossProduct(prevPt, rectMidPt, currPt) < 0 else Result := HeadingClockwise(prev, curr); end; +//------------------------------------------------------------------------------ + +function CountOp(op: POutPt2): integer; +var + op2: POutPt2; +begin + if not Assigned(op) then + begin + Result := 0; + Exit; + end; + Result := 1; + op2 := op; + while op2.next <> op do + begin + inc(Result); + op2 := op2.next; + end; +end; +//------------------------------------------------------------------------------ + +procedure SetNewOwner(op: POutPt2; newIdx: integer); +var + op2: POutPt2; +begin + op2 := op; + op.ownerIdx := newIdx; + while op2.next <> op do + begin + op2 := op2.next; + op2.ownerIdx := newIdx; + end; +end; +//------------------------------------------------------------------------------ + +procedure AddToEdge(edge: POutPtArray; const op: POutPt2); +var + len: integer; +begin + if Assigned(op.edge) then Exit; + op.edge := edge; + len := Length(edge^); + SetLength(edge^, len+1); + edge^[len] := op; +end; +//------------------------------------------------------------------------------ + +function HasHorzOverlap(const left1, right1, left2, right2: TPoint64): boolean; + {$IFDEF INLINING} inline; {$ENDIF} +begin + Result := (left1.X < right2.X) and (right1.X > left2.X); +end; +//------------------------------------------------------------------------------ + +function HasVertOverlap(const top1, bottom1, top2, bottom2: TPoint64): boolean; + {$IFDEF INLINING} inline; {$ENDIF} +begin + Result := (top1.Y < bottom2.Y) and (bottom1.Y > top2.Y); +end; //------------------------------------------------------------------------------ // TRectClip class @@ -232,44 +315,153 @@ function IsClockwise(prev, curr: TLocation; constructor TRectClip.Create(const rect: TRect64); begin + fResults := TList.Create; + fRect := rect; fRectPath := fRect.AsPath; fRectMidPt := rect.MidPoint; fStartLocs := TList.Create; + SetLength(fEdges, 8); end; //------------------------------------------------------------------------------ destructor TRectClip.Destroy; begin fStartLocs.Free; + fResults.Free; end; //------------------------------------------------------------------------------ -procedure TRectClip.Reset; +function TRectClip.GetPrevOp: POutPt2; begin - fResultCnt := 0; - fCapacity := 0; - fResult := nil; + if fResults.Count = 0 then + Result := nil else + Result := fResults[0]; end; //------------------------------------------------------------------------------ -procedure TRectClip.Add(const pt: TPoint64); +procedure UncoupleEdge(op: POutPt2); {$IFDEF INLINING} inline; {$ENDIF} +var + i: integer; begin - if fResultCnt = fCapacity then + if not Assigned(op.edge) then Exit; + for i := 0 to High(POutPtArray(op.edge)^) do + if POutPtArray(op.edge)^[i] = op then + begin + POutPtArray(op.edge)^[i] := nil; + Break; + end; + op.edge := nil; +end; +//------------------------------------------------------------------------------ + +function DisposeOp(op: POutPt2): POutPt2; +begin + if op.next = op then + Result := nil else + Result := op.next; + op.prev.next := op.next; + op.next.prev := op.prev; + Dispose(op); +end; +//------------------------------------------------------------------------------ + +function DisposeOpP(op: POutPt2): POutPt2; +begin + if op.prev = op then + Result := nil else + Result := op.prev; + op.prev.next := op.next; + op.next.prev := op.prev; + Dispose(op); +end; +//------------------------------------------------------------------------------ + +function TRectClip.SafeDisposeOp(op: POutPt2): POutPt2; +begin + if fResults[op.ownerIdx] = op then + begin + if op.next = op then + fResults[op.ownerIdx] := nil else + fResults[op.ownerIdx] := op.next; + end; + + if Assigned(op.edge) then begin - inc(fCapacity, 32); - SetLength(fResult, fCapacity); + op.next.edge := nil; + AddToEdge(POutPtArray(op.edge), op.next); + UncoupleEdge(op); + end; + Result := DisposeOp(op); +end; +//------------------------------------------------------------------------------ + +procedure DisposeOps(op: POutPt2); +var + tmp: POutPt2; +begin + if not Assigned(op) then Exit; + op.prev.next := nil; + while assigned(op) do + begin + tmp := op; + op := op.next; + Dispose(tmp); + end; +end; +//------------------------------------------------------------------------------ + +procedure TRectClip.DisposeResults; +var + i: integer; +begin + for i := 0 to fResults.Count -1 do + DisposeOps(fResults[i]); + fResults.Clear; +end; +//------------------------------------------------------------------------------ + +function TRectClip.Add(const pt: TPoint64): POutPt2; +var + prevOp: POutPt2; +begin + + prevOp := GetPrevOp; + if Assigned(prevOp) and PointsEqual(prevOp.pt, pt) then + begin + Result := prevOp; + Exit; + end; + + new(Result); + Result.pt := pt; + Result.edge := nil; + if not Assigned(prevOp) then + begin + Result.ownerIdx := fResults.Add(Result); + Result.next := Result; + Result.prev := Result; + end else + begin + Result.ownerIdx := 0; + Result.next := prevOp.next; + prevOp.next.prev := Result; + prevOp.next := Result; + Result.prev := prevOp; + fResults[0] := Result; end; - fResult[fResultCnt] := pt; - inc(fResultCnt); end; //------------------------------------------------------------------------------ procedure TRectClip.AddCorner(prev, curr: TLocation); +var + cnrIdx: integer; begin + if prev = curr then Exit; if (HeadingClockwise(prev, curr)) then - Add(fRectPath[Ord(prev)]) else - Add(fRectPath[Ord(curr)]); + cnrIdx := Ord(prev) else + cnrIdx := Ord(curr); + Add(fRectPath[cnrIdx]); end; //------------------------------------------------------------------------------ @@ -287,6 +479,276 @@ procedure TRectClip.AddCorner(var loc: TLocation; isClockwise: Boolean); end; //------------------------------------------------------------------------------ +function GetEdgesForPt(const pt: TPoint64; const rec: TRect64): cardinal; +begin + if pt.X = rec.Left then + Result := 1 + else if pt.X = rec.Right then + Result := 4 + else + Result := 0; + + if pt.Y = rec.Top then + inc(Result, 2) + else if pt.Y = rec.Bottom then + inc(Result, 8); +end; +//------------------------------------------------------------------------------ + +function IsHeadingClockwise(const pt1, pt2: TPoint64; + edgeIdx: integer): Boolean; +begin + case edgeIdx of + 0: Result := pt2.Y < pt1.Y; + 1: Result := pt2.X > pt1.X; + 2: Result := pt2.Y > pt1.Y; + else Result := pt2.X < pt1.X; + end; +end; +//------------------------------------------------------------------------------ + +procedure TRectClip.CheckEdges; +var + i,j: integer; + edgeSet1, edgeSet2, combinedSet: Cardinal; + op, op2: POutPt2; +begin + for i := 0 to fResults.Count -1 do + begin + op := fResults[i]; + if not assigned(op) then Continue; + + op2 := op; + repeat + if (CrossProduct(op2.prev.pt, op2.pt, op2.next.pt) = 0) then + begin + op2 := DisposeOpP(op2); + if not assigned(op2) then break; + op := op2.prev; + end else + op2 := op2.next; + until (op2 = op); + + if not assigned(op2) then + begin + fResults[i] := nil; + Continue; + end; + fResults[i] := op; + + edgeSet1 := GetEdgesForPt(op.prev.pt, fRect); + op2 := op; + repeat + edgeSet2 := GetEdgesForPt(op2.pt, fRect); + combinedSet := edgeSet1 and edgeSet2; + for j := 0 to 3 do + if combinedSet and (1 shl j) <> 0 then + begin + if IsHeadingClockwise(op2.prev.pt, op2.pt, j) then + AddToEdge(@fEdges[j*2], op2) + else + AddToEdge(@fEdges[j*2+1], op2); + end; + edgeSet1 := edgeSet2; + op2 := op2.next; + until op2 = op; + end; +end; +//------------------------------------------------------------------------------ + +procedure TRectClip.MergeEdges(idx: integer); +var + cw, ccw: POutPtArray; + isHorz, cwIsTowardLarger: Boolean; + edgeSetCurrent, edgeSet2, edgeSet3: Cardinal; + i, j, highJ, newIdx: integer; + op, op2, p1, p2, p1a, p2a: POutPt2; + isRejoining, opIsLarger, op2IsLarger: Boolean; +begin + cw := @fEdges[idx *2]; + ccw := @fEdges[idx *2 +1]; + if not Assigned(ccw) then Exit; + edgeSetCurrent := 1 shl idx; + isHorz := idx in [1,3]; + cwIsTowardLarger := idx in [1,2]; + i := 0; j := 0; + while (i <= High(cw^)) do + begin + + p1 := cw^[i]; + if not Assigned(p1) then + begin + inc(i); + Continue; + end; + + highJ := high(ccw^); + while (j <= highJ) and not Assigned(ccw^[j]) do inc(j); + + if (j > highJ) then + begin + j := 0; + inc(i); + Continue; + end; + + if cwIsTowardLarger then + begin + // p1 >>>> p1a; + // p2 <<<< p2a; + p1 := cw^[i].prev; + p1a := cw^[i]; + p2 := ccw^[j]; + p2a := ccw^[j].prev; + end else + begin + // p1 <<<< p1a; + // p2 >>>> p2a; + p1 := cw^[i]; + p1a := cw^[i].prev; + p2 := ccw^[j].prev; + p2a := ccw^[j]; + end; + + if (p1.next = p1.prev) then + begin + cw^[i].edge := nil; + inc(i); + Continue; + end; + + if (p2.next = p2.prev) then + begin + ccw^[j].edge := nil; + ccw^[j] := nil; + inc(j); + Continue; + end; + + if (isHorz and not HasHorzOverlap(p1.pt, p1a.pt, p2.pt, p2a.pt)) or + (not isHorz and not HasVertOverlap(p1.pt, p1a.pt, p2.pt, p2a.pt)) then + begin + inc(j); + Continue; + end; + + // we're either splitting or rejoining the path to get here + isRejoining := cw^[i].ownerIdx <> ccw^[j].ownerIdx; + + if isRejoining then + begin + fResults[p2.ownerIdx] := nil; + SetNewOwner(p2, p1.ownerIdx); + end; + + if cwIsTowardLarger then + begin + // p1 >> | >> p1a; + // p2 << | << p2a; + p1.next := p2; + p2.prev := p1; + p1a.prev := p2a; + p2a.next := p1a; + end else + begin + // p1 << | << p1a; + // p2 >> | >> p2a; + p1.prev := p2; + p2.next := p1; + p1a.next := p2a; + p2a.prev := p1a; + end; + + if not isRejoining then + begin + NewIdx := fResults.Add(p1a); + SetNewOwner(p1a, newIdx); + end; + + if cwIsTowardLarger then + begin + op := p2; + op2 := p1a; + end else + begin + op := p1; + op2 := p2a; + end; + + fResults[op.ownerIdx] := op; + fResults[op2.ownerIdx] := op2; + + if isHorz then // X + begin + opIsLarger := op.pt.X > op.prev.pt.X; + op2IsLarger := op2.pt.X > op2.prev.pt.X; + end else // Y + begin + opIsLarger := op.pt.Y > op.prev.pt.Y; + op2IsLarger := op2.pt.Y > op2.prev.pt.Y; + end; + + if (op.next = op.prev) or + PointsEqual(op.pt, op.prev.pt) then + begin + if op2IsLarger = cwIsTowardLarger then + begin + cw^[i] := op2; + ccw^[j] := nil; + inc(j); + end else + begin + ccw^[j] := op2; + cw^[i] := nil; + inc(i); + end; + end + else if (op2.next = op2.prev) or + PointsEqual(op2.pt, op2.prev.pt) then + begin + if opIsLarger = cwIsTowardLarger then + begin + cw^[i] := op; + ccw^[j] := nil; + inc(j); + end else + begin + ccw^[j] := op; + cw^[i] := nil; + inc(i); + end; + end + else if opIsLarger = op2IsLarger then + begin + if opIsLarger = cwIsTowardLarger then + begin + cw^[i] := op; + UncoupleEdge(op2); + AddToEdge(cw, op2); + ccw^[j] := nil; + inc(j); + end else + begin + cw^[i] := nil; + ccw^[j] := op2; + UncoupleEdge(op); + AddToEdge(ccw, op); + inc(i); + j := 0; + end; + end else + begin + if opIsLarger = cwIsTowardLarger then + cw^[i] := op else + ccw^[j] := op; + if op2IsLarger = cwIsTowardLarger then + cw^[i] := op2 else + ccw^[j] := op2; + end; + end; +end; +//------------------------------------------------------------------------------ + procedure TRectClip.GetNextLocation(const path: TPath64; var loc: TLocation; var i: integer; highI: integer); begin @@ -356,22 +818,53 @@ function Path1ContainsPath2(const path1, path2: TPath64): TPointInPolygonResult; end; //------------------------------------------------------------------------------ -function TRectClip.Execute(const path: TPath64): TPath64; +function TRectClip.Execute(const paths: TPaths64): TPaths64; var - i,k, highI : integer; + i,j, len: integer; + pathrec: TRect64; +begin + result := nil; + + len:= Length(paths); + for i := 0 to len -1 do + begin + pathrec := GetBounds(paths[i]); + if not fRect.Intersects(pathRec) then Continue; + + if fRect.Contains(pathRec) then + begin + if (Length(paths[i]) > 2) then AppendPath(Result, paths[i]); + Continue; + end; + + ExecuteInternal(paths[i]); + + CheckEdges; + for j := 0 to 3 do + MergeEdges(j); // 0 == left ... 3 == bottom; + + for j := 0 to fResults.Count -1 do + AppendPath(Result, GetPath(j)); + DisposeResults; + fEdges := nil; + SetLength(fEdges, 8); + end; +end; +//------------------------------------------------------------------------------ + +procedure TRectClip.ExecuteInternal(const path: TPath64); +var + i,j, highI : integer; prevPt,ip,ip2 : TPoint64; - loc, prev : TLocation; + loc, prevLoc : TLocation; loc2 : TLocation; startingLoc : TLocation; crossingLoc : TLocation; prevCrossLoc : TLocation; tmpRect : TRect64; - isClockw : Boolean; + isCw : Boolean; begin - Result := nil; - if (Length(path) < 3) or fRect.IsEmpty then Exit; - Reset; - + if (Length(path) < 3) then Exit; i := 0; fStartLocs.Clear; highI := Length(path) -1; @@ -382,14 +875,15 @@ function TRectClip.Execute(const path: TPath64): TPath64; begin i := highI - 1; while (i >= 0) and - not GetLocation(fRect, path[i], prev) do + not GetLocation(fRect, path[i], prevLoc) do dec(i); if (i < 0) then begin - Result := path; + // all of path inside fRect + for j := 0 to highI do Add(path[j]); Exit; end; - if (prev = locInside) then + if (prevLoc = locInside) then loc := locInside; i := 0; end; @@ -398,7 +892,7 @@ function TRectClip.Execute(const path: TPath64): TPath64; /////////////////////////////////////////////////// while i <= highI do begin - prev := loc; + prevLoc := loc; prevCrossLoc := crossingLoc; GetNextLocation(path, loc, i, highI); if i > highI then Break; @@ -412,19 +906,19 @@ function TRectClip.Execute(const path: TPath64): TPath64; // ie remains outside (and crossingLoc still == loc) if (prevCrossLoc = locInside) then //ie rect still uncrossed begin - isClockw := IsClockwise(prev, loc, prevPt, path[i], fRectMidPt); + isCw := IsClockwise(prevLoc, loc, prevPt, path[i], fRectMidPt); repeat - fStartLocs.Add(Pointer(prev)); - prev := GetAdjacentLocation(prev, isClockw); - until prev = loc; + fStartLocs.Add(Pointer(prevLoc)); + prevLoc := GetAdjacentLocation(prevLoc, isCw); + until prevLoc = loc; crossingLoc := prevCrossLoc; // because still not crossed end - else if (prev <> locInside) and (prev <> loc) then + else if (prevLoc <> locInside) and (prevLoc <> loc) then begin - isClockw := IsClockwise(prev, loc, prevPt, path[i], fRectMidPt); + isCw := IsClockwise(prevLoc, loc, prevPt, path[i], fRectMidPt); repeat - AddCorner(prev, isClockw); - until prev = loc; + AddCorner(prevLoc, isCw); + until prevLoc = loc; end; inc(i); Continue; @@ -439,21 +933,21 @@ function TRectClip.Execute(const path: TPath64): TPath64; if (fFirstCrossLoc = locInside) then begin fFirstCrossLoc := crossingLoc; - fStartLocs.Add(Pointer(prev)); + fStartLocs.Add(Pointer(prevLoc)); end - else if (prev <> crossingLoc) then + else if (prevLoc <> crossingLoc) then begin - isClockw := IsClockwise(prev, crossingLoc, prevPt, path[i], fRectMidPt); + isCw := IsClockwise(prevLoc, crossingLoc, prevPt, path[i], fRectMidPt); repeat - AddCorner(prev, isClockw); - until prev = crossingLoc; + AddCorner(prevLoc, isCw); + until prevLoc = crossingLoc; end; end - else if (prev <> locInside) then + else if (prevLoc <> locInside) then begin // passing right through rect. 'ip' here will be the second // intersect pt but we'll also need the first intersect pt (ip2) - loc := prev; + loc := prevLoc; GetIntersection(fRectPath, prevPt, path[i], loc, ip2); if (prevCrossLoc <> locInside) then AddCorner(prevCrossLoc, loc); @@ -461,11 +955,14 @@ function TRectClip.Execute(const path: TPath64): TPath64; if (fFirstCrossLoc = locInside) then begin fFirstCrossLoc := loc; - fStartLocs.Add(Pointer(prev)); + fStartLocs.Add(Pointer(prevLoc)); end; - loc := crossingLoc; + //////////////////////////////// Add(ip2); + //////////////////////////////// + + loc := crossingLoc; if PointsEqual(ip, ip2) then begin // it's very likely that path[i] is on rect @@ -480,6 +977,7 @@ function TRectClip.Execute(const path: TPath64): TPath64; if (fFirstCrossLoc = locInside) then fFirstCrossLoc := crossingLoc; end; + Add(ip); end; //while i <= highI /////////////////////////////////////////////////// @@ -494,12 +992,16 @@ function TRectClip.Execute(const path: TPath64): TPath64; tmpRect := GetBounds(path); if tmpRect.Contains(fRect) and (Path1ContainsPath2(path, fRectPath) <> pipOutside) then - Result := fRectPath - else - result := nil; - end - else Result := path; - + begin + for i := 0 to 3 do + begin + Add(fRectPath[i]); + AddToEdge(@fEdges[i*2], fResults[0]); + end; + end; + end else + // path is inside rect + for i := 0 to high(path) do Add(path[i]); Exit; end; @@ -508,72 +1010,74 @@ function TRectClip.Execute(const path: TPath64): TPath64; begin if (fStartLocs.Count > 0) then begin - prev := loc; + prevLoc := loc; for i := 0 to fStartLocs.Count -1 do begin loc2 := TLocation(fStartLocs[i]); - if (prev = loc2) then Continue; - AddCorner(prev, HeadingClockwise(prev, loc2)); - prev := loc2; + if (prevLoc = loc2) then Continue; + AddCorner(prevLoc, HeadingClockwise(prevLoc, loc2)); + prevLoc := loc2; end; - loc := prev; + loc := prevLoc; end; if (loc <> fFirstCrossLoc) then AddCorner(loc, HeadingClockwise(loc, fFirstCrossLoc)); end; +end; +//------------------------------------------------------------------------------ - if fResultCnt < 3 then Exit; +function TRectClip.GetPath(resultIdx: integer): TPath64; +var + j, len: integer; + op, op2: POutPt2; +begin + result := nil; + op := fResults[resultIdx]; + if not Assigned(op) then Exit; - // tidy up duplicates and collinear segments - SetLength(Result, fResultCnt); - k := 0; - prevPt := fResult[fResultCnt -1]; - Result[0] := fResult[0]; - for i := 1 to fResultCnt -1 do - if CrossProduct(prevPt, Result[k], fResult[i]) = 0 then - begin - Result[k] := fResult[i]; - end else + len := CountOp(op); + while (op.next <> op.prev) and + (CrossProduct(op.prev.pt, op.pt, op.next.pt) = 0) do + op := DisposeOp(op); + // this must be incremented and repeated to be sure + // that all collinear edges have been removed. + op := op.next; + while (op.next <> op.prev) and + (CrossProduct(op.prev.pt, op.pt, op.next.pt) = 0) do + op := DisposeOp(op); + fResults[resultIdx] := op; //just incase it was deleted + + if (op.next = op.prev) then Exit; + SetLength(result, len); + Result[0] := op.pt; + op2 := op.next; + j := 0; + while op2 <> op do + begin + if (CrossProduct(Result[j], op2.pt, op2.next.pt) <> 0) then begin - prevPt := Result[k]; - inc(k); - Result[k] := fResult[i]; + inc(j); + Result[j] := op2.pt; end; - - if k < 2 then - Result := nil - // and a final check for collinearity - else if CrossProduct(Result[0], Result[k-1], Result[k]) = 0 then - SetLength(Result, k) - else - SetLength(Result, k +1); + op2 := op2.next; + end; + if (CrossProduct(Result[0],Result[j],Result[j-1]) = 0) then Dec(j); + SetLength(result, j+1); end; //------------------------------------------------------------------------------ // TRectClipLines //------------------------------------------------------------------------------ -function TRectClipLines.GetCurrentPath: TPath64; -begin - SetLength(fResult, fResultCnt); - Result := fResult; - Reset; -end; -//------------------------------------------------------------------------------ - -function TRectClipLines.Execute(const path: TPath64): TPaths64; +procedure TRectClipLines.ExecuteInternal(const path: TPath64); var i, highI : integer; - resCnt : integer; prevPt,ip,ip2 : TPoint64; loc, prev : TLocation; crossingLoc : TLocation; begin - resCnt := 0; - Result := nil; if (Length(path) < 2) or fRect.IsEmpty then Exit; - Reset; i := 1; highI := Length(path) -1; @@ -584,8 +1088,7 @@ function TRectClipLines.Execute(const path: TPath64): TPaths64; inc(i); if (i > highI) then begin - SetLength(Result, 1); - Result[0] := path; + for i := 0 to High(path) do Add(path[i]); Exit; end; if (prev = locInside) then @@ -626,27 +1129,10 @@ function TRectClipLines.Execute(const path: TPath64): TPaths64; Add(ip2); Add(ip); - inc(resCnt); - SetLength(Result, resCnt); - Result[resCnt -1] := GetCurrentPath; end else // path must be exiting rect - begin Add(ip); - inc(resCnt); - SetLength(Result, resCnt); - Result[resCnt -1] := GetCurrentPath; - end; - end; //while i <= highI /////////////////////////////////////////////////// - - if fResultCnt > 1 then - begin - inc(resCnt); - SetLength(Result, resCnt); - Result[resCnt -1] := GetCurrentPath; - end; - SetLength(Result, resCnt); end; //------------------------------------------------------------------------------ diff --git a/Delphi/Clipper2Lib/Clipper.pas b/Delphi/Clipper2Lib/Clipper.pas index 1cd838e9..38fa0822 100644 --- a/Delphi/Clipper2Lib/Clipper.pas +++ b/Delphi/Clipper2Lib/Clipper.pas @@ -367,49 +367,28 @@ function InflatePaths(const paths: TPathsD; delta: Double; //------------------------------------------------------------------------------ function RectClip(const rect: TRect64; const path: TPath64): TPath64; +var + paths: TPaths64; begin - Result := nil; - if rect.IsEmpty or (Length(path) = 0) or - not rect.Intersects(GetBounds(path)) then Exit; - with TRectClip.Create(rect) do - try - Result := Execute(path); - finally - Free; - end; + SetLength(paths, 1); + paths[0] := path; + paths := RectClip(rect, paths); + if Assigned(paths) then + Result := paths[0] else + Result := nil; end; //------------------------------------------------------------------------------ function RectClip(const rect: TRect64; const paths: TPaths64): TPaths64; -var - i,j, len: integer; - pathRec: TRect64; begin Result := nil; - len := Length(paths); - if rect.IsEmpty or (len = 0) then Exit; - SetLength(Result, len); - j := 0; + if rect.IsEmpty then Exit; with TRectClip.Create(rect) do try - for i := 0 to len -1 do - begin - pathRec := GetBounds(paths[i]); - if not rect.Intersects(pathRec) then - Continue - else if rect.Contains(pathRec) then - Result[j] := Copy(paths[i], 0, MaxInt) - else - begin - Result[j] := Execute(paths[i]); - if Result[j] = nil then Continue; - end; - inc(j); - end; + Result := Execute(paths); finally Free; end; - SetLength(Result, j); end; //------------------------------------------------------------------------------ @@ -434,53 +413,35 @@ function RectClip(const rect: TRectD; const path: TPathD; function RectClip(const rect: TRectD; const paths: TPathsD; precision: integer): TPathsD; var - i,j, len: integer; scale: double; - tmpPath: TPath64; + tmpPaths: TPaths64; rec: TRect64; - pathRec: TRectD; begin CheckPrecisionRange(precision); scale := Math.Power(10, precision); rec := Rect64(ScaleRect(rect, scale)); - j := 0; - len := Length(paths); - SetLength(Result, len); - + tmpPaths := ScalePaths(paths, scale); with TRectClip.Create(rec) do try - for i := 0 to len -1 do - begin - pathRec := GetBounds(paths[i]); - if not rect.Intersects(pathRec) then - Continue - else if rect.Contains(pathRec) then - Result[j] := Copy(paths[i], 0, MaxInt) - else - begin - tmpPath := ScalePath(paths[i], scale); - tmpPath := Execute(tmpPath); - if tmpPath = nil then Continue; - Result[j] := ScalePathD(tmpPath, 1/scale); - end; - inc(j); - end; + tmpPaths := Execute(tmpPaths); finally Free; end; - SetLength(Result, j); + Result := ScalePathsD(tmpPaths, 1/scale); end; //------------------------------------------------------------------------------ function RectClipLines(const rect: TRect64; const path: TPath64): TPaths64; overload; +var + tmp: TPaths64; begin Result := nil; - if rect.IsEmpty or (Length(path) = 0) or - not rect.Intersects(GetBounds(path)) then Exit; + SetLength(tmp, 1); + tmp[0] := path; with TRectClipLines.Create(rect) do try - Result := Execute(path); + Result := Execute(tmp); finally Free; end; @@ -488,30 +449,12 @@ function RectClipLines(const rect: TRect64; const path: TPath64): TPaths64; over //------------------------------------------------------------------------------ function RectClipLines(const rect: TRect64; const paths: TPaths64): TPaths64; overload; -var - i,len: integer; - pathRec: TRect64; - tmp: TPaths64; begin Result := nil; - len := Length(paths); - if rect.IsEmpty or (len = 0) then Exit; - SetLength(Result, len); + if rect.IsEmpty then Exit; with TRectClipLines.Create(rect) do try - for i := 0 to len -1 do - begin - pathRec := GetBounds(paths[i]); - if not rect.Intersects(pathRec) then - Continue - else if rect.Contains(pathRec) then - AppendPath(Result, paths[i]) - else - begin - tmp := Execute(paths[i]); - AppendPaths(Result, tmp); - end; - end; + Result := Execute(paths); finally Free; end; @@ -540,39 +483,23 @@ function RectClipLines(const rect: TRectD; const path: TPathD; function RectClipLines(const rect: TRectD; const paths: TPathsD; precision: integer = 2): TPathsD; var - i: integer; scale: double; - tmpPath: TPath64; tmpPaths: TPaths64; rec: TRect64; - pathRec: TRectD; begin Result := nil; if rect.IsEmpty then Exit; CheckPrecisionRange(precision); scale := Math.Power(10, precision); rec := Rect64(ScaleRect(rect, scale)); - + tmpPaths := ScalePaths(paths, scale); with TRectClipLines.Create(rec) do try - for i := 0 to High(paths) do - begin - pathRec := GetBounds(paths[i]); - if not rect.Intersects(pathRec) then - Continue - else if rect.Contains(pathRec) then - AppendPath(Result, paths[i]) - else - begin - tmpPath := ScalePath(paths[i], scale); - tmpPaths := Execute(tmpPath); - if tmpPaths = nil then Continue; - AppendPaths(Result, ScalePathsD(tmpPaths, 1/scale)); - end; - end; + tmpPaths := Execute(tmpPaths); finally Free; end; + Result := ScalePathsD(tmpPaths, 1/scale); end; //------------------------------------------------------------------------------