diff --git a/CPP/BenchMark/CMakeLists.txt b/CPP/BenchMark/CMakeLists.txt index 16a5389e..66cb9368 100644 --- a/CPP/BenchMark/CMakeLists.txt +++ b/CPP/BenchMark/CMakeLists.txt @@ -1,3 +1,12 @@ +cmake_minimum_required(VERSION 3.15) +project(Clipper2_benchmarks VERSION 1.0 LANGUAGES C CXX) + +if(NOT DEFINED CMAKE_CXX_STANDARD OR CMAKE_CXX_STANDARD LESS 17) + set(CMAKE_CXX_STANDARD 17) +endif() +set(CMAKE_CXX_STANDARD_REQUIRED ON) +set(CMAKE_CXX_EXTENSIONS OFF) + # fetch the google benchmark library include(FetchContent) set(BENCHMARK_ENABLE_GTEST_TESTS OFF) @@ -14,6 +23,7 @@ set(gtest_force_shared_crt ON CACHE BOOL "" FORCE) message("fetching is done") set(benchmark_srcs + PointInPolygonBenchmark.cpp StripDuplicateBenchmark.cpp # more to add ) @@ -24,13 +34,12 @@ foreach(benchmark ${benchmark_srcs}) message(STATUS "${PROJECT_NAME} add benchmark ${benchmark_target}") add_executable(${benchmark_target} ${benchmark}) - target_include_directories(${benchmark_target} PUBLIC - + target_include_directories(${benchmark_target} + PUBLIC ../Clipper2Lib/include + PUBLIC ../Utils ) target_link_libraries(${benchmark_target} benchmark::benchmark - Clipper2 - Clipper2utils ) endforeach() diff --git a/CPP/BenchMark/PointInPolygonBenchmark.cpp b/CPP/BenchMark/PointInPolygonBenchmark.cpp new file mode 100644 index 00000000..89a35fe2 --- /dev/null +++ b/CPP/BenchMark/PointInPolygonBenchmark.cpp @@ -0,0 +1,329 @@ +#include "benchmark/benchmark.h" +#include "clipper2/clipper.h" +#include "CommonUtils.h" +#include +#include + +using namespace Clipper2Lib; +using benchmark::State; + +template +inline PointInPolygonResult PIP1(const Point& pt, const Path& polygon) +{ + int val = 0; + typename Path::const_iterator cbegin = polygon.cbegin(), first = cbegin, curr, prev; + typename Path::const_iterator cend = polygon.cend(); + + while (first != cend && first->y == pt.y) ++first; + if (first == cend) // not a proper polygon + return PointInPolygonResult::IsOutside; + + bool is_above = first->y < pt.y, starting_above = is_above; + curr = first + 1; + while (true) + { + if (curr == cend) + { + if (cend == first || first == cbegin) break; + cend = first; + curr = cbegin; + } + + if (is_above) + { + while (curr != cend && curr->y < pt.y) ++curr; + if (curr == cend) continue; + } + else + { + while (curr != cend && curr->y > pt.y) ++curr; + if (curr == cend) continue; + } + + if (curr == cbegin) + prev = polygon.cend() - 1; + else + prev = 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; + ++curr; + if (curr == first) break; + continue; + } + + if (pt.x < curr->x && pt.x < prev->x) + { + // we're only interested in edges crossing on the left + } + else if (pt.x > prev->x && pt.x > curr->x) + val = 1 - val; // toggle val + else + { + double d = CrossProduct(*prev, *curr, pt); + if (d == 0) return PointInPolygonResult::IsOn; + if ((d < 0) == is_above) val = 1 - val; + } + is_above = !is_above; + ++curr; + } + + if (is_above != starting_above) + { + cend = polygon.cend(); + if (curr == cend) curr = cbegin; + if (curr == cbegin) prev = cend - 1; + else prev = curr - 1; + double d = CrossProduct(*prev, *curr, pt); + if (d == 0) return PointInPolygonResult::IsOn; + if ((d < 0) == is_above) val = 1 - val; + } + + return (val == 0) ? + PointInPolygonResult::IsOutside : + PointInPolygonResult::IsInside; +} + + +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(); + + bool is_above; + if (prev->y == pt.y) + { + if (pt == *prev) return PointInPolygonResult::IsOn; + 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; + while (curr != cend) + { + if (is_above) + { + while (curr != cend && curr->y < pt.y) { prev = curr; ++curr; } + if (curr == cend) break; + } + else + { + while (curr != cend && curr->y > pt.y) { prev = curr; ++curr; } + if (curr == cend) break; + } + + 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; + } + + if (pt.x < curr->x && pt.x < prev->x) + { + // we're only interested in edges crossing on the left + } + else if (pt.x > prev->x && pt.x > curr->x) + ++val; + else + { + double d = CrossProduct(*prev, *curr, pt); + 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; +} + + +// "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) +{ + T x1, y1, x2, y2; + int k = 0; + Path::const_iterator itPrev = path.cend() - 1; + Path::const_iterator itCurr = path.cbegin(); + for ( ; itCurr != path.cend(); ++itCurr) + { + y1 = itPrev->y - pt.y; + y2 = itCurr->y - pt.y; + if (((y1 < 0) && (y2 < 0)) || ((y1 > 0) && (y2 > 0))) + { + itPrev = itCurr; + continue; + } + + x1 = itPrev->x - pt.x; + x2 = itCurr->x - pt.x; + 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)) + { + int64_t f = x1 * y2 - x2 * y1; + if (f < 0) ++k; + else if (f == 0) return PointInPolygonResult::IsOn; + } + else if (((y2 == 0) && (y1 < 0)) || ((y1 == 0) && (y2 < 0))) + { + int64_t f = x1 * y2 - x2 * y1; + if (f == 0) return PointInPolygonResult::IsOn; + } + else if ((y1 == 0) && (y2 == 0) && + (((x2 <= 0) && (x1 >= 0)) || ((x1 <= 0) && (x2 >= 0)))) + return PointInPolygonResult::IsOn; + itPrev = itCurr; + } + if (k % 2) return PointInPolygonResult::IsInside; + return PointInPolygonResult::IsOutside; +} + + +Paths64 paths; +Point64 mp; +PointInPolygonResult pip1 = PointInPolygonResult::IsOn; +PointInPolygonResult pip2 = PointInPolygonResult::IsOn; +PointInPolygonResult pip3 = PointInPolygonResult::IsOn; + + +static void BM_PIP1(benchmark::State& state) +{ + for (auto _ : state) + { + pip1 = PIP1(mp, paths[state.range(0)]); + } +} + +static void BM_PIP2(benchmark::State& state) +{ + for (auto _ : state) + { + pip2 = PIP2(mp, paths[state.range(0)]); + } +} + +static void BM_PIP3(benchmark::State& state) +{ + for (auto _ : state) + { + pip3 = PIP3(mp, paths[state.range(0)]); + } +} + +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 }; + +int main(int argc, char** argv) { + + const DoTests do_tests = do_all_tests; + + 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); + 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 << "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 << 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) + { + std::string _; + std::getline(std::cin, _); + return 0; + } + } + + if (do_tests == do_stress_test_only) return 0; + + // compare 3 PIP algorithms + const int width = 600000, height = 400000; + mp = Point64(width / 2, height / 2); + paths.clear(); + srand((unsigned)time(0)); + for (int i = 0, count = 10000; i < 5; ++i, count *= 10) + paths.push_back(MakeRandomPoly(width, height, count)); + + 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(); + + if (pip2 != pip1 || pip3 != pip1) + { + 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; + } + + return 0; +} diff --git a/CPP/BenchMark/StripDuplicateBenchmark.cpp b/CPP/BenchMark/StripDuplicateBenchmark.cpp index d44b89c8..92c07196 100644 --- a/CPP/BenchMark/StripDuplicateBenchmark.cpp +++ b/CPP/BenchMark/StripDuplicateBenchmark.cpp @@ -1,6 +1,6 @@ #include "benchmark/benchmark.h" #include "clipper2/clipper.h" -#include "Utils/CommonUtils.h" +#include "CommonUtils.h" #include static void CustomArguments(benchmark::internal::Benchmark *b) { diff --git a/CPP/CMakeLists.txt b/CPP/CMakeLists.txt index 817cf017..98d8ec49 100644 --- a/CPP/CMakeLists.txt +++ b/CPP/CMakeLists.txt @@ -1,5 +1,5 @@ cmake_minimum_required(VERSION 3.15) -project(Clipper2 VERSION 1.2.4 LANGUAGES C CXX) +project(Clipper2 VERSION 1.3.0 LANGUAGES C CXX) set(CMAKE_POSITION_INDEPENDENT_CODE ON) if(NOT DEFINED CMAKE_CXX_STANDARD OR CMAKE_CXX_STANDARD LESS 17) @@ -19,7 +19,7 @@ set(CLIPPER2_USINGZ "ON" CACHE STRING "Build Clipper2Z, either \"ON\" or \"OFF\" set(CLIPPER2_MAX_PRECISION 8 CACHE STRING "Maximum precision allowed for double to int64 scaling") if (APPLE) - set(CMAKE_SHARED_LIBRARY_SUFFIX ".so") + set(CMAKE_SHARED_LIBRARY_SUFFIX ".dylib") endif () include(GNUInstallDirs) diff --git a/CPP/Clipper2Lib/include/clipper2/clipper.export.h b/CPP/Clipper2Lib/include/clipper2/clipper.export.h index 6115dd5b..d7286132 100644 --- a/CPP/Clipper2Lib/include/clipper2/clipper.export.h +++ b/CPP/Clipper2Lib/include/clipper2/clipper.export.h @@ -34,17 +34,17 @@ __________________________________ 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) and the number of -contained paths (C). +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| |A , C | | _______________________________ CPolytree64 and CPolytreeD: -These are also simple arrays consisting of CPolyPath structures that -represent individual paths in a tree structure. However, the very first -CPolyPath is just the tree container so it won't have a path. And because +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. @@ -59,18 +59,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. -Since this first CPolyPath has no path, instead of a path length, its first -value will contain the total length of the CPolytree array. +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 the exported structures (CPaths64, CPathsD, CPolyTree64 & CPolyTreeD) +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. 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 really safe way to -free up this memory is to use the exported DisposeArray64 and -DisposeArrayD functions below. +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. */ diff --git a/CPP/Clipper2Lib/include/clipper2/clipper.version.h b/CPP/Clipper2Lib/include/clipper2/clipper.version.h index af39c3b5..d7644067 100644 --- a/CPP/Clipper2Lib/include/clipper2/clipper.version.h +++ b/CPP/Clipper2Lib/include/clipper2/clipper.version.h @@ -1,6 +1,6 @@ #ifndef CLIPPER_VERSION_H #define CLIPPER_VERSION_H -constexpr auto CLIPPER2_VERSION = "1.2.4"; +constexpr auto CLIPPER2_VERSION = "1.3.0"; #endif // CLIPPER_VERSION_H diff --git a/CPP/Clipper2Lib/src/clipper.offset.cpp b/CPP/Clipper2Lib/src/clipper.offset.cpp index 48057016..514fbc8c 100644 --- a/CPP/Clipper2Lib/src/clipper.offset.cpp +++ b/CPP/Clipper2Lib/src/clipper.offset.cpp @@ -1,6 +1,6 @@ /******************************************************************************* * Author : Angus Johnson * -* Date : 26 November 2023 * +* Date : 28 November 2023 * * Website : http://www.angusj.com * * Copyright : Angus Johnson 2010-2023 * * Purpose : Path Offset (Inflate/Shrink) * @@ -20,13 +20,17 @@ const double floating_point_tolerance = 1e-12; // Miscellaneous methods //------------------------------------------------------------------------------ -void GetMultiBounds(const Paths64& paths, std::vector& recList, EndType end_type) +inline bool ToggleBoolIf(bool val, bool condition) +{ + return condition ? !val : val; +} + +void GetMultiBounds(const Paths64& paths, std::vector& recList) { - size_t min_path_len = (end_type == EndType::Polygon) ? 3 : 1; recList.reserve(paths.size()); for (const Path64& path : paths) { - if (path.size() < min_path_len) + if (path.size() < 1) { recList.push_back(InvalidRect64); continue; @@ -161,7 +165,7 @@ ClipperOffset::Group::Group(const Paths64& _paths, JoinType _join_type, EndType StripDuplicates(p, is_joined); // get bounds of each path --> bounds_list - GetMultiBounds(paths_in, bounds_list, end_type); + GetMultiBounds(paths_in, bounds_list); if (end_type == EndType::Polygon) { @@ -206,10 +210,10 @@ void ClipperOffset::BuildNormals(const Path64& path) norms.clear(); norms.reserve(path.size()); if (path.size() == 0) return; - Path64::const_iterator path_iter, path_last_iter = --path.cend(); - for (path_iter = path.cbegin(); path_iter != path_last_iter; ++path_iter) + Path64::const_iterator path_iter, path_stop_iter = --path.cend(); + for (path_iter = path.cbegin(); path_iter != path_stop_iter; ++path_iter) norms.push_back(GetUnitNormal(*path_iter,*(path_iter +1))); - norms.push_back(GetUnitNormal(*path_last_iter, *(path.cbegin()))); + norms.push_back(GetUnitNormal(*path_stop_iter, *(path.cbegin()))); } inline PointD TranslatePoint(const PointD& pt, double dx, double dy) @@ -515,8 +519,9 @@ void ClipperOffset::DoGroupOffset(Group& group) { if (group.end_type == EndType::Polygon) { - if (group.lowest_path_idx < 0) return; - //if (area == 0) return; // probably unhelpful (#430) + // 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_; } else @@ -535,7 +540,7 @@ 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) + // 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 @@ -551,7 +556,6 @@ void ClipperOffset::DoGroupOffset(Group& group) steps_per_rad_ = steps_per_360 / (2 * PI); } - std::vector::const_iterator path_rect_it = group.bounds_list.cbegin(); std::vector::const_iterator is_hole_it = group.is_hole_list.cbegin(); Paths64::const_iterator path_in_it = group.paths_in.cbegin(); @@ -561,7 +565,7 @@ void ClipperOffset::DoGroupOffset(Group& group) Path64::size_type pathLen = path_in_it->size(); path_out.clear(); - if (pathLen == 1) // single point - only valid with open paths + if (pathLen == 1) // single point { if (group_delta_ < 1) continue; const Point64& pt = (*path_in_it)[0]; @@ -586,12 +590,12 @@ void ClipperOffset::DoGroupOffset(Group& group) } solution.push_back(path_out); continue; - } // end of offsetting a single (open path) point + } // end of offsetting a single point - // when shrinking, then make sure the path can shrink that far (#593) - // but also make sure this isn't a hole which will be expanding (#715) - if ( ((group_delta_ < 0) != *is_hole_it) && - (std::min(path_rect_it->Width(), path_rect_it->Height()) < -group_delta_ * 2) ) + // 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)) @@ -637,7 +641,7 @@ void ClipperOffset::ExecuteInternal(double delta) if (std::abs(delta) < 0.5) // ie: offset is insignificant { - int64_t sol_size = 0; + Paths64::size_type sol_size = 0; for (const Group& group : groups_) sol_size += group.paths_in.size(); solution.reserve(sol_size); for (const Group& group : groups_) diff --git a/CPP/Tests/TestOffsets.cpp b/CPP/Tests/TestOffsets.cpp index a37be1e8..d5805e65 100644 --- a/CPP/Tests/TestOffsets.cpp +++ b/CPP/Tests/TestOffsets.cpp @@ -464,15 +464,15 @@ static OffsetQual GetOffsetQuality(const Path& subject, const Path& soluti const size_t subVertexCount = 4; // 1 .. 100 :) const double subVertexFrac = 1.0 / subVertexCount; - Point outPrev = solution[solution.size() - 1]; - for (const Point& outPt : solution) + Point solPrev = solution[solution.size() - 1]; + for (const Point& solPt0 : solution) { for (size_t i = 0; i < subVertexCount; ++i) { // divide each edge in solution into series of sub-vertices (solPt), PointD solPt = PointD( - static_cast(outPrev.x) + static_cast(outPt.x - outPrev.x) * subVertexFrac * i, - static_cast(outPrev.y) + static_cast(outPt.y - outPrev.y) * subVertexFrac * i); + 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; @@ -505,7 +505,7 @@ static OffsetQual GetOffsetQuality(const Path& subject, const Path& soluti oq.largestInSol = solPt; } } - outPrev = outPt; + solPrev = solPt0; } return oq; } @@ -619,3 +619,48 @@ 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 + // 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 + // (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); + +} diff --git a/CPP/Utils/Timer.h b/CPP/Utils/Timer.h index 3f1e8a7c..c8994c29 100644 --- a/CPP/Utils/Timer.h +++ b/CPP/Utils/Timer.h @@ -44,6 +44,13 @@ struct Timer { std::chrono::high_resolution_clock::now(); } + void restart() + { + paused_ = false; + duration_ = {}; + time_started_ = std::chrono::high_resolution_clock::now(); + } + void resume() { if (!paused_) return; diff --git a/CSharp/Clipper2Lib/Clipper.Offset.cs b/CSharp/Clipper2Lib/Clipper.Offset.cs index fc519537..6c95210d 100644 --- a/CSharp/Clipper2Lib/Clipper.Offset.cs +++ b/CSharp/Clipper2Lib/Clipper.Offset.cs @@ -1,6 +1,6 @@ /******************************************************************************* * Author : Angus Johnson * -* Date : 26 November 2023 * +* Date : 28 November 2023 * * Website : http://www.angusj.com * * Copyright : Angus Johnson 2010-2023 * * Purpose : Path Offset (Inflate/Shrink) * @@ -55,7 +55,7 @@ public Group(Paths64 paths, JoinType joinType, EndType endType = EndType.Polygon // get bounds of each path --> boundsList boundsList = new List(inPaths.Count); - GetMultiBounds(inPaths, boundsList, endType); + GetMultiBounds(inPaths, boundsList); if (endType == EndType.Polygon) { @@ -255,14 +255,12 @@ public void Execute(DeltaCallback64 deltaCallback, Paths64 solution) Execute(1.0, solution); } - internal static void GetMultiBounds(Paths64 paths, List boundsList, EndType endType) + internal static void GetMultiBounds(Paths64 paths, List boundsList) { - - int minPathLen = (endType == EndType.Polygon) ? 3 : 1; boundsList.Capacity = paths.Count; foreach (Path64 path in paths) { - if (path.Count < minPathLen) + if (path.Count < 1) { boundsList.Add(InvalidRect64); continue; @@ -686,16 +684,22 @@ 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) { - if (group.lowestPathIdx < 0) return; - //if (area == 0) return; // probably unhelpful (#430) + // a straight path (2 points) can now also be 'polygon' offset + // where the ends will be treated as (180 deg.) joins + if (group.lowestPathIdx < 0) _delta = Math.Abs(_delta); _groupDelta = (group.pathsReversed) ? -_delta : _delta; } else - _groupDelta = Math.Abs(_delta);// * 0.5; + _groupDelta = Math.Abs(_delta); double absDelta = Math.Abs(_groupDelta); if (!ValidateBounds(group.boundsList, absDelta)) @@ -760,13 +764,14 @@ private void DoGroupOffset(Group group) } _solution.Add(pathOut); continue; - } // end of offsetting a single (open path) point + } // end of offsetting a single point - // when shrinking, then make sure the path can shrink that far (#593) - // but also make sure this isn't a hole which will be expanding (#715) - if (((_groupDelta < 0) != isHole) && - (Math.Min(pathBounds.Width, pathBounds.Height) < -_groupDelta * 2)) - continue; + + // 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) ? diff --git a/Delphi/Clipper2Lib/Clipper.Offset.pas b/Delphi/Clipper2Lib/Clipper.Offset.pas index fdab44e0..0aa35138 100644 --- a/Delphi/Clipper2Lib/Clipper.Offset.pas +++ b/Delphi/Clipper2Lib/Clipper.Offset.pas @@ -2,7 +2,7 @@ (******************************************************************************* * Author : Angus Johnson * -* Date : 26 November 2023 * +* Date : 28 November 2023 * * Website : http://www.angusj.com * * Copyright : Angus Johnson 2010-2023 * * Purpose : Path Offset (Inflate/Shrink) * @@ -141,22 +141,19 @@ implementation // Miscellaneous offset support functions //------------------------------------------------------------------------------ -procedure GetMultiBounds(const paths: TPaths64; var list: TRect64Array; endType: TEndType); +procedure GetMultiBounds(const paths: TPaths64; var list: TRect64Array); var - i,j, len, len2, minPathLen: integer; + i,j, len, len2: integer; path: TPath64; pt1, pt: TPoint64; r: TRect64; begin - if endType = etPolygon then - minPathLen := 3 else - minPathLen := 1; len := Length(paths); for i := 0 to len -1 do begin path := paths[i]; len2 := Length(path); - if len2 < minPathLen then + if len2 < 1 then begin list[i] := InvalidRect64; continue; @@ -323,7 +320,7 @@ constructor TGroup.Create(const pathsIn: TPaths64; jt: TJoinType; et: TEndType); reversed := false; SetLength(isHoleList, len); SetLength(boundsList, len); - GetMultiBounds(paths, boundsList, et); + GetMultiBounds(paths, boundsList); if (et = etPolygon) then begin lowestPathIdx := GetLowestClosedPathIdx(boundsList); @@ -426,6 +423,15 @@ function GetPerpendicD(const pt: TPoint64; const norm: TPointD; delta: double): end; //------------------------------------------------------------------------------ +function ToggleBoolIf(val, condition: Boolean): Boolean; + {$IFDEF INLINING} inline; {$ENDIF} +begin + if condition then + Result := not val else + Result := val; +end; +//------------------------------------------------------------------------------ + procedure TClipperOffset.DoGroupOffset(group: TGroup); var i,j, len, steps: Integer; @@ -437,8 +443,7 @@ procedure TClipperOffset.DoGroupOffset(group: TGroup); if group.endType = etPolygon then begin - if (group.lowestPathIdx < 0) then Exit; - //if (area == 0) return; // probably unhelpful (#430) + if (group.lowestPathIdx < 0) then fDelta := Abs(fDelta); if group.reversed then fGroupDelta := -fDelta else fGroupDelta := fDelta; @@ -457,7 +462,8 @@ procedure TClipperOffset.DoGroupOffset(group: TGroup); // calculate a sensible number of steps (for 360 deg for the given offset if (group.joinType = jtRound) or (group.endType = etRound) then begin - // arcTol - when fArcTolerance is undefined (0), the amount of + // 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 @@ -480,9 +486,9 @@ procedure TClipperOffset.DoGroupOffset(group: TGroup); fInPath := group.paths[i]; fNorms := nil; + len := Length(fInPath); //if a single vertex then build a circle or a square ... - len := Length(fInPath); if len = 1 then begin if fGroupDelta < 1 then Continue; @@ -509,13 +515,14 @@ procedure TClipperOffset.DoGroupOffset(group: TGroup); end; UpdateSolution; Continue; - end; - - // when shrinking, then make sure the path can shrink that far (#593) - // but also make sure this isn't a hole which will be expanding (#715) - if ((fGroupDelta < 0) <> group.isHoleList[i]) and - (Min(group.boundsList[i].Width, group.boundsList[i].Height) < - fGroupDelta *2) then 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