From bc5855a386f62e8b21868939858b98ea79d8bae0 Mon Sep 17 00:00:00 2001 From: angusj Date: Wed, 29 Nov 2023 14:36:26 +1000 Subject: [PATCH 01/64] Minor tweaks to PointInPolygon benchmark testing. --- CPP/BenchMark/PointInPolygonBenchmark.cpp | 125 ++++++++++++++++----- CPP/Clipper2Lib/include/clipper2/clipper.h | 2 +- 2 files changed, 98 insertions(+), 29 deletions(-) diff --git a/CPP/BenchMark/PointInPolygonBenchmark.cpp b/CPP/BenchMark/PointInPolygonBenchmark.cpp index 89a35fe2..401704a2 100644 --- a/CPP/BenchMark/PointInPolygonBenchmark.cpp +++ b/CPP/BenchMark/PointInPolygonBenchmark.cpp @@ -7,6 +7,7 @@ using namespace Clipper2Lib; using benchmark::State; +// PIP1: This is the current Clipper2 PointInPolygon code template inline PointInPolygonResult PIP1(const Point& pt, const Path& polygon) { @@ -89,13 +90,18 @@ inline PointInPolygonResult PIP1(const Point& pt, const Path& polygon) } +// PIP2: This is a not fully tested modification of the current +// Clipper2 PointInPolygon code. It's a little simpler and it's +// also marginally faster. template inline PointInPolygonResult PIP2(const Point& pt, const Path& polygon) { if (!polygon.size()) return PointInPolygonResult::IsOutside; Path::const_iterator cend = polygon.cend(); - Path::const_iterator prev = cend - 1; - Path::const_iterator curr = polygon.cbegin(); + Path::const_iterator last = cend - 1; + Path::const_iterator first = polygon.cbegin(); + Path::const_iterator curr = first; + Path::const_iterator prev = last; bool is_above; if (prev->y == pt.y) @@ -115,21 +121,21 @@ inline PointInPolygonResult PIP2(const Point& pt, const Path& polygon) { if (is_above) { - while (curr != cend && curr->y < pt.y) { prev = curr; ++curr; } + while (curr != cend && curr->y < pt.y) ++curr; if (curr == cend) break; } else { - while (curr != cend && curr->y > pt.y) { prev = curr; ++curr; } + while (curr != cend && curr->y > pt.y) ++curr; if (curr == cend) break; } + prev = (curr == first) ? last : curr - 1; if (curr->y == pt.y) { if ((curr->x == pt.x) || ((curr->y == prev->y) && ((pt.x > prev->x) == (pt.x < curr->x)))) return PointInPolygonResult::IsOn; - prev = curr; ++curr; continue; } @@ -142,19 +148,20 @@ inline PointInPolygonResult PIP2(const Point& pt, const Path& polygon) ++val; else { - double d = CrossProduct(*prev, *curr, pt); + double d = CrossProduct(*prev, *curr, pt); //avoids integer overflow if (d == 0) return PointInPolygonResult::IsOn; if ((d < 0) == is_above) ++val; } is_above = !is_above; - prev = curr; ++curr; } - return (val % 2) ? PointInPolygonResult::IsInside : PointInPolygonResult::IsOutside; + return (val % 2) ? + PointInPolygonResult::IsInside : + PointInPolygonResult::IsOutside; } - +// PIP3: An entirely different algorithm for comparision. // "Optimal Reliable Point-in-Polygon Test and // Differential Coding Boolean Operations on Polygons" // by Jianqiang Hao et al. @@ -206,8 +213,10 @@ static PointInPolygonResult PIP3(const Point &pt, const Path &path) } +// globals Paths64 paths; Point64 mp; +std::vector pipResults; PointInPolygonResult pip1 = PointInPolygonResult::IsOn; PointInPolygonResult pip2 = PointInPolygonResult::IsOn; PointInPolygonResult pip3 = PointInPolygonResult::IsOn; @@ -242,17 +251,65 @@ static void CustomArguments(benchmark::internal::Benchmark* b) for (int i = 0; i < paths.size(); ++i) b->Args({ i }); } -enum DoTests { do_stress_test_only, do_benchmark_only, do_all_tests }; +enum ConsoleTextColor { + reset = 0, + //normal text colors ... + red = 31, green = 32, yellow = 33, blue = 34, magenta = 35, cyan = 36, white = 37, + //bold text colors ... + red_bold = 91, green_bold = 92, yellow_bold = 93, blue_bold = 94, + magenta_bold = 95, cyan_bold = 96, white_bold = 97 +}; + +struct SetConsoleTextColor +{ +private: + ConsoleTextColor _color; +public: + SetConsoleTextColor(ConsoleTextColor color) : _color(color) {}; + + static friend std::ostream& operator<< (std::ostream& out, SetConsoleTextColor const& scc) + { + return out << "\x1B[" << scc._color << "m"; + } +}; + +static void DoErrorTest(int index) +{ + PointInPolygonResult(*pip_func)(const Point64&, const Path64&); + switch (index) + { + case 1: pip_func = PIP1; break; + case 2: pip_func = PIP2; break; + case 3: pip_func = PIP2; break; + default: throw "oops! - wrong function!"; + } + std::vector errors; + std::cout << SetConsoleTextColor(green_bold) << + "Testing PIP" << index << SetConsoleTextColor(reset) <<":"; + for (size_t i = 0; i < paths.size(); ++i) + if (pip_func(mp, paths[i]) != pipResults[i]) errors.push_back(i); + if (errors.size()) + { + size_t high_error = errors.size() - 1; + std::cout << SetConsoleTextColor(red_bold) << " Error in "; + for (size_t i = 0; i < high_error; ++i) + std::cout << errors[i] << " and "; + std::cout << errors[high_error] << "." << + SetConsoleTextColor(reset) << std::endl; + } + else + std::cout << " No errors found." << std::endl; +} int main(int argc, char** argv) { - const DoTests do_tests = do_all_tests; + enum DoTests { do_error_test_only, do_benchmark_only, do_all_tests }; + const DoTests do_tests = do_all_tests;// do_error_test_only;// if (do_tests != do_benchmark_only) { // stress test PIP2 with unusual polygons mp = Point64(10, 10); - std::vector pipResults; paths.push_back({}); pipResults.push_back(PointInPolygonResult::IsOutside); @@ -273,21 +330,12 @@ int main(int argc, char** argv) { paths.push_back(MakePath({ 0,0, 20,20, 100,0 })); pipResults.push_back(PointInPolygonResult::IsOn); - std::cout << "Stress testing PIP1 for errors: "; - for (size_t i = 0; i < paths.size(); ++i) - if (PIP1(mp, paths[i]) != pipResults[i]) - std::cout << " (" << i << ")"; - std::cout << std::endl; - std::cout << "Stress testing PIP2 for errors: "; - for (size_t i = 0; i < paths.size(); ++i) - if (PIP2(mp, paths[i]) != pipResults[i]) - std::cout << " (" << i << ")"; + std::cout << "Error Tests:" << std::endl << std::endl; + + DoErrorTest(1); + DoErrorTest(2); + DoErrorTest(3); std::cout << std::endl; - std::cout << "Stress testing PIP3 for errors: "; - for (size_t i = 0; i < paths.size(); ++i) - if (PIP3(mp, paths[i]) != pipResults[i]) - std::cout << " (" << i << ")"; - std::cout << std::endl << std::endl; if (do_tests != do_all_tests) { @@ -297,21 +345,42 @@ int main(int argc, char** argv) { } } - if (do_tests == do_stress_test_only) return 0; + if (do_tests == do_error_test_only) return 0; + + std::cout << "Benchmarks 1: " << + "benchmarking PIP on a single elliptical path" << std::endl << std::endl; // compare 3 PIP algorithms const int width = 600000, height = 400000; mp = Point64(width / 2, height / 2); + paths.clear(); + paths.push_back(Ellipse(mp, 10000, 6000, 10000)); + std::vector args{ 0 }; + benchmark::Initialize(&argc, argv); + BENCHMARK(BM_PIP1)->Args(args); // current Clipper2 + BENCHMARK(BM_PIP2)->Args(args); // modified Clipper2 + BENCHMARK(BM_PIP3)->Args(args); // Hao et al. (2018) + benchmark::RunSpecifiedBenchmarks(benchmark::CreateDefaultDisplayReporter()); + benchmark::ClearRegisteredBenchmarks(); + benchmark::Shutdown(); + + std::cout << std::endl << std::endl << + "Setting up before Benchmarks 2 ..." << + std::endl << std::endl; + paths.clear(); srand((unsigned)time(0)); for (int i = 0, count = 10000; i < 5; ++i, count *= 10) paths.push_back(MakeRandomPoly(width, height, count)); + std::cout << "Benchmarks 2: " << + "benchmarking PIP using a single self-intersecting polygon" << std::endl << std::endl; + benchmark::Initialize(&argc, argv); BENCHMARK(BM_PIP1)->Apply(CustomArguments); // current Clipper2 BENCHMARK(BM_PIP2)->Apply(CustomArguments); // modified Clipper2 BENCHMARK(BM_PIP3)->Apply(CustomArguments); // Hao et al. (2018) - benchmark::RunSpecifiedBenchmarks(); + benchmark::RunSpecifiedBenchmarks(benchmark::CreateDefaultDisplayReporter()); if (pip2 != pip1 || pip3 != pip1) { diff --git a/CPP/Clipper2Lib/include/clipper2/clipper.h b/CPP/Clipper2Lib/include/clipper2/clipper.h index 0f516b60..56266225 100644 --- a/CPP/Clipper2Lib/include/clipper2/clipper.h +++ b/CPP/Clipper2Lib/include/clipper2/clipper.h @@ -348,7 +348,7 @@ namespace Clipper2Lib { result.reserve(array_size / 2); for (size_t i = 0; i < array_size; i +=2) #ifdef USINGZ - result.push_back( U{ an_array[i], an_array[i +1], 0} ); + result.push_back( U{ an_array[i], an_array[i + 1], 0} ); #else result.push_back( U{ an_array[i], an_array[i + 1]} ); #endif From 605982cd3e4ae3af829d6615ee503cb1cd03af70 Mon Sep 17 00:00:00 2001 From: angusj Date: Fri, 1 Dec 2023 21:51:57 +1000 Subject: [PATCH 02/64] Fixed a significant (though uncommon) bug in polygon clipping (#736) Fixed bugs in PointInPolygon benchmark testing. --- CPP/BenchMark/PointInPolygonBenchmark.cpp | 410 +++++++++++++++------- CPP/BenchMark/StripDuplicateBenchmark.cpp | 12 +- CPP/Clipper2Lib/src/clipper.engine.cpp | 19 +- CPP/Utils/CommonUtils.h | 15 +- CSharp/Clipper2Lib/Clipper.Engine.cs | 14 +- Delphi/Clipper2Lib/Clipper.Engine.pas | 14 +- 6 files changed, 334 insertions(+), 150 deletions(-) diff --git a/CPP/BenchMark/PointInPolygonBenchmark.cpp b/CPP/BenchMark/PointInPolygonBenchmark.cpp index 401704a2..446a70f5 100644 --- a/CPP/BenchMark/PointInPolygonBenchmark.cpp +++ b/CPP/BenchMark/PointInPolygonBenchmark.cpp @@ -1,13 +1,46 @@ #include "benchmark/benchmark.h" #include "clipper2/clipper.h" #include "CommonUtils.h" +#include "ClipFileLoad.h" #include #include +#include using namespace Clipper2Lib; -using benchmark::State; +enum ConsoleTextColor { + reset = 0, + //normal text colors ... + red = 31, green = 32, yellow = 33, blue = 34, magenta = 35, cyan = 36, white = 37, + //bold text colors ... + red_bold = 91, green_bold = 92, yellow_bold = 93, blue_bold = 94, + magenta_bold = 95, cyan_bold = 96, white_bold = 97 +}; + +///////////////////////////////////////////////////////////////////////////// +// SetConsoleTextColor: a simple class to adjust Windows' Console Text Colors +///////////////////////////////////////////////////////////////////////////// + +struct SetConsoleTextColor +{ +private: + ConsoleTextColor _color; +public: + SetConsoleTextColor(ConsoleTextColor color) : _color(color) {}; + + static friend std::ostream& operator<< (std::ostream& out, SetConsoleTextColor const& scc) + { + return out << "\x1B[" << scc._color << "m"; + } +}; +///////////////////////////////////////////////////////////////////////////// + + +typedef std::function PipFunction; + +///////////////////////////////////////////////////////// // PIP1: This is the current Clipper2 PointInPolygon code +///////////////////////////////////////////////////////// template inline PointInPolygonResult PIP1(const Point& pt, const Path& polygon) { @@ -90,9 +123,11 @@ inline PointInPolygonResult PIP1(const Point& pt, const Path& polygon) } -// PIP2: This is a not fully tested modification of the current -// Clipper2 PointInPolygon code. It's a little simpler and it's -// also marginally faster. +///////////////////////////////////////////////////////// +// PIP2: This is a not fully tested modification of the +// current Clipper2 PointInPolygon code. It's a little +// simpler and also marginally faster. +///////////////////////////////////////////////////////// template inline PointInPolygonResult PIP2(const Point& pt, const Path& polygon) { @@ -161,14 +196,17 @@ inline PointInPolygonResult PIP2(const Point& pt, const Path& polygon) PointInPolygonResult::IsOutside; } +///////////////////////////////////////////////////////// // PIP3: An entirely different algorithm for comparision. // "Optimal Reliable Point-in-Polygon Test and // Differential Coding Boolean Operations on Polygons" // by Jianqiang Hao et al. // Symmetry 2018, 10(10), 477; https://doi.org/10.3390/sym10100477 +///////////////////////////////////////////////////////// template static PointInPolygonResult PIP3(const Point &pt, const Path &path) { + if (!path.size()) return PointInPolygonResult::IsOutside; T x1, y1, x2, y2; int k = 0; Path::const_iterator itPrev = path.cend() - 1; @@ -192,7 +230,7 @@ static PointInPolygonResult PIP3(const Point &pt, const Path &path) if (f > 0) ++k; else if (f == 0) return PointInPolygonResult::IsOn; } - else if ((y1 > 0) && (y2 <= 0)) + else if ((y1 > 0) && (y2 <= 0)) { int64_t f = x1 * y2 - x2 * y1; if (f < 0) ++k; @@ -213,81 +251,128 @@ static PointInPolygonResult PIP3(const Point &pt, const Path &path) } -// globals -Paths64 paths; +///////////////////////////////////////////////////////// +// global data structures +///////////////////////////////////////////////////////// + Point64 mp; -std::vector pipResults; -PointInPolygonResult pip1 = PointInPolygonResult::IsOn; -PointInPolygonResult pip2 = PointInPolygonResult::IsOn; -PointInPolygonResult pip3 = PointInPolygonResult::IsOn; +Paths64 paths; +Path64 points_of_interest_outside; +Path64 points_of_interest_inside; +std::vector < std::vector > pipResults; +///////////////////////////////////////////////////////// +// Benchmark callback functions +///////////////////////////////////////////////////////// + static void BM_PIP1(benchmark::State& state) { - for (auto _ : state) - { - pip1 = PIP1(mp, paths[state.range(0)]); - } + int64_t idx = state.range(0); + for (auto _ : state) + pipResults[0][idx] = PIP1(mp, paths[idx]); + } static void BM_PIP2(benchmark::State& state) { + int64_t idx = state.range(0); for (auto _ : state) - { - pip2 = PIP2(mp, paths[state.range(0)]); - } + pipResults[1][idx] = PIP2(mp, paths[idx]); } static void BM_PIP3(benchmark::State& state) { + int64_t idx = state.range(0); for (auto _ : state) - { - pip3 = PIP3(mp, paths[state.range(0)]); - } + pipResults[2][idx] = PIP3(mp, paths[idx]); } +///////////////////////////////////////////////////////// +// Miscellaneous functions +///////////////////////////////////////////////////////// + static void CustomArguments(benchmark::internal::Benchmark* b) { for (int i = 0; i < paths.size(); ++i) b->Args({ i }); } -enum ConsoleTextColor { - reset = 0, - //normal text colors ... - red = 31, green = 32, yellow = 33, blue = 34, magenta = 35, cyan = 36, white = 37, - //bold text colors ... - red_bold = 91, green_bold = 92, yellow_bold = 93, blue_bold = 94, - magenta_bold = 95, cyan_bold = 96, white_bold = 97 -}; +inline PipFunction GetPIPFunc(int index) +{ + PointInPolygonResult(*result)(const Point64&, const Path64&); + switch (index) + { + case 0: result = PIP1; break; + case 1: result = PIP2; break; + case 2: result = PIP3; break; + default: throw "oops! - wrong function!"; + } + return result; +} -struct SetConsoleTextColor +///////////////////////////////////////////////////////// +// Test functions +///////////////////////////////////////////////////////// + +// DoErrorTest1 +///////////////////////////////////////////////////////// + +static void DoErrorTest1_internal(const Path64& pts_of_int, const Paths64& paths, + PipFunction pip_func, PointInPolygonResult expected) { -private: - ConsoleTextColor _color; -public: - SetConsoleTextColor(ConsoleTextColor color) : _color(color) {}; + Path64 error_points; - static friend std::ostream& operator<< (std::ostream& out, SetConsoleTextColor const& scc) + for (Point64 poi : pts_of_int) { - return out << "\x1B[" << scc._color << "m"; + size_t inside_cnt = 0; + for (const Path64& path : paths) + if (pip_func(poi, path) == PointInPolygonResult::IsInside) ++inside_cnt; + switch (expected) + { + case PointInPolygonResult::IsInside: + if (inside_cnt != 1) error_points.push_back(poi); break; + case PointInPolygonResult::IsOutside: + if (inside_cnt) error_points.push_back(poi); break; + } } -}; -static void DoErrorTest(int index) -{ - PointInPolygonResult(*pip_func)(const Point64&, const Path64&); - switch (index) + if (error_points.size()) { - case 1: pip_func = PIP1; break; - case 2: pip_func = PIP2; break; - case 3: pip_func = PIP2; break; - default: throw "oops! - wrong function!"; + size_t high_error = error_points.size() - 1; + std::cout << SetConsoleTextColor(red_bold) << " Errors at "; + for (size_t i = 0; i < high_error; ++i) std::cout << "(" << error_points[i] << "), "; + std::cout << "(" << error_points[high_error] << ")." << SetConsoleTextColor(reset) << std::endl; } + else + std::cout << " No errors found." << std::endl; +} + +static void DoErrorTest1(int index) +{ + PipFunction pip_func = GetPIPFunc(index); + + std::cout << SetConsoleTextColor(green_bold) << + "Testing PIP" << index +1 << "/outside:" << SetConsoleTextColor(reset); + DoErrorTest1_internal(points_of_interest_outside, paths, + pip_func, PointInPolygonResult::IsOutside); + + std::cout << SetConsoleTextColor(green_bold) << + "Testing PIP" << index +1 << "/inside :" << SetConsoleTextColor(reset); + DoErrorTest1_internal(points_of_interest_inside, paths, + pip_func, PointInPolygonResult::IsInside); +} + +// DoErrorTest2 +///////////////////////////////////////////////////////// +static void DoErrorTest2(int index) +{ + PipFunction pip_func = GetPIPFunc(index); + std::vector errors; std::cout << SetConsoleTextColor(green_bold) << - "Testing PIP" << index << SetConsoleTextColor(reset) <<":"; + "Testing PIP" << index +1 << SetConsoleTextColor(reset) <<":"; for (size_t i = 0; i < paths.size(); ++i) - if (pip_func(mp, paths[i]) != pipResults[i]) errors.push_back(i); + if (pip_func(mp, paths[i]) != pipResults[0][i]) errors.push_back(i); if (errors.size()) { size_t high_error = errors.size() - 1; @@ -301,98 +386,173 @@ static void DoErrorTest(int index) std::cout << " No errors found." << std::endl; } -int main(int argc, char** argv) { - - enum DoTests { do_error_test_only, do_benchmark_only, do_all_tests }; - const DoTests do_tests = do_all_tests;// do_error_test_only;// - - if (do_tests != do_benchmark_only) - { - // stress test PIP2 with unusual polygons - mp = Point64(10, 10); - - paths.push_back({}); - pipResults.push_back(PointInPolygonResult::IsOutside); - paths.push_back(MakePath({ 100,10, 200,10 })); - pipResults.push_back(PointInPolygonResult::IsOutside); - paths.push_back(MakePath({ 100,10, 200,10, 10,10, 20,20 })); - pipResults.push_back(PointInPolygonResult::IsOn); - paths.push_back(MakePath({ 10,10 })); - pipResults.push_back(PointInPolygonResult::IsOn); - paths.push_back(MakePath({ 100,10 })); - pipResults.push_back(PointInPolygonResult::IsOutside); - paths.push_back(MakePath({ 100,10, 110,20, 200,10, 10,10, 20,20 })); - pipResults.push_back(PointInPolygonResult::IsOn); - paths.push_back(MakePath({ 100,10, 110,20, 200,10, 20,20 })); - pipResults.push_back(PointInPolygonResult::IsOutside); - paths.push_back(MakePath({ 200,0, 0,0, 10,20, 200,0, 20,0 })); - pipResults.push_back(PointInPolygonResult::IsInside); - paths.push_back(MakePath({ 0,0, 20,20, 100,0 })); - pipResults.push_back(PointInPolygonResult::IsOn); - - std::cout << "Error Tests:" << std::endl << std::endl; - - DoErrorTest(1); - DoErrorTest(2); - DoErrorTest(3); - std::cout << std::endl; - - if (do_tests != do_all_tests) - { - std::string _; - std::getline(std::cin, _); - return 0; - } - } - - if (do_tests == do_error_test_only) return 0; - - std::cout << "Benchmarks 1: " << - "benchmarking PIP on a single elliptical path" << std::endl << std::endl; +///////////////////////////////////////////////////////// +// Benchmark functions +///////////////////////////////////////////////////////// +// DoBenchmark1 +///////////////////////////////////////////////////////// +static void DoBenchmark1() +{ // compare 3 PIP algorithms - const int width = 600000, height = 400000; - mp = Point64(width / 2, height / 2); - paths.clear(); - paths.push_back(Ellipse(mp, 10000, 6000, 10000)); - std::vector args{ 0 }; - benchmark::Initialize(&argc, argv); - BENCHMARK(BM_PIP1)->Args(args); // current Clipper2 - BENCHMARK(BM_PIP2)->Args(args); // modified Clipper2 - BENCHMARK(BM_PIP3)->Args(args); // Hao et al. (2018) + pipResults.clear(); + pipResults.resize(3); + for (size_t i = 0; i < 3; ++i) pipResults[i].resize(paths.size()); + benchmark::Initialize(0, nullptr); + BENCHMARK(BM_PIP1)->Apply(CustomArguments); // current Clipper2 + BENCHMARK(BM_PIP2)->Apply(CustomArguments); // modified Clipper2 + BENCHMARK(BM_PIP3)->Apply(CustomArguments); // Hao et al. (2018) benchmark::RunSpecifiedBenchmarks(benchmark::CreateDefaultDisplayReporter()); benchmark::ClearRegisteredBenchmarks(); benchmark::Shutdown(); +} - std::cout << std::endl << std::endl << - "Setting up before Benchmarks 2 ..." << - std::endl << std::endl; - - paths.clear(); - srand((unsigned)time(0)); - for (int i = 0, count = 10000; i < 5; ++i, count *= 10) - paths.push_back(MakeRandomPoly(width, height, count)); - - std::cout << "Benchmarks 2: " << - "benchmarking PIP using a single self-intersecting polygon" << std::endl << std::endl; +// DoBenchmark2 +///////////////////////////////////////////////////////// +static bool DoBenchmark2() +{ + pipResults.clear(); + pipResults.resize(3); + for (size_t i = 0; i < 3; ++i) pipResults[i].resize(paths.size()); - benchmark::Initialize(&argc, argv); + benchmark::Initialize(0, nullptr); BENCHMARK(BM_PIP1)->Apply(CustomArguments); // current Clipper2 BENCHMARK(BM_PIP2)->Apply(CustomArguments); // modified Clipper2 BENCHMARK(BM_PIP3)->Apply(CustomArguments); // Hao et al. (2018) benchmark::RunSpecifiedBenchmarks(benchmark::CreateDefaultDisplayReporter()); - if (pip2 != pip1 || pip3 != pip1) + std::cout << std::endl; + // compare results to ensure they all agree :) + bool result = true; + const std::string bad_filename = "test_pip_"; + for (size_t i = 0; i < pipResults[0].size(); ++i) { - if (pip2 != pip1) - std::cout << "PIP2 result is wrong!!!"; - else - std::cout << "PIP3 result is wrong!!!"; - std::cout << paths[2] << std::endl << std::endl; - std::string _; - std::getline(std::cin, _); - return 1; + if ((pipResults[0][i] == pipResults[1][i]) && + (pipResults[0][i] == pipResults[2][i])) continue; + + if (pipResults[0][i] != pipResults[1][i]) + std::cout << "PIP2 returned the " << SetConsoleTextColor(red_bold) << "wrong " << + SetConsoleTextColor(reset) << "result:" << std::endl; + if (pipResults[0][i] != pipResults[2][i]) + std::cout << "PIP3 returned the " << SetConsoleTextColor(red_bold) << "wrong " << + SetConsoleTextColor(reset) << "result:" << std::endl; + + std::cout << "Problematic PIP path saved to - " << bad_filename << i << ".txt" << std::endl; + std::ofstream of(bad_filename); + of << paths[i] << std::endl; + of.close(); + result = false; } + return true; +} + +///////////////////////////////////////////////////////// +// Main Entry +///////////////////////////////////////////////////////// + +int main(int argc, char** argv) +{ + std::cout << SetConsoleTextColor(cyan_bold) << + "Simple error checks ..." << SetConsoleTextColor(reset) << + std::endl; + + ////////////////////////////////////////////////////////////// + std::cout << std::endl << SetConsoleTextColor(yellow_bold) << + "Tests for errors #1:" << SetConsoleTextColor(reset) << + std::endl << std::endl; + ////////////////////////////////////////////////////////////// + + // #1. path is constant but points of interest change + + Paths64 subject, subject_open, clip; + ClipType ct = ClipType::None; + FillRule fr = FillRule::EvenOdd; + int64_t area = 0, count = 0; + const std::string test_file = "../../../../../Tests/PolytreeHoleOwner2.txt"; + points_of_interest_outside = + MakePath({ 21887,10420, 21726,10825, 21662,10845, 21617,10890 }); + points_of_interest_inside = + MakePath({ 21887,10430, 21843,10520, 21810,10686, 21900,10461 }); + if (!FileExists(test_file)) return 1; + std::ifstream ifs(test_file); + if (!ifs || !ifs.good()) return 1; + LoadTestNum(ifs, 1, paths, subject_open, clip, area, count, ct, fr); + ifs.close(); + + for (int i = 0; i < 3; ++i) DoErrorTest1(i); + + ////////////////////////////////////////////////////////////// + std::cout << std::endl << SetConsoleTextColor(yellow_bold) << + "Tests for errors #2:" << SetConsoleTextColor(reset) << + std::endl << std::endl; + ////////////////////////////////////////////////////////////// + + // #2. point of interest is now constant (10,10) but path changes + + mp = Point64(10, 10); + paths.clear(); + pipResults.clear(); + pipResults.resize(1); + paths.push_back({}); // ie test an empty path + pipResults[0].push_back(PointInPolygonResult::IsOutside); + paths.push_back(MakePath({ 100,10, 200,10 })); + pipResults[0].push_back(PointInPolygonResult::IsOutside); + paths.push_back(MakePath({ 100,10, 200,10, 10,10, 20,20 })); + pipResults[0].push_back(PointInPolygonResult::IsOn); + paths.push_back(MakePath({ 10,10 })); + pipResults[0].push_back(PointInPolygonResult::IsOn); + paths.push_back(MakePath({ 100,10 })); + pipResults[0].push_back(PointInPolygonResult::IsOutside); + paths.push_back(MakePath({ 100,10, 110,20, 200,10, 10,10, 20,20 })); + pipResults[0].push_back(PointInPolygonResult::IsOn); + paths.push_back(MakePath({ 100,10, 110,20, 200,10, 20,20 })); + pipResults[0].push_back(PointInPolygonResult::IsOutside); + paths.push_back(MakePath({ 200,0, 0,0, 10,20, 200,0, 20,0 })); + pipResults[0].push_back(PointInPolygonResult::IsInside); + paths.push_back(MakePath({ 0,0, 20,20, 100,0 })); + pipResults[0].push_back(PointInPolygonResult::IsOn); + + for (int i = 0; i < 3; ++i) DoErrorTest2(i); + std::cout << std::endl; + + + ////////////////////////////////////////////////////////////// + std::cout << std::endl << SetConsoleTextColor(cyan_bold) << + "Benchmarking ..." << SetConsoleTextColor(reset) << std::endl; + ////////////////////////////////////////////////////////////// + + int width = 600000, height = 400000; + const int power10_lo = 4, power10_high = 8; + mp = Point64(width / 2, height / 2); + + std::cout << std::endl << SetConsoleTextColor(yellow_bold) << + "Benchmarks 1:" << SetConsoleTextColor(reset) << std::endl; + ////////////////////////////////////////////////////////////// + paths.clear(); + for (int i = power10_lo; i <= power10_high; ++i) + paths.push_back(Ellipse(mp, width / 2.0, height / 2.0, (unsigned)std::pow(10, i))); + std::cout << "A single elliptical path " << std::endl << + "Edge counts between 10^" << power10_lo << " and 10^" << + power10_high << std::endl << std::endl; + + DoBenchmark1(); + std::cout << std::endl; + + + std::cout << std::endl << SetConsoleTextColor(yellow_bold) << + "Benchmarks 2:" << SetConsoleTextColor(reset) << std::endl; + ////////////////////////////////////////////////////////////// + std::cout << "A random self-intersecting polygon (" << + width << " x " << height << ")" << std::endl << + "Edge counts between 10^" << power10_lo << " and 10^" << + power10_high << ". " << std::endl << + "Point (" << mp << ")" << std::endl << std::endl; + + paths.clear(); + for (int i = power10_lo; i <= power10_high; ++i) + paths.push_back(MakeRandomPoly(width, height, (unsigned)std::pow(10, i))); + DoBenchmark2(); + std::cout << std::endl; return 0; } diff --git a/CPP/BenchMark/StripDuplicateBenchmark.cpp b/CPP/BenchMark/StripDuplicateBenchmark.cpp index 92c07196..36d7b45a 100644 --- a/CPP/BenchMark/StripDuplicateBenchmark.cpp +++ b/CPP/BenchMark/StripDuplicateBenchmark.cpp @@ -55,9 +55,9 @@ static void BM_StripDuplicatesCopyVersion(benchmark::State &state) { for (auto _ : state) { state.PauseTiming(); - int width = state.range(0); - int height = state.range(1); - int count = state.range(2); + int width = (int)state.range(0); + int height = (int)state.range(1); + int count = (int)state.range(2); op1.push_back(MakeRandomPoly(width, height, count)); state.ResumeTiming(); StripDuplicatesCopyVersion(op1, true); @@ -69,9 +69,9 @@ static void BM_StripDuplicates(benchmark::State &state) { Paths64 op1; for (auto _ : state) { state.PauseTiming(); - int width = state.range(0); - int height = state.range(1); - int count = state.range(2); + int width = (int)state.range(0); + int height = (int)state.range(1); + int count = (int)state.range(2); op1.push_back(MakeRandomPoly(width, height, count)); state.ResumeTiming(); StripDuplicates(op1, true); diff --git a/CPP/Clipper2Lib/src/clipper.engine.cpp b/CPP/Clipper2Lib/src/clipper.engine.cpp index 9358b74b..027a5676 100644 --- a/CPP/Clipper2Lib/src/clipper.engine.cpp +++ b/CPP/Clipper2Lib/src/clipper.engine.cpp @@ -1,6 +1,6 @@ /******************************************************************************* * Author : Angus Johnson * -* Date : 22 November 2023 * +* Date : 1 December 2023 * * Website : http://www.angusj.com * * Copyright : Angus Johnson 2010-2023 * * Purpose : This is the main polygon clipping module * @@ -63,6 +63,7 @@ namespace Clipper2Lib { } }; + inline bool IsOdd(int val) { return (val & 1) ? true : false; @@ -2536,8 +2537,8 @@ namespace Clipper2Lib { if (IsHotEdge(horz) && IsJoined(*e)) Split(*e, e->top); - //if (IsHotEdge(horz) != IsHotEdge(*e)) - // DoError(undefined_error_i); + //if (IsHotEdge(horz) != IsHotEdge(*e)) + // DoError(undefined_error_i); if (IsHotEdge(horz)) { @@ -2758,8 +2759,10 @@ namespace Clipper2Lib { const Point64& pt, bool check_curr_x) { Active* prev = e.prev_in_ael; - if (IsOpen(e) || !IsHotEdge(e) || !prev || - IsOpen(*prev) || !IsHotEdge(*prev)) return; + if (!prev || + !IsHotEdge(e) || !IsHotEdge(*prev) || + IsHorizontal(e) || IsHorizontal(*prev) || + IsOpen(e) || IsOpen(*prev) ) return; if ((pt.y < e.top.y + 2 || pt.y < prev->top.y + 2) && ((e.bot.y > pt.y) || (prev->bot.y > pt.y))) return; // avoid trivial joins @@ -2784,8 +2787,10 @@ namespace Clipper2Lib { const Point64& pt, bool check_curr_x) { Active* next = e.next_in_ael; - if (IsOpen(e) || !IsHotEdge(e) || - !next || IsOpen(*next) || !IsHotEdge(*next)) return; + if (!next || + !IsHotEdge(e) || !IsHotEdge(*next) || + IsHorizontal(e) || IsHorizontal(*next) || + IsOpen(e) || IsOpen(*next)) return; if ((pt.y < e.top.y +2 || pt.y < next->top.y +2) && ((e.bot.y > pt.y) || (next->bot.y > pt.y))) return; // avoid trivial joins diff --git a/CPP/Utils/CommonUtils.h b/CPP/Utils/CommonUtils.h index c2e2d05c..68cee2ee 100644 --- a/CPP/Utils/CommonUtils.h +++ b/CPP/Utils/CommonUtils.h @@ -1,25 +1,36 @@ #include +#include #include "clipper2/clipper.h" #ifndef __COMMONUTILS_H__ #define __COMMONUTILS_H__ Clipper2Lib::Path64 MakeRandomPoly(int width, int height, unsigned vertCnt) { + std::random_device rd; + std::mt19937 gen(rd()); + std::uniform_int_distribution<> w(0, width); + std::uniform_int_distribution<> h(0, height); + using namespace Clipper2Lib; Path64 result; result.reserve(vertCnt); for (unsigned i = 0; i < vertCnt; ++i) - result.push_back(Point64(std::rand() % width, std::rand() % height)); + result.push_back(Point64(w(gen), h(gen))); return result; } Clipper2Lib::PathD MakeRandomPolyD(int width, int height, unsigned vertCnt) { + std::random_device rd; + std::mt19937 gen(rd()); + std::uniform_int_distribution<> w(0, width); + std::uniform_int_distribution<> h(0, height); + using namespace Clipper2Lib; PathD result; result.reserve(vertCnt); for (unsigned i = 0; i < vertCnt; ++i) - result.push_back(PointD(std::rand() % width, std::rand() % height)); + result.push_back(PointD(w(gen), h(gen))); return result; } diff --git a/CSharp/Clipper2Lib/Clipper.Engine.cs b/CSharp/Clipper2Lib/Clipper.Engine.cs index eaee6a40..bcc8ca57 100644 --- a/CSharp/Clipper2Lib/Clipper.Engine.cs +++ b/CSharp/Clipper2Lib/Clipper.Engine.cs @@ -1,6 +1,6 @@ /******************************************************************************* * Author : Angus Johnson * -* Date : 22 November 2023 * +* Date : 1 December 2023 * * Website : http://www.angusj.com * * Copyright : Angus Johnson 2010-2023 * * Purpose : This is the main polygon clipping module * @@ -2393,8 +2393,10 @@ private void CheckJoinLeft(Active e, Point64 pt, bool checkCurrX = false) { Active? prev = e.prevInAEL; - if (prev == null || IsOpen(e) || IsOpen(prev) || - !IsHotEdge(e) || !IsHotEdge(prev)) return; + if (prev == null || + !IsHotEdge(e) || !IsHotEdge(prev) || + IsHorizontal(e) || IsHorizontal(prev) || + IsOpen(e) || IsOpen(prev)) return; if ((pt.Y < e.top.Y + 2 || pt.Y < prev.top.Y + 2) && // avoid trivial joins ((e.bot.Y > pt.Y) || (prev.bot.Y > pt.Y))) return; // (#490) @@ -2420,8 +2422,10 @@ private void CheckJoinRight(Active e, Point64 pt, bool checkCurrX = false) { Active? next = e.nextInAEL; - if (IsOpen(e) || !IsHotEdge(e) || IsJoined(e) || - next == null || IsOpen(next) || !IsHotEdge(next)) return; + if (next == null || + !IsHotEdge(e) || !IsHotEdge(next) || + IsHorizontal(e) || IsHorizontal(next) || + IsOpen(e) || IsOpen(next)) return; if ((pt.Y < e.top.Y + 2 || pt.Y < next.top.Y + 2) && // avoid trivial joins ((e.bot.Y > pt.Y) || (next.bot.Y > pt.Y))) return; // (#490) diff --git a/Delphi/Clipper2Lib/Clipper.Engine.pas b/Delphi/Clipper2Lib/Clipper.Engine.pas index da38f00a..9339134f 100644 --- a/Delphi/Clipper2Lib/Clipper.Engine.pas +++ b/Delphi/Clipper2Lib/Clipper.Engine.pas @@ -2,7 +2,7 @@ (******************************************************************************* * Author : Angus Johnson * -* Date : 22 November 2023 * +* Date : 1 December 2023 * * Website : http://www.angusj.com * * Copyright : Angus Johnson 2010-2023 * * Purpose : This is the main polygon clipping module * @@ -2374,8 +2374,10 @@ procedure TClipperBase.CheckJoinLeft(e: PActive; prev: PActive; begin prev := e.prevInAEL; - if IsOpen(e) or not IsHotEdge(e) or not Assigned(prev) or - IsOpen(prev) or not IsHotEdge(prev) then Exit; + if not Assigned(prev) or + not IsHotEdge(e) or not IsHotEdge(prev) or + IsHorizontal(e) or IsHorizontal(prev) or + IsOpen(e) or IsOpen(prev) then Exit; if ((pt.Y < e.top.Y +2) or (pt.Y < prev.top.Y +2)) and ((e.bot.Y > pt.Y) or (prev.bot.Y > pt.Y)) then Exit; // (#490) @@ -2403,8 +2405,10 @@ procedure TClipperBase.CheckJoinRight(e: PActive; next: PActive; begin next := e.nextInAEL; - if IsOpen(e) or not IsHotEdge(e) or not Assigned(next) or - not IsHotEdge(next) or IsOpen(next) then Exit; + if not Assigned(next) or + not IsHotEdge(e) or not IsHotEdge(next) or + IsHorizontal(e) or IsHorizontal(next) or + IsOpen(e) or IsOpen(next) then Exit; if ((pt.Y < e.top.Y +2) or (pt.Y < next.top.Y +2)) and ((e.bot.Y > pt.Y) or (next.bot.Y > pt.Y)) then Exit; // (#490) From ab6a16eb015618b3ab19a5f6f99ce143d378064d Mon Sep 17 00:00:00 2001 From: David Cole Date: Fri, 1 Dec 2023 08:20:22 -0500 Subject: [PATCH 03/64] Allow using external gtest with GTest:: qualified names (#737) --- CPP/CMakeLists.txt | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/CPP/CMakeLists.txt b/CPP/CMakeLists.txt index 98d8ec49..5fab727d 100644 --- a/CPP/CMakeLists.txt +++ b/CPP/CMakeLists.txt @@ -201,6 +201,13 @@ else() add_subdirectory("${PROJECT_SOURCE_DIR}/Tests/googletest/") set_target_properties(gtest gtest_main PROPERTIES FOLDER GTest) endif() + + if(TARGET gtest AND TARGET gtest_main) + set(gtest_libs gtest gtest_main) + elseif(TARGET GTest::gtest AND TARGET GTest::gtest_main) + set(gtest_libs GTest::gtest GTest::gtest_main) + endif() + set(ClipperTests_SRC Tests/TestExportHeaders.cpp Tests/TestLines.cpp @@ -223,7 +230,7 @@ endif() if (NOT (CLIPPER2_USINGZ STREQUAL "ONLY")) list(APPEND CLIPPER2_TESTS "ClipperTests") add_executable(ClipperTests ${ClipperTests_SRC}) - target_link_libraries(ClipperTests gtest gtest_main Clipper2 Clipper2utils) + target_link_libraries(ClipperTests ${gtest_libs} Clipper2 Clipper2utils) gtest_discover_tests(ClipperTests # set a working directory to your project root so that you can find test data via paths relative to the project root @@ -235,7 +242,7 @@ endif() if (NOT (CLIPPER2_USINGZ STREQUAL "OFF")) list(APPEND CLIPPER2_TESTS "ClipperTestsZ") add_executable(ClipperTestsZ ${ClipperTests_SRC}) - target_link_libraries(ClipperTestsZ gtest gtest_main Clipper2Z Clipper2Zutils) + target_link_libraries(ClipperTestsZ ${gtest_libs} Clipper2Z Clipper2Zutils) gtest_discover_tests(ClipperTestsZ # set a working directory so your project root so that you can find test data via paths relative to the project root From 147b9b505fb14b47fee2888efe4b48dbd6dd2a53 Mon Sep 17 00:00:00 2001 From: angusj Date: Fri, 1 Dec 2023 23:24:02 +1000 Subject: [PATCH 04/64] More tweaks to PointInPolygon benchmarking. --- CPP/BenchMark/CMakeLists.txt | 34 +++++++ CPP/BenchMark/PointInPolygonBenchmark.cpp | 106 +++++++++------------- 2 files changed, 76 insertions(+), 64 deletions(-) diff --git a/CPP/BenchMark/CMakeLists.txt b/CPP/BenchMark/CMakeLists.txt index 66cb9368..ddae2812 100644 --- a/CPP/BenchMark/CMakeLists.txt +++ b/CPP/BenchMark/CMakeLists.txt @@ -28,12 +28,44 @@ set(benchmark_srcs # more to add ) +set(CLIPPER2_INC + ${CLIPPER2_INC_FOLDER}/clipper.h + ${CLIPPER2_INC_FOLDER}/clipper.version.h + ${CLIPPER2_INC_FOLDER}/clipper.core.h +) + +add_library(Clipper2_bm INTERFACE) +target_include_directories(Clipper2_bm INTERFACE CLIPPER2_INC) + +set(CLIPPER2_UTILS_INC + ../Utils/clipper.svg.h + ../Utils/ClipFileLoad.h + ../Utils/ClipFileSave.h + ../Utils/Timer.h + ../Utils/Colors.h + ../Utils/CommonUtils.h +) +set(CLIPPER2_UTILS_SRC + ../Utils/clipper.svg.cpp + ../Utils/ClipFileLoad.cpp + ../Utils/ClipFileSave.cpp +) +set(CLIPPER2_UTILS "") + list(APPEND CLIPPER2_UTILS Clipper2utils_bm) + add_library(Clipper2utils_bm STATIC ${CLIPPER2_UTILS_INC} ${CLIPPER2_UTILS_SRC}) + target_include_directories(Clipper2utils_bm + PUBLIC ../Clipper2Lib/include + PUBLIC ../Utils + ) + target_link_libraries(Clipper2utils_bm PUBLIC Clipper2_bm) + # add each benchmark from the benchmark_srcs foreach(benchmark ${benchmark_srcs}) get_filename_component(benchmark_target ${benchmark} NAME_WE) message(STATUS "${PROJECT_NAME} add benchmark ${benchmark_target}") add_executable(${benchmark_target} ${benchmark}) + target_include_directories(${benchmark_target} PUBLIC ../Clipper2Lib/include PUBLIC ../Utils @@ -41,5 +73,7 @@ foreach(benchmark ${benchmark_srcs}) target_link_libraries(${benchmark_target} benchmark::benchmark + Clipper2_bm + Clipper2utils_bm ) endforeach() diff --git a/CPP/BenchMark/PointInPolygonBenchmark.cpp b/CPP/BenchMark/PointInPolygonBenchmark.cpp index 446a70f5..fed442ab 100644 --- a/CPP/BenchMark/PointInPolygonBenchmark.cpp +++ b/CPP/BenchMark/PointInPolygonBenchmark.cpp @@ -386,66 +386,6 @@ static void DoErrorTest2(int index) std::cout << " No errors found." << std::endl; } -///////////////////////////////////////////////////////// -// Benchmark functions -///////////////////////////////////////////////////////// - -// DoBenchmark1 -///////////////////////////////////////////////////////// -static void DoBenchmark1() -{ - // compare 3 PIP algorithms - pipResults.clear(); - pipResults.resize(3); - for (size_t i = 0; i < 3; ++i) pipResults[i].resize(paths.size()); - benchmark::Initialize(0, nullptr); - BENCHMARK(BM_PIP1)->Apply(CustomArguments); // current Clipper2 - BENCHMARK(BM_PIP2)->Apply(CustomArguments); // modified Clipper2 - BENCHMARK(BM_PIP3)->Apply(CustomArguments); // Hao et al. (2018) - benchmark::RunSpecifiedBenchmarks(benchmark::CreateDefaultDisplayReporter()); - benchmark::ClearRegisteredBenchmarks(); - benchmark::Shutdown(); -} - -// DoBenchmark2 -///////////////////////////////////////////////////////// -static bool DoBenchmark2() -{ - pipResults.clear(); - pipResults.resize(3); - for (size_t i = 0; i < 3; ++i) pipResults[i].resize(paths.size()); - - benchmark::Initialize(0, nullptr); - BENCHMARK(BM_PIP1)->Apply(CustomArguments); // current Clipper2 - BENCHMARK(BM_PIP2)->Apply(CustomArguments); // modified Clipper2 - BENCHMARK(BM_PIP3)->Apply(CustomArguments); // Hao et al. (2018) - benchmark::RunSpecifiedBenchmarks(benchmark::CreateDefaultDisplayReporter()); - - std::cout << std::endl; - // compare results to ensure they all agree :) - bool result = true; - const std::string bad_filename = "test_pip_"; - for (size_t i = 0; i < pipResults[0].size(); ++i) - { - if ((pipResults[0][i] == pipResults[1][i]) && - (pipResults[0][i] == pipResults[2][i])) continue; - - if (pipResults[0][i] != pipResults[1][i]) - std::cout << "PIP2 returned the " << SetConsoleTextColor(red_bold) << "wrong " << - SetConsoleTextColor(reset) << "result:" << std::endl; - if (pipResults[0][i] != pipResults[2][i]) - std::cout << "PIP3 returned the " << SetConsoleTextColor(red_bold) << "wrong " << - SetConsoleTextColor(reset) << "result:" << std::endl; - - std::cout << "Problematic PIP path saved to - " << bad_filename << i << ".txt" << std::endl; - std::ofstream of(bad_filename); - of << paths[i] << std::endl; - of.close(); - result = false; - } - return true; -} - ///////////////////////////////////////////////////////// // Main Entry ///////////////////////////////////////////////////////// @@ -524,7 +464,8 @@ int main(int argc, char** argv) int width = 600000, height = 400000; const int power10_lo = 4, power10_high = 8; mp = Point64(width / 2, height / 2); - + + std::cout << std::endl << SetConsoleTextColor(yellow_bold) << "Benchmarks 1:" << SetConsoleTextColor(reset) << std::endl; ////////////////////////////////////////////////////////////// @@ -535,7 +476,16 @@ int main(int argc, char** argv) "Edge counts between 10^" << power10_lo << " and 10^" << power10_high << std::endl << std::endl; - DoBenchmark1(); + pipResults.clear(); + pipResults.resize(3); + for (size_t i = 0; i < 3; ++i) pipResults[i].resize(paths.size()); + + benchmark::Initialize(0, nullptr); + BENCHMARK(BM_PIP1)->Apply(CustomArguments); // current Clipper2 + BENCHMARK(BM_PIP2)->Apply(CustomArguments); // modified Clipper2 + BENCHMARK(BM_PIP3)->Apply(CustomArguments); // Hao et al. (2018) + benchmark::RunSpecifiedBenchmarks(benchmark::CreateDefaultDisplayReporter()); + benchmark::ClearRegisteredBenchmarks(); std::cout << std::endl; @@ -551,8 +501,36 @@ int main(int argc, char** argv) paths.clear(); for (int i = power10_lo; i <= power10_high; ++i) paths.push_back(MakeRandomPoly(width, height, (unsigned)std::pow(10, i))); - DoBenchmark2(); + + pipResults.clear(); + pipResults.resize(3); + for (size_t i = 0; i < 3; ++i) pipResults[i].resize(paths.size()); + benchmark::Initialize(0, nullptr); + BENCHMARK(BM_PIP1)->Apply(CustomArguments); // current Clipper2 + BENCHMARK(BM_PIP2)->Apply(CustomArguments); // modified Clipper2 + BENCHMARK(BM_PIP3)->Apply(CustomArguments); // Hao et al. (2018) + benchmark::RunSpecifiedBenchmarks(benchmark::CreateDefaultDisplayReporter()); + std::cout << std::endl; + // compare results to ensure they all agree :) + const std::string bad_filename = "test_pip_"; + for (size_t i = 0; i < pipResults[0].size(); ++i) + { + if ((pipResults[0][i] == pipResults[1][i]) && + (pipResults[0][i] == pipResults[2][i])) continue; + + if (pipResults[0][i] != pipResults[1][i]) + std::cout << "PIP2 returned the " << SetConsoleTextColor(red_bold) << "wrong " << + SetConsoleTextColor(reset) << "result:" << std::endl; + if (pipResults[0][i] != pipResults[2][i]) + std::cout << "PIP3 returned the " << SetConsoleTextColor(red_bold) << "wrong " << + SetConsoleTextColor(reset) << "result:" << std::endl; + + std::cout << "Problematic PIP path saved to - " << bad_filename << i << ".txt" << std::endl; + std::ofstream of(bad_filename); + of << paths[i] << std::endl; + of.close(); + break; + } - return 0; } From 6cf713e7a72d52badadef4185e1596984fb64018 Mon Sep 17 00:00:00 2001 From: angusj Date: Fri, 1 Dec 2023 23:33:50 +1000 Subject: [PATCH 05/64] Fixed minor typecasting issues (#727) --- CPP/Clipper2Lib/include/clipper2/clipper.export.h | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/CPP/Clipper2Lib/include/clipper2/clipper.export.h b/CPP/Clipper2Lib/include/clipper2/clipper.export.h index d7286132..f89ec7c9 100644 --- a/CPP/Clipper2Lib/include/clipper2/clipper.export.h +++ b/CPP/Clipper2Lib/include/clipper2/clipper.export.h @@ -1,6 +1,6 @@ /******************************************************************************* * Author : Angus Johnson * -* Date : 26 November 2023 * +* Date : 1 December 2023 * * Website : http://www.angusj.com * * Copyright : Angus Johnson 2010-2023 * * Purpose : This module exports the Clipper2 Library (ie DLL/so) * @@ -277,11 +277,11 @@ static Paths ConvertCPaths(T* paths) Paths result; if (!paths) return result; T* v = paths; ++v; - size_t cnt = *v++; + size_t cnt = static_cast(*v++); result.reserve(cnt); for (size_t i = 0; i < cnt; ++i) { - size_t cnt2 = *v; + size_t cnt2 = static_cast(*v); v += 2; Path path; path.reserve(cnt2); @@ -302,15 +302,15 @@ static Paths64 ConvertCPathsDToPaths64(const CPathsD paths, double scale) if (!paths) return result; double* v = paths; ++v; // skip the first value (0) - int64_t cnt = (int64_t)*v++; + size_t cnt = static_cast(*v++); result.reserve(cnt); - for (int i = 0; i < cnt; ++i) + for (size_t i = 0; i < cnt; ++i) { - int64_t cnt2 = (int64_t)*v; + size_t cnt2 = static_cast(*v); v += 2; Path64 path; path.reserve(cnt2); - for (int j = 0; j < cnt2; ++j) + for (size_t j = 0; j < cnt2; ++j) { double x = *v++ * scale; double y = *v++ * scale; From ebab8683b462fc76f8fbe010ce4944cf6c7c59e2 Mon Sep 17 00:00:00 2001 From: angusj Date: Sat, 2 Dec 2023 08:13:02 +1000 Subject: [PATCH 06/64] Code formatting only (removed trailing spaces) --- CPP/BenchMark/CMakeLists.txt | 8 +- CPP/BenchMark/PointInPolygonBenchmark.cpp | 52 +++++------ CPP/BenchMark/README.md | 4 +- .../include/clipper2/clipper.core.h | 42 ++++----- .../include/clipper2/clipper.engine.h | 26 +++--- .../include/clipper2/clipper.export.h | 48 +++++----- CPP/Clipper2Lib/include/clipper2/clipper.h | 46 +++++----- .../include/clipper2/clipper.minkowski.h | 2 +- .../include/clipper2/clipper.offset.h | 2 +- CPP/Clipper2Lib/src/clipper.engine.cpp | 90 +++++++++---------- CPP/Clipper2Lib/src/clipper.offset.cpp | 32 +++---- CPP/Clipper2Lib/src/clipper.rectclip.cpp | 40 ++++----- CPP/Tests/TestExportHeaders.cpp | 72 ++++----------- CPP/Tests/TestLines.cpp | 14 +-- CPP/Tests/TestOffsetOrientation.cpp | 13 +-- CPP/Tests/TestOffsets.cpp | 79 ++-------------- CPP/Tests/TestOrientation.cpp | 4 - CPP/Tests/TestPolygons.cpp | 39 ++------ CPP/Tests/TestPolytreeHoles.cpp | 60 +------------ CPP/Tests/TestPolytreeIntersection.cpp | 10 +-- CPP/Tests/TestPolytreeUnion.cpp | 10 +-- CPP/Tests/TestRandomPaths.cpp | 16 ---- CPP/Tests/TestRectClip.cpp | 16 ---- CPP/Tests/TestSimplifyPath.cpp | 5 +- CPP/Tests/TestTrimCollinear.cpp | 14 +-- CPP/Tests/TestWindows.cpp | 3 - 26 files changed, 246 insertions(+), 501 deletions(-) diff --git a/CPP/BenchMark/CMakeLists.txt b/CPP/BenchMark/CMakeLists.txt index ddae2812..e305c5d7 100644 --- a/CPP/BenchMark/CMakeLists.txt +++ b/CPP/BenchMark/CMakeLists.txt @@ -15,7 +15,7 @@ message("start fetching the googlebenchmark") FetchContent_Declare(googlebenchmark GIT_REPOSITORY https://github.com/google/benchmark.git GIT_TAG v1.7.1 -) +) FetchContent_MakeAvailable( googlebenchmark) @@ -50,12 +50,12 @@ set(CLIPPER2_UTILS_SRC ../Utils/ClipFileLoad.cpp ../Utils/ClipFileSave.cpp ) -set(CLIPPER2_UTILS "") +set(CLIPPER2_UTILS "") list(APPEND CLIPPER2_UTILS Clipper2utils_bm) add_library(Clipper2utils_bm STATIC ${CLIPPER2_UTILS_INC} ${CLIPPER2_UTILS_SRC}) target_include_directories(Clipper2utils_bm PUBLIC ../Clipper2Lib/include - PUBLIC ../Utils + PUBLIC ../Utils ) target_link_libraries(Clipper2utils_bm PUBLIC Clipper2_bm) @@ -68,7 +68,7 @@ foreach(benchmark ${benchmark_srcs}) target_include_directories(${benchmark_target} PUBLIC ../Clipper2Lib/include - PUBLIC ../Utils + PUBLIC ../Utils ) target_link_libraries(${benchmark_target} diff --git a/CPP/BenchMark/PointInPolygonBenchmark.cpp b/CPP/BenchMark/PointInPolygonBenchmark.cpp index fed442ab..394ca098 100644 --- a/CPP/BenchMark/PointInPolygonBenchmark.cpp +++ b/CPP/BenchMark/PointInPolygonBenchmark.cpp @@ -8,7 +8,7 @@ using namespace Clipper2Lib; -enum ConsoleTextColor { +enum ConsoleTextColor { reset = 0, //normal text colors ... red = 31, green = 32, yellow = 33, blue = 34, magenta = 35, cyan = 36, white = 37, @@ -17,9 +17,9 @@ enum ConsoleTextColor { magenta_bold = 95, cyan_bold = 96, white_bold = 97 }; -///////////////////////////////////////////////////////////////////////////// -// SetConsoleTextColor: a simple class to adjust Windows' Console Text Colors -///////////////////////////////////////////////////////////////////////////// +////////////////////////////////////////////////////////////////////////////////////// +// SetConsoleTextColor: a simple class to adjust Console Text Colors (Windows & Linux) +////////////////////////////////////////////////////////////////////////////////////// struct SetConsoleTextColor { @@ -33,7 +33,7 @@ struct SetConsoleTextColor return out << "\x1B[" << scc._color << "m"; } }; -///////////////////////////////////////////////////////////////////////////// +////////////////////////////////////////////////////////////////////////////////////// typedef std::function PipFunction; @@ -124,8 +124,8 @@ inline PointInPolygonResult PIP1(const Point& pt, const Path& polygon) ///////////////////////////////////////////////////////// -// PIP2: This is a not fully tested modification of the -// current Clipper2 PointInPolygon code. It's a little +// PIP2: This is a not fully tested modification of the +// current Clipper2 PointInPolygon code. It's a little // simpler and also marginally faster. ///////////////////////////////////////////////////////// template @@ -142,13 +142,13 @@ inline PointInPolygonResult PIP2(const Point& pt, const Path& polygon) if (prev->y == pt.y) { if (pt == *prev) return PointInPolygonResult::IsOn; - if ((curr->y == pt.y) && ((curr->x == pt.x) || + if ((curr->y == pt.y) && ((curr->x == pt.x) || ((pt.x > prev->x) == (pt.x < curr->x)))) return PointInPolygonResult::IsOn; Path::const_reverse_iterator pr = polygon.crbegin() +1; while (pr != polygon.crend() && pr->y == pt.y) ++pr; is_above = pr == polygon.crend() || pr->y < pt.y; - } + } else is_above = prev->y < pt.y; int val = 0; @@ -156,15 +156,15 @@ inline PointInPolygonResult PIP2(const Point& pt, const Path& polygon) { if (is_above) { - while (curr != cend && curr->y < pt.y) ++curr; + while (curr != cend && curr->y < pt.y) ++curr; if (curr == cend) break; } else { - while (curr != cend && curr->y > pt.y) ++curr; + while (curr != cend && curr->y > pt.y) ++curr; if (curr == cend) break; } - + prev = (curr == first) ? last : curr - 1; if (curr->y == pt.y) { @@ -191,8 +191,8 @@ inline PointInPolygonResult PIP2(const Point& pt, const Path& polygon) ++curr; } - return (val % 2) ? - PointInPolygonResult::IsInside : + return (val % 2) ? + PointInPolygonResult::IsInside : PointInPolygonResult::IsOutside; } @@ -223,14 +223,14 @@ static PointInPolygonResult PIP3(const Point &pt, const Path &path) x1 = itPrev->x - pt.x; x2 = itCurr->x - pt.x; - if ((y1 <= 0) && (y2 > 0)) + if ((y1 <= 0) && (y2 > 0)) { //double f = double(x1) * y2 - double(x2) * y1; // avoids int overflow int64_t f = x1 * y2 - x2 * y1; if (f > 0) ++k; else if (f == 0) return PointInPolygonResult::IsOn; } - else if ((y1 > 0) && (y2 <= 0)) + else if ((y1 > 0) && (y2 <= 0)) { int64_t f = x1 * y2 - x2 * y1; if (f < 0) ++k; @@ -246,7 +246,7 @@ static PointInPolygonResult PIP3(const Point &pt, const Path &path) return PointInPolygonResult::IsOn; itPrev = itCurr; } - if (k % 2) return PointInPolygonResult::IsInside; + if (k % 2) return PointInPolygonResult::IsInside; return PointInPolygonResult::IsOutside; } @@ -317,7 +317,7 @@ inline PipFunction GetPIPFunc(int index) // DoErrorTest1 ///////////////////////////////////////////////////////// -static void DoErrorTest1_internal(const Path64& pts_of_int, const Paths64& paths, +static void DoErrorTest1_internal(const Path64& pts_of_int, const Paths64& paths, PipFunction pip_func, PointInPolygonResult expected) { Path64 error_points; @@ -353,12 +353,12 @@ static void DoErrorTest1(int index) std::cout << SetConsoleTextColor(green_bold) << "Testing PIP" << index +1 << "/outside:" << SetConsoleTextColor(reset); - DoErrorTest1_internal(points_of_interest_outside, paths, + DoErrorTest1_internal(points_of_interest_outside, paths, pip_func, PointInPolygonResult::IsOutside); std::cout << SetConsoleTextColor(green_bold) << "Testing PIP" << index +1 << "/inside :" << SetConsoleTextColor(reset); - DoErrorTest1_internal(points_of_interest_inside, paths, + DoErrorTest1_internal(points_of_interest_inside, paths, pip_func, PointInPolygonResult::IsInside); } @@ -379,7 +379,7 @@ static void DoErrorTest2(int index) std::cout << SetConsoleTextColor(red_bold) << " Error in "; for (size_t i = 0; i < high_error; ++i) std::cout << errors[i] << " and "; - std::cout << errors[high_error] << "." << + std::cout << errors[high_error] << "." << SetConsoleTextColor(reset) << std::endl; } else @@ -390,9 +390,9 @@ static void DoErrorTest2(int index) // Main Entry ///////////////////////////////////////////////////////// -int main(int argc, char** argv) +int main(int argc, char** argv) { - std::cout << SetConsoleTextColor(cyan_bold) << + std::cout << SetConsoleTextColor(cyan_bold) << "Simple error checks ..." << SetConsoleTextColor(reset) << std::endl; @@ -411,7 +411,7 @@ int main(int argc, char** argv) const std::string test_file = "../../../../../Tests/PolytreeHoleOwner2.txt"; points_of_interest_outside = MakePath({ 21887,10420, 21726,10825, 21662,10845, 21617,10890 }); - points_of_interest_inside = + points_of_interest_inside = MakePath({ 21887,10430, 21843,10520, 21810,10686, 21900,10461 }); if (!FileExists(test_file)) return 1; std::ifstream ifs(test_file); @@ -473,7 +473,7 @@ int main(int argc, char** argv) for (int i = power10_lo; i <= power10_high; ++i) paths.push_back(Ellipse(mp, width / 2.0, height / 2.0, (unsigned)std::pow(10, i))); std::cout << "A single elliptical path " << std::endl << - "Edge counts between 10^" << power10_lo << " and 10^" << + "Edge counts between 10^" << power10_lo << " and 10^" << power10_high << std::endl << std::endl; pipResults.clear(); @@ -501,7 +501,7 @@ int main(int argc, char** argv) paths.clear(); for (int i = power10_lo; i <= power10_high; ++i) paths.push_back(MakeRandomPoly(width, height, (unsigned)std::pow(10, i))); - + pipResults.clear(); pipResults.resize(3); for (size_t i = 0; i < 3; ++i) pipResults[i].resize(paths.size()); diff --git a/CPP/BenchMark/README.md b/CPP/BenchMark/README.md index 19dc75b4..f04d0592 100644 --- a/CPP/BenchMark/README.md +++ b/CPP/BenchMark/README.md @@ -1,6 +1,6 @@ -# google benchmark +# google benchmark this can be enabled by setting the option in the `CPP/CMakeLists.txt` ```cmake -option(USE_EXTERNAL_GBENCHMARK "Use the googlebenchmark" ON) +option(USE_EXTERNAL_GBENCHMARK "Use the googlebenchmark" ON) ``` \ No newline at end of file diff --git a/CPP/Clipper2Lib/include/clipper2/clipper.core.h b/CPP/Clipper2Lib/include/clipper2/clipper.core.h index b3dddeea..adbf4a04 100644 --- a/CPP/Clipper2Lib/include/clipper2/clipper.core.h +++ b/CPP/Clipper2Lib/include/clipper2/clipper.core.h @@ -49,9 +49,9 @@ namespace Clipper2Lib // error codes (2^n) 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 undefined_error_i = 32; // fatal + const int scale_error_i = 2; // non-fatal + const int non_pair_error_i = 4; // non-fatal + const int undefined_error_i = 32; // fatal const int range_error_i = 64; #ifndef PI @@ -327,7 +327,7 @@ namespace Clipper2Lib }; bool operator==(const Rect& other) const { - return left == other.left && right == other.right && + return left == other.left && right == other.right && top == other.top && bottom == other.bottom; } @@ -361,8 +361,8 @@ namespace Clipper2Lib } static const Rect64 InvalidRect64 = Rect64( - (std::numeric_limits::max)(), - (std::numeric_limits::max)(), + (std::numeric_limits::max)(), + (std::numeric_limits::max)(), (std::numeric_limits::lowest)(), (std::numeric_limits::lowest)()); static const RectD InvalidRectD = RectD( @@ -429,7 +429,7 @@ namespace Clipper2Lib template - inline Path ScalePath(const Path& path, + inline Path ScalePath(const Path& path, double scale_x, double scale_y, int& error_code) { Path result; @@ -445,11 +445,11 @@ namespace Clipper2Lib result.reserve(path.size()); #ifdef USINGZ std::transform(path.begin(), path.end(), back_inserter(result), - [scale_x, scale_y](const auto& pt) + [scale_x, scale_y](const auto& pt) { return Point(pt.x * scale_x, pt.y * scale_y, pt.z); }); #else std::transform(path.begin(), path.end(), back_inserter(result), - [scale_x, scale_y](const auto& pt) + [scale_x, scale_y](const auto& pt) { return Point(pt.x * scale_x, pt.y * scale_y); }); #endif return result; @@ -463,7 +463,7 @@ namespace Clipper2Lib } template - inline Paths ScalePaths(const Paths& paths, + inline Paths ScalePaths(const Paths& paths, double scale_x, double scale_y, int& error_code) { Paths result; @@ -476,7 +476,7 @@ namespace Clipper2Lib (r.right * scale_x) > max_coord || (r.top * scale_y) < min_coord || (r.bottom * scale_y) > max_coord) - { + { error_code |= range_error_i; DoError(range_error_i); return result; // empty path @@ -491,7 +491,7 @@ namespace Clipper2Lib } template - inline Paths ScalePaths(const Paths& paths, + inline Paths ScalePaths(const Paths& paths, double scale, int& error_code) { return ScalePaths(paths, scale, scale, error_code); @@ -679,16 +679,16 @@ namespace Clipper2Lib template inline bool IsPositive(const Path& poly) { - // A curve has positive orientation [and area] if a region 'R' + // A curve has positive orientation [and area] if a region 'R' // is on the left when traveling around the outside of 'R'. //https://mathworld.wolfram.com/CurveOrientation.html //nb: This statement is premised on using Cartesian coordinates return Area(poly) >= 0; } - + inline bool GetIntersectPoint(const Point64& ln1a, const Point64& ln1b, const Point64& ln2a, const Point64& ln2b, Point64& ip) - { + { // https://en.wikipedia.org/wiki/Line%E2%80%93line_intersection double dx1 = static_cast(ln1b.x - ln1a.x); double dy1 = static_cast(ln1b.y - ln1a.y); @@ -768,7 +768,7 @@ namespace Clipper2Lib return PointInPolygonResult::IsOutside; bool is_above = first->y < pt.y, starting_above = is_above; - curr = first +1; + curr = first +1; while (true) { if (curr == cend) @@ -777,7 +777,7 @@ namespace Clipper2Lib cend = first; curr = cbegin; } - + if (is_above) { while (curr != cend && curr->y < pt.y) ++curr; @@ -789,14 +789,14 @@ namespace Clipper2Lib if (curr == cend) continue; } - if (curr == cbegin) + if (curr == cbegin) prev = polygon.cend() - 1; //nb: NOT cend (since might equal first) - else + else prev = curr - 1; if (curr->y == pt.y) { - if (curr->x == pt.x || + if (curr->x == pt.x || (curr->y == prev->y && ((pt.x < prev->x) != (pt.x < curr->x)))) return PointInPolygonResult::IsOn; @@ -820,7 +820,7 @@ namespace Clipper2Lib is_above = !is_above; ++curr; } - + if (is_above != starting_above) { cend = polygon.cend(); diff --git a/CPP/Clipper2Lib/include/clipper2/clipper.engine.h b/CPP/Clipper2Lib/include/clipper2/clipper.engine.h index 13c7f069..f7fe0182 100644 --- a/CPP/Clipper2Lib/include/clipper2/clipper.engine.h +++ b/CPP/Clipper2Lib/include/clipper2/clipper.engine.h @@ -41,7 +41,7 @@ namespace Clipper2Lib { None = 0, OpenStart = 1, OpenEnd = 2, LocalMax = 4, LocalMin = 8 }; - constexpr enum VertexFlags operator &(enum VertexFlags a, enum VertexFlags b) + constexpr enum VertexFlags operator &(enum VertexFlags a, enum VertexFlags b) { return (enum VertexFlags)(uint32_t(a) & uint32_t(b)); } @@ -95,7 +95,7 @@ namespace Clipper2Lib { Path64 path; bool is_open = false; - ~OutRec() { + ~OutRec() { if (splits) delete splits; // nb: don't delete the split pointers // as these are owned by ClipperBase's outrec_list_ @@ -240,7 +240,7 @@ namespace Clipper2Lib { void SwapPositionsInAEL(Active& edge1, Active& edge2); OutRec* NewOutRec(); OutPt* AddOutPt(const Active &e, const Point64& pt); - OutPt* AddLocalMinPoly(Active &e1, Active &e2, + OutPt* AddLocalMinPoly(Active &e1, Active &e2, const Point64& pt, bool is_new = false); OutPt* AddLocalMaxPoly(Active &e1, Active &e2, const Point64& pt); void DoHorizontal(Active &horz); @@ -257,7 +257,7 @@ namespace Clipper2Lib { void ProcessHorzJoins(); void Split(Active& e, const Point64& pt); - inline void CheckJoinLeft(Active& e, + inline void CheckJoinLeft(Active& e, const Point64& pt, bool check_curr_x = false); inline void CheckJoinRight(Active& e, const Point64& pt, bool check_curr_x = false); @@ -326,7 +326,7 @@ namespace Clipper2Lib { const PolyPath* Parent() const { return parent_; } - bool IsHole() const + bool IsHole() const { unsigned lvl = Level(); //Even levels except level 0 @@ -349,9 +349,9 @@ namespace Clipper2Lib { } PolyPath64* operator [] (size_t index) const - { + { return childs_[index].get(); //std::unique_ptr - } + } PolyPath64* Child(size_t index) const { @@ -406,7 +406,7 @@ namespace Clipper2Lib { } PolyPathD* operator [] (size_t index) const - { + { return childs_[index].get(); } @@ -488,7 +488,7 @@ namespace Clipper2Lib { return Execute(clip_type, fill_rule, closed_paths, dummy); } - bool Execute(ClipType clip_type, FillRule fill_rule, + bool Execute(ClipType clip_type, FillRule fill_rule, Paths64& closed_paths, Paths64& open_paths) { closed_paths.clear(); @@ -560,12 +560,12 @@ namespace Clipper2Lib { void CheckCallback() { if(zCallbackD_) - // if the user defined float point callback has been assigned + // if the user defined float point callback has been assigned // then assign the proxy callback function - ClipperBase::zCallback_ = + ClipperBase::zCallback_ = std::bind(&ClipperD::ZCB, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3, - std::placeholders::_4, std::placeholders::_5); + std::placeholders::_4, std::placeholders::_5); else ClipperBase::zCallback_ = nullptr; } @@ -632,6 +632,6 @@ namespace Clipper2Lib { }; -} // namespace +} // namespace #endif // CLIPPER_ENGINE_H diff --git a/CPP/Clipper2Lib/include/clipper2/clipper.export.h b/CPP/Clipper2Lib/include/clipper2/clipper.export.h index f89ec7c9..14ee216d 100644 --- a/CPP/Clipper2Lib/include/clipper2/clipper.export.h +++ b/CPP/Clipper2Lib/include/clipper2/clipper.export.h @@ -8,7 +8,7 @@ *******************************************************************************/ -/* +/* Boolean clipping: cliptype: None=0, Intersection=1, Union=2, Difference=3, Xor=4 fillrule: EvenOdd=0, NonZero=1, Positive=2, Negative=3 @@ -24,7 +24,7 @@ can be understood by just about any programming language. And these C style path structures are simple arrays of int64_t (CPath64) and double (CPathD). CPath64 and CPathD: -These are arrays of consecutive x and y path coordinates preceeded by +These are arrays of consecutive x and y path coordinates preceeded by a pair of values containing the path's length (N) and a 0 value. __________________________________ |counter|coord1|coord2|...|coordN| @@ -34,7 +34,7 @@ __________________________________ CPaths64 and CPathsD: These are also arrays containing any number of consecutive CPath64 or CPathD structures. But preceeding these consecutive paths, there is pair of -values that contain the total length of the array (A) structure and +values that contain the total length of the array (A) structure and the number (C) of CPath64 or CPathD it contains. _______________________________ |counter|path1|path2|...|pathC| @@ -42,15 +42,15 @@ _______________________________ _______________________________ CPolytree64 and CPolytreeD: -These are also arrays consisting of CPolyPath structures that represent +These are also arrays consisting of CPolyPath structures that represent individual paths in a tree structure. However, the very first (ie top) CPolyPath is just the tree container that won't have a path. And because of that, its structure will be very slightly different from the remaining CPolyPath. This difference will be discussed below. CPolyPath64 and CPolyPathD: -These are simple arrays consisting of a series of path coordinates followed -by any number of child (ie nested) CPolyPath. Preceeding these are two values +These are simple arrays consisting of a series of path coordinates followed +by any number of child (ie nested) CPolyPath. Preceeding these are two values indicating the length of the path (N) and the number of child CPolyPath (C). ____________________________________________________________ |counter|coord1|coord2|...|coordN| child1|child2|...|childC| @@ -58,18 +58,18 @@ ____________________________________________________________ ____________________________________________________________ As mentioned above, the very first CPolyPath structure is just a container -that owns (both directly and indirectly) every other CPolyPath in the tree. +that owns (both directly and indirectly) every other CPolyPath in the tree. Since this first CPolyPath has no path, instead of a path length, its very first value will contain the total length of the CPolytree array structure. All theses exported structures (CPaths64, CPathsD, CPolyTree64 & CPolyTreeD) -are arrays of type int64_t or double. And the first value in these arrays +are arrays of type int64_t or double. And the first value in these arrays will always contain the length of that array. -These array structures are allocated in heap memory which will eventually -need to be released. But since applications dynamically linking to these +These array structures are allocated in heap memory which will eventually +need to be released. But since applications dynamically linking to these functions may use different memory managers, the only safe way to free up -this memory is to use the exported DisposeArray64 and DisposeArrayD +this memory is to use the exported DisposeArray64 and DisposeArrayD functions below. */ @@ -128,7 +128,7 @@ inline Rect CRectToRect(const CRect& rect) #ifdef _WIN32 #define EXTERN_DLL_EXPORT extern "C" __declspec(dllexport) #else - #define EXTERN_DLL_EXPORT extern "C" + #define EXTERN_DLL_EXPORT extern "C" #endif @@ -173,8 +173,8 @@ EXTERN_DLL_EXPORT int BooleanOp_PolyTreeD(uint8_t cliptype, bool preserve_collinear = true, bool reverse_solution = false); EXTERN_DLL_EXPORT CPaths64 InflatePaths64(const CPaths64 paths, - double delta, uint8_t jointype, uint8_t endtype, - double miter_limit = 2.0, double arc_tolerance = 0.0, + double delta, uint8_t jointype, uint8_t endtype, + double miter_limit = 2.0, double arc_tolerance = 0.0, bool reverse_solution = false); EXTERN_DLL_EXPORT CPathsD InflatePathsD(const CPathsD paths, double delta, uint8_t jointype, uint8_t endtype, @@ -219,10 +219,10 @@ static size_t GetPolyPath64ArrayLen(const PolyPath64& pp) return result; } -static void GetPolytreeCountAndCStorageSize(const PolyTree64& tree, +static void GetPolytreeCountAndCStorageSize(const PolyTree64& tree, size_t& cnt, size_t& array_len) { - cnt = tree.Count(); // nb: top level count only + cnt = tree.Count(); // nb: top level count only array_len = GetPolyPath64ArrayLen(tree); } @@ -300,7 +300,7 @@ static Paths64 ConvertCPathsDToPaths64(const CPathsD paths, double scale) { Paths64 result; if (!paths) return result; - double* v = paths; + double* v = paths; ++v; // skip the first value (0) size_t cnt = static_cast(*v++); result.reserve(cnt); @@ -362,7 +362,7 @@ EXTERN_DLL_EXPORT const char* Version() return CLIPPER2_VERSION; } -EXTERN_DLL_EXPORT int BooleanOp64(uint8_t cliptype, +EXTERN_DLL_EXPORT int BooleanOp64(uint8_t cliptype, uint8_t fillrule, const CPaths64 subjects, const CPaths64 subjects_open, const CPaths64 clips, CPaths64& solution, CPaths64& solution_open, @@ -370,7 +370,7 @@ EXTERN_DLL_EXPORT int BooleanOp64(uint8_t cliptype, { if (cliptype > static_cast(ClipType::Xor)) return -4; if (fillrule > static_cast(FillRule::Negative)) return -3; - + Paths64 sub, sub_open, clp, sol, sol_open; sub = ConvertCPaths(subjects); sub_open = ConvertCPaths(subjects_open); @@ -382,7 +382,7 @@ EXTERN_DLL_EXPORT int BooleanOp64(uint8_t cliptype, if (sub.size() > 0) clipper.AddSubject(sub); if (sub_open.size() > 0) clipper.AddOpenSubject(sub_open); if (clp.size() > 0) clipper.AddClip(clp); - if (!clipper.Execute(ClipType(cliptype), FillRule(fillrule), sol, sol_open)) + if (!clipper.Execute(ClipType(cliptype), FillRule(fillrule), sol, sol_open)) return -1; // clipping bug - should never happen :) solution = CreateCPaths(sol); solution_open = CreateCPaths(sol_open); @@ -455,7 +455,7 @@ EXTERN_DLL_EXPORT int BooleanOp_PolyTreeD(uint8_t cliptype, if (precision < -8 || precision > 8) return -5; if (cliptype > static_cast(ClipType::Xor)) return -4; if (fillrule > static_cast(FillRule::Negative)) return -3; - + double scale = std::pow(10, precision); int err = 0; @@ -485,10 +485,10 @@ EXTERN_DLL_EXPORT CPaths64 InflatePaths64(const CPaths64 paths, { Paths64 pp; pp = ConvertCPaths(paths); - ClipperOffset clip_offset( miter_limit, + ClipperOffset clip_offset( miter_limit, arc_tolerance, reverse_solution); clip_offset.AddPaths(pp, JoinType(jointype), EndType(endtype)); - Paths64 result; + Paths64 result; clip_offset.Execute(delta, result); return CreateCPaths(result); } @@ -561,5 +561,5 @@ EXTERN_DLL_EXPORT CPathsD RectClipLinesD(const CRectD& rect, } } // end Clipper2Lib namespace - + #endif // CLIPPER2_EXPORT_H diff --git a/CPP/Clipper2Lib/include/clipper2/clipper.h b/CPP/Clipper2Lib/include/clipper2/clipper.h index 56266225..33beef6f 100644 --- a/CPP/Clipper2Lib/include/clipper2/clipper.h +++ b/CPP/Clipper2Lib/include/clipper2/clipper.h @@ -24,7 +24,7 @@ namespace Clipper2Lib { inline Paths64 BooleanOp(ClipType cliptype, FillRule fillrule, const Paths64& subjects, const Paths64& clips) - { + { Paths64 result; Clipper64 clipper; clipper.AddSubject(subjects); @@ -58,7 +58,7 @@ namespace Clipper2Lib { } inline void BooleanOp(ClipType cliptype, FillRule fillrule, - const PathsD& subjects, const PathsD& clips, + const PathsD& subjects, const PathsD& clips, PolyTreeD& polytree, int precision = 2) { polytree.Clear(); @@ -75,7 +75,7 @@ namespace Clipper2Lib { { return BooleanOp(ClipType::Intersection, fillrule, subjects, clips); } - + inline PathsD Intersect(const PathsD& subjects, const PathsD& clips, FillRule fillrule, int decimal_prec = 2) { return BooleanOp(ClipType::Intersection, fillrule, subjects, clips, decimal_prec); @@ -145,7 +145,7 @@ namespace Clipper2Lib { } inline PathsD InflatePaths(const PathsD& paths, double delta, - JoinType jt, EndType et, double miter_limit = 2.0, + JoinType jt, EndType et, double miter_limit = 2.0, int precision = 2, double arc_tolerance = 0.0) { int error_code = 0; @@ -225,7 +225,7 @@ namespace Clipper2Lib { Rect64 r = ScaleRect(rect, scale); RectClip64 rc(r); Paths64 pp = ScalePaths(paths, scale, error_code); - if (error_code) return PathsD(); // ie: error_code result is lost + if (error_code) return PathsD(); // ie: error_code result is lost return ScalePaths( rc.Execute(pp), 1 / scale, error_code); } @@ -290,8 +290,8 @@ namespace Clipper2Lib { { // return false if this child isn't fully contained by its parent - // checking for a single vertex outside is a bit too crude since - // it doesn't account for rounding errors. It's better to check + // checking for a single vertex outside is a bit too crude since + // it doesn't account for rounding errors. It's better to check // for consecutive vertices found outside the parent's polygon. int outsideCnt = 0; @@ -311,7 +311,7 @@ namespace Clipper2Lib { return true; } - static void OutlinePolyPath(std::ostream& os, + static void OutlinePolyPath(std::ostream& os, size_t idx, bool isHole, size_t count, const std::string& preamble) { std::string plural = (count == 1) ? "." : "s."; @@ -342,7 +342,7 @@ namespace Clipper2Lib { } template - inline constexpr void MakePathGeneric(const T an_array, + inline constexpr void MakePathGeneric(const T an_array, size_t array_size, std::vector& result) { result.reserve(array_size / 2); @@ -354,7 +354,7 @@ namespace Clipper2Lib { #endif } - } // end details namespace + } // end details namespace inline std::ostream& operator<< (std::ostream& os, const PolyTree64& pp) { @@ -398,7 +398,7 @@ namespace Clipper2Lib { inline bool CheckPolytreeFullyContainsChildren(const PolyTree64& polytree) { for (const auto& child : polytree) - if (child->Count() > 0 && + if (child->Count() > 0 && !details::PolyPath64ContainsChildren(*child)) return false; return true; @@ -471,7 +471,7 @@ namespace Clipper2Lib { std::size_t size = N / 3; Path64 result(size); for (size_t i = 0; i < size; ++i) - result[i] = Point64(list[i * 3], + result[i] = Point64(list[i * 3], list[i * 3 + 1], list[i * 3 + 2]); return result; } @@ -489,7 +489,7 @@ namespace Clipper2Lib { list[i * 3 + 1], list[i * 3 + 2]); else for (size_t i = 0; i < size; ++i) - result[i] = PointD(list[i * 3], list[i * 3 + 1], + result[i] = PointD(list[i * 3], list[i * 3 + 1], static_cast(list[i * 3 + 2])); return result; } @@ -580,12 +580,12 @@ namespace Clipper2Lib { double cp = std::abs(CrossProduct(pt1, pt2, pt3)); return (cp * cp) / (DistanceSqr(pt1, pt2) * DistanceSqr(pt2, pt3)) < sin_sqrd_min_angle_rads; } - + template inline Path Ellipse(const Rect& rect, int steps = 0) { - return Ellipse(rect.MidPoint(), - static_cast(rect.Width()) *0.5, + return Ellipse(rect.MidPoint(), + static_cast(rect.Width()) *0.5, static_cast(rect.Height()) * 0.5, steps); } @@ -626,7 +626,7 @@ namespace Clipper2Lib { return Sqr(a * d - c * b) / (c * c + d * d); } - inline size_t GetNext(size_t current, size_t high, + inline size_t GetNext(size_t current, size_t high, const std::vector& flags) { ++current; @@ -637,7 +637,7 @@ namespace Clipper2Lib { return current; } - inline size_t GetPrior(size_t current, size_t high, + inline size_t GetPrior(size_t current, size_t high, const std::vector& flags) { if (current == 0) current = high; @@ -650,7 +650,7 @@ namespace Clipper2Lib { } template - inline Path SimplifyPath(const Path &path, + inline Path SimplifyPath(const Path &path, double epsilon, bool isClosedPath = true) { const size_t len = path.size(), high = len -1; @@ -665,7 +665,7 @@ namespace Clipper2Lib { distSqr[0] = PerpendicDistFromLineSqrd(path[0], path[high], path[1]); distSqr[high] = PerpendicDistFromLineSqrd(path[high], path[0], path[high - 1]); } - else + else { distSqr[0] = MAX_DBL; distSqr[high] = MAX_DBL; @@ -684,7 +684,7 @@ namespace Clipper2Lib { } while (curr != start && distSqr[curr] > epsSqr); if (curr == start) break; } - + prior = GetPrior(curr, high, flags); next = GetNext(curr, high, flags); if (next == prior) break; @@ -699,7 +699,7 @@ namespace Clipper2Lib { } else prior2 = GetPrior(prior, high, flags); - + flags[curr] = true; curr = next; next = GetNext(next, high, flags); @@ -717,7 +717,7 @@ namespace Clipper2Lib { } template - inline Paths SimplifyPaths(const Paths &paths, + inline Paths SimplifyPaths(const Paths &paths, double epsilon, bool isClosedPath = true) { Paths result; diff --git a/CPP/Clipper2Lib/include/clipper2/clipper.minkowski.h b/CPP/Clipper2Lib/include/clipper2/clipper.minkowski.h index ebddd08a..a3ddcf86 100644 --- a/CPP/Clipper2Lib/include/clipper2/clipper.minkowski.h +++ b/CPP/Clipper2Lib/include/clipper2/clipper.minkowski.h @@ -15,7 +15,7 @@ #include #include "clipper2/clipper.core.h" -namespace Clipper2Lib +namespace Clipper2Lib { namespace detail diff --git a/CPP/Clipper2Lib/include/clipper2/clipper.offset.h b/CPP/Clipper2Lib/include/clipper2/clipper.offset.h index 30992bfa..48a30fa6 100644 --- a/CPP/Clipper2Lib/include/clipper2/clipper.offset.h +++ b/CPP/Clipper2Lib/include/clipper2/clipper.offset.h @@ -83,7 +83,7 @@ class ClipperOffset { public: explicit ClipperOffset(double miter_limit = 2.0, double arc_tolerance = 0.0, - bool preserve_collinear = false, + bool preserve_collinear = false, bool reverse_solution = false) : miter_limit_(miter_limit), arc_tolerance_(arc_tolerance), preserve_collinear_(preserve_collinear), diff --git a/CPP/Clipper2Lib/src/clipper.engine.cpp b/CPP/Clipper2Lib/src/clipper.engine.cpp index 027a5676..ecea769e 100644 --- a/CPP/Clipper2Lib/src/clipper.engine.cpp +++ b/CPP/Clipper2Lib/src/clipper.engine.cpp @@ -189,7 +189,7 @@ namespace Clipper2Lib { } //PrevPrevVertex: useful to get the (inverted Y-axis) top of the - //alternate edge (ie left or right bound) during edge insertion. + //alternate edge (ie left or right bound) during edge insertion. inline Vertex* PrevPrevVertex(const Active& ae) { if (ae.wind_dx > 0) @@ -234,15 +234,15 @@ namespace Clipper2Lib { Vertex* result = e.vertex_top; if (e.wind_dx > 0) while ((result->next->pt.y == result->pt.y) && - ((result->flags & (VertexFlags::OpenEnd | + ((result->flags & (VertexFlags::OpenEnd | VertexFlags::LocalMax)) == VertexFlags::None)) result = result->next; else while (result->prev->pt.y == result->pt.y && - ((result->flags & (VertexFlags::OpenEnd | + ((result->flags & (VertexFlags::OpenEnd | VertexFlags::LocalMax)) == VertexFlags::None)) result = result->prev; - if (!IsMaxima(*result)) result = nullptr; // not a maxima + if (!IsMaxima(*result)) result = nullptr; // not a maxima return result; } @@ -253,7 +253,7 @@ namespace Clipper2Lib { while (result->next->pt.y == result->pt.y) result = result->next; else while (result->prev->pt.y == result->pt.y) result = result->prev; - if (!IsMaxima(*result)) result = nullptr; // not a maxima + if (!IsMaxima(*result)) result = nullptr; // not a maxima return result; } @@ -614,7 +614,7 @@ namespace Clipper2Lib { list.push_back(std::make_unique (&vert, polytype, is_open)); } - void AddPaths_(const Paths64& paths, PathType polytype, bool is_open, + void AddPaths_(const Paths64& paths, PathType polytype, bool is_open, std::vector& vertexLists, LocalMinimaList& locMinList) { const auto total_vertex_count = @@ -811,7 +811,7 @@ namespace Clipper2Lib { void ClipperBase::SetZ(const Active& e1, const Active& e2, Point64& ip) { if (!zCallback_) return; - // prioritize subject over clip vertices by passing + // prioritize subject over clip vertices by passing // subject vertices before clip vertices in the callback if (GetPolyType(e1) == PathType::Subject) { @@ -846,11 +846,11 @@ namespace Clipper2Lib { if (is_open) has_open_paths_ = true; minima_list_sorted_ = false; AddPaths_(paths, polytype, is_open, vertex_lists_, minima_list_); - } + } - void ClipperBase::AddReuseableData(const ReuseableDataContainer64& reuseable_data) + void ClipperBase::AddReuseableData(const ReuseableDataContainer64& reuseable_data) { - // nb: reuseable_data will continue to own the vertices + // nb: reuseable_data will continue to own the vertices // and remains responsible for their clean up. succeeded_ = false; minima_list_sorted_ = false; @@ -1386,7 +1386,7 @@ namespace Clipper2Lib { { if (IsJoined(e1)) Split(e1, pt); if (IsJoined(e2)) Split(e2, pt); - + if (IsFront(e1) == IsFront(e2)) { if (IsOpenEnd(e1)) @@ -1410,7 +1410,7 @@ namespace Clipper2Lib { { Active* e = GetPrevHotEdge(e1); if (!e) - outrec.owner = nullptr; + outrec.owner = nullptr; else SetOwner(&outrec, e->outrec); // nb: outRec.owner here is likely NOT the real @@ -1477,7 +1477,7 @@ namespace Clipper2Lib { e2.outrec->pts = e1.outrec->pts; e1.outrec->pts = nullptr; } - else + else SetOwner(e2.outrec, e1.outrec); //and e1 and e2 are maxima and are about to be dropped from the Actives list. @@ -1567,7 +1567,7 @@ namespace Clipper2Lib { void ClipperBase::DoSplitOp(OutRec* outrec, OutPt* splitOp) { - // splitOp.prev -> splitOp && + // splitOp.prev -> splitOp && // splitOp.next -> splitOp.next.next are intersecting OutPt* prevOp = splitOp->prev; OutPt* nextNextOp = splitOp->next->next; @@ -1618,7 +1618,7 @@ namespace Clipper2Lib { { OutRec* newOr = NewOutRec(); newOr->owner = outrec->owner; - + splitOp->outrec = newOr; splitOp->next->outrec = newOr; OutPt* newOp = new OutPt(ip, newOr); @@ -2066,7 +2066,7 @@ namespace Clipper2Lib { e->next_in_sel = e->next_in_ael; e->jump = e->next_in_sel; if (e->join_with == JoinWith::Left) - e->curr_x = e->prev_in_ael->curr_x; // also avoids complications + e->curr_x = e->prev_in_ael->curr_x; // also avoids complications else e->curr_x = TopX(*e, top_y); e = e->next_in_ael; @@ -2139,7 +2139,7 @@ namespace Clipper2Lib { if (outrecHasEdges) { OutPt* opA = outrec->pts, * opZ = opA->next; - while (opP != opZ && opP->prev->pt.y == curr_y) + while (opP != opZ && opP->prev->pt.y == curr_y) opP = opP->prev; while (opN != opA && opN->next->pt.y == curr_y) opN = opN->next; @@ -2151,7 +2151,7 @@ namespace Clipper2Lib { while (opN->next != opP && opN->next->pt.y == curr_y) opN = opN->next; } - bool result = + bool result = SetHorzSegHeadingForward(hs, opP, opN) && !hs.left_op->horz; @@ -2161,10 +2161,10 @@ namespace Clipper2Lib { hs.right_op = nullptr; // (for sorting) return result; } - + void ClipperBase::ConvertHorzSegsToJoins() { - auto j = std::count_if(horz_seg_list_.begin(), + auto j = std::count_if(horz_seg_list_.begin(), horz_seg_list_.end(), [](HorzSegment& hs) { return UpdateHorzSegment(hs); }); if (j < 2) return; @@ -2208,8 +2208,8 @@ namespace Clipper2Lib { DuplicateOp(hs1->left_op, false)); horz_join_list_.push_back(join); } - } - } + } + } } void MoveSplits(OutRec* fromOr, OutRec* toOr) @@ -2322,7 +2322,7 @@ namespace Clipper2Lib { ip = GetClosestPointOnSegment(ip, e1.bot, e1.top); else if (abs_dx2 > 100) ip = GetClosestPointOnSegment(ip, e2.bot, e2.top); - else + else { if (ip.y < top_y) ip.y = top_y; else ip.y = bot_y_; @@ -2454,7 +2454,7 @@ namespace Clipper2Lib { horz_seg_list_.push_back(HorzSegment(op)); } - bool ClipperBase::ResetHorzDirection(const Active& horz, + bool ClipperBase::ResetHorzDirection(const Active& horz, const Vertex* max_vertex, int64_t& horz_left, int64_t& horz_right) { if (horz.bot.x == horz.top.x) @@ -2642,7 +2642,7 @@ namespace Clipper2Lib { ResetHorzDirection(horz, vertex_max, horz_left, horz_right); } - if (IsHotEdge(horz)) + if (IsHotEdge(horz)) { OutPt* op = AddOutPt(horz, horz.top); AddTrialHorzJoin(op); @@ -2755,16 +2755,16 @@ namespace Clipper2Lib { } } - void ClipperBase::CheckJoinLeft(Active& e, + void ClipperBase::CheckJoinLeft(Active& e, const Point64& pt, bool check_curr_x) { Active* prev = e.prev_in_ael; - if (!prev || - !IsHotEdge(e) || !IsHotEdge(*prev) || - IsHorizontal(e) || IsHorizontal(*prev) || + if (!prev || + !IsHotEdge(e) || !IsHotEdge(*prev) || + IsHorizontal(e) || IsHorizontal(*prev) || IsOpen(e) || IsOpen(*prev) ) return; if ((pt.y < e.top.y + 2 || pt.y < prev->top.y + 2) && - ((e.bot.y > pt.y) || (prev->bot.y > pt.y))) return; // avoid trivial joins + ((e.bot.y > pt.y) || (prev->bot.y > pt.y))) return; // avoid trivial joins if (check_curr_x) { @@ -2783,16 +2783,16 @@ namespace Clipper2Lib { e.join_with = JoinWith::Left; } - void ClipperBase::CheckJoinRight(Active& e, + void ClipperBase::CheckJoinRight(Active& e, const Point64& pt, bool check_curr_x) { Active* next = e.next_in_ael; - if (!next || - !IsHotEdge(e) || !IsHotEdge(*next) || - IsHorizontal(e) || IsHorizontal(*next) || + if (!next || + !IsHotEdge(e) || !IsHotEdge(*next) || + IsHorizontal(e) || IsHorizontal(*next) || IsOpen(e) || IsOpen(*next)) return; if ((pt.y < e.top.y +2 || pt.y < next->top.y +2) && - ((e.bot.y > pt.y) || (next->bot.y > pt.y))) return; // avoid trivial joins + ((e.bot.y > pt.y) || (next->bot.y > pt.y))) return; // avoid trivial joins if (check_curr_x) { @@ -2800,7 +2800,7 @@ namespace Clipper2Lib { } 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) @@ -2877,8 +2877,8 @@ namespace Clipper2Lib { if (!outrec->pts) return false; if (!outrec->bounds.IsEmpty()) return true; CleanCollinear(outrec); - if (!outrec->pts || - !BuildPath64(outrec->pts, reverse_solution_, false, outrec->path)){ + if (!outrec->pts || + !BuildPath64(outrec->pts, reverse_solution_, false, outrec->path)){ return false;} outrec->bounds = GetBounds(outrec->path); return true; @@ -2892,10 +2892,10 @@ namespace Clipper2Lib { if(!split || split == outrec || split->recursive_split == outrec) continue; split->recursive_split = outrec; // prevent infinite loops - if (split->splits && CheckSplitOwner(outrec, split->splits)) + if (split->splits && CheckSplitOwner(outrec, split->splits)) return true; - else if (CheckBounds(split) && - IsValidOwner(outrec, split) && + else if (CheckBounds(split) && + IsValidOwner(outrec, split) && split->bounds.Contains(outrec->bounds) && Path1InsidePath2(outrec->pts, split->pts)) { @@ -2924,7 +2924,7 @@ namespace Clipper2Lib { if (outrec->owner) { - if (!outrec->owner->polypath) + if (!outrec->owner->polypath) RecursiveCheckOwners(outrec->owner, polypath); outrec->polypath = outrec->owner->polypath->AddChild(outrec->path); } @@ -2973,7 +2973,7 @@ namespace Clipper2Lib { open_paths.resize(0); if (has_open_paths_) open_paths.reserve(outrec_list_.size()); - + // outrec_list_.size() is not static here because // CheckBounds below can indirectly add additional // OutRec (via FixOutRecPts & CleanCollinear) @@ -2996,7 +2996,7 @@ namespace Clipper2Lib { bool BuildPathD(OutPt* op, bool reverse, bool isOpen, PathD& path, double inv_scale) { - if (!op || op->next == op || (!isOpen && op->next == op->prev)) + if (!op || op->next == op || (!isOpen && op->next == op->prev)) return false; path.resize(0); @@ -3029,7 +3029,7 @@ namespace Clipper2Lib { #else path.push_back(PointD(lastPt.x * inv_scale, lastPt.y * inv_scale)); #endif - + } if (reverse) op2 = op2->prev; diff --git a/CPP/Clipper2Lib/src/clipper.offset.cpp b/CPP/Clipper2Lib/src/clipper.offset.cpp index 0282aa49..3cae81cf 100644 --- a/CPP/Clipper2Lib/src/clipper.offset.cpp +++ b/CPP/Clipper2Lib/src/clipper.offset.cpp @@ -29,7 +29,7 @@ void GetMultiBounds(const Paths64& paths, std::vector& recList) { recList.reserve(paths.size()); for (const Path64& path : paths) - { + { if (path.size() < 1) { recList.push_back(InvalidRect64); @@ -96,7 +96,7 @@ inline bool AlmostZero(double value, double epsilon = 0.001) return std::fabs(value) < epsilon; } -inline double Hypot(double x, double y) +inline double Hypot(double x, double y) { //see https://stackoverflow.com/a/32436148/359538 return std::sqrt(x * x + y * y); @@ -271,7 +271,7 @@ void ClipperOffset::DoBevel(const Path64& path, size_t j, size_t k) double abs_delta = std::abs(group_delta_); pt1 = PointD(path[j].x - abs_delta * norms[j].x, path[j].y - abs_delta * norms[j].y); pt2 = PointD(path[j].x + abs_delta * norms[j].x, path[j].y + abs_delta * norms[j].y); - } + } else { pt1 = PointD(path[j].x + group_delta_ * norms[k].x, path[j].y + group_delta_ * norms[k].y); @@ -284,7 +284,7 @@ void ClipperOffset::DoBevel(const Path64& path, size_t j, size_t k) void ClipperOffset::DoSquare(const Path64& path, size_t j, size_t k) { PointD vec; - if (j == k) + if (j == k) vec = PointD(norms[j].y, -norms[j].x); else vec = GetAvgUnitVector( @@ -343,7 +343,7 @@ void ClipperOffset::DoMiter(const Path64& path, size_t j, size_t k, double cos_a void ClipperOffset::DoRound(const Path64& path, size_t j, size_t k, double angle) { if (deltaCallback64_) { - // when deltaCallback64_ is assigned, group_delta_ won't be constant, + // when deltaCallback64_ is assigned, group_delta_ won't be constant, // so we'll need to do the following calculations for *every* vertex. double abs_delta = std::fabs(group_delta_); double arcTol = (arc_tolerance_ > floating_point_tolerance ? @@ -413,9 +413,9 @@ void ClipperOffset::OffsetPoint(Group& group, const Path64& path, size_t j, size path_out.push_back(path[j]); // (#405) path_out.push_back(GetPerpendic(path[j], norms[j], group_delta_)); } - else if (cos_a > 0.999 && join_type_ != JoinType::Round) + else if (cos_a > 0.999 && join_type_ != JoinType::Round) { - // almost straight - less than 2.5 degree (#424, #482, #526 & #724) + // almost straight - less than 2.5 degree (#424, #482, #526 & #724) DoMiter(path, j, k, cos_a); } else if (join_type_ == JoinType::Miter) @@ -483,7 +483,7 @@ void ClipperOffset::OffsetOpenPath(Group& group, const Path64& path) for (Path64::size_type j = 1, k = 0; j < highI; k = j, ++j) OffsetPoint(group, path, j, k); - // reverse normals + // reverse normals for (size_t i = highI; i > 0; --i) norms[i] = PointD(-norms[i - 1].x, -norms[i - 1].y); norms[0] = norms[highI]; @@ -519,7 +519,7 @@ void ClipperOffset::DoGroupOffset(Group& group) { if (group.end_type == EndType::Polygon) { - // a straight path (2 points) can now also be 'polygon' offset + // a straight path (2 points) can now also be 'polygon' offset // where the ends will be treated as (180 deg.) joins if (group.lowest_path_idx < 0) delta_ = std::abs(delta_); group_delta_ = (group.is_reversed) ? -delta_ : delta_; @@ -541,9 +541,9 @@ void ClipperOffset::DoGroupOffset(Group& group) if (group.join_type == JoinType::Round || group.end_type == EndType::Round) { // calculate a sensible number of steps (for 360 deg for the given offset) - // arcTol - when arc_tolerance_ is undefined (0), the amount of - // curve imprecision that's allowed is based on the size of the - // offset (delta). Obviously very large offsets will almost always + // arcTol - when arc_tolerance_ is undefined (0), the amount of + // curve imprecision that's allowed is based on the size of the + // offset (delta). Obviously very large offsets will almost always // require much less precision. See also offset_triginometry2.svg double arcTol = (arc_tolerance_ > floating_point_tolerance ? std::min(abs_delta, arc_tolerance_) : @@ -590,7 +590,7 @@ void ClipperOffset::DoGroupOffset(Group& group) } solution.push_back(path_out); continue; - } // end of offsetting a single point + } // end of offsetting a single point // when shrinking outer paths, make sure they can shrink this far (#593) // also when shrinking holes, make sure they too can shrink this far (#715) @@ -599,8 +599,8 @@ void ClipperOffset::DoGroupOffset(Group& group) continue; if ((pathLen == 2) && (group.end_type == EndType::Joined)) - end_type_ = (group.join_type == JoinType::Round) ? - EndType::Round : + end_type_ = (group.join_type == JoinType::Round) ? + EndType::Round : EndType::Square; BuildNormals(*path_in_it); @@ -639,7 +639,7 @@ void ClipperOffset::ExecuteInternal(double delta) if (groups_.size() == 0) return; solution.reserve(CalcSolutionCapacity()); - if (std::abs(delta) < 0.5) // ie: offset is insignificant + if (std::abs(delta) < 0.5) // ie: offset is insignificant { Paths64::size_type sol_size = 0; for (const Group& group : groups_) sol_size += group.paths_in.size(); diff --git a/CPP/Clipper2Lib/src/clipper.rectclip.cpp b/CPP/Clipper2Lib/src/clipper.rectclip.cpp index 9aa0fc0f..bf9771b6 100644 --- a/CPP/Clipper2Lib/src/clipper.rectclip.cpp +++ b/CPP/Clipper2Lib/src/clipper.rectclip.cpp @@ -71,7 +71,7 @@ namespace Clipper2Lib { return pt1.y == pt2.y; } - inline bool GetSegmentIntersection(const Point64& p1, + bool GetSegmentIntersection(const Point64& p1, const Point64& p2, const Point64& p3, const Point64& p4, Point64& ip) { double res1 = CrossProduct(p1, p3, p4); @@ -125,7 +125,7 @@ namespace Clipper2Lib { { case Location::Left: if (GetSegmentIntersection(p, p2, rectPath[0], rectPath[3], ip)) return true; - else if ((p.y < rectPath[0].y) && GetSegmentIntersection(p, p2, rectPath[0], rectPath[1], ip)) + else if ((p.y < rectPath[0].y) && GetSegmentIntersection(p, p2, rectPath[0], rectPath[1], ip)) { loc = Location::Top; return true; @@ -180,7 +180,7 @@ namespace Clipper2Lib { else return false; default: // loc == rInside - if (GetSegmentIntersection(p, p2, rectPath[0], rectPath[3], ip)) + if (GetSegmentIntersection(p, p2, rectPath[0], rectPath[3], ip)) { loc = Location::Left; return true; @@ -420,7 +420,7 @@ namespace Clipper2Lib { break; //inner loop } break; - } //switch + } //switch } void RectClip64::ExecuteInternal(const Path64& path) @@ -433,7 +433,7 @@ namespace Clipper2Lib { { i = highI - 1; while (i >= 0 && !GetLocation(rect_, path[i], prev)) --i; - if (i < 0) + if (i < 0) { // all of path must be inside fRect for (const auto& pt : path) Add(pt); @@ -454,12 +454,12 @@ namespace Clipper2Lib { if (i > highI) break; Point64 ip, ip2; - Point64 prev_pt = (i) ? - path[static_cast(i - 1)] : + Point64 prev_pt = (i) ? + path[static_cast(i - 1)] : path[highI]; crossing_loc = loc; - if (!GetIntersection(rect_as_path_, + if (!GetIntersection(rect_as_path_, path[i], prev_pt, crossing_loc, ip)) { // ie remaining outside @@ -470,7 +470,7 @@ namespace Clipper2Lib { start_locs_.push_back(prev); prev = GetAdjacentLocation(prev, isClockw); } while (prev != loc); - crossing_loc = crossing_prev; // still not crossed + crossing_loc = crossing_prev; // still not crossed } else if (prev != Location::Inside && prev != loc) { @@ -504,7 +504,7 @@ namespace Clipper2Lib { } else if (prev != Location::Inside) { - // passing right through rect. 'ip' here will be the second + // passing right through rect. 'ip' here will be the second // intersect pt but we'll also need the first intersect pt (ip2) loc = prev; GetIntersection(rect_as_path_, prev_pt, path[i], loc, ip2); @@ -648,7 +648,7 @@ namespace Clipper2Lib { size_t i = 0, j = 0; OutPt2* p1, * p2, * p1a, * p2a, * op, * op2; - while (i < cw.size()) + while (i < cw.size()) { p1 = cw[i]; if (!p1 || p1->next == p1->prev) @@ -825,7 +825,7 @@ namespace Clipper2Lib { OutPt2* op2 = op->next; while (op2 && op2 != op) { - if (CrossProduct(op2->prev->pt, + if (CrossProduct(op2->prev->pt, op2->pt, op2->next->pt) == 0) { op = op2->prev; @@ -854,7 +854,7 @@ namespace Clipper2Lib { if (rect_.IsEmpty()) return result; for (const Path64& path : paths) - { + { if (path.size() < 3) continue; path_bounds_ = GetBounds(path); if (!rect_.Intersects(path_bounds_)) @@ -870,7 +870,7 @@ namespace Clipper2Lib { CheckEdges(); for (int i = 0; i < 4; ++i) TidyEdges(i, edges_[i * 2], edges_[i * 2 + 1]); - + for (OutPt2*& op : results_) { Path64 tmp = GetPath(op); @@ -932,7 +932,7 @@ namespace Clipper2Lib { if (!GetLocation(rect_, path[0], loc)) { while (i <= highI && !GetLocation(rect_, path[i], prev)) ++i; - if (i > highI) + if (i > highI) { // all of path must be inside fRect for (const auto& pt : path) Add(pt); @@ -953,7 +953,7 @@ namespace Clipper2Lib { Point64 prev_pt = path[static_cast(i - 1)]; crossing_loc = loc; - if (!GetIntersection(rect_as_path_, + if (!GetIntersection(rect_as_path_, path[i], prev_pt, crossing_loc, ip)) { // ie remaining outside @@ -971,10 +971,10 @@ namespace Clipper2Lib { } else if (prev != Location::Inside) { - // passing right through rect. 'ip' here will be the second + // passing right through rect. 'ip' here will be the second // intersect pt but we'll also need the first intersect pt (ip2) crossing_loc = prev; - GetIntersection(rect_as_path_, + GetIntersection(rect_as_path_, prev_pt, path[i], crossing_loc, ip2); Add(ip2, true); Add(ip); @@ -991,14 +991,14 @@ namespace Clipper2Lib { { Path64 result; if (!op || op == op->next) return result; - op = op->next; // starting at path beginning + op = op->next; // starting at path beginning result.push_back(op->pt); OutPt2 *op2 = op->next; while (op2 != op) { result.push_back(op2->pt); op2 = op2->next; - } + } return result; } diff --git a/CPP/Tests/TestExportHeaders.cpp b/CPP/Tests/TestExportHeaders.cpp index 02ca6655..f559474b 100644 --- a/CPP/Tests/TestExportHeaders.cpp +++ b/CPP/Tests/TestExportHeaders.cpp @@ -1,11 +1,8 @@ #include - #include "clipper2/clipper.h" #include "clipper2/clipper.core.h" #include "clipper2/clipper.export.h" - using namespace Clipper2Lib; - static bool CreatePolyPath64FromCPolyPath(CPolyPath64& v, PolyPath64& owner) { int64_t poly_len = *v++, child_count = *v++; @@ -17,13 +14,11 @@ static bool CreatePolyPath64FromCPolyPath(CPolyPath64& v, PolyPath64& owner) int64_t x = *v++, y = *v++; path.push_back(Point64(x,y)); } - PolyPath64* new_owner = owner.AddChild(path); for (size_t i = 0; i < child_count; ++i) CreatePolyPath64FromCPolyPath(v, *new_owner); return true; } - static bool BuildPolyTree64FromCPolyTree(CPolyTree64 tree, PolyTree64& result) { result.Clear(); @@ -33,7 +28,6 @@ static bool BuildPolyTree64FromCPolyTree(CPolyTree64 tree, PolyTree64& result) if (!CreatePolyPath64FromCPolyPath(v, result)) return false; return true; } - static bool CreatePolyPathDFromCPolyPath(CPolyPathD& v, PolyPathD& owner) { int64_t poly_len = *v++, child_count = *v++; @@ -50,7 +44,6 @@ static bool CreatePolyPathDFromCPolyPath(CPolyPathD& v, PolyPathD& owner) CreatePolyPathDFromCPolyPath(v, *new_owner); return true; } - static bool BuildPolyTreeDFromCPolyTree(CPolyTreeD tree, PolyTreeD& result) { result.Clear(); @@ -60,49 +53,37 @@ static bool BuildPolyTreeDFromCPolyTree(CPolyTreeD tree, PolyTreeD& result) if (!CreatePolyPathDFromCPolyPath(v, result)) return false; return true; } - TEST(Clipper2Tests, ExportHeader64) { - uint8_t None = 0, Intersection = 1, Union = 2, Difference = 3, Xor = 4; uint8_t EvenOdd = 0, NonZero = 1, Positive = 2, Negative = 3; - Paths64 subj, clip, solution; //subj.push_back(MakeRandomPoly(600, 400, 25)); //clip.push_back(MakeRandomPoly(600, 400, 25)); - for (int i = 1; i < 6; ++i) subj.push_back(MakePath({ -i*20,-i * 20, i * 20,-i * 20, i * 20,i * 20, -i * 20,i * 20 })); clip.push_back(MakePath({ -90,-120,90,-120, 90,120, -90,120 })); - CPaths64 c_subj_open = nullptr, c_sol = nullptr, c_sol_open = nullptr; - - // Note: while CreateCPaths64 isn't exported in clipper.export.h, it can still - // be used here because we're simply statically compiling clipper.export.h. + // Note: while CreateCPaths64 isn't exported in clipper.export.h, it can still + // be used here because we're simply statically compiling clipper.export.h. // Normally clipper.export.h will be compiled into a DLL/so so it can be called // by non C++ applications. If CreateCPaths64 was an exported function and it // was called by a non C++ application, it would crash that application. CPaths64 c_subj = CreateCPaths(subj); CPaths64 c_clip = CreateCPaths(clip); - BooleanOp64(Intersection, EvenOdd, c_subj, c_subj_open, c_clip, c_sol, c_sol_open); solution = ConvertCPaths(c_sol); - //clean up !!! delete[] c_subj; delete[] c_clip; DisposeArray64(c_sol); DisposeArray64(c_sol_open); - EXPECT_EQ(solution.size(), 5); } - TEST(Clipper2Tests, ExportHeaderD) { - uint8_t None = 0, Intersection = 1, Union = 2, Difference = 3, Xor = 4; uint8_t EvenOdd = 0, NonZero = 1, Positive = 2, Negative = 3; - PathsD subj, clip, solution; //subj.push_back(MakeRandomPolyD(600, 400, 25)); //clip.push_back(MakeRandomPolyD(600, 400, 25)); @@ -110,111 +91,88 @@ TEST(Clipper2Tests, ExportHeaderD) subj.push_back(MakePathD({ -i * 20,-i * 20, i * 20,-i * 20, i * 20,i * 20, -i * 20,i * 20 })); clip.push_back(MakePathD({ -90,-120,90,-120, 90,120, -90,120 })); CPathsD c_subj_open = nullptr, c_sol = nullptr, c_sol_open = nullptr; - - // Note: while CreateCPathsD isn't exported in clipper.export.h, it can still - // be used here because we're simply statically compiling clipper.export.h. + // Note: while CreateCPathsD isn't exported in clipper.export.h, it can still + // be used here because we're simply statically compiling clipper.export.h. // Normally clipper.export.h will be compiled into a DLL/so so it can be called // by non C++ applications. If CreateCPathsD was an exported function and it // was called by a non C++ application, it would crash that application. CPathsD c_subj = CreateCPaths(subj); CPathsD c_clip = CreateCPaths(clip); - BooleanOpD(Intersection, EvenOdd, c_subj, c_subj_open, c_clip, c_sol, c_sol_open); solution = ConvertCPaths(c_sol); - //clean up !!! delete[] c_subj; delete[] c_clip; DisposeArrayD(c_sol); DisposeArrayD(c_sol_open); - EXPECT_EQ(solution.size(), 5); } - TEST(Clipper2Tests, ExportHeaderTree64) { uint8_t None = 0, Intersection = 1, Union = 2, Difference = 3, Xor = 4; uint8_t EvenOdd = 0, NonZero = 1, Positive = 2, Negative = 3; - Paths64 subj, clip, solution; for (int i = 1; i < 6; ++i) subj.push_back(MakePath({ -i * 20,-i * 20, i * 20,-i * 20, i * 20,i * 20, -i * 20,i * 20 })); clip.push_back(MakePath({ -90,-120,90,-120, 90,120, -90,120 })); CPaths64 c_subj_open = nullptr, c_sol = nullptr, c_sol_open = nullptr; - - // Note: while CreateCPaths64 isn't exported in clipper.export.h, it can still - // be used here because we're statically compiling clipper.export.h. - // More likely, clipper.export.h will be compiled into a DLL/so so it can be - // called by non C++ applications. If CreateCPaths64 was an exported function + // Note: while CreateCPaths64 isn't exported in clipper.export.h, it can still + // be used here because we're statically compiling clipper.export.h. + // More likely, clipper.export.h will be compiled into a DLL/so so it can be + // called by non C++ applications. If CreateCPaths64 was an exported function // and it was called by a non C++ application, it would crash that application. CPaths64 c_subj = CreateCPaths(subj); CPaths64 c_clip = CreateCPaths(clip); - int64_t* c_sol_tree = nullptr; BooleanOp_PolyTree64(Intersection, EvenOdd, c_subj, c_subj_open, c_clip, c_sol_tree, c_sol_open); - PolyTree64 sol_tree; - - // convert CPolyTree64 to PolyTree64 + // convert CPolyTree64 to PolyTree64 BuildPolyTree64FromCPolyTree(c_sol_tree, sol_tree); - // convert PolyTree64 to Paths64 solution = PolyTreeToPaths64(sol_tree); - //clean up !!! delete[] c_subj; delete[] c_clip; DisposeArray64(c_sol_tree); DisposeArray64(c_sol_open); - PolyPath64* pp = &sol_tree; for (int i = 0; i < 4; ++i) { EXPECT_TRUE(pp->Count() == 1); pp = pp->Child(0); } - } - - TEST(Clipper2Tests, ExportHeaderTreeD) { uint8_t None = 0, Intersection = 1, Union = 2, Difference = 3, Xor = 4; uint8_t EvenOdd = 0, NonZero = 1, Positive = 2, Negative = 3; - PathsD subj, clip, solution; for (int i = 1; i < 6; ++i) subj.push_back(MakePathD({ -i * 20,-i * 20, i * 20,-i * 20, i * 20,i * 20, -i * 20,i * 20 })); clip.push_back(MakePathD({ -90,-120,90,-120, 90,120, -90,120 })); CPathsD c_subj_open = nullptr, c_sol = nullptr, c_sol_open = nullptr; - - // Note: while CreateCPathsD isn't exported in clipper.export.h, it can still - // be used here because we're statically compiling clipper.export.h. - // More likely, clipper.export.h will be compiled into a DLL/so so it can be - // called by non C++ applications. If CreateCPathsD was an exported function + // Note: while CreateCPathsD isn't exported in clipper.export.h, it can still + // be used here because we're statically compiling clipper.export.h. + // More likely, clipper.export.h will be compiled into a DLL/so so it can be + // called by non C++ applications. If CreateCPathsD was an exported function // and it was called by a non C++ application, it would crash that application. CPathsD c_subj = CreateCPaths(subj); CPathsD c_clip = CreateCPaths(clip); - - static const int precision = 4; CPolyPathD c_sol_tree = nullptr; BooleanOp_PolyTreeD(Intersection, EvenOdd, c_subj, c_subj_open, c_clip, c_sol_tree, c_sol_open, precision); - PolyTreeD sol_tree; - // convert CPolyTreeD to PolyTreeD + // convert CPolyTreeD to PolyTreeD BuildPolyTreeDFromCPolyTree(c_sol_tree, sol_tree); // convert PolyTreeD to PathsD solution = PolyTreeToPathsD(sol_tree); - //clean up !!! delete[] c_subj; delete[] c_clip; DisposeArrayD(c_sol_tree); DisposeArrayD(c_sol_open); - PolyPathD* pp = &sol_tree; for (int i = 0; i < 4; ++i) { EXPECT_TRUE(pp->Count() == 1); pp = pp->Child(0); } -} +} \ No newline at end of file diff --git a/CPP/Tests/TestLines.cpp b/CPP/Tests/TestLines.cpp index def6465b..2c7db3e3 100644 --- a/CPP/Tests/TestLines.cpp +++ b/CPP/Tests/TestLines.cpp @@ -1,14 +1,11 @@ #include #include "clipper2/clipper.h" #include "ClipFileLoad.h" - TEST(Clipper2Tests, TestMultipleLines) { - std::ifstream ifs("Lines.txt"); //if (!ifs.good()) return; ASSERT_TRUE(ifs); ASSERT_TRUE(ifs.good()); - int test_number = 1; while (true) { @@ -17,31 +14,26 @@ TEST(Clipper2Tests, TestMultipleLines) { Clipper2Lib::ClipType ct; Clipper2Lib::FillRule fr; int64_t area, count; - if (!LoadTestNum(ifs, test_number, subject, subject_open, clip, area, count, ct, fr)) break; - Clipper2Lib::Clipper64 c; c.AddSubject(subject); c.AddOpenSubject(subject_open); c.AddClip(clip); EXPECT_TRUE(c.Execute(ct, fr, solution, solution_open)); - const int64_t count2 = solution.size() + solution_open.size(); const int64_t count_diff = std::abs(count2 - count); - const double relative_count_diff = count ? - count_diff / static_cast(count) : + const double relative_count_diff = count ? + count_diff / static_cast(count) : 0; - if (test_number == 1) { EXPECT_EQ(solution.size(), 1); if (solution.size() > 0) { - EXPECT_EQ(solution[0].size(), 6); + EXPECT_EQ(solution[0].size(), 6); EXPECT_TRUE(IsPositive(solution[0])); } - EXPECT_EQ(solution_open.size(), 1); if (solution_open.size() > 0) { diff --git a/CPP/Tests/TestOffsetOrientation.cpp b/CPP/Tests/TestOffsetOrientation.cpp index b668a097..0c21aaa6 100644 --- a/CPP/Tests/TestOffsetOrientation.cpp +++ b/CPP/Tests/TestOffsetOrientation.cpp @@ -1,18 +1,15 @@ #include #include "clipper2/clipper.offset.h" #include "ClipFileLoad.h" - TEST(Clipper2Tests, TestOffsettingOrientation1) { const Clipper2Lib::Paths64 subject = { Clipper2Lib::MakePath({ 0,0, 0,5, 5,5, 5,0 }) }; - Clipper2Lib::Paths64 solution = Clipper2Lib::InflatePaths(subject, 1, + Clipper2Lib::Paths64 solution = Clipper2Lib::InflatePaths(subject, 1, Clipper2Lib::JoinType::Round, Clipper2Lib::EndType::Polygon); ASSERT_EQ(solution.size(), 1); //when offsetting, output orientation should match input EXPECT_TRUE(Clipper2Lib::IsPositive(subject[0]) == Clipper2Lib::IsPositive(solution[0])); } - TEST(Clipper2Tests, TestOffsettingOrientation2) { - const Clipper2Lib::Paths64 subject = { Clipper2Lib::MakePath({20, 220, 280, 220, 280, 280, 20, 280}), Clipper2Lib::MakePath({0, 200, 0, 300, 300, 300, 300, 200}) @@ -22,13 +19,11 @@ TEST(Clipper2Tests, TestOffsettingOrientation2) { co.AddPaths(subject, Clipper2Lib::JoinType::Round, Clipper2Lib::EndType::Polygon); Clipper2Lib::Paths64 solution; co.Execute(5, solution); - ASSERT_EQ(solution.size(), 2); // When offsetting, output orientation should match input EXCEPT when ReverseSolution == true - // However, input path ORDER may not match output path order. For example, order will change + // However, input path ORDER may not match output path order. For example, order will change // whenever inner paths (holes) are defined before their container outer paths (as above). - // And when offsetting multiple outer paths, their order will likely change too. Due to the + // And when offsetting multiple outer paths, their order will likely change too. Due to the // sweep-line algorithm used, paths with larger Y coordinates will likely be listed first. EXPECT_TRUE(Clipper2Lib::IsPositive(subject[1]) != Clipper2Lib::IsPositive(solution[0])); -} - +} \ No newline at end of file diff --git a/CPP/Tests/TestOffsets.cpp b/CPP/Tests/TestOffsets.cpp index d5805e65..c0e81684 100644 --- a/CPP/Tests/TestOffsets.cpp +++ b/CPP/Tests/TestOffsets.cpp @@ -2,31 +2,24 @@ #include "clipper2/clipper.offset.h" #include "ClipFileLoad.h" //#include "clipper.svg.utils.h" - using namespace Clipper2Lib; -TEST(Clipper2Tests, TestOffsets) { +TEST(Clipper2Tests, TestOffsets) { std::ifstream ifs("Offsets.txt"); if(!ifs.good()) return; - for (int test_number = 1; test_number <= 2; ++test_number) { ClipperOffset co; - Paths64 subject, subject_open, clip; Paths64 solution, solution_open; ClipType ct = ClipType::None; FillRule fr = FillRule::NonZero; int64_t stored_area = 0, stored_count = 0; - ASSERT_TRUE(LoadTestNum(ifs, test_number, subject, subject_open, clip, stored_area, stored_count, ct, fr)); - co.AddPaths(subject, JoinType::Round, EndType::Polygon); Paths64 outputs; co.Execute(1, outputs); - // is the sum total area of the solution is positive const auto outer_is_positive = Area(outputs) > 0; - // there should be exactly one exterior path const auto is_positive_func = IsPositive; const auto is_positive_count = std::count_if( @@ -40,8 +33,6 @@ TEST(Clipper2Tests, TestOffsets) { } ifs.close(); } - - static Point64 MidPoint(const Point64& p1, const Point64& p2) { Point64 result; @@ -49,22 +40,16 @@ static Point64 MidPoint(const Point64& p1, const Point64& p2) result.y = (p1.y + p2.y) / 2; return result; } - TEST(Clipper2Tests, TestOffsets2) { // see #448 & #456 - double scale = 10, delta = 10 * scale, arc_tol = 0.25 * scale; - Paths64 subject, solution; ClipperOffset c; subject.push_back(MakePath({ 50,50, 100,50, 100,150, 50,150, 0,100 })); - int err; subject = ScalePaths(subject, scale, err); - c.AddPaths(subject, JoinType::Round, EndType::Polygon); c.ArcTolerance(arc_tol); c.Execute(delta, solution); - double min_dist = delta * 2, max_dist = 0; for (const Point64& subjPt : subject[0]) { @@ -81,11 +66,9 @@ TEST(Clipper2Tests, TestOffsets2) { // see #448 & #456 prevPt = pt; } } - EXPECT_GE(min_dist + 1, delta - arc_tol); // +1 for rounding errors - EXPECT_LE(solution[0].size(), 21); + EXPECT_LE(solution[0].size(), 21); } - TEST(Clipper2Tests, TestOffsets3) // see #424 { Paths64 subjects = {{ @@ -108,11 +91,9 @@ TEST(Clipper2Tests, TestOffsets3) // see #424 {1602758902, 1378489467}, {1618990858, 1376350372}, {1615058698, 1344085688}, {1603230761, 1345700495}, {1598648484, 1346329641}, {1598931599, 1348667965}, {1596698132, 1348993024}, {1595775386, 1342722540} }}; - Paths64 solution = InflatePaths(subjects, -209715, JoinType::Miter, EndType::Polygon); EXPECT_LE(solution[0].size() - subjects[0].size(), 1); } - TEST(Clipper2Tests, TestOffsets4) // see #482 { Paths64 paths = { { {0, 0}, {20000, 200}, @@ -121,21 +102,18 @@ TEST(Clipper2Tests, TestOffsets4) // see #482 JoinType::Square, EndType::Polygon); //std::cout << solution[0].size() << std::endl; EXPECT_EQ(solution[0].size(), 5); - paths = { { {0, 0}, {20000, 400}, {40000, 0}, {40000, 50000}, {0, 50000}, {0, 0}} }; solution = InflatePaths(paths, -5000, JoinType::Square, EndType::Polygon); //std::cout << solution[0].size() << std::endl; EXPECT_EQ(solution[0].size(), 5); - paths = { { {0, 0}, {20000, 400}, {40000, 0}, {40000, 50000}, {0, 50000}, {0, 0}} }; solution = InflatePaths(paths, -5000, JoinType::Round, EndType::Polygon, 2, 100); //std::cout << solution[0].size() << std::endl; EXPECT_GT(solution[0].size(), 5); - paths = { { {0, 0}, {20000, 1500}, {40000, 0}, {40000, 50000}, {0, 50000}, {0, 0}} }; solution = InflatePaths(paths, -5000, @@ -143,7 +121,6 @@ TEST(Clipper2Tests, TestOffsets4) // see #482 //std::cout << solution[0].size() << std::endl; EXPECT_GT(solution[0].size(), 5); } - TEST(Clipper2Tests, TestOffsets5) // modified from #593 (tests offset clean up) { Paths64 subject = { @@ -368,17 +345,13 @@ TEST(Clipper2Tests, TestOffsets5) // modified from #593 (tests offset clean up) }), MakePath({ -47877,-47877, 84788,-47877, 84788,81432, -47877,81432 }) }; - Paths64 solution = InflatePaths(subject, -10000, JoinType::Round, EndType::Polygon); EXPECT_EQ(solution.size(), 2); } - - TEST(Clipper2Tests, TestOffsets6) // also modified from #593 (tests rounded ends) { Paths64 subjects = { {{620,620}, {-620,620}, {-620,-620}, {620,-620}}, - {{20,-277}, {42,-275}, {59,-272}, {80,-266}, {97,-261}, {114,-254}, {135,-243}, {149,-235}, {167,-222}, {182,-211}, {197,-197}, {212,-181}, {223,-167}, {234,-150}, {244,-133}, {253,-116}, @@ -387,53 +360,40 @@ TEST(Clipper2Tests, TestOffsets6) // also modified from #593 (tests rounded ends {223,-167}, {212,-181}, {197,-197}, {182,-211}, {168,-222}, {152,-233}, {135,-243}, {114,-254}, {97,-261}, {80,-267}, {59,-272}, {42,-275}, {20,-278}} }; - const double offset = -50; Clipper2Lib::ClipperOffset offseter; Clipper2Lib::Paths64 tmpSubject; - offseter.AddPaths(subjects, Clipper2Lib::JoinType::Round, Clipper2Lib::EndType::Polygon); Clipper2Lib::Paths64 solution; offseter.Execute(offset, solution); - EXPECT_EQ(solution.size(), 2); - double area = Area(solution[1]); EXPECT_LT(area, -47500); } - TEST(Clipper2Tests, TestOffsets7) // (#593 & #715) { Paths64 solution; Paths64 subject = { MakePath({0,0, 100,0, 100,100, 0,100}) }; - solution = InflatePaths(subject, -50, JoinType::Miter, EndType::Polygon); EXPECT_EQ(solution.size(), 0); - subject.push_back(MakePath({ 40,60, 60,60, 60,40, 40,40 })); solution = InflatePaths(subject, 10, JoinType::Miter, EndType::Polygon); EXPECT_EQ(solution.size(), 1); - reverse(subject[0].begin(), subject[0].end()); reverse(subject[1].begin(), subject[1].end()); solution = InflatePaths(subject, 10, JoinType::Miter, EndType::Polygon); EXPECT_EQ(solution.size(), 1); - subject.resize(1); solution = InflatePaths(subject, -50, JoinType::Miter, EndType::Polygon); EXPECT_EQ(solution.size(), 0); } - - struct OffsetQual { - PointD smallestInSub; // smallestInSub & smallestInSol are the points in subject and solution + PointD smallestInSub; // smallestInSub & smallestInSol are the points in subject and solution PointD smallestInSol; // that define the place that most falls short of the expected offset - PointD largestInSub; // largestInSub & largestInSol are the points in subject and solution + PointD largestInSub; // largestInSub & largestInSol are the points in subject and solution PointD largestInSol; // that define the place that most exceeds the expected offset }; - - template inline PointD GetClosestPointOnSegment(const PointD& offPt, const Point& seg1, const Point& seg2) @@ -450,18 +410,14 @@ inline PointD GetClosestPointOnSegment(const PointD& offPt, static_cast(seg1.x) + (q * dx), static_cast(seg1.y) + (q * dy)); } - - template static OffsetQual GetOffsetQuality(const Path& subject, const Path& solution, const double delta) { if (!subject.size() || !solution.size()) return OffsetQual(); - double desiredDistSqr = delta * delta; double smallestSqr = desiredDistSqr, largestSqr = desiredDistSqr; double deviationsSqr = 0; OffsetQual oq; - const size_t subVertexCount = 4; // 1 .. 100 :) const double subVertexFrac = 1.0 / subVertexCount; Point solPrev = solution[solution.size() - 1]; @@ -469,11 +425,10 @@ static OffsetQual GetOffsetQuality(const Path& subject, const Path& soluti { for (size_t i = 0; i < subVertexCount; ++i) { - // divide each edge in solution into series of sub-vertices (solPt), + // divide each edge in solution into series of sub-vertices (solPt), PointD solPt = PointD( static_cast(solPrev.x) + static_cast(solPt0.x - solPrev.x) * subVertexFrac * i, static_cast(solPrev.y) + static_cast(solPt0.y - solPrev.y) * subVertexFrac * i); - // now find the closest point in subject to each of these solPt. PointD closestToSolPt; double closestDistSqr = std::numeric_limits::infinity(); @@ -488,12 +443,10 @@ static OffsetQual GetOffsetQuality(const Path& subject, const Path& soluti closestToSolPt = closestPt; }; } - // we've now found solPt's closest pt in subject (closestToSolPt). // but how does the distance between these 2 points compare with delta // ideally - Distance(closestToSolPt, solPt) == delta; - - // see how this distance compares with every other solPt + // see how this distance compares with every other solPt if (closestDistSqr < smallestSqr) { smallestSqr = closestDistSqr; oq.smallestInSub = closestToSolPt; @@ -509,7 +462,6 @@ static OffsetQual GetOffsetQuality(const Path& subject, const Path& soluti } return oq; } - TEST(Clipper2Tests, TestOffsets8) // (#724) { Paths64 subject = { MakePath({ @@ -593,16 +545,13 @@ TEST(Clipper2Tests, TestOffsets8) // (#724) 124605260, -29584205, 119475951, -35589856, 113470300, -40719165, 106736189, -44845834, 99439432, -47868251, 91759700, -49711991 }) }; - double offset = -50329979.277800001, arc_tol = 5000; - Paths64 solution = InflatePaths(subject, offset, JoinType::Round, EndType::Polygon, 2, arc_tol); OffsetQual oq = GetOffsetQuality(subject[0], solution[0], offset); double smallestDist = Distance(oq.smallestInSub, oq.smallestInSol); double largestDist = Distance(oq.largestInSub, oq.largestInSol); const double rounding_tolerance = 1.0; offset = std::abs(offset); - //std::cout << std::setprecision(0) << std::fixed; //std::cout << "Expected delta : " << offset << std::endl; //std::cout << "Smallest delta : " << smallestDist << " (" << smallestDist - offset << ")" << std::endl; @@ -615,52 +564,42 @@ TEST(Clipper2Tests, TestOffsets8) // (#724) //SvgAddSolution(svg, solution, FillRule::NonZero, false); //std::string filename = "offset_test.svg"; //SvgSaveToFile(svg, filename, 800, 600, 10); - EXPECT_LE(offset - smallestDist - rounding_tolerance, arc_tol); EXPECT_LE(largestDist - offset - rounding_tolerance, arc_tol); } - - TEST(Clipper2Tests, TestOffsets9) // (#733) { - // solution orientations should match subject orientations UNLESS + // solution orientations should match subject orientations UNLESS // reverse_solution is set true in ClipperOffset's constructor - // start subject's orientation positive ... Paths64 subject{ MakePath({100,100, 200,100, 200, 400, 100, 400}) }; Paths64 solution = InflatePaths(subject, 50, JoinType::Miter, EndType::Polygon); EXPECT_EQ(solution.size(), 1); EXPECT_TRUE(IsPositive(solution[0])); - - // reversing subject's orientation should not affect delta direction + // reversing subject's orientation should not affect delta direction // (ie where positive deltas inflate). std::reverse(subject[0].begin(), subject[0].end()); solution = InflatePaths(subject, 50, JoinType::Miter, EndType::Polygon); EXPECT_EQ(solution.size(), 1); EXPECT_TRUE(std::fabs(Area(solution[0])) > std::fabs(Area(subject[0]))); EXPECT_FALSE(IsPositive(solution[0])); - ClipperOffset co(2, 0, false, true); // last param. reverses solution co.AddPaths(subject, JoinType::Miter, EndType::Polygon); co.Execute(50, solution); EXPECT_EQ(solution.size(), 1); EXPECT_TRUE(std::fabs(Area(solution[0])) > std::fabs(Area(subject[0]))); EXPECT_TRUE(IsPositive(solution[0])); - // add a hole (ie has reverse orientation to outer path) subject.push_back( MakePath({130,130, 170,130, 170,370, 130,370}) ); solution = InflatePaths(subject, 30, JoinType::Miter, EndType::Polygon); EXPECT_EQ(solution.size(), 1); EXPECT_FALSE(IsPositive(solution[0])); - co.Clear(); // should still reverse solution orientation co.AddPaths(subject, JoinType::Miter, EndType::Polygon); co.Execute(30, solution); EXPECT_EQ(solution.size(), 1); EXPECT_TRUE(std::fabs(Area(solution[0])) > std::fabs(Area(subject[0]))); EXPECT_TRUE(IsPositive(solution[0])); - solution = InflatePaths(subject, -15, JoinType::Miter, EndType::Polygon); EXPECT_EQ(solution.size(), 0); - -} +} \ No newline at end of file diff --git a/CPP/Tests/TestOrientation.cpp b/CPP/Tests/TestOrientation.cpp index 65a38980..f1cc3eb4 100644 --- a/CPP/Tests/TestOrientation.cpp +++ b/CPP/Tests/TestOrientation.cpp @@ -1,10 +1,7 @@ #include #include "clipper2/clipper.h" - using namespace Clipper2Lib; - 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 })); @@ -13,7 +10,6 @@ TEST(Clipper2Tests, TestNegativeOrientation) { EXPECT_FALSE(IsPositive(subjects[1])); clips.push_back(MakePath({ 50,50, 50,150, 150,150, 150,50 })); EXPECT_FALSE(IsPositive(clips[0])); - solution = Union(subjects, clips, FillRule::Negative); ASSERT_EQ(solution.size(), 1); EXPECT_EQ(solution[0].size(), 12); diff --git a/CPP/Tests/TestPolygons.cpp b/CPP/Tests/TestPolygons.cpp index 068dd318..6910cebd 100644 --- a/CPP/Tests/TestPolygons.cpp +++ b/CPP/Tests/TestPolygons.cpp @@ -1,7 +1,6 @@ #include #include "clipper2/clipper.h" #include "ClipFileLoad.h" - inline Clipper2Lib::PathD MakeRandomPath(int width, int height, unsigned vertCnt) { Clipper2Lib::PathD result; @@ -10,27 +9,21 @@ inline Clipper2Lib::PathD MakeRandomPath(int width, int height, unsigned vertCnt result.push_back(Clipper2Lib::PointD(double(rand()) / RAND_MAX * width, double(rand()) / RAND_MAX * height)); return result; } - template inline bool IsInList(int num, const int (&intArray)[N]) -{ +{ const int* list = &intArray[0]; for (int cnt = N; cnt; --cnt) if (num == *list++) return true; return false; } - TEST(Clipper2Tests, TestMultiplePolygons) { std::ifstream ifs("Polygons.txt"); - - ASSERT_TRUE(ifs); ASSERT_TRUE(ifs.good()); - const int start_num = 1; const int end_num = 1000; - int test_number = start_num; while (test_number <= end_num) { @@ -39,20 +32,16 @@ TEST(Clipper2Tests, TestMultiplePolygons) Clipper2Lib::ClipType ct; Clipper2Lib::FillRule fr; int64_t stored_area, stored_count; - - if (!LoadTestNum(ifs, test_number, + if (!LoadTestNum(ifs, test_number, subject, subject_open, clip, stored_area, stored_count, ct, fr)) break; - // check Paths64 solutions Clipper2Lib::Clipper64 c; c.AddSubject(subject); c.AddOpenSubject(subject_open); c.AddClip(clip); c.Execute(ct, fr, solution, solution_open); - const int64_t measured_area = static_cast(Area(solution)); - const int64_t measured_count = static_cast(solution.size() + solution_open.size()); - + const int64_t measured_count = static_cast(solution.size() + solution_open.size()); // check the polytree variant too Clipper2Lib::PolyTree64 solution_polytree; Clipper2Lib::Paths64 solution_polytree_open; @@ -61,13 +50,11 @@ TEST(Clipper2Tests, TestMultiplePolygons) clipper_polytree.AddOpenSubject(subject_open); clipper_polytree.AddClip(clip); clipper_polytree.Execute(ct, fr, solution_polytree, solution_polytree_open); - - const int64_t measured_area_polytree = + const int64_t measured_area_polytree = static_cast(solution_polytree.Area()); const auto solution_polytree_paths = PolyTreeToPaths64(solution_polytree); - const int64_t measured_count_polytree = + const int64_t measured_count_polytree = static_cast(solution_polytree_paths.size()); - // check polygon counts if (stored_count <= 0) ; // skip count @@ -82,7 +69,6 @@ TEST(Clipper2Tests, TestMultiplePolygons) EXPECT_NEAR(measured_count, stored_count, 1) << " in test " << test_number; else EXPECT_EQ(measured_count, stored_count) << " in test " << test_number; - // check polygon areas if (stored_area <= 0) ; // skip area @@ -100,31 +86,24 @@ TEST(Clipper2Tests, TestMultiplePolygons) EXPECT_NEAR(measured_area, stored_area, 0.02 * measured_area) << " in test " << test_number; else EXPECT_NEAR(measured_area, stored_area, 0.01 * measured_area) << " in test " << test_number; - - EXPECT_EQ(measured_count, measured_count_polytree) + EXPECT_EQ(measured_count, measured_count_polytree) << " in test " << test_number; - EXPECT_EQ(measured_area, measured_area_polytree) + EXPECT_EQ(measured_area, measured_area_polytree) << " in test " << test_number; - ++test_number; } //EXPECT_GE(test_number, 188); - Clipper2Lib::PathsD subjd, clipd, solutiond; Clipper2Lib::FillRule frd = Clipper2Lib::FillRule::NonZero; } - TEST(Clipper2Tests, TestHorzSpikes) //#720 { Clipper2Lib::Paths64 paths = { Clipper2Lib::MakePath({1600,0, 1600,100, 2050,100, 2050,300, 450,300, 450, 0}), Clipper2Lib::MakePath({1800,200, 1800,100, 1600,100, 2000,100, 2000,200}) }; - std::cout << paths << std::endl; - - Clipper2Lib::Clipper64 c; + Clipper2Lib::Clipper64 c; c.AddSubject(paths); c.Execute(Clipper2Lib::ClipType::Union, Clipper2Lib::FillRule::NonZero, paths); - EXPECT_GE(paths.size(), 1); -} +} \ No newline at end of file diff --git a/CPP/Tests/TestPolytreeHoles.cpp b/CPP/Tests/TestPolytreeHoles.cpp index 2ad65551..cea52ff5 100644 --- a/CPP/Tests/TestPolytreeHoles.cpp +++ b/CPP/Tests/TestPolytreeHoles.cpp @@ -1,35 +1,27 @@ #include #include "clipper2/clipper.h" #include "ClipFileLoad.h" - using namespace Clipper2Lib; - TEST(Clipper2Tests, TestPolytreeHoles1) { std::ifstream ifs("PolytreeHoleOwner.txt"); ASSERT_TRUE(ifs); ASSERT_TRUE(ifs.good()); - Paths64 subject, subject_open, clip; PolyTree64 solution; Paths64 solution_open; ClipType ct = ClipType::None; FillRule fr = FillRule::EvenOdd; int64_t area = 0, count = 0; - bool success = false; ASSERT_TRUE(LoadTestNum(ifs, 1, subject, subject_open, clip, area, count, ct, fr)); - Clipper64 c; c.AddSubject(subject); c.AddOpenSubject(subject_open); c.AddClip(clip); c.Execute(ct, fr, solution, solution_open); - EXPECT_TRUE(CheckPolytreeFullyContainsChildren(solution)); } - - void PolyPathContainsPoint(const PolyPath64& pp, const Point64 pt, int& counter) { if (pp.Polygon().size() > 0) @@ -43,7 +35,6 @@ void PolyPathContainsPoint(const PolyPath64& pp, const Point64 pt, int& counter) for (const auto& child : pp) PolyPathContainsPoint(*child, pt, counter); } - bool PolytreeContainsPoint(const PolyPath64& pp, const Point64 pt) { int counter = 0; @@ -52,14 +43,12 @@ bool PolytreeContainsPoint(const PolyPath64& pp, const Point64 pt) EXPECT_GE(counter, 0); //ie 'pt' can't be inside more holes than outers return counter != 0; } - void GetPolyPathArea(const PolyPath64& pp, double& area) { area += Area(pp.Polygon()); for (const auto& child : pp) GetPolyPathArea(*child, area); } - double GetPolytreeArea(const PolyPath64& pp) { double result = 0; @@ -67,27 +56,22 @@ double GetPolytreeArea(const PolyPath64& pp) GetPolyPathArea(*child, result); return result; } - TEST(Clipper2Tests, TestPolytreeHoles2) { std::ifstream ifs("PolytreeHoleOwner2.txt"); ASSERT_TRUE(ifs); ASSERT_TRUE(ifs.good()); - Paths64 subject, subject_open, clip; ClipType ct = ClipType::None; FillRule fr = FillRule::EvenOdd; int64_t area = 0, count = 0; - ASSERT_TRUE(LoadTestNum(ifs, 1, subject, subject_open, clip, area, count, ct, fr)); - const std::vector points_of_interest_outside = { Point64(21887, 10420), Point64(21726, 10825), Point64(21662, 10845), Point64(21617, 10890) }; - // confirm that each 'points_of_interest_outside' is outside every subject, for (const auto& poi_outside : points_of_interest_outside) { @@ -97,14 +81,12 @@ TEST(Clipper2Tests, TestPolytreeHoles2) ++outside_subject_count; EXPECT_EQ(outside_subject_count, 0); } - const std::vector points_of_interest_inside = { Point64(21887, 10430), Point64(21843, 10520), Point64(21810, 10686), Point64(21900, 10461) }; - // confirm that each 'points_of_interest_inside' is inside a subject, // and inside only one subject (to exclude possible subject holes) for (const auto& poi_inside : points_of_interest_inside) @@ -117,7 +99,6 @@ TEST(Clipper2Tests, TestPolytreeHoles2) } EXPECT_EQ(inside_subject_count, 1); } - PolyTree64 solution_tree; Paths64 solution_open; Clipper64 c; @@ -125,39 +106,28 @@ TEST(Clipper2Tests, TestPolytreeHoles2) c.AddOpenSubject(subject_open); c.AddClip(clip); c.Execute(ct, FillRule::Negative, solution_tree, solution_open); - const auto solution_paths = PolyTreeToPaths64(solution_tree); - ASSERT_FALSE(solution_paths.empty()); - const double subject_area = -Area(subject); //negate (see fillrule) const double solution_tree_area = GetPolytreeArea(solution_tree); const double solution_paths_area = Area(solution_paths); - // 1a. check solution_paths_area is smaller than subject_area EXPECT_LT(solution_paths_area, subject_area); // 1b. but not too much smaller EXPECT_GT(solution_paths_area, (subject_area * 0.92)); - // 2. check solution_tree's area matches solution_paths' area EXPECT_NEAR(solution_tree_area, solution_paths_area, 0.0001); - // 3. check that all children are inside their parents EXPECT_TRUE(CheckPolytreeFullyContainsChildren(solution_tree)); - // 4. confirm all 'point_of_interest_outside' are outside polytree for (const auto& poi_outside : points_of_interest_outside) EXPECT_FALSE(PolytreeContainsPoint(solution_tree, poi_outside)); - // 5. confirm all 'point_of_interest_inside' are inside polytree for (const auto& poi_inside : points_of_interest_inside) EXPECT_TRUE(PolytreeContainsPoint(solution_tree, poi_inside)); - } - TEST(Clipper2Tests, TestPolytreeHoles3) { - Paths64 subject, clip, sol; PolyTree64 solution; Clipper64 c; @@ -167,15 +137,12 @@ TEST(Clipper2Tests, TestPolytreeHoles3) 1025,524, 1038,524, 1042,520, 1038,516, 1025,516, 1021,520, 1000,520, 995,516, 983,516, 978,520, 957,520, 953,516, 940,516, 936,520, 915,520, 911,516, 898,516, 894,520, 870,520, 870,516, 870,501, 870,501, 870,501, 1072,501 })); - clip.push_back(MakePath({ 870,501, 971,501, 971,539, 870,539 })); - c.AddSubject(subject); c.AddClip(clip); c.Execute(ClipType::Intersection, FillRule::NonZero, solution); EXPECT_TRUE(solution.Count() == 1 && solution[0]->Count() == 2); } - TEST(Clipper2Tests, TestPolytreeHoles4) //#618 { Paths64 subject; @@ -186,7 +153,6 @@ TEST(Clipper2Tests, TestPolytreeHoles4) //#618 450,175, 400,175, 400,200, 350,200, 350,175, 200,175, 200,250, 150,250, 150,200, 100,200, 100,300, 50,300, 50,125, 500,125, 500,500 })); subject.push_back(MakePath({ 250,425, 250,375, 300,375, 300,425 })); - c.AddSubject(subject); c.Execute(ClipType::Union, FillRule::NonZero, solution); // Polytree root @@ -197,7 +163,6 @@ TEST(Clipper2Tests, TestPolytreeHoles4) //#618 // +- Hole EXPECT_TRUE(solution.Count() == 1 && solution[0]->Count() == 3); } - TEST(Clipper2Tests, TestPolytreeHoles5) { Paths64 subject, clip; @@ -205,23 +170,16 @@ TEST(Clipper2Tests, TestPolytreeHoles5) clip.push_back(MakePath({ 20,30, 30,30, 30,150, 20,150 })); clip.push_back(MakePath({ 200,0, 300,0, 300,30, 280,30, 280,20, 220,20, 220,30, 200,30 })); clip.push_back(MakePath({ 200,50, 300,50, 300,80, 200,80 })); - Clipper64 c; c.AddSubject(subject); c.AddClip(clip); - PolyTree64 tree; c.Execute(ClipType::Xor, FillRule::NonZero, tree); - ////std::cout << tree << std::endl; //Polytree with 3 polygons. // + -Polygon (2) contains 2 holes. - EXPECT_TRUE(tree.Count() == 3 && tree[2]->Count() == 2); - } - - TEST(Clipper2Tests, TestPolytreeHoles6) //#618 { Paths64 subject, clip; @@ -234,40 +192,26 @@ TEST(Clipper2Tests, TestPolytreeHoles6) //#618 clip.push_back(MakePath({ 0,0, 400,0, 400,50, 0,50 })); clip.push_back(MakePath({ 0,100, 400,100, 400,150, 0,150 })); clip.push_back(MakePath({ 260,175, 325,175, 325,275, 260,275 })); - Clipper64 c; c.AddSubject(subject); c.AddClip(clip); - PolyTree64 tree; c.Execute(ClipType::Xor, FillRule::NonZero, tree); - ////std::cout << tree << std::endl; //Polytree with 3 polygons. // + -Polygon (2) contains 1 holes. - EXPECT_TRUE(tree.Count() == 3 && tree[2]->Count() == 1); } - - TEST(Clipper2Tests, TestPolytreeHoles7) //#618 { Paths64 subject; - subject.push_back(MakePath({ 0, 0, 100000, 0, 100000, 100000, 200000, 100000, + subject.push_back(MakePath({ 0, 0, 100000, 0, 100000, 100000, 200000, 100000, 200000, 0, 300000, 0, 300000, 200000, 0, 200000 })); subject.push_back(MakePath({ 0, 0, 0, -100000, 250000, -100000, 250000, 0 })); - PolyTree64 polytree; Clipper64 c; c.AddSubject(subject); - c.Execute(ClipType::Union, FillRule::NonZero, polytree); //std::cout << polytree << std::endl; - EXPECT_TRUE(polytree.Count() == 1 && polytree[0]->Count() == 1); -} - - - - - +} \ No newline at end of file diff --git a/CPP/Tests/TestPolytreeIntersection.cpp b/CPP/Tests/TestPolytreeIntersection.cpp index 58ff3f56..c20390fb 100644 --- a/CPP/Tests/TestPolytreeIntersection.cpp +++ b/CPP/Tests/TestPolytreeIntersection.cpp @@ -1,30 +1,24 @@ #include #include "clipper2/clipper.h" - using namespace Clipper2Lib; - TEST(Clipper2Tests, TestPolyTreeIntersection) { Clipper64 clipper; - - Paths64 subject; + Paths64 subject; 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 })); clipper.AddClip (clip); - PolyTree64 solution; Paths64 open_paths; - if (IsPositive(subject[0])) clipper.Execute(ClipType::Intersection, FillRule::Positive, solution, open_paths); else clipper.Execute(ClipType::Intersection, FillRule::Negative, solution, open_paths); - EXPECT_EQ(open_paths.size(), 0); ASSERT_EQ(solution.Count(), 1); EXPECT_EQ(solution[0]->Polygon().size(), 4); -} +} \ No newline at end of file diff --git a/CPP/Tests/TestPolytreeUnion.cpp b/CPP/Tests/TestPolytreeUnion.cpp index 4d53da87..e9085dad 100644 --- a/CPP/Tests/TestPolytreeUnion.cpp +++ b/CPP/Tests/TestPolytreeUnion.cpp @@ -1,21 +1,16 @@ #include #include "clipper2/clipper.h" - 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 })); - Clipper64 clipper; clipper.AddSubject(subject); - PolyTree64 solution; Paths64 open_paths; if (IsPositive(subject[0])) - clipper.Execute(ClipType::Union, + clipper.Execute(ClipType::Union, FillRule::Positive, solution, open_paths); else { @@ -24,9 +19,8 @@ TEST(Clipper2Tests, TestPolytreeUnion) { clipper.Execute(ClipType::Union, FillRule::Negative, solution, open_paths); } - EXPECT_EQ(open_paths.size(), 0); ASSERT_EQ(solution.Count(), 1); EXPECT_EQ(solution[0]->Polygon().size(), 8); EXPECT_EQ(IsPositive(subject[0]), IsPositive(solution[0]->Polygon())); -} +} \ No newline at end of file diff --git a/CPP/Tests/TestRandomPaths.cpp b/CPP/Tests/TestRandomPaths.cpp index 6fb0e8df..b0cd1683 100644 --- a/CPP/Tests/TestRandomPaths.cpp +++ b/CPP/Tests/TestRandomPaths.cpp @@ -2,31 +2,25 @@ #include "clipper2/clipper.h" #include #include - int GenerateRandomInt(std::default_random_engine& rng, int min_value, int max_value) { if (min_value == max_value) return min_value; - std::uniform_int_distribution distribution(min_value, max_value); return distribution(rng); } - Clipper2Lib::Paths64 GenerateRandomPaths(std::default_random_engine& rng, int min_path_count, int max_complexity) { std::uniform_int_distribution first_point_coordinate_distribution(-max_complexity, max_complexity * 2); std::uniform_int_distribution difference_to_previous_point_distribution(-5, 5); - const int path_count = GenerateRandomInt(rng, min_path_count, max_complexity); Clipper2Lib::Paths64 result(path_count); - for (int path = 0; path < path_count; ++path) { const int min_point_count = 0; const int path_length = GenerateRandomInt(rng, min_point_count, std::max(min_point_count, max_complexity)); auto& result_path = result[path]; result_path.reserve(path_length); - for (int point = 0; point < path_length; ++point) { if (result_path.empty()) { result_path.emplace_back( @@ -45,11 +39,9 @@ Clipper2Lib::Paths64 GenerateRandomPaths(std::default_random_engine& rng, int mi } return result; } - TEST(Clipper2Tests, TestRandomPaths) { std::default_random_engine rng(42); - #if DEBUG for (int i = 0; i < 10; ++i) #else @@ -57,27 +49,20 @@ TEST(Clipper2Tests, TestRandomPaths) #endif { const auto max_complexity = std::max(1, i / 10); - const auto subject = GenerateRandomPaths(rng, 1, max_complexity); const auto subject_open = GenerateRandomPaths(rng, 0, max_complexity); const auto clip = GenerateRandomPaths(rng, 0, max_complexity); - const Clipper2Lib::ClipType ct = static_cast(GenerateRandomInt(rng, 0, 4)); const Clipper2Lib::FillRule fr = static_cast(GenerateRandomInt(rng, 0, 3)); - //SaveInputToFile(subject, subject_open, clip, ct, fr); - Clipper2Lib::Paths64 solution, solution_open; Clipper2Lib::Clipper64 c; c.AddSubject(subject); c.AddOpenSubject(subject_open); c.AddClip(clip); c.Execute(ct, fr, solution, solution_open); - const int64_t area_paths = static_cast(Area(solution)); const int64_t count_paths = solution.size() + solution_open.size(); - - Clipper2Lib::PolyTree64 solution_polytree; Clipper2Lib::Paths64 solution_polytree_open; Clipper2Lib::Clipper64 clipper_polytree; @@ -89,7 +74,6 @@ TEST(Clipper2Tests, TestRandomPaths) const int64_t area_polytree = static_cast(Area(solution_polytree_paths)); const int64_t count_polytree = solution_polytree_paths.size() + solution_polytree_open.size(); EXPECT_EQ(area_paths, area_polytree); - // polytree does an additional bounds check on each path // and discards paths with empty bounds, so count_polytree // may on occasions be slightly less than count_paths even diff --git a/CPP/Tests/TestRectClip.cpp b/CPP/Tests/TestRectClip.cpp index 2b56d16b..c320a9bc 100644 --- a/CPP/Tests/TestRectClip.cpp +++ b/CPP/Tests/TestRectClip.cpp @@ -1,33 +1,26 @@ #include #include "clipper2/clipper.h" - using namespace Clipper2Lib; - TEST(Clipper2Tests, TestRectClip) { Paths64 sub, clp, sol; Rect64 rect = Rect64(100, 100, 700, 500); clp.push_back(rect.AsPath()); - 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 })); sol = RectClip(rect, sub); EXPECT_TRUE(Area(sol) == Area(sub)); - sub.clear(); 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({ 110,110, 690,110, 690,490, 110,490 })); sol = RectClip(rect, sub); EXPECT_TRUE(Area(sol) == Area(sub)); - sub.clear(); clp.clear(); rect = Rect64(390, 290, 410, 310); @@ -35,17 +28,14 @@ TEST(Clipper2Tests, TestRectClip) 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 })); sol = RectClip(rect, sub); EXPECT_TRUE(sol.empty()); - sub.clear(); 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 })); @@ -54,20 +44,15 @@ TEST(Clipper2Tests, TestRectClip) const auto solBounds = GetBounds(sol); EXPECT_EQ(solBounds.Width(), rect.Width()); EXPECT_EQ(solBounds.Height(), rect.Height()); - } - TEST(Clipper2Tests, TestRectClip2) //#597 { Clipper2Lib::Rect64 rect(54690, 0, 65628, 6000); Clipper2Lib::Paths64 subject {{{700000, 6000}, { 0, 6000 }, { 0, 5925 }, { 700000, 5925 }}}; - Clipper2Lib::Paths64 solution = Clipper2Lib::RectClip(rect, subject); - //std::cout << solution << std::endl; EXPECT_TRUE(solution.size() == 1 && solution[0].size() == 4); } - TEST(Clipper2Tests, TestRectClip3) //#637 { Rect64 r(-1800000000LL, -137573171LL, -1741475021LL, 3355443LL); @@ -75,7 +60,6 @@ TEST(Clipper2Tests, TestRectClip3) //#637 subject.push_back(MakePath({ -1800000000LL, 10005000LL, -1800000000LL, -5000LL, -1789994999LL, -5000LL, -1789994999LL, 10005000LL })); clip.push_back(r.AsPath()); - solution = RectClip(r, subject); //std::cout << solution << std::endl; EXPECT_TRUE(solution.size() == 1); diff --git a/CPP/Tests/TestSimplifyPath.cpp b/CPP/Tests/TestSimplifyPath.cpp index 61e701d4..76f34fdf 100644 --- a/CPP/Tests/TestSimplifyPath.cpp +++ b/CPP/Tests/TestSimplifyPath.cpp @@ -1,14 +1,11 @@ #include "clipper2/clipper.h" #include - using namespace Clipper2Lib; - TEST(Clipper2Tests, TestSimplifyPath) { - Path64 input1 = MakePath({ 0,0, 1,1, 0,20, 0,21, 1,40, 0,41, 0,60, 0,61, 0,80, 1,81, 0,100 }); Path64 output1 = SimplifyPath(input1, 2, false); EXPECT_EQ(Length(output1), 100); EXPECT_EQ(output1.size(), 2); -} +} \ No newline at end of file diff --git a/CPP/Tests/TestTrimCollinear.cpp b/CPP/Tests/TestTrimCollinear.cpp index 94e5da0f..05c5095a 100644 --- a/CPP/Tests/TestTrimCollinear.cpp +++ b/CPP/Tests/TestTrimCollinear.cpp @@ -1,35 +1,27 @@ #include #include "clipper2/clipper.h" - using namespace Clipper2Lib; - TEST(Clipper2Tests, TestTrimCollinear) { - - Path64 input1 = + Path64 input1 = 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 = + Path64 input2 = 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 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 output4a = TrimCollinear(input4); Path64 output4b = TrimCollinear(output4a); - - int area4a = static_cast(Area(output4a)); + int area4a = static_cast(Area(output4a)); int area4b = static_cast(Area(output4b)); EXPECT_EQ(output4a.size(), 7); EXPECT_EQ(area4a, -9); EXPECT_EQ(output4a.size(), output4b.size()); EXPECT_EQ(area4a, area4b); - } \ No newline at end of file diff --git a/CPP/Tests/TestWindows.cpp b/CPP/Tests/TestWindows.cpp index 73d6d56e..980ae35d 100644 --- a/CPP/Tests/TestWindows.cpp +++ b/CPP/Tests/TestWindows.cpp @@ -1,8 +1,5 @@ // see: https://github.com/AngusJohnson/Clipper2/issues/601 - #ifdef WIN32 - #include #include "clipper2/clipper.h" - #endif // WIN32 \ No newline at end of file From 385bd371db14e06860c4ec12f9a530bd31ebbc4d Mon Sep 17 00:00:00 2001 From: angusj Date: Sun, 3 Dec 2023 19:38:25 +1000 Subject: [PATCH 07/64] Added new GetIntersectPoint benchmark test Updated other benchmark tests. --- CPP/BenchMark/CMakeLists.txt | 1 + CPP/BenchMark/GetIntersectPtBenchmark.cpp | 307 ++++++++++++++++++++++ CPP/BenchMark/PointInPolygonBenchmark.cpp | 76 +++--- CPP/BenchMark/StripDuplicateBenchmark.cpp | 90 +++---- 4 files changed, 382 insertions(+), 92 deletions(-) create mode 100644 CPP/BenchMark/GetIntersectPtBenchmark.cpp diff --git a/CPP/BenchMark/CMakeLists.txt b/CPP/BenchMark/CMakeLists.txt index e305c5d7..e163369f 100644 --- a/CPP/BenchMark/CMakeLists.txt +++ b/CPP/BenchMark/CMakeLists.txt @@ -23,6 +23,7 @@ set(gtest_force_shared_crt ON CACHE BOOL "" FORCE) message("fetching is done") set(benchmark_srcs + GetIntersectPtBenchmark.cpp PointInPolygonBenchmark.cpp StripDuplicateBenchmark.cpp # more to add diff --git a/CPP/BenchMark/GetIntersectPtBenchmark.cpp b/CPP/BenchMark/GetIntersectPtBenchmark.cpp new file mode 100644 index 00000000..a445a176 --- /dev/null +++ b/CPP/BenchMark/GetIntersectPtBenchmark.cpp @@ -0,0 +1,307 @@ +#include "benchmark/benchmark.h" +#include "clipper2/clipper.h" +#include "clipper2/clipper.core.h" +#include "CommonUtils.h" +#include "ClipFileLoad.h" +#include +#include +#include +#include + +using namespace Clipper2Lib; + +enum ConsoleTextColor { + reset = 0, + //normal text colors ... + red = 31, green = 32, yellow = 33, blue = 34, magenta = 35, cyan = 36, white = 37, + //bold text colors ... + red_bold = 91, green_bold = 92, yellow_bold = 93, blue_bold = 94, + magenta_bold = 95, cyan_bold = 96, white_bold = 97 +}; + +////////////////////////////////////////////////////////////////////////////////////// +// SetConsoleTextColor: a simple class to adjust Console Text Colors (Windows & Linux) +////////////////////////////////////////////////////////////////////////////////////// + +struct SetConsoleTextColor +{ +private: + ConsoleTextColor _color; +public: + SetConsoleTextColor(ConsoleTextColor color) : _color(color) {}; + + static friend std::ostream& operator<< (std::ostream& out, SetConsoleTextColor const& scc) + { + return out << "\x1B[" << scc._color << "m"; + } +}; +////////////////////////////////////////////////////////////////////////////////////// + + +typedef std::function GipFunction; + +///////////////////////////////////////////////////////// +// GIP1: This is the current Clipper2 PointInPolygon code +///////////////////////////////////////////////////////// +static bool GIP1(const Point64& ln1a, const Point64& ln1b, + const Point64& ln2a, const Point64& ln2b, Point64& ip) +{ + double dx1 = static_cast(ln1b.x - ln1a.x); + double dy1 = static_cast(ln1b.y - ln1a.y); + double dx2 = static_cast(ln2b.x - ln2a.x); + double dy2 = static_cast(ln2b.y - ln2a.y); + + double det = dy1 * dx2 - dy2 * dx1; + if (det == 0.0) return false; + double t = ((double)(ln1a.x - ln2a.x) * dy2 - (double)(ln1a.y - ln2a.y) * dx2) / det; + if (t <= 0.0) ip = ln1a; // ?? check further (see also #568) + else if (t >= 1.0) ip = ln1b; // ?? check further + else + { + ip.x = static_cast(ln1a.x + t * dx1); + ip.y = static_cast(ln1a.y + t * dy1); + } + return true; +} + +///////////////////////////////////////////////////////// +// GIP2: This is mathVertexLineLineIntersection_F +// https://github.com/AngusJohnson/Clipper2/issues/317#issuecomment-1314023253 +///////////////////////////////////////////////////////// +#define CC_MIN(x,y) ((x)>(y)?(y):(x)) +#define CC_MAX(x,y) ((x)<(y)?(y):(x)) +#define DETECT_HIT_OVERFLOW (0) +static bool GIP2(const Point64& ln1a, const Point64& ln1b, + const Point64& ln2a, const Point64& ln2b, Point64& ip) +{ + double ln1dy = (double)(ln1b.y - ln1a.y); + double ln1dx = (double)(ln1a.x - ln1b.x); + double ln2dy = (double)(ln2b.y - ln2a.y); + double ln2dx = (double)(ln2a.x - ln2b.x); + double det = (ln2dy * ln1dx) - (ln1dy * ln2dx); + if (det == 0.0) return 0; + int64_t bb0minx = CC_MIN(ln1a.x, ln1b.x); + int64_t bb0miny = CC_MIN(ln1a.y, ln1b.y); + int64_t bb0maxx = CC_MAX(ln1a.x, ln1b.x); + int64_t bb0maxy = CC_MAX(ln1a.y, ln1b.y); + int64_t bb1minx = CC_MIN(ln2a.x, ln2b.x); + int64_t bb1miny = CC_MIN(ln2a.y, ln2b.y); + int64_t bb1maxx = CC_MAX(ln2a.x, ln2b.x); + int64_t bb1maxy = CC_MAX(ln2a.y, ln2b.y); + int64_t originx = (CC_MIN(bb0maxx, bb1maxx) + CC_MAX(bb0minx, bb1minx)) >> 1; + int64_t originy = (CC_MIN(bb0maxy, bb1maxy) + CC_MAX(bb0miny, bb1miny)) >> 1; + double ln0c = (ln1dy * (double)(ln1a.x - originx)) + (ln1dx * (double)(ln1a.y - originy)); + double ln1c = (ln2dy * (double)(ln2a.x - originx)) + (ln2dx * (double)(ln2a.y - originy)); + double hitx = ((ln1dx * ln1c) - (ln2dx * ln0c)) / det; + double hity = ((ln2dy * ln0c) - (ln1dy * ln1c)) / det; +#if DETECT_HIT_OVERFLOW + if (fmax(fabs((double)originx + hitx), + fabs((double)originy + hity)) >= (double)(INT64_MAX - 1)) return 0; +#endif + ip.x = originx + (int64_t)nearbyint(hitx); + ip.y = originy + (int64_t)nearbyint(hity); + return 1; +} + +///////////////////////////////////////////////////////// +// GIP3: mathVertexLineLineIntersection_F but without calling nearbyint +// https://github.com/AngusJohnson/Clipper2/issues/317#issuecomment-1314023253 +///////////////////////////////////////////////////////// +#define CC_MIN(x,y) ((x)>(y)?(y):(x)) +#define CC_MAX(x,y) ((x)<(y)?(y):(x)) +#define DETECT_HIT_OVERFLOW (0) +static bool GIP3(const Point64& ln1a, const Point64& ln1b, + const Point64& ln2a, const Point64& ln2b, Point64& ip) +{ + double ln1dy = (double)(ln1b.y - ln1a.y); + double ln1dx = (double)(ln1a.x - ln1b.x); + double ln2dy = (double)(ln2b.y - ln2a.y); + double ln2dx = (double)(ln2a.x - ln2b.x); + double det = (ln2dy * ln1dx) - (ln1dy * ln2dx); + if (det == 0.0) return 0; + int64_t bb0minx = CC_MIN(ln1a.x, ln1b.x); + int64_t bb0miny = CC_MIN(ln1a.y, ln1b.y); + int64_t bb0maxx = CC_MAX(ln1a.x, ln1b.x); + int64_t bb0maxy = CC_MAX(ln1a.y, ln1b.y); + int64_t bb1minx = CC_MIN(ln2a.x, ln2b.x); + int64_t bb1miny = CC_MIN(ln2a.y, ln2b.y); + int64_t bb1maxx = CC_MAX(ln2a.x, ln2b.x); + int64_t bb1maxy = CC_MAX(ln2a.y, ln2b.y); + int64_t originx = (CC_MIN(bb0maxx, bb1maxx) + CC_MAX(bb0minx, bb1minx)) >> 1; + int64_t originy = (CC_MIN(bb0maxy, bb1maxy) + CC_MAX(bb0miny, bb1miny)) >> 1; + double ln0c = (ln1dy * (double)(ln1a.x - originx)) + (ln1dx * (double)(ln1a.y - originy)); + double ln1c = (ln2dy * (double)(ln2a.x - originx)) + (ln2dx * (double)(ln2a.y - originy)); + double hitx = ((ln1dx * ln1c) - (ln2dx * ln0c)) / det; + double hity = ((ln2dy * ln0c) - (ln1dy * ln1c)) / det; +#if DETECT_HIT_OVERFLOW + if (fmax(fabs((double)originx + hitx), + fabs((double)originy + hity)) >= (double)(INT64_MAX - 1)) return 0; +#endif + ip.x = originx + static_cast(hitx); + ip.y = originy + static_cast(hity); + //ip.x = originx + (int64_t)nearbyint(hitx); + //ip.y = originy + (int64_t)nearbyint(hity); + return 1; +} + +struct TestRecord +{ +public: + Point64 ideal, pt1, pt2, pt3, pt4; + Path64 results; + TestRecord(int participants, const Point64& ip, + const Point64& p1, const Point64& p2, const Point64& p3, const Point64& p4) : + ideal(ip), pt1(p1), pt2(p2), pt3(p3), pt4(p4) { results.resize(participants); }; +}; + +typedef std::unique_ptr TestRecord_ptr; + +// global data +std::vector tests; +typedef std::vector::const_iterator test_iter; + +inline GipFunction GetGipFunc(int index) +{ + GipFunction result; + switch (index) + { + case 0: result = GIP1; break; + case 1: result = GIP3; break; + //case 2: result = GIP3; break; + default: throw "oops! - wrong function!"; + } + return result; +} + +static inline Point64 ReflectPoint(const Point64& pt, const Point64& pivot) +{ + return Point64(pivot.x + (pivot.x - pt.x), pivot.y + (pivot.y - pt.y)); +} + +static inline Point64 MidPoint(const Point64& p1, const Point64& p2) +{ + Point64 result; + result.x = (p1.x + p2.x) / 2; + result.y = (p1.y + p2.y) / 2; + return result; +} + +static inline Point64 MakeRandomPoint(int64_t min_x, int64_t max_x, int64_t min_y, int64_t max_y) +{ + std::random_device rd; + std::mt19937 gen(rd()); + std::uniform_int_distribution x(min_x, max_x); + std::uniform_int_distribution y(min_y, max_y); + return Point64(x(gen), y(gen)); +} + +///////////////////////////////////////////////////////// +// Benchmark callback functions +///////////////////////////////////////////////////////// + +static void BM_GIP(benchmark::State& state) +{ + Point64 ip; + int idx = (int)state.range(0); + GipFunction gip_func = GetGipFunc(idx); + for (auto _ : state) + { + test_iter cit = tests.cbegin(); + for (; cit != tests.cend(); ++cit) + { + gip_func((*cit)->pt1, (*cit)->pt2, (*cit)->pt3, (*cit)->pt4, ip); + (*cit)->results[idx] = ip; + } + } +} + +///////////////////////////////////////////////////////// +// Main Entry +///////////////////////////////////////////////////////// + +int main(int argc, char** argv) +{ + + const int participants = 2; + //setup benchmarking ... + benchmark::Initialize(0, nullptr); + BENCHMARK(BM_GIP)->Args(std::vector{0}); + BENCHMARK(BM_GIP)->Args(std::vector{1}); + + bool first_pass = true; + for (int power10 = 8; power10 <= 18; power10 += 2) + { + int64_t max_coord = static_cast(pow(10, power10)); + for (int64_t i = 0; i < 10000; ++i) + { + Point64 ip1 = MakeRandomPoint(-max_coord, max_coord, -max_coord, max_coord); + Point64 ip2 = MakeRandomPoint(-max_coord, max_coord, -max_coord, max_coord); + Point64 ip = MidPoint(ip1, ip2); + Point64 ip3 = MakeRandomPoint(-max_coord, max_coord, -max_coord, max_coord); + Point64 ip4 = ReflectPoint(ip3, ip); + Point64 _; + // excluding any segments that are collinear + if (GIP1(ip1, ip2, ip3, ip4, _)) + tests.push_back(std::make_unique(participants, ip, ip1, ip2, ip3, ip4)); + } + + // only benchmark the GetIntersectPoint functions once because + // the size of coordinate values won't affect their performance. + if (first_pass) + { + first_pass = false; + std::cout << std::endl << SetConsoleTextColor(green_bold) << + "Benchmarking GetIntersectPoint performance ... " << SetConsoleTextColor(reset) << + std::endl << std::endl; + benchmark::RunSpecifiedBenchmarks(benchmark::CreateDefaultDisplayReporter()); + + std::cout << std::endl << std::endl << SetConsoleTextColor(green_bold) << + "Now comparing function accuracy ..." << SetConsoleTextColor(white_bold) << std::endl << + "and showing how it deteriorates when using very large coordinate ranges." << + SetConsoleTextColor(reset) << std::endl << + "Distance error is the distance between the calculated and actual intersection points." << std::endl << + "nb: The largest errors will occur whenever intersecting edges are very close to collinear." << std::endl; + } + else + { + for (int i = 0; i < participants; ++i) + { + Point64 ip; + GipFunction gip_func = GetGipFunc(i); + test_iter cit = tests.cbegin(); + for (; cit != tests.cend(); ++cit) + { + gip_func((*cit)->pt1, (*cit)->pt2, (*cit)->pt3, (*cit)->pt4, ip); + (*cit)->results[i] = ip; + } + } + } + + double avg_dists[participants] = { 0 }; + double worst_dists[participants] = { 0 }; + + std::vector::const_iterator cit = tests.cbegin(); + for (; cit != tests.cend(); ++cit) + for (int i = 0; i < participants; ++i) + { + double dist = Distance((*cit)->ideal, (*cit)->results[i]); + avg_dists[i] += dist; + if (dist > worst_dists[i]) worst_dists[i] = dist; + } + + + std::cout << std::endl << SetConsoleTextColor(cyan_bold) << + "Coordinate ranges between +/- 10^" << power10 << + SetConsoleTextColor(reset) << std::endl; + + for (int i = 0; i < participants; ++i) + { + avg_dists[i] /= tests.size(); + std::cout << std::fixed << "GIP" << i << ": average distance error = " << std::setprecision(2) << + avg_dists[i] << "; largest dist. = " << std::setprecision(0) << worst_dists[i] << std::endl; + } + tests.clear(); + } + return 0; +} diff --git a/CPP/BenchMark/PointInPolygonBenchmark.cpp b/CPP/BenchMark/PointInPolygonBenchmark.cpp index 394ca098..550f8f29 100644 --- a/CPP/BenchMark/PointInPolygonBenchmark.cpp +++ b/CPP/BenchMark/PointInPolygonBenchmark.cpp @@ -41,12 +41,11 @@ typedef std::function PipFu ///////////////////////////////////////////////////////// // PIP1: This is the current Clipper2 PointInPolygon code ///////////////////////////////////////////////////////// -template -inline PointInPolygonResult PIP1(const Point& pt, const Path& polygon) +inline PointInPolygonResult PIP1(const Point64& pt, const Path64& polygon) { int val = 0; - typename Path::const_iterator cbegin = polygon.cbegin(), first = cbegin, curr, prev; - typename Path::const_iterator cend = polygon.cend(); + typename Path64::const_iterator cbegin = polygon.cbegin(), first = cbegin, curr, prev; + typename Path64::const_iterator cend = polygon.cend(); while (first != cend && first->y == pt.y) ++first; if (first == cend) // not a proper polygon @@ -128,15 +127,14 @@ inline PointInPolygonResult PIP1(const Point& pt, const Path& polygon) // current Clipper2 PointInPolygon code. It's a little // simpler and also marginally faster. ///////////////////////////////////////////////////////// -template -inline PointInPolygonResult PIP2(const Point& pt, const Path& polygon) +inline PointInPolygonResult PIP2(const Point64& pt, const Path64& polygon) { if (!polygon.size()) return PointInPolygonResult::IsOutside; - Path::const_iterator cend = polygon.cend(); - Path::const_iterator last = cend - 1; - Path::const_iterator first = polygon.cbegin(); - Path::const_iterator curr = first; - Path::const_iterator prev = last; + Path64::const_iterator cend = polygon.cend(); + Path64::const_iterator last = cend - 1; + Path64::const_iterator first = polygon.cbegin(); + Path64::const_iterator curr = first; + Path64::const_iterator prev = last; bool is_above; if (prev->y == pt.y) @@ -145,7 +143,7 @@ inline PointInPolygonResult PIP2(const Point& pt, const Path& polygon) if ((curr->y == pt.y) && ((curr->x == pt.x) || ((pt.x > prev->x) == (pt.x < curr->x)))) return PointInPolygonResult::IsOn; - Path::const_reverse_iterator pr = polygon.crbegin() +1; + Path64::const_reverse_iterator pr = polygon.crbegin() +1; while (pr != polygon.crend() && pr->y == pt.y) ++pr; is_above = pr == polygon.crend() || pr->y < pt.y; } @@ -203,14 +201,13 @@ inline PointInPolygonResult PIP2(const Point& pt, const Path& polygon) // by Jianqiang Hao et al. // Symmetry 2018, 10(10), 477; https://doi.org/10.3390/sym10100477 ///////////////////////////////////////////////////////// -template -static PointInPolygonResult PIP3(const Point &pt, const Path &path) +static PointInPolygonResult PIP3(const Point64&pt, const Path64&path) { if (!path.size()) return PointInPolygonResult::IsOutside; - T x1, y1, x2, y2; + int64_t x1, y1, x2, y2; int k = 0; - Path::const_iterator itPrev = path.cend() - 1; - Path::const_iterator itCurr = path.cbegin(); + Path64::const_iterator itPrev = path.cend() - 1; + Path64::const_iterator itCurr = path.cbegin(); for ( ; itCurr != path.cend(); ++itCurr) { y1 = itPrev->y - pt.y; @@ -299,7 +296,7 @@ static void CustomArguments(benchmark::internal::Benchmark* b) inline PipFunction GetPIPFunc(int index) { - PointInPolygonResult(*result)(const Point64&, const Path64&); + PipFunction result; switch (index) { case 0: result = PIP1; break; @@ -386,6 +383,7 @@ static void DoErrorTest2(int index) std::cout << " No errors found." << std::endl; } + ///////////////////////////////////////////////////////// // Main Entry ///////////////////////////////////////////////////////// @@ -398,8 +396,8 @@ int main(int argc, char** argv) ////////////////////////////////////////////////////////////// std::cout << std::endl << SetConsoleTextColor(yellow_bold) << - "Tests for errors #1:" << SetConsoleTextColor(reset) << - std::endl << std::endl; + "Tests for errors #1:" << SetConsoleTextColor(reset) << std::endl << + "(Reusing 'TestPolytreeHoles' tests)" << std::endl << std::endl; ////////////////////////////////////////////////////////////// // #1. path is constant but points of interest change @@ -423,8 +421,8 @@ int main(int argc, char** argv) ////////////////////////////////////////////////////////////// std::cout << std::endl << SetConsoleTextColor(yellow_bold) << - "Tests for errors #2:" << SetConsoleTextColor(reset) << - std::endl << std::endl; + "Tests for errors #2:" << SetConsoleTextColor(reset) << std::endl << + "(Testing with 'unusual' polygons)" << std::endl << std::endl; ////////////////////////////////////////////////////////////// // #2. point of interest is now constant (10,10) but path changes @@ -461,8 +459,7 @@ int main(int argc, char** argv) "Benchmarking ..." << SetConsoleTextColor(reset) << std::endl; ////////////////////////////////////////////////////////////// - int width = 600000, height = 400000; - const int power10_lo = 4, power10_high = 8; + int width = 600000, height = 400000; count = 10000000; mp = Point64(width / 2, height / 2); @@ -470,11 +467,12 @@ int main(int argc, char** argv) "Benchmarks 1:" << SetConsoleTextColor(reset) << std::endl; ////////////////////////////////////////////////////////////// paths.clear(); - for (int i = power10_lo; i <= power10_high; ++i) - paths.push_back(Ellipse(mp, width / 2.0, height / 2.0, (unsigned)std::pow(10, i))); - std::cout << "A single elliptical path " << std::endl << - "Edge counts between 10^" << power10_lo << " and 10^" << - power10_high << std::endl << std::endl; + for (int i = 0; i < 5; ++i) + paths.push_back(Ellipse(mp, width / 2.0, height / 2.0, count)); + std::cout << "A single elliptical path (" << + width << " x " << height << ")" << std::endl << + "Edge count = " << count << ". " << std::endl << + "Point (" << mp << ")" << std::endl << std::endl; pipResults.clear(); pipResults.resize(3); @@ -485,30 +483,26 @@ int main(int argc, char** argv) BENCHMARK(BM_PIP2)->Apply(CustomArguments); // modified Clipper2 BENCHMARK(BM_PIP3)->Apply(CustomArguments); // Hao et al. (2018) benchmark::RunSpecifiedBenchmarks(benchmark::CreateDefaultDisplayReporter()); - benchmark::ClearRegisteredBenchmarks(); - std::cout << std::endl; - + + //benchmark::ClearRegisteredBenchmarks(); // same arguments, just different data - std::cout << std::endl << SetConsoleTextColor(yellow_bold) << + std::cout << std::endl << std::endl << SetConsoleTextColor(yellow_bold) << "Benchmarks 2:" << SetConsoleTextColor(reset) << std::endl; ////////////////////////////////////////////////////////////// std::cout << "A random self-intersecting polygon (" << width << " x " << height << ")" << std::endl << - "Edge counts between 10^" << power10_lo << " and 10^" << - power10_high << ". " << std::endl << + "Edge count = " << count << ". " << std::endl << "Point (" << mp << ")" << std::endl << std::endl; paths.clear(); - for (int i = power10_lo; i <= power10_high; ++i) - paths.push_back(MakeRandomPoly(width, height, (unsigned)std::pow(10, i))); + for (int i = 0; i < 5; ++i) + paths.push_back(MakeRandomPoly(width, height, count)); pipResults.clear(); pipResults.resize(3); for (size_t i = 0; i < 3; ++i) pipResults[i].resize(paths.size()); - benchmark::Initialize(0, nullptr); - BENCHMARK(BM_PIP1)->Apply(CustomArguments); // current Clipper2 - BENCHMARK(BM_PIP2)->Apply(CustomArguments); // modified Clipper2 - BENCHMARK(BM_PIP3)->Apply(CustomArguments); // Hao et al. (2018) + + // rerun benchmarks but this time using different polygons benchmark::RunSpecifiedBenchmarks(benchmark::CreateDefaultDisplayReporter()); std::cout << std::endl; diff --git a/CPP/BenchMark/StripDuplicateBenchmark.cpp b/CPP/BenchMark/StripDuplicateBenchmark.cpp index 36d7b45a..a4a0abfd 100644 --- a/CPP/BenchMark/StripDuplicateBenchmark.cpp +++ b/CPP/BenchMark/StripDuplicateBenchmark.cpp @@ -3,20 +3,16 @@ #include "CommonUtils.h" #include -static void CustomArguments(benchmark::internal::Benchmark *b) { - for (int i = 5; i <= 6; ++i) - for (int j = 5; j <= 6; j *= 8) - for (int k = 5; k <= 10; k++) - b->Args({i, j, k}); -} +using namespace Clipper2Lib; + +// globals +Paths64 test_paths; +// Previous (slow) StripDuplicates function - copies path template -inline Clipper2Lib::Path -StripDuplicatesCopyVersion(const Clipper2Lib::Path &path, - bool is_closed_path) { - using namespace Clipper2Lib; - if (path.size() == 0) - return Path(); +inline Path StripDuplicates1(const Path &path, bool is_closed_path) +{ + if (path.size() == 0) return Path(); Path result; result.reserve(path.size()); typename Path::const_iterator path_iter = path.cbegin(); @@ -35,51 +31,43 @@ StripDuplicatesCopyVersion(const Clipper2Lib::Path &path, return result; } -template -inline Clipper2Lib::Paths -StripDuplicatesCopyVersion(const Clipper2Lib::Paths &paths, - bool is_closed_path) { - using namespace Clipper2Lib; - Paths result; - result.reserve(paths.size()); - for (typename Paths::const_iterator paths_citer = paths.cbegin(); - paths_citer != paths.cend(); ++paths_citer) { - result.push_back(StripDuplicatesCopyVersion(*paths_citer, is_closed_path)); - } - return result; +// Current StripDuplicates function - modifies the path in-place (ie avoids copying) +template +inline void StripDuplicates2(Path& path, bool is_closed_path) +{ + path.erase(std::unique(path.begin(), path.end()), path.end()); + if (is_closed_path) + while (path.size() > 1 && path.back() == path.front()) path.pop_back(); } -static void BM_StripDuplicatesCopyVersion(benchmark::State &state) { - using namespace Clipper2Lib; - Paths64 op1; - - for (auto _ : state) { - state.PauseTiming(); - int width = (int)state.range(0); - int height = (int)state.range(1); - int count = (int)state.range(2); - op1.push_back(MakeRandomPoly(width, height, count)); - state.ResumeTiming(); - StripDuplicatesCopyVersion(op1, true); +static void StripDuplicates_OLD(benchmark::State &state) +{ + for (auto _ : state) + { + for (Path64& p: test_paths) + p = StripDuplicates1(p, true); } } -static void BM_StripDuplicates(benchmark::State &state) { - using namespace Clipper2Lib; - Paths64 op1; +static void StripDuplicates_NEW(benchmark::State &state) +{ for (auto _ : state) { - state.PauseTiming(); - int width = (int)state.range(0); - int height = (int)state.range(1); - int count = (int)state.range(2); - op1.push_back(MakeRandomPoly(width, height, count)); - state.ResumeTiming(); - StripDuplicates(op1, true); + for (Path64& p : test_paths) + StripDuplicates2(p, true); } } -// Register the function as a benchmark -BENCHMARK(BM_StripDuplicatesCopyVersion)->Apply(CustomArguments); -BENCHMARK(BM_StripDuplicates)->Apply(CustomArguments); -// Run the benchmark -BENCHMARK_MAIN(); \ No newline at end of file + +int main(int argc, char** argv) +{ + const size_t max_paths = 5; + const int width = 6000, height = 4000, count = 10000; + test_paths.reserve(max_paths); + for (size_t i = 1; i <= max_paths; ++i) + test_paths.push_back(MakeRandomPoly(width, height, count)); + + benchmark::Initialize(0, nullptr); + BENCHMARK(StripDuplicates_OLD); + BENCHMARK(StripDuplicates_NEW); + benchmark::RunSpecifiedBenchmarks(benchmark::CreateDefaultDisplayReporter()); +} From d41d9a9235a2ca9097410e59d39f24a83142b9af Mon Sep 17 00:00:00 2001 From: angusj Date: Sun, 3 Dec 2023 19:47:53 +1000 Subject: [PATCH 08/64] Fixed incorrect code comment --- CPP/BenchMark/GetIntersectPtBenchmark.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CPP/BenchMark/GetIntersectPtBenchmark.cpp b/CPP/BenchMark/GetIntersectPtBenchmark.cpp index a445a176..cc5d0218 100644 --- a/CPP/BenchMark/GetIntersectPtBenchmark.cpp +++ b/CPP/BenchMark/GetIntersectPtBenchmark.cpp @@ -42,7 +42,7 @@ typedef std::function GipFunction; ///////////////////////////////////////////////////////// -// GIP1: This is the current Clipper2 PointInPolygon code +// GIP1: This is the current Clipper2 GetIntersectPoint code ///////////////////////////////////////////////////////// static bool GIP1(const Point64& ln1a, const Point64& ln1b, const Point64& ln2a, const Point64& ln2b, Point64& ip) From f3b149ed9d0021f0f48f21ac91f0d9f4d95906e2 Mon Sep 17 00:00:00 2001 From: angusj Date: Sun, 3 Dec 2023 20:11:10 +1000 Subject: [PATCH 09/64] Additional GetIntersectPtBenchmark tweaks --- CPP/BenchMark/GetIntersectPtBenchmark.cpp | 96 ++++++++++++++++------- 1 file changed, 69 insertions(+), 27 deletions(-) diff --git a/CPP/BenchMark/GetIntersectPtBenchmark.cpp b/CPP/BenchMark/GetIntersectPtBenchmark.cpp index cc5d0218..74ebf90c 100644 --- a/CPP/BenchMark/GetIntersectPtBenchmark.cpp +++ b/CPP/BenchMark/GetIntersectPtBenchmark.cpp @@ -44,7 +44,7 @@ typedef std::function(ln1b.x - ln1a.x); @@ -72,7 +72,7 @@ static bool GIP1(const Point64& ln1a, const Point64& ln1b, #define CC_MIN(x,y) ((x)>(y)?(y):(x)) #define CC_MAX(x,y) ((x)<(y)?(y):(x)) #define DETECT_HIT_OVERFLOW (0) -static bool GIP2(const Point64& ln1a, const Point64& ln1b, +static bool GIP_Func_F(const Point64& ln1a, const Point64& ln1b, const Point64& ln2a, const Point64& ln2b, Point64& ip) { double ln1dy = (double)(ln1b.y - ln1a.y); @@ -111,7 +111,7 @@ static bool GIP2(const Point64& ln1a, const Point64& ln1b, #define CC_MIN(x,y) ((x)>(y)?(y):(x)) #define CC_MAX(x,y) ((x)<(y)?(y):(x)) #define DETECT_HIT_OVERFLOW (0) -static bool GIP3(const Point64& ln1a, const Point64& ln1b, +static bool GIP_F_Mod(const Point64& ln1a, const Point64& ln1b, const Point64& ln2a, const Point64& ln2b, Point64& ip) { double ln1dy = (double)(ln1b.y - ln1a.y); @@ -161,18 +161,6 @@ typedef std::unique_ptr TestRecord_ptr; std::vector tests; typedef std::vector::const_iterator test_iter; -inline GipFunction GetGipFunc(int index) -{ - GipFunction result; - switch (index) - { - case 0: result = GIP1; break; - case 1: result = GIP3; break; - //case 2: result = GIP3; break; - default: throw "oops! - wrong function!"; - } - return result; -} static inline Point64 ReflectPoint(const Point64& pt, const Point64& pivot) { @@ -196,22 +184,74 @@ static inline Point64 MakeRandomPoint(int64_t min_x, int64_t max_x, int64_t min_ return Point64(x(gen), y(gen)); } +static inline GipFunction GetGipFunc(int index) +{ + GipFunction result; + switch (index) + { + case 0: result = GIP_Current; break; + case 1: result = GIP_Func_F; break; + case 2: result = GIP_F_Mod; break; + default: throw "oops! - wrong function!"; + } + return result; +} + +static inline std::string GetGipFuncName(int index) +{ + std::string result; + switch (index) + { + case 0: result = "GIP_Current"; break; + case 1: result = "GIP_Func_F "; break; + case 2: result = "GIP_F_Mod "; break; + default: throw "oops!"; + } + return result; +} + ///////////////////////////////////////////////////////// // Benchmark callback functions ///////////////////////////////////////////////////////// -static void BM_GIP(benchmark::State& state) +static void BM_GIP_Current(benchmark::State& state) +{ + Point64 ip; + for (auto _ : state) + { + test_iter cit = tests.cbegin(); + for (; cit != tests.cend(); ++cit) + { + GIP_Current((*cit)->pt1, (*cit)->pt2, (*cit)->pt3, (*cit)->pt4, ip); + (*cit)->results[0] = ip; + } + } +} + +static void BM_GIP_Func_F(benchmark::State& state) +{ + Point64 ip; + for (auto _ : state) + { + test_iter cit = tests.cbegin(); + for (; cit != tests.cend(); ++cit) + { + GIP_Func_F((*cit)->pt1, (*cit)->pt2, (*cit)->pt3, (*cit)->pt4, ip); + (*cit)->results[1] = ip; + } + } +} + +static void BM_GIP_F_Mod(benchmark::State& state) { Point64 ip; - int idx = (int)state.range(0); - GipFunction gip_func = GetGipFunc(idx); for (auto _ : state) { test_iter cit = tests.cbegin(); for (; cit != tests.cend(); ++cit) { - gip_func((*cit)->pt1, (*cit)->pt2, (*cit)->pt3, (*cit)->pt4, ip); - (*cit)->results[idx] = ip; + GIP_F_Mod((*cit)->pt1, (*cit)->pt2, (*cit)->pt3, (*cit)->pt4, ip); + (*cit)->results[2] = ip; } } } @@ -223,11 +263,12 @@ static void BM_GIP(benchmark::State& state) int main(int argc, char** argv) { - const int participants = 2; + const int participants = 3; //setup benchmarking ... benchmark::Initialize(0, nullptr); - BENCHMARK(BM_GIP)->Args(std::vector{0}); - BENCHMARK(BM_GIP)->Args(std::vector{1}); + BENCHMARK(BM_GIP_Current); + BENCHMARK(BM_GIP_Func_F); + BENCHMARK(BM_GIP_F_Mod); bool first_pass = true; for (int power10 = 8; power10 <= 18; power10 += 2) @@ -241,8 +282,8 @@ int main(int argc, char** argv) Point64 ip3 = MakeRandomPoint(-max_coord, max_coord, -max_coord, max_coord); Point64 ip4 = ReflectPoint(ip3, ip); Point64 _; - // excluding any segments that are collinear - if (GIP1(ip1, ip2, ip3, ip4, _)) + // just to exclude any segments that are collinear + if (GIP_Current(ip1, ip2, ip3, ip4, _)) tests.push_back(std::make_unique(participants, ip, ip1, ip2, ip3, ip4)); } @@ -298,8 +339,9 @@ int main(int argc, char** argv) for (int i = 0; i < participants; ++i) { avg_dists[i] /= tests.size(); - std::cout << std::fixed << "GIP" << i << ": average distance error = " << std::setprecision(2) << - avg_dists[i] << "; largest dist. = " << std::setprecision(0) << worst_dists[i] << std::endl; + std::cout << std::fixed << GetGipFuncName(i) << + ": average distance error = " << std::setprecision(2) << avg_dists[i] << + "; largest dist. = " << std::setprecision(0) << worst_dists[i] << std::endl; } tests.clear(); } From 9d6a1b4535c467fa29e3d2d83586ce71286bdc51 Mon Sep 17 00:00:00 2001 From: angusj Date: Mon, 4 Dec 2023 08:19:23 +1000 Subject: [PATCH 10/64] Additional tweaks to GetIntersectPtBenchmark.cpp --- CPP/BenchMark/GetIntersectPtBenchmark.cpp | 80 ++++++++++++----------- 1 file changed, 43 insertions(+), 37 deletions(-) diff --git a/CPP/BenchMark/GetIntersectPtBenchmark.cpp b/CPP/BenchMark/GetIntersectPtBenchmark.cpp index 74ebf90c..125325c6 100644 --- a/CPP/BenchMark/GetIntersectPtBenchmark.cpp +++ b/CPP/BenchMark/GetIntersectPtBenchmark.cpp @@ -80,7 +80,7 @@ static bool GIP_Func_F(const Point64& ln1a, const Point64& ln1b, double ln2dy = (double)(ln2b.y - ln2a.y); double ln2dx = (double)(ln2a.x - ln2b.x); double det = (ln2dy * ln1dx) - (ln1dy * ln2dx); - if (det == 0.0) return 0; + if (det == 0.0) return false; int64_t bb0minx = CC_MIN(ln1a.x, ln1b.x); int64_t bb0miny = CC_MIN(ln1a.y, ln1b.y); int64_t bb0maxx = CC_MAX(ln1a.x, ln1b.x); @@ -97,16 +97,16 @@ static bool GIP_Func_F(const Point64& ln1a, const Point64& ln1b, double hity = ((ln2dy * ln0c) - (ln1dy * ln1c)) / det; #if DETECT_HIT_OVERFLOW if (fmax(fabs((double)originx + hitx), - fabs((double)originy + hity)) >= (double)(INT64_MAX - 1)) return 0; + fabs((double)originy + hity)) >= (double)(INT64_MAX - 1)) return false; #endif ip.x = originx + (int64_t)nearbyint(hitx); ip.y = originy + (int64_t)nearbyint(hity); - return 1; + return true; } ///////////////////////////////////////////////////////// -// GIP3: mathVertexLineLineIntersection_F but without calling nearbyint -// https://github.com/AngusJohnson/Clipper2/issues/317#issuecomment-1314023253 +// GIP3: Modified mathVertexLineLineIntersection_F above. +// Uses static casting instead of calling nearbyint ///////////////////////////////////////////////////////// #define CC_MIN(x,y) ((x)>(y)?(y):(x)) #define CC_MAX(x,y) ((x)<(y)?(y):(x)) @@ -119,7 +119,7 @@ static bool GIP_F_Mod(const Point64& ln1a, const Point64& ln1b, double ln2dy = (double)(ln2b.y - ln2a.y); double ln2dx = (double)(ln2a.x - ln2b.x); double det = (ln2dy * ln1dx) - (ln1dy * ln2dx); - if (det == 0.0) return 0; + if (det == 0.0) return false; int64_t bb0minx = CC_MIN(ln1a.x, ln1b.x); int64_t bb0miny = CC_MIN(ln1a.y, ln1b.y); int64_t bb0maxx = CC_MAX(ln1a.x, ln1b.x); @@ -136,31 +136,32 @@ static bool GIP_F_Mod(const Point64& ln1a, const Point64& ln1b, double hity = ((ln2dy * ln0c) - (ln1dy * ln1c)) / det; #if DETECT_HIT_OVERFLOW if (fmax(fabs((double)originx + hitx), - fabs((double)originy + hity)) >= (double)(INT64_MAX - 1)) return 0; + fabs((double)originy + hity)) >= (double)(INT64_MAX - 1)) return false; #endif ip.x = originx + static_cast(hitx); ip.y = originy + static_cast(hity); - //ip.x = originx + (int64_t)nearbyint(hitx); - //ip.y = originy + (int64_t)nearbyint(hity); - return 1; + return true; } struct TestRecord { public: - Point64 ideal, pt1, pt2, pt3, pt4; + Point64 actual, pt1, pt2, pt3, pt4; Path64 results; - TestRecord(int participants, const Point64& ip, + TestRecord(int participants, const Point64& intersect_pt, const Point64& p1, const Point64& p2, const Point64& p3, const Point64& p4) : - ideal(ip), pt1(p1), pt2(p2), pt3(p3), pt4(p4) { results.resize(participants); }; + actual(intersect_pt), pt1(p1), pt2(p2), pt3(p3), pt4(p4) { results.resize(participants); }; }; typedef std::unique_ptr TestRecord_ptr; +typedef std::vector::const_iterator test_iter; // global data std::vector tests; -typedef std::vector::const_iterator test_iter; +///////////////////////////////////////////////////////// +// Miscellaneous functions +///////////////////////////////////////////////////////// static inline Point64 ReflectPoint(const Point64& pt, const Point64& pivot) { @@ -175,12 +176,12 @@ static inline Point64 MidPoint(const Point64& p1, const Point64& p2) return result; } -static inline Point64 MakeRandomPoint(int64_t min_x, int64_t max_x, int64_t min_y, int64_t max_y) +static inline Point64 MakeRandomPoint(int64_t min_val, int64_t max_val) { std::random_device rd; std::mt19937 gen(rd()); - std::uniform_int_distribution x(min_x, max_x); - std::uniform_int_distribution y(min_y, max_y); + std::uniform_int_distribution x(min_val, max_val); + std::uniform_int_distribution y(min_val, max_val); return Point64(x(gen), y(gen)); } @@ -271,43 +272,49 @@ int main(int argc, char** argv) BENCHMARK(BM_GIP_F_Mod); bool first_pass = true; - for (int power10 = 8; power10 <= 18; power10 += 2) + for (int current_pow10 = 12; current_pow10 <= 18; ++current_pow10) { - int64_t max_coord = static_cast(pow(10, power10)); + // create multiple TestRecords containing segment pairs that intersect + // at their midpoints, while using random coordinates that are + // restricted to the specified power of 10 range + int64_t max_coord = static_cast(pow(10, current_pow10)); for (int64_t i = 0; i < 10000; ++i) { - Point64 ip1 = MakeRandomPoint(-max_coord, max_coord, -max_coord, max_coord); - Point64 ip2 = MakeRandomPoint(-max_coord, max_coord, -max_coord, max_coord); - Point64 ip = MidPoint(ip1, ip2); - Point64 ip3 = MakeRandomPoint(-max_coord, max_coord, -max_coord, max_coord); - Point64 ip4 = ReflectPoint(ip3, ip); + Point64 ip1 = MakeRandomPoint(-max_coord, max_coord); + Point64 ip2 = MakeRandomPoint(-max_coord, max_coord); + Point64 actual = MidPoint(ip1, ip2); + Point64 ip3 = MakeRandomPoint(-max_coord, max_coord); + Point64 ip4 = ReflectPoint(ip3, actual); Point64 _; - // just to exclude any segments that are collinear - if (GIP_Current(ip1, ip2, ip3, ip4, _)) - tests.push_back(std::make_unique(participants, ip, ip1, ip2, ip3, ip4)); + // excluding any segments that are collinear + if (!GIP_Current(ip1, ip2, ip3, ip4, _)) continue; + tests.push_back(std::make_unique(participants, actual, ip1, ip2, ip3, ip4)); } - // only benchmark the GetIntersectPoint functions once because - // the size of coordinate values won't affect their performance. if (first_pass) { + // only **benchmark** the GetIntersectPoint functions once because changing + // the maximum range of coordinates won't affect function performance. first_pass = false; std::cout << std::endl << SetConsoleTextColor(green_bold) << - "Benchmarking GetIntersectPoint performance ... " << SetConsoleTextColor(reset) << + "Benchmark GetIntersectPoint performance ... " << SetConsoleTextColor(reset) << std::endl << std::endl; + // using each test function in the callback functions above, benchmarking + // will calculate and store intersect points for each TestRecord benchmark::RunSpecifiedBenchmarks(benchmark::CreateDefaultDisplayReporter()); std::cout << std::endl << std::endl << SetConsoleTextColor(green_bold) << - "Now comparing function accuracy ..." << SetConsoleTextColor(white_bold) << std::endl << - "and showing how it deteriorates when using very large coordinate ranges." << - SetConsoleTextColor(reset) << std::endl << + "Compare function accuracy ..." << SetConsoleTextColor(reset) << std::endl << + "and show how it deteriorates when using very large coordinate ranges." << std::endl << "Distance error is the distance between the calculated and actual intersection points." << std::endl << - "nb: The largest errors will occur whenever intersecting edges are very close to collinear." << std::endl; + "The largest errors will occur whenever intersecting segments are almost collinear." << std::endl; } else { for (int i = 0; i < participants; ++i) { + // although we're not benchmarking, we still need to collect the calculated + // intersect points of each TestRecord using each participating test function Point64 ip; GipFunction gip_func = GetGipFunc(i); test_iter cit = tests.cbegin(); @@ -326,14 +333,13 @@ int main(int argc, char** argv) for (; cit != tests.cend(); ++cit) for (int i = 0; i < participants; ++i) { - double dist = Distance((*cit)->ideal, (*cit)->results[i]); + double dist = Distance((*cit)->actual, (*cit)->results[i]); avg_dists[i] += dist; if (dist > worst_dists[i]) worst_dists[i] = dist; } - std::cout << std::endl << SetConsoleTextColor(cyan_bold) << - "Coordinate ranges between +/- 10^" << power10 << + "Coordinate ranges between +/-10^" << current_pow10 << SetConsoleTextColor(reset) << std::endl; for (int i = 0; i < participants; ++i) From 3e3e3dcfa5402a45132b89ee721ff7c5cd23ee8d Mon Sep 17 00:00:00 2001 From: James Ruan Date: Sat, 9 Dec 2023 10:08:54 +0800 Subject: [PATCH 11/64] fix compiler warning of -Wunused-but-set-parameter with -fno-exceptions (#744) --- CPP/Clipper2Lib/include/clipper2/clipper.core.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CPP/Clipper2Lib/include/clipper2/clipper.core.h b/CPP/Clipper2Lib/include/clipper2/clipper.core.h index adbf4a04..11666db6 100644 --- a/CPP/Clipper2Lib/include/clipper2/clipper.core.h +++ b/CPP/Clipper2Lib/include/clipper2/clipper.core.h @@ -72,7 +72,7 @@ namespace Clipper2Lib static const double MAX_DBL = (std::numeric_limits::max)(); - static void DoError(int error_code) + static void DoError([[maybe_unused]] int error_code) { #if (defined(__cpp_exceptions) && __cpp_exceptions) || (defined(__EXCEPTIONS) && __EXCEPTIONS) switch (error_code) From 69e00c6cf9c4afa16e378c7b7aebf453c7d443ba Mon Sep 17 00:00:00 2001 From: Juha Reunanen Date: Sat, 9 Dec 2023 17:54:36 +0200 Subject: [PATCH 12/64] Make googletest not be a git submodule, and instead clone it separately in the CI script (#745) --- .github/workflows/actions_cpp.yml | 24 ++++++++++++++++++------ .gitmodules | 3 --- CPP/Tests/googletest | 1 - 3 files changed, 18 insertions(+), 10 deletions(-) delete mode 160000 CPP/Tests/googletest diff --git a/.github/workflows/actions_cpp.yml b/.github/workflows/actions_cpp.yml index 1ccd7f4b..9d438975 100644 --- a/.github/workflows/actions_cpp.yml +++ b/.github/workflows/actions_cpp.yml @@ -11,7 +11,9 @@ jobs: with: node-version: '16' - name: Get GoogleTest - run: git submodule update --init + run: | + cd CPP/Tests + git clone https://github.com/google/googletest - name: Add MSBuild to PATH uses: microsoft/setup-msbuild@v1.0.2 - name: Build @@ -32,7 +34,9 @@ jobs: with: node-version: '16' - name: Get GoogleTest - run: git submodule update --init + run: | + cd CPP/Tests + git clone https://github.com/google/googletest - name: Build run: | mkdir CPP/build @@ -51,7 +55,9 @@ jobs: with: node-version: '16' - name: Get GoogleTest - run: git submodule update --init + run: | + cd CPP/Tests + git clone https://github.com/google/googletest - name: Install gcc 11 run: | sudo apt update @@ -75,7 +81,9 @@ jobs: with: node-version: '16' - name: Get GoogleTest - run: git submodule update --init + run: | + cd CPP/Tests + git clone https://github.com/google/googletest - name: Build run: | export CC=/usr/bin/clang @@ -96,7 +104,9 @@ jobs: with: node-version: '16' - name: Get GoogleTest - run: git submodule update --init + run: | + cd CPP/Tests + git clone https://github.com/google/googletest - name: Install clang 13 run: | wget https://apt.llvm.org/llvm.sh @@ -122,7 +132,9 @@ jobs: with: node-version: '16' - name: Get GoogleTest - run: git submodule update --init + run: | + cd CPP/Tests + git clone https://github.com/google/googletest - name: Build run: | mkdir CPP/build diff --git a/.gitmodules b/.gitmodules index 901cd8f5..e69de29b 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +0,0 @@ -[submodule "CPP/Tests/googletest"] - path = CPP/Tests/googletest - url = https://github.com/google/googletest diff --git a/CPP/Tests/googletest b/CPP/Tests/googletest deleted file mode 160000 index 5e6a5336..00000000 --- a/CPP/Tests/googletest +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 5e6a533680fc8292c31f31664d80c48440d4a526 From 326f1274d22ff4a299b0af2958bc02ab8ad28c69 Mon Sep 17 00:00:00 2001 From: angusj Date: Mon, 11 Dec 2023 17:44:51 +1000 Subject: [PATCH 13/64] Additional minor tweaks to cpp/benchmark tests --- CPP/BenchMark/GetIntersectPtBenchmark.cpp | 113 ++++++++++-------- CPP/BenchMark/PointInPolygonBenchmark.cpp | 32 ++--- .../include/clipper2/clipper.core.h | 81 +++++++++---- CPP/GoogleTest in Visual Studio.txt | 11 +- 4 files changed, 143 insertions(+), 94 deletions(-) diff --git a/CPP/BenchMark/GetIntersectPtBenchmark.cpp b/CPP/BenchMark/GetIntersectPtBenchmark.cpp index 125325c6..ea9697d4 100644 --- a/CPP/BenchMark/GetIntersectPtBenchmark.cpp +++ b/CPP/BenchMark/GetIntersectPtBenchmark.cpp @@ -42,7 +42,7 @@ typedef std::function GipFunction; ///////////////////////////////////////////////////////// -// GIP1: This is the current Clipper2 GetIntersectPoint code +// GIP_Current: This is the current Clipper2 GetIntersectPoint code ///////////////////////////////////////////////////////// static bool GIP_Current(const Point64& ln1a, const Point64& ln1b, const Point64& ln2a, const Point64& ln2b, Point64& ip) @@ -66,7 +66,7 @@ static bool GIP_Current(const Point64& ln1a, const Point64& ln1b, } ///////////////////////////////////////////////////////// -// GIP2: This is mathVertexLineLineIntersection_F +// GIP_Func_F: This is mathVertexLineLineIntersection_F // https://github.com/AngusJohnson/Clipper2/issues/317#issuecomment-1314023253 ///////////////////////////////////////////////////////// #define CC_MIN(x,y) ((x)>(y)?(y):(x)) @@ -105,8 +105,8 @@ static bool GIP_Func_F(const Point64& ln1a, const Point64& ln1b, } ///////////////////////////////////////////////////////// -// GIP3: Modified mathVertexLineLineIntersection_F above. -// Uses static casting instead of calling nearbyint +// GIP_F_Mod: Modified GIP_Func_F (see above). +// Uses static_cast instead of nearbyint. ///////////////////////////////////////////////////////// #define CC_MIN(x,y) ((x)>(y)?(y):(x)) #define CC_MAX(x,y) ((x)<(y)?(y):(x)) @@ -135,11 +135,11 @@ static bool GIP_F_Mod(const Point64& ln1a, const Point64& ln1b, double hitx = ((ln1dx * ln1c) - (ln2dx * ln0c)) / det; double hity = ((ln2dy * ln0c) - (ln1dy * ln1c)) / det; #if DETECT_HIT_OVERFLOW - if (fmax(fabs((double)originx + hitx), + if (fmax(fabs((double)originx + hitx), fabs((double)originy + hity)) >= (double)(INT64_MAX - 1)) return false; #endif ip.x = originx + static_cast(hitx); - ip.y = originy + static_cast(hity); + ip.y = originy + static_cast(hity); return true; } @@ -147,22 +147,38 @@ struct TestRecord { public: Point64 actual, pt1, pt2, pt3, pt4; - Path64 results; + std::vector results; TestRecord(int participants, const Point64& intersect_pt, const Point64& p1, const Point64& p2, const Point64& p3, const Point64& p4) : - actual(intersect_pt), pt1(p1), pt2(p2), pt3(p3), pt4(p4) { results.resize(participants); }; + actual(intersect_pt), pt1(p1), pt2(p2), pt3(p3), pt4(p4) { + results.resize(participants); + }; }; -typedef std::unique_ptr TestRecord_ptr; -typedef std::vector::const_iterator test_iter; +typedef std::vector::iterator test_iter; // global data -std::vector tests; +std::vector tests; ///////////////////////////////////////////////////////// // Miscellaneous functions ///////////////////////////////////////////////////////// +double GetSine(const Point64& a, const Point64& b, const Point64& c) +{ + Point64 ab = Point64(b.x - a.x, b.y - a.y); + Point64 bc = Point64(b.x - c.x, b.y - c.y); + double dp = DotProduct(ab,bc); + double distSqr1 = DistanceSqr(a, b); + double distSqr2 = DistanceSqr(b, c); + // cos(A)^2 = dp^2/DistSqr(ab)/DistSqr(bc) + double cosSqr = dp * dp / distSqr1 / distSqr2; + // cos2A = cos(A)^2 * 2 - 1 + double cos2 = cosSqr * 2 - 1; + // sinA = Sqrt(1-cos2A) + return std::sqrt(1 - cos2); +} + static inline Point64 ReflectPoint(const Point64& pt, const Point64& pivot) { return Point64(pivot.x + (pivot.x - pt.x), pivot.y + (pivot.y - pt.y)); @@ -220,11 +236,10 @@ static void BM_GIP_Current(benchmark::State& state) Point64 ip; for (auto _ : state) { - test_iter cit = tests.cbegin(); - for (; cit != tests.cend(); ++cit) + for (test_iter cit = tests.begin(); cit != tests.end(); ++cit) { - GIP_Current((*cit)->pt1, (*cit)->pt2, (*cit)->pt3, (*cit)->pt4, ip); - (*cit)->results[0] = ip; + GIP_Current((*cit).pt1, (*cit).pt2, (*cit).pt3, (*cit).pt4, ip); + (*cit).results[0] = ip; } } } @@ -234,11 +249,10 @@ static void BM_GIP_Func_F(benchmark::State& state) Point64 ip; for (auto _ : state) { - test_iter cit = tests.cbegin(); - for (; cit != tests.cend(); ++cit) + for (test_iter cit = tests.begin(); cit != tests.end(); ++cit) { - GIP_Func_F((*cit)->pt1, (*cit)->pt2, (*cit)->pt3, (*cit)->pt4, ip); - (*cit)->results[1] = ip; + GIP_Func_F((*cit).pt1, (*cit).pt2, (*cit).pt3, (*cit).pt4, ip); + (*cit).results[1] = ip; } } } @@ -248,11 +262,10 @@ static void BM_GIP_F_Mod(benchmark::State& state) Point64 ip; for (auto _ : state) { - test_iter cit = tests.cbegin(); - for (; cit != tests.cend(); ++cit) + for (test_iter cit = tests.begin(); cit != tests.end(); ++cit) { - GIP_F_Mod((*cit)->pt1, (*cit)->pt2, (*cit)->pt3, (*cit)->pt4, ip); - (*cit)->results[2] = ip; + GIP_F_Mod((*cit).pt1, (*cit).pt2, (*cit).pt3, (*cit).pt4, ip); + (*cit).results[2] = ip; } } } @@ -265,11 +278,14 @@ int main(int argc, char** argv) { const int participants = 3; + const double deg_as_rad = 1.0 * PI / 180; + //setup benchmarking ... benchmark::Initialize(0, nullptr); BENCHMARK(BM_GIP_Current); BENCHMARK(BM_GIP_Func_F); BENCHMARK(BM_GIP_F_Mod); + benchmark::Counter(); bool first_pass = true; for (int current_pow10 = 12; current_pow10 <= 18; ++current_pow10) @@ -280,48 +296,52 @@ int main(int argc, char** argv) int64_t max_coord = static_cast(pow(10, current_pow10)); for (int64_t i = 0; i < 10000; ++i) { - Point64 ip1 = MakeRandomPoint(-max_coord, max_coord); - Point64 ip2 = MakeRandomPoint(-max_coord, max_coord); + Point64 ip1 = MakeRandomPoint(-max_coord, max_coord); + Point64 ip2 = MakeRandomPoint(-max_coord, max_coord); Point64 actual = MidPoint(ip1, ip2); - Point64 ip3 = MakeRandomPoint(-max_coord, max_coord); - Point64 ip4 = ReflectPoint(ip3, actual); + Point64 ip3 = MakeRandomPoint(-max_coord, max_coord); + Point64 ip4 = ReflectPoint(ip3, actual); Point64 _; - // excluding any segments that are collinear - if (!GIP_Current(ip1, ip2, ip3, ip4, _)) continue; - tests.push_back(std::make_unique(participants, actual, ip1, ip2, ip3, ip4)); + + // the closer segments are to collinear, the less accurate are + // calculations that determine intersection points. + // So exclude segments that are **almost** collinear. eg. sin(1deg) ~= 0.017 + if (std::abs(GetSine(ip1, actual, ip3)) < 0.017) continue; + // alternatively, only exclude segments that are collinear + //if (!CrossProduct(ip1, actual, ip3)) continue; + tests.push_back(TestRecord(participants, actual, ip1, ip2, ip3, ip4)); } if (first_pass) { - // only **benchmark** the GetIntersectPoint functions once because changing + // only benchmark the GetIntersectPoint functions once because changing // the maximum range of coordinates won't affect function performance. first_pass = false; std::cout << std::endl << SetConsoleTextColor(green_bold) << - "Benchmark GetIntersectPoint performance ... " << SetConsoleTextColor(reset) << + "Benchmark GetIntersectPoint performance ... " << SetConsoleTextColor(reset) << std::endl << std::endl; // using each test function in the callback functions above, benchmarking // will calculate and store intersect points for each TestRecord benchmark::RunSpecifiedBenchmarks(benchmark::CreateDefaultDisplayReporter()); std::cout << std::endl << std::endl << SetConsoleTextColor(green_bold) << - "Compare function accuracy ..." << SetConsoleTextColor(reset) << std::endl << - "and show how it deteriorates when using very large coordinate ranges." << std::endl << - "Distance error is the distance between the calculated and actual intersection points." << std::endl << + "Compare function accuracy ..." << SetConsoleTextColor(reset) << std::endl << + "and show how it deteriorates when using very large coordinate ranges." << std::endl << + "Distance error is the distance between the calculated and actual intersection points." << std::endl << "The largest errors will occur whenever intersecting segments are almost collinear." << std::endl; - } + } else { for (int i = 0; i < participants; ++i) { // although we're not benchmarking, we still need to collect the calculated - // intersect points of each TestRecord using each participating test function + // intersect points of each TestRecord for each participating function. Point64 ip; GipFunction gip_func = GetGipFunc(i); - test_iter cit = tests.cbegin(); - for (; cit != tests.cend(); ++cit) + for (test_iter cit = tests.begin(); cit != tests.end(); ++cit) { - gip_func((*cit)->pt1, (*cit)->pt2, (*cit)->pt3, (*cit)->pt4, ip); - (*cit)->results[i] = ip; + gip_func((*cit).pt1, (*cit).pt2, (*cit).pt3, (*cit).pt4, ip); + (*cit).results[i] = ip; } } } @@ -329,24 +349,23 @@ int main(int argc, char** argv) double avg_dists[participants] = { 0 }; double worst_dists[participants] = { 0 }; - std::vector::const_iterator cit = tests.cbegin(); - for (; cit != tests.cend(); ++cit) + for (test_iter cit = tests.begin(); cit != tests.end(); ++cit) for (int i = 0; i < participants; ++i) { - double dist = Distance((*cit)->actual, (*cit)->results[i]); + double dist = Distance((*cit).actual, (*cit).results[i]); avg_dists[i] += dist; if (dist > worst_dists[i]) worst_dists[i] = dist; } std::cout << std::endl << SetConsoleTextColor(cyan_bold) << - "Coordinate ranges between +/-10^" << current_pow10 << + "Coordinate ranges between +/-10^" << current_pow10 << SetConsoleTextColor(reset) << std::endl; for (int i = 0; i < participants; ++i) { avg_dists[i] /= tests.size(); - std::cout << std::fixed << GetGipFuncName(i) << - ": average distance error = " << std::setprecision(2) << avg_dists[i] << + std::cout << std::fixed << GetGipFuncName(i) << + ": average distance error = " << std::setprecision(2) << avg_dists[i] << "; largest dist. = " << std::setprecision(0) << worst_dists[i] << std::endl; } tests.clear(); diff --git a/CPP/BenchMark/PointInPolygonBenchmark.cpp b/CPP/BenchMark/PointInPolygonBenchmark.cpp index 550f8f29..018b378e 100644 --- a/CPP/BenchMark/PointInPolygonBenchmark.cpp +++ b/CPP/BenchMark/PointInPolygonBenchmark.cpp @@ -308,10 +308,7 @@ inline PipFunction GetPIPFunc(int index) } ///////////////////////////////////////////////////////// -// Test functions -///////////////////////////////////////////////////////// - -// DoErrorTest1 +// Error checking functions ///////////////////////////////////////////////////////// static void DoErrorTest1_internal(const Path64& pts_of_int, const Paths64& paths, @@ -359,8 +356,6 @@ static void DoErrorTest1(int index) pip_func, PointInPolygonResult::IsInside); } -// DoErrorTest2 -///////////////////////////////////////////////////////// static void DoErrorTest2(int index) { PipFunction pip_func = GetPIPFunc(index); @@ -395,12 +390,15 @@ int main(int argc, char** argv) std::endl; ////////////////////////////////////////////////////////////// + // 1. Very basic error testing + ////////////////////////////////////////////////////////////// + std::cout << std::endl << SetConsoleTextColor(yellow_bold) << "Tests for errors #1:" << SetConsoleTextColor(reset) << std::endl << "(Reusing 'TestPolytreeHoles' tests)" << std::endl << std::endl; - ////////////////////////////////////////////////////////////// - // #1. path is constant but points of interest change + + // 1a. use const path with changing points of interest Paths64 subject, subject_open, clip; ClipType ct = ClipType::None; @@ -419,13 +417,12 @@ int main(int argc, char** argv) for (int i = 0; i < 3; ++i) DoErrorTest1(i); - ////////////////////////////////////////////////////////////// + + // 1b. Use a const point of interest (10,10) against various paths + std::cout << std::endl << SetConsoleTextColor(yellow_bold) << "Tests for errors #2:" << SetConsoleTextColor(reset) << std::endl << "(Testing with 'unusual' polygons)" << std::endl << std::endl; - ////////////////////////////////////////////////////////////// - - // #2. point of interest is now constant (10,10) but path changes mp = Point64(10, 10); paths.clear(); @@ -453,11 +450,10 @@ int main(int argc, char** argv) for (int i = 0; i < 3; ++i) DoErrorTest2(i); std::cout << std::endl; + // 2. Benchmark functions - ////////////////////////////////////////////////////////////// std::cout << std::endl << SetConsoleTextColor(cyan_bold) << "Benchmarking ..." << SetConsoleTextColor(reset) << std::endl; - ////////////////////////////////////////////////////////////// int width = 600000, height = 400000; count = 10000000; mp = Point64(width / 2, height / 2); @@ -465,7 +461,7 @@ int main(int argc, char** argv) std::cout << std::endl << SetConsoleTextColor(yellow_bold) << "Benchmarks 1:" << SetConsoleTextColor(reset) << std::endl; - ////////////////////////////////////////////////////////////// + paths.clear(); for (int i = 0; i < 5; ++i) paths.push_back(Ellipse(mp, width / 2.0, height / 2.0, count)); @@ -484,11 +480,9 @@ int main(int argc, char** argv) BENCHMARK(BM_PIP3)->Apply(CustomArguments); // Hao et al. (2018) benchmark::RunSpecifiedBenchmarks(benchmark::CreateDefaultDisplayReporter()); - //benchmark::ClearRegisteredBenchmarks(); // same arguments, just different data - std::cout << std::endl << std::endl << SetConsoleTextColor(yellow_bold) << "Benchmarks 2:" << SetConsoleTextColor(reset) << std::endl; - ////////////////////////////////////////////////////////////// + std::cout << "A random self-intersecting polygon (" << width << " x " << height << ")" << std::endl << "Edge count = " << count << ". " << std::endl << @@ -502,7 +496,7 @@ int main(int argc, char** argv) pipResults.resize(3); for (size_t i = 0; i < 3; ++i) pipResults[i].resize(paths.size()); - // rerun benchmarks but this time using different polygons + // rerun benchmarks using different polygons benchmark::RunSpecifiedBenchmarks(benchmark::CreateDefaultDisplayReporter()); std::cout << std::endl; diff --git a/CPP/Clipper2Lib/include/clipper2/clipper.core.h b/CPP/Clipper2Lib/include/clipper2/clipper.core.h index 11666db6..141e522e 100644 --- a/CPP/Clipper2Lib/include/clipper2/clipper.core.h +++ b/CPP/Clipper2Lib/include/clipper2/clipper.core.h @@ -1,6 +1,6 @@ /******************************************************************************* * Author : Angus Johnson * -* Date : 24 November 2023 * +* Date : 9 December 2023 * * Website : http://www.angusj.com * * Copyright : Angus Johnson 2010-2023 * * Purpose : Core Clipper Library structures and functions * @@ -273,10 +273,19 @@ namespace Clipper2Lib else { left = top = (std::numeric_limits::max)(); - right = bottom = (std::numeric_limits::lowest)(); + right = bottom = std::numeric_limits::lowest(); } } + static Rect InvalidRect() + { + return { + (std::numeric_limits::max)(), + (std::numeric_limits::max)(), + std::numeric_limits::lowest(), + std::numeric_limits::lowest() }; + } + bool IsValid() const { return left != (std::numeric_limits::max)(); } T Width() const { return right - left; } @@ -360,24 +369,16 @@ namespace Clipper2Lib return result; } - static const Rect64 InvalidRect64 = Rect64( - (std::numeric_limits::max)(), - (std::numeric_limits::max)(), - (std::numeric_limits::lowest)(), - (std::numeric_limits::lowest)()); - static const RectD InvalidRectD = RectD( - (std::numeric_limits::max)(), - (std::numeric_limits::max)(), - (std::numeric_limits::lowest)(), - (std::numeric_limits::lowest)()); + static const Rect64 InvalidRect64 = Rect64::InvalidRect(); + static const RectD InvalidRectD = RectD::InvalidRect(); template Rect GetBounds(const Path& path) { - auto xmin = (std::numeric_limits::max)(); - auto ymin = (std::numeric_limits::max)(); - auto xmax = std::numeric_limits::lowest(); - auto ymax = std::numeric_limits::lowest(); + T xmin = (std::numeric_limits::max)(); + T ymin = (std::numeric_limits::max)(); + T xmax = std::numeric_limits::lowest(); + T ymax = std::numeric_limits::lowest(); for (const auto& p : path) { if (p.x < xmin) xmin = p.x; @@ -391,17 +392,52 @@ namespace Clipper2Lib template Rect GetBounds(const Paths& paths) { - auto xmin = (std::numeric_limits::max)(); - auto ymin = (std::numeric_limits::max)(); - auto xmax = std::numeric_limits::lowest(); - auto ymax = std::numeric_limits::lowest(); + T xmin = (std::numeric_limits::max)(); + T ymin = (std::numeric_limits::max)(); + T xmax = std::numeric_limits::lowest(); + T ymax = std::numeric_limits::lowest(); for (const Path& path : paths) for (const Point& p : path) { + if (p.x < xmin) xmin = p.x; + if (p.x > xmax) xmax = p.x; + if (p.y < ymin) ymin = p.y; + if (p.y > ymax) ymax = p.y; + } + return Rect(xmin, ymin, xmax, ymax); + } + + template + Rect GetBounds(const Path& path) + { + T xmin = (std::numeric_limits::max)(); + T ymin = (std::numeric_limits::max)(); + T xmax = std::numeric_limits::lowest(); + T ymax = std::numeric_limits::lowest(); + for (const auto& p : path) + { if (p.x < xmin) xmin = p.x; if (p.x > xmax) xmax = p.x; if (p.y < ymin) ymin = p.y; if (p.y > ymax) ymax = p.y; + } + return Rect(xmin, ymin, xmax, ymax); + } + + template + Rect GetBounds(const Paths& paths) + { + T xmin = (std::numeric_limits::max)(); + T ymin = (std::numeric_limits::max)(); + T xmax = std::numeric_limits::lowest(); + T ymax = std::numeric_limits::lowest(); + for (const Path& path : paths) + for (const Point& p : path) + { + if (p.x < xmin) xmin = p.x; + if (p.x > xmax) xmax = p.x; + if (p.y < ymin) ymin = p.y; + if (p.y > ymax) ymax = p.y; } return Rect(xmin, ymin, xmax, ymax); } @@ -468,10 +504,9 @@ namespace Clipper2Lib { Paths result; - if constexpr (std::numeric_limits::is_integer && - !std::numeric_limits::is_integer) + if constexpr (std::numeric_limits::is_integer) { - RectD r = GetBounds(paths); + RectD r = GetBounds(paths); if ((r.left * scale_x) < min_coord || (r.right * scale_x) > max_coord || (r.top * scale_y) < min_coord || diff --git a/CPP/GoogleTest in Visual Studio.txt b/CPP/GoogleTest in Visual Studio.txt index 6975672d..c694a8c7 100644 --- a/CPP/GoogleTest in Visual Studio.txt +++ b/CPP/GoogleTest in Visual Studio.txt @@ -1,12 +1,13 @@ Installing GoogleTest in Visual Studio -1. Goto https://github.com/google/googletest -2. Click on the bright green "Code" button and then click "Download ZIP". +1. In Clipper2's CPP/Tests folder create a subfolder named googletest. +2. Goto https://github.com/google/googletest +3. Click on the bright green "Code" button and then click "Download ZIP". 3. Open the downloaded Zip package and locate the following: a. CMakeLists.txt b. googlemock folder c. googletest folder -4. Copy these into Clipper2's empty CPP/Tests/googletest folder. -5. In Visual Studio, open Clipper2's CPP folder and wait to see +5. Copy these into the empty googletest folder created in step 1. +6. In Visual Studio, open Clipper2's CPP folder and wait to see "CMake generation finished" in Visual Studio's statusbar. -6. Rebuild all files (Ctrl+Shift+B). +7. Rebuild all files (Ctrl+Shift+B). From 928379cec5321855125b470061b8b30e6f40a0e0 Mon Sep 17 00:00:00 2001 From: angusj Date: Wed, 13 Dec 2023 15:15:27 +1000 Subject: [PATCH 14/64] Added CLIPPER2_HI_PRECISION option in CMakeLists.txt --- CPP/BenchMark/GetIntersectPtBenchmark.cpp | 83 +++++++++---------- CPP/CMakeLists.txt | 26 ++++-- .../include/clipper2/clipper.core.h | 70 +++++++++++++--- .../include/clipper2/clipper.engine.h | 4 +- CPP/Clipper2Lib/include/clipper2/clipper.h | 16 ++-- 5 files changed, 124 insertions(+), 75 deletions(-) diff --git a/CPP/BenchMark/GetIntersectPtBenchmark.cpp b/CPP/BenchMark/GetIntersectPtBenchmark.cpp index ea9697d4..3d4e3852 100644 --- a/CPP/BenchMark/GetIntersectPtBenchmark.cpp +++ b/CPP/BenchMark/GetIntersectPtBenchmark.cpp @@ -38,6 +38,40 @@ struct SetConsoleTextColor ////////////////////////////////////////////////////////////////////////////////////// +///////////////////////////////////////////////////////// +// Miscellaneous functions +///////////////////////////////////////////////////////// + +double GetSineAbc(const Point64& a, const Point64& b, const Point64& c) +{ + double dpB = DotProduct(a, b, c); + double SqrCosB = dpB * dpB / (DistanceSqr(a, b) * DistanceSqr(b, c)); + double cos2B = SqrCosB * 2 - 1; // trig. itentity + return std::sqrt(1 - cos2B); // sin(B) = Sqrt(1-cos(2B)) +} + +static inline Point64 ReflectPoint(const Point64& pt, const Point64& pivot) +{ + return Point64(pivot.x + (pivot.x - pt.x), pivot.y + (pivot.y - pt.y)); +} + +static inline Point64 MidPoint(const Point64& p1, const Point64& p2) +{ + Point64 result; + result.x = (p1.x + p2.x) / 2; + result.y = (p1.y + p2.y) / 2; + return result; +} + +static inline Point64 MakeRandomPoint(int64_t min_val, int64_t max_val) +{ + std::random_device rd; + std::mt19937 gen(rd()); + std::uniform_int_distribution x(min_val, max_val); + std::uniform_int_distribution y(min_val, max_val); + return Point64(x(gen), y(gen)); +} + typedef std::function GipFunction; @@ -106,7 +140,9 @@ static bool GIP_Func_F(const Point64& ln1a, const Point64& ln1b, ///////////////////////////////////////////////////////// // GIP_F_Mod: Modified GIP_Func_F (see above). -// Uses static_cast instead of nearbyint. +// Uses static_cast instead of nearbyint. Surprisingly, +// while faster here, this is much slower than GIP_Func_F +// when replacing GetIntersectPoint() in clipper.core.h. ///////////////////////////////////////////////////////// #define CC_MIN(x,y) ((x)>(y)?(y):(x)) #define CC_MAX(x,y) ((x)<(y)?(y):(x)) @@ -160,46 +196,6 @@ typedef std::vector::iterator test_iter; // global data std::vector tests; -///////////////////////////////////////////////////////// -// Miscellaneous functions -///////////////////////////////////////////////////////// - -double GetSine(const Point64& a, const Point64& b, const Point64& c) -{ - Point64 ab = Point64(b.x - a.x, b.y - a.y); - Point64 bc = Point64(b.x - c.x, b.y - c.y); - double dp = DotProduct(ab,bc); - double distSqr1 = DistanceSqr(a, b); - double distSqr2 = DistanceSqr(b, c); - // cos(A)^2 = dp^2/DistSqr(ab)/DistSqr(bc) - double cosSqr = dp * dp / distSqr1 / distSqr2; - // cos2A = cos(A)^2 * 2 - 1 - double cos2 = cosSqr * 2 - 1; - // sinA = Sqrt(1-cos2A) - return std::sqrt(1 - cos2); -} - -static inline Point64 ReflectPoint(const Point64& pt, const Point64& pivot) -{ - return Point64(pivot.x + (pivot.x - pt.x), pivot.y + (pivot.y - pt.y)); -} - -static inline Point64 MidPoint(const Point64& p1, const Point64& p2) -{ - Point64 result; - result.x = (p1.x + p2.x) / 2; - result.y = (p1.y + p2.y) / 2; - return result; -} - -static inline Point64 MakeRandomPoint(int64_t min_val, int64_t max_val) -{ - std::random_device rd; - std::mt19937 gen(rd()); - std::uniform_int_distribution x(min_val, max_val); - std::uniform_int_distribution y(min_val, max_val); - return Point64(x(gen), y(gen)); -} static inline GipFunction GetGipFunc(int index) { @@ -285,7 +281,6 @@ int main(int argc, char** argv) BENCHMARK(BM_GIP_Current); BENCHMARK(BM_GIP_Func_F); BENCHMARK(BM_GIP_F_Mod); - benchmark::Counter(); bool first_pass = true; for (int current_pow10 = 12; current_pow10 <= 18; ++current_pow10) @@ -294,7 +289,7 @@ int main(int argc, char** argv) // at their midpoints, while using random coordinates that are // restricted to the specified power of 10 range int64_t max_coord = static_cast(pow(10, current_pow10)); - for (int64_t i = 0; i < 10000; ++i) + for (int64_t i = 0; i < 100000; ++i) { Point64 ip1 = MakeRandomPoint(-max_coord, max_coord); Point64 ip2 = MakeRandomPoint(-max_coord, max_coord); @@ -306,7 +301,7 @@ int main(int argc, char** argv) // the closer segments are to collinear, the less accurate are // calculations that determine intersection points. // So exclude segments that are **almost** collinear. eg. sin(1deg) ~= 0.017 - if (std::abs(GetSine(ip1, actual, ip3)) < 0.017) continue; + if (std::abs(GetSineAbc(ip1, actual, ip3)) < 0.017) continue; // alternatively, only exclude segments that are collinear //if (!CrossProduct(ip1, actual, ip3)) continue; tests.push_back(TestRecord(participants, actual, ip1, ip2, ip3, ip4)); diff --git a/CPP/CMakeLists.txt b/CPP/CMakeLists.txt index 5fab727d..f95dd295 100644 --- a/CPP/CMakeLists.txt +++ b/CPP/CMakeLists.txt @@ -9,21 +9,27 @@ set(CMAKE_CXX_STANDARD_REQUIRED ON) set(CMAKE_CXX_EXTENSIONS OFF) set_property(GLOBAL PROPERTY USE_FOLDERS ON) +# CLIPPER2_HI_PRECISION: See GetIntersectPoint() in clipper.core.h +option(CLIPPER2_HI_PRECISION "Caution: enabling this will compromise performance" OFF) + option(CLIPPER2_UTILS "Build utilities" ON) option(CLIPPER2_EXAMPLES "Build examples" ON) option(CLIPPER2_TESTS "Build tests" ON) option(USE_EXTERNAL_GTEST "Use system-wide installed GoogleTest" OFF) -option(USE_EXTERNAL_GBENCHMARK "Use the googlebenchmark" OFF) +option(USE_EXTERNAL_GBENCHMARK "Use the googlebenchmark" OFF) option(BUILD_SHARED_LIBS "Build shared libs" OFF) set(CLIPPER2_USINGZ "ON" CACHE STRING "Build Clipper2Z, either \"ON\" or \"OFF\" or \"ONLY\"") -set(CLIPPER2_MAX_PRECISION 8 CACHE STRING "Maximum precision allowed for double to int64 scaling") + +# CLIPPER2_MAX_DECIMAL_PRECISION: maximum decimal precision when scaling PathsD to Paths64. +# Caution: excessive scaling will increase the likelihood of integer overflow errors. +set(CLIPPER2_MAX_DECIMAL_PRECISION 8 CACHE STRING "Maximum decimal precision range") + if (APPLE) set(CMAKE_SHARED_LIBRARY_SUFFIX ".dylib") endif () include(GNUInstallDirs) - set(CLIPPER2_INC_FOLDER ${PROJECT_SOURCE_DIR}/Clipper2Lib/include/clipper2) configure_file(clipper.version.in ${CLIPPER2_INC_FOLDER}/clipper.version.h) @@ -52,10 +58,12 @@ if (NOT (CLIPPER2_USINGZ STREQUAL "ONLY")) add_library(Clipper2 ${CLIPPER2_INC} ${CLIPPER2_SRC}) target_compile_definitions( - Clipper2 - PUBLIC CLIPPER2_MAX_PRECISION=${CLIPPER2_MAX_PRECISION} + Clipper2 PUBLIC + CLIPPER2_MAX_DECIMAL_PRECISION=${CLIPPER2_MAX_DECIMAL_PRECISION} + $<$:CLIPPER2_HI_PRECISION> ) + target_include_directories(Clipper2 PUBLIC Clipper2Lib/include ) @@ -74,9 +82,11 @@ if (NOT (CLIPPER2_USINGZ STREQUAL "OFF")) add_library(Clipper2Z ${CLIPPER2_INC} ${CLIPPER2_SRC}) target_compile_definitions( - Clipper2Z - PUBLIC USINGZ CLIPPER2_MAX_PRECISION=${CLIPPER2_MAX_PRECISION} - ) + Clipper2Z PUBLIC + USINGZ + CLIPPER2_MAX_DECIMAL_PRECISION=${CLIPPER2_MAX_DECIMAL_PRECISION} + $<$:CLIPPER2_HI_PRECISION> + ) target_include_directories(Clipper2Z PUBLIC Clipper2Lib/include diff --git a/CPP/Clipper2Lib/include/clipper2/clipper.core.h b/CPP/Clipper2Lib/include/clipper2/clipper.core.h index 141e522e..69910789 100644 --- a/CPP/Clipper2Lib/include/clipper2/clipper.core.h +++ b/CPP/Clipper2Lib/include/clipper2/clipper.core.h @@ -1,6 +1,6 @@ /******************************************************************************* * Author : Angus Johnson * -* Date : 9 December 2023 * +* Date : 13 December 2023 * * Website : http://www.angusj.com * * Copyright : Angus Johnson 2010-2023 * * Purpose : Core Clipper Library structures and functions * @@ -58,10 +58,10 @@ namespace Clipper2Lib static const double PI = 3.141592653589793238; #endif -#ifdef CLIPPER2_MAX_PRECISION - const int MAX_DECIMAL_PRECISION = CLIPPER2_MAX_PRECISION; +#ifdef CLIPPER2_MAX_DECIMAL_PRECISION + const int CLIPPER2_MAX_DEC_PRECISION = CLIPPER2_MAX_DECIMAL_PRECISION; #else - const int MAX_DECIMAL_PRECISION = 8; // see Discussions #564 + const int CLIPPER2_MAX_DEC_PRECISION = 8; // see Discussions #564 #endif static const int64_t MAX_COORD = INT64_MAX >> 2; @@ -242,6 +242,14 @@ namespace Clipper2Lib (std::numeric_limits::max)(), (std::numeric_limits::max)()); + template + static inline Point MidPoint(const Point& p1, const Point& p2) + { + Point result; + result.x = (p1.x + p2.x) / 2; + result.y = (p1.y + p2.y) / 2; + return result; + } // Rect ------------------------------------------------------------------------ @@ -623,18 +631,19 @@ namespace Clipper2Lib // Miscellaneous ------------------------------------------------------------ - inline void CheckPrecision(int& precision, int& error_code) + inline void CheckPrecisionRange(int& precision, int& error_code) { - if (precision >= -MAX_DECIMAL_PRECISION && precision <= MAX_DECIMAL_PRECISION) return; + if (precision >= -CLIPPER2_MAX_DEC_PRECISION && + precision <= CLIPPER2_MAX_DEC_PRECISION) return; error_code |= precision_error_i; // non-fatal error - DoError(precision_error_i); // does nothing unless exceptions enabled - precision = precision > 0 ? MAX_DECIMAL_PRECISION : -MAX_DECIMAL_PRECISION; + DoError(precision_error_i); // does nothing when exceptions are disabled + precision = precision > 0 ? CLIPPER2_MAX_DEC_PRECISION : -CLIPPER2_MAX_DEC_PRECISION; } - inline void CheckPrecision(int& precision) + inline void CheckPrecisionRange(int& precision) { int error_code = 0; - CheckPrecision(precision, error_code); + CheckPrecisionRange(precision, error_code); } template @@ -721,6 +730,40 @@ namespace Clipper2Lib return Area(poly) >= 0; } +#if CLIPPER2_HI_PRECISION + // caution: this will compromise performance + // https://github.com/AngusJohnson/Clipper2/issues/317#issuecomment-1314023253 + // See also CPP/BenchMark/GetIntersectPtBenchmark.cpp + #define CC_MIN(x,y) ((x)>(y)?(y):(x)) + #define CC_MAX(x,y) ((x)<(y)?(y):(x)) + inline bool GetIntersectPoint(const Point64& ln1a, const Point64& ln1b, + const Point64& ln2a, const Point64& ln2b, Point64& ip) + { + double ln1dy = (double)(ln1b.y - ln1a.y); + double ln1dx = (double)(ln1a.x - ln1b.x); + double ln2dy = (double)(ln2b.y - ln2a.y); + double ln2dx = (double)(ln2a.x - ln2b.x); + double det = (ln2dy * ln1dx) - (ln1dy * ln2dx); + if (det == 0.0) return false; + int64_t bb0minx = CC_MIN(ln1a.x, ln1b.x); + int64_t bb0miny = CC_MIN(ln1a.y, ln1b.y); + int64_t bb0maxx = CC_MAX(ln1a.x, ln1b.x); + int64_t bb0maxy = CC_MAX(ln1a.y, ln1b.y); + int64_t bb1minx = CC_MIN(ln2a.x, ln2b.x); + int64_t bb1miny = CC_MIN(ln2a.y, ln2b.y); + int64_t bb1maxx = CC_MAX(ln2a.x, ln2b.x); + int64_t bb1maxy = CC_MAX(ln2a.y, ln2b.y); + int64_t originx = (CC_MIN(bb0maxx, bb1maxx) + CC_MAX(bb0minx, bb1minx)) >> 1; + int64_t originy = (CC_MIN(bb0maxy, bb1maxy) + CC_MAX(bb0miny, bb1miny)) >> 1; + double ln0c = (ln1dy * (double)(ln1a.x - originx)) + (ln1dx * (double)(ln1a.y - originy)); + double ln1c = (ln2dy * (double)(ln2a.x - originx)) + (ln2dx * (double)(ln2a.y - originy)); + double hitx = ((ln1dx * ln1c) - (ln2dx * ln0c)) / det; + double hity = ((ln2dy * ln0c) - (ln1dy * ln1c)) / det; + ip.x = originx + (int64_t)nearbyint(hitx); + ip.y = originy + (int64_t)nearbyint(hity); + return true; +} +#else inline bool GetIntersectPoint(const Point64& ln1a, const Point64& ln1b, const Point64& ln2a, const Point64& ln2b, Point64& ip) { @@ -733,15 +776,16 @@ namespace Clipper2Lib double det = dy1 * dx2 - dy2 * dx1; if (det == 0.0) return false; double t = ((ln1a.x - ln2a.x) * dy2 - (ln1a.y - ln2a.y) * dx2) / det; - if (t <= 0.0) ip = ln1a; // ?? check further (see also #568) - else if (t >= 1.0) ip = ln1b; // ?? check further + if (t <= 0.0) ip = ln1a; + else if (t >= 1.0) ip = ln1b; else { ip.x = static_cast(ln1a.x + t * dx1); ip.y = static_cast(ln1a.y + t * dy1); - } + } return true; } +#endif inline bool SegmentsIntersect(const Point64& seg1a, const Point64& seg1b, const Point64& seg2a, const Point64& seg2b, bool inclusive = false) diff --git a/CPP/Clipper2Lib/include/clipper2/clipper.engine.h b/CPP/Clipper2Lib/include/clipper2/clipper.engine.h index f7fe0182..9dd55db7 100644 --- a/CPP/Clipper2Lib/include/clipper2/clipper.engine.h +++ b/CPP/Clipper2Lib/include/clipper2/clipper.engine.h @@ -1,6 +1,6 @@ /******************************************************************************* * Author : Angus Johnson * -* Date : 22 November 2023 * +* Date : 13 December 2023 * * Website : http://www.angusj.com * * Copyright : Angus Johnson 2010-2023 * * Purpose : This is the main polygon clipping module * @@ -530,7 +530,7 @@ namespace Clipper2Lib { public: explicit ClipperD(int precision = 2) : ClipperBase() { - CheckPrecision(precision, error_code_); + CheckPrecisionRange(precision, error_code_); // to optimize scaling / descaling precision // set the scale to a power of double's radix (2) (#25) scale_ = std::pow(std::numeric_limits::radix, diff --git a/CPP/Clipper2Lib/include/clipper2/clipper.h b/CPP/Clipper2Lib/include/clipper2/clipper.h index 33beef6f..f9446220 100644 --- a/CPP/Clipper2Lib/include/clipper2/clipper.h +++ b/CPP/Clipper2Lib/include/clipper2/clipper.h @@ -1,6 +1,6 @@ /******************************************************************************* * Author : Angus Johnson * -* Date : 18 November 2023 * +* Date : 13 December 2023 * * Website : http://www.angusj.com * * Copyright : Angus Johnson 2010-2023 * * Purpose : This module provides a simple interface to the Clipper Library * @@ -47,7 +47,7 @@ namespace Clipper2Lib { const PathsD& subjects, const PathsD& clips, int precision = 2) { int error_code = 0; - CheckPrecision(precision, error_code); + CheckPrecisionRange(precision, error_code); PathsD result; if (error_code) return result; ClipperD clipper(precision); @@ -63,7 +63,7 @@ namespace Clipper2Lib { { polytree.Clear(); int error_code = 0; - CheckPrecision(precision, error_code); + CheckPrecisionRange(precision, error_code); if (error_code) return; ClipperD clipper(precision); clipper.AddSubject(subjects); @@ -104,7 +104,7 @@ namespace Clipper2Lib { { PathsD result; int error_code = 0; - CheckPrecision(precision, error_code); + CheckPrecisionRange(precision, error_code); if (error_code) return result; ClipperD clipper(precision); clipper.AddSubject(subjects); @@ -149,7 +149,7 @@ namespace Clipper2Lib { int precision = 2, double arc_tolerance = 0.0) { int error_code = 0; - CheckPrecision(precision, error_code); + CheckPrecisionRange(precision, error_code); if (!delta) return paths; if (error_code) return PathsD(); const double scale = std::pow(10, precision); @@ -219,7 +219,7 @@ namespace Clipper2Lib { { if (rect.IsEmpty() || paths.empty()) return PathsD(); int error_code = 0; - CheckPrecision(precision, error_code); + CheckPrecisionRange(precision, error_code); if (error_code) return PathsD(); const double scale = std::pow(10, precision); Rect64 r = ScaleRect(rect, scale); @@ -251,7 +251,7 @@ namespace Clipper2Lib { { if (rect.IsEmpty() || lines.empty()) return PathsD(); int error_code = 0; - CheckPrecision(precision, error_code); + CheckPrecisionRange(precision, error_code); if (error_code) return PathsD(); const double scale = std::pow(10, precision); Rect64 r = ScaleRect(rect, scale); @@ -545,7 +545,7 @@ namespace Clipper2Lib { inline PathD TrimCollinear(const PathD& path, int precision, bool is_open_path = false) { int error_code = 0; - CheckPrecision(precision, error_code); + CheckPrecisionRange(precision, error_code); if (error_code) return PathD(); const double scale = std::pow(10, precision); Path64 p = ScalePath(path, scale, error_code); From be61c7e799648f899b2d20c22bfa066cdedcf658 Mon Sep 17 00:00:00 2001 From: angusj Date: Wed, 13 Dec 2023 19:01:55 +1000 Subject: [PATCH 15/64] Minor code tidy --- CPP/BenchMark/GetIntersectPtBenchmark.cpp | 36 ++------- CPP/BenchMark/PointInPolygonBenchmark.cpp | 30 +++---- .../include/clipper2/clipper.core.h | 81 +++++++++++++------ CPP/Clipper2Lib/src/clipper.offset.cpp | 59 +------------- CPP/Utils/CommonUtils.h | 5 +- 5 files changed, 86 insertions(+), 125 deletions(-) diff --git a/CPP/BenchMark/GetIntersectPtBenchmark.cpp b/CPP/BenchMark/GetIntersectPtBenchmark.cpp index 3d4e3852..1ca12e23 100644 --- a/CPP/BenchMark/GetIntersectPtBenchmark.cpp +++ b/CPP/BenchMark/GetIntersectPtBenchmark.cpp @@ -50,19 +50,6 @@ double GetSineAbc(const Point64& a, const Point64& b, const Point64& c) return std::sqrt(1 - cos2B); // sin(B) = Sqrt(1-cos(2B)) } -static inline Point64 ReflectPoint(const Point64& pt, const Point64& pivot) -{ - return Point64(pivot.x + (pivot.x - pt.x), pivot.y + (pivot.y - pt.y)); -} - -static inline Point64 MidPoint(const Point64& p1, const Point64& p2) -{ - Point64 result; - result.x = (p1.x + p2.x) / 2; - result.y = (p1.y + p2.y) / 2; - return result; -} - static inline Point64 MakeRandomPoint(int64_t min_val, int64_t max_val) { std::random_device rd; @@ -89,8 +76,8 @@ static bool GIP_Current(const Point64& ln1a, const Point64& ln1b, double det = dy1 * dx2 - dy2 * dx1; if (det == 0.0) return false; double t = ((double)(ln1a.x - ln2a.x) * dy2 - (double)(ln1a.y - ln2a.y) * dx2) / det; - if (t <= 0.0) ip = ln1a; // ?? check further (see also #568) - else if (t >= 1.0) ip = ln1b; // ?? check further + if (t <= 0.0) ip = ln1a; + else if (t >= 1.0) ip = ln1b; else { ip.x = static_cast(ln1a.x + t * dx1); @@ -105,7 +92,6 @@ static bool GIP_Current(const Point64& ln1a, const Point64& ln1b, ///////////////////////////////////////////////////////// #define CC_MIN(x,y) ((x)>(y)?(y):(x)) #define CC_MAX(x,y) ((x)<(y)?(y):(x)) -#define DETECT_HIT_OVERFLOW (0) static bool GIP_Func_F(const Point64& ln1a, const Point64& ln1b, const Point64& ln2a, const Point64& ln2b, Point64& ip) { @@ -129,10 +115,6 @@ static bool GIP_Func_F(const Point64& ln1a, const Point64& ln1b, double ln1c = (ln2dy * (double)(ln2a.x - originx)) + (ln2dx * (double)(ln2a.y - originy)); double hitx = ((ln1dx * ln1c) - (ln2dx * ln0c)) / det; double hity = ((ln2dy * ln0c) - (ln1dy * ln1c)) / det; -#if DETECT_HIT_OVERFLOW - if (fmax(fabs((double)originx + hitx), - fabs((double)originy + hity)) >= (double)(INT64_MAX - 1)) return false; -#endif ip.x = originx + (int64_t)nearbyint(hitx); ip.y = originy + (int64_t)nearbyint(hity); return true; @@ -140,13 +122,13 @@ static bool GIP_Func_F(const Point64& ln1a, const Point64& ln1b, ///////////////////////////////////////////////////////// // GIP_F_Mod: Modified GIP_Func_F (see above). -// Uses static_cast instead of nearbyint. Surprisingly, -// while faster here, this is much slower than GIP_Func_F -// when replacing GetIntersectPoint() in clipper.core.h. +// Replaces nearbyint with static_cast. Surprisingly, while +// this function is a little faster here than GIP_Func_F, +// it's much slower than GIP_Func_F when using it as a +// GetIntersectPoint() replacement in clipper.core.h. ///////////////////////////////////////////////////////// #define CC_MIN(x,y) ((x)>(y)?(y):(x)) #define CC_MAX(x,y) ((x)<(y)?(y):(x)) -#define DETECT_HIT_OVERFLOW (0) static bool GIP_F_Mod(const Point64& ln1a, const Point64& ln1b, const Point64& ln2a, const Point64& ln2b, Point64& ip) { @@ -170,15 +152,12 @@ static bool GIP_F_Mod(const Point64& ln1a, const Point64& ln1b, double ln1c = (ln2dy * (double)(ln2a.x - originx)) + (ln2dx * (double)(ln2a.y - originy)); double hitx = ((ln1dx * ln1c) - (ln2dx * ln0c)) / det; double hity = ((ln2dy * ln0c) - (ln1dy * ln1c)) / det; -#if DETECT_HIT_OVERFLOW - if (fmax(fabs((double)originx + hitx), - fabs((double)originy + hity)) >= (double)(INT64_MAX - 1)) return false; -#endif ip.x = originx + static_cast(hitx); ip.y = originy + static_cast(hity); return true; } + struct TestRecord { public: @@ -274,7 +253,6 @@ int main(int argc, char** argv) { const int participants = 3; - const double deg_as_rad = 1.0 * PI / 180; //setup benchmarking ... benchmark::Initialize(0, nullptr); diff --git a/CPP/BenchMark/PointInPolygonBenchmark.cpp b/CPP/BenchMark/PointInPolygonBenchmark.cpp index 018b378e..353ddbd7 100644 --- a/CPP/BenchMark/PointInPolygonBenchmark.cpp +++ b/CPP/BenchMark/PointInPolygonBenchmark.cpp @@ -252,10 +252,13 @@ static PointInPolygonResult PIP3(const Point64&pt, const Path64&path) // global data structures ///////////////////////////////////////////////////////// +const Path64 points_of_interest_outside = + MakePath({ 21887,10420, 21726,10825, 21662,10845, 21617,10890 }); +const Path64 points_of_interest_inside = + MakePath({ 21887,10430, 21843,10520, 21810,10686, 21900,10461 }); + Point64 mp; Paths64 paths; -Path64 points_of_interest_outside; -Path64 points_of_interest_inside; std::vector < std::vector > pipResults; @@ -397,27 +400,21 @@ int main(int argc, char** argv) "Tests for errors #1:" << SetConsoleTextColor(reset) << std::endl << "(Reusing 'TestPolytreeHoles' tests)" << std::endl << std::endl; - - // 1a. use const path with changing points of interest + // 1a. use const paths (PolytreeHoleOwner2.txt) with changing points of interest Paths64 subject, subject_open, clip; - ClipType ct = ClipType::None; - FillRule fr = FillRule::EvenOdd; - int64_t area = 0, count = 0; + int64_t _, __; + ClipType ___; + FillRule ____; const std::string test_file = "../../../../../Tests/PolytreeHoleOwner2.txt"; - points_of_interest_outside = - MakePath({ 21887,10420, 21726,10825, 21662,10845, 21617,10890 }); - points_of_interest_inside = - MakePath({ 21887,10430, 21843,10520, 21810,10686, 21900,10461 }); if (!FileExists(test_file)) return 1; std::ifstream ifs(test_file); if (!ifs || !ifs.good()) return 1; - LoadTestNum(ifs, 1, paths, subject_open, clip, area, count, ct, fr); + LoadTestNum(ifs, 1, paths, subject_open, clip, _, __, ___, ____); ifs.close(); - + for (int i = 0; i < 3; ++i) DoErrorTest1(i); - // 1b. Use a const point of interest (10,10) against various paths std::cout << std::endl << SetConsoleTextColor(yellow_bold) << @@ -454,8 +451,11 @@ int main(int argc, char** argv) std::cout << std::endl << SetConsoleTextColor(cyan_bold) << "Benchmarking ..." << SetConsoleTextColor(reset) << std::endl; + std::cout << "Note: function performance varies depending on the proportion of edges" << + std::endl << "that intersect with an imaginary horizontal line passing through the" << + std::endl << "point of interest." << std::endl << std::endl; - int width = 600000, height = 400000; count = 10000000; + unsigned int width = 600000, height = 400000, count = 10000000; mp = Point64(width / 2, height / 2); diff --git a/CPP/Clipper2Lib/include/clipper2/clipper.core.h b/CPP/Clipper2Lib/include/clipper2/clipper.core.h index 69910789..75075cb1 100644 --- a/CPP/Clipper2Lib/include/clipper2/clipper.core.h +++ b/CPP/Clipper2Lib/include/clipper2/clipper.core.h @@ -736,36 +736,48 @@ namespace Clipper2Lib // See also CPP/BenchMark/GetIntersectPtBenchmark.cpp #define CC_MIN(x,y) ((x)>(y)?(y):(x)) #define CC_MAX(x,y) ((x)<(y)?(y):(x)) - inline bool GetIntersectPoint(const Point64& ln1a, const Point64& ln1b, - const Point64& ln2a, const Point64& ln2b, Point64& ip) + template + inline bool GetIntersectPoint(const Point& ln1a, const Point& ln1b, + const Point& ln2a, const Point& ln2b, Point& ip) { - double ln1dy = (double)(ln1b.y - ln1a.y); - double ln1dx = (double)(ln1a.x - ln1b.x); - double ln2dy = (double)(ln2b.y - ln2a.y); - double ln2dx = (double)(ln2a.x - ln2b.x); + double ln1dy = static_cast(ln1b.y - ln1a.y); + double ln1dx = static_cast(ln1a.x - ln1b.x); + double ln2dy = static_cast(ln2b.y - ln2a.y); + double ln2dx = static_cast(ln2a.x - ln2b.x); double det = (ln2dy * ln1dx) - (ln1dy * ln2dx); if (det == 0.0) return false; - int64_t bb0minx = CC_MIN(ln1a.x, ln1b.x); - int64_t bb0miny = CC_MIN(ln1a.y, ln1b.y); - int64_t bb0maxx = CC_MAX(ln1a.x, ln1b.x); - int64_t bb0maxy = CC_MAX(ln1a.y, ln1b.y); - int64_t bb1minx = CC_MIN(ln2a.x, ln2b.x); - int64_t bb1miny = CC_MIN(ln2a.y, ln2b.y); - int64_t bb1maxx = CC_MAX(ln2a.x, ln2b.x); - int64_t bb1maxy = CC_MAX(ln2a.y, ln2b.y); - int64_t originx = (CC_MIN(bb0maxx, bb1maxx) + CC_MAX(bb0minx, bb1minx)) >> 1; - int64_t originy = (CC_MIN(bb0maxy, bb1maxy) + CC_MAX(bb0miny, bb1miny)) >> 1; - double ln0c = (ln1dy * (double)(ln1a.x - originx)) + (ln1dx * (double)(ln1a.y - originy)); - double ln1c = (ln2dy * (double)(ln2a.x - originx)) + (ln2dx * (double)(ln2a.y - originy)); + T bb0minx = CC_MIN(ln1a.x, ln1b.x); + T bb0miny = CC_MIN(ln1a.y, ln1b.y); + T bb0maxx = CC_MAX(ln1a.x, ln1b.x); + T bb0maxy = CC_MAX(ln1a.y, ln1b.y); + T bb1minx = CC_MIN(ln2a.x, ln2b.x); + T bb1miny = CC_MIN(ln2a.y, ln2b.y); + T bb1maxx = CC_MAX(ln2a.x, ln2b.x); + T bb1maxy = CC_MAX(ln2a.y, ln2b.y); + T originx = (CC_MIN(bb0maxx, bb1maxx) + CC_MAX(bb0minx, bb1minx)) >> 1; + T originy = (CC_MIN(bb0maxy, bb1maxy) + CC_MAX(bb0miny, bb1miny)) >> 1; + double ln0c = (ln1dy * static_cast(ln1a.x - originx)) + + (ln1dx * static_cast(ln1a.y - originy)); + double ln1c = (ln2dy * static_cast(ln2a.x - originx)) + + (ln2dx * static_cast(ln2a.y - originy)); double hitx = ((ln1dx * ln1c) - (ln2dx * ln0c)) / det; double hity = ((ln2dy * ln0c) - (ln1dy * ln1c)) / det; - ip.x = originx + (int64_t)nearbyint(hitx); - ip.y = originy + (int64_t)nearbyint(hity); + if constexpr (std::numeric_limits::is_integer) + { + ip.x = originx + (T)nearbyint(hitx); + ip.y = originy + (T)nearbyint(hity); + } + else + { + ip.x = originx + static_cast(hitx); + ip.y = originy + static_cast(hity); + } return true; } #else - inline bool GetIntersectPoint(const Point64& ln1a, const Point64& ln1b, - const Point64& ln2a, const Point64& ln2b, Point64& ip) + template + inline bool GetIntersectPoint(const Point& ln1a, const Point& ln1b, + const Point& ln2a, const Point& ln2b, Point& ip) { // https://en.wikipedia.org/wiki/Line%E2%80%93line_intersection double dx1 = static_cast(ln1b.x - ln1a.x); @@ -780,13 +792,34 @@ namespace Clipper2Lib else if (t >= 1.0) ip = ln1b; else { - ip.x = static_cast(ln1a.x + t * dx1); - ip.y = static_cast(ln1a.y + t * dy1); + ip.x = static_cast(ln1a.x + t * dx1); + ip.y = static_cast(ln1a.y + t * dy1); } return true; } #endif + template + inline Point TranslatePoint(const Point& pt, double dx, double dy) + { +#ifdef USINGZ + return Point(pt.x + dx, pt.y + dy, pt.z); +#else + return Point(pt.x + dx, pt.y + dy); +#endif + } + + + template + inline Point ReflectPoint(const Point& pt, const Point& pivot) + { +#ifdef USINGZ + return Point(pivot.x + (pivot.x - pt.x), pivot.y + (pivot.y - pt.y), pt.z); +#else + return Point(pivot.x + (pivot.x - pt.x), pivot.y + (pivot.y - pt.y)); +#endif + } + inline bool SegmentsIntersect(const Point64& seg1a, const Point64& seg1b, const Point64& seg2a, const Point64& seg2b, bool inclusive = false) { diff --git a/CPP/Clipper2Lib/src/clipper.offset.cpp b/CPP/Clipper2Lib/src/clipper.offset.cpp index 3cae81cf..dab7d7a2 100644 --- a/CPP/Clipper2Lib/src/clipper.offset.cpp +++ b/CPP/Clipper2Lib/src/clipper.offset.cpp @@ -216,53 +216,6 @@ void ClipperOffset::BuildNormals(const Path64& path) norms.push_back(GetUnitNormal(*path_stop_iter, *(path.cbegin()))); } -inline PointD TranslatePoint(const PointD& pt, double dx, double dy) -{ -#ifdef USINGZ - return PointD(pt.x + dx, pt.y + dy, pt.z); -#else - return PointD(pt.x + dx, pt.y + dy); -#endif -} - -inline PointD ReflectPoint(const PointD& pt, const PointD& pivot) -{ -#ifdef USINGZ - return PointD(pivot.x + (pivot.x - pt.x), pivot.y + (pivot.y - pt.y), pt.z); -#else - return PointD(pivot.x + (pivot.x - pt.x), pivot.y + (pivot.y - pt.y)); -#endif -} - -PointD IntersectPoint(const PointD& pt1a, const PointD& pt1b, - const PointD& pt2a, const PointD& pt2b) -{ - if (pt1a.x == pt1b.x) //vertical - { - if (pt2a.x == pt2b.x) return PointD(0, 0); - - double m2 = (pt2b.y - pt2a.y) / (pt2b.x - pt2a.x); - double b2 = pt2a.y - m2 * pt2a.x; - return PointD(pt1a.x, m2 * pt1a.x + b2); - } - else if (pt2a.x == pt2b.x) //vertical - { - double m1 = (pt1b.y - pt1a.y) / (pt1b.x - pt1a.x); - double b1 = pt1a.y - m1 * pt1a.x; - return PointD(pt2a.x, m1 * pt2a.x + b1); - } - else - { - double m1 = (pt1b.y - pt1a.y) / (pt1b.x - pt1a.x); - double b1 = pt1a.y - m1 * pt1a.x; - double m2 = (pt2b.y - pt2a.y) / (pt2b.x - pt2a.x); - double b2 = pt2a.y - m2 * pt2a.x; - if (m1 == m2) return PointD(0, 0); - double x = (b2 - b1) / (m1 - m2); - return PointD(x, m1 * x + b1); - } -} - void ClipperOffset::DoBevel(const Path64& path, size_t j, size_t k) { PointD pt1, pt2; @@ -304,10 +257,8 @@ void ClipperOffset::DoSquare(const Path64& path, size_t j, size_t k) if (j == k) { PointD pt4 = PointD(pt3.x + vec.x * group_delta_, pt3.y + vec.y * group_delta_); - PointD pt = IntersectPoint(pt1, pt2, pt3, pt4); -#ifdef USINGZ - pt.z = ptQ.z; -#endif + PointD pt = ptQ; + GetIntersectPoint(pt1, pt2, pt3, pt4, pt); //get the second intersect point through reflecion path_out.push_back(Point64(ReflectPoint(pt, ptQ))); path_out.push_back(Point64(pt)); @@ -315,10 +266,8 @@ void ClipperOffset::DoSquare(const Path64& path, size_t j, size_t k) else { PointD pt4 = GetPerpendicD(path[j], norms[k], group_delta_); - PointD pt = IntersectPoint(pt1, pt2, pt3, pt4); -#ifdef USINGZ - pt.z = ptQ.z; -#endif + PointD pt = ptQ; + GetIntersectPoint(pt1, pt2, pt3, pt4, pt); path_out.push_back(Point64(pt)); //get the second intersect point through reflecion path_out.push_back(Point64(ReflectPoint(pt, ptQ))); diff --git a/CPP/Utils/CommonUtils.h b/CPP/Utils/CommonUtils.h index 68cee2ee..30e5f204 100644 --- a/CPP/Utils/CommonUtils.h +++ b/CPP/Utils/CommonUtils.h @@ -1,8 +1,9 @@ +#ifndef __COMMONUTILS_H__ +#define __COMMONUTILS_H__ + #include #include #include "clipper2/clipper.h" -#ifndef __COMMONUTILS_H__ -#define __COMMONUTILS_H__ Clipper2Lib::Path64 MakeRandomPoly(int width, int height, unsigned vertCnt) { From 2d7564f4a6f4ec5491876eff9a4ae9cac11d42e9 Mon Sep 17 00:00:00 2001 From: SebastianDirks <68428517+SebastianDirks@users.noreply.github.com> Date: Tue, 19 Dec 2023 02:06:19 +0100 Subject: [PATCH 16/64] CSharp Performance: when reusing clipper objects, currently clipper sets the internal lists capacity, even if the capacity already sufficient. This causes C# to drop the currently allocated array and create a new, smaller one. (#750) This changes check if the capacity is sufficient before setting it to reduces allocations. --- CSharp/Clipper2Lib/Clipper.Engine.cs | 23 +++++++++++++++-------- CSharp/Clipper2Lib/Clipper.Offset.cs | 6 +++--- 2 files changed, 18 insertions(+), 11 deletions(-) diff --git a/CSharp/Clipper2Lib/Clipper.Engine.cs b/CSharp/Clipper2Lib/Clipper.Engine.cs index bcc8ca57..1d9eb384 100644 --- a/CSharp/Clipper2Lib/Clipper.Engine.cs +++ b/CSharp/Clipper2Lib/Clipper.Engine.cs @@ -242,12 +242,19 @@ internal static void AddLocMin(Vertex vert, PathType polytype, bool isOpen, minimaList.Add(lm); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static void EnsureCapacity(this List list, int minCapacity) + { + if(list.Capacity < minCapacity) + list.Capacity = minCapacity; + } + internal static void AddPathsToVertexList(Paths64 paths, PathType polytype, bool isOpen, List minimaList, List vertexList) { int totalVertCnt = 0; foreach (Path64 path in paths) totalVertCnt += path.Count; - vertexList.Capacity = vertexList.Count + totalVertCnt; + vertexList.EnsureCapacity(vertexList.Count + totalVertCnt); foreach (Path64 path in paths) { @@ -800,7 +807,7 @@ protected void Reset() _isSortedMinimaList = true; } - _scanlineList.Capacity = _minimaList.Count; + _scanlineList.EnsureCapacity(_minimaList.Count); for (int i = _minimaList.Count - 1; i >= 0; i--) _scanlineList.Add(_minimaList[i].vertex.pt.Y); @@ -2982,8 +2989,8 @@ protected bool BuildPaths(Paths64 solutionClosed, Paths64 solutionOpen) { solutionClosed.Clear(); solutionOpen.Clear(); - solutionClosed.Capacity = _outrecList.Count; - solutionOpen.Capacity = _outrecList.Count; + solutionClosed.EnsureCapacity(_outrecList.Count); + solutionOpen.EnsureCapacity(_outrecList.Count); int i = 0; // _outrecList.Count is not static here because @@ -3089,7 +3096,7 @@ protected void BuildTree(PolyPathBase polytree, Paths64 solutionOpen) polytree.Clear(); solutionOpen.Clear(); if (_hasOpenPaths) - solutionOpen.Capacity = _outrecList.Count; + solutionOpen.EnsureCapacity(_outrecList.Count); int i = 0; // _outrecList.Count is not static here because @@ -3354,10 +3361,10 @@ public bool Execute(ClipType clipType, FillRule fillRule, ClearSolutionOnly(); if (!success) return false; - solutionClosed.Capacity = solClosed64.Count; + solutionClosed.EnsureCapacity(solClosed64.Count); foreach (Path64 path in solClosed64) solutionClosed.Add(Clipper.ScalePathD(path, _invScale)); - solutionOpen.Capacity = solOpen64.Count; + solutionOpen.EnsureCapacity(solOpen64.Count); foreach (Path64 path in solOpen64) solutionOpen.Add(Clipper.ScalePathD(path, _invScale)); @@ -3394,7 +3401,7 @@ public bool Execute(ClipType clipType, FillRule fillRule, PolyTreeD polytree, Pa if (!success) return false; if (oPaths.Count > 0) { - openPaths.Capacity = oPaths.Count; + openPaths.EnsureCapacity(oPaths.Count); foreach (Path64 path in oPaths) openPaths.Add(Clipper.ScalePathD(path, _invScale)); } diff --git a/CSharp/Clipper2Lib/Clipper.Offset.cs b/CSharp/Clipper2Lib/Clipper.Offset.cs index 6c95210d..67a6121d 100644 --- a/CSharp/Clipper2Lib/Clipper.Offset.cs +++ b/CSharp/Clipper2Lib/Clipper.Offset.cs @@ -162,7 +162,7 @@ private void ExecuteInternal(double delta) { _solution.Clear(); if (_groupList.Count == 0) return; - _solution.Capacity = CalcSolutionCapacity(); + _solution.EnsureCapacity(CalcSolutionCapacity()); // make sure the offset delta is significant if (Math.Abs(delta) < 0.5) @@ -257,7 +257,7 @@ public void Execute(DeltaCallback64 deltaCallback, Paths64 solution) internal static void GetMultiBounds(Paths64 paths, List boundsList) { - boundsList.Capacity = paths.Count; + boundsList.EnsureCapacity(paths.Count); foreach (Path64 path in paths) { if (path.Count < 1) @@ -545,7 +545,7 @@ private void BuildNormals(Path64 path) { int cnt = path.Count; _normals.Clear(); - _normals.Capacity = cnt; + _normals.EnsureCapacity(cnt); for (int i = 0; i < cnt - 1; i++) _normals.Add(GetUnitNormal(path[i], path[i + 1])); From 84b30f17f5bd726f8a22534f1f039934b8d96ba5 Mon Sep 17 00:00:00 2001 From: angusj Date: Wed, 20 Dec 2023 08:26:50 +1000 Subject: [PATCH 17/64] Minor code tidy of variable offset example --- .../VariableOffset/VariableOffset.cpp | 38 +++++++++---------- 1 file changed, 17 insertions(+), 21 deletions(-) diff --git a/CPP/Examples/VariableOffset/VariableOffset.cpp b/CPP/Examples/VariableOffset/VariableOffset.cpp index 62df56e4..9c6232db 100644 --- a/CPP/Examples/VariableOffset/VariableOffset.cpp +++ b/CPP/Examples/VariableOffset/VariableOffset.cpp @@ -23,17 +23,15 @@ void test1() { ClipperOffset co; co.SetDeltaCallback([delta](const Path64& path, - const PathD& path_norms, size_t curr_idx, size_t prev_idx) + const PathD& path_norms, size_t curr_idx, size_t prev_idx) { - // gradually scale down the offset to a minimum of 25% of delta - double high = static_cast(path.size() - 1) * 1.25; - return (high - curr_idx) / high * delta; + // gradually scale down the offset to a minimum of 25% of delta + double high = static_cast(path.size() - 1) * 1.25; + return (high - curr_idx) / high * delta; }); - Path64 ellipse = Ellipse(Rect64(0, 0, 200 * scale, 180 * scale)); - size_t el_size = ellipse.size() * 0.9; - ellipse.resize(el_size); - Paths64 subject = { ellipse }; + Paths64 subject{ Ellipse(Rect64(0, 0, 200 * scale, 180 * scale)) }; + subject[0].resize(subject[0].size() * 0.9); co.AddPaths(subject, JoinType::Miter, EndType::Round); Paths64 solution; @@ -53,17 +51,15 @@ void test2() { double delta = 10 * scale; ClipperOffset co; - co.SetDeltaCallback([delta](const Path64& path, + co.SetDeltaCallback([delta](const Path64& path, const PathD& path_norms, size_t curr_idx, size_t prev_idx) { - // calculate offset based on distance from the middle of the path - double mid_idx = static_cast(path.size()) / 2.0; - return delta * (1.0 - 0.70 * (std::fabs(curr_idx - mid_idx) / mid_idx)); + // calculate offset based on distance from the middle of the path + double mid_idx = static_cast(path.size()) / 2.0; + return delta * (1.0 - 0.70 * (std::fabs(curr_idx - mid_idx) / mid_idx)); }); - Path64 ellipse = Ellipse(Rect64(0, 0, 200 * scale, 180 * scale)); - size_t el_size = ellipse.size() * 0.9; - ellipse.resize(el_size); - Paths64 subject = { ellipse }; + Paths64 subject{ Ellipse(Rect64(0, 0, 200 * scale, 180 * scale)) }; + subject[0].resize(subject[0].size() * 0.9); co.AddPaths(subject, JoinType::Miter, EndType::Round); Paths64 solution; @@ -85,12 +81,12 @@ void test3() { ClipperOffset co; co.AddPaths(subject, JoinType::Miter, EndType::Polygon); - co.SetDeltaCallback([radius](const Path64& path, + co.SetDeltaCallback([radius](const Path64& path, const PathD& path_norms, size_t curr_idx, size_t prev_idx) { - // when multiplying the x & y of edge unit normal vectors, the value will be - // largest (0.5) when edges are at 45 deg. and least (-0.5) at negative 45 deg. - double delta = path_norms[curr_idx].y * path_norms[curr_idx].x; - return radius * 0.5 + radius * delta; + // when multiplying the x & y of edge unit normal vectors, the value will be + // largest (0.5) when edges are at 45 deg. and least (-0.5) at negative 45 deg. + double delta = path_norms[curr_idx].y * path_norms[curr_idx].x; + return radius * 0.5 + radius * delta; }); // solution From 348e9c4244268955ec465448f705c1d2788696f7 Mon Sep 17 00:00:00 2001 From: angusj Date: Thu, 21 Dec 2023 10:59:38 +1000 Subject: [PATCH 18/64] Fixed a bug when offsetting, using delta callback, a path with a single point. (#752) Minor code tidy (Delphi). --- CPP/Clipper2Lib/src/clipper.offset.cpp | 9 +++- CSharp/Clipper2Lib/Clipper.Offset.cs | 25 ++++++++--- Delphi/Clipper2Lib/Clipper.Core.pas | 35 ++++++++++++++- Delphi/Clipper2Lib/Clipper.Engine.pas | 57 +++++++++--------------- Delphi/Clipper2Lib/Clipper.Minkowski.pas | 11 ++--- Delphi/Clipper2Lib/Clipper.Offset.pas | 38 +++++++++------- Delphi/Clipper2Lib/Clipper.RectClip.pas | 14 +++--- Delphi/Clipper2Lib/Clipper.pas | 19 ++++---- 8 files changed, 122 insertions(+), 86 deletions(-) diff --git a/CPP/Clipper2Lib/src/clipper.offset.cpp b/CPP/Clipper2Lib/src/clipper.offset.cpp index dab7d7a2..2b05409f 100644 --- a/CPP/Clipper2Lib/src/clipper.offset.cpp +++ b/CPP/Clipper2Lib/src/clipper.offset.cpp @@ -1,6 +1,6 @@ /******************************************************************************* * Author : Angus Johnson * -* Date : 28 November 2023 * +* Date : 21 December 2023 * * Website : http://www.angusj.com * * Copyright : Angus Johnson 2010-2023 * * Purpose : Path Offset (Inflate/Shrink) * @@ -516,6 +516,13 @@ void ClipperOffset::DoGroupOffset(Group& group) if (pathLen == 1) // single point { + if (deltaCallback64_) + { + group_delta_ = deltaCallback64_(*path_in_it, norms, 0, 0); + if (group.is_reversed) group_delta_ = -group_delta_; + abs_delta = std::fabs(group_delta_); + } + if (group_delta_ < 1) continue; const Point64& pt = (*path_in_it)[0]; //single vertex so build a circle or square ... diff --git a/CSharp/Clipper2Lib/Clipper.Offset.cs b/CSharp/Clipper2Lib/Clipper.Offset.cs index 67a6121d..f47125e0 100644 --- a/CSharp/Clipper2Lib/Clipper.Offset.cs +++ b/CSharp/Clipper2Lib/Clipper.Offset.cs @@ -1,6 +1,6 @@ /******************************************************************************* * Author : Angus Johnson * -* Date : 28 November 2023 * +* Date : 21 December 2023 * * Website : http://www.angusj.com * * Copyright : Angus Johnson 2010-2023 * * Purpose : Path Offset (Inflate/Shrink) * @@ -420,13 +420,21 @@ private void DoBevel(Path64 path, int j, int k) if (j == k) { double absDelta = Math.Abs(_groupDelta); - pt1 = new Point64(path[j].X - absDelta * _normals[j].x, path[j].Y - absDelta * _normals[j].y); - pt2 = new Point64(path[j].X + absDelta * _normals[j].x, path[j].Y + absDelta * _normals[j].y); + pt1 = new Point64( + path[j].X - absDelta * _normals[j].x, + path[j].Y - absDelta * _normals[j].y); + pt2 = new Point64( + path[j].X + absDelta * _normals[j].x, + path[j].Y + absDelta * _normals[j].y); } else { - pt1 = new Point64(path[j].X + _groupDelta * _normals[k].x, path[j].Y + _groupDelta * _normals[k].y); - pt2 = new Point64(path[j].X + _groupDelta * _normals[j].x, path[j].Y + _groupDelta * _normals[j].y); + pt1 = new Point64( + path[j].X + _groupDelta * _normals[k].x, + path[j].Y + _groupDelta * _normals[k].y); + pt2 = new Point64( + path[j].X + _groupDelta * _normals[j].x, + path[j].Y + _groupDelta * _normals[j].y); } pathOut.Add(pt1); pathOut.Add(pt2); @@ -743,6 +751,13 @@ private void DoGroupOffset(Group group) { Point64 pt = p[0]; + if (DeltaCallback != null) + { + _groupDelta = DeltaCallback(p, _normals, 0, 0); + if (group.pathsReversed) _groupDelta = -_groupDelta; + absDelta = Math.Abs(_groupDelta); + } + // single vertex so build a circle or square ... if (group.endType == EndType.Round) { diff --git a/Delphi/Clipper2Lib/Clipper.Core.pas b/Delphi/Clipper2Lib/Clipper.Core.pas index 1ed1e243..d93f8f81 100644 --- a/Delphi/Clipper2Lib/Clipper.Core.pas +++ b/Delphi/Clipper2Lib/Clipper.Core.pas @@ -2,7 +2,7 @@ (******************************************************************************* * Author : Angus Johnson * -* Date : 5 November 2023 * +* Date : 21 December 2023 * * Website : http://www.angusj.com * * Copyright : Angus Johnson 2010-2023 * * Purpose : Core Clipper Library module * @@ -337,6 +337,11 @@ procedure QuickSort(SortList: TPointerList; procedure CheckPrecisionRange(var precision: integer); +function Iif(eval: Boolean; trueVal, falseVal: Boolean): Boolean; overload; +function Iif(eval: Boolean; trueVal, falseVal: integer): integer; overload; +function Iif(eval: Boolean; trueVal, falseVal: Int64): Int64; overload; +function Iif(eval: Boolean; trueVal, falseVal: double): double; overload; + const MaxInt64 = 9223372036854775807; MinInt64 = -MaxInt64; @@ -655,6 +660,34 @@ procedure TListEx.Swap(idx1, idx2: integer); // Miscellaneous Functions ... //------------------------------------------------------------------------------ +function Iif(eval: Boolean; trueVal, falseVal: Boolean): Boolean; + {$IFDEF INLINING} inline; {$ENDIF} +begin + if eval then Result := trueVal else Result := falseVal; +end; +//------------------------------------------------------------------------------ + +function Iif(eval: Boolean; trueVal, falseVal: integer): integer; + {$IFDEF INLINING} inline; {$ENDIF} +begin + if eval then Result := trueVal else Result := falseVal; +end; +//------------------------------------------------------------------------------ + +function Iif(eval: Boolean; trueVal, falseVal: Int64): Int64; + {$IFDEF INLINING} inline; {$ENDIF} +begin + if eval then Result := trueVal else Result := falseVal; +end; +//------------------------------------------------------------------------------ + +function Iif(eval: Boolean; trueVal, falseVal: double): double; + {$IFDEF INLINING} inline; {$ENDIF} +begin + if eval then Result := trueVal else Result := falseVal; +end; +//------------------------------------------------------------------------------ + procedure CheckPrecisionRange(var precision: integer); begin if (precision < -MaxDecimalPrecision) or (precision > MaxDecimalPrecision) then diff --git a/Delphi/Clipper2Lib/Clipper.Engine.pas b/Delphi/Clipper2Lib/Clipper.Engine.pas index 9339134f..6a576abf 100644 --- a/Delphi/Clipper2Lib/Clipper.Engine.pas +++ b/Delphi/Clipper2Lib/Clipper.Engine.pas @@ -2,7 +2,7 @@ (******************************************************************************* * Author : Angus Johnson * -* Date : 1 December 2023 * +* Date : 21 December 2023 * * Website : http://www.angusj.com * * Copyright : Angus Johnson 2010-2023 * * Purpose : This is the main polygon clipping module * @@ -1721,23 +1721,18 @@ procedure TClipperBase.SetWindCountForClosedPathEdge(e: PActive); if (Abs(e2.windCnt) > 1) then begin // outside prev poly but still inside another. - if (e2.windDx * e.windDx < 0) then - // reversing direction so use the same WC - e.windCnt := e2.windCnt else - // otherwise keep 'reducing' the WC by 1 (ie towards 0) ... - e.windCnt := e2.windCnt + e.windDx; + e.windCnt := Iif(e2.windDx * e.windDx < 0, + e2.windCnt, // reversing direction so use the same WC + e2.windCnt + e.windDx); end // now outside all polys of same polytype so set own WC ... else e.windCnt := e.windDx; end else begin //'e' must be inside 'e2' - if (e2.windDx * e.windDx < 0) then - // reversing direction so use the same WC - e.windCnt := e2.windCnt - else - // otherwise keep 'increasing' the WC by 1 (ie away from 0) ... - e.windCnt := e2.windCnt + e.windDx; + e.windCnt := Iif(e2.windDx * e.windDx < 0, + e2.windCnt, // reversing direction so use the same WC + e2.windCnt + e.windDx); // else keep 'increasing' the WC end; e.windCnt2 := e2.windCnt2; e2 := e2.nextInAEL; @@ -1778,8 +1773,8 @@ procedure TClipperBase.SetWindCountForOpenPathEdge(e: PActive); else if not IsOpen(e2) then inc(cnt1); e2 := e2.nextInAEL; end; - if Odd(cnt1) then e.windCnt := 1 else e.windCnt := 0; - if Odd(cnt2) then e.windCnt2 := 1 else e.windCnt2 := 0; + e.windCnt := Iif(Odd(cnt1), 1, 0); + e.windCnt2 := Iif(Odd(cnt2), 1, 0); end else begin // if FClipType in [ctUnion, ctDifference] then e.WindCnt := e.WindDx; @@ -2637,12 +2632,10 @@ function TClipperBase.IntersectEdges(e1, e2: PActive; pt: TPoint64): POutPt; e2.windCnt := e1WindCnt; end else begin - if e1.windCnt + e2.windDx = 0 then - e1.windCnt := -e1.windCnt else - Inc(e1.windCnt, e2.windDx); - if e2.windCnt - e1.windDx = 0 then - e2.windCnt := -e2.windCnt else - Dec(e2.windCnt, e1.windDx); + e1.windCnt := Iif(e1.windCnt + e2.windDx = 0, + -e1.windCnt, e1.windCnt + e2.windDx); + e2.windCnt := Iif(e2.windCnt - e1.windDx = 0, + -e2.windCnt, e2.windCnt - e1.windDx); end; end else begin @@ -2909,14 +2902,14 @@ function HorzontalsOverlap(const horz1a, horz1b, horz2a, horz2b: TPoint64): bool begin if horz1a.X < horz1b.X then begin - if horz2a.X < horz2b.X then - Result := HorzOverlapWithLRSet(horz1a, horz1b, horz2a, horz2b) else - Result := HorzOverlapWithLRSet(horz1a, horz1b, horz2b, horz2a); + Result := Iif(horz2a.X < horz2b.X, + HorzOverlapWithLRSet(horz1a, horz1b, horz2a, horz2b), + HorzOverlapWithLRSet(horz1a, horz1b, horz2b, horz2a)); end else begin - if horz2a.X < horz2b.X then - Result := HorzOverlapWithLRSet(horz1b, horz1a, horz2a, horz2b) else - Result := HorzOverlapWithLRSet(horz1b, horz1a, horz2b, horz2a); + Result := Iif(horz2a.X < horz2b.X, + HorzOverlapWithLRSet(horz1b, horz1a, horz2a, horz2b), + HorzOverlapWithLRSet(horz1b, horz1a, horz2b, horz2a)); end; end; //------------------------------------------------------------------------------ @@ -3175,12 +3168,8 @@ procedure TClipperBase.AddNewIntersectNode(e1, e2: PActive; topY: Int64); ip := GetClosestPointOnSegment(ip, e2.bot, e2.top) else begin - if (ip.Y < topY) then - ip.Y := topY else - ip.Y := fBotY; - if (absDx1 < absDx2) then - ip.X := TopX(e1, ip.Y) else - ip.X := TopX(e2, ip.Y); + ip.Y := Iif(ip.Y < topY, topY , fBotY); + ip.X := Iif(absDx1 < absDx2, TopX(e1, ip.Y), TopX(e2, ip.Y)); end; end; new(node); @@ -4031,9 +4020,7 @@ function TPolyPathBase.GetLevel: Integer; function TPolyPathBase.GetIsHole: Boolean; begin - if not Assigned(Parent) then - Result := false else - Result := not Odd(GetLevel); + Result := Iif(Assigned(Parent), not Odd(GetLevel), false); end; //------------------------------------------------------------------------------ diff --git a/Delphi/Clipper2Lib/Clipper.Minkowski.pas b/Delphi/Clipper2Lib/Clipper.Minkowski.pas index 1d7a82b2..bacb3ea2 100644 --- a/Delphi/Clipper2Lib/Clipper.Minkowski.pas +++ b/Delphi/Clipper2Lib/Clipper.Minkowski.pas @@ -2,7 +2,7 @@ (******************************************************************************* * Author : Angus Johnson * -* Date : 15 October 2022 * +* Date : 21 December 2023 * * Copyright : Angus Johnson 2010-2022 * * Purpose : Minkowski Addition and Difference * * License : http://www.boost.org/LICENSE_1_0.txt * @@ -51,9 +51,7 @@ function Minkowski(const Base, Path: TPath64; tmp: TPaths64; quad: TPath64; begin - if IsClosed then - delta := 0 else - delta := 1; + delta := Iif(IsClosed, 0 , 1); baseLen := Length(Base); pathLen := Length(Path); setLength(tmp, pathLen); @@ -71,10 +69,7 @@ function Minkowski(const Base, Path: TPath64; SetLength(quad, 4); SetLength(Result, (pathLen - delta) * baseLen); - - if IsClosed then - g := pathLen - 1 else - g := 0; + g := Iif(IsClosed, pathLen - 1, 0); for i := delta to pathLen - 1 do begin diff --git a/Delphi/Clipper2Lib/Clipper.Offset.pas b/Delphi/Clipper2Lib/Clipper.Offset.pas index 0aa35138..86c7727e 100644 --- a/Delphi/Clipper2Lib/Clipper.Offset.pas +++ b/Delphi/Clipper2Lib/Clipper.Offset.pas @@ -2,7 +2,7 @@ (******************************************************************************* * Author : Angus Johnson * -* Date : 28 November 2023 * +* Date : 21 December 2023 * * Website : http://www.angusj.com * * Copyright : Angus Johnson 2010-2023 * * Purpose : Path Offset (Inflate/Shrink) * @@ -426,9 +426,7 @@ function GetPerpendicD(const pt: TPoint64; const norm: TPointD; delta: double): function ToggleBoolIf(val, condition: Boolean): Boolean; {$IFDEF INLINING} inline; {$ENDIF} begin - if condition then - Result := not val else - Result := val; + Result := Iif(condition, not val, val); end; //------------------------------------------------------------------------------ @@ -444,13 +442,10 @@ procedure TClipperOffset.DoGroupOffset(group: TGroup); if group.endType = etPolygon then begin if (group.lowestPathIdx < 0) then fDelta := Abs(fDelta); - if group.reversed then - fGroupDelta := -fDelta else - fGroupDelta := fDelta; - end else - begin - fGroupDelta := Abs(fDelta);// * 0.5; - end; + fGroupDelta := Iif(group.reversed, -fDelta, fDelta); + end + else + fGroupDelta := Abs(fDelta); absDelta := Abs(fGroupDelta); if not ValidateBounds(group.boundsList, absDelta) then @@ -467,9 +462,10 @@ procedure TClipperOffset.DoGroupOffset(group: TGroup); // curve imprecision that's allowed is based on the size of the // offset (delta). Obviously very large offsets will almost always // require much less precision. See also offset_triginometry2.svg - if fArcTolerance > 0.01 then - arcTol := Min(absDelta, fArcTolerance) else - arcTol := Log10(2 + absDelta) * 0.25; // empirically derived + arcTol := Iif(fArcTolerance > 0.01, + Min(absDelta, fArcTolerance), + Log10(2 + absDelta) * 0.25); // empirically derived + //http://www.angusj.com/clipper2/Docs/Trigonometry.htm stepsPer360 := Pi / ArcCos(1 - arcTol / absDelta); if (stepsPer360 > absDelta * Pi) then @@ -493,6 +489,14 @@ procedure TClipperOffset.DoGroupOffset(group: TGroup); begin if fGroupDelta < 1 then Continue; pt0 := fInPath[0]; + + if Assigned(fDeltaCallback64) then + begin + fGroupDelta := fDeltaCallback64(fInPath, fNorms, 0, 0); + if TGroup(fGroupList[0]).reversed then fGroupDelta := -fGroupDelta; + absDelta := Abs(fGroupDelta); + end; + if (group.endType = etRound) then begin r := absDelta; @@ -955,9 +959,9 @@ procedure TClipperOffset.DoRound(j, k: Integer; angle: double); // when fDeltaCallback64 is assigned, fGroupDelta won't be constant, // so we'll need to do the following calculations for *every* vertex. absDelta := Abs(fGroupDelta); - if fArcTolerance > 0.01 then - arcTol := Min(absDelta, fArcTolerance) else - arcTol := Log10(2 + absDelta) * 0.25; // empirically derived + arcTol := Iif(fArcTolerance > 0.01, + Min(absDelta, fArcTolerance), + Log10(2 + absDelta) * 0.25); // empirically derived //http://www.angusj.com/clipper2/Docs/Trigonometry.htm stepsPer360 := Pi / ArcCos(1 - arcTol / absDelta); if (stepsPer360 > absDelta * Pi) then diff --git a/Delphi/Clipper2Lib/Clipper.RectClip.pas b/Delphi/Clipper2Lib/Clipper.RectClip.pas index c687a1fc..3eab8963 100644 --- a/Delphi/Clipper2Lib/Clipper.RectClip.pas +++ b/Delphi/Clipper2Lib/Clipper.RectClip.pas @@ -2,7 +2,7 @@ (******************************************************************************* * Author : Angus Johnson * -* Date : 9 September 2023 * +* Date : 21 December 2023 * * Website : http://www.angusj.com * * Copyright : Angus Johnson 2010-2023 * * Purpose : FAST rectangular clipping * @@ -282,7 +282,7 @@ function GetAdjacentLocation(loc: TLocation; isClockwise: Boolean): TLocation; var delta: integer; begin - if isClockwise then delta := 1 else delta := 3; + delta := Iif(isClockwise, 1 , 3); Result := TLocation((Ord(loc) + delta) mod 4); end; //------------------------------------------------------------------------------ @@ -291,9 +291,9 @@ function IsClockwise(prev, curr: TLocation; const prevPt, currPt, rectMidPt: TPoint64): Boolean; {$IFDEF INLINING} inline; {$ENDIF} begin - if AreOpposites(prev, curr) then - Result := CrossProduct(prevPt, rectMidPt, currPt) < 0 else - Result := HeadingClockwise(prev, curr); + Result := Iif(AreOpposites(prev, curr), + CrossProduct(prevPt, rectMidPt, currPt) < 0, + HeadingClockwise(prev, curr)); end; //------------------------------------------------------------------------------ @@ -517,9 +517,7 @@ procedure TRectClip64.AddCorner(prev, curr: TLocation); cnrIdx: integer; begin if prev = curr then Exit; - if (HeadingClockwise(prev, curr)) then - cnrIdx := Ord(prev) else - cnrIdx := Ord(curr); + cnrIdx := Iif(HeadingClockwise(prev, curr), Ord(prev), Ord(curr)); Add(fRectPath[cnrIdx]); end; //------------------------------------------------------------------------------ diff --git a/Delphi/Clipper2Lib/Clipper.pas b/Delphi/Clipper2Lib/Clipper.pas index 1d34c92b..1c36223b 100644 --- a/Delphi/Clipper2Lib/Clipper.pas +++ b/Delphi/Clipper2Lib/Clipper.pas @@ -2,7 +2,7 @@ (******************************************************************************* * Author : Angus Johnson * -* Date : 18 November 2023 * +* Date : 21 December 2023 * * Website : http://www.angusj.com * * Copyright : Angus Johnson 2010-2023 * * Purpose : This module provides a simple interface to the Clipper Library * @@ -836,9 +836,8 @@ function PerpendicDistSqrd(const pt, line1, line2: TPoint64): double; b := pt.Y - line1.Y; c := line2.X - line1.X; d := line2.Y - line1.Y; - if (c = 0) and (d = 0) then - result := 0 else - result := Sqr(a * d - c * b) / (c * c + d * d); + result := Iif((c = 0) and (d = 0), + 0, Sqr(a * d - c * b) / (c * c + d * d)); end; //------------------------------------------------------------------------------ @@ -865,7 +864,7 @@ function SimplifyPath(const path: TPath64; Result := nil; highI := High(path); - if isClosedPath then minHigh := 2 else minHigh := 1; + minHigh := Iif(isClosedPath, 2, 1); if highI < minHigh then Exit; SetLength(srArray, highI +1); @@ -874,9 +873,8 @@ function SimplifyPath(const path: TPath64; pt := path[0]; prev := @srArray[highI]; next := @srArray[1]; - if isClosedPath then - pdSqrd := PerpendicDistSqrd(path[0], path[highI], path[1]) else - pdSqrd := invalidD; + pdSqrd := Iif(isClosedPath, + PerpendicDistSqrd(path[0], path[highI], path[1]), invalidD); end; with srArray[highI] do @@ -884,9 +882,8 @@ function SimplifyPath(const path: TPath64; pt := path[highI]; prev := @srArray[highI-1]; next := @srArray[0]; - if isClosedPath then - pdSqrd := PerpendicDistSqrd(path[highI], path[highI-1], path[0]) else - pdSqrd := invalidD; + pdSqrd := Iif(isClosedPath, + PerpendicDistSqrd(path[highI], path[highI-1], path[0]), invalidD); end; for i := 1 to highI -1 do From 76fa070a647596369da56786345442cd7c19db4a Mon Sep 17 00:00:00 2001 From: angusj Date: Thu, 21 Dec 2023 18:06:25 +1000 Subject: [PATCH 19/64] Minor tweak to CMakeLists.txt (#754) --- CPP/CMakeLists.txt | 3 ++- CPP/Clipper2Lib/include/clipper2/clipper.core.h | 10 +++++----- CPP/Clipper2Lib/include/clipper2/clipper.engine.h | 10 +++++----- CPP/Clipper2Lib/src/clipper.offset.cpp | 12 ++++++------ 4 files changed, 18 insertions(+), 17 deletions(-) diff --git a/CPP/CMakeLists.txt b/CPP/CMakeLists.txt index f95dd295..df30921e 100644 --- a/CPP/CMakeLists.txt +++ b/CPP/CMakeLists.txt @@ -31,7 +31,8 @@ endif () include(GNUInstallDirs) set(CLIPPER2_INC_FOLDER ${PROJECT_SOURCE_DIR}/Clipper2Lib/include/clipper2) -configure_file(clipper.version.in ${CLIPPER2_INC_FOLDER}/clipper.version.h) +configure_file(clipper.version.in + ${CLIPPER2_INC_FOLDER}/clipper.version.h NEWLINE_STYLE UNIX) set(CLIPPER2_INC ${CLIPPER2_INC_FOLDER}/clipper.h diff --git a/CPP/Clipper2Lib/include/clipper2/clipper.core.h b/CPP/Clipper2Lib/include/clipper2/clipper.core.h index 75075cb1..0368d4b4 100644 --- a/CPP/Clipper2Lib/include/clipper2/clipper.core.h +++ b/CPP/Clipper2Lib/include/clipper2/clipper.core.h @@ -730,7 +730,7 @@ namespace Clipper2Lib return Area(poly) >= 0; } -#if CLIPPER2_HI_PRECISION +#if CLIPPER2_HI_PRECISION // caution: this will compromise performance // https://github.com/AngusJohnson/Clipper2/issues/317#issuecomment-1314023253 // See also CPP/BenchMark/GetIntersectPtBenchmark.cpp @@ -756,9 +756,9 @@ namespace Clipper2Lib T bb1maxy = CC_MAX(ln2a.y, ln2b.y); T originx = (CC_MIN(bb0maxx, bb1maxx) + CC_MAX(bb0minx, bb1minx)) >> 1; T originy = (CC_MIN(bb0maxy, bb1maxy) + CC_MAX(bb0miny, bb1miny)) >> 1; - double ln0c = (ln1dy * static_cast(ln1a.x - originx)) + + double ln0c = (ln1dy * static_cast(ln1a.x - originx)) + (ln1dx * static_cast(ln1a.y - originy)); - double ln1c = (ln2dy * static_cast(ln2a.x - originx)) + + double ln1c = (ln2dy * static_cast(ln2a.x - originx)) + (ln2dx * static_cast(ln2a.y - originy)); double hitx = ((ln1dx * ln1c) - (ln2dx * ln0c)) / det; double hity = ((ln2dy * ln0c) - (ln1dy * ln1c)) / det; @@ -774,7 +774,7 @@ namespace Clipper2Lib } return true; } -#else +#else template inline bool GetIntersectPoint(const Point& ln1a, const Point& ln1b, const Point& ln2a, const Point& ln2b, Point& ip) @@ -788,7 +788,7 @@ namespace Clipper2Lib double det = dy1 * dx2 - dy2 * dx1; if (det == 0.0) return false; double t = ((ln1a.x - ln2a.x) * dy2 - (ln1a.y - ln2a.y) * dx2) / det; - if (t <= 0.0) ip = ln1a; + if (t <= 0.0) ip = ln1a; else if (t >= 1.0) ip = ln1b; else { diff --git a/CPP/Clipper2Lib/include/clipper2/clipper.engine.h b/CPP/Clipper2Lib/include/clipper2/clipper.engine.h index 9dd55db7..4ff62cb2 100644 --- a/CPP/Clipper2Lib/include/clipper2/clipper.engine.h +++ b/CPP/Clipper2Lib/include/clipper2/clipper.engine.h @@ -33,7 +33,7 @@ namespace Clipper2Lib { //Note: all clipping operations except for Difference are commutative. enum class ClipType { None, Intersection, Union, Difference, Xor }; - + enum class PathType { Subject, Clip }; enum class JoinWith { None, Left, Right }; @@ -106,7 +106,7 @@ namespace Clipper2Lib { //Important: UP and DOWN here are premised on Y-axis positive down //displays, which is the orientation used in Clipper's development. /////////////////////////////////////////////////////////////////// - + struct Active { Point64 bot; Point64 top; @@ -251,7 +251,7 @@ namespace Clipper2Lib { void JoinOutrecPaths(Active &e1, Active &e2); void FixSelfIntersects(OutRec* outrec); void DoSplitOp(OutRec* outRec, OutPt* splitOp); - + inline void AddTrialHorzJoin(OutPt* op); void ConvertHorzSegsToJoins(); void ProcessHorzJoins(); @@ -331,7 +331,7 @@ namespace Clipper2Lib { unsigned lvl = Level(); //Even levels except level 0 return lvl && !(lvl & 1); - } + } }; typedef typename std::vector> PolyPath64List; @@ -420,7 +420,7 @@ namespace Clipper2Lib { void SetScale(double value) { scale_ = value; } double Scale() const { return scale_; } - + PolyPathD* AddChild(const Path64& path) override { int error_code = 0; diff --git a/CPP/Clipper2Lib/src/clipper.offset.cpp b/CPP/Clipper2Lib/src/clipper.offset.cpp index 2b05409f..70189087 100644 --- a/CPP/Clipper2Lib/src/clipper.offset.cpp +++ b/CPP/Clipper2Lib/src/clipper.offset.cpp @@ -67,7 +67,7 @@ int GetLowestClosedPathIdx(std::vector& boundsList) int i = -1, result = -1; Point64 botPt = Point64(INT64_MAX, INT64_MIN); for (const Rect64& r : boundsList) - { + { ++i; if (!r.IsValid()) continue; // ignore invalid paths else if (r.bottom > botPt.y || (r.bottom == botPt.y && r.left < botPt.x)) @@ -103,7 +103,7 @@ inline double Hypot(double x, double y) } inline PointD NormalizeVector(const PointD& vec) -{ +{ double h = Hypot(vec.x, vec.y); if (AlmostZero(h)) return PointD(0,0); double inverseHypot = 1 / h; @@ -358,7 +358,7 @@ void ClipperOffset::OffsetPoint(Group& group, const Path64& path, size_t j, size // is concave path_out.push_back(GetPerpendic(path[j], norms[k], group_delta_)); // this extra point is the only (simple) way to ensure that - // path reversals are fully cleaned with the trailing clipper + // path reversals are fully cleaned with the trailing clipper path_out.push_back(path[j]); // (#405) path_out.push_back(GetPerpendic(path[j], norms[j], group_delta_)); } @@ -394,7 +394,7 @@ void ClipperOffset::OffsetOpenJoined(Group& group, const Path64& path) OffsetPolygon(group, path); Path64 reverse_path(path); std::reverse(reverse_path.begin(), reverse_path.end()); - + //rebuild normals // BuildNormals(path); std::reverse(norms.begin(), norms.end()); norms.push_back(norms[0]); @@ -408,7 +408,7 @@ void ClipperOffset::OffsetOpenPath(Group& group, const Path64& path) { // do the line start cap if (deltaCallback64_) group_delta_ = deltaCallback64_(path, norms, 0, 0); - + if (std::fabs(group_delta_) <= floating_point_tolerance) path_out.push_back(path[0]); else @@ -426,7 +426,7 @@ void ClipperOffset::OffsetOpenPath(Group& group, const Path64& path) break; } } - + size_t highI = path.size() - 1; // offset the left side going forward for (Path64::size_type j = 1, k = 0; j < highI; k = j, ++j) From 31acd3ade8148973336ef175147bda8b48fdd1f2 Mon Sep 17 00:00:00 2001 From: angusj Date: Fri, 22 Dec 2023 20:55:45 +1000 Subject: [PATCH 20/64] Fixed a compiler error when enabling CLIPPER2_HI_PRECISION. --- .../include/clipper2/clipper.core.h | 29 +++++++++++++------ 1 file changed, 20 insertions(+), 9 deletions(-) diff --git a/CPP/Clipper2Lib/include/clipper2/clipper.core.h b/CPP/Clipper2Lib/include/clipper2/clipper.core.h index 0368d4b4..dae00653 100644 --- a/CPP/Clipper2Lib/include/clipper2/clipper.core.h +++ b/CPP/Clipper2Lib/include/clipper2/clipper.core.h @@ -1,6 +1,6 @@ /******************************************************************************* * Author : Angus Johnson * -* Date : 13 December 2023 * +* Date : 22 December 2023 * * Website : http://www.angusj.com * * Copyright : Angus Johnson 2010-2023 * * Purpose : Core Clipper Library structures and functions * @@ -754,21 +754,32 @@ namespace Clipper2Lib T bb1miny = CC_MIN(ln2a.y, ln2b.y); T bb1maxx = CC_MAX(ln2a.x, ln2b.x); T bb1maxy = CC_MAX(ln2a.y, ln2b.y); - T originx = (CC_MIN(bb0maxx, bb1maxx) + CC_MAX(bb0minx, bb1minx)) >> 1; - T originy = (CC_MIN(bb0maxy, bb1maxy) + CC_MAX(bb0miny, bb1miny)) >> 1; - double ln0c = (ln1dy * static_cast(ln1a.x - originx)) + - (ln1dx * static_cast(ln1a.y - originy)); - double ln1c = (ln2dy * static_cast(ln2a.x - originx)) + - (ln2dx * static_cast(ln2a.y - originy)); - double hitx = ((ln1dx * ln1c) - (ln2dx * ln0c)) / det; - double hity = ((ln2dy * ln0c) - (ln1dy * ln1c)) / det; + if constexpr (std::numeric_limits::is_integer) { + int64_t originx = (CC_MIN(bb0maxx, bb1maxx) + CC_MAX(bb0minx, bb1minx)) >> 1; + int64_t originy = (CC_MIN(bb0maxy, bb1maxy) + CC_MAX(bb0miny, bb1miny)) >> 1; + double ln0c = (ln1dy * static_cast(ln1a.x - originx)) + + (ln1dx * static_cast(ln1a.y - originy)); + double ln1c = (ln2dy * static_cast(ln2a.x - originx)) + + (ln2dx * static_cast(ln2a.y - originy)); + double hitx = ((ln1dx * ln1c) - (ln2dx * ln0c)) / det; + double hity = ((ln2dy * ln0c) - (ln1dy * ln1c)) / det; + ip.x = originx + (T)nearbyint(hitx); ip.y = originy + (T)nearbyint(hity); } else { + double originx = (CC_MIN(bb0maxx, bb1maxx) + CC_MAX(bb0minx, bb1minx)) / 2.0; + double originy = (CC_MIN(bb0maxy, bb1maxy) + CC_MAX(bb0miny, bb1miny)) / 2.0; + double ln0c = (ln1dy * static_cast(ln1a.x - originx)) + + (ln1dx * static_cast(ln1a.y - originy)); + double ln1c = (ln2dy * static_cast(ln2a.x - originx)) + + (ln2dx * static_cast(ln2a.y - originy)); + double hitx = ((ln1dx * ln1c) - (ln2dx * ln0c)) / det; + double hity = ((ln2dy * ln0c) - (ln1dy * ln1c)) / det; + ip.x = originx + static_cast(hitx); ip.y = originy + static_cast(hity); } From 24c42c9222cdca5d76d1eb4b215b85db455b3be8 Mon Sep 17 00:00:00 2001 From: angusj Date: Thu, 28 Dec 2023 10:00:18 +1000 Subject: [PATCH 21/64] Added WASM link to ReadMe --- README.md | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 42c23922..2a7dbc54 100644 --- a/README.md +++ b/README.md @@ -47,6 +47,9 @@ The Clipper2 library performs **intersection**, **union**, **difference**
### Ports to other languages -**Java**: https://github.com/micycle1/Clipper2-java/
-**Kotlin**: https://github.com/Monkey-Maestro/clipper2-kotlin
-**golang**: https://github.com/epit3d/goclipper2 +| lang. | link | +| ------ | ------ | +| **WASM** | https://github.com/ErikSom/Clipper2-WASM/ | +| **Java** | https://github.com/micycle1/Clipper2-java/ | +| **Kotlin** | https://github.com/Monkey-Maestro/clipper2-kotlin | +| **golang** | https://github.com/epit3d/goclipper2 | From cee24f5314e9fc0f502c0471520d1366a8add32d Mon Sep 17 00:00:00 2001 From: angusj Date: Thu, 11 Jan 2024 14:46:49 +1000 Subject: [PATCH 22/64] Fixed LF vs CRLF bug in C++ CI testing (#764). Other minor tweaks --- CPP/BenchMark/GetIntersectPtBenchmark.cpp | 472 ++++++++++++++++------ CPP/CMakeLists.txt | 1 + CPP/Tests/TestLines.cpp | 2 - CPP/Tests/TestOffsets.cpp | 2 +- CPP/Tests/TestPolygons.cpp | 1 - CPP/Tests/TestPolytreeHoles.cpp | 1 - CPP/Utils/ClipFileLoad.cpp | 43 +- 7 files changed, 376 insertions(+), 146 deletions(-) diff --git a/CPP/BenchMark/GetIntersectPtBenchmark.cpp b/CPP/BenchMark/GetIntersectPtBenchmark.cpp index 1ca12e23..229bcff1 100644 --- a/CPP/BenchMark/GetIntersectPtBenchmark.cpp +++ b/CPP/BenchMark/GetIntersectPtBenchmark.cpp @@ -3,14 +3,13 @@ #include "clipper2/clipper.core.h" #include "CommonUtils.h" #include "ClipFileLoad.h" -#include #include #include #include using namespace Clipper2Lib; -enum ConsoleTextColor { +enum TextColor { reset = 0, //normal text colors ... red = 31, green = 32, yellow = 33, blue = 34, magenta = 35, cyan = 36, white = 37, @@ -26,45 +25,259 @@ enum ConsoleTextColor { struct SetConsoleTextColor { private: - ConsoleTextColor _color; + TextColor _color; public: - SetConsoleTextColor(ConsoleTextColor color) : _color(color) {}; + SetConsoleTextColor(TextColor color) : _color(color) {}; static friend std::ostream& operator<< (std::ostream& out, SetConsoleTextColor const& scc) { return out << "\x1B[" << scc._color << "m"; } }; + +////////////////////////////////////////////////////////////////////////////////////// +// Int128 - class that very minimally supports 128bit integer math ////////////////////////////////////////////////////////////////////////////////////// +class Int128 +{ +public: + uint64_t lo; + int64_t hi; -///////////////////////////////////////////////////////// -// Miscellaneous functions -///////////////////////////////////////////////////////// + Int128(int64_t _lo = 0) + { + lo = (uint64_t)_lo; + if (_lo < 0) hi = -1; else hi = 0; + } + + Int128(const Int128& val) : lo(val.lo), hi(val.hi) {} + + Int128(const int64_t& _hi, const uint64_t& _lo) : lo(_lo), hi(_hi) {} + + Int128& operator = (const int64_t& val) + { + lo = (uint64_t)val; + if (val < 0) hi = -1; else hi = 0; + return *this; + } + + bool operator == (const Int128& val) const + { + return (hi == val.hi && lo == val.lo); + } + + bool operator != (const Int128& val) const + { + return !(*this == val); + } + + bool operator > (const Int128& val) const + { + return (hi != val.hi) ? hi > val.hi: lo > val.lo; + } + + bool operator < (const Int128& val) const + { + return (hi != val.hi) ? hi < val.hi : lo < val.lo; + } + + bool operator >= (const Int128& val) const + { + return !(*this < val); + } + + bool operator <= (const Int128& val) const + { + return !(*this > val); + } + + bool is_zero() const + { + return (hi == 0 && lo == 0); + } + + bool is_negative() const + { + return (hi < 0); + } + + Int128& operator += (const Int128& rhs) + { + hi += rhs.hi; + lo += rhs.lo; + if (lo < rhs.lo) hi++; + return *this; + } + + Int128 operator + (const Int128& rhs) const + { + Int128 result(*this); + result += rhs; + return result; + } -double GetSineAbc(const Point64& a, const Point64& b, const Point64& c) + Int128& operator -= (const Int128& rhs) + { + *this += -rhs; + return *this; + } + + Int128 operator - (const Int128& rhs) const + { + Int128 result(*this); + result -= rhs; + return result; + } + + Int128 operator-() const //unary negation + { + return (lo == 0) ? Int128(-hi, 0) : Int128(~hi, ~lo + 1); + } + + void negate() + { + if (lo == 0) hi = -hi; + else { hi = ~hi; lo = ~lo + 1; } + } + + operator double() const + { + const double shift64 = 18446744073709551616.0; //2^64 + if (hi < 0) + { + if (lo == 0) return (double)hi * shift64; + else return -(double)(~lo + ~hi * shift64); + } + else + return (double)(lo + hi * shift64); + } +}; + +static inline Int128 multiply(int64_t lhs, int64_t rhs) { - double dpB = DotProduct(a, b, c); - double SqrCosB = dpB * dpB / (DistanceSqr(a, b) * DistanceSqr(b, c)); - double cos2B = SqrCosB * 2 - 1; // trig. itentity - return std::sqrt(1 - cos2B); // sin(B) = Sqrt(1-cos(2B)) -} + bool negate = (lhs < 0) != (rhs < 0); + + if (lhs < 0) lhs = -lhs; + uint64_t int1Hi = uint64_t(lhs) >> 32; + uint64_t int1Lo = uint64_t(lhs & 0xFFFFFFFF); + + if (rhs < 0) rhs = -rhs; + uint64_t int2Hi = uint64_t(rhs) >> 32; + uint64_t int2Lo = uint64_t(rhs & 0xFFFFFFFF); + + uint64_t a = int1Hi * int2Hi; + uint64_t b = int1Lo * int2Lo; + uint64_t c = int1Hi * int2Lo + int1Lo * int2Hi; + + Int128 result; + result.hi = a + (c >> 32); + result.lo = c << 32; + result.lo += b; + if (result.lo < b) result.hi++; + return negate ? -result : result; +}; -static inline Point64 MakeRandomPoint(int64_t min_val, int64_t max_val) +static int64_t divide(Int128 dividend, Int128 divisor) { - std::random_device rd; - std::mt19937 gen(rd()); - std::uniform_int_distribution x(min_val, max_val); - std::uniform_int_distribution y(min_val, max_val); - return Point64(x(gen), y(gen)); + // this function assumes that the parameter values will + // generate a result that fits into a 64bit integer. + bool negate = (divisor.hi < 0) != (dividend.hi < 0); + if (dividend.hi < 0) dividend = -dividend; + if (divisor.hi < 0) divisor = -divisor; + if (divisor.lo == 0 && divisor.hi == 0) + throw "Int128: divide by zero error"; + + if (dividend == divisor) return negate ? -1 : 1; + if (divisor > dividend) return 0; + + Int128 cntr = Int128(1); + while (divisor.hi >= 0 && divisor <= dividend) + { + divisor.hi <<= 1; + if ((int64_t)divisor.lo < 0) divisor.hi++; + divisor.lo <<= 1; + + cntr.hi <<= 1; + if ((int64_t)cntr.lo < 0) cntr.hi++; + cntr.lo <<= 1; + } + divisor.lo >>= 1; + if (divisor.hi & 1) + divisor.lo |= 0x8000000000000000LL; + divisor.hi >>= 1; + + cntr.lo >>= 1; + if (cntr.hi & 1) + cntr.lo |= 0x8000000000000000LL; + cntr.hi >>= 1; + + Int128 result = Int128(0); + while (cntr.hi != 0 || cntr.lo != 0) + { + if (dividend >= divisor) + { + dividend -= divisor; + result.hi |= cntr.hi; + result.lo |= cntr.lo; + } + divisor.lo >>= 1; + if (divisor.hi & 1) + divisor.lo |= 0x8000000000000000LL; + divisor.hi >>= 1; + + cntr.lo >>= 1; + if (cntr.hi & 1) + cntr.lo |= 0x8000000000000000LL; + cntr.hi >>= 1; + } + if (result.hi || (int64_t)result.lo < 0) + return negate ? INT64_MIN : INT64_MAX; + else + return negate ? -(int64_t)result.lo : result.lo; } -typedef std::function GipFunction; +static inline int64_t muldiv(Int128 lhs, int64_t rhs, Int128 divisor) +{ + // this function assumes that the parameter values will + // generate a result that fits into a 64bit integer. + int64_t sign = (lhs.is_negative() != divisor.is_negative()) != (rhs < 0) ? -2 : 2; + if (lhs.is_negative()) lhs.negate(); + if (divisor.is_negative()) divisor.negate(); + if (rhs < 0) rhs = -rhs; + + // if 'lhs' is very large, then 'divisor' will be very large too + while (lhs.hi && divisor.hi) + { + // divide dividend and divisor by 2 ... + lhs.lo >>= 1; + if (lhs.hi & 1) + lhs.lo |= 0x8000000000000000LL; + lhs.hi >>= 1; + divisor.lo >>= 1; + if (divisor.hi & 1) + divisor.lo |= 0x8000000000000000LL; + divisor.hi >>= 1; + } + + lhs.lo >>= 1; // divide by 2 to avoid casting a 'sign' bit + Int128 result = multiply((int64_t)lhs.lo, rhs); + result.hi += lhs.hi * rhs; + return divide(result, divisor) * sign; // and multiplies by 2 +}; ///////////////////////////////////////////////////////// -// GIP_Current: This is the current Clipper2 GetIntersectPoint code +// Several GetIntersectPoint functions for testing ///////////////////////////////////////////////////////// + +const int number_of_test_functions = 4; + +typedef std::function GipFunction; + +// GIP_Current: This is Clipper2's current GetIntersectPoint. +// It's definitely the fastest function, but its accuracy declines +// a little when using very large 64bit integers (eg +/-10e17). static bool GIP_Current(const Point64& ln1a, const Point64& ln1b, const Point64& ln2a, const Point64& ln2b, Point64& ip) { @@ -86,12 +299,11 @@ static bool GIP_Current(const Point64& ln1a, const Point64& ln1b, return true; } -///////////////////////////////////////////////////////// // GIP_Func_F: This is mathVertexLineLineIntersection_F // https://github.com/AngusJohnson/Clipper2/issues/317#issuecomment-1314023253 -///////////////////////////////////////////////////////// #define CC_MIN(x,y) ((x)>(y)?(y):(x)) #define CC_MAX(x,y) ((x)<(y)?(y):(x)) + static bool GIP_Func_F(const Point64& ln1a, const Point64& ln1b, const Point64& ln2a, const Point64& ln2b, Point64& ip) { @@ -120,15 +332,10 @@ static bool GIP_Func_F(const Point64& ln1a, const Point64& ln1b, return true; } -///////////////////////////////////////////////////////// -// GIP_F_Mod: Modified GIP_Func_F (see above). -// Replaces nearbyint with static_cast. Surprisingly, while -// this function is a little faster here than GIP_Func_F, -// it's much slower than GIP_Func_F when using it as a -// GetIntersectPoint() replacement in clipper.core.h. -///////////////////////////////////////////////////////// -#define CC_MIN(x,y) ((x)>(y)?(y):(x)) -#define CC_MAX(x,y) ((x)<(y)?(y):(x)) +// GIP_F_Mod: GIP_Func_F except replaces nearbyint with static casts. +// Surprisingly, while this function is faster that GIP_Func_F here, +// it's much slower than both GIP_Func_F and GIP_Current when using it +// as a replacement for GetIntersectPoint() in clipper.core.h. static bool GIP_F_Mod(const Point64& ln1a, const Point64& ln1b, const Point64& ln2a, const Point64& ln2b, Point64& ip) { @@ -157,115 +364,143 @@ static bool GIP_F_Mod(const Point64& ln1a, const Point64& ln1b, return true; } - -struct TestRecord +// GIP_128: GetIntersectPoint using 128bit integer precision +// This function is the most precise, but it's also very slow. +static bool GIP_128(const Point64& ln1a, const Point64& ln1b, + const Point64& ln2a, const Point64& ln2b, Point64& ip) { -public: - Point64 actual, pt1, pt2, pt3, pt4; - std::vector results; - TestRecord(int participants, const Point64& intersect_pt, - const Point64& p1, const Point64& p2, const Point64& p3, const Point64& p4) : - actual(intersect_pt), pt1(p1), pt2(p2), pt3(p3), pt4(p4) { - results.resize(participants); - }; -}; - -typedef std::vector::iterator test_iter; - -// global data -std::vector tests; - + int64_t dx1 = ln1b.x - ln1a.x; + int64_t dy1 = ln1b.y - ln1a.y; + int64_t dx2 = ln2b.x - ln2a.x; + int64_t dy2 = ln2b.y - ln2a.y; + Int128 det = multiply(dy1, dx2) - multiply(dy2, dx1); + if (det.is_zero()) return false; + + Int128 t_num = multiply(ln1a.x - ln2a.x, dy2) - multiply(ln1a.y - ln2a.y, dx2); + bool is_negative = t_num.is_negative() != det.is_negative(); + if (t_num.is_zero() || is_negative) + ip = ln1a; + else if (t_num.is_negative() == -t_num > -det) + ip = ln1b; + else + { + ip.x = ln1a.x + muldiv(t_num, dx1, det); + ip.y = ln1a.y + muldiv(t_num, dy1, det); + } + return true; +} -static inline GipFunction GetGipFunc(int index) +static inline GipFunction GetGipFunc(int64_t index) { - GipFunction result; switch (index) { - case 0: result = GIP_Current; break; - case 1: result = GIP_Func_F; break; - case 2: result = GIP_F_Mod; break; - default: throw "oops! - wrong function!"; + case 0: return GIP_Current; + case 1: return GIP_Func_F; + case 2: return GIP_F_Mod; + case 3: return GIP_128; + default: throw "Invalid function!"; } - return result; } -static inline std::string GetGipFuncName(int index) +static inline std::string GetGipFuncName(int64_t index) { - std::string result; switch (index) { - case 0: result = "GIP_Current"; break; - case 1: result = "GIP_Func_F "; break; - case 2: result = "GIP_F_Mod "; break; - default: throw "oops!"; + case 0: return "GIP_Current"; + case 1: return "GIP_Func_F "; + case 2: return "GIP_F_Mod "; + case 3: return "GIP_128 "; + default: throw "Invalid function!"; } - return result; } ///////////////////////////////////////////////////////// -// Benchmark callback functions +// Other miscellaneous functions ///////////////////////////////////////////////////////// -static void BM_GIP_Current(benchmark::State& state) +double GetSineFrom3Points(const Point64& a, const Point64& b, const Point64& c) { - Point64 ip; - for (auto _ : state) - { - for (test_iter cit = tests.begin(); cit != tests.end(); ++cit) - { - GIP_Current((*cit).pt1, (*cit).pt2, (*cit).pt3, (*cit).pt4, ip); - (*cit).results[0] = ip; - } - } + double dpB = DotProduct(a, b, c); + double SqrCosB = dpB * dpB / (DistanceSqr(a, b) * DistanceSqr(b, c)); + double cos2B = SqrCosB * 2 - 1; // trig. itentity + return std::sqrt(1 - cos2B); // sin(B) = Sqrt(1-cos(2B)) } -static void BM_GIP_Func_F(benchmark::State& state) +static inline Point64 MakeRandomPoint(int64_t min_val, int64_t max_val) { - Point64 ip; - for (auto _ : state) - { - for (test_iter cit = tests.begin(); cit != tests.end(); ++cit) - { - GIP_Func_F((*cit).pt1, (*cit).pt2, (*cit).pt3, (*cit).pt4, ip); - (*cit).results[1] = ip; - } - } + std::random_device rd; + std::mt19937 gen(rd()); + std::uniform_int_distribution x(min_val, max_val); + std::uniform_int_distribution y(min_val, max_val); + return Point64(x(gen), y(gen)); } -static void BM_GIP_F_Mod(benchmark::State& state) +///////////////////////////////////////////////////////// +// global data storage +///////////////////////////////////////////////////////// + +struct TestRecord +{ +public: + Point64 actual, pt1, pt2, pt3, pt4; + std::vector results; + TestRecord(const Point64& intersect_pt, + const Point64& p1, const Point64& p2, const Point64& p3, const Point64& p4) : + actual(intersect_pt), pt1(p1), pt2(p2), pt3(p3), pt4(p4) { + results.resize(number_of_test_functions); + }; +}; + +std::vector tests; +typedef std::vector::iterator test_iter; + +///////////////////////////////////////////////////////// +// Benchmark callback functions +///////////////////////////////////////////////////////// + +static void BM_GIP(benchmark::State& state) { - Point64 ip; + int64_t idx = state.range(0); + state.SetLabel(GetGipFuncName(idx)); + GipFunction func = GetGipFunc(idx); for (auto _ : state) { - for (test_iter cit = tests.begin(); cit != tests.end(); ++cit) + for (test_iter test = tests.begin(); test != tests.end(); ++test) { - GIP_F_Mod((*cit).pt1, (*cit).pt2, (*cit).pt3, (*cit).pt4, ip); - (*cit).results[2] = ip; + Point64 ip; + func(test->pt1, test->pt2, test->pt3, test->pt4, ip); + test->results[idx] = ip; } } } +static void CustomArguments(benchmark::internal::Benchmark* b) +{ + for (int i = 0; i < number_of_test_functions; ++i) + b->Args({ i }); +} + ///////////////////////////////////////////////////////// // Main Entry ///////////////////////////////////////////////////////// int main(int argc, char** argv) { - - const int participants = 3; - //setup benchmarking ... benchmark::Initialize(0, nullptr); - BENCHMARK(BM_GIP_Current); - BENCHMARK(BM_GIP_Func_F); - BENCHMARK(BM_GIP_F_Mod); + BENCHMARK(BM_GIP)->Apply(CustomArguments); + + // the closer test segments are to collinear, the less accurate + // calculations will be in determining their intersection points. + const double min_angle_degrees = 0.5; + const double sine_min_angle = std::sin(min_angle_degrees *PI / 180.0); bool first_pass = true; for (int current_pow10 = 12; current_pow10 <= 18; ++current_pow10) { - // create multiple TestRecords containing segment pairs that intersect - // at their midpoints, while using random coordinates that are - // restricted to the specified power of 10 range + // using random coordinates that are restricted to the specified + // power of 10 range, create multiple TestRecords containing + // segment pairs that intersect at their midpoints int64_t max_coord = static_cast(pow(10, current_pow10)); for (int64_t i = 0; i < 100000; ++i) { @@ -274,15 +509,13 @@ int main(int argc, char** argv) Point64 actual = MidPoint(ip1, ip2); Point64 ip3 = MakeRandomPoint(-max_coord, max_coord); Point64 ip4 = ReflectPoint(ip3, actual); - Point64 _; - // the closer segments are to collinear, the less accurate are - // calculations that determine intersection points. - // So exclude segments that are **almost** collinear. eg. sin(1deg) ~= 0.017 - if (std::abs(GetSineAbc(ip1, actual, ip3)) < 0.017) continue; - // alternatively, only exclude segments that are collinear + // Exclude segments that are **almost** collinear. + if (std::abs(GetSineFrom3Points(ip1, actual, ip3)) < sine_min_angle) continue; + // Alternatively, just exclude segments that are collinear //if (!CrossProduct(ip1, actual, ip3)) continue; - tests.push_back(TestRecord(participants, actual, ip1, ip2, ip3, ip4)); + + tests.push_back(TestRecord(actual, ip1, ip2, ip3, ip4)); } if (first_pass) @@ -293,39 +526,38 @@ int main(int argc, char** argv) std::cout << std::endl << SetConsoleTextColor(green_bold) << "Benchmark GetIntersectPoint performance ... " << SetConsoleTextColor(reset) << std::endl << std::endl; - // using each test function in the callback functions above, benchmarking - // will calculate and store intersect points for each TestRecord - benchmark::RunSpecifiedBenchmarks(benchmark::CreateDefaultDisplayReporter()); + benchmark::RunSpecifiedBenchmarks(); std::cout << std::endl << std::endl << SetConsoleTextColor(green_bold) << "Compare function accuracy ..." << SetConsoleTextColor(reset) << std::endl << "and show how it deteriorates when using very large coordinate ranges." << std::endl << "Distance error is the distance between the calculated and actual intersection points." << std::endl << - "The largest errors will occur whenever intersecting segments are almost collinear." << std::endl; + "(The largest errors will occur whenever the segments are close to collinear.)" << std::endl; } else { - for (int i = 0; i < participants; ++i) + for (int i = 0; i < number_of_test_functions; ++i) { // although we're not benchmarking, we still need to collect the calculated // intersect points of each TestRecord for each participating function. + // (In first_pass above, benchmark::RunSpecifiedBenchmarks() does this internally.) Point64 ip; GipFunction gip_func = GetGipFunc(i); - for (test_iter cit = tests.begin(); cit != tests.end(); ++cit) + for (test_iter test = tests.begin(); test != tests.end(); ++test) { - gip_func((*cit).pt1, (*cit).pt2, (*cit).pt3, (*cit).pt4, ip); - (*cit).results[i] = ip; + gip_func(test->pt1, test->pt2, test->pt3, test->pt4, ip); + test->results[i] = ip; } } } - double avg_dists[participants] = { 0 }; - double worst_dists[participants] = { 0 }; + double avg_dists[number_of_test_functions] = { 0 }; + double worst_dists[number_of_test_functions] = { 0 }; - for (test_iter cit = tests.begin(); cit != tests.end(); ++cit) - for (int i = 0; i < participants; ++i) + for (test_iter test = tests.begin(); test != tests.end(); ++test) + for (int i = 0; i < number_of_test_functions; ++i) { - double dist = Distance((*cit).actual, (*cit).results[i]); + double dist = Distance(test->actual, test->results[i]); avg_dists[i] += dist; if (dist > worst_dists[i]) worst_dists[i] = dist; } @@ -334,7 +566,7 @@ int main(int argc, char** argv) "Coordinate ranges between +/-10^" << current_pow10 << SetConsoleTextColor(reset) << std::endl; - for (int i = 0; i < participants; ++i) + for (int i = 0; i < number_of_test_functions; ++i) { avg_dists[i] /= tests.size(); std::cout << std::fixed << GetGipFuncName(i) << diff --git a/CPP/CMakeLists.txt b/CPP/CMakeLists.txt index df30921e..20e96522 100644 --- a/CPP/CMakeLists.txt +++ b/CPP/CMakeLists.txt @@ -275,6 +275,7 @@ endif() file(COPY ../Tests/PolytreeHoleOwner2.txt DESTINATION ${CMAKE_BINARY_DIR} FILE_PERMISSIONS OWNER_READ GROUP_READ WORLD_READ ) file(COPY ../Tests/Lines.txt DESTINATION ${CMAKE_BINARY_DIR} FILE_PERMISSIONS OWNER_READ GROUP_READ WORLD_READ ) file(COPY ../Tests/Polygons.txt DESTINATION ${CMAKE_BINARY_DIR} FILE_PERMISSIONS OWNER_READ GROUP_READ WORLD_READ ) + file(COPY ../Tests/Offsets.txt DESTINATION ${CMAKE_BINARY_DIR} FILE_PERMISSIONS OWNER_READ GROUP_READ WORLD_READ ) endif() if(USE_EXTERNAL_GBENCHMARK) diff --git a/CPP/Tests/TestLines.cpp b/CPP/Tests/TestLines.cpp index 2c7db3e3..6dcb36b2 100644 --- a/CPP/Tests/TestLines.cpp +++ b/CPP/Tests/TestLines.cpp @@ -3,8 +3,6 @@ #include "ClipFileLoad.h" TEST(Clipper2Tests, TestMultipleLines) { std::ifstream ifs("Lines.txt"); - //if (!ifs.good()) return; - ASSERT_TRUE(ifs); ASSERT_TRUE(ifs.good()); int test_number = 1; while (true) diff --git a/CPP/Tests/TestOffsets.cpp b/CPP/Tests/TestOffsets.cpp index c0e81684..c4a8a3a6 100644 --- a/CPP/Tests/TestOffsets.cpp +++ b/CPP/Tests/TestOffsets.cpp @@ -5,7 +5,7 @@ using namespace Clipper2Lib; TEST(Clipper2Tests, TestOffsets) { std::ifstream ifs("Offsets.txt"); - if(!ifs.good()) return; + ASSERT_TRUE(ifs.good()); for (int test_number = 1; test_number <= 2; ++test_number) { ClipperOffset co; diff --git a/CPP/Tests/TestPolygons.cpp b/CPP/Tests/TestPolygons.cpp index 6910cebd..c5a77735 100644 --- a/CPP/Tests/TestPolygons.cpp +++ b/CPP/Tests/TestPolygons.cpp @@ -20,7 +20,6 @@ inline bool IsInList(int num, const int (&intArray)[N]) TEST(Clipper2Tests, TestMultiplePolygons) { std::ifstream ifs("Polygons.txt"); - ASSERT_TRUE(ifs); ASSERT_TRUE(ifs.good()); const int start_num = 1; const int end_num = 1000; diff --git a/CPP/Tests/TestPolytreeHoles.cpp b/CPP/Tests/TestPolytreeHoles.cpp index cea52ff5..891094ba 100644 --- a/CPP/Tests/TestPolytreeHoles.cpp +++ b/CPP/Tests/TestPolytreeHoles.cpp @@ -5,7 +5,6 @@ using namespace Clipper2Lib; TEST(Clipper2Tests, TestPolytreeHoles1) { std::ifstream ifs("PolytreeHoleOwner.txt"); - ASSERT_TRUE(ifs); ASSERT_TRUE(ifs.good()); Paths64 subject, subject_open, clip; PolyTree64 solution; diff --git a/CPP/Utils/ClipFileLoad.cpp b/CPP/Utils/ClipFileLoad.cpp index f156a2a5..d6572944 100644 --- a/CPP/Utils/ClipFileLoad.cpp +++ b/CPP/Utils/ClipFileLoad.cpp @@ -42,7 +42,7 @@ bool GetPath(const string& line, Paths64& paths) return true; } -void GetPaths(ifstream& source, Paths64& paths) +void GetPaths(ifstream& source, Paths64& paths) { while (true) { @@ -50,13 +50,14 @@ void GetPaths(ifstream& source, Paths64& paths) stringstream::pos_type last_read_line_pos = source.tellg(); if (getline(source, line) && GetPath(line, paths)) continue; + last_read_line_pos -= 1; // workaround for LF vs LFCR (#764) source.seekg(last_read_line_pos, ios_base::beg); break; } } bool LoadTestNum(ifstream &source, int test_num, - Paths64 &subj, Paths64 &subj_open, Paths64 &clip, + Paths64 &subj, Paths64 &subj_open, Paths64 &clip, int64_t& area, int64_t& count, ClipType &ct, FillRule &fr) { string line; @@ -65,37 +66,37 @@ bool LoadTestNum(ifstream &source, int test_num, source.seekg(0, ios_base::beg); subj.clear(); subj_open.clear(); clip.clear(); - while (std::getline(source, line)) - { + while (getline(source, line)) + { if (test_num) { if (line.find("CAPTION:") != string::npos) --test_num; continue; } - if (line.find("CAPTION:") != string::npos) break; + if (line.find("CAPTION:") != string::npos) break; // ie don't go beyond current test - else if (line.find("INTERSECTION") != string::npos) - ct = ClipType::Intersection; - else if (line.find("UNION") != string::npos) - ct = ClipType::Union; - else if (line.find("DIFFERENCE") != string::npos) - ct = ClipType::Difference; - else if (line.find("XOR") != string::npos) - ct = ClipType::Xor; - else if (line.find("EVENODD") != string::npos) - fr = FillRule::EvenOdd; - else if (line.find("NONZERO") != string::npos) - fr = FillRule::NonZero ; - else if (line.find("POSITIVE") != string::npos) - fr = FillRule::Positive; + else if (line.find("INTERSECTION") != string::npos) + ct = ClipType::Intersection; + else if (line.find("UNION") != string::npos) + ct = ClipType::Union; + else if (line.find("DIFFERENCE") != string::npos) + ct = ClipType::Difference; + else if (line.find("XOR") != string::npos) + ct = ClipType::Xor; + else if (line.find("EVENODD") != string::npos) + fr = FillRule::EvenOdd; + else if (line.find("NONZERO") != string::npos) + fr = FillRule::NonZero ; + else if (line.find("POSITIVE") != string::npos) + fr = FillRule::Positive; else if (line.find("NEGATIVE") != string::npos) - fr = FillRule::Negative; + fr = FillRule::Negative; else if (line.find("SOL_AREA") != string::npos) { string::const_iterator s_it, s_end = line.cend(); s_it = (line.cbegin() + 10); - GetInt(s_it, s_end, area); + GetInt(s_it, s_end, area); } else if (line.find("SOL_COUNT") != string::npos) { From 609d87385e8e62d9658e4b39521f57aa281bc75c Mon Sep 17 00:00:00 2001 From: angusj Date: Sat, 13 Jan 2024 10:56:43 +1000 Subject: [PATCH 23/64] Fixed USINGZ option when bevel offsetting (Disc. 766). --- CPP/Clipper2Lib/src/clipper.offset.cpp | 14 ++++++++++++-- CSharp/Clipper2Lib/Clipper.Offset.cs | 26 ++++++++++++++++++++++---- Delphi/Clipper2Lib/Clipper.Offset.pas | 22 ++++++++++++++++++++-- 3 files changed, 54 insertions(+), 8 deletions(-) diff --git a/CPP/Clipper2Lib/src/clipper.offset.cpp b/CPP/Clipper2Lib/src/clipper.offset.cpp index 70189087..b231efb8 100644 --- a/CPP/Clipper2Lib/src/clipper.offset.cpp +++ b/CPP/Clipper2Lib/src/clipper.offset.cpp @@ -1,8 +1,8 @@ /******************************************************************************* * Author : Angus Johnson * -* Date : 21 December 2023 * +* Date : 13 January 2024 * * Website : http://www.angusj.com * -* Copyright : Angus Johnson 2010-2023 * +* Copyright : Angus Johnson 2010-2024 * * Purpose : Path Offset (Inflate/Shrink) * * License : http://www.boost.org/LICENSE_1_0.txt * *******************************************************************************/ @@ -222,13 +222,23 @@ void ClipperOffset::DoBevel(const Path64& path, size_t j, size_t k) if (j == k) { double abs_delta = std::abs(group_delta_); +#ifdef USINGZ + pt1 = PointD(path[j].x - abs_delta * norms[j].x, path[j].y - abs_delta * norms[j].y, path[j].z); + pt2 = PointD(path[j].x + abs_delta * norms[j].x, path[j].y + abs_delta * norms[j].y, path[j].z); +#else pt1 = PointD(path[j].x - abs_delta * norms[j].x, path[j].y - abs_delta * norms[j].y); pt2 = PointD(path[j].x + abs_delta * norms[j].x, path[j].y + abs_delta * norms[j].y); +#endif } else { +#ifdef USINGZ + pt1 = PointD(path[j].x + group_delta_ * norms[k].x, path[j].y + group_delta_ * norms[k].y, path[j].z); + pt2 = PointD(path[j].x + group_delta_ * norms[j].x, path[j].y + group_delta_ * norms[j].y, path[j].z); +#else pt1 = PointD(path[j].x + group_delta_ * norms[k].x, path[j].y + group_delta_ * norms[k].y); pt2 = PointD(path[j].x + group_delta_ * norms[j].x, path[j].y + group_delta_ * norms[j].y); +#endif } path_out.push_back(Point64(pt1)); path_out.push_back(Point64(pt2)); diff --git a/CSharp/Clipper2Lib/Clipper.Offset.cs b/CSharp/Clipper2Lib/Clipper.Offset.cs index f47125e0..ac653bac 100644 --- a/CSharp/Clipper2Lib/Clipper.Offset.cs +++ b/CSharp/Clipper2Lib/Clipper.Offset.cs @@ -1,8 +1,8 @@ /******************************************************************************* * Author : Angus Johnson * -* Date : 21 December 2023 * +* Date : 13 January 2024 * * Website : http://www.angusj.com * -* Copyright : Angus Johnson 2010-2023 * +* Copyright : Angus Johnson 2010-2024 * * Purpose : Path Offset (Inflate/Shrink) * * License : http://www.boost.org/LICENSE_1_0.txt * *******************************************************************************/ @@ -420,21 +420,39 @@ private void DoBevel(Path64 path, int j, int k) if (j == k) { double absDelta = Math.Abs(_groupDelta); +#if USINGZ pt1 = new Point64( path[j].X - absDelta * _normals[j].x, - path[j].Y - absDelta * _normals[j].y); + path[j].Y - absDelta * _normals[j].y, path[j].Z); pt2 = new Point64( path[j].X + absDelta * _normals[j].x, + path[j].Y + absDelta * _normals[j].y, path[j].Z); +#else + pt1 = new Point64( + path[j].X - absDelta * _normals[j].x, + path[j].Y - absDelta * _normals[j].y); + pt2 = new Point64( + path[j].X + absDelta * _normals[j].x, path[j].Y + absDelta * _normals[j].y); +#endif } else { +#if USINGZ + pt1 = new Point64( + path[j].X + _groupDelta * _normals[k].x, + path[j].Y + _groupDelta * _normals[k].y, path[j].Z); + pt2 = new Point64( + path[j].X + _groupDelta * _normals[j].x, + path[j].Y + _groupDelta * _normals[j].y, path[j].Z); +#else pt1 = new Point64( path[j].X + _groupDelta * _normals[k].x, path[j].Y + _groupDelta * _normals[k].y); pt2 = new Point64( - path[j].X + _groupDelta * _normals[j].x, + path[j].X + _groupDelta * _normals[j].x, path[j].Y + _groupDelta * _normals[j].y); +#endif } pathOut.Add(pt1); pathOut.Add(pt2); diff --git a/Delphi/Clipper2Lib/Clipper.Offset.pas b/Delphi/Clipper2Lib/Clipper.Offset.pas index 86c7727e..55cdfc9d 100644 --- a/Delphi/Clipper2Lib/Clipper.Offset.pas +++ b/Delphi/Clipper2Lib/Clipper.Offset.pas @@ -2,9 +2,9 @@ (******************************************************************************* * Author : Angus Johnson * -* Date : 21 December 2023 * +* Date : 13 January 2024 * * Website : http://www.angusj.com * -* Copyright : Angus Johnson 2010-2023 * +* Copyright : Angus Johnson 2010-2024 * * Purpose : Path Offset (Inflate/Shrink) * * License : http://www.boost.org/LICENSE_1_0.txt * *******************************************************************************) @@ -850,20 +850,38 @@ procedure TClipperOffset.DoBevel(j, k: Integer); if k = j then begin absDelta := abs(fGroupDelta); +{$IFDEF USINGZ} + AddPoint( + fInPath[j].x - absDelta * fNorms[j].x, + fInPath[j].y - absDelta * fNorms[j].y, fInPath[j].z); + AddPoint( + fInPath[j].x + absDelta * fNorms[j].x, + fInPath[j].y + absDelta * fNorms[j].y, fInPath[j].z); +{$ELSE} AddPoint( fInPath[j].x - absDelta * fNorms[j].x, fInPath[j].y - absDelta * fNorms[j].y); AddPoint( fInPath[j].x + absDelta * fNorms[j].x, fInPath[j].y + absDelta * fNorms[j].y); +{$ENDIF} end else begin +{$IFDEF USINGZ} + AddPoint( + fInPath[j].x + fGroupDelta * fNorms[k].x, + fInPath[j].y + fGroupDelta * fNorms[k].y, fInPath[j].z); + AddPoint( + fInPath[j].x + fGroupDelta * fNorms[j].x, + fInPath[j].y + fGroupDelta * fNorms[j].y, fInPath[j].z); +{$ELSE} AddPoint( fInPath[j].x + fGroupDelta * fNorms[k].x, fInPath[j].y + fGroupDelta * fNorms[k].y); AddPoint( fInPath[j].x + fGroupDelta * fNorms[j].x, fInPath[j].y + fGroupDelta * fNorms[j].y); +{$ENDIF} end; end; //------------------------------------------------------------------------------ From e2fbc32dbb7ab613e8c488bdd5eb6e833d92c433 Mon Sep 17 00:00:00 2001 From: angusj Date: Wed, 14 Feb 2024 20:20:21 +1000 Subject: [PATCH 24/64] Fixed a rare bug affecting open path clipping (#771) Renamed GetIntersectPoint to GetSegmentIntersectPt (clipper.core) Renamed DistanceFromLineSqrd to PerpendicDistFromLineSqrd (clipper.core) Other minor tweaks. --- .../include/clipper2/clipper.core.h | 24 +-- .../include/clipper2/clipper.export.h | 33 ++-- CPP/Clipper2Lib/include/clipper2/clipper.h | 12 -- .../include/clipper2/clipper.offset.h | 9 +- CPP/Clipper2Lib/src/clipper.engine.cpp | 24 +-- CPP/Clipper2Lib/src/clipper.offset.cpp | 126 +++++-------- CPP/Clipper2Lib/src/clipper.rectclip.cpp | 2 +- CSharp/Clipper2Lib/Clipper.Core.cs | 9 +- CSharp/Clipper2Lib/Clipper.Engine.cs | 10 +- CSharp/Clipper2Lib/Clipper.Offset.cs | 126 +++++-------- CSharp/Clipper2Lib/Clipper.RectClip.cs | 2 +- Delphi/Clipper2Lib/Clipper.Core.pas | 52 ++--- Delphi/Clipper2Lib/Clipper.Engine.pas | 14 +- Delphi/Clipper2Lib/Clipper.Offset.pas | 177 +++++------------- Delphi/Clipper2Lib/Clipper.RectClip.pas | 40 ++-- 15 files changed, 241 insertions(+), 419 deletions(-) diff --git a/CPP/Clipper2Lib/include/clipper2/clipper.core.h b/CPP/Clipper2Lib/include/clipper2/clipper.core.h index dae00653..87ec88a7 100644 --- a/CPP/Clipper2Lib/include/clipper2/clipper.core.h +++ b/CPP/Clipper2Lib/include/clipper2/clipper.core.h @@ -1,8 +1,8 @@ /******************************************************************************* * Author : Angus Johnson * -* Date : 22 December 2023 * +* Date : 14 February 2024 * * Website : http://www.angusj.com * -* Copyright : Angus Johnson 2010-2023 * +* Copyright : Angus Johnson 2010-2024 * * Purpose : Core Clipper Library structures and functions * * License : http://www.boost.org/LICENSE_1_0.txt * *******************************************************************************/ @@ -677,15 +677,17 @@ namespace Clipper2Lib } template - inline double DistanceFromLineSqrd(const Point& pt, const Point& ln1, const Point& ln2) + inline double PerpendicDistFromLineSqrd(const Point& pt, + const Point& line1, const Point& line2) { //perpendicular distance of point (x³,y³) = (Ax³ + By³ + C)/Sqrt(A² + B²) //see http://en.wikipedia.org/wiki/Perpendicular_distance - double A = static_cast(ln1.y - ln2.y); - double B = static_cast(ln2.x - ln1.x); - double C = A * ln1.x + B * ln1.y; - C = A * pt.x + B * pt.y - C; - return (C * C) / (A * A + B * B); + double a = static_cast(pt.x - line1.x); + double b = static_cast(pt.y - line1.y); + double c = static_cast(line2.x - line1.x); + double d = static_cast(line2.y - line1.y); + if (c == 0 && d == 0) return 0; + return Sqr(a * d - c * b) / (c * c + d * d); } template @@ -705,7 +707,7 @@ namespace Clipper2Lib } if (cnt & 1) a += static_cast(it2->y + it1->y) * (it2->x - it1->x); - return a * 0.5; + return (a * 0.5); } template @@ -737,7 +739,7 @@ namespace Clipper2Lib #define CC_MIN(x,y) ((x)>(y)?(y):(x)) #define CC_MAX(x,y) ((x)<(y)?(y):(x)) template - inline bool GetIntersectPoint(const Point& ln1a, const Point& ln1b, + inline bool GetSegmentIntersectPt(const Point& ln1a, const Point& ln1b, const Point& ln2a, const Point& ln2b, Point& ip) { double ln1dy = static_cast(ln1b.y - ln1a.y); @@ -787,7 +789,7 @@ namespace Clipper2Lib } #else template - inline bool GetIntersectPoint(const Point& ln1a, const Point& ln1b, + inline bool GetSegmentIntersectPt(const Point& ln1a, const Point& ln1b, const Point& ln2a, const Point& ln2b, Point& ip) { // https://en.wikipedia.org/wiki/Line%E2%80%93line_intersection diff --git a/CPP/Clipper2Lib/include/clipper2/clipper.export.h b/CPP/Clipper2Lib/include/clipper2/clipper.export.h index 14ee216d..2e892451 100644 --- a/CPP/Clipper2Lib/include/clipper2/clipper.export.h +++ b/CPP/Clipper2Lib/include/clipper2/clipper.export.h @@ -1,8 +1,8 @@ /******************************************************************************* * Author : Angus Johnson * -* Date : 1 December 2023 * +* Date : 14 February 2024 * * Website : http://www.angusj.com * -* Copyright : Angus Johnson 2010-2023 * +* Copyright : Angus Johnson 2010-2024 * * Purpose : This module exports the Clipper2 Library (ie DLL/so) * * License : http://www.boost.org/LICENSE_1_0.txt * *******************************************************************************/ @@ -19,9 +19,9 @@ The path structures used extensively in other parts of this library are all based on std::vector classes. Since C++ classes can't be accessed by other -languages, these paths must be converted into simple C data structures that -can be understood by just about any programming language. And these C style -path structures are simple arrays of int64_t (CPath64) and double (CPathD). +languages, these paths are converted into very simple array data structures +(of either int64_t for CPath64 or double for CPathD) that can be parsed by +just about any programming language. CPath64 and CPathD: These are arrays of consecutive x and y path coordinates preceeded by @@ -34,8 +34,9 @@ __________________________________ CPaths64 and CPathsD: These are also arrays containing any number of consecutive CPath64 or CPathD structures. But preceeding these consecutive paths, there is pair of -values that contain the total length of the array (A) structure and -the number (C) of CPath64 or CPathD it contains. +values that contain the total length of the array structure (A) and the +number of CPath64 or CPathD it contains (C). The space these structures will +occupy in memory = A * sizeof(int64_t) or A * sizeof(double) respectively. _______________________________ |counter|path1|path2|...|pathC| |A , C | | @@ -44,7 +45,7 @@ _______________________________ CPolytree64 and CPolytreeD: These are also arrays consisting of CPolyPath structures that represent individual paths in a tree structure. However, the very first (ie top) -CPolyPath is just the tree container that won't have a path. And because +CPolyPath is just the tree container that doesn't have a path. And because of that, its structure will be very slightly different from the remaining CPolyPath. This difference will be discussed below. @@ -60,17 +61,17 @@ ____________________________________________________________ As mentioned above, the very first CPolyPath structure is just a container that owns (both directly and indirectly) every other CPolyPath in the tree. Since this first CPolyPath has no path, instead of a path length, its very -first value will contain the total length of the CPolytree array structure. +first value will contain the total length of the CPolytree array. -All theses exported structures (CPaths64, CPathsD, CPolyTree64 & CPolyTreeD) -are arrays of type int64_t or double. And the first value in these arrays -will always contain the length of that array. +Again, all theses exported structures (CPaths64, CPathsD, CPolyTree64 & +CPolyTreeD) are arrays of either type int64_t or double, and the first +value in these arrays will always be the length of that array. These array structures are allocated in heap memory which will eventually -need to be released. But since applications dynamically linking to these -functions may use different memory managers, the only safe way to free up -this memory is to use the exported DisposeArray64 and DisposeArrayD -functions below. +need to be released. However, since applications dynamically linking to +these functions may use different memory managers, the only safe way to +free up this memory is to use the exported DisposeArray64 and +DisposeArrayD functions (see below). */ diff --git a/CPP/Clipper2Lib/include/clipper2/clipper.h b/CPP/Clipper2Lib/include/clipper2/clipper.h index f9446220..e086b6ab 100644 --- a/CPP/Clipper2Lib/include/clipper2/clipper.h +++ b/CPP/Clipper2Lib/include/clipper2/clipper.h @@ -614,18 +614,6 @@ namespace Clipper2Lib { return result; } - template - inline double PerpendicDistFromLineSqrd(const Point& pt, - const Point& line1, const Point& line2) - { - double a = static_cast(pt.x - line1.x); - double b = static_cast(pt.y - line1.y); - double c = static_cast(line2.x - line1.x); - double d = static_cast(line2.y - line1.y); - if (c == 0 && d == 0) return 0; - return Sqr(a * d - c * b) / (c * c + d * d); - } - inline size_t GetNext(size_t current, size_t high, const std::vector& flags) { diff --git a/CPP/Clipper2Lib/include/clipper2/clipper.offset.h b/CPP/Clipper2Lib/include/clipper2/clipper.offset.h index 48a30fa6..5536a2e3 100644 --- a/CPP/Clipper2Lib/include/clipper2/clipper.offset.h +++ b/CPP/Clipper2Lib/include/clipper2/clipper.offset.h @@ -1,8 +1,8 @@ /******************************************************************************* * Author : Angus Johnson * -* Date : 19 November 2023 * +* Date : 14 February 2024 * * Website : http://www.angusj.com * -* Copyright : Angus Johnson 2010-2023 * +* Copyright : Angus Johnson 2010-2024 * * Purpose : Path Offset (Inflate/Shrink) * * License : http://www.boost.org/LICENSE_1_0.txt * *******************************************************************************/ @@ -35,7 +35,8 @@ class ClipperOffset { public: Paths64 paths_in; std::vector is_hole_list; - std::vector bounds_list; + std::vector areas_list; + //std::vector bounds_list; int lowest_path_idx = -1; bool is_reversed = false; JoinType join_type; @@ -74,7 +75,7 @@ class ClipperOffset { void DoMiter(const Path64& path, size_t j, size_t k, double cos_a); void DoRound(const Path64& path, size_t j, size_t k, double angle); void BuildNormals(const Path64& path); - void OffsetPolygon(Group& group, const Path64& path); + void OffsetPolygon(Group& group, const Path64& path, bool is_shrinking, double area); void OffsetOpenJoined(Group& group, const Path64& path); void OffsetOpenPath(Group& group, const Path64& path); void OffsetPoint(Group& group, const Path64& path, size_t j, size_t k); diff --git a/CPP/Clipper2Lib/src/clipper.engine.cpp b/CPP/Clipper2Lib/src/clipper.engine.cpp index ecea769e..863dfec4 100644 --- a/CPP/Clipper2Lib/src/clipper.engine.cpp +++ b/CPP/Clipper2Lib/src/clipper.engine.cpp @@ -1,8 +1,8 @@ /******************************************************************************* * Author : Angus Johnson * -* Date : 1 December 2023 * +* Date : 14 February 2024 * * Website : http://www.angusj.com * -* Copyright : Angus Johnson 2010-2023 * +* Copyright : Angus Johnson 2010-2024 * * Purpose : This is the main polygon clipping module * * License : http://www.boost.org/LICENSE_1_0.txt * *******************************************************************************/ @@ -31,11 +31,11 @@ namespace Clipper2Lib { static const Rect64 invalid_rect = Rect64(false); - // Every closed path (or polygon) is made up of a series of vertices forming - // edges that alternate between going up (relative to the Y-axis) and going - // down. Edges consecutively going up or consecutively going down are called - // 'bounds' (ie sides if they're simple polygons). 'Local Minima' refer to - // vertices where descending bounds become ascending ones. + // Every closed path (ie polygon) is made up of a series of vertices forming edge + // 'bounds' that alternate between ascending bounds (containing edges going up + // relative to the Y-axis) and descending bounds. 'Local Minima' refers to + // vertices where ascending and descending bounds join at the bottom, and + // 'Local Maxima' are where ascending and descending bounds join at the top. struct Scanline { int64_t y = 0; @@ -1574,7 +1574,7 @@ namespace Clipper2Lib { outrec->pts = prevOp; Point64 ip; - GetIntersectPoint(prevOp->pt, splitOp->pt, + GetSegmentIntersectPt(prevOp->pt, splitOp->pt, splitOp->next->pt, nextNextOp->pt, ip); #ifdef USINGZ @@ -2302,7 +2302,7 @@ namespace Clipper2Lib { void ClipperBase::AddNewIntersectNode(Active& e1, Active& e2, int64_t top_y) { Point64 ip; - if (!GetIntersectPoint(e1.bot, e1.top, e2.bot, e2.top, ip)) + if (!GetSegmentIntersectPt(e1.bot, e1.top, e2.bot, e2.top, ip)) ip = Point64(e1.curr_x, top_y); //parallel edges //rounding errors can occasionally place the calculated intersection @@ -2768,7 +2768,7 @@ namespace Clipper2Lib { if (check_curr_x) { - if (DistanceFromLineSqrd(pt, prev->bot, prev->top) > 0.25) return; + if (PerpendicDistFromLineSqrd(pt, prev->bot, prev->top) > 0.25) return; } else if (e.curr_x != prev->curr_x) return; if (CrossProduct(e.top, pt, prev->top)) return; @@ -2796,7 +2796,7 @@ namespace Clipper2Lib { if (check_curr_x) { - if (DistanceFromLineSqrd(pt, next->bot, next->top) > 0.35) return; + if (PerpendicDistFromLineSqrd(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; @@ -2868,7 +2868,7 @@ namespace Clipper2Lib { op2 = op2->next; } - if (path.size() == 3 && IsVerySmallTriangle(*op2)) return false; + if (!isOpen && path.size() == 3 && IsVerySmallTriangle(*op2)) return false; else return true; } diff --git a/CPP/Clipper2Lib/src/clipper.offset.cpp b/CPP/Clipper2Lib/src/clipper.offset.cpp index b231efb8..086f787d 100644 --- a/CPP/Clipper2Lib/src/clipper.offset.cpp +++ b/CPP/Clipper2Lib/src/clipper.offset.cpp @@ -1,6 +1,6 @@ /******************************************************************************* * Author : Angus Johnson * -* Date : 13 January 2024 * +* Date : 14 February 2024 * * Website : http://www.angusj.com * * Copyright : Angus Johnson 2010-2024 * * Purpose : Path Offset (Inflate/Shrink) * @@ -20,60 +20,19 @@ const double floating_point_tolerance = 1e-12; // Miscellaneous methods //------------------------------------------------------------------------------ -inline bool ToggleBoolIf(bool val, bool condition) +int GetLowestClosedPathIdx(const Paths64& paths) { - return condition ? !val : val; -} - -void GetMultiBounds(const Paths64& paths, std::vector& recList) -{ - recList.reserve(paths.size()); - for (const Path64& path : paths) - { - if (path.size() < 1) - { - recList.push_back(InvalidRect64); - continue; - } - int64_t x = path[0].x, y = path[0].y; - Rect64 r = Rect64(x, y, x, y); - for (const Point64& pt : path) - { - if (pt.y > r.bottom) r.bottom = pt.y; - else if (pt.y < r.top) r.top = pt.y; - if (pt.x > r.right) r.right = pt.x; - else if (pt.x < r.left) r.left = pt.x; - } - recList.push_back(r); - } -} - -bool ValidateBounds(std::vector& recList, double delta) -{ - int64_t int_delta = static_cast(delta); - int64_t big = MAX_COORD - int_delta; - int64_t small = MIN_COORD + int_delta; - for (const Rect64& r : recList) - { - if (!r.IsValid()) continue; // ignore invalid paths - else if (r.left < small || r.right > big || - r.top < small || r.bottom > big) return false; - } - return true; -} - -int GetLowestClosedPathIdx(std::vector& boundsList) -{ - int i = -1, result = -1; + int result = -1; Point64 botPt = Point64(INT64_MAX, INT64_MIN); - for (const Rect64& r : boundsList) + for (size_t i = 0; i < paths.size(); ++i) { - ++i; - if (!r.IsValid()) continue; // ignore invalid paths - else if (r.bottom > botPt.y || (r.bottom == botPt.y && r.left < botPt.x)) + for (const Point64& pt : paths[i]) { - botPt = Point64(r.left, r.bottom); - result = static_cast(i); + if ((pt.y < botPt.y) || + ((pt.y == botPt.y) && (pt.x >= botPt.x))) continue; + result = static_cast(i); + botPt.x = pt.x; + botPt.y = pt.y; } } return result; @@ -164,15 +123,17 @@ ClipperOffset::Group::Group(const Paths64& _paths, JoinType _join_type, EndType for (Path64& p: paths_in) StripDuplicates(p, is_joined); - // get bounds of each path --> bounds_list - GetMultiBounds(paths_in, bounds_list); - if (end_type == EndType::Polygon) { is_hole_list.reserve(paths_in.size()); + areas_list.reserve(paths_in.size()); for (const Path64& path : paths_in) - is_hole_list.push_back(Area(path) < 0); - lowest_path_idx = GetLowestClosedPathIdx(bounds_list); + { + double a = Area(path); + areas_list.push_back(a); + is_hole_list.push_back(a < 0); + } + lowest_path_idx = GetLowestClosedPathIdx(paths_in); // the lowermost path must be an outer path, so if its orientation is negative, // then flag the whole group is 'reversed' (will negate delta etc.) // as this is much more efficient than reversing every path. @@ -184,6 +145,7 @@ ClipperOffset::Group::Group(const Paths64& _paths, JoinType _join_type, EndType lowest_path_idx = -1; is_reversed = false; is_hole_list.resize(paths_in.size()); + areas_list.resize(paths_in.size()); } } @@ -268,7 +230,7 @@ void ClipperOffset::DoSquare(const Path64& path, size_t j, size_t k) { PointD pt4 = PointD(pt3.x + vec.x * group_delta_, pt3.y + vec.y * group_delta_); PointD pt = ptQ; - GetIntersectPoint(pt1, pt2, pt3, pt4, pt); + GetSegmentIntersectPt(pt1, pt2, pt3, pt4, pt); //get the second intersect point through reflecion path_out.push_back(Point64(ReflectPoint(pt, ptQ))); path_out.push_back(Point64(pt)); @@ -277,7 +239,7 @@ void ClipperOffset::DoSquare(const Path64& path, size_t j, size_t k) { PointD pt4 = GetPerpendicD(path[j], norms[k], group_delta_); PointD pt = ptQ; - GetIntersectPoint(pt1, pt2, pt3, pt4, pt); + GetSegmentIntersectPt(pt1, pt2, pt3, pt4, pt); path_out.push_back(Point64(pt)); //get the second intersect point through reflecion path_out.push_back(Point64(ReflectPoint(pt, ptQ))); @@ -346,7 +308,7 @@ void ClipperOffset::OffsetPoint(Group& group, const Path64& path, size_t j, size // sin(A) < 0: right turning // cos(A) < 0: change in angle is more than 90 degree - if (path[j] == path[k]) { k = j; return; } + if (path[j] == path[k]) return; double sin_a = CrossProduct(norms[j], norms[k]); double cos_a = DotProduct(norms[j], norms[k]); @@ -368,7 +330,7 @@ void ClipperOffset::OffsetPoint(Group& group, const Path64& path, size_t j, size // is concave path_out.push_back(GetPerpendic(path[j], norms[k], group_delta_)); // this extra point is the only (simple) way to ensure that - // path reversals are fully cleaned with the trailing clipper + // path reversals are fully cleaned with the trailing clipper path_out.push_back(path[j]); // (#405) path_out.push_back(GetPerpendic(path[j], norms[j], group_delta_)); } @@ -391,27 +353,34 @@ void ClipperOffset::OffsetPoint(Group& group, const Path64& path, size_t j, size DoSquare(path, j, k); } -void ClipperOffset::OffsetPolygon(Group& group, const Path64& path) +void ClipperOffset::OffsetPolygon(Group& group, const Path64& path, bool is_shrinking, double area) { path_out.clear(); - for (Path64::size_type j = 0, k = path.size() -1; j < path.size(); k = j, ++j) + for (Path64::size_type j = 0, k = path.size() - 1; j < path.size(); k = j, ++j) OffsetPoint(group, path, j, k); + + // make sure that polygon areas aren't reversing which would indicate + // that the polygon has shrunk too far and that it should be discarded. + // See also - #593 & #715 + if (is_shrinking && area // area == 0.0 when JoinType::Joined + && ((area < 0) != Area(path_out) < 0)) return; + solution.push_back(path_out); } void ClipperOffset::OffsetOpenJoined(Group& group, const Path64& path) { - OffsetPolygon(group, path); + OffsetPolygon(group, path, false, 0); Path64 reverse_path(path); std::reverse(reverse_path.begin(), reverse_path.end()); - //rebuild normals // BuildNormals(path); + //rebuild normals std::reverse(norms.begin(), norms.end()); norms.push_back(norms[0]); norms.erase(norms.begin()); NegatePath(norms); - OffsetPolygon(group, reverse_path); + OffsetPolygon(group, reverse_path, true, 0); } void ClipperOffset::OffsetOpenPath(Group& group, const Path64& path) @@ -487,13 +456,6 @@ void ClipperOffset::DoGroupOffset(Group& group) group_delta_ = std::abs(delta_);// *0.5; double abs_delta = std::fabs(group_delta_); - if (!ValidateBounds(group.bounds_list, abs_delta)) - { - DoError(range_error_i); - error_code_ |= range_error_i; - return; - } - join_type_ = group.join_type; end_type_ = group.end_type; @@ -515,12 +477,17 @@ void ClipperOffset::DoGroupOffset(Group& group) steps_per_rad_ = steps_per_360 / (2 * PI); } - std::vector::const_iterator path_rect_it = group.bounds_list.cbegin(); + double min_area = PI * Sqr(group_delta_); std::vector::const_iterator is_hole_it = group.is_hole_list.cbegin(); + std::vector::const_iterator area_it = group.areas_list.cbegin(); Paths64::const_iterator path_in_it = group.paths_in.cbegin(); - for ( ; path_in_it != group.paths_in.cend(); ++path_in_it, ++path_rect_it, ++is_hole_it) + for ( ; path_in_it != group.paths_in.cend(); ++path_in_it, ++is_hole_it, ++area_it) { - if (!path_rect_it->IsValid()) continue; + bool is_shrinking = + (group.end_type == EndType::Polygon) && + (group.is_reversed == ((group_delta_ < 0) == *is_hole_it)); + if (is_shrinking && (std::fabs(*area_it) < min_area)) continue; + Path64::size_type pathLen = path_in_it->size(); path_out.clear(); @@ -554,23 +521,18 @@ void ClipperOffset::DoGroupOffset(Group& group) for (auto& p : path_out) p.z = pt.z; #endif } + solution.push_back(path_out); continue; } // end of offsetting a single point - // when shrinking outer paths, make sure they can shrink this far (#593) - // also when shrinking holes, make sure they too can shrink this far (#715) - if ((group_delta_ > 0) == ToggleBoolIf(*is_hole_it, group.is_reversed) && - (std::min(path_rect_it->Width(), path_rect_it->Height()) <= -group_delta_ * 2) ) - continue; - if ((pathLen == 2) && (group.end_type == EndType::Joined)) end_type_ = (group.join_type == JoinType::Round) ? EndType::Round : EndType::Square; BuildNormals(*path_in_it); - if (end_type_ == EndType::Polygon) OffsetPolygon(group, *path_in_it); + if (end_type_ == EndType::Polygon) OffsetPolygon(group, *path_in_it, is_shrinking, *area_it); else if (end_type_ == EndType::Joined) OffsetOpenJoined(group, *path_in_it); else OffsetOpenPath(group, *path_in_it); } diff --git a/CPP/Clipper2Lib/src/clipper.rectclip.cpp b/CPP/Clipper2Lib/src/clipper.rectclip.cpp index bf9771b6..d5792267 100644 --- a/CPP/Clipper2Lib/src/clipper.rectclip.cpp +++ b/CPP/Clipper2Lib/src/clipper.rectclip.cpp @@ -113,7 +113,7 @@ namespace Clipper2Lib { if ((res3 > 0) == (res4 > 0)) return false; // segments must intersect to get here - return GetIntersectPoint(p1, p2, p3, p4, ip); + return GetSegmentIntersectPt(p1, p2, p3, p4, ip); } inline bool GetIntersection(const Path64& rectPath, diff --git a/CSharp/Clipper2Lib/Clipper.Core.cs b/CSharp/Clipper2Lib/Clipper.Core.cs index 6e35fb8a..d63d4aeb 100644 --- a/CSharp/Clipper2Lib/Clipper.Core.cs +++ b/CSharp/Clipper2Lib/Clipper.Core.cs @@ -1,8 +1,8 @@ /******************************************************************************* * Author : Angus Johnson * -* Date : 1 October 2023 * +* Date : 14 February 2024 * * Website : http://www.angusj.com * -* Copyright : Angus Johnson 2010-2023 * +* Copyright : Angus Johnson 2010-2024 * * Purpose : Core structures and functions for the Clipper Library * * License : http://www.boost.org/LICENSE_1_0.txt * *******************************************************************************/ @@ -11,6 +11,7 @@ using System; using System.Collections.Generic; using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; namespace Clipper2Lib { @@ -508,7 +509,7 @@ public string ToString(int precision = 2) { string s = ""; foreach (PointD p in this) - s = s + p.ToString(precision) + " "; + s = s + p.ToString(precision) + ", "; return s; } } @@ -634,7 +635,7 @@ internal static long CheckCastInt64(double val) [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static bool GetIntersectPoint(Point64 ln1a, + public static bool GetSegmentIntersectPt(Point64 ln1a, Point64 ln1b, Point64 ln2a, Point64 ln2b, out Point64 ip) { double dy1 = (ln1b.Y - ln1a.Y); diff --git a/CSharp/Clipper2Lib/Clipper.Engine.cs b/CSharp/Clipper2Lib/Clipper.Engine.cs index 1d9eb384..db5988f5 100644 --- a/CSharp/Clipper2Lib/Clipper.Engine.cs +++ b/CSharp/Clipper2Lib/Clipper.Engine.cs @@ -1,8 +1,8 @@ /******************************************************************************* * Author : Angus Johnson * -* Date : 1 December 2023 * +* Date : 14 February 2024 * * Website : http://www.angusj.com * -* Copyright : Angus Johnson 2010-2023 * +* Copyright : Angus Johnson 2010-2024 * * Purpose : This is the main polygon clipping module * * Thanks : Special thanks to Thong Nguyen, Guus Kuiper, Phil Stopford, * * : and Daniel Gosnell for their invaluable assistance with C#. * @@ -1901,7 +1901,7 @@ private void DisposeIntersectNodes() [MethodImpl(MethodImplOptions.AggressiveInlining)] private void AddNewIntersectNode(Active ae1, Active ae2, long topY) { - if (!InternalClipper.GetIntersectPoint( + if (!InternalClipper.GetSegmentIntersectPt( ae1.bot, ae1.top, ae2.bot, ae2.top, out Point64 ip)) ip = new Point64(ae1.curX, topY); @@ -2856,7 +2856,7 @@ private void DoSplitOp(OutRec outrec, OutPt splitOp) outrec.pts = prevOp; OutPt result = prevOp; - InternalClipper.GetIntersectPoint( + InternalClipper.GetSegmentIntersectPt( prevOp.pt, splitOp.pt, splitOp.next.pt, nextNextOp.pt, out Point64 ip); #if USINGZ @@ -2981,7 +2981,7 @@ internal static bool BuildPath(OutPt? op, bool reverse, bool isOpen, Path64 path op2 = op2.next!; } - if (path.Count == 3 && IsVerySmallTriangle(op2)) return false; + if (path.Count == 3 && !isOpen && IsVerySmallTriangle(op2)) return false; else return true; } diff --git a/CSharp/Clipper2Lib/Clipper.Offset.cs b/CSharp/Clipper2Lib/Clipper.Offset.cs index ac653bac..c3afdd01 100644 --- a/CSharp/Clipper2Lib/Clipper.Offset.cs +++ b/CSharp/Clipper2Lib/Clipper.Offset.cs @@ -1,6 +1,6 @@ /******************************************************************************* * Author : Angus Johnson * -* Date : 13 January 2024 * +* Date : 14 February 2024 * * Website : http://www.angusj.com * * Copyright : Angus Johnson 2010-2024 * * Purpose : Path Offset (Inflate/Shrink) * @@ -9,6 +9,7 @@ using System; using System.Collections.Generic; +using System.IO; using System.Runtime.CompilerServices; namespace Clipper2Lib @@ -36,7 +37,7 @@ public class ClipperOffset private class Group { internal Paths64 inPaths; - internal List boundsList; + internal List areasList; internal List isHoleList; internal JoinType joinType; internal EndType endType; @@ -53,17 +54,19 @@ public Group(Paths64 paths, JoinType joinType, EndType endType = EndType.Polygon foreach(Path64 path in paths) inPaths.Add(Clipper.StripDuplicates(path, isJoined)); - // get bounds of each path --> boundsList - boundsList = new List(inPaths.Count); - GetMultiBounds(inPaths, boundsList); - if (endType == EndType.Polygon) { - lowestPathIdx = GetLowestPathIdx(boundsList); isHoleList = new List(inPaths.Count); + areasList = new List(inPaths.Count); foreach (Path64 path in inPaths) - isHoleList.Add(Clipper.Area(path) < 0); + { + double a = Clipper.Area(path); + areasList.Add(a); + isHoleList.Add(a < 0); + } + + lowestPathIdx = GetLowestPathIdx(inPaths); // the lowermost path must be an outer path, so if its orientation is negative, // then flag that the whole group is 'reversed' (will negate delta etc.) // as this is much more efficient than reversing every path. @@ -75,6 +78,7 @@ public Group(Paths64 paths, JoinType joinType, EndType endType = EndType.Polygon { lowestPathIdx = -1; isHoleList = new List(new bool[inPaths.Count]); + areasList = new List(new double[inPaths.Count]); pathsReversed = false; } } @@ -253,66 +257,27 @@ public void Execute(DeltaCallback64 deltaCallback, Paths64 solution) { DeltaCallback = deltaCallback; Execute(1.0, solution); - } - - internal static void GetMultiBounds(Paths64 paths, List boundsList) - { - boundsList.EnsureCapacity(paths.Count); - foreach (Path64 path in paths) - { - if (path.Count < 1) - { - boundsList.Add(InvalidRect64); - continue; - } - - Point64 pt1 = path[0]; - Rect64 r = new Rect64(pt1.X, pt1.Y, pt1.X, pt1.Y); - foreach (Point64 pt in path) - { - if (pt.Y > r.bottom) r.bottom = pt.Y; - else if (pt.Y < r.top) r.top = pt.Y; - if (pt.X > r.right) r.right = pt.X; - else if (pt.X < r.left) r.left = pt.X; - } - boundsList.Add(r); - } - } - - internal static bool ValidateBounds(List boundsList, double delta) - { - int int_delta = (int)delta; - - Point64 botPt = new Point64(int.MaxValue, int.MinValue); - foreach (Rect64 r in boundsList) - { - if (!r.IsValid()) continue; // ignore invalid paths - else if (r.left < MIN_COORD + int_delta || - r.right > MAX_COORD + int_delta || - r.top < MIN_COORD + int_delta || - r.bottom > MAX_COORD + int_delta) return false; - } - return true; - } - - internal static int GetLowestPathIdx(List boundsList) + } + + internal static int GetLowestPathIdx(Paths64 paths) { int result = -1; - Point64 botPt = new Point64(long.MaxValue, long.MinValue); - for (int i = 0; i < boundsList.Count; i++) + Point64 botPt = new Point64(Int64.MaxValue, Int64.MinValue); + for (int i = 0; i < paths.Count; ++i) { - Rect64 r = boundsList[i]; - if (!r.IsValid()) continue; // ignore invalid paths - else if (r.bottom > botPt.Y || (r.bottom == botPt.Y && r.left < botPt.X)) - { - botPt = new Point64(r.left, r.bottom); + foreach (Point64 pt in paths[i]) + { + if ((pt.Y < botPt.Y) || + ((pt.Y == botPt.Y) && (pt.X >= botPt.X))) continue; result = i; + botPt.X = pt.X; + botPt.Y = pt.Y; } } - return result; + return result; } - [MethodImpl(MethodImplOptions.AggressiveInlining)] + [MethodImpl(MethodImplOptions.AggressiveInlining)] private static PointD TranslatePoint(PointD pt, double dx, double dy) { #if USINGZ @@ -580,6 +545,8 @@ private void BuildNormals(Path64 path) private void OffsetPoint(Group group, Path64 path, int j, ref int k) { + if (path[j] == path[k]) { k = j; return; } + // Let A = change in angle where edges join // A == 0: ie no change in angle (flat join) // A == PI: edges 'spike' @@ -632,22 +599,29 @@ private void OffsetPoint(Group group, Path64 path, int j, ref int k) } [MethodImpl(MethodImplOptions.AggressiveInlining)] - private void OffsetPolygon(Group group, Path64 path) + private void OffsetPolygon(Group group, Path64 path, bool is_shrinking, double area) { pathOut = new Path64(); int cnt = path.Count, prev = cnt - 1; for (int i = 0; i < cnt; i++) OffsetPoint(group, path, i, ref prev); + + // make sure that polygon areas aren't reversing which would indicate + // that the polygon has shrunk too far and that it should be discarded. + // See also - #593 & #715 + if (is_shrinking && area != 0 && // area == 0.0 when JoinType.Joined + ((area < 0) != Clipper.Area(pathOut) < 0)) return; + _solution.Add(pathOut); } [MethodImpl(MethodImplOptions.AggressiveInlining)] private void OffsetOpenJoined(Group group, Path64 path) { - OffsetPolygon(group, path); + OffsetPolygon(group, path, false, 0); path = Clipper.ReversePath(path); BuildNormals(path); - OffsetPolygon(group, path); + OffsetPolygon(group, path, true, 0); } private void OffsetOpenPath(Group group, Path64 path) @@ -710,11 +684,6 @@ private void OffsetOpenPath(Group group, Path64 path) _solution.Add(pathOut); } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static bool ToggleBoolIf(bool val, bool condition) - { - return condition ? !val : val; - } private void DoGroupOffset(Group group) { if (group.endType == EndType.Polygon) @@ -728,8 +697,6 @@ private void DoGroupOffset(Group group) _groupDelta = Math.Abs(_delta); double absDelta = Math.Abs(_groupDelta); - if (!ValidateBounds(group.boundsList, absDelta)) - throw new Exception(coord_range_error); _joinType = group.joinType; _endType = group.endType; @@ -751,13 +718,16 @@ private void DoGroupOffset(Group group) _stepsPerRad = stepsPer360 / (2 * Math.PI); } + double min_area = Math.PI * Clipper.Sqr(_groupDelta); using List.Enumerator pathIt = group.inPaths.GetEnumerator(); - using List.Enumerator boundsIt = group.boundsList.GetEnumerator(); using List.Enumerator isHoleIt = group.isHoleList.GetEnumerator(); - while (pathIt.MoveNext() && boundsIt.MoveNext() && isHoleIt.MoveNext()) + using List.Enumerator areaIt = group.areasList.GetEnumerator(); + while (pathIt.MoveNext() && isHoleIt.MoveNext() && areaIt.MoveNext()) { - Rect64 pathBounds = boundsIt.Current; - if (!pathBounds.IsValid()) continue; + bool isShrinking = + (group.endType == EndType.Polygon) && + (group.pathsReversed == ((_groupDelta < 0) == isHoleIt.Current)); + if (isShrinking && (Math.Abs(areaIt.Current) < min_area)) continue; Path64 p = pathIt.Current; bool isHole = isHoleIt.Current; @@ -800,19 +770,13 @@ private void DoGroupOffset(Group group) } // end of offsetting a single point - // when shrinking outer paths, make sure they can shrink this far (#593) - // also when shrinking holes, make sure they too can shrink this far (#715) - if (((_groupDelta > 0) == ToggleBoolIf(isHole, group.pathsReversed)) && - (Math.Min(pathBounds.Width, pathBounds.Height) <= -_groupDelta * 2)) - continue; - if (cnt == 2 && group.endType == EndType.Joined) _endType = (group.joinType == JoinType.Round) ? EndType.Round : EndType.Square; BuildNormals(p); - if (_endType == EndType.Polygon) OffsetPolygon(group, p); + if (_endType == EndType.Polygon) OffsetPolygon(group, p, isShrinking, areaIt.Current); else if (_endType == EndType.Joined) OffsetOpenJoined(group, p); else OffsetOpenPath(group, p); } diff --git a/CSharp/Clipper2Lib/Clipper.RectClip.cs b/CSharp/Clipper2Lib/Clipper.RectClip.cs index ea4f8623..70b34098 100644 --- a/CSharp/Clipper2Lib/Clipper.RectClip.cs +++ b/CSharp/Clipper2Lib/Clipper.RectClip.cs @@ -332,7 +332,7 @@ private static bool GetSegmentIntersection(Point64 p1, } // segments must intersect to get here - return InternalClipper.GetIntersectPoint(p1, p2, p3, p4, out ip); + return InternalClipper.GetSegmentIntersectPt(p1, p2, p3, p4, out ip); } diff --git a/Delphi/Clipper2Lib/Clipper.Core.pas b/Delphi/Clipper2Lib/Clipper.Core.pas index d93f8f81..0a765711 100644 --- a/Delphi/Clipper2Lib/Clipper.Core.pas +++ b/Delphi/Clipper2Lib/Clipper.Core.pas @@ -2,9 +2,9 @@ (******************************************************************************* * Author : Angus Johnson * -* Date : 21 December 2023 * +* Date : 14 February 2024 * * Website : http://www.angusj.com * -* Copyright : Angus Johnson 2010-2023 * +* Copyright : Angus Johnson 2010-2024 * * Purpose : Core Clipper Library module * * Contains structures and functions used throughout the library * * License : http://www.boost.org/LICENSE_1_0.txt * @@ -172,8 +172,8 @@ function DistanceSqr(const pt1, pt2: TPoint64): double; overload; {$IFDEF INLINING} inline; {$ENDIF} function DistanceSqr(const pt1, pt2: TPointD): double; overload; {$IFDEF INLINING} inline; {$ENDIF} -function DistanceFromLineSqrd(const pt, linePt1, linePt2: TPoint64): double; overload; -function DistanceFromLineSqrd(const pt, linePt1, linePt2: TPointD): double; overload; +function PerpendicDistFromLineSqrd(const pt, linePt1, linePt2: TPoint64): double; overload; +function PerpendicDistFromLineSqrd(const pt, linePt1, linePt2: TPointD): double; overload; function SegmentsIntersect(const s1a, s1b, s2a, s2b: TPoint64; inclusive: Boolean = false): boolean; {$IFDEF INLINING} inline; {$ENDIF} @@ -315,7 +315,7 @@ procedure AppendPaths(var paths: TPathsD; const extra: TPathsD); overload; function ArrayOfPathsToPaths(const ap: TArrayOfPaths): TPaths64; -function GetIntersectPoint(const ln1a, ln1b, ln2a, ln2b: TPoint64; +function GetSegmentIntersectPt(const ln1a, ln1b, ln2a, ln2b: TPoint64; out ip: TPoint64): Boolean; function PointInPolygon(const pt: TPoint64; const polygon: TPath64): TPointInPolygonResult; @@ -1886,7 +1886,7 @@ function DistanceSqr(const pt1, pt2: TPointD): double; end; //------------------------------------------------------------------------------ -function DistanceFromLineSqrd(const pt, linePt1, linePt2: TPoint64): double; +function PerpendicDistFromLineSqrd(const pt, linePt1, linePt2: TPoint64): double; var a,b,c: double; begin @@ -1897,11 +1897,13 @@ function DistanceFromLineSqrd(const pt, linePt1, linePt2: TPoint64): double; b := (linePt2.X - linePt1.X); c := a * linePt1.X + b * linePt1.Y; c := a * pt.x + b * pt.y - c; - Result := (c * c) / (a * a + b * b); + if (a = 0) and (b = 0) then + Result := 0 else + Result := (c * c) / (a * a + b * b); end; //--------------------------------------------------------------------------- -function DistanceFromLineSqrd(const pt, linePt1, linePt2: TPointD): double; +function PerpendicDistFromLineSqrd(const pt, linePt1, linePt2: TPointD): double; var a,b,c: double; begin @@ -1909,7 +1911,9 @@ function DistanceFromLineSqrd(const pt, linePt1, linePt2: TPointD): double; b := (linePt2.X - linePt1.X); c := a * linePt1.X + b * linePt1.Y; c := a * pt.x + b * pt.y - c; - Result := (c * c) / (a * a + b * b); + if (a = 0) and (b = 0) then + Result := 0 else + Result := (c * c) / (a * a + b * b); end; //--------------------------------------------------------------------------- @@ -1989,7 +1993,7 @@ function __Trunc(val: double): Int64; {$IFDEF INLINE} inline; {$ENDIF} end; //------------------------------------------------------------------------------ -function GetIntersectPoint(const ln1a, ln1b, ln2a, ln2b: TPoint64; +function GetSegmentIntersectPt(const ln1a, ln1b, ln2a, ln2b: TPoint64; out ip: TPoint64): Boolean; var dx1,dy1, dx2,dy2, t, cp: double; @@ -2174,20 +2178,6 @@ function GetClosestPointOnSegment(const pt, seg1, seg2: TPoint64): TPoint64; end; //------------------------------------------------------------------------------ -function PerpendicDistFromLineSqrd(const pt, line1, line2: TPoint64): double; overload; -var - a,b,c,d: double; -begin - a := pt.X - line1.X; - b := pt.Y - line1.Y; - c := line2.X - line1.X; - d := line2.Y - line1.Y; - if (c = 0) and (d = 0) then - result := 0 else - result := Sqr(a * d - c * b) / (c * c + d * d); -end; -//------------------------------------------------------------------------------ - procedure RDP(const path: TPath64; startIdx, endIdx: integer; epsilonSqrd: double; var boolArray: TArrayOfBoolean); overload; var @@ -2217,20 +2207,6 @@ procedure RDP(const path: TPath64; startIdx, endIdx: integer; end; //------------------------------------------------------------------------------ -function PerpendicDistFromLineSqrd(const pt, line1, line2: TPointD): double; overload; -var - a,b,c,d: double; -begin - a := pt.X - line1.X; - b := pt.Y - line1.Y; - c := line2.X - line1.X; - d := line2.Y - line1.Y; - if (c = 0) and (d = 0) then - result := 0 else - result := Sqr(a * d - c * b) / (c * c + d * d); -end; -//------------------------------------------------------------------------------ - procedure RDP(const path: TPathD; startIdx, endIdx: integer; epsilonSqrd: double; var boolArray: TArrayOfBoolean); overload; var diff --git a/Delphi/Clipper2Lib/Clipper.Engine.pas b/Delphi/Clipper2Lib/Clipper.Engine.pas index 6a576abf..26ac2202 100644 --- a/Delphi/Clipper2Lib/Clipper.Engine.pas +++ b/Delphi/Clipper2Lib/Clipper.Engine.pas @@ -2,9 +2,9 @@ (******************************************************************************* * Author : Angus Johnson * -* Date : 21 December 2023 * +* Date : 14 February 2024 * * Website : http://www.angusj.com * -* Copyright : Angus Johnson 2010-2023 * +* Copyright : Angus Johnson 2010-2024 * * Purpose : This is the main polygon clipping module * * License : http://www.boost.org/LICENSE_1_0.txt * *******************************************************************************) @@ -1125,7 +1125,7 @@ function BuildPath(op: POutPt; reverse, isOpen: Boolean; Exit; end; - if (cnt = 3) and IsVerySmallTriangle(op) then + if (cnt = 3) and not IsOpen and IsVerySmallTriangle(op) then begin Result := false; Exit; @@ -2150,7 +2150,7 @@ procedure TClipperBase.DoSplitOp(outrec: POutRec; splitOp: POutPt); prevOp := splitOp.prev; nextNextOp := splitOp.next.next; outrec.pts := prevOp; - GetIntersectPoint( + GetSegmentIntersectPt( prevOp.pt, splitOp.pt, splitOp.next.pt, nextNextOp.pt, ip); {$IFDEF USINGZ} if Assigned(fZCallback) then @@ -2378,7 +2378,7 @@ procedure TClipperBase.CheckJoinLeft(e: PActive; if checkCurrX then begin - if DistanceFromLineSqrd(pt, prev.bot, prev.top) > 0.25 then Exit + if PerpendicDistFromLineSqrd(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; @@ -2409,7 +2409,7 @@ procedure TClipperBase.CheckJoinRight(e: PActive; if (checkCurrX) then begin - if DistanceFromLineSqrd(pt, next.bot, next.top) > 0.25 then Exit + if PerpendicDistFromLineSqrd(pt, next.bot, next.top) > 0.25 then Exit end else if (e.currX <> next.currX) then Exit; @@ -3148,7 +3148,7 @@ procedure TClipperBase.AddNewIntersectNode(e1, e2: PActive; topY: Int64); absDx1, absDx2: double; node: PIntersectNode; begin - if not GetIntersectPoint(e1.bot, e1.top, e2.bot, e2.top, ip) then + if not GetSegmentIntersectPt(e1.bot, e1.top, e2.bot, e2.top, ip) then ip := Point64(e1.currX, topY); // Rounding errors can occasionally place the calculated intersection // point either below or above the scanbeam, so check and correct ... diff --git a/Delphi/Clipper2Lib/Clipper.Offset.pas b/Delphi/Clipper2Lib/Clipper.Offset.pas index 55cdfc9d..ec16a263 100644 --- a/Delphi/Clipper2Lib/Clipper.Offset.pas +++ b/Delphi/Clipper2Lib/Clipper.Offset.pas @@ -32,17 +32,17 @@ interface TDeltaCallback64 = function (const path: TPath64; const path_norms: TPathD; currIdx, prevIdx: integer): double of object; - TRect64Array = array of TRect64; + TDoubleArray = array of double; BooleanArray = array of Boolean; TGroup = class - paths : TPaths64; - joinType : TJoinType; - endType : TEndType; - reversed : Boolean; - lowestPathIdx: integer; - boundsList: TRect64Array; - isHoleList: BooleanArray; + paths : TPaths64; + joinType : TJoinType; + endType : TEndType; + reversed : Boolean; + lowestPathIdx : integer; + areasList : TDoubleArray; + isHoleList : BooleanArray; constructor Create(const pathsIn: TPaths64; jt: TJoinType; et: TEndType); end; @@ -86,7 +86,7 @@ TClipperOffset = class procedure BuildNormals; procedure DoGroupOffset(group: TGroup); - procedure OffsetPolygon; + procedure OffsetPolygon(isShrinking: Boolean; area_: double); procedure OffsetOpenJoined; procedure OffsetOpenPath; function CalcSolutionCapacity: integer; @@ -141,79 +141,6 @@ implementation // Miscellaneous offset support functions //------------------------------------------------------------------------------ -procedure GetMultiBounds(const paths: TPaths64; var list: TRect64Array); -var - i,j, len, len2: integer; - path: TPath64; - pt1, pt: TPoint64; - r: TRect64; -begin - len := Length(paths); - for i := 0 to len -1 do - begin - path := paths[i]; - len2 := Length(path); - if len2 < 1 then - begin - list[i] := InvalidRect64; - continue; - end; - pt1 := path[0]; - r := Rect64(pt1.X, pt1.Y, pt1.X, pt1.Y); - for j := 1 to len2 -1 do - begin - pt := path[j]; - if (pt.y > r.bottom) then r.bottom := pt.y - else if (pt.y < r.top) then r.top := pt.y; - if (pt.x > r.right) then r.right := pt.x - else if (pt.x < r.left) then r.left := pt.x; - end; - list[i] := r; - end; -end; -//------------------------------------------------------------------------------ - -function ValidateBounds(const boundsList: TRect64Array; delta: double): Boolean; -var - i: integer; - iDelta, big, small: Int64; -begin - Result := false; - iDelta := Round(delta); - big := MaxCoord - iDelta; - small := MinCoord + iDelta; - for i := 0 to High(boundsList) do - with boundsList[i] do - begin - if not IsValid then continue; // skip invalid paths - if (left < small) or (right > big) or - (top < small) or (bottom > big) then Exit; - end; - Result := true; -end; -//------------------------------------------------------------------------------ - -function GetLowestClosedPathIdx(const boundsList: TRect64Array): integer; -var - i: integer; - botPt: TPoint64; -begin - Result := -1; - botPt := Point64(MaxInt64, MinInt64); - for i := 0 to High(boundsList) do - with boundsList[i] do - begin - if not IsValid or IsEmpty then Continue; - if (bottom > botPt.y) or - ((bottom = botPt.Y) and (left < botPt.X)) then - begin - botPt := Point64(left, bottom); - Result := i; - end; - end; -end; -//------------------------------------------------------------------------------ - function DotProduct(const vec1, vec2: TPointD): double; {$IFDEF INLINING} inline; {$ENDIF} begin @@ -273,21 +200,21 @@ function GetUnitNormal(const pt1, pt2: TPoint64): TPointD; function GetLowestPolygonIdx(const paths: TPaths64): integer; var i,j: integer; - lp: TPoint64; - p: TPath64; + botPt: TPoint64; begin Result := -1; - lp := Point64(0, -MaxInt64); - for i := 0 to High(paths) do - begin - p := paths[i]; - for j := 0 to High(p) do - begin - if (p[j].Y < lp.Y) or - ((p[j].Y = lp.Y) and (p[j].X >= lp.X)) then Continue; - Result := i; - lp := p[j]; - end; + botPt := Point64(MaxInt64, MinInt64); + for i := 0 to High(paths) do + begin + for j := 0 to High(paths[i]) do + with paths[i][j] do + begin + if (Y < botPt.Y) or + ((Y = botPt.Y) and (X >= botPt.X)) then Continue; + result := i; + botPt.X := X; + botPt.Y := Y; + end; end; end; //------------------------------------------------------------------------------ @@ -305,6 +232,7 @@ function UnsafeGet(List: TList; Index: Integer): Pointer; constructor TGroup.Create(const pathsIn: TPaths64; jt: TJoinType; et: TEndType); var i, len: integer; + a: double; isJoined: boolean; pb: PBoolean; begin @@ -319,20 +247,21 @@ constructor TGroup.Create(const pathsIn: TPaths64; jt: TJoinType; et: TEndType); reversed := false; SetLength(isHoleList, len); - SetLength(boundsList, len); - GetMultiBounds(paths, boundsList); + SetLength(areasList, len); if (et = etPolygon) then begin - lowestPathIdx := GetLowestClosedPathIdx(boundsList); - if lowestPathIdx < 0 then Exit; pb := @isHoleList[0]; for i := 0 to len -1 do begin - pb^ := Area(paths[i]) < 0; inc(pb); + a := Area(paths[i]); + pb^ := a < 0; + inc(pb); end; + // the lowermost path must be an outer path, so if its orientation is // negative, then flag that the whole group is 'reversed' (so negate // delta etc.) as this is much more efficient than reversing every path. + lowestPathIdx := GetLowestPolygonIdx(pathsIn); reversed := (lowestPathIdx >= 0) and isHoleList[lowestPathIdx]; if not reversed then Exit; pb := @isHoleList[0]; @@ -423,18 +352,12 @@ function GetPerpendicD(const pt: TPoint64; const norm: TPointD; delta: double): end; //------------------------------------------------------------------------------ -function ToggleBoolIf(val, condition: Boolean): Boolean; - {$IFDEF INLINING} inline; {$ENDIF} -begin - Result := Iif(condition, not val, val); -end; -//------------------------------------------------------------------------------ - procedure TClipperOffset.DoGroupOffset(group: TGroup); var i,j, len, steps: Integer; - r, stepsPer360, arcTol: Double; + r, stepsPer360, minArea, arcTol: Double; absDelta: double; + isShrinking: Boolean; rec: TRect64; pt0: TPoint64; begin @@ -448,8 +371,6 @@ procedure TClipperOffset.DoGroupOffset(group: TGroup); fGroupDelta := Abs(fDelta); absDelta := Abs(fGroupDelta); - if not ValidateBounds(group.boundsList, absDelta) then - Raise EClipper2LibException(rsClipper_CoordRangeError); fJoinType := group.joinType; fEndType := group.endType; @@ -476,9 +397,13 @@ procedure TClipperOffset.DoGroupOffset(group: TGroup); fStepsPerRad := stepsPer360 / TwoPi; end; + minArea := PI * Sqr(fGroupDelta); for i := 0 to High(group.paths) do begin - if not group.boundsList[i].IsValid then Continue; + isShrinking := (group.endType = etPolygon) and + (group.reversed = ((fGroupDelta < 0) = group.isHoleList[i])); + if (isShrinking and (Abs(group.areasList[i]) < minArea)) then + Continue; fInPath := group.paths[i]; fNorms := nil; @@ -521,13 +446,6 @@ procedure TClipperOffset.DoGroupOffset(group: TGroup); Continue; end; // end of offsetting a single point - // when shrinking outer paths, make sure they can shrink this far (#593) - // also when shrinking holes, make sure they too can shrink this far (#715) - with group do - if ((fGroupDelta > 0) = ToggleBoolIf(isHoleList[i], reversed)) and - (Min(boundsList[i].Width, boundsList[i].Height) <= -fGroupDelta *2) then - Continue; - if (len = 2) and (group.endType = etJoined) then begin if fJoinType = jtRound then @@ -536,9 +454,12 @@ procedure TClipperOffset.DoGroupOffset(group: TGroup); end; BuildNormals; - if fEndType = etPolygon then OffsetPolygon - else if fEndType = etJoined then OffsetOpenJoined - else OffsetOpenPath; + if fEndType = etPolygon then + OffsetPolygon(isShrinking, group.areasList[i]) + else if fEndType = etJoined then + OffsetOpenJoined + else + OffsetOpenPath; end; end; //------------------------------------------------------------------------------ @@ -579,27 +500,33 @@ function TClipperOffset.CalcSolutionCapacity: integer; end; //------------------------------------------------------------------------------ -procedure TClipperOffset.OffsetPolygon; +procedure TClipperOffset.OffsetPolygon(isShrinking: Boolean; area_: double); var i,j: integer; begin j := high(fInPath); for i := 0 to high(fInPath) do OffsetPoint(i, j); + + // make sure that polygon areas aren't reversing which would indicate + // that the polygon has shrunk too far and that it should be discarded. + // See also - #593 & #715 + if isShrinking and (area_ <> 0) and // area = 0.0 when JoinType.Joined + ((area_ < 0) <> (Area(fOutPath) < 0)) then Exit; + UpdateSolution; end; //------------------------------------------------------------------------------ procedure TClipperOffset.OffsetOpenJoined; begin - OffsetPolygon; + OffsetPolygon(false, 0); fInPath := ReversePath(fInPath); // Rebuild normals // BuildNormals; fNorms := ReversePath(fNorms); fNorms := ShiftPath(fNorms, 1); fNorms := NegatePath(fNorms); - - OffsetPolygon; + OffsetPolygon(true, 0); end; //------------------------------------------------------------------------------ diff --git a/Delphi/Clipper2Lib/Clipper.RectClip.pas b/Delphi/Clipper2Lib/Clipper.RectClip.pas index 3eab8963..4e2da7dc 100644 --- a/Delphi/Clipper2Lib/Clipper.RectClip.pas +++ b/Delphi/Clipper2Lib/Clipper.RectClip.pas @@ -2,9 +2,9 @@ (******************************************************************************* * Author : Angus Johnson * -* Date : 21 December 2023 * +* Date : 14 February 2024 * * Website : http://www.angusj.com * -* Copyright : Angus Johnson 2010-2023 * +* Copyright : Angus Johnson 2010-2024 * * Purpose : FAST rectangular clipping * * License : http://www.boost.org/LICENSE_1_0.txt * *******************************************************************************) @@ -121,7 +121,7 @@ function IsHorizontal(pt1: TPoint64; pt2: TPoint64): Boolean; end; //------------------------------------------------------------------------------ -function GetSegmentIntersection(p1: TPoint64; +function GetSegmentIntersectPt2(p1: TPoint64; p2: TPoint64; p3: TPoint64; p4: TPoint64; out ip: TPoint64): Boolean; var res1, res2, res3, res4: double; @@ -189,7 +189,7 @@ function GetSegmentIntersection(p1: TPoint64; end else // segments must intersect to get here - Result := GetIntersectPoint(p1, p2, p3, p4, ip); + Result := GetSegmentIntersectPt(p1, p2, p3, p4, ip); end; //------------------------------------------------------------------------------ @@ -201,60 +201,60 @@ function GetIntersection(const rectPath: TPath64; Result := True; case loc of locLeft: - if GetSegmentIntersection(p, p2, rectPath[0], rectPath[3], ip) then + if GetSegmentIntersectPt2(p, p2, rectPath[0], rectPath[3], ip) then //Result := True else if (p.Y < rectPath[0].Y) and - GetSegmentIntersection(p, p2, rectPath[0], rectPath[1], ip) then + GetSegmentIntersectPt2(p, p2, rectPath[0], rectPath[1], ip) then loc := locTop - else if GetSegmentIntersection(p, p2, rectPath[2], rectPath[3], ip) then + else if GetSegmentIntersectPt2(p, p2, rectPath[2], rectPath[3], ip) then loc := locBottom else Result := False; locRight: - if GetSegmentIntersection(p, p2, rectPath[1], rectPath[2], ip) then + if GetSegmentIntersectPt2(p, p2, rectPath[1], rectPath[2], ip) then //Result := True else if (p.Y < rectPath[0].Y) and - GetSegmentIntersection(p, p2, rectPath[0], rectPath[1], ip) then + GetSegmentIntersectPt2(p, p2, rectPath[0], rectPath[1], ip) then loc := locTop - else if GetSegmentIntersection(p, p2, rectPath[2], rectPath[3], ip) then + else if GetSegmentIntersectPt2(p, p2, rectPath[2], rectPath[3], ip) then loc := locBottom else Result := False; locTop: - if GetSegmentIntersection(p, p2, rectPath[0], rectPath[1], ip) then + if GetSegmentIntersectPt2(p, p2, rectPath[0], rectPath[1], ip) then //Result := True else if (p.X < rectPath[0].X) and - GetSegmentIntersection(p, p2, rectPath[0], rectPath[3], ip) then + GetSegmentIntersectPt2(p, p2, rectPath[0], rectPath[3], ip) then loc := locLeft else if (p.X > rectPath[1].X) and - GetSegmentIntersection(p, p2, rectPath[1], rectPath[2], ip) then + GetSegmentIntersectPt2(p, p2, rectPath[1], rectPath[2], ip) then loc := locRight else Result := False; locBottom: - if GetSegmentIntersection(p, p2, rectPath[2], rectPath[3], ip) then + if GetSegmentIntersectPt2(p, p2, rectPath[2], rectPath[3], ip) then //Result := True else if (p.X < rectPath[3].X) and - GetSegmentIntersection(p, p2, rectPath[0], rectPath[3], ip) then + GetSegmentIntersectPt2(p, p2, rectPath[0], rectPath[3], ip) then loc := locLeft else if (p.X > rectPath[2].X) and - GetSegmentIntersection(p, p2, rectPath[1], rectPath[2], ip) then + GetSegmentIntersectPt2(p, p2, rectPath[1], rectPath[2], ip) then loc := locRight else Result := False; else // loc = rInside begin - if GetSegmentIntersection(p, p2, rectPath[0], rectPath[3], ip) then + if GetSegmentIntersectPt2(p, p2, rectPath[0], rectPath[3], ip) then loc := locLeft - else if GetSegmentIntersection(p, p2, rectPath[0], rectPath[1], ip) then + else if GetSegmentIntersectPt2(p, p2, rectPath[0], rectPath[1], ip) then loc := locTop - else if GetSegmentIntersection(p, p2, rectPath[1], rectPath[2], ip) then + else if GetSegmentIntersectPt2(p, p2, rectPath[1], rectPath[2], ip) then loc := locRight - else if GetSegmentIntersection(p, p2, rectPath[2], rectPath[3], ip) then + else if GetSegmentIntersectPt2(p, p2, rectPath[2], rectPath[3], ip) then loc := locBottom else Result := False; From a4ae9e4871245e396aaccfad7f760e2fc1d4cbf4 Mon Sep 17 00:00:00 2001 From: angusj Date: Wed, 14 Feb 2024 20:31:45 +1000 Subject: [PATCH 25/64] Fixed compile bug in previous commit --- CPP/Clipper2Lib/src/clipper.offset.cpp | 2 +- CSharp/Clipper2Lib/Clipper.Offset.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CPP/Clipper2Lib/src/clipper.offset.cpp b/CPP/Clipper2Lib/src/clipper.offset.cpp index 086f787d..5d7006be 100644 --- a/CPP/Clipper2Lib/src/clipper.offset.cpp +++ b/CPP/Clipper2Lib/src/clipper.offset.cpp @@ -363,7 +363,7 @@ void ClipperOffset::OffsetPolygon(Group& group, const Path64& path, bool is_shri // that the polygon has shrunk too far and that it should be discarded. // See also - #593 & #715 if (is_shrinking && area // area == 0.0 when JoinType::Joined - && ((area < 0) != Area(path_out) < 0)) return; + && ((area < 0) != (Area(path_out) < 0))) return; solution.push_back(path_out); } diff --git a/CSharp/Clipper2Lib/Clipper.Offset.cs b/CSharp/Clipper2Lib/Clipper.Offset.cs index c3afdd01..cdd64bf5 100644 --- a/CSharp/Clipper2Lib/Clipper.Offset.cs +++ b/CSharp/Clipper2Lib/Clipper.Offset.cs @@ -610,7 +610,7 @@ private void OffsetPolygon(Group group, Path64 path, bool is_shrinking, double a // that the polygon has shrunk too far and that it should be discarded. // See also - #593 & #715 if (is_shrinking && area != 0 && // area == 0.0 when JoinType.Joined - ((area < 0) != Clipper.Area(pathOut) < 0)) return; + ((area < 0) != (Clipper.Area(pathOut) < 0))) return; _solution.Add(pathOut); } From 1df29405b9c2176293db9783c61f7b09c2fe66d8 Mon Sep 17 00:00:00 2001 From: Seth Hillbrand Date: Sat, 24 Feb 2024 20:34:13 -0800 Subject: [PATCH 26/64] Update CMakeLists.txt (#781) Add -Wno-c++20-compat to compile flags. Without this, g++ will turn on compatibility testing. In C++20, template IDs are not allowed in constructors (clipper.core.h). The combination of `-Wall`, which turns on the warning, and `-Werror`, which makes the warning an error prevented builds using newer g++ versions --- CPP/CMakeLists.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CPP/CMakeLists.txt b/CPP/CMakeLists.txt index 20e96522..e89ce45f 100644 --- a/CPP/CMakeLists.txt +++ b/CPP/CMakeLists.txt @@ -72,7 +72,7 @@ if (NOT (CLIPPER2_USINGZ STREQUAL "ONLY")) if (MSVC) target_compile_options(Clipper2 PRIVATE /W4 /WX) else() - target_compile_options(Clipper2 PRIVATE -Wall -Wextra -Wpedantic -Werror) + target_compile_options(Clipper2 PRIVATE -Wall -Wextra -Wpedantic -Werror -Wno-c++20-compat) target_link_libraries(Clipper2 PUBLIC -lm) endif() endif() @@ -96,7 +96,7 @@ if (NOT (CLIPPER2_USINGZ STREQUAL "OFF")) if (MSVC) target_compile_options(Clipper2Z PRIVATE /W4 /WX) else() - target_compile_options(Clipper2Z PRIVATE -Wall -Wextra -Wpedantic -Werror) + target_compile_options(Clipper2Z PRIVATE -Wall -Wextra -Wpedantic -Werror -Wno-c++20-compat) target_link_libraries(Clipper2Z PUBLIC -lm) endif() endif() From f7c126460ea45d4541961c01a4db6f64c9008384 Mon Sep 17 00:00:00 2001 From: angusj Date: Sun, 3 Mar 2024 20:17:15 +1000 Subject: [PATCH 27/64] Fixed C# code crashing when compiling in iOS. (#773) --- CSharp/Clipper2Lib/Clipper.Engine.cs | 33 +++++++++++++--------------- 1 file changed, 15 insertions(+), 18 deletions(-) diff --git a/CSharp/Clipper2Lib/Clipper.Engine.cs b/CSharp/Clipper2Lib/Clipper.Engine.cs index db5988f5..d967a77f 100644 --- a/CSharp/Clipper2Lib/Clipper.Engine.cs +++ b/CSharp/Clipper2Lib/Clipper.Engine.cs @@ -1,6 +1,6 @@ /******************************************************************************* * Author : Angus Johnson * -* Date : 14 February 2024 * +* Date : 28 February 2024 * * Website : http://www.angusj.com * * Copyright : Angus Johnson 2010-2024 * * Purpose : This is the main polygon clipping module * @@ -154,22 +154,6 @@ internal class OutRec public OutRec? recursiveSplit; }; - internal struct HorzSegSorter : IComparer - { - public readonly int Compare(HorzSegment? hs1, HorzSegment? hs2) - { - if (hs1 == null || hs2 == null) return 0; - if (hs1.rightOp == null) - { - return hs2.rightOp == null ? 0 : 1; - } - else if (hs2.rightOp == null) - return -1; - else - return hs1.leftOp!.pt.X.CompareTo(hs2.leftOp!.pt.X); - } - } - internal class HorzSegment { public OutPt? leftOp; @@ -2539,13 +2523,26 @@ private static OutPt DuplicateOp(OutPt op, bool insert_after) return result; } + private int HorzSegSort(HorzSegment? hs1, HorzSegment? hs2) + { + if (hs1 == null || hs2 == null) return 0; + if (hs1.rightOp == null) + { + return hs2.rightOp == null ? 0 : 1; + } + else if (hs2.rightOp == null) + return -1; + else + return hs1.leftOp!.pt.X.CompareTo(hs2.leftOp!.pt.X); + } + private void ConvertHorzSegsToJoins() { int k = 0; foreach (HorzSegment hs in _horzSegList) if (UpdateHorzSegment(hs)) k++; if (k < 2) return; - _horzSegList.Sort(new HorzSegSorter()); + _horzSegList.Sort(HorzSegSort); for (int i = 0; i < k -1; i++) { From 5c42a990e54e4e1628d40913485a128c7e60fa2b Mon Sep 17 00:00:00 2001 From: angusj Date: Fri, 8 Mar 2024 08:08:22 +1000 Subject: [PATCH 28/64] Minor tweak to Clipper.Offset (#780) Improved Free Pascal compatibility (#787) --- CPP/Clipper2Lib/src/clipper.offset.cpp | 14 +++--- CSharp/Clipper2Lib/Clipper.Offset.cs | 14 +++--- Delphi/Clipper2Lib/Clipper.Offset.pas | 16 +++--- Delphi/Utils/Clipper.SVG.pas | 67 ++++++++++++++++++++++---- 4 files changed, 78 insertions(+), 33 deletions(-) diff --git a/CPP/Clipper2Lib/src/clipper.offset.cpp b/CPP/Clipper2Lib/src/clipper.offset.cpp index 5d7006be..7ad08146 100644 --- a/CPP/Clipper2Lib/src/clipper.offset.cpp +++ b/CPP/Clipper2Lib/src/clipper.offset.cpp @@ -1,6 +1,6 @@ /******************************************************************************* * Author : Angus Johnson * -* Date : 14 February 2024 * +* Date : 8 March 2024 * * Website : http://www.angusj.com * * Copyright : Angus Johnson 2010-2024 * * Purpose : Path Offset (Inflate/Shrink) * @@ -325,7 +325,7 @@ void ClipperOffset::OffsetPoint(Group& group, const Path64& path, size_t j, size return; } - if (cos_a > -0.99 && (sin_a * group_delta_ < 0)) // test for concavity first (#593) + if (cos_a > -0.999 && (sin_a * group_delta_ < 0)) // test for concavity first (#593) { // is concave path_out.push_back(GetPerpendic(path[j], norms[k], group_delta_)); @@ -461,11 +461,11 @@ void ClipperOffset::DoGroupOffset(Group& group) if (group.join_type == JoinType::Round || group.end_type == EndType::Round) { - // calculate a sensible number of steps (for 360 deg for the given offset) - // arcTol - when arc_tolerance_ is undefined (0), the amount of - // curve imprecision that's allowed is based on the size of the - // offset (delta). Obviously very large offsets will almost always - // require much less precision. See also offset_triginometry2.svg + // calculate the number of steps required to approximate a circle + // (see http://www.angusj.com/clipper2/Docs/Trigonometry.htm) + // arcTol - when arc_tolerance_ is undefined (0) then curve imprecision + // will be relative to the size of the offset (delta). Obviously very + //large offsets will almost always require much less precision. double arcTol = (arc_tolerance_ > floating_point_tolerance ? std::min(abs_delta, arc_tolerance_) : std::log10(2 + abs_delta) * default_arc_tolerance); diff --git a/CSharp/Clipper2Lib/Clipper.Offset.cs b/CSharp/Clipper2Lib/Clipper.Offset.cs index cdd64bf5..775b5d90 100644 --- a/CSharp/Clipper2Lib/Clipper.Offset.cs +++ b/CSharp/Clipper2Lib/Clipper.Offset.cs @@ -1,6 +1,6 @@ /******************************************************************************* * Author : Angus Johnson * -* Date : 14 February 2024 * +* Date : 8 March 2024 * * Website : http://www.angusj.com * * Copyright : Angus Johnson 2010-2024 * * Purpose : Path Offset (Inflate/Shrink) * @@ -568,7 +568,7 @@ private void OffsetPoint(Group group, Path64 path, int j, ref int k) return; } - if (cosA > -0.99 && (sinA * _groupDelta < 0)) // test for concavity first (#593) + if (cosA > -0.999 && (sinA * _groupDelta < 0)) // test for concavity first (#593) { // is concave pathOut.Add(GetPerpendic(path[j], _normals[k])); @@ -703,11 +703,11 @@ private void DoGroupOffset(Group group) if (group.joinType == JoinType.Round || group.endType == EndType.Round) { - // calculate a sensible number of steps (for 360 deg for the given offset - // arcTol - when fArcTolerance is undefined (0), the amount of - // curve imprecision that's allowed is based on the size of the - // offset (delta). Obviously very large offsets will almost always - // require much less precision. See also offset_triginometry2.svg + // calculate the number of steps required to approximate a circle + // (see http://www.angusj.com/clipper2/Docs/Trigonometry.htm) + // arcTol - when arc_tolerance_ is undefined (0) then curve imprecision + // will be relative to the size of the offset (delta). Obviously very + //large offsets will almost always require much less precision. double arcTol = ArcTolerance > 0.01 ? ArcTolerance : Math.Log10(2 + absDelta) * InternalClipper.defaultArcTolerance; diff --git a/Delphi/Clipper2Lib/Clipper.Offset.pas b/Delphi/Clipper2Lib/Clipper.Offset.pas index ec16a263..dc079f17 100644 --- a/Delphi/Clipper2Lib/Clipper.Offset.pas +++ b/Delphi/Clipper2Lib/Clipper.Offset.pas @@ -2,7 +2,7 @@ (******************************************************************************* * Author : Angus Johnson * -* Date : 13 January 2024 * +* Date : 8 March 2024 * * Website : http://www.angusj.com * * Copyright : Angus Johnson 2010-2024 * * Purpose : Path Offset (Inflate/Shrink) * @@ -375,19 +375,17 @@ procedure TClipperOffset.DoGroupOffset(group: TGroup); fJoinType := group.joinType; fEndType := group.endType; - // calculate a sensible number of steps (for 360 deg for the given offset if (group.joinType = jtRound) or (group.endType = etRound) then begin - // calculate a sensible number of steps (for 360 deg for the given offset) - // arcTol - when arc_tolerance_ is undefined (0), the amount of - // curve imprecision that's allowed is based on the size of the - // offset (delta). Obviously very large offsets will almost always - // require much less precision. See also offset_triginometry2.svg + // calculate the number of steps required to approximate a circle + // (see http://www.angusj.com/clipper2/Docs/Trigonometry.htm) + // arcTol - when arc_tolerance_ is undefined (0) then curve imprecision + // will be relative to the size of the offset (delta). Obviously very + //large offsets will almost always require much less precision. arcTol := Iif(fArcTolerance > 0.01, Min(absDelta, fArcTolerance), Log10(2 + absDelta) * 0.25); // empirically derived - //http://www.angusj.com/clipper2/Docs/Trigonometry.htm stepsPer360 := Pi / ArcCos(1 - arcTol / absDelta); if (stepsPer360 > absDelta * Pi) then stepsPer360 := absDelta * Pi; // avoid excessive precision @@ -974,7 +972,7 @@ procedure TClipperOffset.OffsetPoint(j: Integer; var k: integer); end; //test for concavity first (#593) - if (cosA > -0.99) and (sinA * fGroupDelta < 0) then + if (cosA > -0.999) and (sinA * fGroupDelta < 0) then begin // is concave AddPoint(GetPerpendic(fInPath[j], fNorms[k], fGroupDelta)); diff --git a/Delphi/Utils/Clipper.SVG.pas b/Delphi/Utils/Clipper.SVG.pas index 849db43c..13ad1929 100644 --- a/Delphi/Utils/Clipper.SVG.pas +++ b/Delphi/Utils/Clipper.SVG.pas @@ -2,10 +2,9 @@ (******************************************************************************* * Author : Angus Johnson * -* Version : Clipper2 - ver.1.0.4 * -* Date : 6 September 2022 * +* Date : 7 March 2024 * * Website : http://www.angusj.com * -* Copyright : Angus Johnson 2010-2022 * +* Copyright : Angus Johnson 2010-2024 * * Purpose : This module provides a very simple SVG Writer for Clipper2 * * License : http://www.boost.org/LICENSE_1_0.txt * *******************************************************************************) @@ -106,12 +105,14 @@ TSvgWriter = class procedure AddPaths(const paths: TPaths64; isOpen: Boolean; brushColor, penColor: Cardinal; penWidth: double; showCoords: Boolean = false); overload; + procedure AddDashedPath(const paths: TPaths64; penColor: Cardinal; + penWidth: double; const dashes: array of integer); overload; procedure AddPaths(const paths: TPathsD; isOpen: Boolean; brushColor, penColor: Cardinal; penWidth: double; showCoords: Boolean = false); overload; procedure AddDashedPath(const paths: TPathsD; penColor: Cardinal; - penWidth: double; const dashes: array of integer); + penWidth: double; const dashes: array of integer); overload; procedure AddArrow(const center: TPointD; radius: double; angleRad: double; @@ -139,6 +140,8 @@ TSvgWriter = class procedure AddClip(svg: TSvgWriter; const paths: TPaths64); overload; procedure AddSolution(svg: TSvgWriter; const paths: TPaths64; showCoords: Boolean = false); overload; procedure AddOpenSolution(svg: TSvgWriter; const paths: TPaths64); overload; + procedure AddDots(svg: TSvgWriter; const paths: TPaths64; radius: double; color: Cardinal); overload; + procedure SaveSvg(svg: TSvgWriter; const filename: string; width: integer = 0; height: integer = 0; margin: integer = 0); overload; @@ -147,6 +150,7 @@ TSvgWriter = class procedure AddClip(svg: TSvgWriter; const paths: TPathsD); overload; procedure AddSolution(svg: TSvgWriter; const paths: TPathsD; showCoords: Boolean= false); overload; procedure AddOpenSolution(svg: TSvgWriter; const paths: TPathsD); overload; + procedure AddDots(svg: TSvgWriter; const paths: TPathsD; radius: double; color: Cardinal); overload; implementation @@ -257,6 +261,28 @@ procedure TSvgWriter.AddPaths(const paths: TPathsD; isOpen: Boolean; fPolyInfos.Add(pi); end; +procedure TSvgWriter.AddDashedPath(const paths: TPaths64; + penColor: Cardinal; penWidth: double; const dashes: array of integer); +var + pi: PPolyInfo; + i, dLen: integer; +begin + dLen := Length(dashes); + if dLen = 0 then Exit; + new(pi); + pi.paths := PathsD(paths); + pi.BrushClr := 0; + pi.PenClr := penColor; + pi.PenWidth := penWidth; + pi.ShowCoords := false; + pi.IsOpen := true; + SetLength(pi.dashes, dLen); + //Move(dashes[0], pi.dashes, dLen * sizeOf(integer)); + for i := 0 to High(dashes) do + pi.dashes[i] := dashes[i]; + fPolyInfos.Add(pi); +end; + procedure TSvgWriter.AddDashedPath(const paths: TPathsD; penColor: Cardinal; penWidth: double; const dashes: array of integer); var @@ -389,6 +415,7 @@ function TSvgWriter.SaveToFile(const filename: string; maxWidth: integer = 0; maxHeight: integer = 0; margin: integer = 20): Boolean; var i, j, k : integer; + x,y : double; bounds : TRectD; scale : double; offsetX, offsetY : integer; @@ -413,7 +440,8 @@ function TSvgWriter.SaveToFile(const filename: string; begin -{$IF CompilerVersion > 19} //Delphi XE + + +{$IF NOT Defined(fpc) AND (CompilerVersion > 19)} //Delphi XE + formatSettings := TFormatSettings.Create; {$IFEND} formatSettings.DecimalSeparator := '.'; @@ -453,9 +481,11 @@ function TSvgWriter.SaveToFile(const filename: string; [paths[j][0].x * scale + offsetX, paths[j][0].y * scale + offsetY], formatSettings)); for k := 1 to High(paths[j]) do + begin + x := paths[j][k].x; y := paths[j][k].y; AddInline(Format('%1.2f %1.2f ', - [paths[j][k].x * scale + offsetX, - paths[j][k].y * scale + offsetY], formatSettings)); + [x * scale + offsetX, y * scale + offsetY], formatSettings)); + end; if not IsOpen then AddInline('Z'); end; @@ -552,8 +582,7 @@ procedure AddClip(svg: TSvgWriter; const paths: TPaths64); procedure AddSolution(svg: TSvgWriter; const paths: TPaths64; showCoords: Boolean); begin - svg.AddPaths(paths, false, $8066FF66, $FF006600, 1.5, showCoords); - //svg.AddPaths(paths, false, $8066FF66, $FF006600, 1.5, showCoords); + svg.AddPaths(paths, false, $8066FF66, $FF006600, 1.0, showCoords); end; procedure AddOpenSolution(svg: TSvgWriter; const paths: TPaths64); @@ -584,7 +613,7 @@ procedure AddClip(svg: TSvgWriter; const paths: TPathsD); procedure AddSolution(svg: TSvgWriter; const paths: TPathsD; showCoords: Boolean); begin - svg.AddPaths(paths, false, $8066FF66, $FF006600, 1.5, showCoords); + svg.AddPaths(paths, false, $8066FF66, $FF006600, 1.0, showCoords); end; procedure AddOpenSolution(svg: TSvgWriter; const paths: TPathsD); @@ -592,5 +621,23 @@ procedure AddOpenSolution(svg: TSvgWriter; const paths: TPathsD); svg.AddPaths(paths, true, $0, $FF006600, 1.5); end; +procedure AddDots(svg: TSvgWriter; const paths: TPaths64; radius: double; color: Cardinal); +var + i,j: integer; +begin + for i := 0 to High(paths) do + for j := 0 to High(paths[i]) do + svg.AddCircle(paths[i][j], radius, color, color, 1); +end; + +procedure AddDots(svg: TSvgWriter; const paths: TPathsD; radius: double; color: Cardinal); +var + i,j: integer; +begin + for i := 0 to High(paths) do + for j := 0 to High(paths[i]) do + svg.AddCircle(paths[i][j], radius, color, color, 1); +end; + end. From b3eabaee8ff973d7499ac1c0927ffefad0ba702e Mon Sep 17 00:00:00 2001 From: angusj Date: Thu, 14 Mar 2024 18:33:32 +1000 Subject: [PATCH 29/64] Fixed a minor bug in open path offsetting (see https://github.com/graphics32/graphics32/issues/277) --- CPP/Clipper2Lib/src/clipper.offset.cpp | 4 ++-- CSharp/Clipper2Lib/Clipper.Offset.cs | 4 ++-- Delphi/Clipper2Lib/Clipper.Offset.pas | 6 +++--- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/CPP/Clipper2Lib/src/clipper.offset.cpp b/CPP/Clipper2Lib/src/clipper.offset.cpp index 7ad08146..ff8d1f4a 100644 --- a/CPP/Clipper2Lib/src/clipper.offset.cpp +++ b/CPP/Clipper2Lib/src/clipper.offset.cpp @@ -1,6 +1,6 @@ /******************************************************************************* * Author : Angus Johnson * -* Date : 8 March 2024 * +* Date : 14 March 2024 * * Website : http://www.angusj.com * * Copyright : Angus Johnson 2010-2024 * * Purpose : Path Offset (Inflate/Shrink) * @@ -438,7 +438,7 @@ void ClipperOffset::OffsetOpenPath(Group& group, const Path64& path) } } - for (size_t j = highI, k = 0; j > 0; k = j, --j) + for (size_t j = highI -1, k = highI; j > 0; k = j, --j) OffsetPoint(group, path, j, k); solution.push_back(path_out); } diff --git a/CSharp/Clipper2Lib/Clipper.Offset.cs b/CSharp/Clipper2Lib/Clipper.Offset.cs index 775b5d90..ed13ef00 100644 --- a/CSharp/Clipper2Lib/Clipper.Offset.cs +++ b/CSharp/Clipper2Lib/Clipper.Offset.cs @@ -1,6 +1,6 @@ /******************************************************************************* * Author : Angus Johnson * -* Date : 8 March 2024 * +* Date : 14 March 2024 * * Website : http://www.angusj.com * * Copyright : Angus Johnson 2010-2024 * * Purpose : Path Offset (Inflate/Shrink) * @@ -678,7 +678,7 @@ private void OffsetOpenPath(Group group, Path64 path) } // offset the left side going back - for (int i = highI, k = 0; i > 0; i--) + for (int i = highI -1, k = highI; i > 0; i--) OffsetPoint(group, path, i, ref k); _solution.Add(pathOut); diff --git a/Delphi/Clipper2Lib/Clipper.Offset.pas b/Delphi/Clipper2Lib/Clipper.Offset.pas index dc079f17..7a2be9ea 100644 --- a/Delphi/Clipper2Lib/Clipper.Offset.pas +++ b/Delphi/Clipper2Lib/Clipper.Offset.pas @@ -2,7 +2,7 @@ (******************************************************************************* * Author : Angus Johnson * -* Date : 8 March 2024 * +* Date : 14 March 2024 * * Website : http://www.angusj.com * * Copyright : Angus Johnson 2010-2024 * * Purpose : Path Offset (Inflate/Shrink) * @@ -576,8 +576,8 @@ procedure TClipperOffset.OffsetOpenPath; end; // offset the left side going back - k := 0; - for i := highI downto 1 do //and stop at 1! + k := highI; + for i := highI -1 downto 1 do //and stop at 1! OffsetPoint(i, k); UpdateSolution; From 0e27e1601d13370672b372568b85c4530c25f90b Mon Sep 17 00:00:00 2001 From: angusj Date: Sun, 17 Mar 2024 08:05:05 +1000 Subject: [PATCH 30/64] Fixed a bug in Clipper.Offset (was too agressive in removing 'over-shrunk' paths) --- .../include/clipper2/clipper.offset.h | 5 +- CPP/Clipper2Lib/src/clipper.offset.cpp | 42 ++-------- CPP/Tests/TestOffsets.cpp | 59 +++++++++++++- CSharp/Clipper2Lib/Clipper.Offset.cs | 43 ++--------- Delphi/Clipper2Lib/Clipper.Offset.pas | 77 +++++++------------ 5 files changed, 101 insertions(+), 125 deletions(-) diff --git a/CPP/Clipper2Lib/include/clipper2/clipper.offset.h b/CPP/Clipper2Lib/include/clipper2/clipper.offset.h index 5536a2e3..c347a9fc 100644 --- a/CPP/Clipper2Lib/include/clipper2/clipper.offset.h +++ b/CPP/Clipper2Lib/include/clipper2/clipper.offset.h @@ -34,9 +34,6 @@ class ClipperOffset { class Group { public: Paths64 paths_in; - std::vector is_hole_list; - std::vector areas_list; - //std::vector bounds_list; int lowest_path_idx = -1; bool is_reversed = false; JoinType join_type; @@ -75,7 +72,7 @@ class ClipperOffset { void DoMiter(const Path64& path, size_t j, size_t k, double cos_a); void DoRound(const Path64& path, size_t j, size_t k, double angle); void BuildNormals(const Path64& path); - void OffsetPolygon(Group& group, const Path64& path, bool is_shrinking, double area); + void OffsetPolygon(Group& group, const Path64& path); void OffsetOpenJoined(Group& group, const Path64& path); void OffsetOpenPath(Group& group, const Path64& path); void OffsetPoint(Group& group, const Path64& path, size_t j, size_t k); diff --git a/CPP/Clipper2Lib/src/clipper.offset.cpp b/CPP/Clipper2Lib/src/clipper.offset.cpp index ff8d1f4a..fb01914f 100644 --- a/CPP/Clipper2Lib/src/clipper.offset.cpp +++ b/CPP/Clipper2Lib/src/clipper.offset.cpp @@ -125,31 +125,19 @@ ClipperOffset::Group::Group(const Paths64& _paths, JoinType _join_type, EndType if (end_type == EndType::Polygon) { - is_hole_list.reserve(paths_in.size()); - areas_list.reserve(paths_in.size()); - for (const Path64& path : paths_in) - { - double a = Area(path); - areas_list.push_back(a); - is_hole_list.push_back(a < 0); - } lowest_path_idx = GetLowestClosedPathIdx(paths_in); // the lowermost path must be an outer path, so if its orientation is negative, // then flag the whole group is 'reversed' (will negate delta etc.) // as this is much more efficient than reversing every path. - is_reversed = (lowest_path_idx >= 0) && is_hole_list[lowest_path_idx]; - if (is_reversed) is_hole_list.flip(); + is_reversed = (lowest_path_idx >= 0) && Area(paths_in[lowest_path_idx]) < 0; } else { lowest_path_idx = -1; is_reversed = false; - is_hole_list.resize(paths_in.size()); - areas_list.resize(paths_in.size()); } } - //------------------------------------------------------------------------------ // ClipperOffset methods //------------------------------------------------------------------------------ @@ -353,24 +341,17 @@ void ClipperOffset::OffsetPoint(Group& group, const Path64& path, size_t j, size DoSquare(path, j, k); } -void ClipperOffset::OffsetPolygon(Group& group, const Path64& path, bool is_shrinking, double area) +void ClipperOffset::OffsetPolygon(Group& group, const Path64& path) { path_out.clear(); for (Path64::size_type j = 0, k = path.size() - 1; j < path.size(); k = j, ++j) - OffsetPoint(group, path, j, k); - - // make sure that polygon areas aren't reversing which would indicate - // that the polygon has shrunk too far and that it should be discarded. - // See also - #593 & #715 - if (is_shrinking && area // area == 0.0 when JoinType::Joined - && ((area < 0) != (Area(path_out) < 0))) return; - + OffsetPoint(group, path, j, k); solution.push_back(path_out); } void ClipperOffset::OffsetOpenJoined(Group& group, const Path64& path) { - OffsetPolygon(group, path, false, 0); + OffsetPolygon(group, path); Path64 reverse_path(path); std::reverse(reverse_path.begin(), reverse_path.end()); @@ -380,7 +361,7 @@ void ClipperOffset::OffsetOpenJoined(Group& group, const Path64& path) norms.erase(norms.begin()); NegatePath(norms); - OffsetPolygon(group, reverse_path, true, 0); + OffsetPolygon(group, reverse_path); } void ClipperOffset::OffsetOpenPath(Group& group, const Path64& path) @@ -477,17 +458,10 @@ void ClipperOffset::DoGroupOffset(Group& group) steps_per_rad_ = steps_per_360 / (2 * PI); } - double min_area = PI * Sqr(group_delta_); - std::vector::const_iterator is_hole_it = group.is_hole_list.cbegin(); - std::vector::const_iterator area_it = group.areas_list.cbegin(); + //double min_area = PI * Sqr(group_delta_); Paths64::const_iterator path_in_it = group.paths_in.cbegin(); - for ( ; path_in_it != group.paths_in.cend(); ++path_in_it, ++is_hole_it, ++area_it) + for ( ; path_in_it != group.paths_in.cend(); ++path_in_it) { - bool is_shrinking = - (group.end_type == EndType::Polygon) && - (group.is_reversed == ((group_delta_ < 0) == *is_hole_it)); - if (is_shrinking && (std::fabs(*area_it) < min_area)) continue; - Path64::size_type pathLen = path_in_it->size(); path_out.clear(); @@ -532,7 +506,7 @@ void ClipperOffset::DoGroupOffset(Group& group) EndType::Square; BuildNormals(*path_in_it); - if (end_type_ == EndType::Polygon) OffsetPolygon(group, *path_in_it, is_shrinking, *area_it); + if (end_type_ == EndType::Polygon) OffsetPolygon(group, *path_in_it); else if (end_type_ == EndType::Joined) OffsetOpenJoined(group, *path_in_it); else OffsetOpenPath(group, *path_in_it); } diff --git a/CPP/Tests/TestOffsets.cpp b/CPP/Tests/TestOffsets.cpp index c4a8a3a6..9fdc15e8 100644 --- a/CPP/Tests/TestOffsets.cpp +++ b/CPP/Tests/TestOffsets.cpp @@ -1,8 +1,9 @@ #include #include "clipper2/clipper.offset.h" #include "ClipFileLoad.h" -//#include "clipper.svg.utils.h" + using namespace Clipper2Lib; + TEST(Clipper2Tests, TestOffsets) { std::ifstream ifs("Offsets.txt"); ASSERT_TRUE(ifs.good()); @@ -69,6 +70,7 @@ TEST(Clipper2Tests, TestOffsets2) { // see #448 & #456 EXPECT_GE(min_dist + 1, delta - arc_tol); // +1 for rounding errors EXPECT_LE(solution[0].size(), 21); } + TEST(Clipper2Tests, TestOffsets3) // see #424 { Paths64 subjects = {{ @@ -94,6 +96,7 @@ TEST(Clipper2Tests, TestOffsets3) // see #424 Paths64 solution = InflatePaths(subjects, -209715, JoinType::Miter, EndType::Polygon); EXPECT_LE(solution[0].size() - subjects[0].size(), 1); } + TEST(Clipper2Tests, TestOffsets4) // see #482 { Paths64 paths = { { {0, 0}, {20000, 200}, @@ -121,6 +124,7 @@ TEST(Clipper2Tests, TestOffsets4) // see #482 //std::cout << solution[0].size() << std::endl; EXPECT_GT(solution[0].size(), 5); } + TEST(Clipper2Tests, TestOffsets5) // modified from #593 (tests offset clean up) { Paths64 subject = { @@ -348,6 +352,7 @@ TEST(Clipper2Tests, TestOffsets5) // modified from #593 (tests offset clean up) Paths64 solution = InflatePaths(subject, -10000, JoinType::Round, EndType::Polygon); EXPECT_EQ(solution.size(), 2); } + TEST(Clipper2Tests, TestOffsets6) // also modified from #593 (tests rounded ends) { Paths64 subjects = { @@ -370,6 +375,7 @@ TEST(Clipper2Tests, TestOffsets6) // also modified from #593 (tests rounded ends double area = Area(solution[1]); EXPECT_LT(area, -47500); } + TEST(Clipper2Tests, TestOffsets7) // (#593 & #715) { Paths64 solution; @@ -387,6 +393,7 @@ TEST(Clipper2Tests, TestOffsets7) // (#593 & #715) solution = InflatePaths(subject, -50, JoinType::Miter, EndType::Polygon); EXPECT_EQ(solution.size(), 0); } + struct OffsetQual { PointD smallestInSub; // smallestInSub & smallestInSol are the points in subject and solution @@ -394,6 +401,7 @@ struct OffsetQual PointD largestInSub; // largestInSub & largestInSol are the points in subject and solution PointD largestInSol; // that define the place that most exceeds the expected offset }; + template inline PointD GetClosestPointOnSegment(const PointD& offPt, const Point& seg1, const Point& seg2) @@ -410,6 +418,7 @@ inline PointD GetClosestPointOnSegment(const PointD& offPt, static_cast(seg1.x) + (q * dx), static_cast(seg1.y) + (q * dy)); } + template static OffsetQual GetOffsetQuality(const Path& subject, const Path& solution, const double delta) { @@ -462,6 +471,7 @@ static OffsetQual GetOffsetQuality(const Path& subject, const Path& soluti } return oq; } + TEST(Clipper2Tests, TestOffsets8) // (#724) { Paths64 subject = { MakePath({ @@ -567,6 +577,7 @@ TEST(Clipper2Tests, TestOffsets8) // (#724) EXPECT_LE(offset - smallestDist - rounding_tolerance, arc_tol); EXPECT_LE(largestDist - offset - rounding_tolerance, arc_tol); } + TEST(Clipper2Tests, TestOffsets9) // (#733) { // solution orientations should match subject orientations UNLESS @@ -602,4 +613,48 @@ TEST(Clipper2Tests, TestOffsets9) // (#733) EXPECT_TRUE(IsPositive(solution[0])); solution = InflatePaths(subject, -15, JoinType::Miter, EndType::Polygon); EXPECT_EQ(solution.size(), 0); -} \ No newline at end of file +} + +TEST(Clipper2Tests, TestOffsets10) // see #715 +{ + Paths64 subjects = { + {{508685336, -435806096}, + {509492982, -434729201}, + {509615525, -434003092}, + {509615525, 493372891}, + {509206033, 494655198}, + {508129138, 495462844}, + {507403029, 495585387}, + {-545800889, 495585387}, + {-547083196, 495175895}, + {-547890842, 494099000}, + {-548013385, 493372891}, + {-548013385, -434003092}, + {-547603893, -435285399}, + {-546526998, -436093045}, + {-545800889, -436215588}, + {507403029, -436215588}}, + {{106954765, -62914568}, + {106795129, -63717113}, + {106340524, -64397478}, + {105660159, -64852084}, + {104857613, -65011720}, + {104055068, -64852084}, + {103374703, -64397478}, + {102920097, -63717113}, + {102760461, -62914568}, + {102920097, -62112022}, + {103374703, -61431657}, + {104055068, -60977052}, + {104857613, -60817416}, + {105660159, -60977052}, + {106340524, -61431657}, + {106795129, -62112022}} }; + + Clipper2Lib::ClipperOffset offseter(2, 104857.61318750000); + Paths64 solution; + offseter.AddPaths(subjects, Clipper2Lib::JoinType::Round, Clipper2Lib::EndType::Polygon); + offseter.Execute(-2212495.6382562499, solution); + EXPECT_EQ(solution.size(), 2); +} + diff --git a/CSharp/Clipper2Lib/Clipper.Offset.cs b/CSharp/Clipper2Lib/Clipper.Offset.cs index ed13ef00..ecfef3be 100644 --- a/CSharp/Clipper2Lib/Clipper.Offset.cs +++ b/CSharp/Clipper2Lib/Clipper.Offset.cs @@ -37,8 +37,6 @@ public class ClipperOffset private class Group { internal Paths64 inPaths; - internal List areasList; - internal List isHoleList; internal JoinType joinType; internal EndType endType; internal bool pathsReversed; @@ -56,29 +54,15 @@ public Group(Paths64 paths, JoinType joinType, EndType endType = EndType.Polygon if (endType == EndType.Polygon) { - isHoleList = new List(inPaths.Count); - areasList = new List(inPaths.Count); - - foreach (Path64 path in inPaths) - { - double a = Clipper.Area(path); - areasList.Add(a); - isHoleList.Add(a < 0); - } - lowestPathIdx = GetLowestPathIdx(inPaths); // the lowermost path must be an outer path, so if its orientation is negative, // then flag that the whole group is 'reversed' (will negate delta etc.) // as this is much more efficient than reversing every path. - pathsReversed = (lowestPathIdx >= 0) && isHoleList[lowestPathIdx]; - if (pathsReversed) - for (int i = 0; i < isHoleList.Count; i++) isHoleList[i] = !isHoleList[i]; + pathsReversed = (lowestPathIdx >= 0) && (Clipper.Area(inPaths[lowestPathIdx]) < 0); } else { lowestPathIdx = -1; - isHoleList = new List(new bool[inPaths.Count]); - areasList = new List(new double[inPaths.Count]); pathsReversed = false; } } @@ -599,29 +583,22 @@ private void OffsetPoint(Group group, Path64 path, int j, ref int k) } [MethodImpl(MethodImplOptions.AggressiveInlining)] - private void OffsetPolygon(Group group, Path64 path, bool is_shrinking, double area) + private void OffsetPolygon(Group group, Path64 path) { pathOut = new Path64(); int cnt = path.Count, prev = cnt - 1; for (int i = 0; i < cnt; i++) OffsetPoint(group, path, i, ref prev); - - // make sure that polygon areas aren't reversing which would indicate - // that the polygon has shrunk too far and that it should be discarded. - // See also - #593 & #715 - if (is_shrinking && area != 0 && // area == 0.0 when JoinType.Joined - ((area < 0) != (Clipper.Area(pathOut) < 0))) return; - _solution.Add(pathOut); } [MethodImpl(MethodImplOptions.AggressiveInlining)] private void OffsetOpenJoined(Group group, Path64 path) { - OffsetPolygon(group, path, false, 0); + OffsetPolygon(group, path); path = Clipper.ReversePath(path); BuildNormals(path); - OffsetPolygon(group, path, true, 0); + OffsetPolygon(group, path); } private void OffsetOpenPath(Group group, Path64 path) @@ -720,17 +697,9 @@ private void DoGroupOffset(Group group) double min_area = Math.PI * Clipper.Sqr(_groupDelta); using List.Enumerator pathIt = group.inPaths.GetEnumerator(); - using List.Enumerator isHoleIt = group.isHoleList.GetEnumerator(); - using List.Enumerator areaIt = group.areasList.GetEnumerator(); - while (pathIt.MoveNext() && isHoleIt.MoveNext() && areaIt.MoveNext()) + while (pathIt.MoveNext()) { - bool isShrinking = - (group.endType == EndType.Polygon) && - (group.pathsReversed == ((_groupDelta < 0) == isHoleIt.Current)); - if (isShrinking && (Math.Abs(areaIt.Current) < min_area)) continue; - Path64 p = pathIt.Current; - bool isHole = isHoleIt.Current; pathOut = new Path64(); int cnt = p.Count; @@ -776,7 +745,7 @@ private void DoGroupOffset(Group group) EndType.Square; BuildNormals(p); - if (_endType == EndType.Polygon) OffsetPolygon(group, p, isShrinking, areaIt.Current); + if (_endType == EndType.Polygon) OffsetPolygon(group, p); else if (_endType == EndType.Joined) OffsetOpenJoined(group, p); else OffsetOpenPath(group, p); } diff --git a/Delphi/Clipper2Lib/Clipper.Offset.pas b/Delphi/Clipper2Lib/Clipper.Offset.pas index 7a2be9ea..9f732cdf 100644 --- a/Delphi/Clipper2Lib/Clipper.Offset.pas +++ b/Delphi/Clipper2Lib/Clipper.Offset.pas @@ -2,7 +2,7 @@ (******************************************************************************* * Author : Angus Johnson * -* Date : 14 March 2024 * +* Date : 17 March 2024 * * Website : http://www.angusj.com * * Copyright : Angus Johnson 2010-2024 * * Purpose : Path Offset (Inflate/Shrink) * @@ -41,8 +41,6 @@ TGroup = class endType : TEndType; reversed : Boolean; lowestPathIdx : integer; - areasList : TDoubleArray; - isHoleList : BooleanArray; constructor Create(const pathsIn: TPaths64; jt: TJoinType; et: TEndType); end; @@ -86,7 +84,7 @@ TClipperOffset = class procedure BuildNormals; procedure DoGroupOffset(group: TGroup); - procedure OffsetPolygon(isShrinking: Boolean; area_: double); + procedure OffsetPolygon; procedure OffsetOpenJoined; procedure OffsetOpenPath; function CalcSolutionCapacity: integer; @@ -246,29 +244,13 @@ constructor TGroup.Create(const pathsIn: TPaths64; jt: TJoinType; et: TEndType); paths[i] := StripDuplicates(pathsIn[i], isJoined); reversed := false; - SetLength(isHoleList, len); - SetLength(areasList, len); if (et = etPolygon) then begin - pb := @isHoleList[0]; - for i := 0 to len -1 do - begin - a := Area(paths[i]); - pb^ := a < 0; - inc(pb); - end; - // the lowermost path must be an outer path, so if its orientation is // negative, then flag that the whole group is 'reversed' (so negate // delta etc.) as this is much more efficient than reversing every path. lowestPathIdx := GetLowestPolygonIdx(pathsIn); - reversed := (lowestPathIdx >= 0) and isHoleList[lowestPathIdx]; - if not reversed then Exit; - pb := @isHoleList[0]; - for i := 0 to len -1 do - begin - pb^ := not pb^; inc(pb); - end; + reversed := (lowestPathIdx >= 0) and (Area(pathsIn[lowestPathIdx]) < 0); end else lowestPathIdx := -1; end; @@ -355,7 +337,7 @@ function GetPerpendicD(const pt: TPoint64; const norm: TPointD; delta: double): procedure TClipperOffset.DoGroupOffset(group: TGroup); var i,j, len, steps: Integer; - r, stepsPer360, minArea, arcTol: Double; + r, stepsPer360, arcTol: Double; absDelta: double; isShrinking: Boolean; rec: TRect64; @@ -395,14 +377,8 @@ procedure TClipperOffset.DoGroupOffset(group: TGroup); fStepsPerRad := stepsPer360 / TwoPi; end; - minArea := PI * Sqr(fGroupDelta); for i := 0 to High(group.paths) do begin - isShrinking := (group.endType = etPolygon) and - (group.reversed = ((fGroupDelta < 0) = group.isHoleList[i])); - if (isShrinking and (Abs(group.areasList[i]) < minArea)) then - Continue; - fInPath := group.paths[i]; fNorms := nil; len := Length(fInPath); @@ -453,7 +429,7 @@ procedure TClipperOffset.DoGroupOffset(group: TGroup); BuildNormals; if fEndType = etPolygon then - OffsetPolygon(isShrinking, group.areasList[i]) + OffsetPolygon else if fEndType = etJoined then OffsetOpenJoined else @@ -498,33 +474,26 @@ function TClipperOffset.CalcSolutionCapacity: integer; end; //------------------------------------------------------------------------------ -procedure TClipperOffset.OffsetPolygon(isShrinking: Boolean; area_: double); +procedure TClipperOffset.OffsetPolygon; var i,j: integer; begin j := high(fInPath); for i := 0 to high(fInPath) do OffsetPoint(i, j); - - // make sure that polygon areas aren't reversing which would indicate - // that the polygon has shrunk too far and that it should be discarded. - // See also - #593 & #715 - if isShrinking and (area_ <> 0) and // area = 0.0 when JoinType.Joined - ((area_ < 0) <> (Area(fOutPath) < 0)) then Exit; - UpdateSolution; end; //------------------------------------------------------------------------------ procedure TClipperOffset.OffsetOpenJoined; begin - OffsetPolygon(false, 0); + OffsetPolygon; fInPath := ReversePath(fInPath); // Rebuild normals // BuildNormals; fNorms := ReversePath(fNorms); fNorms := ShiftPath(fNorms, 1); fNorms := NegatePath(fNorms); - OffsetPolygon(true, 0); + OffsetPolygon; end; //------------------------------------------------------------------------------ @@ -537,17 +506,29 @@ procedure TClipperOffset.OffsetOpenPath; if Assigned(fDeltaCallback64) then fGroupDelta := fDeltaCallback64(fInPath, fNorms, 0, 0); - // do the line start cap - if Abs(fGroupDelta) < Tolerance then + if (Abs(fGroupDelta) < Tolerance) and + not Assigned(fDeltaCallback64) then begin - AddPoint(fInPath[0]); - end else - case fEndType of - etButt: DoBevel(0, 0); - etRound: DoRound(0,0, PI); - else DoSquare(0, 0); + inc(highI); + SetLength(fOutPath, highI); + Move(fInPath[0], fOutPath, highI + SizeOf(TPointD)); + fOutPathLen := highI; + Exit; end; + // do the line start cap + if Assigned(fDeltaCallback64) then + fGroupDelta := fDeltaCallback64(fInPath, fNorms, 0, 0); + + if (Abs(fGroupDelta) < Tolerance) then + AddPoint(fInPath[0]) + else + case fEndType of + etButt: DoBevel(0, 0); + etRound: DoRound(0,0, PI); + else DoSquare(0, 0); + end; + // offset the left side going forward k := 0; for i := 1 to highI -1 do //nb: -1 is important @@ -939,7 +920,7 @@ procedure TClipperOffset.DoRound(j, k: Integer; angle: double); end; //------------------------------------------------------------------------------ -procedure TClipperOffset.OffsetPoint(j: Integer; var k: integer); + procedure TClipperOffset.OffsetPoint(j: Integer; var k: integer); var sinA, cosA: Double; begin From dde8d96cba119e8ab7a9aa541fec99d60b931b53 Mon Sep 17 00:00:00 2001 From: angusj Date: Thu, 21 Mar 2024 08:25:57 +1000 Subject: [PATCH 31/64] Fixed a bug in Clipper.Engine affecting Delphi only (#792) --- Delphi/Clipper2Lib/Clipper.Engine.pas | 27 ++++----- Delphi/Utils/Clipper.SVG.pas | 84 ++++++++++++++++++--------- 2 files changed, 69 insertions(+), 42 deletions(-) diff --git a/Delphi/Clipper2Lib/Clipper.Engine.pas b/Delphi/Clipper2Lib/Clipper.Engine.pas index 26ac2202..e733e437 100644 --- a/Delphi/Clipper2Lib/Clipper.Engine.pas +++ b/Delphi/Clipper2Lib/Clipper.Engine.pas @@ -2,7 +2,7 @@ (******************************************************************************* * Author : Angus Johnson * -* Date : 14 February 2024 * +* Date : 21 March 2024 * * Website : http://www.angusj.com * * Copyright : Angus Johnson 2010-2024 * * Purpose : This is the main polygon clipping module * @@ -282,7 +282,7 @@ TClipperBase = class function ClearSolutionOnly: Boolean; procedure ExecuteInternal(clipType: TClipType; fillRule: TFillRule; usingPolytree: Boolean); - function BuildPaths(out closedPaths, openPaths: TPaths64): Boolean; + function BuildPaths(var closedPaths, openPaths: TPaths64): Boolean; function BuildTree(polytree: TPolyPathBase; out openPaths: TPaths64): Boolean; {$IFDEF USINGZ} procedure SetZ( e1, e2: PActive; var intersectPt: TPoint64); @@ -3665,17 +3665,15 @@ function TClipperBase.DoMaxima(e: PActive): PActive; end; //------------------------------------------------------------------------------ -function TClipperBase.BuildPaths(out closedPaths, openPaths: TPaths64): Boolean; +function TClipperBase.BuildPaths(var closedPaths, openPaths: TPaths64): Boolean; var - i, cntClosed, cntOpen: Integer; + i: Integer; + closedCnt, openCnt: integer; outRec: POutRec; begin + closedCnt := Length(closedPaths); + openCnt := Length(openPaths); try - cntClosed := 0; cntOpen := 0; - SetLength(closedPaths, FOutRecList.Count); - if FHasOpenPaths then - SetLength(openPaths, FOutRecList.Count); - i := 0; while i < FOutRecList.Count do begin @@ -3685,22 +3683,21 @@ function TClipperBase.BuildPaths(out closedPaths, openPaths: TPaths64): Boolean; if outRec.isOpen then begin + SetLength(openPaths, openCnt +1); if BuildPath(outRec.pts, FReverseSolution, - true, openPaths[cntOpen]) then - inc(cntOpen); + true, openPaths[openCnt]) then inc(openCnt); end else begin // nb: CleanCollinear can add to FOutRecList CleanCollinear(outRec); // closed paths should always return a Positive orientation // except when ReverseSolution == true + SetLength(closedPaths, closedCnt +1); if BuildPath(outRec.pts, FReverseSolution, - false, closedPaths[cntClosed]) then - inc(cntClosed); + false, closedPaths[closedCnt]) then + inc(closedCnt); end; end; - SetLength(closedPaths, cntClosed); - SetLength(openPaths, cntOpen); result := true; except result := false; diff --git a/Delphi/Utils/Clipper.SVG.pas b/Delphi/Utils/Clipper.SVG.pas index 13ad1929..279d1211 100644 --- a/Delphi/Utils/Clipper.SVG.pas +++ b/Delphi/Utils/Clipper.SVG.pas @@ -2,7 +2,7 @@ (******************************************************************************* * Author : Angus Johnson * -* Date : 7 March 2024 * +* Date : 21 March 2024 * * Website : http://www.angusj.com * * Copyright : Angus Johnson 2010-2024 * * Purpose : This module provides a very simple SVG Writer for Clipper2 * @@ -69,6 +69,7 @@ TPolyInfo = record PenClr : Cardinal; PenWidth : double; ShowCoords: Boolean; + ShowFrac : Boolean; IsOpen : Boolean; dashes : TArrayOfInteger; end; @@ -135,9 +136,9 @@ TSvgWriter = class procedure ClearAll; end; - procedure AddSubject(svg: TSvgWriter; const paths: TPaths64); overload; + procedure AddSubject(svg: TSvgWriter; const paths: TPaths64; showCoords: Boolean = false); overload; procedure AddOpenSubject(svg: TSvgWriter; const paths: TPaths64); overload; - procedure AddClip(svg: TSvgWriter; const paths: TPaths64); overload; + procedure AddClip(svg: TSvgWriter; const paths: TPaths64; showCoords: Boolean = false); overload; procedure AddSolution(svg: TSvgWriter; const paths: TPaths64; showCoords: Boolean = false); overload; procedure AddOpenSolution(svg: TSvgWriter; const paths: TPaths64); overload; procedure AddDots(svg: TSvgWriter; const paths: TPaths64; radius: double; color: Cardinal); overload; @@ -145,9 +146,9 @@ TSvgWriter = class procedure SaveSvg(svg: TSvgWriter; const filename: string; width: integer = 0; height: integer = 0; margin: integer = 0); overload; - procedure AddSubject(svg: TSvgWriter; const paths: TPathsD); overload; + procedure AddSubject(svg: TSvgWriter; const paths: TPathsD; showCoords: Boolean = false); overload; procedure AddOpenSubject(svg: TSvgWriter; const paths: TPathsD); overload; - procedure AddClip(svg: TSvgWriter; const paths: TPathsD); overload; + procedure AddClip(svg: TSvgWriter; const paths: TPathsD; showCoords: Boolean = false); overload; procedure AddSolution(svg: TSvgWriter; const paths: TPathsD; showCoords: Boolean= false); overload; procedure AddOpenSolution(svg: TSvgWriter; const paths: TPathsD); overload; procedure AddDots(svg: TSvgWriter; const paths: TPathsD; radius: double; color: Cardinal); overload; @@ -236,12 +237,13 @@ procedure TSvgWriter.AddPaths(const paths: TPaths64; begin if Length(paths) = 0 then Exit; new(pi); - pi.paths := PathsD(paths); - pi.BrushClr := brushColor; - pi.PenClr := penColor; - pi.PenWidth := penWidth; + pi.paths := PathsD(paths); + pi.BrushClr := brushColor; + pi.PenClr := penColor; + pi.PenWidth := penWidth; pi.ShowCoords := showCoords; - pi.IsOpen := isOpen; + pi.ShowFrac := false; + pi.IsOpen := isOpen; fPolyInfos.Add(pi); end; @@ -252,11 +254,12 @@ procedure TSvgWriter.AddPaths(const paths: TPathsD; isOpen: Boolean; pi: PPolyInfo; begin new(pi); - pi.paths := Copy(paths, 0, Length(paths)); - pi.BrushClr := brushColor; - pi.PenClr := penColor; - pi.PenWidth := penWidth; + pi.paths := Copy(paths, 0, Length(paths)); + pi.BrushClr := brushColor; + pi.PenClr := penColor; + pi.PenWidth := penWidth; pi.ShowCoords := showCoords; + pi.ShowFrac := true; pi.IsOpen := isOpen; fPolyInfos.Add(pi); end; @@ -275,6 +278,7 @@ procedure TSvgWriter.AddDashedPath(const paths: TPaths64; pi.PenClr := penColor; pi.PenWidth := penWidth; pi.ShowCoords := false; + pi.ShowFrac := false; pi.IsOpen := true; SetLength(pi.dashes, dLen); //Move(dashes[0], pi.dashes, dLen * sizeOf(integer)); @@ -297,6 +301,7 @@ procedure TSvgWriter.AddDashedPath(const paths: TPathsD; pi.PenClr := penColor; pi.PenWidth := penWidth; pi.ShowCoords := false; + pi.ShowFrac := true; pi.IsOpen := true; SetLength(pi.dashes, dLen); Move(dashes[0], pi.dashes, dLen * sizeOf(integer)); @@ -376,6 +381,7 @@ function TSvgWriter.GetBounds: TRectD; with PPolyInfo(fPolyInfos[i])^ do begin bounds := Clipper.Core.GetBounds(paths); + if bounds.IsEmpty then Continue; if (bounds.left < Result.Left) then Result.Left := bounds.Left; if (bounds.right> Result.Right) then Result.Right := bounds.Right; if (bounds.top < Result.Top) then Result.Top := bounds.Top; @@ -415,11 +421,15 @@ function TSvgWriter.SaveToFile(const filename: string; maxWidth: integer = 0; maxHeight: integer = 0; margin: integer = 20): Boolean; var i, j, k : integer; + frac : integer; + showCo : boolean; + showFr : boolean; x,y : double; bounds : TRectD; scale : double; offsetX, offsetY : integer; - s, sInline, dashStr: string; + s : string; + sInline, dashStr : string; sl : TStringList; formatSettings: TFormatSettings; const @@ -439,15 +449,33 @@ function TSvgWriter.SaveToFile(const filename: string; end; begin - + Result := false; {$IF NOT Defined(fpc) AND (CompilerVersion > 19)} //Delphi XE + formatSettings := TFormatSettings.Create; {$IFEND} formatSettings.DecimalSeparator := '.'; - Result := false; + // adjust margin if (margin < 20) then margin := 20; + showCo := false; + showFr := false; + for i := 0 to fPolyInfos.Count -1 do + with PPolyInfo(fPolyInfos[i])^ do + if ShowCoords then + begin + showCo := true; + if showFrac then showFr := true; + + end; + if showCo then + begin + if showFr then + inc(margin, Abs(fCoordStyle.FontSize *4)) else + inc(margin, Abs(fCoordStyle.FontSize *2)); + end; + + // get scale and offset bounds := GetBounds; if bounds.IsEmpty then Exit; @@ -460,6 +488,7 @@ function TSvgWriter.SaveToFile(const filename: string; offsetX := margin - Round(bounds.left * scale); offsetY := margin - Round(bounds.top * scale); + // write SVG sl := TStringList.Create; try if (maxWidth <= 0) or (maxHeight <= 0) then @@ -511,14 +540,15 @@ function TSvgWriter.SaveToFile(const filename: string; if (ShowCoords) then begin + if ShowFrac then frac := 2 else frac := 0; with fCoordStyle do Add(Format('', [FontName, FontSize, ColorToHtml(FontColor)], formatSettings)); for j := 0 to High(paths) do for k := 0 to High(paths[j]) do with paths[j][k] do - Add(Format(' %1.0f,%1.0f', - [x * scale + offsetX, y * scale + offsetY, x, y], + Add(Format(' %1.*f,%1.*f', + [x * scale + offsetX, y * scale + offsetY, frac, x, frac, y], formatSettings)); Add(''#10); end; @@ -565,9 +595,9 @@ function TSvgWriter.SaveToFile(const filename: string; end; -procedure AddSubject(svg: TSvgWriter; const paths: TPaths64); +procedure AddSubject(svg: TSvgWriter; const paths: TPaths64; showCoords: Boolean = false); begin - svg.AddPaths(paths, false, $200099FF, $800066FF, 1.0); + svg.AddPaths(paths, false, $200099FF, $800066FF, 1.0, showCoords); end; procedure AddOpenSubject(svg: TSvgWriter; const paths: TPaths64); @@ -575,9 +605,9 @@ procedure AddOpenSubject(svg: TSvgWriter; const paths: TPaths64); svg.AddPaths(paths, true, $0, $800066FF, 2.2); end; -procedure AddClip(svg: TSvgWriter; const paths: TPaths64); +procedure AddClip(svg: TSvgWriter; const paths: TPaths64; showCoords: Boolean = false); begin - svg.AddPaths(paths, false, $10FF9900, $80FF6600, 1.0); + svg.AddPaths(paths, false, $10FF9900, $80FF6600, 1.0, showCoords); end; procedure AddSolution(svg: TSvgWriter; const paths: TPaths64; showCoords: Boolean); @@ -596,9 +626,9 @@ procedure SaveSvg(svg: TSvgWriter; const filename: string; svg.SaveToFile(filename, width, height, margin); end; -procedure AddSubject(svg: TSvgWriter; const paths: TPathsD); +procedure AddSubject(svg: TSvgWriter; const paths: TPathsD; showCoords: Boolean = false); begin - svg.AddPaths(paths, false, $200099FF, $800066FF, 1.0); + svg.AddPaths(paths, false, $200099FF, $800066FF, 1.0, showCoords); end; procedure AddOpenSubject(svg: TSvgWriter; const paths: TPathsD); @@ -606,9 +636,9 @@ procedure AddOpenSubject(svg: TSvgWriter; const paths: TPathsD); svg.AddPaths(paths, true, $0, $400066FF, 2.2); end; -procedure AddClip(svg: TSvgWriter; const paths: TPathsD); +procedure AddClip(svg: TSvgWriter; const paths: TPathsD; showCoords: Boolean = false); begin - svg.AddPaths(paths, false, $10FF9900, $80FF6600, 1.0); + svg.AddPaths(paths, false, $10FF9900, $80FF6600, 1.0, showCoords); end; procedure AddSolution(svg: TSvgWriter; const paths: TPathsD; showCoords: Boolean); From ddf4416113e4807cb361ce39dfe66508701a258b Mon Sep 17 00:00:00 2001 From: angusj Date: Mon, 25 Mar 2024 08:43:31 +1000 Subject: [PATCH 32/64] Clipper.Offset: fixed assigning Z values when USINGZ enabled (#772) --- .../include/clipper2/clipper.core.h | 8 +- .../include/clipper2/clipper.offset.h | 10 +- CPP/Clipper2Lib/src/clipper.engine.cpp | 3 +- CPP/Clipper2Lib/src/clipper.offset.cpp | 121 ++++++++++-------- CPP/Utils/clipper.svg.cpp | 65 +++++----- CPP/Utils/clipper.svg.h | 24 ++-- CPP/Utils/clipper.svg.utils.h | 10 +- CSharp/Clipper2Lib/Clipper.Core.cs | 6 +- CSharp/Clipper2Lib/Clipper.Offset.cs | 88 ++++++------- CSharp/Clipper2Lib/Clipper.cs | 20 +++ CSharp/USINGZ.TestApp/Program.cs | 104 ++++++++------- CSharp/USINGZ.TestApp/UsingZTestApp.csproj | 4 + CSharp/Utils/SVG/Clipper.SVG.cs | 29 +++-- Delphi/Clipper2Lib/Clipper.Core.pas | 41 +++++- Delphi/Clipper2Lib/Clipper.Offset.pas | 51 +++++++- 15 files changed, 362 insertions(+), 222 deletions(-) diff --git a/CPP/Clipper2Lib/include/clipper2/clipper.core.h b/CPP/Clipper2Lib/include/clipper2/clipper.core.h index 87ec88a7..2513f4c9 100644 --- a/CPP/Clipper2Lib/include/clipper2/clipper.core.h +++ b/CPP/Clipper2Lib/include/clipper2/clipper.core.h @@ -1,6 +1,6 @@ /******************************************************************************* * Author : Angus Johnson * -* Date : 14 February 2024 * +* Date : 24 March 2024 * * Website : http://www.angusj.com * * Copyright : Angus Johnson 2010-2024 * * Purpose : Core Clipper Library structures and functions * @@ -141,6 +141,12 @@ namespace Clipper2Lib Init(p.x, p.y, p.z); } + template + explicit Point(const Point& p, int64_t z_) + { + Init(p.x, p.y, z_); + } + Point operator * (const double scale) const { return Point(x * scale, y * scale, z); diff --git a/CPP/Clipper2Lib/include/clipper2/clipper.offset.h b/CPP/Clipper2Lib/include/clipper2/clipper.offset.h index c347a9fc..19ee9676 100644 --- a/CPP/Clipper2Lib/include/clipper2/clipper.offset.h +++ b/CPP/Clipper2Lib/include/clipper2/clipper.offset.h @@ -1,6 +1,6 @@ /******************************************************************************* * Author : Angus Johnson * -* Date : 14 February 2024 * +* Date : 24 March 2024 * * Website : http://www.angusj.com * * Copyright : Angus Johnson 2010-2024 * * Purpose : Path Offset (Inflate/Shrink) * @@ -50,7 +50,8 @@ class ClipperOffset { double step_cos_ = 0.0; PathD norms; Path64 path_out; - Paths64 solution; + Paths64* solution = nullptr; + PolyTree64* solution_tree = nullptr; std::vector groups_; JoinType join_type_ = JoinType::Bevel; EndType end_type_ = EndType::Polygon; @@ -62,9 +63,10 @@ class ClipperOffset { #ifdef USINGZ ZCallback64 zCallback64_ = nullptr; + void ZCB(const Point64& bot1, const Point64& top1, + const Point64& bot2, const Point64& top2, Point64& ip); #endif DeltaCallback64 deltaCallback64_ = nullptr; - size_t CalcSolutionCapacity(); bool CheckReverseOrientation(); void DoBevel(const Path64& path, size_t j, size_t k); @@ -89,7 +91,7 @@ class ClipperOffset { ~ClipperOffset() { Clear(); }; - int ErrorCode() { return error_code_; }; + int ErrorCode() const { return error_code_; }; void AddPath(const Path64& path, JoinType jt_, EndType et_); void AddPaths(const Paths64& paths, JoinType jt_, EndType et_); void Clear() { groups_.clear(); norms.clear(); }; diff --git a/CPP/Clipper2Lib/src/clipper.engine.cpp b/CPP/Clipper2Lib/src/clipper.engine.cpp index 863dfec4..c604faf4 100644 --- a/CPP/Clipper2Lib/src/clipper.engine.cpp +++ b/CPP/Clipper2Lib/src/clipper.engine.cpp @@ -1,6 +1,6 @@ /******************************************************************************* * Author : Angus Johnson * -* Date : 14 February 2024 * +* Date : 24 March 2024 * * Website : http://www.angusj.com * * Copyright : Angus Johnson 2010-2024 * * Purpose : This is the main polygon clipping module * @@ -2168,6 +2168,7 @@ namespace Clipper2Lib { horz_seg_list_.end(), [](HorzSegment& hs) { return UpdateHorzSegment(hs); }); if (j < 2) return; + std::stable_sort(horz_seg_list_.begin(), horz_seg_list_.end(), HorzSegSorter()); HorzSegmentList::iterator hs1 = horz_seg_list_.begin(), hs2; diff --git a/CPP/Clipper2Lib/src/clipper.offset.cpp b/CPP/Clipper2Lib/src/clipper.offset.cpp index fb01914f..da6daed9 100644 --- a/CPP/Clipper2Lib/src/clipper.offset.cpp +++ b/CPP/Clipper2Lib/src/clipper.offset.cpp @@ -1,6 +1,6 @@ /******************************************************************************* * Author : Angus Johnson * -* Date : 14 March 2024 * +* Date : 24 March 2024 * * Website : http://www.angusj.com * * Copyright : Angus Johnson 2010-2024 * * Purpose : Path Offset (Inflate/Shrink) * @@ -316,11 +316,19 @@ void ClipperOffset::OffsetPoint(Group& group, const Path64& path, size_t j, size if (cos_a > -0.999 && (sin_a * group_delta_ < 0)) // test for concavity first (#593) { // is concave +#ifdef USINGZ + path_out.push_back(Point64(GetPerpendic(path[j], norms[k], group_delta_), path[j].z)); +#else path_out.push_back(GetPerpendic(path[j], norms[k], group_delta_)); +#endif // this extra point is the only (simple) way to ensure that // path reversals are fully cleaned with the trailing clipper path_out.push_back(path[j]); // (#405) +#ifdef USINGZ + path_out.push_back(Point64(GetPerpendic(path[j], norms[j], group_delta_), path[j].z)); +#else path_out.push_back(GetPerpendic(path[j], norms[j], group_delta_)); +#endif } else if (cos_a > 0.999 && join_type_ != JoinType::Round) { @@ -346,7 +354,7 @@ void ClipperOffset::OffsetPolygon(Group& group, const Path64& path) path_out.clear(); for (Path64::size_type j = 0, k = path.size() - 1; j < path.size(); k = j, ++j) OffsetPoint(group, path, j, k); - solution.push_back(path_out); + solution->push_back(path_out); } void ClipperOffset::OffsetOpenJoined(Group& group, const Path64& path) @@ -421,7 +429,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.push_back(path_out); + solution->push_back(path_out); } void ClipperOffset::DoGroupOffset(Group& group) @@ -496,7 +504,7 @@ void ClipperOffset::DoGroupOffset(Group& group) #endif } - solution.push_back(path_out); + solution->push_back(path_out); continue; } // end of offsetting a single point @@ -512,6 +520,16 @@ void ClipperOffset::DoGroupOffset(Group& group) } } +#ifdef USINGZ +void ClipperOffset::ZCB(const Point64& bot1, const Point64& top1, + const Point64& bot2, const Point64& top2, Point64& ip) +{ + if (bot1.z && (bot1.z == bot2.z) || (bot1.z == top2.z)) ip.z = bot1.z; + else if (bot2.z && (bot2.z == top1.z)) ip.z = bot2.z; + else if (top1.z && (top1.z == top2.z)) ip.z = top1.z; + else if (zCallback64_) zCallback64_(bot1, top1, bot2, top2, ip); +} +#endif size_t ClipperOffset::CalcSolutionCapacity() { @@ -537,40 +555,35 @@ bool ClipperOffset::CheckReverseOrientation() void ClipperOffset::ExecuteInternal(double delta) { error_code_ = 0; - solution.clear(); if (groups_.size() == 0) return; - solution.reserve(CalcSolutionCapacity()); + solution->reserve(CalcSolutionCapacity()); if (std::abs(delta) < 0.5) // ie: offset is insignificant { Paths64::size_type sol_size = 0; for (const Group& group : groups_) sol_size += group.paths_in.size(); - solution.reserve(sol_size); + solution->reserve(sol_size); for (const Group& group : groups_) - copy(group.paths_in.begin(), group.paths_in.end(), back_inserter(solution)); - return; + copy(group.paths_in.begin(), group.paths_in.end(), back_inserter(*solution)); } + else + { - temp_lim_ = (miter_limit_ <= 1) ? - 2.0 : - 2.0 / (miter_limit_ * miter_limit_); + temp_lim_ = (miter_limit_ <= 1) ? + 2.0 : + 2.0 / (miter_limit_ * miter_limit_); - delta_ = delta; - std::vector::iterator git; - for (git = groups_.begin(); git != groups_.end(); ++git) - { - DoGroupOffset(*git); - if (!error_code_) continue; // all OK - solution.clear(); + delta_ = delta; + std::vector::iterator git; + for (git = groups_.begin(); git != groups_.end(); ++git) + { + DoGroupOffset(*git); + if (!error_code_) continue; // all OK + solution->clear(); + } } -} -void ClipperOffset::Execute(double delta, Paths64& paths) -{ - paths.clear(); - - ExecuteInternal(delta); - if (!solution.size()) return; + if (!solution->size()) return; bool paths_reversed = CheckReverseOrientation(); //clean up self-intersections ... @@ -579,41 +592,45 @@ void ClipperOffset::Execute(double delta, Paths64& paths) //the solution should retain the orientation of the input c.ReverseSolution(reverse_solution_ != paths_reversed); #ifdef USINGZ - if (zCallback64_) { c.SetZCallback(zCallback64_); } + auto fp = std::bind(&ClipperOffset::ZCB, this, std::placeholders::_1, + std::placeholders::_2, std::placeholders::_3, + std::placeholders::_4, std::placeholders::_5); + c.SetZCallback(fp); #endif - c.AddSubject(solution); - if (paths_reversed) - c.Execute(ClipType::Union, FillRule::Negative, paths); + c.AddSubject(*solution); + if (solution_tree) + { + if (paths_reversed) + c.Execute(ClipType::Union, FillRule::Negative, *solution_tree); + else + c.Execute(ClipType::Union, FillRule::Positive, *solution_tree); + } else - c.Execute(ClipType::Union, FillRule::Positive, paths); + { + if (paths_reversed) + c.Execute(ClipType::Union, FillRule::Negative, *solution); + else + c.Execute(ClipType::Union, FillRule::Positive, *solution); + } +} + +void ClipperOffset::Execute(double delta, Paths64& paths) +{ + paths.clear(); + solution = &paths; + solution_tree = nullptr; + ExecuteInternal(delta); } void ClipperOffset::Execute(double delta, PolyTree64& polytree) { polytree.Clear(); - + solution_tree = &polytree; + solution = new Paths64(); ExecuteInternal(delta); - if (!solution.size()) return; - - bool paths_reversed = CheckReverseOrientation(); - //clean up self-intersections ... - Clipper64 c; - c.PreserveCollinear(false); - //the solution should retain the orientation of the input - c.ReverseSolution (reverse_solution_ != paths_reversed); -#ifdef USINGZ - if (zCallback64_) { - c.SetZCallback(zCallback64_); - } -#endif - c.AddSubject(solution); - - - if (paths_reversed) - c.Execute(ClipType::Union, FillRule::Negative, polytree); - else - c.Execute(ClipType::Union, FillRule::Positive, polytree); + delete solution; + solution = nullptr; } void ClipperOffset::Execute(DeltaCallback64 delta_cb, Paths64& paths) diff --git a/CPP/Utils/clipper.svg.cpp b/CPP/Utils/clipper.svg.cpp index c0a8754d..1206bbb1 100644 --- a/CPP/Utils/clipper.svg.cpp +++ b/CPP/Utils/clipper.svg.cpp @@ -1,8 +1,8 @@ /******************************************************************************* * Author : Angus Johnson * -* Date : 28 January 2023 * +* Date : 24 March 2024 * * Website : http://www.angusj.com * -* Copyright : Angus Johnson 2010-2023 * +* Copyright : Angus Johnson 2010-2024 * * License : http://www.boost.org/LICENSE_1_0.txt * *******************************************************************************/ @@ -47,11 +47,11 @@ namespace Clipper2Lib { } //------------------------------------------------------------------------------ - void SvgWriter::Clear() + void SvgWriter::Clear() { - for (PathInfoList::iterator pi_iter = path_infos.begin(); + for (PathInfoList::iterator pi_iter = path_infos.begin(); pi_iter != path_infos.end(); ++pi_iter) delete (*pi_iter); - path_infos.resize(0); + path_infos.resize(0); } //------------------------------------------------------------------------------ @@ -65,7 +65,7 @@ namespace Clipper2Lib { //------------------------------------------------------------------------------ void SvgWriter::AddText(const std::string &text, - unsigned font_color, unsigned font_size, int x, int y) + unsigned font_color, unsigned font_size, double x, double y) { text_infos.push_back(new TextInfo(text, "", font_color, 600, font_size, x, y)); } @@ -113,7 +113,7 @@ namespace Clipper2Lib { unsigned brush_color, unsigned pen_color, double pen_width, bool show_coords) { if (paths.size() == 0) return; - path_infos.push_back(new PathInfo(paths, is_open, fillrule, + path_infos.push_back(new PathInfo(paths, is_open, fillrule, brush_color, pen_color, pen_width, show_coords)); } //------------------------------------------------------------------------------ @@ -157,7 +157,7 @@ namespace Clipper2Lib { double scale = std::min( static_cast(max_width - margin * 2) / rec.Width(), static_cast(max_height - margin * 2) / rec.Height()); - + rec.Scale(scale); double offsetX = margin -rec.left; double offsetY = margin -rec.top; @@ -174,15 +174,15 @@ namespace Clipper2Lib { max_height << svg_xml_header_3; setlocale(LC_NUMERIC, "C"); file.precision(2); - + for (PathInfo* pi : path_infos) { if (pi->is_open_path || GetAlphaAsFrac(pi->brush_color_) == 0 || (pi->fillrule_ != FillRule::Positive && pi->fillrule_ != FillRule::Negative)) continue; - PathsD ppp = pi->fillrule_ == FillRule::Positive ? - SimulatePositiveFill(pi->paths_) : + PathsD ppp = pi->fillrule_ == FillRule::Positive ? + SimulatePositiveFill(pi->paths_) : SimulateNegativeFill(pi->paths_); file << " pen_width_ << svg_xml_6; } - for (PathInfo* pi : path_infos) + for (PathInfo* pi : path_infos) { unsigned brushColor = (pi->fillrule_ == FillRule::Positive || pi->fillrule_ == FillRule::Negative) ? 0 : pi->brush_color_; - file << " paths_) { if (path.size() < 2 || (path.size() == 2 && !pi->is_open_path)) continue; file << " M " << (static_cast(path[0].x) * scale + offsetX) << " " << (static_cast(path[0].y) * scale + offsetY); for (PointD& pt : path) - file << " L " << (pt.x * scale + offsetX) << " " + file << " L " << (pt.x * scale + offsetX) << " " << (pt.y * scale + offsetY); if(!pi->is_open_path) file << " z"; } @@ -231,18 +231,18 @@ namespace Clipper2Lib { svg_xml_5 << pi->pen_width_ << svg_xml_6; if (pi->show_coords_) { - file << std::setprecision(0) << + file << std::setprecision(0) << " \n"; - for (PathD& path : pi->paths_) + for (const PathD& path : pi->paths_) { size_t path_len = path.size(); if (path_len < 2 || (path_len == 2 && !pi->is_open_path)) continue; - for (PointD& pt : path) + for (const PointD& pt : path) file << " (pt.x * scale + offsetX) << "\" y=\"" << static_cast(pt.y * scale + offsetY) << "\">" << - pt.x << "," << pt.y << "\n"; + pt.x << "," << pt.y << "\n"; } file << " \n\n"; } @@ -255,12 +255,13 @@ namespace Clipper2Lib { // for (PointD& pt : path) // DrawCircle(file, pt.x * scale + offsetX, pt.y * scale + offsetY, 1.6); - for (TextInfo* ti : text_infos) + for (TextInfo* ti : text_infos) { file << " font_name << "\" font-size=\"" << ti->font_size << "\" fill=\"" << ColorToHtml(ti->font_color) << "\" fill-opacity=\"" << GetAlphaAsFrac(ti->font_color) << "\">\n"; - file << " x * scale + offsetX) << "\" y=\"" << (ti->y * scale + offsetY) << "\">" << + file << " (ti->x * scale + offsetX) << + "\" y=\"" << static_cast(ti->y * scale + offsetY) << "\">" << ti->text << "\n \n\n"; } @@ -269,7 +270,7 @@ namespace Clipper2Lib { setlocale(LC_NUMERIC, ""); return true; } - + //------------------------------------------------------------------------------ //------------------------------------------------------------------------------ @@ -351,7 +352,7 @@ namespace Clipper2Lib { char command; bool is_relative; int vals_needed = 2; - //nb: M == absolute move, m == relative move + //nb: M == absolute move, m == relative move if (!GetCommand(p, command, is_relative) || command != 'M') return false; double vals[2] { 0, 0 }; double x = 0, y = 0; @@ -360,19 +361,19 @@ namespace Clipper2Lib { PathsD ppp; PathD pp; pp.push_back(PointD(x, y)); - while (SkipBlanks(p, pe)) + while (SkipBlanks(p, pe)) { - if (GetCommand(p, command, is_relative)) + if (GetCommand(p, command, is_relative)) { switch (command) { - case 'L': + case 'L': case 'M': {vals_needed = 2; break; } - case 'H': + case 'H': case 'V': {vals_needed = 1; break; } case 'Z': { if (pp.size() > 2) ppp.push_back(pp); pp.clear(); - vals_needed = 0; + vals_needed = 0; break; } default: vals_needed = -1; @@ -385,13 +386,13 @@ namespace Clipper2Lib { if (!GetNum(p, pe, vals[i])) vals_needed = -1; if (vals_needed <= 0) break; //oops! switch (vals_needed) { - case 1: + case 1: { if (command == 'V') y = (is_relative ? y + vals[0] : vals[0]); else x = (is_relative ? x + vals[0] : vals[0]); break; } - case 2: + case 2: { x = (is_relative ? x + vals[0] : vals[0]); y = (is_relative ? y + vals[1] : vals[1]); @@ -421,7 +422,7 @@ namespace Clipper2Lib { std::string::const_iterator p = xml.cbegin(), q, xml_end = xml.cend(); while (Find("", p, xml_end)) break; @@ -431,7 +432,7 @@ namespace Clipper2Lib { return path_infos.size() > 0; } - PathsD SvgReader::GetPaths() + PathsD SvgReader::GetPaths() { PathsD result; for (size_t i = 0; i < path_infos.size(); ++i) diff --git a/CPP/Utils/clipper.svg.h b/CPP/Utils/clipper.svg.h index 35b59fcb..2c718137 100644 --- a/CPP/Utils/clipper.svg.h +++ b/CPP/Utils/clipper.svg.h @@ -1,8 +1,8 @@ /******************************************************************************* * Author : Angus Johnson * -* Date : 23 July 2022 * +* Date : 24 March 2024 * * Website : http://www.angusj.com * -* Copyright : Angus Johnson 2010-2022 * +* Copyright : Angus Johnson 2010-2024 * * License : http://www.boost.org/LICENSE_1_0.txt * *******************************************************************************/ @@ -29,7 +29,7 @@ namespace Clipper2Lib { PathInfo(const PathsD& paths, bool is_open, FillRule fillrule, unsigned brush_clr, unsigned pen_clr, double pen_width, bool show_coords) : paths_(paths), is_open_path(is_open), fillrule_(fillrule), - brush_color_(brush_clr), pen_color_(pen_clr), + brush_color_(brush_clr), pen_color_(pen_clr), pen_width_(pen_width), show_coords_(show_coords) {}; friend class SvgWriter; friend class SvgReader; @@ -56,11 +56,11 @@ namespace Clipper2Lib { unsigned font_color = 0xFF000000; unsigned font_weight = 600; unsigned font_size = 11; - int x = 0; - int y = 0; + double x = 0; + double y = 0; TextInfo(const std::string &txt, const std::string &fnt_name, unsigned color, - unsigned weight, unsigned size, int coord_x, int coord_y) : + unsigned weight, unsigned size, double coord_x, double coord_y) : text(txt), font_name(fnt_name), font_color(color), font_weight(weight), font_size(size), x(coord_x), y(coord_y) {}; friend class SvgWriter; @@ -77,27 +77,27 @@ namespace Clipper2Lib { void DrawCircle(std::ofstream& file, double x, double y, double radius); public: explicit SvgWriter(int precision = 0) - { + { fill_rule_ = FillRule::NonZero; coords_style.font_name = "Verdana"; - coords_style.font_color = 0xFF000000; - coords_style.font_size = 11; + coords_style.font_color = 0xFF000000; + coords_style.font_size = 11; scale_ = std::pow(10, precision); }; ~SvgWriter() { Clear(); }; - + void Clear(); FillRule Fill_Rule() { return fill_rule_; } void SetCoordsStyle(const std::string &font_name, unsigned font_color, unsigned font_size); - void AddText(const std::string &text, unsigned font_color, unsigned font_size, int x, int y); + void AddText(const std::string &text, unsigned font_color, unsigned font_size, double x, double y); void AddPath(const Path64& path, bool is_open, FillRule fillrule, unsigned brush_color, unsigned pen_color, double pen_width, bool show_coords); void AddPath(const PathD& path, bool is_open, FillRule fillrule, unsigned brush_color, unsigned pen_color, double pen_width, bool show_coords); void AddPaths(const PathsD& paths, bool is_open, FillRule fillrule, unsigned brush_color, unsigned pen_color, double pen_width, bool show_coords); - void AddPaths(const Paths64& paths, bool is_open, FillRule fillrule, + void AddPaths(const Paths64& paths, bool is_open, FillRule fillrule, unsigned brush_color, unsigned pen_color, double pen_width, bool show_coords); bool SaveToFile(const std::string &filename, int max_width, int max_height, int margin); }; diff --git a/CPP/Utils/clipper.svg.utils.h b/CPP/Utils/clipper.svg.utils.h index 6d94ab47..dbfe5c55 100644 --- a/CPP/Utils/clipper.svg.utils.h +++ b/CPP/Utils/clipper.svg.utils.h @@ -22,8 +22,8 @@ namespace Clipper2Lib { static const unsigned subj_brush_clr = 0x1800009C; - static const unsigned subj_stroke_clr = 0xCCB3B3DA; - static const unsigned clip_brush_clr = 0x129C0000; + static const unsigned subj_stroke_clr = 0xFFB3B3DA; + static const unsigned clip_brush_clr = 0x129C0000; static const unsigned clip_stroke_clr = 0xCCFFA07A; static const unsigned solution_brush_clr = 0x4466FF66; @@ -52,7 +52,7 @@ namespace Clipper2Lib { svg.AddPaths(path, false, fillrule, 0x0, subj_stroke_clr, 0.8, false); PathsD tmp = Union(path, svg.Fill_Rule()); svg.AddPaths(tmp, false, fillrule, subj_brush_clr, subj_stroke_clr, 0.8, false); - } + } else svg.AddPaths(path, false, fillrule, subj_brush_clr, subj_stroke_clr, 0.8, false); } @@ -105,7 +105,7 @@ namespace Clipper2Lib { 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); + false, fillrule, solution_brush_clr, 0xFF003300, 1.0, show_coords); } @@ -116,7 +116,7 @@ namespace Clipper2Lib { } - inline void SvgAddOpenSolution(SvgWriter& svg, const Paths64& path, + inline void SvgAddOpenSolution(SvgWriter& svg, const Paths64& path, FillRule fillrule, bool show_coords, bool is_joined = false) { svg.AddPaths(TransformPaths(path), diff --git a/CSharp/Clipper2Lib/Clipper.Core.cs b/CSharp/Clipper2Lib/Clipper.Core.cs index d63d4aeb..55d0ae30 100644 --- a/CSharp/Clipper2Lib/Clipper.Core.cs +++ b/CSharp/Clipper2Lib/Clipper.Core.cs @@ -1,6 +1,6 @@ /******************************************************************************* * Author : Angus Johnson * -* Date : 14 February 2024 * +* Date : 24 March 2024 * * Website : http://www.angusj.com * * Copyright : Angus Johnson 2010-2024 * * Purpose : Core structures and functions for the Clipper Library * @@ -85,7 +85,7 @@ public Point64(PointD pt, double scale) return new Point64(lhs.X - rhs.X, lhs.Y - rhs.Y, lhs.Z - rhs.Z); } - public override string ToString() + public readonly override string ToString() { return $"{X},{Y},{Z} "; // nb: trailing space } @@ -216,7 +216,7 @@ public PointD(double x, double y, long z = 0) this.z = z; } - public string ToString(int precision = 2) + public readonly string ToString(int precision = 2) { return string.Format($"{{0:F{precision}}},{{1:F{precision}}},{{2:D}}", x,y,z); } diff --git a/CSharp/Clipper2Lib/Clipper.Offset.cs b/CSharp/Clipper2Lib/Clipper.Offset.cs index ecfef3be..81ad74fd 100644 --- a/CSharp/Clipper2Lib/Clipper.Offset.cs +++ b/CSharp/Clipper2Lib/Clipper.Offset.cs @@ -1,6 +1,6 @@ /******************************************************************************* * Author : Angus Johnson * -* Date : 14 March 2024 * +* Date : 24 March 2024 * * Website : http://www.angusj.com * * Copyright : Angus Johnson 2010-2024 * * Purpose : Path Offset (Inflate/Shrink) * @@ -69,21 +69,13 @@ public Group(Paths64 paths, JoinType joinType, EndType endType = EndType.Polygon } private static readonly double Tolerance = 1.0E-12; - private static readonly Rect64 InvalidRect64 = - new Rect64(long.MaxValue, long.MaxValue, long.MinValue, long.MinValue); - private static readonly RectD InvalidRectD = - new RectD(double.MaxValue, double.MaxValue, double.MinValue, double.MinValue); - private static readonly long MAX_COORD = long.MaxValue >> 2; - private static readonly long MIN_COORD = -MAX_COORD; - - private static readonly string - coord_range_error = "Error: Coordinate range."; - private readonly List _groupList = new List(); private Path64 pathOut = new Path64(); private readonly PathD _normals = new PathD(); - private readonly Paths64 _solution = new Paths64(); + private Paths64 _solution = new Paths64(); + private PolyTree64? _solutionTree = null; + private double _groupDelta; //*0.5 for open paths; *-1.0 for negative areas private double _delta; private double _mitLimSqr; @@ -103,6 +95,15 @@ public delegate double DeltaCallback64(Path64 path, public ClipperOffset.DeltaCallback64? DeltaCallback { get; set; } #if USINGZ + internal void ZCB(Point64 bot1, Point64 top1, + Point64 bot2, Point64 top2, ref Point64 ip) + { + if (bot1.Z != 0 && + ((bot1.Z == bot2.Z) || (bot1.Z == top2.Z))) ip.Z = bot1.Z; + else if (bot2.Z != 0 && bot2.Z == top1.Z) ip.Z = bot2.Z; + else if (top1.Z != 0 && top1.Z == top2.Z) ip.Z = top1.Z; + else ZCallback?.Invoke(bot1, top1, bot2, top2, ref ip); + } public ClipperBase.ZCallback64? ZCallback { get; set; } #endif public ClipperOffset(double miterLimit = 2.0, @@ -146,9 +147,20 @@ private int CalcSolutionCapacity() return result; } + internal bool CheckPathsReversed() + { + bool result = false; + foreach (Group g in _groupList) + if (g.endType == EndType.Polygon) + { + result = g.pathsReversed; + break; + } + return result; + } + private void ExecuteInternal(double delta) { - _solution.Clear(); if (_groupList.Count == 0) return; _solution.EnsureCapacity(CalcSolutionCapacity()); @@ -167,24 +179,7 @@ private void ExecuteInternal(double delta) foreach (Group group in _groupList) DoGroupOffset(group); - } - - internal bool CheckPathsReversed() - { - bool result = false; - foreach (Group g in _groupList) - if (g.endType == EndType.Polygon) - { - result = g.pathsReversed; - break; - } - return result; - } - public void Execute(double delta, Paths64 solution) - { - solution.Clear(); - ExecuteInternal(delta); if (_groupList.Count == 0) return; bool pathsReversed = CheckPathsReversed(); @@ -196,30 +191,29 @@ public void Execute(double delta, Paths64 solution) // the solution should retain the orientation of the input c.ReverseSolution = ReverseSolution != pathsReversed; #if USINGZ - c.ZCallback = ZCallback; + c.ZCallback = ZCB; #endif c.AddSubject(_solution); - c.Execute(ClipType.Union, fillRule, solution); + if (_solutionTree != null) + c.Execute(ClipType.Union, fillRule, _solutionTree); + else + c.Execute(ClipType.Union, fillRule, _solution); + + } + + public void Execute(double delta, Paths64 solution) + { + solution.Clear(); + _solution = solution; + ExecuteInternal(delta); } public void Execute(double delta, PolyTree64 solutionTree) { solutionTree.Clear(); + _solutionTree = solutionTree; + _solution.Clear(); ExecuteInternal(delta); - if (_groupList.Count == 0) return; - - bool pathsReversed = CheckPathsReversed(); - FillRule fillRule = pathsReversed ? FillRule.Negative : FillRule.Positive; - // clean up self-intersections ... - Clipper64 c = new Clipper64(); - c.PreserveCollinear = PreserveCollinear; - // the solution should normally retain the orientation of the input - c.ReverseSolution = ReverseSolution != pathsReversed; -#if USINGZ - c.ZCallback = ZCallback; -#endif - c.AddSubject(_solution); - c.Execute(ClipType.Union, fillRule, solutionTree); } @@ -246,7 +240,7 @@ public void Execute(DeltaCallback64 deltaCallback, Paths64 solution) internal static int GetLowestPathIdx(Paths64 paths) { int result = -1; - Point64 botPt = new Point64(Int64.MaxValue, Int64.MinValue); + Point64 botPt = new Point64(long.MaxValue, long.MinValue); for (int i = 0; i < paths.Count; ++i) { foreach (Point64 pt in paths[i]) diff --git a/CSharp/Clipper2Lib/Clipper.cs b/CSharp/Clipper2Lib/Clipper.cs index 6b63f05d..912ab461 100644 --- a/CSharp/Clipper2Lib/Clipper.cs +++ b/CSharp/Clipper2Lib/Clipper.cs @@ -633,6 +633,26 @@ public static PathD MakePath(double[] arr) return p; } +#if USINGZ + public static Path64 MakePathZ(long[] arr) + { + int len = arr.Length / 3; + Path64 p = new Path64(len); + for (int i = 0; i < len; i++) + p.Add(new Point64(arr[i * 3], arr[i * 3 + 1], arr[i * 3 + 2])); + return p; + } + public static PathD MakePathZ(double[] arr) + { + int len = arr.Length / 3; + PathD p = new PathD(len); + for (int i = 0; i < len; i++) + p.Add(new PointD(arr[i * 3], arr[i * 3 + 1], (long)arr[i * 3 + 2])); + return p; + } +#endif + + [MethodImpl(MethodImplOptions.AggressiveInlining)] public static double Sqr(double value) { diff --git a/CSharp/USINGZ.TestApp/Program.cs b/CSharp/USINGZ.TestApp/Program.cs index 4d290bd7..a86af66e 100644 --- a/CSharp/USINGZ.TestApp/Program.cs +++ b/CSharp/USINGZ.TestApp/Program.cs @@ -1,8 +1,8 @@ /******************************************************************************* * Author : Angus Johnson * -* Date : 24 January 2023 * +* Date : 24 March 2024 * * Website : http://www.angusj.com * -* Copyright : Angus Johnson 2010-2023 * +* Copyright : Angus Johnson 2010-2024 * * License : http://www.boost.org/LICENSE_1_0.txt * *******************************************************************************/ @@ -17,63 +17,73 @@ using Clipper2Lib; -public class Application +namespace UsingZTestApp { - public class MyCallbacks + public class Application { - public void MyCallback64(Point64 bot1, Point64 top1, - Point64 bot2, Point64 top2, ref Point64 intersectPt) + public class MyCallbacks { - intersectPt.Z = 1; - } + public int cnt = 0; + public void MyCallback64(Point64 _, Point64 __, + Point64 ___, Point64 ____, ref Point64 intersectPt) + { + cnt--; + intersectPt.Z = cnt; + } - public void MyCallbackD(PointD bot1, PointD top1, - PointD bot2, PointD top2, ref PointD intersectPt) - { - intersectPt.z = 1; + public void MyCallbackD(PointD _, PointD __, + PointD ___, PointD ____, ref PointD intersectPt) + { + cnt--; + intersectPt.z = cnt; + } } - } - public static void Main() - { - PathsD solution = new PathsD(); - PathsD subject = new PathsD(); - subject.Add(Clipper.MakePath(new double[] { 100, 50, 10, 79, 65, 2, 65, 98, 10, 21 })); + public static void Main() + { + PathsD solution = new(); + PathsD subject = new() + { + Clipper.MakePathZ(new double[] { 100,50,1, 10,79,2, 65,2,3, 65,98,4, 10,21,5 }) + }; - ClipperD clipperD = new ClipperD(); - MyCallbacks cb = new MyCallbacks(); - clipperD.ZCallback = cb.MyCallbackD; - clipperD.AddSubject(subject); - clipperD.Execute(ClipType.Union, FillRule.NonZero, solution); + ClipperD clipperD = new (); + MyCallbacks cb = new (); + clipperD.ZCallback = cb.MyCallbackD; + clipperD.AddSubject(subject); + clipperD.Execute(ClipType.Union, FillRule.NonZero, solution); + solution = Clipper.InflatePaths(solution, -3, JoinType.Miter, EndType.Polygon); + Console.WriteLine(solution.ToString(0)); - Console.WriteLine(solution.ToString(0)); + SvgWriter svg = new (FillRule.NonZero); + SvgUtils.AddSubject(svg, subject); + SvgUtils.AddSolution(svg, solution, true); - SvgWriter svg= new SvgWriter(FillRule.NonZero); - SvgUtils.AddSubject(svg, subject); - SvgUtils.AddSolution(svg, solution, false); + PathsD ellipses = new (); + for (int i = 0; i < solution[0].Count; i++) + { + if (solution[0][i].z < 0) + ellipses.Add(Clipper.Ellipse( + new PointD(solution[0][i].x, solution[0][i].y), 4)); + svg.AddText(solution[0][i].z.ToString(), + solution[0][i].x, solution[0][i].y, 12, 0xFF000000); + } + svg.AddClosedPaths(ellipses, 0x20FF0000, 0xFFFF0000, 1); + svg.SaveToFile("usingz.svg", 300, 300); + OpenFileWithDefaultApp("usingz.svg"); + } - PathsD ellipses = new PathsD(); - for (int i = 0; i < solution[0].Count; i++) + public static void OpenFileWithDefaultApp(string filename) { - if (solution[0][i].z == 1) - ellipses.Add(Clipper.Ellipse( - new PointD(solution[0][i].x, solution[0][i].y), 4)); + string path = Path.GetFullPath(filename); + if (!File.Exists(path)) return; + Process p = new () + { + StartInfo = new ProcessStartInfo(path) { UseShellExecute = true } + }; + p.Start(); } - svg.AddClosedPaths(ellipses, 0x20FF0000, 0xFFFF0000, 1); - svg.SaveToFile("usingz.svg", 300, 300); - OpenFileWithDefaultApp("usingz.svg"); - } - public static void OpenFileWithDefaultApp(string filename) - { - string path = Path.GetFullPath(filename); - if (!File.Exists(path)) return; - Process p = new Process() - { - StartInfo = new ProcessStartInfo(path) { UseShellExecute = true } - }; - p.Start(); } - -} +} \ No newline at end of file diff --git a/CSharp/USINGZ.TestApp/UsingZTestApp.csproj b/CSharp/USINGZ.TestApp/UsingZTestApp.csproj index 11464c72..13e9be54 100644 --- a/CSharp/USINGZ.TestApp/UsingZTestApp.csproj +++ b/CSharp/USINGZ.TestApp/UsingZTestApp.csproj @@ -11,6 +11,10 @@ $(DefineConstants);USINGZ + + $(DefineConstants);USINGZ + + diff --git a/CSharp/Utils/SVG/Clipper.SVG.cs b/CSharp/Utils/SVG/Clipper.SVG.cs index 42ffdd5e..e3481da0 100644 --- a/CSharp/Utils/SVG/Clipper.SVG.cs +++ b/CSharp/Utils/SVG/Clipper.SVG.cs @@ -1,8 +1,8 @@ /******************************************************************************* * Author : Angus Johnson * -* Date : 16 September 2022 * +* Date : 24 March 2024 * * Website : http://www.angusj.com * -* Copyright : Angus Johnson 2010-2022 * +* Copyright : Angus Johnson 2010-2024 * * License : http://www.boost.org/LICENSE_1_0.txt * *******************************************************************************/ @@ -56,9 +56,9 @@ private readonly struct TextInfo public readonly string text; public readonly int fontSize; public readonly uint fontColor; - public readonly int posX; - public readonly int posY; - public TextInfo(string text, int x, int y, + public readonly double posX; + public readonly double posY; + public TextInfo(string text, double x, double y, int fontsize = 12, uint fontcolor = black) { this.text = text; @@ -190,7 +190,7 @@ public void AddOpenPaths(PathsD paths, uint penColor, } - public void AddText(string cap, int posX, int posY, int fontSize, uint fontClr = black) + public void AddText(string cap, double posX, double posY, int fontSize, uint fontClr = black) { textInfos.Add(new TextInfo(cap, posX, posY, fontSize, fontClr)); } @@ -283,17 +283,18 @@ public bool SaveToFile(string filename, int maxWidth = 0, int maxHeight = 0, int if (pi.ShowCoords) { - writer.Write("\n", coordStyle.FontName, coordStyle.FontSize, ColorToHtml(coordStyle.FontColor)); + writer.Write("\n", + coordStyle.FontName, coordStyle.FontSize, ColorToHtml(coordStyle.FontColor)); foreach (PathD path in pi.paths) { foreach (PointD pt in path) { #if USINGZ - writer.Write(string.Format( - "{2},{3},{4}\n", - (int)(pt.x * scale + offsetX), (int)(pt.y * scale + offsetY), pt.x, pt.y, pt.z)); + writer.Write("{2:f2},{3:f2},{4}\n", + (pt.x * scale + offsetX), (pt.y * scale + offsetY), pt.x, pt.y, pt.z); #else - writer.Write("{2},{3}\n", (pt.x * scale + offsetX), (pt.y * scale + offsetY), pt.x, pt.y); + writer.Write("{2:f2},{3:f2}\n", + (pt.x * scale + offsetX), (pt.y * scale + offsetY), pt.x, pt.y); #endif } } @@ -304,8 +305,10 @@ public bool SaveToFile(string filename, int maxWidth = 0, int maxHeight = 0, int foreach (TextInfo captionInfo in textInfos) { writer.Write("\n", captionInfo.fontSize, ColorToHtml(captionInfo.fontColor)); - writer.Write("{2}\n\n", captionInfo.posX * scale + offsetX, captionInfo.posY * scale + offsetX, captionInfo.text); + "font-weight=\"normal\" font-size=\"{0}\" fill=\"{1}\">\n", + captionInfo.fontSize, ColorToHtml(captionInfo.fontColor)); + writer.Write("{2}\n\n", + captionInfo.posX * scale + offsetX, captionInfo.posY * scale + offsetY, captionInfo.text); } writer.Write("\n"); diff --git a/Delphi/Clipper2Lib/Clipper.Core.pas b/Delphi/Clipper2Lib/Clipper.Core.pas index 0a765711..0e58de5d 100644 --- a/Delphi/Clipper2Lib/Clipper.Core.pas +++ b/Delphi/Clipper2Lib/Clipper.Core.pas @@ -2,7 +2,7 @@ (******************************************************************************* * Author : Angus Johnson * -* Date : 14 February 2024 * +* Date : 24 March 2024 * * Website : http://www.angusj.com * * Copyright : Angus Johnson 2010-2024 * * Purpose : Core Clipper Library module * @@ -829,6 +829,9 @@ function ScalePath(const path: TPath64; sx, sy: double): TPath64; begin result[i].X := Round(path[i].X * sx); result[i].Y := Round(path[i].Y * sy); +{$IFDEF USINGZ} + result[i].Z := path[i].Z; +{$ENDIF} end; end; //------------------------------------------------------------------------------ @@ -845,10 +848,16 @@ function ScalePath(const path: TPathD; sx, sy: double): TPath64; j := 1; result[0].X := Round(path[0].X * sx); result[0].Y := Round(path[0].Y * sy); +{$IFDEF USINGZ} + result[0].Z := path[0].Z; +{$ENDIF} for i := 1 to len -1 do begin result[j].X := Round(path[i].X * sx); result[j].Y := Round(path[i].Y * sy); +{$IFDEF USINGZ} + result[j].Z := path[i].Z; +{$ENDIF} if (result[j].X <> result[j-1].X) or (result[j].Y <> result[j-1].Y) then inc(j); end; @@ -866,10 +875,16 @@ function ScalePath(const path: TPath64; scale: double): TPath64; j := 1; result[0].X := Round(path[0].X * scale); result[0].Y := Round(path[0].Y * scale); +{$IFDEF USINGZ} + result[0].Z := path[0].Z; +{$ENDIF} for i := 1 to len -1 do begin result[j].X := Round(path[i].X * scale); result[j].Y := Round(path[i].Y * scale); +{$IFDEF USINGZ} + result[j].Z := path[i].Z; +{$ENDIF} if (result[j].X <> result[j-1].X) or (result[j].Y <> result[j-1].Y) then inc(j); end; @@ -887,6 +902,9 @@ function ScalePath(const path: TPathD; scale: double): TPath64; begin result[i].X := Round(path[i].X * scale); result[i].Y := Round(path[i].Y * scale); +{$IFDEF USINGZ} + result[i].Z := path[i].Z; +{$ENDIF} end; end; //------------------------------------------------------------------------------ @@ -926,6 +944,9 @@ function ScalePathD(const path: TPath64; sx, sy: double): TPathD; begin result[i].X := path[i].X * sx; result[i].Y := path[i].Y * sy; +{$IFDEF USINGZ} + result[i].Z := path[i].Z; +{$ENDIF} end; end; //------------------------------------------------------------------------------ @@ -939,6 +960,9 @@ function ScalePathD(const path: TPathD; sx, sy: double): TPathD; begin result[i].X := path[i].X * sx; result[i].Y := path[i].Y * sy; +{$IFDEF USINGZ} + result[i].Z := path[i].Z; +{$ENDIF} end; end; //------------------------------------------------------------------------------ @@ -989,6 +1013,9 @@ function ScalePathsD(const paths: TPaths64; sx, sy: double): TPathsD; begin result[i][j].X := (paths[i][j].X * sx); result[i][j].Y := (paths[i][j].Y * sy); +{$IFDEF USINGZ} + result[i][j].Z := paths[i][j].Z; +{$ENDIF} end; end; end; @@ -1008,6 +1035,9 @@ function ScalePathsD(const paths: TPathsD; sx, sy: double): TPathsD; begin result[i][j].X := paths[i][j].X * sx; result[i][j].Y := paths[i][j].Y * sy; +{$IFDEF USINGZ} + result[i][j].Z := paths[i][j].Z; +{$ENDIF} end; end; end; @@ -1103,6 +1133,9 @@ function Path64(const pathD: TPathD): TPath64; begin Result[i].X := Round(pathD[i].X); Result[i].Y := Round(pathD[i].Y); +{$IFDEF USINGZ} + Result[i].Z := pathD[i].Z; +{$ENDIF} end; end; //------------------------------------------------------------------------------ @@ -1117,6 +1150,9 @@ function PathD(const path: TPath64): TPathD; begin Result[i].X := path[i].X; Result[i].Y := path[i].Y; +{$IFDEF USINGZ} + Result[i].Z := path[i].Z; +{$ENDIF} end; end; //------------------------------------------------------------------------------ @@ -2011,6 +2047,9 @@ function GetSegmentIntersectPt(const ln1a, ln1b, ln2a, ln2b: TPoint64; else if t >= 1.0 then ip := ln1b; ip.X := Trunc(ln1a.X + t * dx1); ip.Y := Trunc(ln1a.Y + t * dy1); +{$IFDEF USINGZ} + ip.Z := 0; +{$ENDIF} end; //------------------------------------------------------------------------------ diff --git a/Delphi/Clipper2Lib/Clipper.Offset.pas b/Delphi/Clipper2Lib/Clipper.Offset.pas index 9f732cdf..213bf658 100644 --- a/Delphi/Clipper2Lib/Clipper.Offset.pas +++ b/Delphi/Clipper2Lib/Clipper.Offset.pas @@ -2,7 +2,7 @@ (******************************************************************************* * Author : Angus Johnson * -* Date : 17 March 2024 * +* Date : 24 March 2024 * * Website : http://www.angusj.com * * Copyright : Angus Johnson 2010-2024 * * Purpose : Path Offset (Inflate/Shrink) * @@ -70,12 +70,18 @@ TClipperOffset = class fDeltaCallback64 : TDeltaCallback64; {$IFDEF USINGZ} fZCallback64 : TZCallback64; + procedure ZCB(const bot1, top1, bot2, top2: TPoint64; + var intersectPt: TPoint64); procedure AddPoint(x,y: double; z: Int64); overload; + procedure AddPoint(const pt: TPoint64); overload; + {$IFDEF INLINING} inline; {$ENDIF} + procedure AddPoint(const pt: TPoint64; newZ: Int64); overload; + {$IFDEF INLINING} inline; {$ENDIF} {$ELSE} procedure AddPoint(x,y: double); overload; -{$ENDIF} procedure AddPoint(const pt: TPoint64); overload; {$IFDEF INLINING} inline; {$ENDIF} +{$ENDIF} procedure DoSquare(j, k: Integer); procedure DoBevel(j, k: Integer); procedure DoMiter(j, k: Integer; cosA: Double); @@ -619,6 +625,10 @@ procedure TClipperOffset.ExecuteInternal(delta: Double); PreserveCollinear := fPreserveCollinear; // the solution should retain the orientation of the input ReverseSolution := fReverseSolution <> pathsReversed; +{$IFDEF USINGZ} + ZCallback := ZCB; +{$ENDIF} + AddSubject(fSolution); if assigned(fSolutionTree) then Execute(ctUnion, fillRule, fSolutionTree, dummy); @@ -672,6 +682,20 @@ procedure TClipperOffset.Execute(delta: Double; polytree: TPolyTree64); end; //------------------------------------------------------------------------------ +{$IFDEF USINGZ} +procedure TClipperOffset.ZCB(const bot1, top1, bot2, top2: TPoint64; + var intersectPt: TPoint64); +begin + if (bot1.Z <> 0) and + ((bot1.Z = bot2.Z) or (bot1.Z = top2.Z)) then intersectPt.Z := bot1.Z + else if (bot2.Z <> 0) and (bot2.Z = top1.Z) then intersectPt.Z := bot2.Z + else if (top1.Z <> 0) and (top1.Z = top2.Z) then intersectPt.Z := top1.Z + else if Assigned(ZCallback) then + ZCallback(bot1, top1, bot2, top2, intersectPt); +end; +{$ENDIF} +//------------------------------------------------------------------------------ + {$IFDEF USINGZ} procedure TClipperOffset.AddPoint(x,y: double; z: Int64); {$ELSE} @@ -696,15 +720,26 @@ procedure TClipperOffset.AddPoint(x,y: double); end; //------------------------------------------------------------------------------ +{$IFDEF USINGZ} +procedure TClipperOffset.AddPoint(const pt: TPoint64; newZ: Int64); +begin + AddPoint(pt.X, pt.Y, newZ); +end; +//------------------------------------------------------------------------------ + procedure TClipperOffset.AddPoint(const pt: TPoint64); begin -{$IFDEF USINGZ} AddPoint(pt.X, pt.Y, pt.Z); +end; +//------------------------------------------------------------------------------ + {$ELSE} +procedure TClipperOffset.AddPoint(const pt: TPoint64); +begin AddPoint(pt.X, pt.Y); -{$ENDIF} end; //------------------------------------------------------------------------------ +{$ENDIF} function IntersectPoint(const ln1a, ln1b, ln2a, ln2b: TPointD): TPointD; var @@ -956,11 +991,19 @@ procedure TClipperOffset.DoRound(j, k: Integer; angle: double); if (cosA > -0.999) and (sinA * fGroupDelta < 0) then begin // is concave +{$IFDEF USINGZ} + AddPoint(GetPerpendic(fInPath[j], fNorms[k], fGroupDelta), fInPath[j].Z); +{$ELSE} AddPoint(GetPerpendic(fInPath[j], fNorms[k], fGroupDelta)); +{$ENDIF} // this extra point is the only (simple) way to ensure that // path reversals are fully cleaned with the trailing clipper AddPoint(fInPath[j]); // (#405) +{$IFDEF USINGZ} + AddPoint(GetPerpendic(fInPath[j], fNorms[j], fGroupDelta), fInPath[j].Z); +{$ELSE} AddPoint(GetPerpendic(fInPath[j], fNorms[j], fGroupDelta)); +{$ENDIF} end else if (cosA > 0.999) and (fJoinType <> jtRound) then begin From 3c3c2b0fca66ae7dc5d43a7583a810b9432cf4e3 Mon Sep 17 00:00:00 2001 From: angusj Date: Mon, 25 Mar 2024 08:52:12 +1000 Subject: [PATCH 33/64] Clipper.Offset: Fixed compile issue in previous revision. --- CPP/Clipper2Lib/src/clipper.offset.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CPP/Clipper2Lib/src/clipper.offset.cpp b/CPP/Clipper2Lib/src/clipper.offset.cpp index da6daed9..cd0afdca 100644 --- a/CPP/Clipper2Lib/src/clipper.offset.cpp +++ b/CPP/Clipper2Lib/src/clipper.offset.cpp @@ -525,8 +525,8 @@ void ClipperOffset::ZCB(const Point64& bot1, const Point64& top1, const Point64& bot2, const Point64& top2, Point64& ip) { if (bot1.z && (bot1.z == bot2.z) || (bot1.z == top2.z)) ip.z = bot1.z; - else if (bot2.z && (bot2.z == top1.z)) ip.z = bot2.z; - else if (top1.z && (top1.z == top2.z)) ip.z = top1.z; + else if ((bot2.z != 0) && (bot2.z == top1.z)) ip.z = bot2.z; + else if ((top1.z != 0) && (top1.z == top2.z)) ip.z = top1.z; else if (zCallback64_) zCallback64_(bot1, top1, bot2, top2, ip); } #endif From e0a8f10e2fd1519f62a3552de7d5daddef145d5d Mon Sep 17 00:00:00 2001 From: angusj Date: Mon, 25 Mar 2024 08:56:20 +1000 Subject: [PATCH 34/64] Clipper.Offset: Fixed another compile issue in previous revision --- CPP/Clipper2Lib/src/clipper.offset.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CPP/Clipper2Lib/src/clipper.offset.cpp b/CPP/Clipper2Lib/src/clipper.offset.cpp index cd0afdca..4b4a2218 100644 --- a/CPP/Clipper2Lib/src/clipper.offset.cpp +++ b/CPP/Clipper2Lib/src/clipper.offset.cpp @@ -524,7 +524,7 @@ void ClipperOffset::DoGroupOffset(Group& group) void ClipperOffset::ZCB(const Point64& bot1, const Point64& top1, const Point64& bot2, const Point64& top2, Point64& ip) { - if (bot1.z && (bot1.z == bot2.z) || (bot1.z == top2.z)) ip.z = bot1.z; + if ((bot1.z != 0) && (bot1.z == bot2.z) || (bot1.z == top2.z)) ip.z = bot1.z; else if ((bot2.z != 0) && (bot2.z == top1.z)) ip.z = bot2.z; else if ((top1.z != 0) && (top1.z == top2.z)) ip.z = top1.z; else if (zCallback64_) zCallback64_(bot1, top1, bot2, top2, ip); From 3bc5e49a11d6ad0dd6fd4fd5ebe9aea3a1062e80 Mon Sep 17 00:00:00 2001 From: angusj Date: Mon, 25 Mar 2024 08:59:09 +1000 Subject: [PATCH 35/64] Clipper.Offset: Fixed another compile issue in previous revision --- CPP/Clipper2Lib/src/clipper.offset.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/CPP/Clipper2Lib/src/clipper.offset.cpp b/CPP/Clipper2Lib/src/clipper.offset.cpp index 4b4a2218..729497c2 100644 --- a/CPP/Clipper2Lib/src/clipper.offset.cpp +++ b/CPP/Clipper2Lib/src/clipper.offset.cpp @@ -524,9 +524,9 @@ void ClipperOffset::DoGroupOffset(Group& group) void ClipperOffset::ZCB(const Point64& bot1, const Point64& top1, const Point64& bot2, const Point64& top2, Point64& ip) { - if ((bot1.z != 0) && (bot1.z == bot2.z) || (bot1.z == top2.z)) ip.z = bot1.z; - else if ((bot2.z != 0) && (bot2.z == top1.z)) ip.z = bot2.z; - else if ((top1.z != 0) && (top1.z == top2.z)) ip.z = top1.z; + if (bot1.z && ((bot1.z == bot2.z) || (bot1.z == top2.z))) ip.z = bot1.z; + else if (bot2.z && (bot2.z == top1.z)) ip.z = bot2.z; + else if (top1.z && (top1.z == top2.z)) ip.z = top1.z; else if (zCallback64_) zCallback64_(bot1, top1, bot2, top2, ip); } #endif From 7daac7db4d40b98fdec0c03eb9f77b14a9837987 Mon Sep 17 00:00:00 2001 From: angusj Date: Mon, 25 Mar 2024 11:20:16 +1000 Subject: [PATCH 36/64] Clipper.Engine: fixed collinearity test error in macOS (#777) --- CPP/Clipper2Lib/src/clipper.engine.cpp | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/CPP/Clipper2Lib/src/clipper.engine.cpp b/CPP/Clipper2Lib/src/clipper.engine.cpp index c604faf4..06813eb6 100644 --- a/CPP/Clipper2Lib/src/clipper.engine.cpp +++ b/CPP/Clipper2Lib/src/clipper.engine.cpp @@ -1,6 +1,6 @@ /******************************************************************************* * Author : Angus Johnson * -* Date : 24 March 2024 * +* Date : 25 March 2024 * * Website : http://www.angusj.com * * Copyright : Angus Johnson 2010-2024 * * Purpose : This is the main polygon clipping module * @@ -1118,6 +1118,16 @@ namespace Clipper2Lib { } } + inline bool IsCollinear(const Point64& pt1, + const Point64& sharedPt, const Point64& pt2) // #777 + { +#ifdef __APPLE__ + double cp = CrossProduct(pt1, sharedPt, pt2); + return std::fabs(cp) < 0.00000001; +#else + return CrossProduct(pt1, sharedPt, pt2) == 0; +#endif + } bool IsValidAelOrder(const Active& resident, const Active& newcomer) { @@ -1150,8 +1160,8 @@ namespace Clipper2Lib { //resident must also have just been inserted else if (resident.is_left_bound != newcomerIsLeft) return newcomerIsLeft; - else if (CrossProduct(PrevPrevVertex(resident)->pt, - resident.bot, resident.top) == 0) return true; + else if (IsCollinear(PrevPrevVertex(resident)->pt, + resident.bot, resident.top)) return true; else //compare turning direction of the alternate bound return (CrossProduct(PrevPrevVertex(resident)->pt, @@ -1527,7 +1537,6 @@ namespace Clipper2Lib { return new_op; } - void ClipperBase::CleanCollinear(OutRec* outrec) { outrec = GetRealOutRec(outrec); @@ -1542,7 +1551,7 @@ namespace Clipper2Lib { for (; ; ) { //NB if preserveCollinear == true, then only remove 180 deg. spikes - if ((CrossProduct(op2->prev->pt, op2->pt, op2->next->pt) == 0) && + if (IsCollinear(op2->prev->pt, op2->pt, op2->next->pt) && (op2->pt == op2->prev->pt || op2->pt == op2->next->pt || !preserve_collinear_ || DotProduct(op2->prev->pt, op2->pt, op2->next->pt) < 0)) From 7c91a187ecc061a354a828e3cdcdd893d1fd1b98 Mon Sep 17 00:00:00 2001 From: angusj Date: Mon, 25 Mar 2024 11:37:08 +1000 Subject: [PATCH 37/64] Added Collinearity test // #777 --- CPP/Tests/TestPolygons.cpp | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/CPP/Tests/TestPolygons.cpp b/CPP/Tests/TestPolygons.cpp index c5a77735..e2a18c4b 100644 --- a/CPP/Tests/TestPolygons.cpp +++ b/CPP/Tests/TestPolygons.cpp @@ -105,4 +105,19 @@ TEST(Clipper2Tests, TestHorzSpikes) //#720 c.AddSubject(paths); c.Execute(Clipper2Lib::ClipType::Union, Clipper2Lib::FillRule::NonZero, paths); EXPECT_GE(paths.size(), 1); +} + +TEST(Clipper2Tests, TestCollinearOnMacOs) //#777 +{ + Clipper2Lib::Paths64 subject; + subject.push_back(Clipper2Lib::MakePath({ 0, -453054451,0, -433253797,-455550000, 0 })); + subject.push_back(Clipper2Lib::MakePath({ 0, -433253797,0, 0,-455550000, 0 })); + Clipper2Lib::Clipper64 clipper; + clipper.PreserveCollinear(false); + clipper.AddSubject(subject); + Clipper2Lib::Paths64 solution; + clipper.Execute(Clipper2Lib::ClipType::Union, Clipper2Lib::FillRule::NonZero, solution); + ASSERT_EQ(solution.size(), 1); + EXPECT_EQ(solution[0].size(), 3); + EXPECT_EQ(IsPositive(subject[0]), IsPositive(solution[0])); } \ No newline at end of file From ce4c338ba6a064e28198ed9f03442a1dc7cb638b Mon Sep 17 00:00:00 2001 From: philstopford Date: Tue, 2 Apr 2024 16:09:46 -0500 Subject: [PATCH 38/64] Minor tweaks (#801) * Floating point comparison * Clean-up The variables are initialized in the constructor, and this using line seems to be unused. * Update Clipper.RectClip.cs Variable initialized in constructor. * This looked to be unnecessary * group is not used, remove unused using statement. min_area also not used. _solutionTree initialized in constructor. * Null check corrections Assuming that the user can supply nulls as input. This addresses parts of https://github.com/AngusJohnson/Clipper2/issues/802 One remaining issue is that there is a null check against a value returned from PathFromStr(), but that method always returns a non-null value, so the check would appear to be meaningless. * Clean up null checks These seem to be redundant due to outer checks. * More redundant null checks Also remove one unused variable (result) * Remove redundant base constructors Remove redundant .ToString() * Readability change The long while statement made this confusing to understand. Use braces to make the end of the statement and the action more apparent. --- CSharp/Clipper2Lib/Clipper.Core.cs | 7 +------ CSharp/Clipper2Lib/Clipper.Engine.cs | 14 ++++++-------- CSharp/Clipper2Lib/Clipper.Offset.cs | 10 ++++------ CSharp/Clipper2Lib/Clipper.RectClip.cs | 8 ++++---- CSharp/Clipper2Lib/Clipper.cs | 10 ++++++---- CSharp/Utils/ClipFileIO/Clipper.FileIO.cs | 12 ++++++------ 6 files changed, 27 insertions(+), 34 deletions(-) diff --git a/CSharp/Clipper2Lib/Clipper.Core.cs b/CSharp/Clipper2Lib/Clipper.Core.cs index 55d0ae30..d0128ff0 100644 --- a/CSharp/Clipper2Lib/Clipper.Core.cs +++ b/CSharp/Clipper2Lib/Clipper.Core.cs @@ -11,7 +11,6 @@ using System; using System.Collections.Generic; using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; namespace Clipper2Lib { @@ -474,7 +473,6 @@ public readonly PathD AsPath() public class Path64 : List { - private Path64() : base() { } public Path64(int capacity = 0) : base(capacity) { } public Path64(IEnumerable path) : base(path) { } public override string ToString() @@ -488,21 +486,19 @@ public override string ToString() public class Paths64 : List { - private Paths64() : base() { } public Paths64(int capacity = 0) : base(capacity) { } public Paths64(IEnumerable paths) : base(paths) { } public override string ToString() { string s = ""; foreach (Path64 p in this) - s = s + p.ToString() + "\n"; + s = s + p + "\n"; return s; } } public class PathD : List { - private PathD() : base() { } public PathD(int capacity = 0) : base(capacity) { } public PathD(IEnumerable path) : base(path) { } public string ToString(int precision = 2) @@ -516,7 +512,6 @@ public string ToString(int precision = 2) public class PathsD : List { - private PathsD() : base() { } public PathsD(int capacity = 0) : base(capacity) { } public PathsD(IEnumerable paths) : base(paths) { } public string ToString(int precision = 2) diff --git a/CSharp/Clipper2Lib/Clipper.Engine.cs b/CSharp/Clipper2Lib/Clipper.Engine.cs index d967a77f..d34b9332 100644 --- a/CSharp/Clipper2Lib/Clipper.Engine.cs +++ b/CSharp/Clipper2Lib/Clipper.Engine.cs @@ -13,7 +13,6 @@ using System; using System.Collections; using System.Collections.Generic; -using System.Reflection.Emit; using System.Runtime.CompilerServices; namespace Clipper2Lib @@ -147,10 +146,10 @@ internal class OutRec public Active? backEdge; public OutPt? pts; public PolyPathBase? polypath; - public Rect64 bounds = new Rect64(); + public Rect64 bounds; public Path64 path = new Path64(); public bool isOpen; - public List? splits = null; + public List? splits; public OutRec? recursiveSplit; }; @@ -2158,7 +2157,7 @@ private void DoHorizontal(Active horz) if (ae.vertexTop == vertex_max) { // do this first!! - if (IsHotEdge(horz) && IsJoined(ae!)) Split(ae, ae.top); + if (IsHotEdge(horz) && IsJoined(ae)) Split(ae, ae.top); if (IsHotEdge(horz)) { @@ -2444,7 +2443,7 @@ private static void FixOutRecPts(OutRec outrec) OutPt op = outrec.pts!; do { - op!.outrec = outrec; + op.outrec = outrec; op = op.next!; } while (op != outrec.pts); } @@ -2715,7 +2714,7 @@ private void ProcessHorzJoins() OutRec or2 = GetRealOutRec(j.op2!.outrec)!; OutPt op1b = j.op1.next!; - OutPt op2b = j.op2.prev!; + OutPt op2b = j.op2.prev; j.op1.next = j.op2; j.op2.prev = j.op1; op1b.prev = op2b; @@ -2851,7 +2850,6 @@ private void DoSplitOp(OutRec outrec, OutPt splitOp) OutPt prevOp = splitOp.prev; OutPt nextNextOp = splitOp.next!.next!; outrec.pts = prevOp; - OutPt result = prevOp; InternalClipper.GetSegmentIntersectPt( prevOp.pt, splitOp.pt, splitOp.next.pt, nextNextOp.pt, out Point64 ip); @@ -3050,7 +3048,7 @@ private bool CheckSplitOwner(OutRec outrec, List? splits) OutRec? split = GetRealOutRec(_outrecList[i]); if (split == null || split == outrec || split.recursiveSplit == outrec) continue; split.recursiveSplit = outrec; //#599 - if (split!.splits != null && CheckSplitOwner(outrec, split.splits)) return true; + if (split.splits != null && CheckSplitOwner(outrec, split.splits)) return true; if (IsValidOwner(outrec, split) && CheckBounds(split) && split.bounds.Contains(outrec.bounds) && diff --git a/CSharp/Clipper2Lib/Clipper.Offset.cs b/CSharp/Clipper2Lib/Clipper.Offset.cs index 81ad74fd..9d6d4090 100644 --- a/CSharp/Clipper2Lib/Clipper.Offset.cs +++ b/CSharp/Clipper2Lib/Clipper.Offset.cs @@ -9,7 +9,6 @@ using System; using System.Collections.Generic; -using System.IO; using System.Runtime.CompilerServices; namespace Clipper2Lib @@ -74,7 +73,7 @@ public Group(Paths64 paths, JoinType joinType, EndType endType = EndType.Polygon private Path64 pathOut = new Path64(); private readonly PathD _normals = new PathD(); private Paths64 _solution = new Paths64(); - private PolyTree64? _solutionTree = null; + private PolyTree64? _solutionTree; private double _groupDelta; //*0.5 for open paths; *-1.0 for negative areas private double _delta; @@ -454,7 +453,7 @@ private void DoSquare(Path64 path, int j, int k) } [MethodImpl(MethodImplOptions.AggressiveInlining)] - private void DoMiter(Group group, Path64 path, int j, int k, double cosA) + private void DoMiter(Path64 path, int j, int k, double cosA) { double q = _groupDelta / (cosA + 1); #if USINGZ @@ -558,12 +557,12 @@ private void OffsetPoint(Group group, Path64 path, int j, ref int k) else if ((cosA > 0.999) && (_joinType != JoinType.Round)) { // almost straight - less than 2.5 degree (#424, #482, #526 & #724) - DoMiter(group, path, j, k, cosA); + DoMiter(path, j, k, cosA); } else if (_joinType == JoinType.Miter) { // miter unless the angle is sufficiently acute to exceed ML - if (cosA > _mitLimSqr - 1) DoMiter(group, path, j, k, cosA); + if (cosA > _mitLimSqr - 1) DoMiter(path, j, k, cosA); else DoSquare(path, j, k); } else if (_joinType == JoinType.Round) @@ -689,7 +688,6 @@ private void DoGroupOffset(Group group) _stepsPerRad = stepsPer360 / (2 * Math.PI); } - double min_area = Math.PI * Clipper.Sqr(_groupDelta); using List.Enumerator pathIt = group.inPaths.GetEnumerator(); while (pathIt.MoveNext()) { diff --git a/CSharp/Clipper2Lib/Clipper.RectClip.cs b/CSharp/Clipper2Lib/Clipper.RectClip.cs index 70b34098..716ab079 100644 --- a/CSharp/Clipper2Lib/Clipper.RectClip.cs +++ b/CSharp/Clipper2Lib/Clipper.RectClip.cs @@ -41,7 +41,7 @@ protected enum Location protected Rect64 pathBounds_; protected List results_; protected List[] edges_; - protected int currIdx_ = -1; + protected int currIdx_; internal RectClip64(Rect64 rect) { currIdx_ = -1; @@ -195,7 +195,7 @@ private static bool HasVertOverlap(Point64 top1, Point64 bottom1, private static void AddToEdge(List edge, OutPt2 op) { if (op.edge != null) return; - op.edge = edge!; + op.edge = edge; edge.Add(op); } @@ -931,7 +931,7 @@ private Path64 GetPath(OutPt2? op) while (op2 != null && op2 != op) { if (InternalClipper.CrossProduct( - op2!.prev!.pt, op2.pt, op2!.next!.pt) == 0) + op2.prev!.pt, op2.pt, op2.next!.pt) == 0) { op = op2.prev; op2 = UnlinkOp(op2); @@ -995,7 +995,7 @@ private Path64 GetPath(OutPt2? op) OutPt2 op2 = op.next!; while (op2 != op) { - result.Add(op2!.pt); + result.Add(op2.pt); op2 = op2.next!; } return result; diff --git a/CSharp/Clipper2Lib/Clipper.cs b/CSharp/Clipper2Lib/Clipper.cs index 912ab461..152af315 100644 --- a/CSharp/Clipper2Lib/Clipper.cs +++ b/CSharp/Clipper2Lib/Clipper.cs @@ -589,7 +589,7 @@ public static RectD GetBounds(PathD path) if (pt.y < result.top) result.top = pt.y; if (pt.y > result.bottom) result.bottom = pt.y; } - return result.left == double.MaxValue ? new RectD() : result; + return Math.Abs(result.left - double.MaxValue) < InternalClipper.floatingPointTolerance ? new RectD() : result; } public static RectD GetBounds(PathsD paths) @@ -603,7 +603,7 @@ public static RectD GetBounds(PathsD paths) if (pt.y < result.top) result.top = pt.y; if (pt.y > result.bottom) result.bottom = pt.y; } - return result.left == double.MaxValue ? new RectD() : result; + return Math.Abs(result.left - double.MaxValue) < InternalClipper.floatingPointTolerance ? new RectD() : result; } public static Path64 MakePath(int[] arr) @@ -1054,8 +1054,10 @@ public static Path64 TrimCollinear(Path64 path, bool isOpen = false) else { while (result.Count > 2 && InternalClipper.CrossProduct( - result[result.Count - 1], result[result.Count - 2], result[0]) == 0) - result.RemoveAt(result.Count - 1); + result[result.Count - 1], result[result.Count - 2], result[0]) == 0) + { + result.RemoveAt(result.Count - 1); + } if (result.Count < 3) result.Clear(); } diff --git a/CSharp/Utils/ClipFileIO/Clipper.FileIO.cs b/CSharp/Utils/ClipFileIO/Clipper.FileIO.cs index 2efad9c8..6c49c7a8 100644 --- a/CSharp/Utils/ClipFileIO/Clipper.FileIO.cs +++ b/CSharp/Utils/ClipFileIO/Clipper.FileIO.cs @@ -15,7 +15,7 @@ namespace Clipper2Lib public static class ClipperFileIO { - public static Paths64 PathFromStr(string s) + public static Paths64 PathFromStr(string? s) { if (s == null) return new Paths64(); Path64 p = new Path64(); @@ -71,7 +71,7 @@ public static Paths64 PathFromStr(string s) //------------------------------------------------------------------------------ public static bool LoadTestNum(string filename, int num, - Paths64 subj, Paths64 subj_open, Paths64 clip, + Paths64? subj, Paths64? subj_open, Paths64? clip, out ClipType ct, out FillRule fillRule, out long area, out int count, out string caption) { if (subj == null) subj = new Paths64(); else subj.Clear(); @@ -96,7 +96,7 @@ public static bool LoadTestNum(string filename, int num, } while (true) { - string s = reader.ReadLine(); + string? s = reader.ReadLine(); if (s == null) break; if (s.IndexOf("CAPTION: ", StringComparison.Ordinal) == 0) @@ -150,7 +150,7 @@ public static bool LoadTestNum(string filename, int num, { s = reader.ReadLine(); if (s == null) break; - Paths64 paths = PathFromStr(s); //0 or 1 path + Paths64? paths = PathFromStr(s); //0 or 1 path if (paths == null || paths.Count == 0) { if (GetIdx == 3) return result; @@ -168,8 +168,8 @@ public static bool LoadTestNum(string filename, int num, } //----------------------------------------------------------------------- - public static void SaveClippingOp(string filename, Paths64 subj, - Paths64 subj_open, Paths64 clip, ClipType ct, FillRule fillRule, bool append) + public static void SaveClippingOp(string filename, Paths64? subj, + Paths64? subj_open, Paths64? clip, ClipType ct, FillRule fillRule, bool append) { StreamWriter writer; try From 0135062a1fe7570a1ebafee3ef688fae0793886d Mon Sep 17 00:00:00 2001 From: Manuel Massing Date: Sat, 6 Apr 2024 19:01:41 +0200 Subject: [PATCH 39/64] Generate and install a cmake configuration file, which can be (#447) * Generate and install a cmake configuration file, which can be automatically picked up by downstream cmake projects. Downstream projects can link to Clipper2 or Clipper2Z by adding 'Clipper2::Clipper2' or 'Clipper2::Clipper2Z' as target dependency, and don't need to implement a FindPackage module. * Fix package config for Clipper2Z * Be a little less lenient when version-checking the configuration file. --- CPP/CMakeLists.txt | 72 ++++++++++++++++++++++++++++++------- CPP/Clipper2Config.cmake.in | 6 ++++ 2 files changed, 66 insertions(+), 12 deletions(-) create mode 100644 CPP/Clipper2Config.cmake.in diff --git a/CPP/CMakeLists.txt b/CPP/CMakeLists.txt index e89ce45f..f64c5372 100644 --- a/CPP/CMakeLists.txt +++ b/CPP/CMakeLists.txt @@ -60,13 +60,14 @@ if (NOT (CLIPPER2_USINGZ STREQUAL "ONLY")) target_compile_definitions( Clipper2 PUBLIC - CLIPPER2_MAX_DECIMAL_PRECISION=${CLIPPER2_MAX_DECIMAL_PRECISION} - $<$:CLIPPER2_HI_PRECISION> + CLIPPER2_MAX_DECIMAL_PRECISION=${CLIPPER2_MAX_DECIMAL_PRECISION} + $<$:CLIPPER2_HI_PRECISION> ) - - target_include_directories(Clipper2 - PUBLIC Clipper2Lib/include + target_include_directories( + Clipper2 PUBLIC + $ + $ ) if (MSVC) @@ -84,13 +85,14 @@ if (NOT (CLIPPER2_USINGZ STREQUAL "OFF")) target_compile_definitions( Clipper2Z PUBLIC - USINGZ - CLIPPER2_MAX_DECIMAL_PRECISION=${CLIPPER2_MAX_DECIMAL_PRECISION} - $<$:CLIPPER2_HI_PRECISION> + USINGZ + CLIPPER2_MAX_DECIMAL_PRECISION=${CLIPPER2_MAX_DECIMAL_PRECISION} + $<$:CLIPPER2_HI_PRECISION> ) - - target_include_directories(Clipper2Z - PUBLIC Clipper2Lib/include + target_include_directories( + Clipper2Z PUBLIC + $ + $ ) if (MSVC) @@ -286,7 +288,12 @@ set(CLIPPER2_PCFILES "") foreach(lib ${CLIPPER2_LIBS}) set(pc "${CMAKE_CURRENT_BINARY_DIR}/${lib}.pc") list(APPEND CLIPPER2_PCFILES ${pc}) - CONFIGURE_FILE(Clipper2.pc.cmakein "${pc}" @ONLY) + if (lib STREQUAL "Clipper2Z") + set(PCFILE_LIB_SUFFIX "Z") + else() + set(PCFILE_LIB_SUFFIX "") + endif() + configure_file(Clipper2.pc.cmakein "${pc}" @ONLY) endforeach() install(TARGETS ${CLIPPER2_LIBS} @@ -294,6 +301,47 @@ install(TARGETS ${CLIPPER2_LIBS} ) install(FILES ${CLIPPER2_PCFILES} DESTINATION ${CMAKE_INSTALL_LIBDIR}/pkgconfig) + +# create package config file +include(CMakePackageConfigHelpers) +include(GNUInstallDirs) + +write_basic_package_version_file("${PROJECT_BINARY_DIR}/Clipper2ConfigVersion.cmake" COMPATIBILITY SameMajorVersion) + +configure_package_config_file( + "Clipper2Config.cmake.in" + "${PROJECT_BINARY_DIR}/Clipper2Config.cmake" + INSTALL_DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/clipper2 +) + +export(TARGETS ${CLIPPER2_LIBS} FILE "${PROJECT_BINARY_DIR}/Clipper2Targets.cmake") + +# installation +install(TARGETS ${CLIPPER2_LIBS} EXPORT Clipper2-targets + PUBLIC_HEADER DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/clipper2 + LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR} +) + +install( + FILES + ${PROJECT_BINARY_DIR}/Clipper2Config.cmake + ${PROJECT_BINARY_DIR}/Clipper2ConfigVersion.cmake + DESTINATION + ${CMAKE_INSTALL_LIBDIR}/cmake/clipper2 +) + +install( + EXPORT + Clipper2-targets + NAMESPACE + Clipper2:: + FILE + Clipper2Targets.cmake + DESTINATION + ${CMAKE_INSTALL_LIBDIR}/cmake/clipper2 +) + + # disable exceptions #string(REGEX REPLACE "/W[3|4]" "/w" CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS}") #add_definitions(-D_HAS_EXCEPTIONS=0) # for STL diff --git a/CPP/Clipper2Config.cmake.in b/CPP/Clipper2Config.cmake.in new file mode 100644 index 00000000..7fad4da1 --- /dev/null +++ b/CPP/Clipper2Config.cmake.in @@ -0,0 +1,6 @@ +@PACKAGE_INIT@ + +# Provide all our library targets to users. +include("${CMAKE_CURRENT_LIST_DIR}/Clipper2Targets.cmake") + +check_required_components(Clipper2) From 29b6bdd51b0c7ef84df43661233ead5a21d660e9 Mon Sep 17 00:00:00 2001 From: Juha Reunanen Date: Sat, 13 Apr 2024 14:54:45 +0300 Subject: [PATCH 40/64] Let CMake automatically clone the googletest repo, so that users do not need to do it separately (#813) * Do not explicitly clone googletest in the Github Actions script * Automatically clone googletest in the CMake script --- .github/workflows/actions_cpp.yml | 24 ------------------------ CPP/CMakeLists.txt | 5 ++++- 2 files changed, 4 insertions(+), 25 deletions(-) diff --git a/.github/workflows/actions_cpp.yml b/.github/workflows/actions_cpp.yml index 9d438975..4e93a4a4 100644 --- a/.github/workflows/actions_cpp.yml +++ b/.github/workflows/actions_cpp.yml @@ -10,10 +10,6 @@ jobs: - uses: actions/setup-node@v2 with: node-version: '16' - - name: Get GoogleTest - run: | - cd CPP/Tests - git clone https://github.com/google/googletest - name: Add MSBuild to PATH uses: microsoft/setup-msbuild@v1.0.2 - name: Build @@ -33,10 +29,6 @@ jobs: - uses: actions/setup-node@v2 with: node-version: '16' - - name: Get GoogleTest - run: | - cd CPP/Tests - git clone https://github.com/google/googletest - name: Build run: | mkdir CPP/build @@ -54,10 +46,6 @@ jobs: - uses: actions/setup-node@v2 with: node-version: '16' - - name: Get GoogleTest - run: | - cd CPP/Tests - git clone https://github.com/google/googletest - name: Install gcc 11 run: | sudo apt update @@ -80,10 +68,6 @@ jobs: - uses: actions/setup-node@v2 with: node-version: '16' - - name: Get GoogleTest - run: | - cd CPP/Tests - git clone https://github.com/google/googletest - name: Build run: | export CC=/usr/bin/clang @@ -103,10 +87,6 @@ jobs: - uses: actions/setup-node@v2 with: node-version: '16' - - name: Get GoogleTest - run: | - cd CPP/Tests - git clone https://github.com/google/googletest - name: Install clang 13 run: | wget https://apt.llvm.org/llvm.sh @@ -131,10 +111,6 @@ jobs: - uses: actions/setup-node@v2 with: node-version: '16' - - name: Get GoogleTest - run: | - cd CPP/Tests - git clone https://github.com/google/googletest - name: Build run: | mkdir CPP/build diff --git a/CPP/CMakeLists.txt b/CPP/CMakeLists.txt index f64c5372..5c8deb51 100644 --- a/CPP/CMakeLists.txt +++ b/CPP/CMakeLists.txt @@ -210,7 +210,10 @@ if(USE_EXTERNAL_GTEST) find_package(GTest REQUIRED) else() include(GoogleTest) - + execute_process( + COMMAND "git" "clone" "https://github.com/google/googletest" + WORKING_DIRECTORY ${PROJECT_SOURCE_DIR}/Tests + ) add_subdirectory("${PROJECT_SOURCE_DIR}/Tests/googletest/") set_target_properties(gtest gtest_main PROPERTIES FOLDER GTest) endif() From fdd312b2ec2abfc925d00e54bd406bd352204e61 Mon Sep 17 00:00:00 2001 From: Juha Reunanen Date: Sat, 13 Apr 2024 15:28:52 +0300 Subject: [PATCH 41/64] Clone googletest only if it does not exist (#814) --- CPP/CMakeLists.txt | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/CPP/CMakeLists.txt b/CPP/CMakeLists.txt index 5c8deb51..2ad05657 100644 --- a/CPP/CMakeLists.txt +++ b/CPP/CMakeLists.txt @@ -210,10 +210,12 @@ if(USE_EXTERNAL_GTEST) find_package(GTest REQUIRED) else() include(GoogleTest) - execute_process( - COMMAND "git" "clone" "https://github.com/google/googletest" - WORKING_DIRECTORY ${PROJECT_SOURCE_DIR}/Tests - ) + if(NOT EXISTS ${PROJECT_SOURCE_DIR}/Tests/googletest) + execute_process( + COMMAND "git" "clone" "https://github.com/google/googletest" + WORKING_DIRECTORY ${PROJECT_SOURCE_DIR}/Tests + ) + endif() add_subdirectory("${PROJECT_SOURCE_DIR}/Tests/googletest/") set_target_properties(gtest gtest_main PROPERTIES FOLDER GTest) endif() From 7714d940ca86eb7cc8cf5f9eda2b1486daf32cb1 Mon Sep 17 00:00:00 2001 From: angusj Date: Wed, 17 Apr 2024 22:16:02 +1000 Subject: [PATCH 42/64] Clipper.Engine - minor tweak to IntersectEdges function (#798) --- .../include/clipper2/clipper.engine.h | 6 +-- CPP/Clipper2Lib/src/clipper.engine.cpp | 37 +++++++------- CPP/Clipper2Lib/src/clipper.offset.cpp | 13 +++-- CSharp/Clipper2Lib/Clipper.Engine.cs | 36 ++++++------- CSharp/Clipper2Lib/Clipper.Offset.cs | 9 ++-- Delphi/Clipper2Lib/Clipper.Engine.pas | 50 ++++++++++--------- Delphi/Clipper2Lib/Clipper.Offset.pas | 10 ++-- 7 files changed, 86 insertions(+), 75 deletions(-) diff --git a/CPP/Clipper2Lib/include/clipper2/clipper.engine.h b/CPP/Clipper2Lib/include/clipper2/clipper.engine.h index 4ff62cb2..a4c8fd82 100644 --- a/CPP/Clipper2Lib/include/clipper2/clipper.engine.h +++ b/CPP/Clipper2Lib/include/clipper2/clipper.engine.h @@ -1,8 +1,8 @@ /******************************************************************************* * Author : Angus Johnson * -* Date : 13 December 2023 * +* Date : 17 April 2024 * * Website : http://www.angusj.com * -* Copyright : Angus Johnson 2010-2023 * +* Copyright : Angus Johnson 2010-2024 * * Purpose : This is the main polygon clipping module * * License : http://www.boost.org/LICENSE_1_0.txt * *******************************************************************************/ @@ -230,7 +230,7 @@ namespace Clipper2Lib { inline bool PopHorz(Active *&e); inline OutPt* StartOpenPath(Active &e, const Point64& pt); inline void UpdateEdgeIntoAEL(Active *e); - OutPt* IntersectEdges(Active &e1, Active &e2, const Point64& pt); + void IntersectEdges(Active &e1, Active &e2, const Point64& pt); inline void DeleteFromAEL(Active &e); inline void AdjustCurrXAndCopyToSEL(const int64_t top_y); void DoIntersections(const int64_t top_y); diff --git a/CPP/Clipper2Lib/src/clipper.engine.cpp b/CPP/Clipper2Lib/src/clipper.engine.cpp index 06813eb6..5a7e4fa2 100644 --- a/CPP/Clipper2Lib/src/clipper.engine.cpp +++ b/CPP/Clipper2Lib/src/clipper.engine.cpp @@ -1,6 +1,6 @@ /******************************************************************************* * Author : Angus Johnson * -* Date : 25 March 2024 * +* Date : 17 April 2024 * * Website : http://www.angusj.com * * Copyright : Angus Johnson 2010-2024 * * Purpose : This is the main polygon clipping module * @@ -1121,7 +1121,7 @@ namespace Clipper2Lib { inline bool IsCollinear(const Point64& pt1, const Point64& sharedPt, const Point64& pt2) // #777 { -#ifdef __APPLE__ +#ifdef __aarch64__ double cp = CrossProduct(pt1, sharedPt, pt2); return std::fabs(cp) < 0.00000001; #else @@ -1782,12 +1782,12 @@ namespace Clipper2Lib { } - OutPt* ClipperBase::IntersectEdges(Active& e1, Active& e2, const Point64& pt) + void ClipperBase::IntersectEdges(Active& e1, Active& e2, const Point64& pt) { //MANAGE OPEN PATH INTERSECTIONS SEPARATELY ... if (has_open_paths_ && (IsOpen(e1) || IsOpen(e2))) { - if (IsOpen(e1) && IsOpen(e2)) return nullptr; + if (IsOpen(e1) && IsOpen(e2)) return; Active* edge_o, * edge_c; if (IsOpen(e1)) { @@ -1801,22 +1801,27 @@ namespace Clipper2Lib { } if (IsJoined(*edge_c)) Split(*edge_c, pt); // needed for safety - if (abs(edge_c->wind_cnt) != 1) return nullptr; + if (abs(edge_c->wind_cnt) != 1) return; switch (cliptype_) { case ClipType::Union: - if (!IsHotEdge(*edge_c)) return nullptr; + if (!IsHotEdge(*edge_c)) return; break; default: if (edge_c->local_min->polytype == PathType::Subject) - return nullptr; + return; } switch (fillrule_) { - case FillRule::Positive: if (edge_c->wind_cnt != 1) return nullptr; break; - case FillRule::Negative: if (edge_c->wind_cnt != -1) return nullptr; break; - default: if (std::abs(edge_c->wind_cnt) != 1) return nullptr; break; + case FillRule::Positive: + if (edge_c->wind_cnt != 1) return; + break; + case FillRule::Negative: + if (edge_c->wind_cnt != -1) return; + break; + default: + if (std::abs(edge_c->wind_cnt) != 1) return; } OutPt* resultOp; @@ -1843,7 +1848,7 @@ namespace Clipper2Lib { SetSides(*e3->outrec, *edge_o, *e3); else SetSides(*e3->outrec, *e3, *edge_o); - return e3->outrec->pts; + return; } else resultOp = StartOpenPath(*edge_o, pt); @@ -1854,7 +1859,7 @@ namespace Clipper2Lib { #ifdef USINGZ if (zCallback_) SetZ(*edge_o, *edge_c, resultOp->pt); #endif - return resultOp; + return; } // end of an open path intersection //MANAGING CLOSED PATHS FROM HERE ON @@ -1923,10 +1928,9 @@ namespace Clipper2Lib { const bool e1_windcnt_in_01 = old_e1_windcnt == 0 || old_e1_windcnt == 1; const bool e2_windcnt_in_01 = old_e2_windcnt == 0 || old_e2_windcnt == 1; - if ((!IsHotEdge(e1) && !e1_windcnt_in_01) || (!IsHotEdge(e2) && !e2_windcnt_in_01)) - { - return nullptr; - } + if ((!IsHotEdge(e1) && !e1_windcnt_in_01) || + (!IsHotEdge(e2) && !e2_windcnt_in_01)) + return; //NOW PROCESS THE INTERSECTION ... OutPt* resultOp = nullptr; @@ -2048,7 +2052,6 @@ namespace Clipper2Lib { #endif } } - return resultOp; } inline void ClipperBase::DeleteFromAEL(Active& e) diff --git a/CPP/Clipper2Lib/src/clipper.offset.cpp b/CPP/Clipper2Lib/src/clipper.offset.cpp index 729497c2..e0959188 100644 --- a/CPP/Clipper2Lib/src/clipper.offset.cpp +++ b/CPP/Clipper2Lib/src/clipper.offset.cpp @@ -1,6 +1,6 @@ /******************************************************************************* * Author : Angus Johnson * -* Date : 24 March 2024 * +* Date : 17 April 2024 * * Website : http://www.angusj.com * * Copyright : Angus Johnson 2010-2024 * * Purpose : Path Offset (Inflate/Shrink) * @@ -315,15 +315,18 @@ void ClipperOffset::OffsetPoint(Group& group, const Path64& path, size_t j, size if (cos_a > -0.999 && (sin_a * group_delta_ < 0)) // test for concavity first (#593) { - // is concave + // is concave (so insert 3 points that will create a negative region) #ifdef USINGZ path_out.push_back(Point64(GetPerpendic(path[j], norms[k], group_delta_), path[j].z)); #else path_out.push_back(GetPerpendic(path[j], norms[k], group_delta_)); #endif - // this extra point is the only (simple) way to ensure that - // path reversals are fully cleaned with the trailing clipper - path_out.push_back(path[j]); // (#405) + + // this extra point is the only simple way to ensure that path reversals + // (ie over-shrunk paths) are fully cleaned out with the trailing union op. + // However it's probably safe to skip this whenever an angle is almost flat. + if (cos_a < 0.99) path_out.push_back(path[j]); // (#405) + #ifdef USINGZ path_out.push_back(Point64(GetPerpendic(path[j], norms[j], group_delta_), path[j].z)); #else diff --git a/CSharp/Clipper2Lib/Clipper.Engine.cs b/CSharp/Clipper2Lib/Clipper.Engine.cs index d34b9332..e6800b7e 100644 --- a/CSharp/Clipper2Lib/Clipper.Engine.cs +++ b/CSharp/Clipper2Lib/Clipper.Engine.cs @@ -1,6 +1,6 @@ /******************************************************************************* * Author : Angus Johnson * -* Date : 28 February 2024 * +* Date : 17 April 2024 * * Website : http://www.angusj.com * * Copyright : Angus Johnson 2010-2024 * * Purpose : This is the main polygon clipping module * @@ -1548,33 +1548,34 @@ private void UpdateEdgeIntoAEL(Active ae) return result; } - private OutPt? IntersectEdges(Active ae1, Active ae2, Point64 pt) + private void IntersectEdges(Active ae1, Active ae2, Point64 pt) { OutPt? resultOp = null; - // MANAGE OPEN PATH INTERSECTIONS SEPARATELY ... if (_hasOpenPaths && (IsOpen(ae1) || IsOpen(ae2))) { - if (IsOpen(ae1) && IsOpen(ae2)) return null; + if (IsOpen(ae1) && IsOpen(ae2)) return; // the following line avoids duplicating quite a bit of code if (IsOpen(ae2)) SwapActives(ref ae1, ref ae2); if (IsJoined(ae2)) Split(ae2, pt); // needed for safety if (_cliptype == ClipType.Union) { - if (!IsHotEdge(ae2)) return null; + if (!IsHotEdge(ae2)) return; } - else if (ae2.localMin.polytype == PathType.Subject) - return null; + else if (ae2.localMin.polytype == PathType.Subject) return; switch (_fillrule) { case FillRule.Positive: - if (ae2.windCount != 1) return null; break; + if (ae2.windCount != 1) return; + break; case FillRule.Negative: - if (ae2.windCount != -1) return null; break; + if (ae2.windCount != -1) return; + break; default: - if (Math.Abs(ae2.windCount) != 1) return null; break; + if (Math.Abs(ae2.windCount) != 1) return; + break; } // toggle contribution ... @@ -1605,7 +1606,7 @@ private void UpdateEdgeIntoAEL(Active ae) SetSides(ae3.outrec!, ae1, ae3); else SetSides(ae3.outrec!, ae3, ae1); - return ae3.outrec!.pts; + return; } resultOp = StartOpenPath(ae1, pt); @@ -1616,7 +1617,7 @@ private void UpdateEdgeIntoAEL(Active ae) #if USINGZ SetZ(ae1, ae2, ref resultOp.pt); #endif - return resultOp; + return; } // MANAGING CLOSED PATHS FROM HERE ON @@ -1677,7 +1678,8 @@ private void UpdateEdgeIntoAEL(Active ae) bool e1WindCountIs0or1 = oldE1WindCount == 0 || oldE1WindCount == 1; bool e2WindCountIs0or1 = oldE2WindCount == 0 || oldE2WindCount == 1; - if ((!IsHotEdge(ae1) && !e1WindCountIs0or1) || (!IsHotEdge(ae2) && !e2WindCountIs0or1)) return null; + if ((!IsHotEdge(ae1) && !e1WindCountIs0or1) || + (!IsHotEdge(ae2) && !e2WindCountIs0or1)) return; // NOW PROCESS THE INTERSECTION ... @@ -1770,11 +1772,11 @@ private void UpdateEdgeIntoAEL(Active ae) } else if (oldE1WindCount == 1 && oldE2WindCount == 1) { - resultOp = null; + resultOp = null; switch (_cliptype) { case ClipType.Union: - if (e1Wc2 > 0 && e2Wc2 > 0) return null; + if (e1Wc2 > 0 && e2Wc2 > 0) return; resultOp = AddLocalMinPoly(ae1, ae2, pt); break; @@ -1792,7 +1794,7 @@ private void UpdateEdgeIntoAEL(Active ae) break; default: // ClipType.Intersection: - if (e1Wc2 <= 0 || e2Wc2 <= 0) return null; + if (e1Wc2 <= 0 || e2Wc2 <= 0) return; resultOp = AddLocalMinPoly(ae1, ae2, pt); break; } @@ -1801,8 +1803,6 @@ private void UpdateEdgeIntoAEL(Active ae) #endif } } - - return resultOp; } [MethodImpl(MethodImplOptions.AggressiveInlining)] diff --git a/CSharp/Clipper2Lib/Clipper.Offset.cs b/CSharp/Clipper2Lib/Clipper.Offset.cs index 9d6d4090..4acc5a29 100644 --- a/CSharp/Clipper2Lib/Clipper.Offset.cs +++ b/CSharp/Clipper2Lib/Clipper.Offset.cs @@ -1,6 +1,6 @@ /******************************************************************************* * Author : Angus Johnson * -* Date : 24 March 2024 * +* Date : 17 April 2024 * * Website : http://www.angusj.com * * Copyright : Angus Johnson 2010-2024 * * Purpose : Path Offset (Inflate/Shrink) * @@ -549,9 +549,10 @@ private void OffsetPoint(Group group, Path64 path, int j, ref int k) { // is concave pathOut.Add(GetPerpendic(path[j], _normals[k])); - // this extra point is the only (simple) way to ensure that - // path reversals are fully cleaned with the trailing clipper - pathOut.Add(path[j]); // (#405) + // this extra point is the only simple way to ensure that path reversals + // (ie over-shrunk paths) are fully cleaned out with the trailing union op. + // However it's probably safe to skip this whenever an angle is almost flat. + if (cosA < 0.99) pathOut.Add(path[j]); // (#405) pathOut.Add(GetPerpendic(path[j], _normals[j])); } else if ((cosA > 0.999) && (_joinType != JoinType.Round)) diff --git a/Delphi/Clipper2Lib/Clipper.Engine.pas b/Delphi/Clipper2Lib/Clipper.Engine.pas index e733e437..6961e913 100644 --- a/Delphi/Clipper2Lib/Clipper.Engine.pas +++ b/Delphi/Clipper2Lib/Clipper.Engine.pas @@ -2,7 +2,7 @@ (******************************************************************************* * Author : Angus Johnson * -* Date : 21 March 2024 * +* Date : 17 April 2024 * * Website : http://www.angusj.com * * Copyright : Angus Johnson 2010-2024 * * Purpose : This is the main polygon clipping module * @@ -239,7 +239,7 @@ TClipperBase = class function PopHorz(out e: PActive): Boolean; {$IFDEF INLINING} inline; {$ENDIF} function StartOpenPath(e: PActive; const pt: TPoint64): POutPt; procedure UpdateEdgeIntoAEL(var e: PActive); - function IntersectEdges(e1, e2: PActive; pt: TPoint64): POutPt; + procedure IntersectEdges(e1, e2: PActive; pt: TPoint64); procedure DeleteEdges(var e: PActive); procedure DeleteFromAEL(e: PActive); procedure AdjustCurrXAndCopyToSEL(topY: Int64); @@ -287,7 +287,7 @@ TClipperBase = class {$IFDEF USINGZ} procedure SetZ( e1, e2: PActive; var intersectPt: TPoint64); property ZCallback : TZCallback64 read fZCallback write fZCallback; - property DefaultZ : Int64 READ fDefaultZ write fDefaultZ; + property DefaultZ : Int64 read fDefaultZ write fDefaultZ; {$ENDIF} property Succeeded : Boolean read FSucceeded; public @@ -1462,7 +1462,11 @@ function XYCoordsEqual(const pt1, pt2: TPoint64): Boolean; procedure TClipperBase.SetZ(e1, e2: PActive; var intersectPt: TPoint64); begin - if not Assigned(fZCallback) then Exit; + if not Assigned(fZCallback) then + begin + intersectPt.Z := 0; + Exit; + end; // prioritize subject vertices over clip vertices // and pass the subject vertices before clip vertices in the callback @@ -2551,14 +2555,13 @@ function FindEdgeWithMatchingLocMin(e: PActive): PActive; {$IFNDEF USINGZ} {$HINTS OFF} {$ENDIF} -function TClipperBase.IntersectEdges(e1, e2: PActive; pt: TPoint64): POutPt; +procedure TClipperBase.IntersectEdges(e1, e2: PActive; pt: TPoint64); var e1WindCnt, e2WindCnt, e1WindCnt2, e2WindCnt2: Integer; e3: PActive; - op2: POutPt; + resultOp, op2: POutPt; begin - Result := nil; - + resultOp := nil; // MANAGE OPEN PATH INTERSECTIONS SEPARATELY ... if FHasOpenPaths and (IsOpen(e1) or IsOpen(e2)) then begin @@ -2583,7 +2586,7 @@ function TClipperBase.IntersectEdges(e1, e2: PActive; pt: TPoint64): POutPt; // toggle contribution ... if IsHotEdge(e1) then begin - Result := AddOutPt(e1, pt); + resultOp := AddOutPt(e1, pt); if IsFront(e1) then e1.outrec.frontE := nil else e1.outrec.backE := nil; @@ -2605,15 +2608,14 @@ function TClipperBase.IntersectEdges(e1, e2: PActive; pt: TPoint64): POutPt; if e1.windDx > 0 then SetSides(e3.outrec, e1, e3) else SetSides(e3.outrec, e3, e1); - Result := e3.outrec.pts; Exit; end else - Result := StartOpenPath(e1, pt); + resultOp := StartOpenPath(e1, pt); end else - Result := StartOpenPath(e1, pt); + resultOp := StartOpenPath(e1, pt); {$IFDEF USINGZ} - SetZ(e1, e2, Result.pt); + SetZ(e1, e2, resultOp.pt); {$ENDIF} Exit; end; @@ -2677,7 +2679,7 @@ function TClipperBase.IntersectEdges(e1, e2: PActive; pt: TPoint64): POutPt; if not (e1WindCnt in [0,1]) or not (e2WindCnt in [0,1]) or (not IsSamePolyType(e1, e2) and (fClipType <> ctXor)) then begin - Result := AddLocalMaxPoly(e1, e2, pt); + resultOp := AddLocalMaxPoly(e1, e2, pt); {$IFDEF USINGZ} if Assigned(Result) then SetZ(e1, e2, Result.pt); {$ENDIF} @@ -2687,7 +2689,7 @@ function TClipperBase.IntersectEdges(e1, e2: PActive; pt: TPoint64): POutPt; // this 'else if' condition isn't strictly needed but // it's sensible to split polygons that ony touch at // a common vertex (not at common edges). - Result := AddLocalMaxPoly(e1, e2, pt); + resultOp := AddLocalMaxPoly(e1, e2, pt); {$IFDEF USINGZ} op2 := AddLocalMinPoly(e1, e2, pt); if Assigned(Result) then SetZ(e1, e2, Result.pt); @@ -2698,7 +2700,7 @@ function TClipperBase.IntersectEdges(e1, e2: PActive; pt: TPoint64): POutPt; end else begin // can't treat as maxima & minima - Result := AddOutPt(e1, pt); + resultOp := AddOutPt(e1, pt); {$IFDEF USINGZ} op2 := AddOutPt(e2, pt); SetZ(e1, e2, Result.pt); @@ -2713,7 +2715,7 @@ function TClipperBase.IntersectEdges(e1, e2: PActive; pt: TPoint64): POutPt; // if one or other edge is 'hot' ... else if IsHotEdge(e1) then begin - Result := AddOutPt(e1, pt); + resultOp := AddOutPt(e1, pt); {$IFDEF USINGZ} SetZ(e1, e2, Result.pt); {$ENDIF} @@ -2721,7 +2723,7 @@ function TClipperBase.IntersectEdges(e1, e2: PActive; pt: TPoint64): POutPt; end else if IsHotEdge(e2) then begin - Result := AddOutPt(e2, pt); + resultOp := AddOutPt(e2, pt); {$IFDEF USINGZ} SetZ(e1, e2, Result.pt); {$ENDIF} @@ -2751,29 +2753,29 @@ function TClipperBase.IntersectEdges(e1, e2: PActive; pt: TPoint64): POutPt; if not IsSamePolyType(e1, e2) then begin - Result := AddLocalMinPoly(e1, e2, pt, false); + resultOp := AddLocalMinPoly(e1, e2, pt, false); {$IFDEF USINGZ} SetZ(e1, e2, Result.pt); {$ENDIF} end else if (e1WindCnt = 1) and (e2WindCnt = 1) then begin - Result := nil; + resultOp := nil; case FClipType of ctIntersection: if (e1WindCnt2 <= 0) or (e2WindCnt2 <= 0) then Exit - else Result := AddLocalMinPoly(e1, e2, pt, false); + else resultOp := AddLocalMinPoly(e1, e2, pt, false); ctUnion: if (e1WindCnt2 <= 0) and (e2WindCnt2 <= 0) then - Result := AddLocalMinPoly(e1, e2, pt, false); + resultOp := AddLocalMinPoly(e1, e2, pt, false); ctDifference: if ((GetPolyType(e1) = ptClip) and (e1WindCnt2 > 0) and (e2WindCnt2 > 0)) or ((GetPolyType(e1) = ptSubject) and (e1WindCnt2 <= 0) and (e2WindCnt2 <= 0)) then - Result := AddLocalMinPoly(e1, e2, pt, false); + resultOp := AddLocalMinPoly(e1, e2, pt, false); else // xOr - Result := AddLocalMinPoly(e1, e2, pt, false); + resultOp := AddLocalMinPoly(e1, e2, pt, false); end; {$IFDEF USINGZ} if assigned(Result) then SetZ(e1, e2, Result.pt); diff --git a/Delphi/Clipper2Lib/Clipper.Offset.pas b/Delphi/Clipper2Lib/Clipper.Offset.pas index 213bf658..7e7d99f0 100644 --- a/Delphi/Clipper2Lib/Clipper.Offset.pas +++ b/Delphi/Clipper2Lib/Clipper.Offset.pas @@ -2,7 +2,7 @@ (******************************************************************************* * Author : Angus Johnson * -* Date : 24 March 2024 * +* Date : 17 April 2024 * * Website : http://www.angusj.com * * Copyright : Angus Johnson 2010-2024 * * Purpose : Path Offset (Inflate/Shrink) * @@ -996,9 +996,11 @@ procedure TClipperOffset.DoRound(j, k: Integer; angle: double); {$ELSE} AddPoint(GetPerpendic(fInPath[j], fNorms[k], fGroupDelta)); {$ENDIF} - // this extra point is the only (simple) way to ensure that - // path reversals are fully cleaned with the trailing clipper - AddPoint(fInPath[j]); // (#405) + // this extra point is the only simple way to ensure that path reversals + // (ie over-shrunk paths) are fully cleaned out with the trailing union op. + // However it's probably safe to skip this whenever an angle is almost flat. + if (cosA < 0.99) then + AddPoint(fInPath[j]); // (#405) {$IFDEF USINGZ} AddPoint(GetPerpendic(fInPath[j], fNorms[j], fGroupDelta), fInPath[j].Z); {$ELSE} From 718e714a643aee69826441b042bb729bfc310923 Mon Sep 17 00:00:00 2001 From: angusj Date: Wed, 17 Apr 2024 22:31:26 +1000 Subject: [PATCH 43/64] clipper.engine.cpp - Fixed compile bug introduced in previous commit --- CPP/Clipper2Lib/src/clipper.engine.cpp | 56 +++++++++++++++++++++++--- 1 file changed, 50 insertions(+), 6 deletions(-) diff --git a/CPP/Clipper2Lib/src/clipper.engine.cpp b/CPP/Clipper2Lib/src/clipper.engine.cpp index 5a7e4fa2..dc2a597a 100644 --- a/CPP/Clipper2Lib/src/clipper.engine.cpp +++ b/CPP/Clipper2Lib/src/clipper.engine.cpp @@ -1824,11 +1824,17 @@ namespace Clipper2Lib { if (std::abs(edge_c->wind_cnt) != 1) return; } +#ifdef USINGZ OutPt* resultOp; +#endif //toggle contribution ... if (IsHotEdge(*edge_o)) { +#ifdef USINGZ resultOp = AddOutPt(*edge_o, pt); +#else + AddOutPt(*edge_o, pt); +#endif if (IsFront(*edge_o)) edge_o->outrec->front_edge = nullptr; else edge_o->outrec->back_edge = nullptr; edge_o->outrec = nullptr; @@ -1851,10 +1857,18 @@ namespace Clipper2Lib { return; } else +#ifdef USINGZ resultOp = StartOpenPath(*edge_o, pt); +#else + StartOpenPath(*edge_o, pt); +#endif } else +#ifdef USINGZ resultOp = StartOpenPath(*edge_o, pt); +#else + StartOpenPath(*edge_o, pt); +#endif #ifdef USINGZ if (zCallback_) SetZ(*edge_o, *edge_c, resultOp->pt); @@ -1933,16 +1947,20 @@ namespace Clipper2Lib { return; //NOW PROCESS THE INTERSECTION ... +#ifdef USINGZ OutPt* resultOp = nullptr; +#endif //if both edges are 'hot' ... if (IsHotEdge(e1) && IsHotEdge(e2)) { if ((old_e1_windcnt != 0 && old_e1_windcnt != 1) || (old_e2_windcnt != 0 && old_e2_windcnt != 1) || (e1.local_min->polytype != e2.local_min->polytype && cliptype_ != ClipType::Xor)) { - resultOp = AddLocalMaxPoly(e1, e2, pt); #ifdef USINGZ + resultOp = AddLocalMaxPoly(e1, e2, pt); if (zCallback_ && resultOp) SetZ(e1, e2, resultOp->pt); +#else + AddLocalMaxPoly(e1, e2, pt); #endif } else if (IsFront(e1) || (e1.outrec == e2.outrec)) @@ -1951,19 +1969,20 @@ namespace Clipper2Lib { //it's sensible to split polygons that ony touch at //a common vertex (not at common edges). - resultOp = AddLocalMaxPoly(e1, e2, pt); #ifdef USINGZ + resultOp = AddLocalMaxPoly(e1, e2, pt); OutPt* op2 = AddLocalMinPoly(e1, e2, pt); if (zCallback_ && resultOp) SetZ(e1, e2, resultOp->pt); if (zCallback_) SetZ(e1, e2, op2->pt); #else + AddLocalMaxPoly(e1, e2, pt); AddLocalMinPoly(e1, e2, pt); #endif } else { - resultOp = AddOutPt(e1, pt); #ifdef USINGZ + resultOp = AddOutPt(e1, pt); OutPt* op2 = AddOutPt(e2, pt); if (zCallback_) { @@ -1971,6 +1990,7 @@ namespace Clipper2Lib { SetZ(e1, e2, op2->pt); } #else + AddOutPt(e1, pt); AddOutPt(e2, pt); #endif SwapOutrecs(e1, e2); @@ -1978,17 +1998,21 @@ namespace Clipper2Lib { } else if (IsHotEdge(e1)) { - resultOp = AddOutPt(e1, pt); #ifdef USINGZ + resultOp = AddOutPt(e1, pt); if (zCallback_) SetZ(e1, e2, resultOp->pt); +#else + AddOutPt(e1, pt); #endif SwapOutrecs(e1, e2); } else if (IsHotEdge(e2)) { - resultOp = AddOutPt(e2, pt); #ifdef USINGZ + resultOp = AddOutPt(e2, pt); if (zCallback_) SetZ(e1, e2, resultOp->pt); +#else + AddOutPt(e2, pt); #endif SwapOutrecs(e1, e2); } @@ -2018,33 +2042,53 @@ namespace Clipper2Lib { if (!IsSamePolyType(e1, e2)) { - resultOp = AddLocalMinPoly(e1, e2, pt, false); #ifdef USINGZ + resultOp = AddLocalMinPoly(e1, e2, pt, false); if (zCallback_) SetZ(e1, e2, resultOp->pt); +#else + AddLocalMinPoly(e1, e2, pt, false); #endif } else if (old_e1_windcnt == 1 && old_e2_windcnt == 1) { +#ifdef USINGZ resultOp = nullptr; +#endif switch (cliptype_) { case ClipType::Union: if (e1Wc2 <= 0 && e2Wc2 <= 0) +#ifdef USINGZ resultOp = AddLocalMinPoly(e1, e2, pt, false); +#else + AddLocalMinPoly(e1, e2, pt, false); +#endif break; case ClipType::Difference: if (((GetPolyType(e1) == PathType::Clip) && (e1Wc2 > 0) && (e2Wc2 > 0)) || ((GetPolyType(e1) == PathType::Subject) && (e1Wc2 <= 0) && (e2Wc2 <= 0))) { +#ifdef USINGZ resultOp = AddLocalMinPoly(e1, e2, pt, false); +#else + AddLocalMinPoly(e1, e2, pt, false); +#endif } break; case ClipType::Xor: +#ifdef USINGZ resultOp = AddLocalMinPoly(e1, e2, pt, false); +#else + AddLocalMinPoly(e1, e2, pt, false); +#endif break; default: if (e1Wc2 > 0 && e2Wc2 > 0) +#ifdef USINGZ resultOp = AddLocalMinPoly(e1, e2, pt, false); +#else + AddLocalMinPoly(e1, e2, pt, false); +#endif break; } #ifdef USINGZ From adc000af981545feed8c58a3687bd9177259536a Mon Sep 17 00:00:00 2001 From: angusj Date: Wed, 17 Apr 2024 22:45:20 +1000 Subject: [PATCH 44/64] Clipper.Offset.cs - minor tweak to aid compiler (#800) --- CSharp/Clipper2Lib/Clipper.Offset.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CSharp/Clipper2Lib/Clipper.Offset.cs b/CSharp/Clipper2Lib/Clipper.Offset.cs index 4acc5a29..1e03d37c 100644 --- a/CSharp/Clipper2Lib/Clipper.Offset.cs +++ b/CSharp/Clipper2Lib/Clipper.Offset.cs @@ -692,7 +692,7 @@ private void DoGroupOffset(Group group) using List.Enumerator pathIt = group.inPaths.GetEnumerator(); while (pathIt.MoveNext()) { - Path64 p = pathIt.Current; + Path64 p = pathIt.Current!; pathOut = new Path64(); int cnt = p.Count; From b4ee03378751247961a4bd40601ffc6ed9b46c2d Mon Sep 17 00:00:00 2001 From: angusj Date: Sat, 27 Apr 2024 14:00:57 +1000 Subject: [PATCH 45/64] clipper.core.h - fixed ambiguous 'round' (#824) also fixed a compiler warning (#823) --- .../include/clipper2/clipper.core.h | 29 ++++++++++++------- 1 file changed, 18 insertions(+), 11 deletions(-) diff --git a/CPP/Clipper2Lib/include/clipper2/clipper.core.h b/CPP/Clipper2Lib/include/clipper2/clipper.core.h index 2513f4c9..516d162a 100644 --- a/CPP/Clipper2Lib/include/clipper2/clipper.core.h +++ b/CPP/Clipper2Lib/include/clipper2/clipper.core.h @@ -1,6 +1,6 @@ /******************************************************************************* * Author : Angus Johnson * -* Date : 24 March 2024 * +* Date : 27 April 2024 * * Website : http://www.angusj.com * * Copyright : Angus Johnson 2010-2024 * * Purpose : Core Clipper Library structures and functions * @@ -93,6 +93,13 @@ namespace Clipper2Lib #endif } + // can we call std::round on T? (default false) (#824) + template + struct is_round_invocable : std::false_type {}; + + template + struct is_round_invocable()))>> : std::true_type {}; + //By far the most widely used filling rules for polygons are EvenOdd //and NonZero, sometimes called Alternate and Winding respectively. @@ -111,8 +118,8 @@ namespace Clipper2Lib template inline void Init(const T2 x_ = 0, const T2 y_ = 0, const int64_t z_ = 0) { - if constexpr (std::numeric_limits::is_integer && - !std::numeric_limits::is_integer) + if constexpr (std::is_integral_v && + is_round_invocable::value && !std::is_integral_v) { x = static_cast(std::round(x_)); y = static_cast(std::round(y_)); @@ -165,8 +172,8 @@ namespace Clipper2Lib template inline void Init(const T2 x_ = 0, const T2 y_ = 0) { - if constexpr (std::numeric_limits::is_integer && - !std::numeric_limits::is_integer) + if constexpr (std::is_integral_v && + is_round_invocable::value && !std::is_integral_v) { x = static_cast(std::round(x_)); y = static_cast(std::round(y_)); @@ -184,7 +191,7 @@ namespace Clipper2Lib Point(const T2 x_, const T2 y_) { Init(x_, y_); } template - explicit Point(const Point& p) { Init(p.x, p.y); } + explicit Point(const Point& p) { Init(p.x, p.y); } Point operator * (const double scale) const { @@ -365,8 +372,8 @@ namespace Clipper2Lib { Rect result; - if constexpr (std::numeric_limits::is_integer && - !std::numeric_limits::is_integer) + if constexpr (std::is_integral_v && + is_round_invocable::value && !std::is_integral_v) { result.left = static_cast(std::round(rect.left * scale)); result.top = static_cast(std::round(rect.top * scale)); @@ -518,7 +525,7 @@ namespace Clipper2Lib { Paths result; - if constexpr (std::numeric_limits::is_integer) + if constexpr (std::is_integral_v) { RectD r = GetBounds(paths); if ((r.left * scale_x) < min_coord || @@ -763,7 +770,7 @@ namespace Clipper2Lib T bb1maxx = CC_MAX(ln2a.x, ln2b.x); T bb1maxy = CC_MAX(ln2a.y, ln2b.y); - if constexpr (std::numeric_limits::is_integer) + if constexpr (std::is_integral_v) { int64_t originx = (CC_MIN(bb0maxx, bb1maxx) + CC_MAX(bb0minx, bb1minx)) >> 1; int64_t originy = (CC_MIN(bb0maxy, bb1maxy) + CC_MAX(bb0miny, bb1miny)) >> 1; @@ -872,7 +879,7 @@ namespace Clipper2Lib static_cast(offPt.y - seg1.y) * dy) / (Sqr(dx) + Sqr(dy)); if (q < 0) q = 0; else if (q > 1) q = 1; - if constexpr (std::numeric_limits::is_integer) + if constexpr (std::is_integral_v) return Point( seg1.x + static_cast(nearbyint(q * dx)), seg1.y + static_cast(nearbyint(q * dy))); From ea67db3d2f87531778c4678e076e66afc8e9a2c2 Mon Sep 17 00:00:00 2001 From: angusj Date: Sat, 27 Apr 2024 17:09:18 +1000 Subject: [PATCH 46/64] Attempt to fix run error on MacOS --- CPP/Clipper2Lib/src/clipper.engine.cpp | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/CPP/Clipper2Lib/src/clipper.engine.cpp b/CPP/Clipper2Lib/src/clipper.engine.cpp index dc2a597a..9ac02073 100644 --- a/CPP/Clipper2Lib/src/clipper.engine.cpp +++ b/CPP/Clipper2Lib/src/clipper.engine.cpp @@ -1121,12 +1121,12 @@ namespace Clipper2Lib { inline bool IsCollinear(const Point64& pt1, const Point64& sharedPt, const Point64& pt2) // #777 { -#ifdef __aarch64__ - double cp = CrossProduct(pt1, sharedPt, pt2); - return std::fabs(cp) < 0.00000001; -#else +//#ifdef __aarch64__ +// double cp = CrossProduct(pt1, sharedPt, pt2); +// return std::fabs(cp) < 0.00000001; +//#else return CrossProduct(pt1, sharedPt, pt2) == 0; -#endif +//#endif } bool IsValidAelOrder(const Active& resident, const Active& newcomer) From e9fe6b0166b29c640033d90801a26faa1ac59681 Mon Sep 17 00:00:00 2001 From: angusj Date: Sat, 27 Apr 2024 17:13:13 +1000 Subject: [PATCH 47/64] Another attempt to fix run error in MacOS (C++ only) --- CPP/Clipper2Lib/src/clipper.engine.cpp | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/CPP/Clipper2Lib/src/clipper.engine.cpp b/CPP/Clipper2Lib/src/clipper.engine.cpp index 9ac02073..58123fe1 100644 --- a/CPP/Clipper2Lib/src/clipper.engine.cpp +++ b/CPP/Clipper2Lib/src/clipper.engine.cpp @@ -1121,12 +1121,12 @@ namespace Clipper2Lib { inline bool IsCollinear(const Point64& pt1, const Point64& sharedPt, const Point64& pt2) // #777 { -//#ifdef __aarch64__ -// double cp = CrossProduct(pt1, sharedPt, pt2); -// return std::fabs(cp) < 0.00000001; -//#else +#ifdef __APPLE__ + double cp = CrossProduct(pt1, sharedPt, pt2); + return std::fabs(cp) < 0.00000001; +#else return CrossProduct(pt1, sharedPt, pt2) == 0; -//#endif +#endif } bool IsValidAelOrder(const Active& resident, const Active& newcomer) From cef6a1071d8e03db393f32cd5b4bd286c95ccc00 Mon Sep 17 00:00:00 2001 From: angusj Date: Sat, 27 Apr 2024 17:21:36 +1000 Subject: [PATCH 48/64] another attempt to fix collinearity testing on MacOS (C++) (#777) --- CPP/Clipper2Lib/src/clipper.engine.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CPP/Clipper2Lib/src/clipper.engine.cpp b/CPP/Clipper2Lib/src/clipper.engine.cpp index 58123fe1..ad0fc9b9 100644 --- a/CPP/Clipper2Lib/src/clipper.engine.cpp +++ b/CPP/Clipper2Lib/src/clipper.engine.cpp @@ -1121,9 +1121,9 @@ namespace Clipper2Lib { inline bool IsCollinear(const Point64& pt1, const Point64& sharedPt, const Point64& pt2) // #777 { -#ifdef __APPLE__ +#ifdef __aarch64__ double cp = CrossProduct(pt1, sharedPt, pt2); - return std::fabs(cp) < 0.00000001; + return std::fabs(cp) < 0.0000001; #else return CrossProduct(pt1, sharedPt, pt2) == 0; #endif From 99a9706701c4a9a073a66ee73aeaa0ede9e40bec Mon Sep 17 00:00:00 2001 From: angusj Date: Sat, 27 Apr 2024 19:22:23 +1000 Subject: [PATCH 49/64] Additional minor code tidy (#823) --- CPP/Clipper2Lib/include/clipper2/clipper.core.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CPP/Clipper2Lib/include/clipper2/clipper.core.h b/CPP/Clipper2Lib/include/clipper2/clipper.core.h index 516d162a..80641260 100644 --- a/CPP/Clipper2Lib/include/clipper2/clipper.core.h +++ b/CPP/Clipper2Lib/include/clipper2/clipper.core.h @@ -143,13 +143,13 @@ namespace Clipper2Lib } template - explicit Point(const Point& p) + explicit Point(const Point& p) { Init(p.x, p.y, p.z); } template - explicit Point(const Point& p, int64_t z_) + explicit Point(const Point& p, int64_t z_) { Init(p.x, p.y, z_); } From cf841d74732a12b32e63dcf9c4dd415996074ede Mon Sep 17 00:00:00 2001 From: angusj Date: Sat, 27 Apr 2024 21:53:53 +1000 Subject: [PATCH 50/64] Improved IsCollinear function (#777) --- .../include/clipper2/clipper.core.h | 26 ++++++-- CPP/Clipper2Lib/include/clipper2/clipper.h | 14 ++--- CPP/Clipper2Lib/src/clipper.engine.cpp | 17 +----- CPP/Clipper2Lib/src/clipper.rectclip.cpp | 11 ++-- CSharp/Clipper2Lib/Clipper.Core.cs | 10 ++- CSharp/Clipper2Lib/Clipper.Engine.cs | 13 ++-- CSharp/Clipper2Lib/Clipper.RectClip.cs | 12 ++-- CSharp/Clipper2Lib/Clipper.cs | 21 +++---- Delphi/Clipper2Lib/Clipper.Core.pas | 61 +++++++++---------- Delphi/Clipper2Lib/Clipper.Engine.pas | 12 ++-- Delphi/Clipper2Lib/Clipper.RectClip.pas | 6 +- Delphi/Clipper2Lib/Clipper.pas | 14 ++--- 12 files changed, 112 insertions(+), 105 deletions(-) diff --git a/CPP/Clipper2Lib/include/clipper2/clipper.core.h b/CPP/Clipper2Lib/include/clipper2/clipper.core.h index 80641260..4e9be2f5 100644 --- a/CPP/Clipper2Lib/include/clipper2/clipper.core.h +++ b/CPP/Clipper2Lib/include/clipper2/clipper.core.h @@ -659,6 +659,17 @@ namespace Clipper2Lib CheckPrecisionRange(precision, error_code); } + template + inline bool IsCollinear(const Point& pt1, + const Point& sharedPt, const Point& pt2) // #777 + { + const double a = static_cast(sharedPt.x - pt1.x); + const double b = static_cast(pt2.y - sharedPt.y); + const double c = static_cast(sharedPt.y - pt1.y); + const double d = static_cast(pt2.x - sharedPt.x); + return a * b == c * d; + } + template inline double CrossProduct(const Point& pt1, const Point& pt2, const Point& pt3) { return (static_cast(pt2.x - pt1.x) * static_cast(pt3.y - @@ -846,6 +857,13 @@ namespace Clipper2Lib #endif } + template + inline int GetSign(const T& val) + { + if (!val) return 0; + return (val > 0) ? 1 : -1; + } + inline bool SegmentsIntersect(const Point64& seg1a, const Point64& seg1b, const Point64& seg2a, const Point64& seg2b, bool inclusive = false) { @@ -860,10 +878,10 @@ namespace Clipper2Lib return (res1 || res2 || res3 || res4); // ensures not collinear } else { - return (CrossProduct(seg1a, seg2a, seg2b) * - CrossProduct(seg1b, seg2a, seg2b) < 0) && - (CrossProduct(seg2a, seg1a, seg1b) * - CrossProduct(seg2b, seg1a, seg1b) < 0); + return (GetSign(CrossProduct(seg1a, seg2a, seg2b)) * + GetSign(CrossProduct(seg1b, seg2a, seg2b)) < 0) && + (GetSign(CrossProduct(seg2a, seg1a, seg1b)) * + GetSign(CrossProduct(seg2b, seg1a, seg1b)) < 0); } } diff --git a/CPP/Clipper2Lib/include/clipper2/clipper.h b/CPP/Clipper2Lib/include/clipper2/clipper.h index e086b6ab..4f55df87 100644 --- a/CPP/Clipper2Lib/include/clipper2/clipper.h +++ b/CPP/Clipper2Lib/include/clipper2/clipper.h @@ -1,8 +1,8 @@ /******************************************************************************* * Author : Angus Johnson * -* Date : 13 December 2023 * +* Date : 27 April 2024 * * Website : http://www.angusj.com * -* Copyright : Angus Johnson 2010-2023 * +* Copyright : Angus Johnson 2010-2024 * * Purpose : This module provides a simple interface to the Clipper Library * * License : http://www.boost.org/LICENSE_1_0.txt * *******************************************************************************/ @@ -510,9 +510,9 @@ namespace Clipper2Lib { if (!is_open_path) { - while (srcIt != stop && !CrossProduct(*stop, *srcIt, *(srcIt + 1))) + while (srcIt != stop && IsCollinear(*stop, *srcIt, *(srcIt + 1))) ++srcIt; - while (srcIt != stop && !CrossProduct(*(stop - 1), *stop, *srcIt)) + while (srcIt != stop && IsCollinear(*(stop - 1), *stop, *srcIt)) --stop; if (srcIt == stop) return Path64(); } @@ -521,7 +521,7 @@ namespace Clipper2Lib { dst.push_back(*prevIt); for (; srcIt != stop; ++srcIt) { - if (CrossProduct(*prevIt, *srcIt, *(srcIt + 1))) + if (!IsCollinear(*prevIt, *srcIt, *(srcIt + 1))) { prevIt = srcIt; dst.push_back(*prevIt); @@ -530,12 +530,12 @@ namespace Clipper2Lib { if (is_open_path) dst.push_back(*srcIt); - else if (CrossProduct(*prevIt, *stop, dst[0])) + else if (!IsCollinear(*prevIt, *stop, dst[0])) dst.push_back(*stop); else { while (dst.size() > 2 && - !CrossProduct(dst[dst.size() - 1], dst[dst.size() - 2], dst[0])) + IsCollinear(dst[dst.size() - 1], dst[dst.size() - 2], dst[0])) dst.pop_back(); if (dst.size() < 3) return Path64(); } diff --git a/CPP/Clipper2Lib/src/clipper.engine.cpp b/CPP/Clipper2Lib/src/clipper.engine.cpp index ad0fc9b9..de288d7f 100644 --- a/CPP/Clipper2Lib/src/clipper.engine.cpp +++ b/CPP/Clipper2Lib/src/clipper.engine.cpp @@ -1,6 +1,6 @@ /******************************************************************************* * Author : Angus Johnson * -* Date : 17 April 2024 * +* Date : 27 April 2024 * * Website : http://www.angusj.com * * Copyright : Angus Johnson 2010-2024 * * Purpose : This is the main polygon clipping module * @@ -1118,17 +1118,6 @@ namespace Clipper2Lib { } } - inline bool IsCollinear(const Point64& pt1, - const Point64& sharedPt, const Point64& pt2) // #777 - { -#ifdef __aarch64__ - double cp = CrossProduct(pt1, sharedPt, pt2); - return std::fabs(cp) < 0.0000001; -#else - return CrossProduct(pt1, sharedPt, pt2) == 0; -#endif - } - bool IsValidAelOrder(const Active& resident, const Active& newcomer) { if (newcomer.curr_x != resident.curr_x) @@ -2828,7 +2817,7 @@ namespace Clipper2Lib { if (PerpendicDistFromLineSqrd(pt, prev->bot, prev->top) > 0.25) return; } else if (e.curr_x != prev->curr_x) return; - if (CrossProduct(e.top, pt, prev->top)) return; + if (!IsCollinear(e.top, pt, prev->top)) return; if (e.outrec->idx == prev->outrec->idx) AddLocalMaxPoly(*prev, e, pt); @@ -2856,7 +2845,7 @@ namespace Clipper2Lib { if (PerpendicDistFromLineSqrd(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 (!IsCollinear(e.top, pt, next->top)) return; if (e.outrec->idx == next->outrec->idx) AddLocalMaxPoly(e, *next, pt); diff --git a/CPP/Clipper2Lib/src/clipper.rectclip.cpp b/CPP/Clipper2Lib/src/clipper.rectclip.cpp index d5792267..c5ae9e2e 100644 --- a/CPP/Clipper2Lib/src/clipper.rectclip.cpp +++ b/CPP/Clipper2Lib/src/clipper.rectclip.cpp @@ -1,8 +1,8 @@ /******************************************************************************* * Author : Angus Johnson * -* Date : 8 September 2023 * +* Date : 27 April 2024 * * Website : http://www.angusj.com * -* Copyright : Angus Johnson 2010-2023 * +* Copyright : Angus Johnson 2010-2024 * * Purpose : FAST rectangular clipping * * License : http://www.boost.org/LICENSE_1_0.txt * *******************************************************************************/ @@ -589,8 +589,7 @@ namespace Clipper2Lib { OutPt2* op2 = op; do { - if (!CrossProduct(op2->prev->pt, - op2->pt, op2->next->pt)) + if (IsCollinear(op2->prev->pt, op2->pt, op2->next->pt)) { if (op2 == op) { @@ -825,8 +824,8 @@ namespace Clipper2Lib { OutPt2* op2 = op->next; while (op2 && op2 != op) { - if (CrossProduct(op2->prev->pt, - op2->pt, op2->next->pt) == 0) + if (!IsCollinear(op2->prev->pt, + op2->pt, op2->next->pt)) { op = op2->prev; op2 = UnlinkOp(op2); diff --git a/CSharp/Clipper2Lib/Clipper.Core.cs b/CSharp/Clipper2Lib/Clipper.Core.cs index d0128ff0..0979e7df 100644 --- a/CSharp/Clipper2Lib/Clipper.Core.cs +++ b/CSharp/Clipper2Lib/Clipper.Core.cs @@ -1,6 +1,6 @@ /******************************************************************************* * Author : Angus Johnson * -* Date : 24 March 2024 * +* Date : 27 April 2024 * * Website : http://www.angusj.com * * Copyright : Angus Johnson 2010-2024 * * Purpose : Core structures and functions for the Clipper Library * @@ -601,6 +601,14 @@ internal static double CrossProduct(Point64 pt1, Point64 pt2, Point64 pt3) (double) (pt2.Y - pt1.Y) * (pt3.X - pt2.X)); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static bool IsCollinear(Point64 pt1, Point64 pt2, Point64 pt3) + { + // typecast to double to avoid potential int overflow + return (double) (pt2.X - pt1.X) * (double) (pt3.Y - pt2.Y) == + (double) (pt2.Y - pt1.Y) * (double) (pt3.X - pt2.X); + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] internal static double DotProduct(Point64 pt1, Point64 pt2, Point64 pt3) { diff --git a/CSharp/Clipper2Lib/Clipper.Engine.cs b/CSharp/Clipper2Lib/Clipper.Engine.cs index e6800b7e..c33ff708 100644 --- a/CSharp/Clipper2Lib/Clipper.Engine.cs +++ b/CSharp/Clipper2Lib/Clipper.Engine.cs @@ -1,6 +1,6 @@ /******************************************************************************* * Author : Angus Johnson * -* Date : 17 April 2024 * +* Date : 27 April 2024 * * Website : http://www.angusj.com * * Copyright : Angus Johnson 2010-2024 * * Purpose : This is the main polygon clipping module * @@ -1112,8 +1112,8 @@ private static bool IsValidAelOrder(Active resident, Active newcomer) // resident must also have just been inserted if (resident.isLeftBound != newcomerIsLeft) return newcomerIsLeft; - if (InternalClipper.CrossProduct(PrevPrevVertex(resident).pt, - resident.bot, resident.top) == 0) return true; + if (InternalClipper.IsCollinear(PrevPrevVertex(resident).pt, + resident.bot, resident.top)) return true; // compare turning direction of the alternate bound return (InternalClipper.CrossProduct(PrevPrevVertex(resident).pt, newcomer.bot, PrevPrevVertex(newcomer).pt) > 0) == newcomerIsLeft; @@ -2395,7 +2395,7 @@ private void CheckJoinLeft(Active e, 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 (!InternalClipper.IsCollinear(e.top, pt, prev.top)) return; if (e.outrec!.idx == prev.outrec!.idx) AddLocalMaxPoly(prev, e, pt); @@ -2424,8 +2424,7 @@ private void CheckJoinRight(Active e, 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 (!InternalClipper.IsCollinear(e.top, pt, next.top)) return; if (e.outrec!.idx == next.outrec!.idx) AddLocalMaxPoly(e, next, pt); @@ -2821,7 +2820,7 @@ private void CleanCollinear(OutRec? outrec) for (; ; ) { // NB if preserveCollinear == true, then only remove 180 deg. spikes - if ((InternalClipper.CrossProduct(op2!.prev.pt, op2.pt, op2.next!.pt) == 0) && + if ((InternalClipper.IsCollinear(op2!.prev.pt, op2.pt, op2.next!.pt)) && ((op2.pt == op2.prev.pt) || (op2.pt == op2.next.pt) || !PreserveCollinear || (InternalClipper.DotProduct(op2.prev.pt, op2.pt, op2.next.pt) < 0))) { diff --git a/CSharp/Clipper2Lib/Clipper.RectClip.cs b/CSharp/Clipper2Lib/Clipper.RectClip.cs index 716ab079..a8d1047c 100644 --- a/CSharp/Clipper2Lib/Clipper.RectClip.cs +++ b/CSharp/Clipper2Lib/Clipper.RectClip.cs @@ -1,8 +1,8 @@ /******************************************************************************* * Author : Angus Johnson * -* Date : 8 September 2023 * +* Date : 27 April 2024 * * Website : http://www.angusj.com * -* Copyright : Angus Johnson 2010-2023 * +* Copyright : Angus Johnson 2010-2024 * * Purpose : FAST rectangular clipping * * License : http://www.boost.org/LICENSE_1_0.txt * *******************************************************************************/ @@ -694,8 +694,8 @@ private void CheckEdges() if (op == null) continue; do { - if (InternalClipper.CrossProduct( - op2!.prev!.pt, op2.pt, op2.next!.pt) == 0) + if (InternalClipper.IsCollinear( + op2!.prev!.pt, op2.pt, op2.next!.pt)) { if (op2 == op) { @@ -930,8 +930,8 @@ private Path64 GetPath(OutPt2? op) OutPt2? op2 = op.next; while (op2 != null && op2 != op) { - if (InternalClipper.CrossProduct( - op2.prev!.pt, op2.pt, op2.next!.pt) == 0) + if (InternalClipper.IsCollinear( + op2.prev!.pt, op2.pt, op2.next!.pt)) { op = op2.prev; op2 = UnlinkOp(op2); diff --git a/CSharp/Clipper2Lib/Clipper.cs b/CSharp/Clipper2Lib/Clipper.cs index 152af315..6722bbd2 100644 --- a/CSharp/Clipper2Lib/Clipper.cs +++ b/CSharp/Clipper2Lib/Clipper.cs @@ -1,8 +1,8 @@ /******************************************************************************* * Author : Angus Johnson * -* Date : 18 October 2023 * +* Date : 27 April 2024 * * Website : http://www.angusj.com * -* Copyright : Angus Johnson 2010-2023 * +* Copyright : Angus Johnson 2010-2024 * * Purpose : This module contains simple functions that will likely cover * * most polygon boolean and offsetting needs, while also avoiding * * the inherent complexities of the other modules. * @@ -1022,10 +1022,9 @@ public static Path64 TrimCollinear(Path64 path, bool isOpen = false) int i = 0; if (!isOpen) { - while (i < len - 1 && InternalClipper.CrossProduct( - path[len - 1], path[i], path[i + 1]) == 0) i++; - while (i < len - 1 && InternalClipper.CrossProduct( - path[len - 2], path[len - 1], path[i]) == 0) len--; + while (i < len - 1 && + InternalClipper.IsCollinear(path[len - 1], path[i], path[i + 1])) i++; + while (i < len - 1 && InternalClipper.IsCollinear(path[len - 2], path[len - 1], path[i])) len--; } if (len - i < 3) @@ -1040,21 +1039,19 @@ public static Path64 TrimCollinear(Path64 path, bool isOpen = false) result.Add(last); for (i++; i < len - 1; i++) { - if (InternalClipper.CrossProduct( - last, path[i], path[i + 1]) == 0) continue; + if (InternalClipper.IsCollinear(last, path[i], path[i + 1])) continue; last = path[i]; result.Add(last); } if (isOpen) result.Add(path[len - 1]); - else if (InternalClipper.CrossProduct( - last, path[len - 1], result[0]) != 0) + else if (!InternalClipper.IsCollinear(last, path[len - 1], result[0])) result.Add(path[len - 1]); else { - while (result.Count > 2 && InternalClipper.CrossProduct( - result[result.Count - 1], result[result.Count - 2], result[0]) == 0) + while (result.Count > 2 && InternalClipper.IsCollinear( + result[result.Count - 1], result[result.Count - 2], result[0])) { result.RemoveAt(result.Count - 1); } diff --git a/Delphi/Clipper2Lib/Clipper.Core.pas b/Delphi/Clipper2Lib/Clipper.Core.pas index 0e58de5d..a306a9ab 100644 --- a/Delphi/Clipper2Lib/Clipper.Core.pas +++ b/Delphi/Clipper2Lib/Clipper.Core.pas @@ -2,7 +2,7 @@ (******************************************************************************* * Author : Angus Johnson * -* Date : 24 March 2024 * +* Date : 27 April 2024 * * Website : http://www.angusj.com * * Copyright : Angus Johnson 2010-2024 * * Purpose : Core Clipper Library module * @@ -154,7 +154,8 @@ function IsPositive(const path: TPath64): Boolean; overload; function IsPositive(const path: TPathD): Boolean; overload; {$IFDEF INLINING} inline; {$ENDIF} -function __Trunc(val: double): Int64; {$IFDEF INLINE} inline; {$ENDIF} +function IsCollinear(const pt1, pt2, pt3: TPoint64): Boolean; + {$IFDEF INLINING} inline; {$ENDIF} function CrossProduct(const pt1, pt2, pt3: TPoint64): double; overload; {$IFDEF INLINING} inline; {$ENDIF} @@ -1863,6 +1864,18 @@ function IsPositive(const path: TPathD): Boolean; end; //------------------------------------------------------------------------------ +function IsCollinear(const pt1, pt2, pt3: TPoint64): Boolean; +var + a,b,c,d: double; // avoids potential int overflow +begin + a := (pt2.X - pt1.X); + b := (pt2.Y - pt1.Y); + c := (pt3.X - pt2.X); + d := (pt3.Y - pt2.Y); + result := a * d = b * c; +end; +//------------------------------------------------------------------------------ + function CrossProduct(const pt1, pt2, pt3: TPoint64): double; begin result := CrossProduct( @@ -1961,14 +1974,14 @@ function CleanPath(const path: TPath64): TPath64; Result := nil; len := Length(path); while (len > 2) and - (CrossProduct(path[len-2], path[len-1], path[0]) = 0) do dec(len); + (IsCollinear(path[len-2], path[len-1], path[0])) do dec(len); SetLength(Result, len); if (len < 2) then Exit; prev := path[len -1]; j := 0; for i := 0 to len -2 do begin - if CrossProduct(prev, path[i], path[i+1]) = 0 then Continue; + if IsCollinear(prev, path[i], path[i+1]) then Continue; Result[j] := path[i]; inc(j); prev := path[i]; @@ -1978,6 +1991,14 @@ function CleanPath(const path: TPath64): TPath64; end; //------------------------------------------------------------------------------ +function GetSign(const val: double): integer; {$IFDEF INLINING} inline; {$ENDIF} +begin + if val = 0 then Result := 0 + else if val < 0 then Result := -1 + else Result := 1; +end; +//------------------------------------------------------------------------------ + function SegmentsIntersect(const s1a, s1b, s2a, s2b: TPoint64; inclusive: Boolean): boolean; var @@ -1997,35 +2018,11 @@ function SegmentsIntersect(const s1a, s1b, s2a, s2b: TPoint64; (res3 <> 0) or (res4 <> 0); // ensures not collinear end else begin - result := (CrossProduct(s1a, s2a, s2b) * CrossProduct(s1b, s2a, s2b) < 0) and - (CrossProduct(s2a, s1a, s1b) * CrossProduct(s2b, s1a, s1b) < 0); - end; -end; -//------------------------------------------------------------------------------ - -function __Trunc(val: double): Int64; {$IFDEF INLINE} inline; {$ENDIF} -var - exp: integer; - i64: UInt64 absolute val; -const - shl51: UInt64 = UInt64(1) shl 51; -begin - Result := 0; - if i64 = 0 then Exit; - exp := Integer(Cardinal(i64 shr 52) and $7FF) - 1023; - //nb: when exp == 1024 then val == INF or NAN. - if exp < 0 then - Exit - else if exp > 52 then - begin - Result := ((i64 and $1FFFFFFFFFFFFF) shl (exp - 52)) or (UInt64(1) shl exp) - end else - begin - Result := ((i64 and $1FFFFFFFFFFFFF) shr (52 - exp)) or (UInt64(1) shl exp); - //the following line will round - //if (i64 and (shl51 shr (exp)) <> 0) then inc(Result); + result := (GetSign(CrossProduct(s1a, s2a, s2b)) * + GetSign(CrossProduct(s1b, s2a, s2b)) < 0) and + (GetSign(CrossProduct(s2a, s1a, s1b)) * + GetSign(CrossProduct(s2b, s1a, s1b)) < 0); end; - if val < 0 then Result := -Result; end; //------------------------------------------------------------------------------ diff --git a/Delphi/Clipper2Lib/Clipper.Engine.pas b/Delphi/Clipper2Lib/Clipper.Engine.pas index 6961e913..950e847c 100644 --- a/Delphi/Clipper2Lib/Clipper.Engine.pas +++ b/Delphi/Clipper2Lib/Clipper.Engine.pas @@ -2,7 +2,7 @@ (******************************************************************************* * Author : Angus Johnson * -* Date : 17 April 2024 * +* Date : 27 April 2024 * * Website : http://www.angusj.com * * Copyright : Angus Johnson 2010-2024 * * Purpose : This is the main polygon clipping module * @@ -1838,8 +1838,8 @@ function IsValidAelOrder(resident, newcomer: PActive): Boolean; // resident must also have just been inserted else if IsLeftBound(resident) <> newcomerIsLeft then Result := newcomerIsLeft - else if (CrossProduct(PrevPrevVertex(resident).pt, - resident.bot, resident.top) = 0) then + else if IsCollinear(PrevPrevVertex(resident).pt, + resident.bot, resident.top) then Result := true else // otherwise compare turning direction of the alternate bound @@ -2109,7 +2109,7 @@ procedure TClipperBase.CleanCollinear(outRec: POutRec); // a duplicate point OR // not preserving collinear points OR // is a 180 degree 'spike' - if (CrossProduct(op2.prev.pt, op2.pt, op2.next.pt) = 0) and + if IsCollinear(op2.prev.pt, op2.pt, op2.next.pt) and (PointsEqual(op2.pt,op2.prev.pt) or PointsEqual(op2.pt,op2.next.pt) or not FPreserveCollinear or @@ -2385,7 +2385,7 @@ procedure TClipperBase.CheckJoinLeft(e: PActive; if PerpendicDistFromLineSqrd(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 not IsCollinear(e.top, pt, prev.top) then Exit; if (e.outrec.idx = prev.outrec.idx) then AddLocalMaxPoly(prev, e, pt) @@ -2417,7 +2417,7 @@ procedure TClipperBase.CheckJoinRight(e: PActive; end else if (e.currX <> next.currX) then Exit; - if (CrossProduct(e.top, pt, next.top) <> 0) then Exit; + if not IsCollinear(e.top, pt, next.top) 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.RectClip.pas b/Delphi/Clipper2Lib/Clipper.RectClip.pas index 4e2da7dc..8a0cf05c 100644 --- a/Delphi/Clipper2Lib/Clipper.RectClip.pas +++ b/Delphi/Clipper2Lib/Clipper.RectClip.pas @@ -2,7 +2,7 @@ (******************************************************************************* * Author : Angus Johnson * -* Date : 14 February 2024 * +* Date : 27 April 2024 * * Website : http://www.angusj.com * * Copyright : Angus Johnson 2010-2024 * * Purpose : FAST rectangular clipping * @@ -840,7 +840,7 @@ procedure TRectClip64.CheckEdges; op2 := op; repeat - if (CrossProduct(op2.prev.pt, op2.pt, op2.next.pt) = 0) then + if IsCollinear(op2.prev.pt, op2.pt, op2.next.pt) then begin if op2 = op then begin @@ -1082,7 +1082,7 @@ function TRectClip64.GetPath(resultIdx: integer): TPath64; op2 := op.next; while Assigned(op2) and (op2 <> op) do begin - if (CrossProduct(op2.prev.pt, op2.pt, op2.next.pt) = 0) then + if IsCollinear(op2.prev.pt, op2.pt, op2.next.pt) then begin op := op2.prev; op2 := DisposeOp(op2); diff --git a/Delphi/Clipper2Lib/Clipper.pas b/Delphi/Clipper2Lib/Clipper.pas index 1c36223b..a91a2552 100644 --- a/Delphi/Clipper2Lib/Clipper.pas +++ b/Delphi/Clipper2Lib/Clipper.pas @@ -2,9 +2,9 @@ (******************************************************************************* * Author : Angus Johnson * -* Date : 21 December 2023 * +* Date : 27 April 2024 * * Website : http://www.angusj.com * -* Copyright : Angus Johnson 2010-2023 * +* Copyright : Angus Johnson 2010-2024 * * Purpose : This module provides a simple interface to the Clipper Library * * License : http://www.boost.org/LICENSE_1_0.txt * *******************************************************************************) @@ -753,9 +753,9 @@ function TrimCollinear(const p: TPath64; isOpenPath: Boolean = false): TPath64; if not isOpenPath then begin while (i < len -1) and - (CrossProduct(p[len -1], p[i], p[i+1]) = 0) do inc(i); + IsCollinear(p[len -1], p[i], p[i+1]) do inc(i); while (i < len -1) and - (CrossProduct(p[len -2], p[len -1], p[i]) = 0) do dec(len); + IsCollinear(p[len -2], p[len -1], p[i]) do dec(len); end; if (len - i < 3) then begin @@ -770,7 +770,7 @@ function TrimCollinear(const p: TPath64; isOpenPath: Boolean = false): TPath64; Result[0] := p[i]; j := 0; for i := i+1 to len -2 do - if CrossProduct(result[j], p[i], p[i+1]) <> 0 then + if not IsCollinear(result[j], p[i], p[i+1]) then begin inc(j); result[j] := p[i]; @@ -781,14 +781,14 @@ function TrimCollinear(const p: TPath64; isOpenPath: Boolean = false): TPath64; inc(j); result[j] := p[len-1]; end - else if CrossProduct(result[j], p[len-1], result[0]) <> 0 then + else if not IsCollinear(result[j], p[len-1], result[0]) then begin inc(j); result[j] := p[len-1]; end else begin while (j > 1) and - (CrossProduct(result[j-1], result[j], result[0]) = 0) do dec(j); + IsCollinear(result[j-1], result[j], result[0]) do dec(j); if j < 2 then j := -1; end; SetLength(Result, j +1); From 33ec483d38197f7c461282ef0aed96fc51c39ad6 Mon Sep 17 00:00:00 2001 From: angusj Date: Sat, 27 Apr 2024 22:04:21 +1000 Subject: [PATCH 51/64] clipper.rectclip.cpp - fixed bug in previous commit --- CPP/Clipper2Lib/src/clipper.rectclip.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CPP/Clipper2Lib/src/clipper.rectclip.cpp b/CPP/Clipper2Lib/src/clipper.rectclip.cpp index c5ae9e2e..3fc6fac2 100644 --- a/CPP/Clipper2Lib/src/clipper.rectclip.cpp +++ b/CPP/Clipper2Lib/src/clipper.rectclip.cpp @@ -824,7 +824,7 @@ namespace Clipper2Lib { OutPt2* op2 = op->next; while (op2 && op2 != op) { - if (!IsCollinear(op2->prev->pt, + if (IsCollinear(op2->prev->pt, op2->pt, op2->next->pt)) { op = op2->prev; From 3963baeb70b64aaec299dedc0bb3611d9e7ab66e Mon Sep 17 00:00:00 2001 From: Juha Reunanen Date: Wed, 1 May 2024 23:49:20 +0300 Subject: [PATCH 52/64] Remove typecasting in `IsCollinear` (#825) * Remove typecasting in `IsCollinear` * Update the C# and Delphi versions as well * In the C++ version, multiply unsigned numbers (so that the result just wraps, and there is no undefined behavior) * Add test case for `IsCollinear` --- CPP/CMakeLists.txt | 1 + CPP/Clipper2Lib/include/clipper2/clipper.core.h | 8 ++++---- CPP/Tests/TestIsCollinear.cpp | 13 +++++++++++++ CSharp/Clipper2Lib/Clipper.Core.cs | 5 ++--- Delphi/Clipper2Lib/Clipper.Core.pas | 2 +- 5 files changed, 21 insertions(+), 8 deletions(-) create mode 100644 CPP/Tests/TestIsCollinear.cpp diff --git a/CPP/CMakeLists.txt b/CPP/CMakeLists.txt index 2ad05657..1e8b0271 100644 --- a/CPP/CMakeLists.txt +++ b/CPP/CMakeLists.txt @@ -228,6 +228,7 @@ endif() set(ClipperTests_SRC Tests/TestExportHeaders.cpp + Tests/TestIsCollinear.cpp Tests/TestLines.cpp Tests/TestOffsets.cpp Tests/TestOffsetOrientation.cpp diff --git a/CPP/Clipper2Lib/include/clipper2/clipper.core.h b/CPP/Clipper2Lib/include/clipper2/clipper.core.h index 4e9be2f5..598db49a 100644 --- a/CPP/Clipper2Lib/include/clipper2/clipper.core.h +++ b/CPP/Clipper2Lib/include/clipper2/clipper.core.h @@ -663,10 +663,10 @@ namespace Clipper2Lib inline bool IsCollinear(const Point& pt1, const Point& sharedPt, const Point& pt2) // #777 { - const double a = static_cast(sharedPt.x - pt1.x); - const double b = static_cast(pt2.y - sharedPt.y); - const double c = static_cast(sharedPt.y - pt1.y); - const double d = static_cast(pt2.x - sharedPt.x); + const auto a = static_cast(sharedPt.x - pt1.x); + const auto b = static_cast(pt2.y - sharedPt.y); + const auto c = static_cast(sharedPt.y - pt1.y); + const auto d = static_cast(pt2.x - sharedPt.x); return a * b == c * d; } diff --git a/CPP/Tests/TestIsCollinear.cpp b/CPP/Tests/TestIsCollinear.cpp new file mode 100644 index 00000000..72c04780 --- /dev/null +++ b/CPP/Tests/TestIsCollinear.cpp @@ -0,0 +1,13 @@ +#include +#include "clipper2/clipper.core.h" + +TEST(Clipper2Tests, TestIsCollinear) { + // a large integer not representable by double + const int64_t i = 9007199254740993; + + const Clipper2Lib::Point64 pt1(0, 0); + const Clipper2Lib::Point64 sharedPt(i, i * 10); + const Clipper2Lib::Point64 pt2(i * 10, i * 100); + + EXPECT_TRUE(IsCollinear(pt1, sharedPt, pt2)); +} diff --git a/CSharp/Clipper2Lib/Clipper.Core.cs b/CSharp/Clipper2Lib/Clipper.Core.cs index 0979e7df..ea840abf 100644 --- a/CSharp/Clipper2Lib/Clipper.Core.cs +++ b/CSharp/Clipper2Lib/Clipper.Core.cs @@ -604,9 +604,8 @@ internal static double CrossProduct(Point64 pt1, Point64 pt2, Point64 pt3) [MethodImpl(MethodImplOptions.AggressiveInlining)] internal static bool IsCollinear(Point64 pt1, Point64 pt2, Point64 pt3) { - // typecast to double to avoid potential int overflow - return (double) (pt2.X - pt1.X) * (double) (pt3.Y - pt2.Y) == - (double) (pt2.Y - pt1.Y) * (double) (pt3.X - pt2.X); + return (pt2.X - pt1.X) * (pt3.Y - pt2.Y) == + (pt2.Y - pt1.Y) * (pt3.X - pt2.X); } [MethodImpl(MethodImplOptions.AggressiveInlining)] diff --git a/Delphi/Clipper2Lib/Clipper.Core.pas b/Delphi/Clipper2Lib/Clipper.Core.pas index a306a9ab..71aee7c0 100644 --- a/Delphi/Clipper2Lib/Clipper.Core.pas +++ b/Delphi/Clipper2Lib/Clipper.Core.pas @@ -1866,7 +1866,7 @@ function IsPositive(const path: TPathD): Boolean; function IsCollinear(const pt1, pt2, pt3: TPoint64): Boolean; var - a,b,c,d: double; // avoids potential int overflow + a,b,c,d: Int64; begin a := (pt2.X - pt1.X); b := (pt2.Y - pt1.Y); From bf561697d555867794ca2d9c15507e1d429d5251 Mon Sep 17 00:00:00 2001 From: angusj Date: Tue, 7 May 2024 08:28:18 +1000 Subject: [PATCH 53/64] Clipper.RectClip.cs - fixed a bug in C# RectClipLines (#828) Clipper.Core.pas - added overflow management for IsCollinear function. --- CSharp/Clipper2Lib/Clipper.RectClip.cs | 5 +++-- Delphi/Clipper2Lib/Clipper.Core.pas | 14 +++++++------- Delphi/Clipper2Lib/Clipper.pas | 3 ++- 3 files changed, 12 insertions(+), 10 deletions(-) diff --git a/CSharp/Clipper2Lib/Clipper.RectClip.cs b/CSharp/Clipper2Lib/Clipper.RectClip.cs index a8d1047c..43fcf90e 100644 --- a/CSharp/Clipper2Lib/Clipper.RectClip.cs +++ b/CSharp/Clipper2Lib/Clipper.RectClip.cs @@ -1,6 +1,6 @@ /******************************************************************************* * Author : Angus Johnson * -* Date : 27 April 2024 * +* Date : 7 May 2024 * * Website : http://www.angusj.com * * Copyright : Angus Johnson 2010-2024 * * Purpose : FAST rectangular clipping * @@ -1014,6 +1014,7 @@ private void ExecuteInternal(Path64 path) if (i > highI) { foreach (Point64 pt in path) Add(pt); + return; } if (prev == Location.inside) loc = Location.inside; i = 1; @@ -1050,7 +1051,7 @@ private void ExecuteInternal(Path64 path) // intersect pt but we'll also need the first intersect pt (ip2) crossingLoc = prev; GetIntersection(rectPath_, prevPt, path[i], ref crossingLoc, out Point64 ip2); - Add(ip2); + Add(ip2, true); Add(ip); } else // path must be exiting rect diff --git a/Delphi/Clipper2Lib/Clipper.Core.pas b/Delphi/Clipper2Lib/Clipper.Core.pas index 71aee7c0..84b7a14d 100644 --- a/Delphi/Clipper2Lib/Clipper.Core.pas +++ b/Delphi/Clipper2Lib/Clipper.Core.pas @@ -2,7 +2,7 @@ (******************************************************************************* * Author : Angus Johnson * -* Date : 27 April 2024 * +* Date : 3 May 2024 * * Website : http://www.angusj.com * * Copyright : Angus Johnson 2010-2024 * * Purpose : Core Clipper Library module * @@ -1864,16 +1864,16 @@ function IsPositive(const path: TPathD): Boolean; end; //------------------------------------------------------------------------------ +{$OVERFLOWCHECKS OFF} function IsCollinear(const pt1, pt2, pt3: TPoint64): Boolean; var - a,b,c,d: Int64; + a,b: Int64; begin - a := (pt2.X - pt1.X); - b := (pt2.Y - pt1.Y); - c := (pt3.X - pt2.X); - d := (pt3.Y - pt2.Y); - result := a * d = b * c; + a := (pt2.X - pt1.X) * (pt3.Y - pt2.Y); + b := (pt2.Y - pt1.Y) * (pt3.X - pt2.X); + result := a = b; end; +{$OVERFLOWCHECKS ON} //------------------------------------------------------------------------------ function CrossProduct(const pt1, pt2, pt3: TPoint64): double; diff --git a/Delphi/Clipper2Lib/Clipper.pas b/Delphi/Clipper2Lib/Clipper.pas index a91a2552..0c2fe87e 100644 --- a/Delphi/Clipper2Lib/Clipper.pas +++ b/Delphi/Clipper2Lib/Clipper.pas @@ -2,7 +2,7 @@ (******************************************************************************* * Author : Angus Johnson * -* Date : 27 April 2024 * +* Date : 7 May 2024 * * Website : http://www.angusj.com * * Copyright : Angus Johnson 2010-2024 * * Purpose : This module provides a simple interface to the Clipper Library * @@ -42,6 +42,7 @@ interface frNonZero = Clipper.Core.frNonZero; frPositive = Clipper.Core.frPositive; frNegative = Clipper.Core.frNegative; + jtBevel = Clipper.Offset.jtBevel; jtSquare = Clipper.Offset.jtSquare; jtRound = Clipper.Offset.jtRound; jtMiter = Clipper.Offset.jtMiter; From 75cfda6fe7b6d604c1605f88adb84205ef1c3ea4 Mon Sep 17 00:00:00 2001 From: angusj Date: Tue, 7 May 2024 15:55:59 +1000 Subject: [PATCH 54/64] clipper.core.h - fixed GetBounds compiler warning (#830) --- .../include/clipper2/clipper.core.h | 26 +++++++++---------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/CPP/Clipper2Lib/include/clipper2/clipper.core.h b/CPP/Clipper2Lib/include/clipper2/clipper.core.h index 598db49a..1b7f9119 100644 --- a/CPP/Clipper2Lib/include/clipper2/clipper.core.h +++ b/CPP/Clipper2Lib/include/clipper2/clipper.core.h @@ -1,6 +1,6 @@ /******************************************************************************* * Author : Angus Johnson * -* Date : 27 April 2024 * +* Date : 7 May 2024 * * Website : http://www.angusj.com * * Copyright : Angus Johnson 2010-2024 * * Purpose : Core Clipper Library structures and functions * @@ -382,10 +382,10 @@ namespace Clipper2Lib } else { - result.left = rect.left * scale; - result.top = rect.top * scale; - result.right = rect.right * scale; - result.bottom = rect.bottom * scale; + result.left = static_cast(rect.left * scale); + result.top = static_cast(rect.top * scale); + result.right = static_cast(rect.right * scale); + result.bottom = static_cast(rect.bottom * scale); } return result; } @@ -437,10 +437,10 @@ namespace Clipper2Lib T ymax = std::numeric_limits::lowest(); for (const auto& p : path) { - if (p.x < xmin) xmin = p.x; - if (p.x > xmax) xmax = p.x; - if (p.y < ymin) ymin = p.y; - if (p.y > ymax) ymax = p.y; + if (p.x < xmin) xmin = static_cast(p.x); + if (p.x > xmax) xmax = static_cast(p.x); + if (p.y < ymin) ymin = static_cast(p.y); + if (p.y > ymax) ymax = static_cast(p.y); } return Rect(xmin, ymin, xmax, ymax); } @@ -455,10 +455,10 @@ namespace Clipper2Lib for (const Path& path : paths) for (const Point& p : path) { - if (p.x < xmin) xmin = p.x; - if (p.x > xmax) xmax = p.x; - if (p.y < ymin) ymin = p.y; - if (p.y > ymax) ymax = p.y; + if (p.x < xmin) xmin = static_cast(p.x); + if (p.x > xmax) xmax = static_cast(p.x); + if (p.y < ymin) ymin = static_cast(p.y); + if (p.y > ymax) ymax = static_cast(p.y); } return Rect(xmin, ymin, xmax, ymax); } From 3b60aeac0507a1e9ca3779766c6cc7fa9d7c90a8 Mon Sep 17 00:00:00 2001 From: philstopford Date: Fri, 10 May 2024 00:28:16 -0500 Subject: [PATCH 55/64] Restore default constructors (#833) Required for hash functions to work --- CSharp/Clipper2Lib/Clipper.Core.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/CSharp/Clipper2Lib/Clipper.Core.cs b/CSharp/Clipper2Lib/Clipper.Core.cs index ea840abf..833f15a7 100644 --- a/CSharp/Clipper2Lib/Clipper.Core.cs +++ b/CSharp/Clipper2Lib/Clipper.Core.cs @@ -473,6 +473,7 @@ public readonly PathD AsPath() public class Path64 : List { + public Path64() : base() { } public Path64(int capacity = 0) : base(capacity) { } public Path64(IEnumerable path) : base(path) { } public override string ToString() @@ -486,6 +487,7 @@ public override string ToString() public class Paths64 : List { + public Paths64() : base() { } public Paths64(int capacity = 0) : base(capacity) { } public Paths64(IEnumerable paths) : base(paths) { } public override string ToString() @@ -499,6 +501,7 @@ public override string ToString() public class PathD : List { + public PathD() : base() { } public PathD(int capacity = 0) : base(capacity) { } public PathD(IEnumerable path) : base(path) { } public string ToString(int precision = 2) @@ -512,6 +515,7 @@ public string ToString(int precision = 2) public class PathsD : List { + public PathsD() : base() { } public PathsD(int capacity = 0) : base(capacity) { } public PathsD(IEnumerable paths) : base(paths) { } public string ToString(int precision = 2) @@ -784,4 +788,4 @@ public static PointInPolygonResult PointInPolygon(Point64 pt, Path64 polygon) } // InternalClipper -} // namespace \ No newline at end of file +} // namespace From 6e4feb8eea514ad06fb7c9636ebb8f3efb31d88e Mon Sep 17 00:00:00 2001 From: Juha Reunanen Date: Sat, 11 May 2024 13:28:47 +0300 Subject: [PATCH 56/64] `IsCollinear`: detect and handle integer multiplication wrapping (version 2) (#834) * Add test case that fails * IsCollinear: detect and handle integer multiplication wrapping --- .../include/clipper2/clipper.core.h | 50 +++++++++++++++++-- CPP/Tests/TestIsCollinear.cpp | 18 ++++++- 2 files changed, 62 insertions(+), 6 deletions(-) diff --git a/CPP/Clipper2Lib/include/clipper2/clipper.core.h b/CPP/Clipper2Lib/include/clipper2/clipper.core.h index 1b7f9119..6daa3e33 100644 --- a/CPP/Clipper2Lib/include/clipper2/clipper.core.h +++ b/CPP/Clipper2Lib/include/clipper2/clipper.core.h @@ -659,15 +659,55 @@ namespace Clipper2Lib CheckPrecisionRange(precision, error_code); } + inline int Sign(int64_t x) + { + return (x > 0) - (x < 0); + } + + inline uint64_t CalculateCarry(uint64_t a, uint64_t b) + { + // adapted from: https://stackoverflow.com/a/1815391/19254 + const uint64_t a0 = a & ((1ull << 32) - 1); + const uint64_t a1 = a >> 32; + const uint64_t b0 = b & ((1ull << 32) - 1); + const uint64_t b1 = b >> 32; + const uint64_t d11 = a1 * b0 + (a0 * b0 >> 32); + const uint64_t d12 = a0 * b1; + const uint64_t c1 = (d11 > (std::numeric_limits::max)() - d12) ? 1 : 0; + return a1 * b1 + c1; + } + + // returns true if (and only if) a * b == c * d + inline bool ProductIsEqual(int64_t a, int64_t b, int64_t c, int64_t d) + { + const auto abs_a = static_cast(std::abs(a)); + const auto abs_b = static_cast(std::abs(b)); + const auto abs_c = static_cast(std::abs(c)); + const auto abs_d = static_cast(std::abs(d)); + + // NB: the multiplication here may potentially wrap + const auto abs_ab = abs_a * abs_b; + const auto abs_cd = abs_c * abs_d; + + const auto sign_ab = Sign(a) * Sign(b); + const auto sign_cd = Sign(c) * Sign(d); + + const auto carry_ab = CalculateCarry(abs_a, abs_b); + const auto carry_cd = CalculateCarry(abs_c, abs_d); + + return abs_ab == abs_cd && sign_ab == sign_cd && carry_ab == carry_cd; + } + template inline bool IsCollinear(const Point& pt1, const Point& sharedPt, const Point& pt2) // #777 { - const auto a = static_cast(sharedPt.x - pt1.x); - const auto b = static_cast(pt2.y - sharedPt.y); - const auto c = static_cast(sharedPt.y - pt1.y); - const auto d = static_cast(pt2.x - sharedPt.x); - return a * b == c * d; + const auto a = sharedPt.x - pt1.x; + const auto b = pt2.y - sharedPt.y; + const auto c = sharedPt.y - pt1.y; + const auto d = pt2.x - sharedPt.x; + + return ProductIsEqual(a, b, c, d); } template diff --git a/CPP/Tests/TestIsCollinear.cpp b/CPP/Tests/TestIsCollinear.cpp index 72c04780..7f3908f2 100644 --- a/CPP/Tests/TestIsCollinear.cpp +++ b/CPP/Tests/TestIsCollinear.cpp @@ -1,5 +1,5 @@ #include -#include "clipper2/clipper.core.h" +#include "clipper2/clipper.h" TEST(Clipper2Tests, TestIsCollinear) { // a large integer not representable by double @@ -11,3 +11,19 @@ TEST(Clipper2Tests, TestIsCollinear) { EXPECT_TRUE(IsCollinear(pt1, sharedPt, pt2)); } + +TEST(Clipper2Tests, TestIsCollinear2) { + // see https://github.com/AngusJohnson/Clipper2/issues/831 + const int64_t i = 0x4000000000000; + const Clipper2Lib::Path64 subject = { + Clipper2Lib::Point64(-i, -i), + Clipper2Lib::Point64( i, -i), + Clipper2Lib::Point64(-i, i), + Clipper2Lib::Point64( i, i) + }; + Clipper2Lib::Clipper64 clipper; + clipper.AddSubject({ subject }); + Clipper2Lib::Paths64 solution; + clipper.Execute(Clipper2Lib::ClipType::Union, Clipper2Lib::FillRule::EvenOdd, solution); + EXPECT_EQ(solution.size(), 2); +} From 2b9b7ce73aa9a8eae67380848119e9730e555008 Mon Sep 17 00:00:00 2001 From: angusj Date: Sat, 11 May 2024 21:33:37 +1000 Subject: [PATCH 57/64] Minor code tidies --- .../include/clipper2/clipper.core.h | 32 +++++++------ CSharp/Clipper2Lib/Clipper.Core.cs | 21 ++++---- CSharp/Clipper2Lib/Clipper.cs | 48 +++++++++++++++++-- 3 files changed, 73 insertions(+), 28 deletions(-) diff --git a/CPP/Clipper2Lib/include/clipper2/clipper.core.h b/CPP/Clipper2Lib/include/clipper2/clipper.core.h index 6daa3e33..b44ce649 100644 --- a/CPP/Clipper2Lib/include/clipper2/clipper.core.h +++ b/CPP/Clipper2Lib/include/clipper2/clipper.core.h @@ -1,6 +1,6 @@ /******************************************************************************* * Author : Angus Johnson * -* Date : 7 May 2024 * +* Date : 11 May 2024 * * Website : http://www.angusj.com * * Copyright : Angus Johnson 2010-2024 * * Purpose : Core Clipper Library structures and functions * @@ -664,17 +664,19 @@ namespace Clipper2Lib return (x > 0) - (x < 0); } - inline uint64_t CalculateCarry(uint64_t a, uint64_t b) + inline uint64_t CalcOverflowCarry(uint64_t a, uint64_t b) { - // adapted from: https://stackoverflow.com/a/1815391/19254 - const uint64_t a0 = a & ((1ull << 32) - 1); - const uint64_t a1 = a >> 32; - const uint64_t b0 = b & ((1ull << 32) - 1); - const uint64_t b1 = b >> 32; - const uint64_t d11 = a1 * b0 + (a0 * b0 >> 32); - const uint64_t d12 = a0 * b1; - const uint64_t c1 = (d11 > (std::numeric_limits::max)() - d12) ? 1 : 0; - return a1 * b1 + c1; + const uint64_t aHi = a & 0xFFFFFFFF00000000; + const uint64_t aLo = a & 0xFFFFFFFF; + const uint64_t bHi = b & 0xFFFFFFFF00000000; + const uint64_t bLo = b & 0xFFFFFFFF; + // a * b == (aHi + aLo) * (bHi + bLo) + // a * b == (aHi * bHi) + (aHi * bLo) + (aLo * bHi) + (aLo * bLo) + uint64_t carry = (aHi >> 32) * (bHi >> 32); + // but since (aHi * bLo) + (aLo * bHi) + (aLo * bLo) may also (just) overflow + // do safely ... if ((aHi * bLo) + (aLo * bHi) + (aLo * bLo) > MAX_UINT64) ++carry + if ((aLo * bHi) + (aLo * bLo) > ((std::numeric_limits::max)() - (aHi * bLo))) ++carry; + return carry; } // returns true if (and only if) a * b == c * d @@ -685,16 +687,16 @@ namespace Clipper2Lib const auto abs_c = static_cast(std::abs(c)); const auto abs_d = static_cast(std::abs(d)); - // NB: the multiplication here may potentially wrap + // the multiplications here can potentially overflow + // but any overflows will be compared further below. const auto abs_ab = abs_a * abs_b; const auto abs_cd = abs_c * abs_d; const auto sign_ab = Sign(a) * Sign(b); const auto sign_cd = Sign(c) * Sign(d); - const auto carry_ab = CalculateCarry(abs_a, abs_b); - const auto carry_cd = CalculateCarry(abs_c, abs_d); - + const auto carry_ab = CalcOverflowCarry(abs_a, abs_b); + const auto carry_cd = CalcOverflowCarry(abs_c, abs_d); return abs_ab == abs_cd && sign_ab == sign_cd && carry_ab == carry_cd; } diff --git a/CSharp/Clipper2Lib/Clipper.Core.cs b/CSharp/Clipper2Lib/Clipper.Core.cs index 833f15a7..af0bee16 100644 --- a/CSharp/Clipper2Lib/Clipper.Core.cs +++ b/CSharp/Clipper2Lib/Clipper.Core.cs @@ -1,6 +1,6 @@ /******************************************************************************* * Author : Angus Johnson * -* Date : 27 April 2024 * +* Date : 10 May 2024 * * Website : http://www.angusj.com * * Copyright : Angus Johnson 2010-2024 * * Purpose : Core structures and functions for the Clipper Library * @@ -480,7 +480,8 @@ public override string ToString() { string s = ""; foreach (Point64 p in this) - s = s + p.ToString() + " "; + s = s + p.ToString() + ", "; + if (s != "") s = s.Remove(s.Length - 2); return s; } } @@ -509,6 +510,7 @@ public string ToString(int precision = 2) string s = ""; foreach (PointD p in this) s = s + p.ToString(precision) + ", "; + if (s != "") s = s.Remove(s.Length - 2); return s; } } @@ -577,6 +579,13 @@ public static class InternalClipper private static readonly string precision_range_error = "Error: Precision is out of range."; + public static double CrossProduct(Point64 pt1, Point64 pt2, Point64 pt3) + { + // typecast to double to avoid potential int overflow + return ((double) (pt2.X - pt1.X) * (pt3.Y - pt2.Y) - + (double) (pt2.Y - pt1.Y) * (pt3.X - pt2.X)); + } + #if USINGZ public static Path64 SetZ(Path64 path, long Z) { @@ -597,14 +606,6 @@ internal static bool IsAlmostZero(double value) return (Math.Abs(value) <= floatingPointTolerance); } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - internal static double CrossProduct(Point64 pt1, Point64 pt2, Point64 pt3) - { - // typecast to double to avoid potential int overflow - return ((double) (pt2.X - pt1.X) * (pt3.Y - pt2.Y) - - (double) (pt2.Y - pt1.Y) * (pt3.X - pt2.X)); - } - [MethodImpl(MethodImplOptions.AggressiveInlining)] internal static bool IsCollinear(Point64 pt1, Point64 pt2, Point64 pt3) { diff --git a/CSharp/Clipper2Lib/Clipper.cs b/CSharp/Clipper2Lib/Clipper.cs index 6722bbd2..bb15959b 100644 --- a/CSharp/Clipper2Lib/Clipper.cs +++ b/CSharp/Clipper2Lib/Clipper.cs @@ -1,6 +1,6 @@ /******************************************************************************* * Author : Angus Johnson * -* Date : 27 April 2024 * +* Date : 10 May 2024 * * Website : http://www.angusj.com * * Copyright : Angus Johnson 2010-2024 * * Purpose : This module contains simple functions that will likely cover * @@ -654,9 +654,51 @@ public static PathD MakePathZ(double[] arr) [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static double Sqr(double value) + public static double Sqr(double val) { - return value * value; + return val * val; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static double Sqr(long val) + { + return (double) val * (double) val; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static double DistanceSqr(Point64 pt1, Point64 pt2) + { + return Sqr(pt1.X - pt2.X) + Sqr(pt1.Y - pt2.Y); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Point64 MidPoint(Point64 pt1, Point64 pt2) + { + return new Point64((pt1.X + pt2.X) / 2, (pt1.Y + pt2.Y) / 2); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static PointD MidPoint(PointD pt1, PointD pt2) + { + return new PointD((pt1.x + pt2.x) / 2, (pt1.y + pt2.y) / 2); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void InflateRect(ref Rect64 rec, int dx, int dy) + { + rec.left -= dx; + rec.right += dx; + rec.top -= dy; + rec.bottom += dy; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void InflateRect(ref RectD rec, double dx, double dy) + { + rec.left -= dx; + rec.right += dx; + rec.top -= dy; + rec.bottom += dy; } [MethodImpl(MethodImplOptions.AggressiveInlining)] From fdf3700f4211d5fe9637a6c53d10af1647e3fe8c Mon Sep 17 00:00:00 2001 From: angusj Date: Sun, 12 May 2024 07:12:15 +1000 Subject: [PATCH 58/64] clipper.core.h - fixed CalcOverflowCarry function --- CPP/Clipper2Lib/include/clipper2/clipper.core.h | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/CPP/Clipper2Lib/include/clipper2/clipper.core.h b/CPP/Clipper2Lib/include/clipper2/clipper.core.h index b44ce649..7c0ebc26 100644 --- a/CPP/Clipper2Lib/include/clipper2/clipper.core.h +++ b/CPP/Clipper2Lib/include/clipper2/clipper.core.h @@ -666,17 +666,18 @@ namespace Clipper2Lib inline uint64_t CalcOverflowCarry(uint64_t a, uint64_t b) { - const uint64_t aHi = a & 0xFFFFFFFF00000000; const uint64_t aLo = a & 0xFFFFFFFF; - const uint64_t bHi = b & 0xFFFFFFFF00000000; + //const uint64_t aHi = a & 0xFFFFFFFF00000000; + const uint64_t aHiShr = a >> 32; + const uint64_t bLo = b & 0xFFFFFFFF; + //const uint64_t bHi = b & 0xFFFFFFFF00000000; + const uint64_t bHiShr = b >> 32; + // a * b == (aHi + aLo) * (bHi + bLo) // a * b == (aHi * bHi) + (aHi * bLo) + (aLo * bHi) + (aLo * bLo) - uint64_t carry = (aHi >> 32) * (bHi >> 32); - // but since (aHi * bLo) + (aLo * bHi) + (aLo * bLo) may also (just) overflow - // do safely ... if ((aHi * bLo) + (aLo * bHi) + (aLo * bLo) > MAX_UINT64) ++carry - if ((aLo * bHi) + (aLo * bLo) > ((std::numeric_limits::max)() - (aHi * bLo))) ++carry; - return carry; + //int overflow of 64bit a * 64bit b ==> + return aHiShr * bHiShr + ((aHiShr * bLo) >> 32) + ((bHiShr * aLo) >> 32); } // returns true if (and only if) a * b == c * d From 235879be5704632c3464d39355e900151c810047 Mon Sep 17 00:00:00 2001 From: angusj Date: Sun, 12 May 2024 10:01:00 +1000 Subject: [PATCH 59/64] Updated IsCollinear functions in C# and Delphi (#834) Otherwise a minor code tidy and updated code comments --- .../include/clipper2/clipper.core.h | 45 +++++------ CSharp/Clipper2Lib/Clipper.Core.cs | 63 ++++++++++++++-- Delphi/Clipper2Lib/Clipper.Core.pas | 74 +++++++++++++++++-- 3 files changed, 148 insertions(+), 34 deletions(-) diff --git a/CPP/Clipper2Lib/include/clipper2/clipper.core.h b/CPP/Clipper2Lib/include/clipper2/clipper.core.h index 7c0ebc26..c73476de 100644 --- a/CPP/Clipper2Lib/include/clipper2/clipper.core.h +++ b/CPP/Clipper2Lib/include/clipper2/clipper.core.h @@ -1,6 +1,6 @@ /******************************************************************************* * Author : Angus Johnson * -* Date : 11 May 2024 * +* Date : 12 May 2024 * * Website : http://www.angusj.com * * Copyright : Angus Johnson 2010-2024 * * Purpose : Core Clipper Library structures and functions * @@ -659,42 +659,43 @@ namespace Clipper2Lib CheckPrecisionRange(precision, error_code); } - inline int Sign(int64_t x) + inline int TriSign(int64_t x) // returns 0, 1 or -1 { - return (x > 0) - (x < 0); + return (x > 0) - (x < 0); } - inline uint64_t CalcOverflowCarry(uint64_t a, uint64_t b) + inline uint64_t CalcOverflowCarry(uint64_t a, uint64_t b) // #834 { - const uint64_t aLo = a & 0xFFFFFFFF; - //const uint64_t aHi = a & 0xFFFFFFFF00000000; - const uint64_t aHiShr = a >> 32; - - const uint64_t bLo = b & 0xFFFFFFFF; - //const uint64_t bHi = b & 0xFFFFFFFF00000000; - const uint64_t bHiShr = b >> 32; - + // given aLo = (a & 0xFFFFFFFF) and + // aHi = (a & 0xFFFFFFFF00000000) and similarly with b, then // a * b == (aHi + aLo) * (bHi + bLo) // a * b == (aHi * bHi) + (aHi * bLo) + (aLo * bHi) + (aLo * bLo) - //int overflow of 64bit a * 64bit b ==> - return aHiShr * bHiShr + ((aHiShr * bLo) >> 32) + ((bHiShr * aLo) >> 32); + + const uint64_t aLo = a & 0xFFFFFFFF; + const uint64_t aHi = a >> 32; // this avoids repeating shifts + const uint64_t bLo = b & 0xFFFFFFFF; + const uint64_t bHi = b >> 32; + // integer overflow of multiplying the unsigned 64bits a and b ==> + return aHi * bHi + ((aHi * bLo) >> 32) + ((bHi * aLo) >> 32); } // returns true if (and only if) a * b == c * d - inline bool ProductIsEqual(int64_t a, int64_t b, int64_t c, int64_t d) + inline bool ProductsAreEqual(int64_t a, int64_t b, int64_t c, int64_t d) { + // nb: unsigned values will be needed for CalcOverflowCarry() const auto abs_a = static_cast(std::abs(a)); const auto abs_b = static_cast(std::abs(b)); const auto abs_c = static_cast(std::abs(c)); const auto abs_d = static_cast(std::abs(d)); - // the multiplications here can potentially overflow - // but any overflows will be compared further below. + // the multiplications here can potentially overflow, but + // any overflows will be compared using CalcOverflowCarry() const auto abs_ab = abs_a * abs_b; const auto abs_cd = abs_c * abs_d; - const auto sign_ab = Sign(a) * Sign(b); - const auto sign_cd = Sign(c) * Sign(d); + // nb: it's important to differentiate 0 values here from other values + const auto sign_ab = TriSign(a) * TriSign(b); + const auto sign_cd = TriSign(c) * TriSign(d); const auto carry_ab = CalcOverflowCarry(abs_a, abs_b); const auto carry_cd = CalcOverflowCarry(abs_c, abs_d); @@ -709,10 +710,12 @@ namespace Clipper2Lib const auto b = pt2.y - sharedPt.y; const auto c = sharedPt.y - pt1.y; const auto d = pt2.x - sharedPt.x; - - return ProductIsEqual(a, b, c, d); + // When checking for collinearity with very large coordinate values + // then ProductsAreEqual is more accurate than using CrossProduct. + return ProductsAreEqual(a, b, c, d); } + template inline double CrossProduct(const Point& pt1, const Point& pt2, const Point& pt3) { return (static_cast(pt2.x - pt1.x) * static_cast(pt3.y - diff --git a/CSharp/Clipper2Lib/Clipper.Core.cs b/CSharp/Clipper2Lib/Clipper.Core.cs index af0bee16..a4ba38b9 100644 --- a/CSharp/Clipper2Lib/Clipper.Core.cs +++ b/CSharp/Clipper2Lib/Clipper.Core.cs @@ -1,6 +1,6 @@ /******************************************************************************* * Author : Angus Johnson * -* Date : 10 May 2024 * +* Date : 12 May 2024 * * Website : http://www.angusj.com * * Copyright : Angus Johnson 2010-2024 * * Purpose : Core structures and functions for the Clipper Library * @@ -585,7 +585,7 @@ public static double CrossProduct(Point64 pt1, Point64 pt2, Point64 pt3) return ((double) (pt2.X - pt1.X) * (pt3.Y - pt2.Y) - (double) (pt2.Y - pt1.Y) * (pt3.X - pt2.X)); } - + #if USINGZ public static Path64 SetZ(Path64 path, long Z) { @@ -595,22 +595,75 @@ public static Path64 SetZ(Path64 path, long Z) } #endif + [MethodImpl(MethodImplOptions.AggressiveInlining)] internal static void CheckPrecision(int precision) { if (precision < -8 || precision > 8) throw new Exception(precision_range_error); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] internal static bool IsAlmostZero(double value) { return (Math.Abs(value) <= floatingPointTolerance); } [MethodImpl(MethodImplOptions.AggressiveInlining)] - internal static bool IsCollinear(Point64 pt1, Point64 pt2, Point64 pt3) + internal static int TriSign(long x) // returns 0, 1 or -1 { - return (pt2.X - pt1.X) * (pt3.Y - pt2.Y) == - (pt2.Y - pt1.Y) * (pt3.X - pt2.X); + if (x < 0) return -1; + else if (x > 1) return 1; + else return 0; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static ulong CalcOverflowCarry(ulong a, ulong b) // #834 + { + // given aLo = (a & 0xFFFFFFFF) and + // aHi = (a & 0xFFFFFFFF00000000) and similarly with b, then + // a * b == (aHi + aLo) * (bHi + bLo) + // a * b == (aHi * bHi) + (aHi * bLo) + (aLo * bHi) + (aLo * bLo) + + ulong aLo = a & 0xFFFFFFFF; + ulong aHi = a >> 32; + ulong bLo = b & 0xFFFFFFFF; + ulong bHi = b >> 32; + // integer overflow of multiplying the unsigned 64bits a and b ==> + return aHi * bHi + ((aHi * bLo) >> 32) + ((bHi * aLo) >> 32); + } + + // returns true if (and only if) a * b == c * d + internal static bool ProductsAreEqual(long a, long b, long c, long d) + { + // nb: unsigned values will be needed for CalcOverflowCarry() + ulong absA = (ulong) Math.Abs(a); + ulong absB = (ulong) Math.Abs(b); + ulong absC = (ulong) Math.Abs(c); + ulong absD = (ulong) Math.Abs(d); + // the multiplications here can potentially overflow, but + // any overflows will be compared using CalcOverflowCarry() + ulong abs_ab = absA * absB; + ulong abs_cd = absC * absD; + + // nb: it's important to differentiate 0 values here from other values + int sign_ab = TriSign(a) * TriSign(b); + int sign_cd = TriSign(c) * TriSign(d); + + ulong carry_ab = CalcOverflowCarry(absA, absB); + ulong carry_cd = CalcOverflowCarry(absC, absD); + return abs_ab == abs_cd && sign_ab == sign_cd && carry_ab == carry_cd; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static bool IsCollinear(Point64 pt1, Point64 sharedPt, Point64 pt2) + { + long a = sharedPt.X - pt1.X; + long b = pt2.Y - sharedPt.Y; + long c = sharedPt.Y - pt1.Y; + long d = pt2.X - sharedPt.X; + // When checking for collinearity with very large coordinate values + // then ProductsAreEqual is more accurate than using CrossProduct. + return ProductsAreEqual(a, b, c, d); } [MethodImpl(MethodImplOptions.AggressiveInlining)] diff --git a/Delphi/Clipper2Lib/Clipper.Core.pas b/Delphi/Clipper2Lib/Clipper.Core.pas index 84b7a14d..723607e6 100644 --- a/Delphi/Clipper2Lib/Clipper.Core.pas +++ b/Delphi/Clipper2Lib/Clipper.Core.pas @@ -2,7 +2,7 @@ (******************************************************************************* * Author : Angus Johnson * -* Date : 3 May 2024 * +* Date : 12 May 2024 * * Website : http://www.angusj.com * * Copyright : Angus Johnson 2010-2024 * * Purpose : Core Clipper Library module * @@ -154,8 +154,7 @@ function IsPositive(const path: TPath64): Boolean; overload; function IsPositive(const path: TPathD): Boolean; overload; {$IFDEF INLINING} inline; {$ENDIF} -function IsCollinear(const pt1, pt2, pt3: TPoint64): Boolean; - {$IFDEF INLINING} inline; {$ENDIF} +function IsCollinear(const pt1, sharedPt, pt2: TPoint64): Boolean; function CrossProduct(const pt1, pt2, pt3: TPoint64): double; overload; {$IFDEF INLINING} inline; {$ENDIF} @@ -1864,18 +1863,77 @@ function IsPositive(const path: TPathD): Boolean; end; //------------------------------------------------------------------------------ +function TriSign(val: Int64): integer; // returns 0, 1 or -1 +{$IFDEF INLINING} inline; {$ENDIF} +begin + if (val < 0) then Result := -1 + else if (val > 1) then Result := 1 + else Result := 0; +end; +//------------------------------------------------------------------------------ + +function CalcOverflowCarry(a, b: UInt64): UInt64; // #834 +{$IFDEF INLINING} inline; {$ENDIF} +var + aLo, aHi, bLo, bHi: UInt64; +begin + // given aLo = (a and $FFFFFFFF) and + // aHi = (a and $FFFFFFFF00000000) and similarly with b, then + // a * b == (aHi + aLo) * (bHi + bLo) + // a * b == (aHi * bHi) + (aHi * bLo) + (aLo * bHi) + (aLo * bLo) + aLo := a and $FFFFFFFF; + aHi := a shr 32; // this avoids multiple shifts + bLo := b and $FFFFFFFF; + bHi := b shr 32; + //integer overflow of multiplying the unsigned 64bits a and b ==> + Result := (aHi * bHi) + ((aHi * bLo) shr 32) + ((bHi * aLo) shr 32); +end; +//------------------------------------------------------------------------------ + {$OVERFLOWCHECKS OFF} -function IsCollinear(const pt1, pt2, pt3: TPoint64): Boolean; +function ProductsAreEqual(a, b, c, d: Int64): Boolean; var - a,b: Int64; + absA,absB,absC,absD: UInt64; + absAB, absCD : UInt64; + carryAB, carryCD : UInt64; + signAB, signCD : integer; begin - a := (pt2.X - pt1.X) * (pt3.Y - pt2.Y); - b := (pt2.Y - pt1.Y) * (pt3.X - pt2.X); - result := a = b; + // nb: unsigned values will be needed for CalcOverflowCarry() + absA := UInt64(Abs(a)); + absB := UInt64(Abs(b)); + absC := UInt64(Abs(c)); + absD := UInt64(Abs(d)); + + // the multiplications here can potentially overflow, but + // any overflows will be compared using CalcOverflowCarry() + absAB := absA * absB; + absCD := absC * absD; + + // nb: it's important to differentiate 0 values here from other values + signAB := TriSign(a) * TriSign(b); + signCD := TriSign(c) * TriSign(d); + + carryAB := CalcOverflowCarry(absA, absB); + carryCD := CalcOverflowCarry(absC, absD); + Result := (absAB = absCD) and (signAB = signCD) and (carryAB = carryCD); end; {$OVERFLOWCHECKS ON} //------------------------------------------------------------------------------ +function IsCollinear(const pt1, sharedPt, pt2: TPoint64): Boolean; +var + a,b,c,d: Int64; +begin + a := sharedPt.X - pt1.X; + b := pt2.Y - sharedPt.Y; + c := sharedPt.Y - pt1.Y; + d := pt2.X - sharedPt.X; + // When checking for collinearity with very large coordinate values + // then ProductsAreEqual is more accurate than using CrossProduct. + Result := ProductsAreEqual(a, b, c, d); +end; +//------------------------------------------------------------------------------ + function CrossProduct(const pt1, pt2, pt3: TPoint64): double; begin result := CrossProduct( From 82cd8870c74f62b96405d4e2d9e5b390ab67737e Mon Sep 17 00:00:00 2001 From: Juha Reunanen Date: Sun, 12 May 2024 16:40:08 +0300 Subject: [PATCH 60/64] More carry calculation fixing (#835) * Add carry calculation tests * Fix carry calculation * Minor simplification * Test both ways, now that we are here * Add helpful remark * Avoid a single multiplication instruction per product (=one for `a*b`, and another for `c*d`) --- .../include/clipper2/clipper.core.h | 49 +++++++++++-------- CPP/Tests/TestIsCollinear.cpp | 23 +++++++++ 2 files changed, 52 insertions(+), 20 deletions(-) diff --git a/CPP/Clipper2Lib/include/clipper2/clipper.core.h b/CPP/Clipper2Lib/include/clipper2/clipper.core.h index c73476de..0d82b5c6 100644 --- a/CPP/Clipper2Lib/include/clipper2/clipper.core.h +++ b/CPP/Clipper2Lib/include/clipper2/clipper.core.h @@ -664,42 +664,51 @@ namespace Clipper2Lib return (x > 0) - (x < 0); } - inline uint64_t CalcOverflowCarry(uint64_t a, uint64_t b) // #834 + struct MultiplicationResult { - // given aLo = (a & 0xFFFFFFFF) and - // aHi = (a & 0xFFFFFFFF00000000) and similarly with b, then - // a * b == (aHi + aLo) * (bHi + bLo) - // a * b == (aHi * bHi) + (aHi * bLo) + (aLo * bHi) + (aLo * bLo) - - const uint64_t aLo = a & 0xFFFFFFFF; - const uint64_t aHi = a >> 32; // this avoids repeating shifts - const uint64_t bLo = b & 0xFFFFFFFF; - const uint64_t bHi = b >> 32; - // integer overflow of multiplying the unsigned 64bits a and b ==> - return aHi * bHi + ((aHi * bLo) >> 32) + ((bHi * aLo) >> 32); + const uint64_t result = 0; + const uint64_t carry = 0; + + bool operator==(const MultiplicationResult& other) const + { + return result == other.result && carry == other.carry; + }; + }; + + inline MultiplicationResult Multiply(uint64_t a, uint64_t b) // #834 + { + const auto lo = [](uint64_t x) { return x & 0xFFFFFFFF; }; + const auto hi = [](uint64_t x) { return x >> 32; }; + + // https://stackoverflow.com/a/1815371/1158913 + const uint64_t x1 = lo(a) * lo(b); + const uint64_t x2 = hi(a) * lo(b) + hi(x1); + const uint64_t x3 = lo(a) * hi(b) + lo(x2); + const uint64_t x4 = hi(a) * hi(b) + hi(x2) + hi(x3); + + const uint64_t result = lo(x3) << 32 | lo(x1); + const uint64_t carry = x4; + + return { result, carry }; } // returns true if (and only if) a * b == c * d inline bool ProductsAreEqual(int64_t a, int64_t b, int64_t c, int64_t d) { - // nb: unsigned values will be needed for CalcOverflowCarry() + // nb: unsigned values needed for calculating overflow carry const auto abs_a = static_cast(std::abs(a)); const auto abs_b = static_cast(std::abs(b)); const auto abs_c = static_cast(std::abs(c)); const auto abs_d = static_cast(std::abs(d)); - // the multiplications here can potentially overflow, but - // any overflows will be compared using CalcOverflowCarry() - const auto abs_ab = abs_a * abs_b; - const auto abs_cd = abs_c * abs_d; + const auto abs_ab = Multiply(abs_a, abs_b); + const auto abs_cd = Multiply(abs_c, abs_d); // nb: it's important to differentiate 0 values here from other values const auto sign_ab = TriSign(a) * TriSign(b); const auto sign_cd = TriSign(c) * TriSign(d); - const auto carry_ab = CalcOverflowCarry(abs_a, abs_b); - const auto carry_cd = CalcOverflowCarry(abs_c, abs_d); - return abs_ab == abs_cd && sign_ab == sign_cd && carry_ab == carry_cd; + return abs_ab == abs_cd && sign_ab == sign_cd; } template diff --git a/CPP/Tests/TestIsCollinear.cpp b/CPP/Tests/TestIsCollinear.cpp index 7f3908f2..f6826d8f 100644 --- a/CPP/Tests/TestIsCollinear.cpp +++ b/CPP/Tests/TestIsCollinear.cpp @@ -1,6 +1,29 @@ #include #include "clipper2/clipper.h" +TEST(Clipper2Tests, TestCarryCalculation) { + EXPECT_EQ(Clipper2Lib::Multiply(0x51eaed81157de061, 0x3a271fb2745b6fe9).carry, 0x129bbebdfae0464e); + EXPECT_EQ(Clipper2Lib::Multiply(0x3a271fb2745b6fe9, 0x51eaed81157de061).carry, 0x129bbebdfae0464e); + EXPECT_EQ(Clipper2Lib::Multiply(0xc2055706a62883fa, 0x26c78bc79c2322cc).carry, 0x1d640701d192519b); + EXPECT_EQ(Clipper2Lib::Multiply(0x26c78bc79c2322cc, 0xc2055706a62883fa).carry, 0x1d640701d192519b); + EXPECT_EQ(Clipper2Lib::Multiply(0x874ddae32094b0de, 0x9b1559a06fdf83e0).carry, 0x51f76c49563e5bfe); + EXPECT_EQ(Clipper2Lib::Multiply(0x9b1559a06fdf83e0, 0x874ddae32094b0de).carry, 0x51f76c49563e5bfe); + EXPECT_EQ(Clipper2Lib::Multiply(0x81fb3ad3636ca900, 0x239c000a982a8da4).carry, 0x12148e28207b83a3); + EXPECT_EQ(Clipper2Lib::Multiply(0x239c000a982a8da4, 0x81fb3ad3636ca900).carry, 0x12148e28207b83a3); + EXPECT_EQ(Clipper2Lib::Multiply(0x4be0b4c5d2725c44, 0x990cd6db34a04c30).carry, 0x2d5d1a4183fd6165); + EXPECT_EQ(Clipper2Lib::Multiply(0x990cd6db34a04c30, 0x4be0b4c5d2725c44).carry, 0x2d5d1a4183fd6165); + EXPECT_EQ(Clipper2Lib::Multiply(0x978ec0c0433c01f6, 0x2df03d097966b536).carry, 0x1b3251d91fe272a5); + EXPECT_EQ(Clipper2Lib::Multiply(0x2df03d097966b536, 0x978ec0c0433c01f6).carry, 0x1b3251d91fe272a5); + EXPECT_EQ(Clipper2Lib::Multiply(0x49c5cbbcfd716344, 0xc489e3b34b007ad3).carry, 0x38a32c74c8c191a4); + EXPECT_EQ(Clipper2Lib::Multiply(0xc489e3b34b007ad3, 0x49c5cbbcfd716344).carry, 0x38a32c74c8c191a4); + EXPECT_EQ(Clipper2Lib::Multiply(0xd3361cdbeed655d5, 0x1240da41e324953a).carry, 0x0f0f4fa11e7e8f2a); + EXPECT_EQ(Clipper2Lib::Multiply(0x1240da41e324953a, 0xd3361cdbeed655d5).carry, 0x0f0f4fa11e7e8f2a); + EXPECT_EQ(Clipper2Lib::Multiply(0x51b854f8e71b0ae0, 0x6f8d438aae530af5).carry, 0x239c04ee3c8cc248); + EXPECT_EQ(Clipper2Lib::Multiply(0x6f8d438aae530af5, 0x51b854f8e71b0ae0).carry, 0x239c04ee3c8cc248); + EXPECT_EQ(Clipper2Lib::Multiply(0xbbecf7dbc6147480, 0xbb0f73d0f82e2236).carry, 0x895170f4e9a216a7); + EXPECT_EQ(Clipper2Lib::Multiply(0xbb0f73d0f82e2236, 0xbbecf7dbc6147480).carry, 0x895170f4e9a216a7); +} + TEST(Clipper2Tests, TestIsCollinear) { // a large integer not representable by double const int64_t i = 9007199254740993; From 07cabaddd8541be322ecc555f186c1d867b506d3 Mon Sep 17 00:00:00 2001 From: angusj Date: Mon, 13 May 2024 15:23:21 +1000 Subject: [PATCH 61/64] Updated C# and Delphi code with bugfixed IsCollinear function (#835) --- .../include/clipper2/clipper.core.h | 11 ++--- CSharp/Clipper2Lib/Clipper.Core.cs | 39 ++++++++--------- Delphi/Clipper2Lib/Clipper.Core.pas | 43 ++++++++----------- 3 files changed, 42 insertions(+), 51 deletions(-) diff --git a/CPP/Clipper2Lib/include/clipper2/clipper.core.h b/CPP/Clipper2Lib/include/clipper2/clipper.core.h index 0d82b5c6..4fe8a8cb 100644 --- a/CPP/Clipper2Lib/include/clipper2/clipper.core.h +++ b/CPP/Clipper2Lib/include/clipper2/clipper.core.h @@ -664,30 +664,27 @@ namespace Clipper2Lib return (x > 0) - (x < 0); } - struct MultiplicationResult + struct MultiplyUInt64Result { const uint64_t result = 0; const uint64_t carry = 0; - bool operator==(const MultiplicationResult& other) const + bool operator==(const MultiplyUInt64Result& other) const { return result == other.result && carry == other.carry; }; }; - inline MultiplicationResult Multiply(uint64_t a, uint64_t b) // #834 + inline MultiplyUInt64Result Multiply(uint64_t a, uint64_t b) // #834, #835 { const auto lo = [](uint64_t x) { return x & 0xFFFFFFFF; }; const auto hi = [](uint64_t x) { return x >> 32; }; - // https://stackoverflow.com/a/1815371/1158913 const uint64_t x1 = lo(a) * lo(b); const uint64_t x2 = hi(a) * lo(b) + hi(x1); const uint64_t x3 = lo(a) * hi(b) + lo(x2); - const uint64_t x4 = hi(a) * hi(b) + hi(x2) + hi(x3); - const uint64_t result = lo(x3) << 32 | lo(x1); - const uint64_t carry = x4; + const uint64_t carry = hi(a) * hi(b) + hi(x2) + hi(x3); return { result, carry }; } diff --git a/CSharp/Clipper2Lib/Clipper.Core.cs b/CSharp/Clipper2Lib/Clipper.Core.cs index a4ba38b9..6358aef8 100644 --- a/CSharp/Clipper2Lib/Clipper.Core.cs +++ b/CSharp/Clipper2Lib/Clipper.Core.cs @@ -1,6 +1,6 @@ /******************************************************************************* * Author : Angus Johnson * -* Date : 12 May 2024 * +* Date : 13 May 2024 * * Website : http://www.angusj.com * * Copyright : Angus Johnson 2010-2024 * * Purpose : Core structures and functions for the Clipper Library * @@ -616,20 +616,22 @@ internal static int TriSign(long x) // returns 0, 1 or -1 else return 0; } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - internal static ulong CalcOverflowCarry(ulong a, ulong b) // #834 + public struct MultiplyUInt64Result { - // given aLo = (a & 0xFFFFFFFF) and - // aHi = (a & 0xFFFFFFFF00000000) and similarly with b, then - // a * b == (aHi + aLo) * (bHi + bLo) - // a * b == (aHi * bHi) + (aHi * bLo) + (aLo * bHi) + (aLo * bLo) + public ulong lo64; + public ulong hi64; + } - ulong aLo = a & 0xFFFFFFFF; - ulong aHi = a >> 32; - ulong bLo = b & 0xFFFFFFFF; - ulong bHi = b >> 32; - // integer overflow of multiplying the unsigned 64bits a and b ==> - return aHi * bHi + ((aHi * bLo) >> 32) + ((bHi * aLo) >> 32); + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static MultiplyUInt64Result MultiplyUInt64(ulong a, ulong b) // #834,#835 + { + ulong x1 = (a & 0xFFFFFFFF) * (b & 0xFFFFFFFF); + ulong x2 = (a >> 32) * (b & 0xFFFFFFFF) + (x1 >> 32); + ulong x3 = (a & 0xFFFFFFFF) * (b >> 32) + (x2 & 0xFFFFFFFF); + MultiplyUInt64Result result; + result.lo64 = (x3 & 0xFFFFFFFF) << 32 | (x1 & 0xFFFFFFFF); + result.hi64 = (a >> 32) * (b >> 32) + (x2 >> 32) + (x3 >> 32); + return result; } // returns true if (and only if) a * b == c * d @@ -640,18 +642,15 @@ internal static bool ProductsAreEqual(long a, long b, long c, long d) ulong absB = (ulong) Math.Abs(b); ulong absC = (ulong) Math.Abs(c); ulong absD = (ulong) Math.Abs(d); - // the multiplications here can potentially overflow, but - // any overflows will be compared using CalcOverflowCarry() - ulong abs_ab = absA * absB; - ulong abs_cd = absC * absD; + + MultiplyUInt64Result mul_ab = MultiplyUInt64(absA, absB); + MultiplyUInt64Result mul_cd = MultiplyUInt64(absC, absD); // nb: it's important to differentiate 0 values here from other values int sign_ab = TriSign(a) * TriSign(b); int sign_cd = TriSign(c) * TriSign(d); - ulong carry_ab = CalcOverflowCarry(absA, absB); - ulong carry_cd = CalcOverflowCarry(absC, absD); - return abs_ab == abs_cd && sign_ab == sign_cd && carry_ab == carry_cd; + return mul_ab.lo64 == mul_cd.lo64 && mul_ab.hi64 == mul_cd.hi64 && sign_ab == sign_cd; } [MethodImpl(MethodImplOptions.AggressiveInlining)] diff --git a/Delphi/Clipper2Lib/Clipper.Core.pas b/Delphi/Clipper2Lib/Clipper.Core.pas index 723607e6..a6ee145c 100644 --- a/Delphi/Clipper2Lib/Clipper.Core.pas +++ b/Delphi/Clipper2Lib/Clipper.Core.pas @@ -2,7 +2,7 @@ (******************************************************************************* * Author : Angus Johnson * -* Date : 12 May 2024 * +* Date : 13 May 2024 * * Website : http://www.angusj.com * * Copyright : Angus Johnson 2010-2024 * * Purpose : Core Clipper Library module * @@ -1872,30 +1872,29 @@ function TriSign(val: Int64): integer; // returns 0, 1 or -1 end; //------------------------------------------------------------------------------ -function CalcOverflowCarry(a, b: UInt64): UInt64; // #834 +type + TMultiplyUInt64Result = record + lo64: UInt64; + hi64 : UInt64; + end; + +function MultiplyUInt64(a, b: UInt64): TMultiplyUInt64Result; // #834, #835 {$IFDEF INLINING} inline; {$ENDIF} var - aLo, aHi, bLo, bHi: UInt64; + x1, x2, x3: UInt64; begin - // given aLo = (a and $FFFFFFFF) and - // aHi = (a and $FFFFFFFF00000000) and similarly with b, then - // a * b == (aHi + aLo) * (bHi + bLo) - // a * b == (aHi * bHi) + (aHi * bLo) + (aLo * bHi) + (aLo * bLo) - aLo := a and $FFFFFFFF; - aHi := a shr 32; // this avoids multiple shifts - bLo := b and $FFFFFFFF; - bHi := b shr 32; - //integer overflow of multiplying the unsigned 64bits a and b ==> - Result := (aHi * bHi) + ((aHi * bLo) shr 32) + ((bHi * aLo) shr 32); + x1 := (a and $FFFFFFFF) * (b and $FFFFFFFF); + x2 := (a shr 32) * (b and $FFFFFFFF) + (x1 shr 32); + x3 := (a and $FFFFFFFF) * (b shr 32) + (x2 and $FFFFFFFF); + Result.lo64 := ((x3 and $FFFFFFFF) shl 32) or (x1 and $FFFFFFFF); + Result.hi64 := hi(a shr 32) * (b shr 32) + (x2 shr 32) + (x3 shr 32); end; //------------------------------------------------------------------------------ -{$OVERFLOWCHECKS OFF} function ProductsAreEqual(a, b, c, d: Int64): Boolean; var absA,absB,absC,absD: UInt64; - absAB, absCD : UInt64; - carryAB, carryCD : UInt64; + absAB, absCD : TMultiplyUInt64Result; signAB, signCD : integer; begin // nb: unsigned values will be needed for CalcOverflowCarry() @@ -1904,20 +1903,16 @@ function ProductsAreEqual(a, b, c, d: Int64): Boolean; absC := UInt64(Abs(c)); absD := UInt64(Abs(d)); - // the multiplications here can potentially overflow, but - // any overflows will be compared using CalcOverflowCarry() - absAB := absA * absB; - absCD := absC * absD; + absAB := MultiplyUInt64(absA, absB); + absCD := MultiplyUInt64(absC, absD); // nb: it's important to differentiate 0 values here from other values signAB := TriSign(a) * TriSign(b); signCD := TriSign(c) * TriSign(d); - carryAB := CalcOverflowCarry(absA, absB); - carryCD := CalcOverflowCarry(absC, absD); - Result := (absAB = absCD) and (signAB = signCD) and (carryAB = carryCD); + Result := (absAB.lo64 = absCD.lo64) and + (absAB.hi64 = absCD.hi64) and (signAB = signCD); end; -{$OVERFLOWCHECKS ON} //------------------------------------------------------------------------------ function IsCollinear(const pt1, sharedPt, pt2: TPoint64): Boolean; From f95f4b357d492bf90e305b17aab372290da1d873 Mon Sep 17 00:00:00 2001 From: Juha Reunanen Date: Tue, 14 May 2024 14:21:44 +0300 Subject: [PATCH 62/64] Use compiler-provided int128 type on clang and gcc (but only when the target architecture is at least 64-bit) (#837) --- CPP/Clipper2Lib/include/clipper2/clipper.core.h | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CPP/Clipper2Lib/include/clipper2/clipper.core.h b/CPP/Clipper2Lib/include/clipper2/clipper.core.h index 4fe8a8cb..8dc40755 100644 --- a/CPP/Clipper2Lib/include/clipper2/clipper.core.h +++ b/CPP/Clipper2Lib/include/clipper2/clipper.core.h @@ -692,6 +692,11 @@ namespace Clipper2Lib // returns true if (and only if) a * b == c * d inline bool ProductsAreEqual(int64_t a, int64_t b, int64_t c, int64_t d) { +#if (defined(__clang__) || defined(__GNUC__)) && UINTPTR_MAX >= UINT64_MAX + const auto ab = static_cast<__int128_t>(a) * static_cast<__int128_t>(b); + const auto cd = static_cast<__int128_t>(c) * static_cast<__int128_t>(d); + return ab == cd; +#else // nb: unsigned values needed for calculating overflow carry const auto abs_a = static_cast(std::abs(a)); const auto abs_b = static_cast(std::abs(b)); @@ -706,6 +711,7 @@ namespace Clipper2Lib const auto sign_cd = TriSign(c) * TriSign(d); return abs_ab == abs_cd && sign_ab == sign_cd; +#endif } template From efde1c9b1f2dbc2ec2aa533361795aeab2c039fc Mon Sep 17 00:00:00 2001 From: angusj Date: Tue, 14 May 2024 21:53:51 +1000 Subject: [PATCH 63/64] Added MinkowskiSum64 & MinkowskiDiff64 to clipper.export.h. (#836) --- .../include/clipper2/clipper.export.h | 38 ++++++++++++++++++- 1 file changed, 36 insertions(+), 2 deletions(-) diff --git a/CPP/Clipper2Lib/include/clipper2/clipper.export.h b/CPP/Clipper2Lib/include/clipper2/clipper.export.h index 2e892451..6bb71bab 100644 --- a/CPP/Clipper2Lib/include/clipper2/clipper.export.h +++ b/CPP/Clipper2Lib/include/clipper2/clipper.export.h @@ -1,6 +1,6 @@ /******************************************************************************* * Author : Angus Johnson * -* Date : 14 February 2024 * +* Date : 14 May 2024 * * Website : http://www.angusj.com * * Copyright : Angus Johnson 2010-2024 * * Purpose : This module exports the Clipper2 Library (ie DLL/so) * @@ -61,7 +61,8 @@ ____________________________________________________________ As mentioned above, the very first CPolyPath structure is just a container that owns (both directly and indirectly) every other CPolyPath in the tree. Since this first CPolyPath has no path, instead of a path length, its very -first value will contain the total length of the CPolytree array. +first value will contain the total length of the CPolytree array (not its +total bytes length). Again, all theses exported structures (CPaths64, CPathsD, CPolyTree64 & CPolyTreeD) are arrays of either type int64_t or double, and the first @@ -272,6 +273,23 @@ CPathsD CreateCPathsDFromPaths64(const Paths64& paths, double scale) return result; } +template +static Path ConvertCPath(T* path) +{ + Path result; + if (!path) return result; + T* v = path; + size_t cnt = static_cast(*v); + v += 2; // skip 0 value + path.reserve(cnt); + for (size_t j = 0; j < cnt; ++j) + { + T x = *v++, y = *v++; + path.push_back(Point(x, y)); + } + return result; +} + template static Paths ConvertCPaths(T* paths) { @@ -561,6 +579,22 @@ EXTERN_DLL_EXPORT CPathsD RectClipLinesD(const CRectD& rect, return CreateCPathsDFromPaths64(result, 1 / scale); } +EXTERN_DLL_EXPORT CPaths64 MinkowskiSum64(const CPath64& cpattern, const CPath64& cpath, bool is_closed) +{ + Path64 path = ConvertCPath(cpath); + Path64 pattern = ConvertCPath(cpattern); + Paths64 solution = MinkowskiSum(pattern, path, is_closed); + return CreateCPaths(solution); +} + +EXTERN_DLL_EXPORT CPaths64 MinkowskiDiff64(const CPath64& cpattern, const CPath64& cpath, bool is_closed) +{ + Path64 path = ConvertCPath(cpath); + Path64 pattern = ConvertCPath(cpattern); + Paths64 solution = MinkowskiDiff(pattern, path, is_closed); + return CreateCPaths(solution); +} + } // end Clipper2Lib namespace #endif // CLIPPER2_EXPORT_H From ff378668baae3570e9d8070aa9eb339bdd5a6aba Mon Sep 17 00:00:00 2001 From: angusj Date: Tue, 14 May 2024 22:01:51 +1000 Subject: [PATCH 64/64] clipper.export.h - fixed bug in previous commit (#836) --- CPP/Clipper2Lib/include/clipper2/clipper.export.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CPP/Clipper2Lib/include/clipper2/clipper.export.h b/CPP/Clipper2Lib/include/clipper2/clipper.export.h index 6bb71bab..53a44536 100644 --- a/CPP/Clipper2Lib/include/clipper2/clipper.export.h +++ b/CPP/Clipper2Lib/include/clipper2/clipper.export.h @@ -281,11 +281,11 @@ static Path ConvertCPath(T* path) T* v = path; size_t cnt = static_cast(*v); v += 2; // skip 0 value - path.reserve(cnt); + result.reserve(cnt); for (size_t j = 0; j < cnt; ++j) { T x = *v++, y = *v++; - path.push_back(Point(x, y)); + result.push_back(Point(x, y)); } return result; }