diff --git a/CPP/BenchMark/CMakeLists.txt b/CPP/BenchMark/CMakeLists.txt new file mode 100644 index 00000000..66cb9368 --- /dev/null +++ b/CPP/BenchMark/CMakeLists.txt @@ -0,0 +1,45 @@ +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) +set(BENCHMARK_ENABLE_TESTING OFF) +message("start fetching the googlebenchmark") +FetchContent_Declare(googlebenchmark + GIT_REPOSITORY https://github.com/google/benchmark.git + GIT_TAG v1.7.1 +) + +FetchContent_MakeAvailable( + googlebenchmark) +set(gtest_force_shared_crt ON CACHE BOOL "" FORCE) +message("fetching is done") + +set(benchmark_srcs + PointInPolygonBenchmark.cpp + StripDuplicateBenchmark.cpp + # more to add +) + +# 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 + ) + + target_link_libraries(${benchmark_target} + benchmark::benchmark + ) +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/README.md b/CPP/BenchMark/README.md new file mode 100644 index 00000000..19dc75b4 --- /dev/null +++ b/CPP/BenchMark/README.md @@ -0,0 +1,6 @@ +# google benchmark +this can be enabled by setting the option in the `CPP/CMakeLists.txt` + +```cmake +option(USE_EXTERNAL_GBENCHMARK "Use the googlebenchmark" ON) +``` \ No newline at end of file diff --git a/CPP/BenchMark/StripDuplicateBenchmark.cpp b/CPP/BenchMark/StripDuplicateBenchmark.cpp new file mode 100644 index 00000000..92c07196 --- /dev/null +++ b/CPP/BenchMark/StripDuplicateBenchmark.cpp @@ -0,0 +1,85 @@ +#include "benchmark/benchmark.h" +#include "clipper2/clipper.h" +#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}); +} + +template +inline Clipper2Lib::Path +StripDuplicatesCopyVersion(const Clipper2Lib::Path &path, + bool is_closed_path) { + using namespace Clipper2Lib; + if (path.size() == 0) + return Path(); + Path result; + result.reserve(path.size()); + typename Path::const_iterator path_iter = path.cbegin(); + Point first_pt = *path_iter++, last_pt = first_pt; + result.push_back(first_pt); + for (; path_iter != path.cend(); ++path_iter) { + if (*path_iter != last_pt) { + last_pt = *path_iter; + result.push_back(last_pt); + } + } + if (!is_closed_path) + return result; + while (result.size() > 1 && result.back() == first_pt) + result.pop_back(); + 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; +} + +static void BM_StripDuplicatesCopyVersion(benchmark::State &state) { + using namespace Clipper2Lib; + Paths64 op1; + + for (auto _ : state) { + state.PauseTiming(); + int width = state.range(0); + int height = state.range(1); + int count = state.range(2); + op1.push_back(MakeRandomPoly(width, height, count)); + state.ResumeTiming(); + StripDuplicatesCopyVersion(op1, true); + } +} + +static void BM_StripDuplicates(benchmark::State &state) { + using namespace Clipper2Lib; + Paths64 op1; + for (auto _ : state) { + state.PauseTiming(); + int width = state.range(0); + int height = state.range(1); + int count = state.range(2); + op1.push_back(MakeRandomPoly(width, height, count)); + state.ResumeTiming(); + StripDuplicates(op1, 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 diff --git a/CPP/CMakeLists.txt b/CPP/CMakeLists.txt index bf121bf8..98d8ec49 100644 --- a/CPP/CMakeLists.txt +++ b/CPP/CMakeLists.txt @@ -1,8 +1,10 @@ cmake_minimum_required(VERSION 3.15) -project(Clipper2 VERSION 1.2.2 LANGUAGES C CXX) +project(Clipper2 VERSION 1.3.0 LANGUAGES C CXX) set(CMAKE_POSITION_INDEPENDENT_CODE ON) -set(CMAKE_CXX_STANDARD 17) +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) set_property(GLOBAL PROPERTY USE_FOLDERS ON) @@ -11,19 +13,29 @@ 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(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") + +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) + set(CLIPPER2_INC - Clipper2Lib/include/clipper2/clipper.h - Clipper2Lib/include/clipper2/clipper.core.h - Clipper2Lib/include/clipper2/clipper.engine.h - Clipper2Lib/include/clipper2/clipper.export.h - Clipper2Lib/include/clipper2/clipper.minkowski.h - Clipper2Lib/include/clipper2/clipper.offset.h - Clipper2Lib/include/clipper2/clipper.rectclip.h + ${CLIPPER2_INC_FOLDER}/clipper.h + ${CLIPPER2_INC_FOLDER}/clipper.version.h + ${CLIPPER2_INC_FOLDER}/clipper.core.h + ${CLIPPER2_INC_FOLDER}/clipper.engine.h + ${CLIPPER2_INC_FOLDER}/clipper.export.h + ${CLIPPER2_INC_FOLDER}/clipper.minkowski.h + ${CLIPPER2_INC_FOLDER}/clipper.offset.h + ${CLIPPER2_INC_FOLDER}/clipper.rectclip.h ) set(CLIPPER2_SRC @@ -34,11 +46,16 @@ set(CLIPPER2_SRC set(CLIPPER2_LIBS "") # one or both of Clipper2/Clipper2Z -# 2d version of Clipper2 +# primary Clipper2 library if (NOT (CLIPPER2_USINGZ STREQUAL "ONLY")) list(APPEND CLIPPER2_LIBS Clipper2) add_library(Clipper2 ${CLIPPER2_INC} ${CLIPPER2_SRC}) + target_compile_definitions( + Clipper2 + PUBLIC CLIPPER2_MAX_PRECISION=${CLIPPER2_MAX_PRECISION} + ) + target_include_directories(Clipper2 PUBLIC Clipper2Lib/include ) @@ -51,12 +68,15 @@ if (NOT (CLIPPER2_USINGZ STREQUAL "ONLY")) endif() endif() -# Clipper2 but with USINGZ defined +# secondary Clipper2 library with USINGZ defined (if required) if (NOT (CLIPPER2_USINGZ STREQUAL "OFF")) list(APPEND CLIPPER2_LIBS Clipper2Z) add_library(Clipper2Z ${CLIPPER2_INC} ${CLIPPER2_SRC}) - target_compile_definitions(Clipper2Z PUBLIC USINGZ) + target_compile_definitions( + Clipper2Z + PUBLIC USINGZ CLIPPER2_MAX_PRECISION=${CLIPPER2_MAX_PRECISION} + ) target_include_directories(Clipper2Z PUBLIC Clipper2Lib/include @@ -83,6 +103,7 @@ if(CLIPPER2_UTILS OR CLIPPER2_TESTS OR CLIPPER2_EXAMPLES) Utils/ClipFileSave.h Utils/Timer.h Utils/Colors.h + Utils/CommonUtils.h ) set(CLIPPER2_UTILS_SRC Utils/clipper.svg.cpp @@ -135,6 +156,7 @@ if(CLIPPER2_EXAMPLES) UnionClipping RectClipping SimpleClipping + VariableOffset ) foreach(ex ${EXAMPLES}) @@ -180,19 +202,20 @@ else() set_target_properties(gtest gtest_main PROPERTIES FOLDER GTest) endif() set(ClipperTests_SRC + Tests/TestExportHeaders.cpp Tests/TestLines.cpp Tests/TestOffsets.cpp Tests/TestOffsetOrientation.cpp Tests/TestOrientation.cpp Tests/TestPolygons.cpp - Tests/TestPolytreeHoles1.cpp - Tests/TestPolytreeHoles2.cpp - Tests/TestPolytreeHoles3.cpp + Tests/TestPolytreeHoles.cpp Tests/TestPolytreeIntersection.cpp Tests/TestPolytreeUnion.cpp Tests/TestRandomPaths.cpp Tests/TestRectClip.cpp + Tests/TestSimplifyPath.cpp Tests/TestTrimCollinear.cpp + Tests/TestWindows.cpp ) set(CLIPPER2_TESTS "") # one or both of ClipperTests/ClipperTestsZ @@ -203,7 +226,7 @@ endif() target_link_libraries(ClipperTests gtest gtest_main Clipper2 Clipper2utils) gtest_discover_tests(ClipperTests - # set a working directory so your project root so that you can find test data via paths relative to the project root + # set a working directory to your project root so that you can find test data via paths relative to the project root WORKING_DIRECTORY ${PROJECT_SOURCE_DIR}/../Tests PROPERTIES VS_DEBUGGER_WORKING_DIRECTORY "${PROJECT_DIR}" ) @@ -236,6 +259,10 @@ endif() file(COPY ../Tests/Polygons.txt DESTINATION ${CMAKE_BINARY_DIR} FILE_PERMISSIONS OWNER_READ GROUP_READ WORLD_READ ) endif() +if(USE_EXTERNAL_GBENCHMARK) + add_subdirectory(BenchMark) +endif() + set(CLIPPER2_PCFILES "") foreach(lib ${CLIPPER2_LIBS}) set(pc "${CMAKE_CURRENT_BINARY_DIR}/${lib}.pc") diff --git a/CPP/Clipper2Lib/include/clipper2/clipper.core.h b/CPP/Clipper2Lib/include/clipper2/clipper.core.h index c7522cb9..b3dddeea 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 : 22 March 2023 * +* Date : 24 November 2023 * * Website : http://www.angusj.com * * Copyright : Angus Johnson 2010-2023 * * Purpose : Core Clipper Library structures and functions * @@ -19,6 +19,7 @@ #include #include #include +#include "clipper2/clipper.version.h" namespace Clipper2Lib { @@ -42,15 +43,27 @@ namespace Clipper2Lib "Invalid scale (either 0 or too large)"; static const char* non_pair_error = "There must be 2 values for each coordinate"; + static const char* undefined_error = + "There is an undefined error in Clipper2"; #endif // 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 range_error_i = 64; + 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 range_error_i = 64; +#ifndef PI static const double PI = 3.141592653589793238; +#endif + +#ifdef CLIPPER2_MAX_PRECISION + const int MAX_DECIMAL_PRECISION = CLIPPER2_MAX_PRECISION; +#else + const int MAX_DECIMAL_PRECISION = 8; // see Discussions #564 +#endif + static const int64_t MAX_COORD = INT64_MAX >> 2; static const int64_t MIN_COORD = -MAX_COORD; static const int64_t INVALID = INT64_MAX; @@ -70,6 +83,8 @@ namespace Clipper2Lib throw Clipper2Exception(scale_error); case non_pair_error_i: throw Clipper2Exception(non_pair_error); + case undefined_error_i: + throw Clipper2Exception(undefined_error); case range_error_i: throw Clipper2Exception(range_error); } @@ -78,6 +93,7 @@ namespace Clipper2Lib #endif } + //By far the most widely used filling rules for polygons are EvenOdd //and NonZero, sometimes called Alternate and Winding respectively. //https://en.wikipedia.org/wiki/Nonzero-rule @@ -130,10 +146,11 @@ namespace Clipper2Lib return Point(x * scale, y * scale, z); } + void SetZ(const int64_t z_value) { z = z_value; } friend std::ostream& operator<<(std::ostream& os, const Point& point) { - os << point.x << "," << point.y << "," << point.z << " "; + os << point.x << "," << point.y << "," << point.z; return os; } @@ -170,7 +187,7 @@ namespace Clipper2Lib friend std::ostream& operator<<(std::ostream& os, const Point& point) { - os << point.x << "," << point.y << " "; + os << point.x << "," << point.y; return os; } #endif @@ -218,6 +235,14 @@ namespace Clipper2Lib using Paths64 = std::vector< Path64>; using PathsD = std::vector< PathD>; + static const Point64 InvalidPoint64 = Point64( + (std::numeric_limits::max)(), + (std::numeric_limits::max)()); + static const PointD InvalidPointD = PointD( + (std::numeric_limits::max)(), + (std::numeric_limits::max)()); + + // Rect ------------------------------------------------------------------------ template @@ -233,19 +258,13 @@ namespace Clipper2Lib T right; T bottom; - Rect() : - left(0), - top(0), - right(0), - bottom(0) {} - Rect(T l, T t, T r, T b) : left(l), top(t), right(r), bottom(b) {} - Rect(bool is_valid) + Rect(bool is_valid = true) { if (is_valid) { @@ -253,11 +272,13 @@ namespace Clipper2Lib } else { - left = top = std::numeric_limits::max(); - right = bottom = -std::numeric_limits::max(); + left = top = (std::numeric_limits::max)(); + right = bottom = (std::numeric_limits::lowest)(); } } + bool IsValid() const { return left != (std::numeric_limits::max)(); } + T Width() const { return right - left; } T Height() const { return bottom - top; } void Width(T width) { right = left + width; } @@ -305,10 +326,13 @@ namespace Clipper2Lib ((std::max)(top, rec.top) <= (std::min)(bottom, rec.bottom)); }; + bool operator==(const Rect& other) const { + return left == other.left && right == other.right && + top == other.top && bottom == other.bottom; + } + friend std::ostream& operator<<(std::ostream& os, const Rect& rect) { - os << "(" - << rect.left << "," << rect.top << "," << rect.right << "," << rect.bottom - << ")"; + os << "(" << rect.left << "," << rect.top << "," << rect.right << "," << rect.bottom << ") "; return os; } }; @@ -336,16 +360,22 @@ namespace Clipper2Lib return result; } - static const Rect64 MaxInvalidRect64 = Rect64( - INT64_MAX, INT64_MAX, INT64_MIN, INT64_MIN); - static const RectD MaxInvalidRectD = RectD( - MAX_DBL, MAX_DBL, -MAX_DBL, -MAX_DBL); + 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)()); template Rect GetBounds(const Path& path) { - auto xmin = std::numeric_limits::max(); - auto ymin = std::numeric_limits::max(); + auto xmin = (std::numeric_limits::max)(); + auto ymin = (std::numeric_limits::max)(); auto xmax = std::numeric_limits::lowest(); auto ymax = std::numeric_limits::lowest(); for (const auto& p : path) @@ -361,8 +391,8 @@ namespace Clipper2Lib template Rect GetBounds(const Paths& paths) { - auto xmin = std::numeric_limits::max(); - auto ymin = std::numeric_limits::max(); + auto xmin = (std::numeric_limits::max)(); + auto ymin = (std::numeric_limits::max)(); auto xmax = std::numeric_limits::lowest(); auto ymax = std::numeric_limits::lowest(); for (const Path& path : paths) @@ -426,7 +456,7 @@ namespace Clipper2Lib } template - inline Path ScalePath(const Path& path, + inline Path ScalePath(const Path& path, double scale, int& error_code) { return ScalePath(path, scale, scale, error_code); @@ -486,26 +516,6 @@ namespace Clipper2Lib return result; } - inline PathD Path64ToPathD(const Path64& path) - { - return TransformPath(path); - } - - inline PathsD Paths64ToPathsD(const Paths64& paths) - { - return TransformPaths(paths); - } - - inline Path64 PathDToPath64(const PathD& path) - { - return TransformPath(path); - } - - inline Paths64 PathsDToPaths64(const PathsD& paths) - { - return TransformPaths(paths); - } - template inline double Sqr(T val) { @@ -558,48 +568,32 @@ namespace Clipper2Lib } template - inline Path StripDuplicates(const Path& path, bool is_closed_path) + inline void StripDuplicates( 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(); - Point first_pt = *path_iter++, last_pt = first_pt; - result.push_back(first_pt); - for (; path_iter != path.cend(); ++path_iter) - { - if (*path_iter != last_pt) - { - last_pt = *path_iter; - result.push_back(last_pt); - } - } - if (!is_closed_path) return result; - while (result.size() > 1 && result.back() == first_pt) result.pop_back(); - return result; + //https://stackoverflow.com/questions/1041620/whats-the-most-efficient-way-to-erase-duplicates-and-sort-a-vector#:~:text=Let%27s%20compare%20three%20approaches%3A + 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(); } template - inline Paths StripDuplicates(const Paths& paths, bool is_closed_path) + inline void StripDuplicates( Paths& paths, bool is_closed_path) { - Paths result; - result.reserve(paths.size()); - for (typename Paths::const_iterator paths_citer = paths.cbegin(); - paths_citer != paths.cend(); ++paths_citer) + for (typename Paths::iterator paths_citer = paths.begin(); + paths_citer != paths.end(); ++paths_citer) { - result.push_back(StripDuplicates(*paths_citer, is_closed_path)); + StripDuplicates(*paths_citer, is_closed_path); } - return result; } // Miscellaneous ------------------------------------------------------------ inline void CheckPrecision(int& precision, int& error_code) { - if (precision >= -8 && precision <= 8) return; + if (precision >= -MAX_DECIMAL_PRECISION && precision <= MAX_DECIMAL_PRECISION) return; error_code |= precision_error_i; // non-fatal error - DoError(precision_error_i); // unless exceptions enabled - precision = precision > 8 ? 8 : -8; + DoError(precision_error_i); // does nothing unless exceptions enabled + precision = precision > 0 ? MAX_DECIMAL_PRECISION : -MAX_DECIMAL_PRECISION; } inline void CheckPrecision(int& precision) @@ -691,29 +685,27 @@ namespace Clipper2Lib //nb: This statement is premised on using Cartesian coordinates return Area(poly) >= 0; } - - inline int64_t CheckCastInt64(double val) - { - if ((val >= max_coord) || (val <= min_coord)) return INVALID; - else return static_cast(val); - } - + 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); 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 0; - double qx = dx1 * ln1a.y - dy1 * ln1a.x; - double qy = dx2 * ln2a.y - dy2 * ln2a.x; - ip.x = CheckCastInt64((dx1 * qy - dx2 * qx) / det); - ip.y = CheckCastInt64((dy1 * qy - dy2 * qx) / det); - return (ip.x != INVALID && ip.y != INVALID); + 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 + else + { + ip.x = static_cast(ln1a.x + t * dx1); + ip.y = static_cast(ln1a.y + t * dy1); + } + return true; } inline bool SegmentsIntersect(const Point64& seg1a, const Point64& seg1b, @@ -737,8 +729,9 @@ namespace Clipper2Lib } } - inline Point64 GetClosestPointOnSegment(const Point64& offPt, - const Point64& seg1, const Point64& seg2) + template + inline Point GetClosestPointOnSegment(const Point& offPt, + const Point& seg1, const Point& seg2) { if (seg1.x == seg2.x && seg1.y == seg2.y) return seg1; double dx = static_cast(seg2.x - seg1.x); @@ -748,9 +741,14 @@ namespace Clipper2Lib static_cast(offPt.y - seg1.y) * dy) / (Sqr(dx) + Sqr(dy)); if (q < 0) q = 0; else if (q > 1) q = 1; - return Point64( - seg1.x + static_cast(nearbyint(q * dx)), - seg1.y + static_cast(nearbyint(q * dy))); + if constexpr (std::numeric_limits::is_integer) + return Point( + seg1.x + static_cast(nearbyint(q * dx)), + seg1.y + static_cast(nearbyint(q * dy))); + else + return Point( + seg1.x + static_cast(q * dx), + seg1.y + static_cast(q * dy)); } enum class PointInPolygonResult { IsOn, IsInside, IsOutside }; diff --git a/CPP/Clipper2Lib/include/clipper2/clipper.engine.h b/CPP/Clipper2Lib/include/clipper2/clipper.engine.h index 30dc6c86..13c7f069 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 : 26 March 2023 * +* Date : 22 November 2023 * * Website : http://www.angusj.com * * Copyright : Angus Johnson 2010-2023 * * Purpose : This is the main polygon clipping module * @@ -10,9 +10,8 @@ #ifndef CLIPPER_ENGINE_H #define CLIPPER_ENGINE_H -constexpr auto CLIPPER2_VERSION = "1.2.2"; - #include +#include //#541 #include #include #include @@ -20,7 +19,7 @@ constexpr auto CLIPPER2_VERSION = "1.2.2"; #include #include -#include "clipper.core.h" +#include "clipper2/clipper.core.h" namespace Clipper2Lib { @@ -91,10 +90,11 @@ namespace Clipper2Lib { OutPt* pts = nullptr; PolyPath* polypath = nullptr; OutRecList* splits = nullptr; + OutRec* recursive_split = nullptr; Rect64 bounds = {}; Path64 path; bool is_open = false; - bool horz_done = false; + ~OutRec() { if (splits) delete splits; // nb: don't delete the split pointers @@ -179,6 +179,20 @@ namespace Clipper2Lib { typedef std::vector LocalMinimaList; typedef std::vector IntersectNodeList; + // ReuseableDataContainer64 ------------------------------------------------ + + class ReuseableDataContainer64 { + private: + friend class ClipperBase; + LocalMinimaList minima_list_; + std::vector vertex_lists_; + void AddLocMin(Vertex& vert, PathType polytype, bool is_open); + public: + virtual ~ReuseableDataContainer64(); + void Clear(); + void AddPaths(const Paths64& paths, PathType polytype, bool is_open); + }; + // ClipperBase ------------------------------------------------------------- class ClipperBase { @@ -235,7 +249,6 @@ namespace Clipper2Lib { void DoTopOfScanbeam(const int64_t top_y); Active *DoMaxima(Active &e); void JoinOutrecPaths(Active &e1, Active &e2); - void CompleteSplit(OutPt* op1, OutPt* op2, OutRec& outrec); void FixSelfIntersects(OutRec* outrec); void DoSplitOp(OutRec* outRec, OutPt* splitOp); @@ -249,6 +262,8 @@ namespace Clipper2Lib { inline void CheckJoinRight(Active& e, const Point64& pt, bool check_curr_x = false); protected: + bool preserve_collinear_ = true; + bool reverse_solution_ = false; int error_code_ = 0; bool has_open_paths_ = false; bool succeeded_ = true; @@ -256,8 +271,8 @@ namespace Clipper2Lib { bool ExecuteInternal(ClipType ct, FillRule ft, bool use_polytrees); void CleanCollinear(OutRec* outrec); bool CheckBounds(OutRec* outrec); + bool CheckSplitOwner(OutRec* outrec, OutRecList* splits); void RecursiveCheckOwners(OutRec* outrec, PolyPath* polypath); - void DeepCheckOwners(OutRec* outrec, PolyPath* polypath); #ifdef USINGZ ZCallback64 zCallback_ = nullptr; void SetZ(const Active& e1, const Active& e2, Point64& pt); @@ -267,10 +282,13 @@ namespace Clipper2Lib { void AddPaths(const Paths64& paths, PathType polytype, bool is_open); public: virtual ~ClipperBase(); - int ErrorCode() { return error_code_; }; - bool PreserveCollinear = true; - bool ReverseSolution = false; + int ErrorCode() const { return error_code_; }; + void PreserveCollinear(bool val) { preserve_collinear_ = val; }; + bool PreserveCollinear() const { return preserve_collinear_;}; + void ReverseSolution(bool val) { reverse_solution_ = val; }; + bool ReverseSolution() const { return reverse_solution_; }; void Clear(); + void AddReuseableData(const ReuseableDataContainer64& reuseable_data); #ifdef USINGZ int64_t DefaultZ = 0; #endif @@ -330,12 +348,12 @@ namespace Clipper2Lib { childs_.resize(0); } - const PolyPath64* operator [] (size_t index) const + PolyPath64* operator [] (size_t index) const { - return childs_[index].get(); + return childs_[index].get(); //std::unique_ptr } - const PolyPath64* Child(size_t index) const + PolyPath64* Child(size_t index) const { return childs_[index].get(); } @@ -375,24 +393,24 @@ namespace Clipper2Lib { class PolyPathD : public PolyPath { private: PolyPathDList childs_; - double inv_scale_; + double scale_; PathD polygon_; public: explicit PolyPathD(PolyPathD* parent = nullptr) : PolyPath(parent) { - inv_scale_ = parent ? parent->inv_scale_ : 1.0; + scale_ = parent ? parent->scale_ : 1.0; } ~PolyPathD() { childs_.resize(0); } - const PolyPathD* operator [] (size_t index) const + PolyPathD* operator [] (size_t index) const { return childs_[index].get(); } - const PolyPathD* Child(size_t index) const + PolyPathD* Child(size_t index) const { return childs_[index].get(); } @@ -400,14 +418,23 @@ namespace Clipper2Lib { PolyPathDList::const_iterator begin() const { return childs_.cbegin(); } PolyPathDList::const_iterator end() const { return childs_.cend(); } - void SetInvScale(double value) { inv_scale_ = value; } - double InvScale() { return inv_scale_; } + void SetScale(double value) { scale_ = value; } + double Scale() const { return scale_; } + PolyPathD* AddChild(const Path64& path) override { int error_code = 0; auto p = std::make_unique(this); PolyPathD* result = childs_.emplace_back(std::move(p)).get(); - result->polygon_ = ScalePath(path, inv_scale_, error_code); + result->polygon_ = ScalePath(path, scale_, error_code); + return result; + } + + PolyPathD* AddChild(const PathD& path) + { + auto p = std::make_unique(this); + PolyPathD* result = childs_.emplace_back(std::move(p)).get(); + result->polygon_ = path; return result; } @@ -595,7 +622,7 @@ namespace Clipper2Lib { if (ExecuteInternal(clip_type, fill_rule, true)) { polytree.Clear(); - polytree.SetInvScale(invScale_); + polytree.SetScale(invScale_); open_paths.clear(); BuildTreeD(polytree, open_paths); } diff --git a/CPP/Clipper2Lib/include/clipper2/clipper.export.h b/CPP/Clipper2Lib/include/clipper2/clipper.export.h index e8d678a4..d7286132 100644 --- a/CPP/Clipper2Lib/include/clipper2/clipper.export.h +++ b/CPP/Clipper2Lib/include/clipper2/clipper.export.h @@ -1,39 +1,78 @@ /******************************************************************************* * Author : Angus Johnson * -* Date : 23 March 2023 * +* Date : 26 November 2023 * * Website : http://www.angusj.com * * Copyright : Angus Johnson 2010-2023 * * Purpose : This module exports the Clipper2 Library (ie DLL/so) * * License : http://www.boost.org/LICENSE_1_0.txt * *******************************************************************************/ -// The exported functions below refer to simple structures that -// can be understood across multiple languages. Consequently -// Path64, PathD, Polytree64 etc are converted from C++ classes -// (std::vector<> etc) into the following data structures: -// -// CPath64 (int64_t*) & CPathD (double_t*): -// Path64 and PathD are converted into arrays of x,y coordinates. -// However in these arrays the first x,y coordinate pair is a -// counter with 'x' containing the number of following coordinate -// pairs. ('y' should be 0, with one exception explained below.) -// __________________________________ -// |counter|coord1|coord2|...|coordN| -// |N ,0 |x1, y1|x2, y2|...|xN, yN| -// __________________________________ -// -// CPaths64 (int64_t**) & CPathsD (double_t**): -// These are arrays of pointers to CPath64 and CPathD where -// the first pointer is to a 'counter path'. This 'counter -// path' has a single x,y coord pair with 'y' (not 'x') -// containing the number of paths that follow. ('x' = 0). -// _______________________________ -// |counter|path1|path2|...|pathN| -// |addr0 |addr1|addr2|...|addrN| (*addr0[0]=0; *addr0[1]=N) -// _______________________________ -// -// The structures of CPolytree64 and CPolytreeD are defined -// below and these structures don't need to be explained here. + +/* + Boolean clipping: + cliptype: None=0, Intersection=1, Union=2, Difference=3, Xor=4 + fillrule: EvenOdd=0, NonZero=1, Positive=2, Negative=3 + + Polygon offsetting (inflate/deflate): + jointype: Square=0, Bevel=1, Round=2, Miter=3 + endtype: Polygon=0, Joined=1, Butt=2, Square=3, Round=4 + +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). + +CPath64 and CPathD: +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| +|N, 0 |x1, y1|x2, y2|...|xN, yN| +__________________________________ + +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. +_______________________________ +|counter|path1|path2|...|pathC| +|A , C | | +_______________________________ + +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 +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 +indicating the length of the path (N) and the number of child CPolyPath (C). +____________________________________________________________ +|counter|coord1|coord2|...|coordN| child1|child2|...|childC| +|N , C |x1, y1|x2, y2|...|xN, yN| | +____________________________________________________________ + +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. + +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 safe way to free up +this memory is to use the exported DisposeArray64 and DisposeArrayD +functions below. +*/ + #ifndef CLIPPER2_EXPORT_H #define CLIPPER2_EXPORT_H @@ -49,25 +88,14 @@ namespace Clipper2Lib { typedef int64_t* CPath64; -typedef int64_t** CPaths64; -typedef double* CPathD; -typedef double** CPathsD; - -typedef struct CPolyPath64 { - CPath64 polygon; - uint32_t is_hole; - uint32_t child_count; - CPolyPath64* childs; -} -CPolyTree64; +typedef int64_t* CPaths64; +typedef double* CPathD; +typedef double* CPathsD; -typedef struct CPolyPathD { - CPathD polygon; - uint32_t is_hole; - uint32_t child_count; - CPolyPathD* childs; -} -CPolyTreeD; +typedef int64_t* CPolyPath64; +typedef int64_t* CPolyTree64; +typedef double* CPolyPathD; +typedef double* CPolyTreeD; template struct CRect { @@ -97,57 +125,53 @@ inline Rect CRectToRect(const CRect& rect) return result; } -#define EXTERN_DLL_EXPORT extern "C" __declspec(dllexport) +#ifdef _WIN32 + #define EXTERN_DLL_EXPORT extern "C" __declspec(dllexport) +#else + #define EXTERN_DLL_EXPORT extern "C" +#endif + ////////////////////////////////////////////////////// -// EXPORTED FUNCTION DEFINITIONS +// EXPORTED FUNCTION DECLARATIONS ////////////////////////////////////////////////////// EXTERN_DLL_EXPORT const char* Version(); -// Some of the functions below will return data in the various CPath -// and CPolyTree structures which are pointers to heap allocated -// memory. Eventually this memory will need to be released with one -// of the following 'DisposeExported' functions. (This may be the -// only safe way to release this memory since the executable -// accessing these exported functions may use a memory manager that -// allocates and releases heap memory in a different way. Also, -// CPath structures that have been constructed by the executable -// should not be destroyed using these 'DisposeExported' functions.) -EXTERN_DLL_EXPORT void DisposeExportedCPath64(CPath64 p); -EXTERN_DLL_EXPORT void DisposeExportedCPaths64(CPaths64& pp); -EXTERN_DLL_EXPORT void DisposeExportedCPathD(CPathD p); -EXTERN_DLL_EXPORT void DisposeExportedCPathsD(CPathsD& pp); -EXTERN_DLL_EXPORT void DisposeExportedCPolyTree64(CPolyTree64*& cpt); -EXTERN_DLL_EXPORT void DisposeExportedCPolyTreeD(CPolyTreeD*& cpt); - -// Boolean clipping: -// cliptype: None=0, Intersection=1, Union=2, Difference=3, Xor=4 -// fillrule: EvenOdd=0, NonZero=1, Positive=2, Negative=3 +EXTERN_DLL_EXPORT void DisposeArray64(int64_t*& p) +{ + delete[] p; +} + +EXTERN_DLL_EXPORT void DisposeArrayD(double*& p) +{ + delete[] p; +} + 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, bool preserve_collinear = true, bool reverse_solution = false); -EXTERN_DLL_EXPORT int BooleanOpPt64(uint8_t cliptype, + +EXTERN_DLL_EXPORT int BooleanOp_PolyTree64(uint8_t cliptype, uint8_t fillrule, const CPaths64 subjects, const CPaths64 subjects_open, const CPaths64 clips, - CPolyTree64*& solution, CPaths64& solution_open, + CPolyTree64& sol_tree, CPaths64& solution_open, bool preserve_collinear = true, bool reverse_solution = false); + EXTERN_DLL_EXPORT int BooleanOpD(uint8_t cliptype, uint8_t fillrule, const CPathsD subjects, const CPathsD subjects_open, const CPathsD clips, CPathsD& solution, CPathsD& solution_open, int precision = 2, bool preserve_collinear = true, bool reverse_solution = false); -EXTERN_DLL_EXPORT int BooleanOpPtD(uint8_t cliptype, + +EXTERN_DLL_EXPORT int BooleanOp_PolyTreeD(uint8_t cliptype, uint8_t fillrule, const CPathsD subjects, const CPathsD subjects_open, const CPathsD clips, - CPolyTreeD*& solution, CPathsD& solution_open, int precision = 2, + CPolyTreeD& solution, CPathsD& solution_open, int precision = 2, bool preserve_collinear = true, bool reverse_solution = false); -// Polygon offsetting (inflate/deflate): -// jointype: Square=0, Round=1, Miter=2 -// endtype: Polygon=0, Joined=1, Butt=2, Square=3, Round=4 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, @@ -157,78 +181,185 @@ EXTERN_DLL_EXPORT CPathsD InflatePathsD(const CPathsD paths, int precision = 2, double miter_limit = 2.0, double arc_tolerance = 0.0, bool reverse_solution = false); -// ExecuteRectClip & ExecuteRectClipLines: -EXTERN_DLL_EXPORT CPaths64 ExecuteRectClip64(const CRect64& rect, - const CPaths64 paths, bool convex_only = false); -EXTERN_DLL_EXPORT CPathsD ExecuteRectClipD(const CRectD& rect, - const CPathsD paths, int precision = 2, bool convex_only = false); -EXTERN_DLL_EXPORT CPaths64 ExecuteRectClipLines64(const CRect64& rect, +// RectClip & RectClipLines: +EXTERN_DLL_EXPORT CPaths64 RectClip64(const CRect64& rect, + const CPaths64 paths); +EXTERN_DLL_EXPORT CPathsD RectClipD(const CRectD& rect, + const CPathsD paths, int precision = 2); +EXTERN_DLL_EXPORT CPaths64 RectClipLines64(const CRect64& rect, const CPaths64 paths); -EXTERN_DLL_EXPORT CPathsD ExecuteRectClipLinesD(const CRectD& rect, +EXTERN_DLL_EXPORT CPathsD RectClipLinesD(const CRectD& rect, const CPathsD paths, int precision = 2); ////////////////////////////////////////////////////// // INTERNAL FUNCTIONS ////////////////////////////////////////////////////// -inline CPath64 CreateCPath64(size_t cnt1, size_t cnt2); -inline CPath64 CreateCPath64(const Path64& p); -inline CPaths64 CreateCPaths64(const Paths64& pp); -inline Path64 ConvertCPath64(const CPath64& p); -inline Paths64 ConvertCPaths64(const CPaths64& pp); +template +static void GetPathCountAndCPathsArrayLen(const Paths& paths, + size_t& cnt, size_t& array_len) +{ + array_len = 2; + cnt = 0; + for (const Path& path : paths) + if (path.size()) + { + array_len += path.size() * 2 + 2; + ++cnt; + } +} -inline CPathD CreateCPathD(size_t cnt1, size_t cnt2); -inline CPathD CreateCPathD(const PathD& p); -inline CPathsD CreateCPathsD(const PathsD& pp); -inline PathD ConvertCPathD(const CPathD& p); -inline PathsD ConvertCPathsD(const CPathsD& pp); +static size_t GetPolyPath64ArrayLen(const PolyPath64& pp) +{ + size_t result = 2; // poly_length + child_count + result += pp.Polygon().size() * 2; + //plus nested children :) + for (size_t i = 0; i < pp.Count(); ++i) + result += GetPolyPath64ArrayLen(*pp[i]); + return result; +} -// the following function avoid multiple conversions -inline CPathD CreateCPathD(const Path64& p, double scale); -inline CPathsD CreateCPathsD(const Paths64& pp, double scale); -inline Path64 ConvertCPathD(const CPathD& p, double scale); -inline Paths64 ConvertCPathsD(const CPathsD& pp, double scale); +static void GetPolytreeCountAndCStorageSize(const PolyTree64& tree, + size_t& cnt, size_t& array_len) +{ + cnt = tree.Count(); // nb: top level count only + array_len = GetPolyPath64ArrayLen(tree); +} -inline CPolyTree64* CreateCPolyTree64(const PolyTree64& pt); -inline CPolyTreeD* CreateCPolyTreeD(const PolyTree64& pt, double scale); +template +static T* CreateCPaths(const Paths& paths) +{ + size_t cnt = 0, array_len = 0; + GetPathCountAndCPathsArrayLen(paths, cnt, array_len); + T* result = new T[array_len], * v = result; + *v++ = array_len; + *v++ = cnt; + for (const Path& path : paths) + { + if (!path.size()) continue; + *v++ = path.size(); + *v++ = 0; + for (const Point& pt : path) + { + *v++ = pt.x; + *v++ = pt.y; + } + } + return result; +} -EXTERN_DLL_EXPORT const char* Version() + +CPathsD CreateCPathsDFromPaths64(const Paths64& paths, double scale) { - return CLIPPER2_VERSION; + if (!paths.size()) return nullptr; + size_t cnt, array_len; + GetPathCountAndCPathsArrayLen(paths, cnt, array_len); + CPathsD result = new double[array_len], v = result; + *v++ = (double)array_len; + *v++ = (double)cnt; + for (const Path64& path : paths) + { + if (!path.size()) continue; + *v = (double)path.size(); + ++v; *v++ = 0; + for (const Point64& pt : path) + { + *v++ = pt.x * scale; + *v++ = pt.y * scale; + } + } + return result; } -EXTERN_DLL_EXPORT void DisposeExportedCPath64(CPath64 p) +template +static Paths ConvertCPaths(T* paths) +{ + Paths result; + if (!paths) return result; + T* v = paths; ++v; + size_t cnt = *v++; + result.reserve(cnt); + for (size_t i = 0; i < cnt; ++i) + { + size_t cnt2 = *v; + v += 2; + Path path; + path.reserve(cnt2); + for (size_t j = 0; j < cnt2; ++j) + { + T x = *v++, y = *v++; + path.push_back(Point(x, y)); + } + result.push_back(path); + } + return result; +} + + +static Paths64 ConvertCPathsDToPaths64(const CPathsD paths, double scale) { - if (p) delete[] p; + Paths64 result; + if (!paths) return result; + double* v = paths; + ++v; // skip the first value (0) + int64_t cnt = (int64_t)*v++; + result.reserve(cnt); + for (int i = 0; i < cnt; ++i) + { + int64_t cnt2 = (int64_t)*v; + v += 2; + Path64 path; + path.reserve(cnt2); + for (int j = 0; j < cnt2; ++j) + { + double x = *v++ * scale; + double y = *v++ * scale; + path.push_back(Point64(x, y)); + } + result.push_back(path); + } + return result; } -EXTERN_DLL_EXPORT void DisposeExportedCPaths64(CPaths64& pp) +template +static void CreateCPolyPath(const PolyPath64* pp, T*& v, T scale) { - if (!pp) return; - CPaths64 v = pp; - CPath64 cnts = *v; - const size_t cnt = static_cast(cnts[1]); - for (size_t i = 0; i <= cnt; ++i) //nb: cnt +1 - DisposeExportedCPath64(*v++); - delete[] pp; - pp = nullptr; + *v++ = static_cast(pp->Polygon().size()); + *v++ = static_cast(pp->Count()); + for (const Point64& pt : pp->Polygon()) + { + *v++ = static_cast(pt.x * scale); + *v++ = static_cast(pt.y * scale); + } + for (size_t i = 0; i < pp->Count(); ++i) + CreateCPolyPath(pp->Child(i), v, scale); } -EXTERN_DLL_EXPORT void DisposeExportedCPathD(CPathD p) +template +static T* CreateCPolyTree(const PolyTree64& tree, T scale) { - if (p) delete[] p; + if (scale == 0) scale = 1; + size_t cnt, array_len; + GetPolytreeCountAndCStorageSize(tree, cnt, array_len); + if (!cnt) return nullptr; + // allocate storage + T* result = new T[array_len]; + T* v = result; + + *v++ = static_cast(array_len); + *v++ = static_cast(tree.Count()); + for (size_t i = 0; i < tree.Count(); ++i) + CreateCPolyPath(tree.Child(i), v, scale); + return result; } -EXTERN_DLL_EXPORT void DisposeExportedCPathsD(CPathsD& pp) +////////////////////////////////////////////////////// +// EXPORTED FUNCTION DEFINITIONS +////////////////////////////////////////////////////// + +EXTERN_DLL_EXPORT const char* Version() { - if (!pp) return; - CPathsD v = pp; - CPathD cnts = *v; - size_t cnt = static_cast(cnts[1]); - for (size_t i = 0; i <= cnt; ++i) //nb: cnt +1 - DisposeExportedCPathD(*v++); - delete[] pp; - pp = nullptr; + return CLIPPER2_VERSION; } EXTERN_DLL_EXPORT int BooleanOp64(uint8_t cliptype, @@ -241,48 +372,48 @@ EXTERN_DLL_EXPORT int BooleanOp64(uint8_t cliptype, if (fillrule > static_cast(FillRule::Negative)) return -3; Paths64 sub, sub_open, clp, sol, sol_open; - sub = ConvertCPaths64(subjects); - sub_open = ConvertCPaths64(subjects_open); - clp = ConvertCPaths64(clips); + sub = ConvertCPaths(subjects); + sub_open = ConvertCPaths(subjects_open); + clp = ConvertCPaths(clips); Clipper64 clipper; - clipper.PreserveCollinear = preserve_collinear; - clipper.ReverseSolution = reverse_solution; + clipper.PreserveCollinear(preserve_collinear); + clipper.ReverseSolution(reverse_solution); 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)) return -1; // clipping bug - should never happen :) - solution = CreateCPaths64(sol); - solution_open = CreateCPaths64(sol_open); + solution = CreateCPaths(sol); + solution_open = CreateCPaths(sol_open); return 0; //success !! } -EXTERN_DLL_EXPORT int BooleanOpPt64(uint8_t cliptype, +EXTERN_DLL_EXPORT int BooleanOp_PolyTree64(uint8_t cliptype, uint8_t fillrule, const CPaths64 subjects, const CPaths64 subjects_open, const CPaths64 clips, - CPolyTree64*& solution, CPaths64& solution_open, + CPolyTree64& sol_tree, CPaths64& solution_open, bool preserve_collinear, bool reverse_solution) { if (cliptype > static_cast(ClipType::Xor)) return -4; if (fillrule > static_cast(FillRule::Negative)) return -3; Paths64 sub, sub_open, clp, sol_open; - sub = ConvertCPaths64(subjects); - sub_open = ConvertCPaths64(subjects_open); - clp = ConvertCPaths64(clips); + sub = ConvertCPaths(subjects); + sub_open = ConvertCPaths(subjects_open); + clp = ConvertCPaths(clips); - PolyTree64 pt; + PolyTree64 tree; Clipper64 clipper; - clipper.PreserveCollinear = preserve_collinear; - clipper.ReverseSolution = reverse_solution; + clipper.PreserveCollinear(preserve_collinear); + clipper.ReverseSolution(reverse_solution); 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), pt, sol_open)) + if (!clipper.Execute(ClipType(cliptype), FillRule(fillrule), tree, sol_open)) return -1; // clipping bug - should never happen :) - solution = CreateCPolyTree64(pt); - solution_open = CreateCPaths64(sol_open); + sol_tree = CreateCPolyTree(tree, (int64_t)1); + solution_open = CreateCPaths(sol_open); return 0; //success !! } @@ -298,57 +429,54 @@ EXTERN_DLL_EXPORT int BooleanOpD(uint8_t cliptype, const double scale = std::pow(10, precision); Paths64 sub, sub_open, clp, sol, sol_open; - sub = ConvertCPathsD(subjects, scale); - sub_open = ConvertCPathsD(subjects_open, scale); - clp = ConvertCPathsD(clips, scale); + sub = ConvertCPathsDToPaths64(subjects, scale); + sub_open = ConvertCPathsDToPaths64(subjects_open, scale); + clp = ConvertCPathsDToPaths64(clips, scale); Clipper64 clipper; - clipper.PreserveCollinear = preserve_collinear; - clipper.ReverseSolution = reverse_solution; + clipper.PreserveCollinear(preserve_collinear); + clipper.ReverseSolution(reverse_solution); if (sub.size() > 0) clipper.AddSubject(sub); - if (sub_open.size() > 0) - clipper.AddOpenSubject(sub_open); + 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)) return -1; - - if (sol.size() > 0) solution = CreateCPathsD(sol, 1 / scale); - if (sol_open.size() > 0) - solution_open = CreateCPathsD(sol_open, 1 / scale); + solution = CreateCPathsDFromPaths64(sol, 1 / scale); + solution_open = CreateCPathsDFromPaths64(sol_open, 1 / scale); return 0; } -EXTERN_DLL_EXPORT int BooleanOpPtD(uint8_t cliptype, +EXTERN_DLL_EXPORT int BooleanOp_PolyTreeD(uint8_t cliptype, uint8_t fillrule, const CPathsD subjects, const CPathsD subjects_open, const CPathsD clips, - CPolyTreeD*& solution, CPathsD& solution_open, int precision, + CPolyTreeD& solution, CPathsD& solution_open, int precision, bool preserve_collinear, bool reverse_solution) { if (precision < -8 || precision > 8) return -5; if (cliptype > static_cast(ClipType::Xor)) return -4; if (fillrule > static_cast(FillRule::Negative)) return -3; - const double scale = std::pow(10, precision); + double scale = std::pow(10, precision); + + int err = 0; Paths64 sub, sub_open, clp, sol_open; - sub = ConvertCPathsD(subjects, scale); - sub_open = ConvertCPathsD(subjects_open, scale); - clp = ConvertCPathsD(clips, scale); + sub = ConvertCPathsDToPaths64(subjects, scale); + sub_open = ConvertCPathsDToPaths64(subjects_open, scale); + clp = ConvertCPathsDToPaths64(clips, scale); - PolyTree64 sol; + PolyTree64 tree; Clipper64 clipper; - clipper.PreserveCollinear = preserve_collinear; - clipper.ReverseSolution = reverse_solution; + clipper.PreserveCollinear(preserve_collinear); + clipper.ReverseSolution(reverse_solution); if (sub.size() > 0) clipper.AddSubject(sub); - if (sub_open.size() > 0) - clipper.AddOpenSubject(sub_open); + 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)) return -1; + if (!clipper.Execute(ClipType(cliptype), FillRule(fillrule), tree, sol_open)) + return -1; // clipping bug - should never happen :) - solution = CreateCPolyTreeD(sol, 1 / scale); - if (sol_open.size() > 0) - solution_open = CreateCPathsD(sol_open, 1 / scale); - return 0; + solution = CreateCPolyTree(tree, 1/scale); + solution_open = CreateCPathsDFromPaths64(sol_open, 1 / scale); + return 0; //success !! } EXTERN_DLL_EXPORT CPaths64 InflatePaths64(const CPaths64 paths, @@ -356,14 +484,13 @@ EXTERN_DLL_EXPORT CPaths64 InflatePaths64(const CPaths64 paths, double arc_tolerance, bool reverse_solution) { Paths64 pp; - pp = ConvertCPaths64(paths); - + pp = ConvertCPaths(paths); ClipperOffset clip_offset( miter_limit, arc_tolerance, reverse_solution); clip_offset.AddPaths(pp, JoinType(jointype), EndType(endtype)); Paths64 result; clip_offset.Execute(delta, result); - return CreateCPaths64(result); + return CreateCPaths(result); } EXTERN_DLL_EXPORT CPathsD InflatePathsD(const CPathsD paths, @@ -372,28 +499,28 @@ EXTERN_DLL_EXPORT CPathsD InflatePathsD(const CPathsD paths, double arc_tolerance, bool reverse_solution) { if (precision < -8 || precision > 8 || !paths) return nullptr; + const double scale = std::pow(10, precision); ClipperOffset clip_offset(miter_limit, arc_tolerance, reverse_solution); - Paths64 pp = ConvertCPathsD(paths, scale); + Paths64 pp = ConvertCPathsDToPaths64(paths, scale); clip_offset.AddPaths(pp, JoinType(jointype), EndType(endtype)); Paths64 result; clip_offset.Execute(delta * scale, result); - return CreateCPathsD(result, 1/scale); + + return CreateCPathsDFromPaths64(result, 1 / scale); } -EXTERN_DLL_EXPORT CPaths64 ExecuteRectClip64(const CRect64& rect, - const CPaths64 paths, bool convex_only) +EXTERN_DLL_EXPORT CPaths64 RectClip64(const CRect64& rect, const CPaths64 paths) { if (CRectIsEmpty(rect) || !paths) return nullptr; Rect64 r64 = CRectToRect(rect); - class RectClip rc(r64); - Paths64 pp = ConvertCPaths64(paths); - Paths64 result = rc.Execute(pp, convex_only); - return CreateCPaths64(result); + class RectClip64 rc(r64); + Paths64 pp = ConvertCPaths(paths); + Paths64 result = rc.Execute(pp); + return CreateCPaths(result); } -EXTERN_DLL_EXPORT CPathsD ExecuteRectClipD(const CRectD& rect, - const CPathsD paths, int precision, bool convex_only) +EXTERN_DLL_EXPORT CPathsD RectClipD(const CRectD& rect, const CPathsD paths, int precision) { if (CRectIsEmpty(rect) || !paths) return nullptr; if (precision < -8 || precision > 8) return nullptr; @@ -401,372 +528,36 @@ EXTERN_DLL_EXPORT CPathsD ExecuteRectClipD(const CRectD& rect, RectD r = CRectToRect(rect); Rect64 rec = ScaleRect(r, scale); - Paths64 pp = ConvertCPathsD(paths, scale); - class RectClip rc(rec); - Paths64 result = rc.Execute(pp, convex_only); - return CreateCPathsD(result, 1/scale); + Paths64 pp = ConvertCPathsDToPaths64(paths, scale); + class RectClip64 rc(rec); + Paths64 result = rc.Execute(pp); + + return CreateCPathsDFromPaths64(result, 1 / scale); } -EXTERN_DLL_EXPORT CPaths64 ExecuteRectClipLines64(const CRect64& rect, +EXTERN_DLL_EXPORT CPaths64 RectClipLines64(const CRect64& rect, const CPaths64 paths) { if (CRectIsEmpty(rect) || !paths) return nullptr; Rect64 r = CRectToRect(rect); - class RectClipLines rcl (r); - Paths64 pp = ConvertCPaths64(paths); + class RectClipLines64 rcl (r); + Paths64 pp = ConvertCPaths(paths); Paths64 result = rcl.Execute(pp); - return CreateCPaths64(result); + return CreateCPaths(result); } -EXTERN_DLL_EXPORT CPathsD ExecuteRectClipLinesD(const CRectD& rect, +EXTERN_DLL_EXPORT CPathsD RectClipLinesD(const CRectD& rect, const CPathsD paths, int precision) { if (CRectIsEmpty(rect) || !paths) return nullptr; if (precision < -8 || precision > 8) return nullptr; + const double scale = std::pow(10, precision); Rect64 r = ScaleRect(CRectToRect(rect), scale); - class RectClipLines rcl(r); - Paths64 pp = ConvertCPathsD(paths, scale); + class RectClipLines64 rcl(r); + Paths64 pp = ConvertCPathsDToPaths64(paths, scale); Paths64 result = rcl.Execute(pp); - return CreateCPathsD(result, 1/scale); -} - -inline CPath64 CreateCPath64(size_t cnt1, size_t cnt2) -{ - // allocates memory for CPath64, fills in the counter, and - // returns the structure ready to be filled with path data - CPath64 result = new int64_t[2 + cnt1 *2]; - result[0] = cnt1; - result[1] = cnt2; - return result; -} - -inline CPath64 CreateCPath64(const Path64& p) -{ - // allocates memory for CPath64, fills the counter - // and returns the memory filled with path data - size_t cnt = p.size(); - if (!cnt) return nullptr; - CPath64 result = CreateCPath64(cnt, 0); - CPath64 v = result; - v += 2; // skip counters - for (const Point64& pt : p) - { - *v++ = pt.x; - *v++ = pt.y; - } - return result; -} - -inline Path64 ConvertCPath64(const CPath64& p) -{ - Path64 result; - if (p && *p) - { - CPath64 v = p; - const size_t cnt = static_cast(p[0]); - v += 2; // skip counters - result.reserve(cnt); - for (size_t i = 0; i < cnt; ++i) - { - // x,y here avoids right to left function evaluation - // result.push_back(Point64(*v++, *v++)); - int64_t x = *v++; - int64_t y = *v++; - result.push_back(Point64(x, y)); - } - } - return result; -} - -inline CPaths64 CreateCPaths64(const Paths64& pp) -{ - // allocates memory for multiple CPath64 and - // and returns this memory filled with path data - size_t cnt = pp.size(), cnt2 = cnt; - - // don't allocate space for empty paths - for (size_t i = 0; i < cnt; ++i) - if (!pp[i].size()) --cnt2; - if (!cnt2) return nullptr; - - CPaths64 result = new int64_t* [cnt2 + 1]; - CPaths64 v = result; - *v++ = CreateCPath64(0, cnt2); // assign a counter path - for (const Path64& p : pp) - { - *v = CreateCPath64(p); - if (*v) ++v; - } - return result; -} - -inline Paths64 ConvertCPaths64(const CPaths64& pp) -{ - Paths64 result; - if (pp) - { - CPaths64 v = pp; - CPath64 cnts = pp[0]; - const size_t cnt = static_cast(cnts[1]); // nb 2nd cnt - ++v; // skip cnts - result.reserve(cnt); - for (size_t i = 0; i < cnt; ++i) - result.push_back(ConvertCPath64(*v++)); - } - return result; -} - -inline CPathD CreateCPathD(size_t cnt1, size_t cnt2) -{ - // allocates memory for CPathD, fills in the counter, and - // returns the structure ready to be filled with path data - CPathD result = new double[2 + cnt1 * 2]; - result[0] = static_cast(cnt1); - result[1] = static_cast(cnt2); - return result; -} - -inline CPathD CreateCPathD(const PathD& p) -{ - // allocates memory for CPath, fills the counter - // and returns the memory fills with path data - size_t cnt = p.size(); - if (!cnt) return nullptr; - CPathD result = CreateCPathD(cnt, 0); - CPathD v = result; - v += 2; // skip counters - for (const PointD& pt : p) - { - *v++ = pt.x; - *v++ = pt.y; - } - return result; -} - -inline PathD ConvertCPathD(const CPathD& p) -{ - PathD result; - if (p) - { - CPathD v = p; - size_t cnt = static_cast(v[0]); - v += 2; // skip counters - result.reserve(cnt); - for (size_t i = 0; i < cnt; ++i) - { - // x,y here avoids right to left function evaluation - // result.push_back(PointD(*v++, *v++)); - double x = *v++; - double y = *v++; - result.push_back(PointD(x, y)); - } - } - return result; -} - -inline CPathsD CreateCPathsD(const PathsD& pp) -{ - size_t cnt = pp.size(), cnt2 = cnt; - // don't allocate space for empty paths - for (size_t i = 0; i < cnt; ++i) - if (!pp[i].size()) --cnt2; - if (!cnt2) return nullptr; - CPathsD result = new double * [cnt2 + 1]; - CPathsD v = result; - *v++ = CreateCPathD(0, cnt2); // assign counter path - for (const PathD& p : pp) - { - *v = CreateCPathD(p); - if (*v) { ++v; } - } - return result; -} - -inline PathsD ConvertCPathsD(const CPathsD& pp) -{ - PathsD result; - if (pp) - { - CPathsD v = pp; - CPathD cnts = v[0]; - size_t cnt = static_cast(cnts[1]); - ++v; // skip cnts path - result.reserve(cnt); - for (size_t i = 0; i < cnt; ++i) - result.push_back(ConvertCPathD(*v++)); - } - return result; -} - -inline Path64 ConvertCPathD(const CPathD& p, double scale) -{ - Path64 result; - if (p) - { - CPathD v = p; - size_t cnt = static_cast(*v); - v += 2; // skip counters - result.reserve(cnt); - for (size_t i = 0; i < cnt; ++i) - { - // x,y here avoids right to left function evaluation - // result.push_back(PointD(*v++, *v++)); - double x = *v++ * scale; - double y = *v++ * scale; - result.push_back(Point64(x, y)); - } - } - return result; -} - -inline Paths64 ConvertCPathsD(const CPathsD& pp, double scale) -{ - Paths64 result; - if (pp) - { - CPathsD v = pp; - CPathD cnts = v[0]; - size_t cnt = static_cast(cnts[1]); - result.reserve(cnt); - ++v; // skip cnts path - for (size_t i = 0; i < cnt; ++i) - result.push_back(ConvertCPathD(*v++, scale)); - } - return result; -} - -inline CPathD CreateCPathD(const Path64& p, double scale) -{ - // allocates memory for CPathD, fills in the counter, and - // returns the structure filled with *scaled* path data - size_t cnt = p.size(); - if (!cnt) return nullptr; - CPathD result = CreateCPathD(cnt, 0); - CPathD v = result; - v += 2; // skip cnts - for (const Point64& pt : p) - { - *v++ = pt.x * scale; - *v++ = pt.y * scale; - } - return result; -} - -inline CPathsD CreateCPathsD(const Paths64& pp, double scale) -{ - // allocates memory for *multiple* CPathD, and - // returns the structure filled with scaled path data - size_t cnt = pp.size(), cnt2 = cnt; - // don't allocate space for empty paths - for (size_t i = 0; i < cnt; ++i) - if (!pp[i].size()) --cnt2; - if (!cnt2) return nullptr; - CPathsD result = new double* [cnt2 + 1]; - CPathsD v = result; - *v++ = CreateCPathD(0, cnt2); - for (const Path64& p : pp) - { - *v = CreateCPathD(p, scale); - if (*v) ++v; - } - return result; -} - -inline void InitCPolyPath64(CPolyTree64* cpt, - bool is_hole, const std::unique_ptr & pp) -{ - cpt->polygon = CreateCPath64(pp->Polygon()); - cpt->is_hole = is_hole; - size_t child_cnt = pp->Count(); - cpt->child_count = static_cast(child_cnt); - cpt->childs = nullptr; - if (!child_cnt) return; - cpt->childs = new CPolyPath64[child_cnt]; - CPolyPath64* child = cpt->childs; - for (const std::unique_ptr & pp_child : *pp) - InitCPolyPath64(child++, !is_hole, pp_child); -} - -inline CPolyTree64* CreateCPolyTree64(const PolyTree64& pt) -{ - CPolyTree64* result = new CPolyTree64(); - result->polygon = nullptr; - result->is_hole = false; - size_t child_cnt = pt.Count(); - result->childs = nullptr; - result->child_count = static_cast(child_cnt); - if (!child_cnt) return result; - result->childs = new CPolyPath64[child_cnt]; - CPolyPath64* child = result->childs; - for (const std::unique_ptr & pp : pt) - InitCPolyPath64(child++, true, pp); - return result; -} - -inline void DisposeCPolyPath64(CPolyPath64* cpp) -{ - if (!cpp->child_count) return; - CPolyPath64* child = cpp->childs; - for (size_t i = 0; i < cpp->child_count; ++i) - DisposeCPolyPath64(child); - delete[] cpp->childs; -} - -EXTERN_DLL_EXPORT void DisposeExportedCPolyTree64(CPolyTree64*& cpt) -{ - if (!cpt) return; - DisposeCPolyPath64(cpt); - delete cpt; - cpt = nullptr; -} - -inline void InitCPolyPathD(CPolyTreeD* cpt, - bool is_hole, const std::unique_ptr & pp, double scale) -{ - cpt->polygon = CreateCPathD(pp->Polygon(), scale); - cpt->is_hole = is_hole; - size_t child_cnt = pp->Count(); - cpt->child_count = static_cast(child_cnt); - cpt->childs = nullptr; - if (!child_cnt) return; - cpt->childs = new CPolyPathD[child_cnt]; - CPolyPathD* child = cpt->childs; - for (const std::unique_ptr & pp_child : *pp) - InitCPolyPathD(child++, !is_hole, pp_child, scale); -} - -inline CPolyTreeD* CreateCPolyTreeD(const PolyTree64& pt, double scale) -{ - CPolyTreeD* result = new CPolyTreeD(); - result->polygon = nullptr; - result->is_hole = false; - size_t child_cnt = pt.Count(); - result->child_count = static_cast(child_cnt); - result->childs = nullptr; - if (!child_cnt) return result; - result->childs = new CPolyPathD[child_cnt]; - CPolyPathD* child = result->childs; - for (const std::unique_ptr & pp : pt) - InitCPolyPathD(child++, true, pp, scale); - return result; -} - -inline void DisposeCPolyPathD(CPolyPathD* cpp) -{ - if (!cpp->child_count) return; - CPolyPathD* child = cpp->childs; - for (size_t i = 0; i < cpp->child_count; ++i) - DisposeCPolyPathD(child++); - delete[] cpp->childs; -} - -EXTERN_DLL_EXPORT void DisposeExportedCPolyTreeD(CPolyTreeD*& cpt) -{ - if (!cpt) return; - DisposeCPolyPathD(cpt); - delete cpt; - cpt = nullptr; + return CreateCPathsDFromPaths64(result, 1 / scale); } } // end Clipper2Lib namespace diff --git a/CPP/Clipper2Lib/include/clipper2/clipper.h b/CPP/Clipper2Lib/include/clipper2/clipper.h index 6579f59c..0f516b60 100644 --- a/CPP/Clipper2Lib/include/clipper2/clipper.h +++ b/CPP/Clipper2Lib/include/clipper2/clipper.h @@ -1,6 +1,6 @@ /******************************************************************************* * Author : Angus Johnson * -* Date : 23 March 2023 * +* Date : 18 November 2023 * * Website : http://www.angusj.com * * Copyright : Angus Johnson 2010-2023 * * Purpose : This module provides a simple interface to the Clipper Library * @@ -14,11 +14,11 @@ #include #include -#include "clipper.core.h" -#include "clipper.engine.h" -#include "clipper.offset.h" -#include "clipper.minkowski.h" -#include "clipper.rectclip.h" +#include "clipper2/clipper.core.h" +#include "clipper2/clipper.engine.h" +#include "clipper2/clipper.offset.h" +#include "clipper2/clipper.minkowski.h" +#include "clipper2/clipper.rectclip.h" namespace Clipper2Lib { @@ -161,60 +161,61 @@ namespace Clipper2Lib { return ScalePaths(solution, 1 / scale, error_code); } - inline Path64 TranslatePath(const Path64& path, int64_t dx, int64_t dy) + template + inline Path TranslatePath(const Path& path, T dx, T dy) { - Path64 result; + Path result; result.reserve(path.size()); std::transform(path.begin(), path.end(), back_inserter(result), - [dx, dy](const auto& pt) { return Point64(pt.x + dx, pt.y +dy); }); + [dx, dy](const auto& pt) { return Point(pt.x + dx, pt.y +dy); }); return result; } + inline Path64 TranslatePath(const Path64& path, int64_t dx, int64_t dy) + { + return TranslatePath(path, dx, dy); + } + inline PathD TranslatePath(const PathD& path, double dx, double dy) { - PathD result; - result.reserve(path.size()); - std::transform(path.begin(), path.end(), back_inserter(result), - [dx, dy](const auto& pt) { return PointD(pt.x + dx, pt.y + dy); }); - return result; + return TranslatePath(path, dx, dy); } - inline Paths64 TranslatePaths(const Paths64& paths, int64_t dx, int64_t dy) + template + inline Paths TranslatePaths(const Paths& paths, T dx, T dy) { - Paths64 result; + Paths result; result.reserve(paths.size()); std::transform(paths.begin(), paths.end(), back_inserter(result), [dx, dy](const auto& path) { return TranslatePath(path, dx, dy); }); return result; } + inline Paths64 TranslatePaths(const Paths64& paths, int64_t dx, int64_t dy) + { + return TranslatePaths(paths, dx, dy); + } + inline PathsD TranslatePaths(const PathsD& paths, double dx, double dy) { - PathsD result; - result.reserve(paths.size()); - std::transform(paths.begin(), paths.end(), back_inserter(result), - [dx, dy](const auto& path) { return TranslatePath(path, dx, dy); }); - return result; + return TranslatePaths(paths, dx, dy); } - inline Paths64 ExecuteRectClip(const Rect64& rect, - const Paths64& paths, bool convex_only = false) + inline Paths64 RectClip(const Rect64& rect, const Paths64& paths) { if (rect.IsEmpty() || paths.empty()) return Paths64(); - RectClip rc(rect); - return rc.Execute(paths, convex_only); + RectClip64 rc(rect); + return rc.Execute(paths); } - inline Paths64 ExecuteRectClip(const Rect64& rect, - const Path64& path, bool convex_only = false) + inline Paths64 RectClip(const Rect64& rect, const Path64& path) { if (rect.IsEmpty() || path.empty()) return Paths64(); - RectClip rc(rect); - return rc.Execute(Paths64{ path }, convex_only); + RectClip64 rc(rect); + return rc.Execute(Paths64{ path }); } - inline PathsD ExecuteRectClip(const RectD& rect, - const PathsD& paths, bool convex_only = false, int precision = 2) + inline PathsD RectClip(const RectD& rect, const PathsD& paths, int precision = 2) { if (rect.IsEmpty() || paths.empty()) return PathsD(); int error_code = 0; @@ -222,37 +223,31 @@ namespace Clipper2Lib { if (error_code) return PathsD(); const double scale = std::pow(10, precision); Rect64 r = ScaleRect(rect, scale); - RectClip rc(r); + RectClip64 rc(r); Paths64 pp = ScalePaths(paths, scale, error_code); if (error_code) return PathsD(); // ie: error_code result is lost return ScalePaths( - rc.Execute(pp, convex_only), 1 / scale, error_code); + rc.Execute(pp), 1 / scale, error_code); } - inline PathsD ExecuteRectClip(const RectD& rect, - const PathD& path, bool convex_only = false, int precision = 2) + inline PathsD RectClip(const RectD& rect, const PathD& path, int precision = 2) { - return ExecuteRectClip(rect, PathsD{ path }, convex_only, precision); + return RectClip(rect, PathsD{ path }, precision); } - inline Paths64 ExecuteRectClipLines(const Rect64& rect, const Paths64& lines) + inline Paths64 RectClipLines(const Rect64& rect, const Paths64& lines) { if (rect.IsEmpty() || lines.empty()) return Paths64(); - RectClipLines rcl(rect); + RectClipLines64 rcl(rect); return rcl.Execute(lines); } - inline Paths64 ExecuteRectClipLines(const Rect64& rect, const Path64& line) - { - return ExecuteRectClipLines(rect, Paths64{ line }); - } - - inline PathsD ExecuteRectClipLines(const RectD& rect, const PathD& line, int precision = 2) + inline Paths64 RectClipLines(const Rect64& rect, const Path64& line) { - return ExecuteRectClip(rect, PathsD{ line }, precision); + return RectClipLines(rect, Paths64{ line }); } - inline PathsD ExecuteRectClipLines(const RectD& rect, const PathsD& lines, int precision = 2) + inline PathsD RectClipLines(const RectD& rect, const PathsD& lines, int precision = 2) { if (rect.IsEmpty() || lines.empty()) return PathsD(); int error_code = 0; @@ -260,13 +255,18 @@ namespace Clipper2Lib { if (error_code) return PathsD(); const double scale = std::pow(10, precision); Rect64 r = ScaleRect(rect, scale); - RectClipLines rcl(r); + RectClipLines64 rcl(r); Paths64 p = ScalePaths(lines, scale, error_code); if (error_code) return PathsD(); p = rcl.Execute(p); return ScalePaths(p, 1 / scale, error_code); } + inline PathsD RectClipLines(const RectD& rect, const PathD& line, int precision = 2) + { + return RectClipLines(rect, PathsD{ line }, precision); + } + namespace details { @@ -290,14 +290,9 @@ namespace Clipper2Lib { { // return false if this child isn't fully contained by its parent - // the following algorithm is a bit too crude, and doesn't account - // for rounding errors. A better algorithm is to return false when - // consecutive vertices are found outside the parent's polygon. - - //const Path64& path = pp.Polygon(); - //if (std::any_of(child->Polygon().cbegin(), child->Polygon().cend(), - // [path](const auto& pt) {return (PointInPolygon(pt, path) == - // PointInPolygonResult::IsOutside); })) return false; + // 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; for (const Point64& pt : child->Polygon()) @@ -317,74 +312,68 @@ namespace Clipper2Lib { } static void OutlinePolyPath(std::ostream& os, - bool isHole, size_t count, const std::string& preamble) + size_t idx, bool isHole, size_t count, const std::string& preamble) { std::string plural = (count == 1) ? "." : "s."; if (isHole) - { - if (count) - os << preamble << "+- Hole with " << count << - " nested polygon" << plural << std::endl; - else - os << preamble << "+- Hole" << std::endl; - } + os << preamble << "+- Hole (" << idx << ") contains " << count << + " nested polygon" << plural << std::endl; else - { - if (count) - os << preamble << "+- Polygon with " << count << + os << preamble << "+- Polygon (" << idx << ") contains " << count << " hole" << plural << std::endl; - else - os << preamble << "+- Polygon" << std::endl; - } } static void OutlinePolyPath64(std::ostream& os, const PolyPath64& pp, - std::string preamble, bool last_child) + size_t idx, std::string preamble) { - OutlinePolyPath(os, pp.IsHole(), pp.Count(), preamble); - preamble += (!last_child) ? "| " : " "; - if (pp.Count()) - { - PolyPath64List::const_iterator it = pp.begin(); - for (; it < pp.end() - 1; ++it) - OutlinePolyPath64(os, **it, preamble, false); - OutlinePolyPath64(os, **it, preamble, true); - } + OutlinePolyPath(os, idx, pp.IsHole(), pp.Count(), preamble); + for (size_t i = 0; i < pp.Count(); ++i) + if (pp.Child(i)->Count()) + details::OutlinePolyPath64(os, *pp.Child(i), i, preamble + " "); } static void OutlinePolyPathD(std::ostream& os, const PolyPathD& pp, - std::string preamble, bool last_child) + size_t idx, std::string preamble) { - OutlinePolyPath(os, pp.IsHole(), pp.Count(), preamble); - preamble += (!last_child) ? "| " : " "; - if (pp.Count()) - { - PolyPathDList::const_iterator it = pp.begin(); - for (; it < pp.end() - 1; ++it) - OutlinePolyPathD(os, **it, preamble, false); - OutlinePolyPathD(os, **it, preamble, true); - } + OutlinePolyPath(os, idx, pp.IsHole(), pp.Count(), preamble); + for (size_t i = 0; i < pp.Count(); ++i) + if (pp.Child(i)->Count()) + details::OutlinePolyPathD(os, *pp.Child(i), i, preamble + " "); + } + + template + inline constexpr void MakePathGeneric(const T an_array, + size_t array_size, std::vector& result) + { + 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} ); +#else + result.push_back( U{ an_array[i], an_array[i + 1]} ); +#endif } } // end details namespace inline std::ostream& operator<< (std::ostream& os, const PolyTree64& pp) { - PolyPath64List::const_iterator it = pp.begin(); - for (; it < pp.end() - 1; ++it) - details::OutlinePolyPath64(os, **it, " ", false); - details::OutlinePolyPath64(os, **it, " ", true); + std::string plural = (pp.Count() == 1) ? " polygon." : " polygons."; + os << std::endl << "Polytree with " << pp.Count() << plural << std::endl; + for (size_t i = 0; i < pp.Count(); ++i) + if (pp.Child(i)->Count()) + details::OutlinePolyPath64(os, *pp.Child(i), i, " "); os << std::endl << std::endl; - if (!pp.Level()) os << std::endl; return os; } inline std::ostream& operator<< (std::ostream& os, const PolyTreeD& pp) { - PolyPathDList::const_iterator it = pp.begin(); - for (; it < pp.end() - 1; ++it) - details::OutlinePolyPathD(os, **it, " ", false); - details::OutlinePolyPathD(os, **it, " ", true); + std::string plural = (pp.Count() == 1) ? " polygon." : " polygons."; + os << std::endl << "Polytree with " << pp.Count() << plural << std::endl; + for (size_t i = 0; i < pp.Count(); ++i) + if (pp.Child(i)->Count()) + details::OutlinePolyPathD(os, *pp.Child(i), i, " "); os << std::endl << std::endl; if (!pp.Level()) os << std::endl; return os; @@ -415,22 +404,6 @@ namespace Clipper2Lib { return true; } - namespace details { - - template - inline constexpr void MakePathGeneric(const T list, size_t size, - std::vector& result) - { - for (size_t i = 0; i < size; ++i) -#ifdef USINGZ - result[i / 2] = U{list[i], list[++i], 0}; -#else - result[i / 2] = U{list[i], list[++i]}; -#endif - } - - } // end details namespace - template::value && @@ -441,7 +414,7 @@ namespace Clipper2Lib { const auto size = list.size() - list.size() % 2; if (list.size() != size) DoError(non_pair_error_i); // non-fatal without exception handling - Path64 result(size / 2); // else ignores unpaired value + Path64 result; details::MakePathGeneric(list, size, result); return result; } @@ -455,7 +428,7 @@ namespace Clipper2Lib { { // Make the compiler error on unpaired value (i.e. no runtime effects). static_assert(N % 2 == 0, "MakePath requires an even number of arguments"); - Path64 result(N / 2); + Path64 result; details::MakePathGeneric(list, N, result); return result; } @@ -470,7 +443,7 @@ namespace Clipper2Lib { const auto size = list.size() - list.size() % 2; if (list.size() != size) DoError(non_pair_error_i); // non-fatal without exception handling - PathD result(size / 2); // else ignores unpaired value + PathD result; details::MakePathGeneric(list, size, result); return result; } @@ -484,11 +457,44 @@ namespace Clipper2Lib { { // Make the compiler error on unpaired value (i.e. no runtime effects). static_assert(N % 2 == 0, "MakePath requires an even number of arguments"); - PathD result(N / 2); + PathD result; details::MakePathGeneric(list, N, result); return result; } +#ifdef USINGZ + template + inline Path64 MakePathZ(const T2(&list)[N]) + { + static_assert(N % 3 == 0 && std::numeric_limits::is_integer, + "MakePathZ requires integer values in multiples of 3"); + std::size_t size = N / 3; + Path64 result(size); + for (size_t i = 0; i < size; ++i) + result[i] = Point64(list[i * 3], + list[i * 3 + 1], list[i * 3 + 2]); + return result; + } + + template + inline PathD MakePathZD(const T2(&list)[N]) + { + static_assert(N % 3 == 0, + "MakePathZD requires values in multiples of 3"); + std::size_t size = N / 3; + PathD result(size); + if constexpr (std::numeric_limits::is_integer) + for (size_t i = 0; i < size; ++i) + result[i] = PointD(list[i * 3], + 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], + static_cast(list[i * 3 + 2])); + return result; + } +#endif + inline Path64 TrimCollinear(const Path64& p, bool is_open_path = false) { size_t len = p.size(); @@ -644,8 +650,8 @@ namespace Clipper2Lib { } template - inline Path SimplifyPath(const Path path, - double epsilon, bool isOpenPath = false) + inline Path SimplifyPath(const Path &path, + double epsilon, bool isClosedPath = true) { const size_t len = path.size(), high = len -1; const double epsSqr = Sqr(epsilon); @@ -653,16 +659,16 @@ namespace Clipper2Lib { std::vector flags(len); std::vector distSqr(len); - size_t prior = high, curr = 0, start, next, prior2, next2; - if (isOpenPath) + size_t prior = high, curr = 0, start, next, prior2; + if (isClosedPath) { - distSqr[0] = MAX_DBL; - distSqr[high] = MAX_DBL; + distSqr[0] = PerpendicDistFromLineSqrd(path[0], path[high], path[1]); + distSqr[high] = PerpendicDistFromLineSqrd(path[high], path[0], path[high - 1]); } else { - distSqr[0] = PerpendicDistFromLineSqrd(path[0], path[high], path[1]); - distSqr[high] = PerpendicDistFromLineSqrd(path[high], path[0], path[high - 1]); + distSqr[0] = MAX_DBL; + distSqr[high] = MAX_DBL; } for (size_t i = 1; i < high; ++i) distSqr[i] = PerpendicDistFromLineSqrd(path[i], path[i - 1], path[i + 1]); @@ -683,26 +689,25 @@ namespace Clipper2Lib { next = GetNext(curr, high, flags); if (next == prior) break; + // flag for removal the smaller of adjacent 'distances' if (distSqr[next] < distSqr[curr]) { - flags[next] = true; - next = GetNext(next, high, flags); - next2 = GetNext(next, high, flags); - distSqr[curr] = PerpendicDistFromLineSqrd(path[curr], path[prior], path[next]); - if (next != high || !isOpenPath) - distSqr[next] = PerpendicDistFromLineSqrd(path[next], path[curr], path[next2]); + prior2 = prior; + prior = curr; curr = next; + next = GetNext(next, high, flags); } else - { - flags[curr] = true; - curr = next; - next = GetNext(next, high, flags); prior2 = GetPrior(prior, high, flags); + + flags[curr] = true; + curr = next; + next = GetNext(next, high, flags); + + if (isClosedPath || ((curr != high) && (curr != 0))) distSqr[curr] = PerpendicDistFromLineSqrd(path[curr], path[prior], path[next]); - if (prior != 0 || !isOpenPath) - distSqr[prior] = PerpendicDistFromLineSqrd(path[prior], path[prior2], path[curr]); - } + if (isClosedPath || ((prior != 0) && (prior != high))) + distSqr[prior] = PerpendicDistFromLineSqrd(path[prior], path[prior2], path[curr]); } Path result; result.reserve(len); @@ -712,13 +717,13 @@ namespace Clipper2Lib { } template - inline Paths SimplifyPaths(const Paths paths, - double epsilon, bool isOpenPath = false) + inline Paths SimplifyPaths(const Paths &paths, + double epsilon, bool isClosedPath = true) { Paths result; result.reserve(paths.size()); for (const auto& path : paths) - result.push_back(SimplifyPath(path, epsilon, isOpenPath)); + result.push_back(SimplifyPath(path, epsilon, isClosedPath)); return result; } diff --git a/CPP/Clipper2Lib/include/clipper2/clipper.minkowski.h b/CPP/Clipper2Lib/include/clipper2/clipper.minkowski.h index 71c221bb..ebddd08a 100644 --- a/CPP/Clipper2Lib/include/clipper2/clipper.minkowski.h +++ b/CPP/Clipper2Lib/include/clipper2/clipper.minkowski.h @@ -1,6 +1,6 @@ /******************************************************************************* * Author : Angus Johnson * -* Date : 28 January 2023 * +* Date : 1 November 2023 * * Website : http://www.angusj.com * * Copyright : Angus Johnson 2010-2023 * * Purpose : Minkowski Sum and Difference * @@ -13,7 +13,7 @@ #include #include #include -#include "clipper.core.h" +#include "clipper2/clipper.core.h" namespace Clipper2Lib { diff --git a/CPP/Clipper2Lib/include/clipper2/clipper.offset.h b/CPP/Clipper2Lib/include/clipper2/clipper.offset.h index f5d47e07..30992bfa 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 : 22 March 2023 * +* Date : 19 November 2023 * * Website : http://www.angusj.com * * Copyright : Angus Johnson 2010-2023 * * Purpose : Path Offset (Inflate/Shrink) * @@ -15,7 +15,9 @@ namespace Clipper2Lib { -enum class JoinType { Square, Round, Miter }; +enum class JoinType { Square, Bevel, Round, Miter }; +//Square : Joins are 'squared' at exactly the offset distance (more complex code) +//Bevel : Similar to Square, but the offset distance varies with angle (simple code & faster) enum class EndType {Polygon, Joined, Butt, Square, Round}; //Butt : offsets both sides of a path, with square blunt ends @@ -24,6 +26,7 @@ enum class EndType {Polygon, Joined, Butt, Square, Round}; //Joined : offsets both sides of a path, with joined ends //Polygon: offsets only one side of a closed path +typedef std::function DeltaCallback64; class ClipperOffset { private: @@ -31,27 +34,27 @@ class ClipperOffset { class Group { public: Paths64 paths_in; - Paths64 paths_out; - Path64 path; + std::vector is_hole_list; + std::vector bounds_list; + int lowest_path_idx = -1; bool is_reversed = false; JoinType join_type; EndType end_type; - Group(const Paths64& _paths, JoinType _join_type, EndType _end_type) : - paths_in(_paths), join_type(_join_type), end_type(_end_type) {} + Group(const Paths64& _paths, JoinType _join_type, EndType _end_type); }; int error_code_ = 0; double delta_ = 0.0; double group_delta_ = 0.0; - double abs_group_delta_ = 0.0; double temp_lim_ = 0.0; double steps_per_rad_ = 0.0; double step_sin_ = 0.0; double step_cos_ = 0.0; PathD norms; + Path64 path_out; Paths64 solution; std::vector groups_; - JoinType join_type_ = JoinType::Square; + JoinType join_type_ = JoinType::Bevel; EndType end_type_ = EndType::Polygon; double miter_limit_ = 0.0; @@ -62,15 +65,19 @@ class ClipperOffset { #ifdef USINGZ ZCallback64 zCallback64_ = nullptr; #endif - - void DoSquare(Group& group, const Path64& path, size_t j, size_t k); - void DoMiter(Group& group, const Path64& path, size_t j, size_t k, double cos_a); - void DoRound(Group& group, const Path64& path, size_t j, size_t k, double angle); + DeltaCallback64 deltaCallback64_ = nullptr; + + size_t CalcSolutionCapacity(); + bool CheckReverseOrientation(); + void DoBevel(const Path64& path, size_t j, size_t k); + void DoSquare(const Path64& path, size_t j, size_t k); + 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, Path64& path); - void OffsetOpenJoined(Group& group, Path64& path); - void OffsetOpenPath(Group& group, Path64& path); - void OffsetPoint(Group& group, Path64& path, size_t j, size_t& k); + 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); void DoGroupOffset(Group &group); void ExecuteInternal(double delta); public: @@ -91,6 +98,7 @@ class ClipperOffset { void Execute(double delta, Paths64& paths); void Execute(double delta, PolyTree64& polytree); + void Execute(DeltaCallback64 delta_cb, Paths64& paths); double MiterLimit() const { return miter_limit_; } void MiterLimit(double miter_limit) { miter_limit_ = miter_limit; } @@ -108,6 +116,8 @@ class ClipperOffset { #ifdef USINGZ void SetZCallback(ZCallback64 cb) { zCallback64_ = cb; } #endif + void SetDeltaCallback(DeltaCallback64 cb) { deltaCallback64_ = cb; } + }; } diff --git a/CPP/Clipper2Lib/include/clipper2/clipper.rectclip.h b/CPP/Clipper2Lib/include/clipper2/clipper.rectclip.h index 2a9bb35d..ff043f25 100644 --- a/CPP/Clipper2Lib/include/clipper2/clipper.rectclip.h +++ b/CPP/Clipper2Lib/include/clipper2/clipper.rectclip.h @@ -1,6 +1,6 @@ /******************************************************************************* * Author : Angus Johnson * -* Date : 9 February 2023 * +* Date : 1 November 2023 * * Website : http://www.angusj.com * * Copyright : Angus Johnson 2010-2023 * * Purpose : FAST rectangular clipping * @@ -13,8 +13,7 @@ #include #include #include -#include "clipper.h" -#include "clipper.core.h" +#include "clipper2/clipper.core.h" namespace Clipper2Lib { @@ -34,10 +33,10 @@ namespace Clipper2Lib }; //------------------------------------------------------------------------------ - // RectClip + // RectClip64 //------------------------------------------------------------------------------ - class RectClip { + class RectClip64 { private: void ExecuteInternal(const Path64& path); Path64 GetPath(OutPt2*& op); @@ -58,23 +57,23 @@ namespace Clipper2Lib void AddCorner(Location prev, Location curr); void AddCorner(Location& loc, bool isClockwise); public: - explicit RectClip(const Rect64& rect) : + explicit RectClip64(const Rect64& rect) : rect_(rect), rect_as_path_(rect.AsPath()), rect_mp_(rect.MidPoint()) {} - Paths64 Execute(const Paths64& paths, bool convex_only = false); + Paths64 Execute(const Paths64& paths); }; //------------------------------------------------------------------------------ - // RectClipLines + // RectClipLines64 //------------------------------------------------------------------------------ - class RectClipLines : public RectClip { + class RectClipLines64 : public RectClip64 { private: void ExecuteInternal(const Path64& path); Path64 GetPath(OutPt2*& op); public: - explicit RectClipLines(const Rect64& rect) : RectClip(rect) {}; + explicit RectClipLines64(const Rect64& rect) : RectClip64(rect) {}; Paths64 Execute(const Paths64& paths); }; diff --git a/CPP/Clipper2Lib/include/clipper2/clipper.version.h b/CPP/Clipper2Lib/include/clipper2/clipper.version.h new file mode 100644 index 00000000..d7644067 --- /dev/null +++ b/CPP/Clipper2Lib/include/clipper2/clipper.version.h @@ -0,0 +1,6 @@ +#ifndef CLIPPER_VERSION_H +#define CLIPPER_VERSION_H + +constexpr auto CLIPPER2_VERSION = "1.3.0"; + +#endif // CLIPPER_VERSION_H diff --git a/CPP/Clipper2Lib/src/clipper.engine.cpp b/CPP/Clipper2Lib/src/clipper.engine.cpp index 2d61b8aa..9358b74b 100644 --- a/CPP/Clipper2Lib/src/clipper.engine.cpp +++ b/CPP/Clipper2Lib/src/clipper.engine.cpp @@ -1,6 +1,6 @@ /******************************************************************************* * Author : Angus Johnson * -* Date : 19 March 2023 * +* Date : 22 November 2023 * * Website : http://www.angusj.com * * Copyright : Angus Johnson 2010-2023 * * Purpose : This is the main polygon clipping module * @@ -15,6 +15,7 @@ #include #include "clipper2/clipper.engine.h" +#include "clipper2/clipper.h" // https://github.com/AngusJohnson/Clipper2/discussions/334 // #discussioncomment-4248602 @@ -419,6 +420,12 @@ namespace Clipper2Lib { return outrec; } + inline bool IsValidOwner(OutRec* outrec, OutRec* testOwner) + { + // prevent outrec owning itself either directly or indirectly + while (testOwner && testOwner != outrec) testOwner = testOwner->owner; + return !testOwner; + } inline void UncoupleOutRec(Active ae) { @@ -484,108 +491,135 @@ namespace Clipper2Lib { outrec->owner = new_owner; } - //------------------------------------------------------------------------------ - // ClipperBase methods ... - //------------------------------------------------------------------------------ - - ClipperBase::~ClipperBase() + static PointInPolygonResult PointInOpPolygon(const Point64& pt, OutPt* op) { - Clear(); - } + if (op == op->next || op->prev == op->next) + return PointInPolygonResult::IsOutside; - void ClipperBase::DeleteEdges(Active*& e) - { - while (e) + OutPt* op2 = op; + do { - Active* e2 = e; - e = e->next_in_ael; - delete e2; - } - } + if (op->pt.y != pt.y) break; + op = op->next; + } while (op != op2); + if (op->pt.y == pt.y) // not a proper polygon + return PointInPolygonResult::IsOutside; - void ClipperBase::CleanUp() - { - DeleteEdges(actives_); - scanline_list_ = std::priority_queue(); - intersect_nodes_.clear(); - DisposeAllOutRecs(); - horz_seg_list_.clear(); - horz_join_list_.clear(); - } + bool is_above = op->pt.y < pt.y, starting_above = is_above; + int val = 0; + op2 = op->next; + while (op2 != op) + { + if (is_above) + while (op2 != op && op2->pt.y < pt.y) op2 = op2->next; + else + while (op2 != op && op2->pt.y > pt.y) op2 = op2->next; + if (op2 == op) break; + // must have touched or crossed the pt.Y horizonal + // and this must happen an even number of times - void ClipperBase::Clear() - { - CleanUp(); - DisposeVerticesAndLocalMinima(); - current_locmin_iter_ = minima_list_.begin(); - minima_list_sorted_ = false; - has_open_paths_ = false; - } + if (op2->pt.y == pt.y) // touching the horizontal + { + if (op2->pt.x == pt.x || (op2->pt.y == op2->prev->pt.y && + (pt.x < op2->prev->pt.x) != (pt.x < op2->pt.x))) + return PointInPolygonResult::IsOn; + + op2 = op2->next; + if (op2 == op) break; + continue; + } + if (pt.x < op2->pt.x && pt.x < op2->prev->pt.x); + // do nothing because + // we're only interested in edges crossing on the left + else if ((pt.x > op2->prev->pt.x && pt.x > op2->pt.x)) + val = 1 - val; // toggle val + else + { + double d = CrossProduct(op2->prev->pt, op2->pt, pt); + if (d == 0) return PointInPolygonResult::IsOn; + if ((d < 0) == is_above) val = 1 - val; + } + is_above = !is_above; + op2 = op2->next; + } - void ClipperBase::Reset() - { - if (!minima_list_sorted_) + if (is_above != starting_above) { - std::sort(minima_list_.begin(), minima_list_.end(), LocMinSorter()); - minima_list_sorted_ = true; + double d = CrossProduct(op2->prev->pt, op2->pt, pt); + if (d == 0) return PointInPolygonResult::IsOn; + if ((d < 0) == is_above) val = 1 - val; } - LocalMinimaList::const_reverse_iterator i; - for (i = minima_list_.rbegin(); i != minima_list_.rend(); ++i) - InsertScanline((*i)->vertex->pt.y); - current_locmin_iter_ = minima_list_.begin(); - actives_ = nullptr; - sel_ = nullptr; - succeeded_ = true; + if (val == 0) return PointInPolygonResult::IsOutside; + else return PointInPolygonResult::IsInside; } - -#ifdef USINGZ - void ClipperBase::SetZ(const Active& e1, const Active& e2, Point64& ip) + inline Path64 GetCleanPath(OutPt* op) { - if (!zCallback_) return; - // prioritize subject over clip vertices by passing - // subject vertices before clip vertices in the callback - if (GetPolyType(e1) == PathType::Subject) - { - if (ip == e1.bot) ip.z = e1.bot.z; - else if (ip == e1.top) ip.z = e1.top.z; - else if (ip == e2.bot) ip.z = e2.bot.z; - else if (ip == e2.top) ip.z = e2.top.z; - else ip.z = DefaultZ; - zCallback_(e1.bot, e1.top, e2.bot, e2.top, ip); - } - else + Path64 result; + OutPt* op2 = op; + while (op2->next != op && + ((op2->pt.x == op2->next->pt.x && op2->pt.x == op2->prev->pt.x) || + (op2->pt.y == op2->next->pt.y && op2->pt.y == op2->prev->pt.y))) op2 = op2->next; + result.push_back(op2->pt); + OutPt* prevOp = op2; + op2 = op2->next; + while (op2 != op) { - if (ip == e2.bot) ip.z = e2.bot.z; - else if (ip == e2.top) ip.z = e2.top.z; - else if (ip == e1.bot) ip.z = e1.bot.z; - else if (ip == e1.top) ip.z = e1.top.z; - else ip.z = DefaultZ; - zCallback_(e2.bot, e2.top, e1.bot, e1.top, ip); + if ((op2->pt.x != op2->next->pt.x || op2->pt.x != prevOp->pt.x) && + (op2->pt.y != op2->next->pt.y || op2->pt.y != prevOp->pt.y)) + { + result.push_back(op2->pt); + prevOp = op2; + } + op2 = op2->next; } + return result; } -#endif - void ClipperBase::AddPath(const Path64& path, PathType polytype, bool is_open) + inline bool Path1InsidePath2(OutPt* op1, OutPt* op2) { - Paths64 tmp; - tmp.push_back(path); - AddPaths(tmp, polytype, is_open); + // we need to make some accommodation for rounding errors + // so we won't jump if the first vertex is found outside + PointInPolygonResult result; + int outside_cnt = 0; + OutPt* op = op1; + do + { + result = PointInOpPolygon(op->pt, op2); + if (result == PointInPolygonResult::IsOutside) ++outside_cnt; + else if (result == PointInPolygonResult::IsInside) --outside_cnt; + op = op->next; + } while (op != op1 && std::abs(outside_cnt) < 2); + if (std::abs(outside_cnt) > 1) return (outside_cnt < 0); + // since path1's location is still equivocal, check its midpoint + Point64 mp = GetBounds(GetCleanPath(op1)).MidPoint(); + Path64 path2 = GetCleanPath(op2); + return PointInPolygon(mp, path2) != PointInPolygonResult::IsOutside; } + //------------------------------------------------------------------------------ + //------------------------------------------------------------------------------ - void ClipperBase::AddPaths(const Paths64& paths, PathType polytype, bool is_open) + void AddLocMin(LocalMinimaList& list, + Vertex& vert, PathType polytype, bool is_open) { - if (is_open) has_open_paths_ = true; - minima_list_sorted_ = false; + //make sure the vertex is added only once ... + if ((VertexFlags::LocalMin & vert.flags) != VertexFlags::None) return; + vert.flags = (vert.flags | VertexFlags::LocalMin); + list.push_back(std::make_unique (&vert, polytype, is_open)); + } + + void AddPaths_(const Paths64& paths, PathType polytype, bool is_open, + std::vector& vertexLists, LocalMinimaList& locMinList) + { const auto total_vertex_count = - std::accumulate(paths.begin(), paths.end(), 0, - [](const auto& a, const Path64& path) - {return a + static_cast(path.size());}); + std::accumulate(paths.begin(), paths.end(), 0, + [](const auto& a, const Path64& path) + {return a + static_cast(path.size()); }); if (total_vertex_count == 0) return; Vertex* vertices = new Vertex[total_vertex_count], * v = vertices; @@ -631,7 +665,7 @@ namespace Clipper2Lib { if (going_up) { v0->flags = VertexFlags::OpenStart; - AddLocMin(*v0, polytype, true); + AddLocMin(locMinList , *v0, polytype, true); } else v0->flags = VertexFlags::OpenStart | VertexFlags::LocalMax; @@ -659,7 +693,7 @@ namespace Clipper2Lib { else if (curr_v->pt.y < prev_v->pt.y && !going_up) { going_up = true; - AddLocMin(*prev_v, polytype, is_open); + AddLocMin(locMinList, *prev_v, polytype, is_open); } prev_v = curr_v; curr_v = curr_v->next; @@ -671,18 +705,161 @@ namespace Clipper2Lib { if (going_up) prev_v->flags = prev_v->flags | VertexFlags::LocalMax; else - AddLocMin(*prev_v, polytype, is_open); + AddLocMin(locMinList, *prev_v, polytype, is_open); } else if (going_up != going_up0) { - if (going_up0) AddLocMin(*prev_v, polytype, false); + if (going_up0) AddLocMin(locMinList, *prev_v, polytype, false); else prev_v->flags = prev_v->flags | VertexFlags::LocalMax; } } // end processing current path - vertex_lists_.emplace_back(vertices); - } // end AddPaths + vertexLists.emplace_back(vertices); + } + //------------------------------------------------------------------------------ + // ReuseableDataContainer64 methods ... + //------------------------------------------------------------------------------ + + void ReuseableDataContainer64::AddLocMin(Vertex& vert, PathType polytype, bool is_open) + { + //make sure the vertex is added only once ... + if ((VertexFlags::LocalMin & vert.flags) != VertexFlags::None) return; + + vert.flags = (vert.flags | VertexFlags::LocalMin); + minima_list_.push_back(std::make_unique (&vert, polytype, is_open)); + } + + void ReuseableDataContainer64::AddPaths(const Paths64& paths, + PathType polytype, bool is_open) + { + AddPaths_(paths, polytype, is_open, vertex_lists_, minima_list_); + } + + ReuseableDataContainer64::~ReuseableDataContainer64() + { + Clear(); + } + + void ReuseableDataContainer64::Clear() + { + minima_list_.clear(); + for (auto v : vertex_lists_) delete[] v; + vertex_lists_.clear(); + } + + //------------------------------------------------------------------------------ + // ClipperBase methods ... + //------------------------------------------------------------------------------ + + ClipperBase::~ClipperBase() + { + Clear(); + } + + void ClipperBase::DeleteEdges(Active*& e) + { + while (e) + { + Active* e2 = e; + e = e->next_in_ael; + delete e2; + } + } + + void ClipperBase::CleanUp() + { + DeleteEdges(actives_); + scanline_list_ = std::priority_queue(); + intersect_nodes_.clear(); + DisposeAllOutRecs(); + horz_seg_list_.clear(); + horz_join_list_.clear(); + } + + + void ClipperBase::Clear() + { + CleanUp(); + DisposeVerticesAndLocalMinima(); + current_locmin_iter_ = minima_list_.begin(); + minima_list_sorted_ = false; + has_open_paths_ = false; + } + + + void ClipperBase::Reset() + { + if (!minima_list_sorted_) + { + std::stable_sort(minima_list_.begin(), minima_list_.end(), LocMinSorter()); //#594 + minima_list_sorted_ = true; + } + LocalMinimaList::const_reverse_iterator i; + for (i = minima_list_.rbegin(); i != minima_list_.rend(); ++i) + InsertScanline((*i)->vertex->pt.y); + + current_locmin_iter_ = minima_list_.begin(); + actives_ = nullptr; + sel_ = nullptr; + succeeded_ = true; + } + + +#ifdef USINGZ + void ClipperBase::SetZ(const Active& e1, const Active& e2, Point64& ip) + { + if (!zCallback_) return; + // prioritize subject over clip vertices by passing + // subject vertices before clip vertices in the callback + if (GetPolyType(e1) == PathType::Subject) + { + if (ip == e1.bot) ip.z = e1.bot.z; + else if (ip == e1.top) ip.z = e1.top.z; + else if (ip == e2.bot) ip.z = e2.bot.z; + else if (ip == e2.top) ip.z = e2.top.z; + else ip.z = DefaultZ; + zCallback_(e1.bot, e1.top, e2.bot, e2.top, ip); + } + else + { + if (ip == e2.bot) ip.z = e2.bot.z; + else if (ip == e2.top) ip.z = e2.top.z; + else if (ip == e1.bot) ip.z = e1.bot.z; + else if (ip == e1.top) ip.z = e1.top.z; + else ip.z = DefaultZ; + zCallback_(e2.bot, e2.top, e1.bot, e1.top, ip); + } + } +#endif + + void ClipperBase::AddPath(const Path64& path, PathType polytype, bool is_open) + { + Paths64 tmp; + tmp.push_back(path); + AddPaths(tmp, polytype, is_open); + } + + void ClipperBase::AddPaths(const Paths64& paths, PathType polytype, bool is_open) + { + 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) + { + // nb: reuseable_data will continue to own the vertices + // and remains responsible for their clean up. + succeeded_ = false; + minima_list_sorted_ = false; + LocalMinimaList::const_iterator i; + for (i = reuseable_data.minima_list_.cbegin(); i != reuseable_data.minima_list_.cend(); ++i) + { + minima_list_.push_back(std::make_unique ((*i)->vertex, (*i)->polytype, (*i)->is_open)); + if ((*i)->is_open) has_open_paths_ = true; + } + } void ClipperBase::InsertScanline(int64_t y) { @@ -1236,7 +1413,7 @@ namespace Clipper2Lib { else SetOwner(&outrec, e->outrec); // nb: outRec.owner here is likely NOT the real - // owner but this will be checked in DeepCheckOwner() + // owner but this will be checked in RecursiveCheckOwners() } UncoupleOutRec(e1); @@ -1293,13 +1470,14 @@ namespace Clipper2Lib { e2.outrec->front_edge = nullptr; e2.outrec->back_edge = nullptr; e2.outrec->pts = nullptr; - SetOwner(e2.outrec, e1.outrec); if (IsOpenEnd(e1)) { e2.outrec->pts = e1.outrec->pts; e1.outrec->pts = nullptr; } + else + SetOwner(e2.outrec, e1.outrec); //and e1 and e2 are maxima and are about to be dropped from the Actives list. e1.outrec = nullptr; @@ -1315,6 +1493,7 @@ namespace Clipper2Lib { result->owner = nullptr; result->polypath = nullptr; result->is_open = false; + result->splits = nullptr; return result; } @@ -1364,7 +1543,7 @@ namespace Clipper2Lib { //NB if preserveCollinear == true, then only remove 180 deg. spikes if ((CrossProduct(op2->prev->pt, op2->pt, op2->next->pt) == 0) && (op2->pt == op2->prev->pt || - op2->pt == op2->next->pt || !PreserveCollinear || + op2->pt == op2->next->pt || !preserve_collinear_ || DotProduct(op2->prev->pt, op2->pt, op2->next->pt) < 0)) { @@ -1409,11 +1588,6 @@ namespace Clipper2Lib { return; } - // nb: area1 is the path's area *before* splitting, whereas area2 is - // the area of the triangle containing splitOp & splitOp.next. - // So the only way for these areas to have the same sign is if - // the split triangle is larger than the path containing prevOp or - // if there's more than one self=intersection. double area2 = AreaTriangle(ip, splitOp->pt, splitOp->next->pt); double absArea2 = std::fabs(area2); @@ -1433,18 +1607,17 @@ namespace Clipper2Lib { prevOp->next = newOp2; } + // area1 is the path's area *before* splitting, whereas area2 is + // the area of the triangle containing splitOp & splitOp.next. + // So the only way for these areas to have the same sign is if + // the split triangle is larger than the path containing prevOp or + // if there's more than one self-intersection. if (absArea2 >= 1 && (absArea2 > absArea1 || (area2 > 0) == (area1 > 0))) { OutRec* newOr = NewOutRec(); newOr->owner = outrec->owner; - if (using_polytree_) - { - if (!outrec->splits) outrec->splits = new OutRecList(); - outrec->splits->push_back(newOr); - } - splitOp->outrec = newOr; splitOp->next->outrec = newOr; OutPt* newOp = new OutPt(ip, newOr); @@ -1453,6 +1626,20 @@ namespace Clipper2Lib { newOr->pts = newOp; splitOp->prev = newOp; splitOp->next->next = newOp; + + if (using_polytree_) + { + if (Path1InsidePath2(prevOp, newOp)) + { + newOr->splits = new OutRecList(); + newOr->splits->push_back(outrec); + } + else + { + if (!outrec->splits) outrec->splits = new OutRecList(); + outrec->splits->push_back(newOr); + } + } } else { @@ -1521,6 +1708,28 @@ namespace Clipper2Lib { return op; } + inline void TrimHorz(Active& horzEdge, bool preserveCollinear) + { + bool wasTrimmed = false; + Point64 pt = NextVertex(horzEdge)->pt; + while (pt.y == horzEdge.top.y) + { + //always trim 180 deg. spikes (in closed paths) + //but otherwise break if preserveCollinear = true + if (preserveCollinear && + ((pt.x < horzEdge.top.x) != (horzEdge.bot.x < horzEdge.top.x))) + break; + + horzEdge.vertex_top = NextVertex(horzEdge); + horzEdge.top = pt; + wasTrimmed = true; + if (IsMaxima(horzEdge)) break; + pt = NextVertex(horzEdge)->pt; + } + + if (wasTrimmed) SetDx(horzEdge); // +/-infinity + } + inline void ClipperBase::UpdateEdgeIntoAEL(Active* e) { @@ -1532,11 +1741,15 @@ namespace Clipper2Lib { if (IsJoined(*e)) Split(*e, e->bot); - if (IsHorizontal(*e)) return; - InsertScanline(e->top.y); + if (IsHorizontal(*e)) + { + if (!IsOpen(*e)) TrimHorz(*e, preserve_collinear_); + return; + } + InsertScanline(e->top.y); CheckJoinLeft(*e, e->bot); - CheckJoinRight(*e, e->bot); + CheckJoinRight(*e, e->bot, true); // (#500) } Active* FindEdgeWithMatchingLocMin(Active* e) @@ -1596,17 +1809,14 @@ namespace Clipper2Lib { default: if (std::abs(edge_c->wind_cnt) != 1) return nullptr; break; } + OutPt* resultOp; //toggle contribution ... if (IsHotEdge(*edge_o)) { - OutPt* resultOp = AddOutPt(*edge_o, pt); -#ifdef USINGZ - if (zCallback_) SetZ(e1, e2, resultOp->pt); -#endif + resultOp = AddOutPt(*edge_o, pt); if (IsFront(*edge_o)) edge_o->outrec->front_edge = nullptr; else edge_o->outrec->back_edge = nullptr; edge_o->outrec = nullptr; - return resultOp; } //horizontal edges can pass under open paths at a LocMins @@ -1626,11 +1836,16 @@ namespace Clipper2Lib { return e3->outrec->pts; } else - return StartOpenPath(*edge_o, pt); + resultOp = StartOpenPath(*edge_o, pt); } else - return StartOpenPath(*edge_o, pt); - } + resultOp = StartOpenPath(*edge_o, pt); + +#ifdef USINGZ + if (zCallback_) SetZ(*edge_o, *edge_c, resultOp->pt); +#endif + return resultOp; + } // end of an open path intersection //MANAGING CLOSED PATHS FROM HERE ON @@ -1895,105 +2110,6 @@ namespace Clipper2Lib { } while (op != outrec->pts); } - inline Rect64 GetBounds(OutPt* op) - { - Rect64 result(op->pt.x, op->pt.y, op->pt.x, op->pt.y); - OutPt* op2 = op->next; - while (op2 != op) - { - if (op2->pt.x < result.left) result.left = op2->pt.x; - else if (op2->pt.x > result.right) result.right = op2->pt.x; - if (op2->pt.y < result.top) result.top = op2->pt.y; - else if (op2->pt.y > result.bottom) result.bottom = op2->pt.y; - op2 = op2->next; - } - return result; - } - - static PointInPolygonResult PointInOpPolygon(const Point64& pt, OutPt* op) - { - if (op == op->next || op->prev == op->next) - return PointInPolygonResult::IsOutside; - - OutPt* op2 = op; - do - { - if (op->pt.y != pt.y) break; - op = op->next; - } while (op != op2); - if (op->pt.y == pt.y) // not a proper polygon - return PointInPolygonResult::IsOutside; - - bool is_above = op->pt.y < pt.y, starting_above = is_above; - int val = 0; - op2 = op->next; - while (op2 != op) - { - if (is_above) - while (op2 != op && op2->pt.y < pt.y) op2 = op2->next; - else - while (op2 != op && op2->pt.y > pt.y) op2 = op2->next; - if (op2 == op) break; - - // must have touched or crossed the pt.Y horizonal - // and this must happen an even number of times - - if (op2->pt.y == pt.y) // touching the horizontal - { - if (op2->pt.x == pt.x || (op2->pt.y == op2->prev->pt.y && - (pt.x < op2->prev->pt.x) != (pt.x < op2->pt.x))) - return PointInPolygonResult::IsOn; - - op2 = op2->next; - if (op2 == op) break; - continue; - } - - if (pt.x < op2->pt.x && pt.x < op2->prev->pt.x); - // do nothing because - // we're only interested in edges crossing on the left - else if ((pt.x > op2->prev->pt.x && pt.x > op2->pt.x)) - val = 1 - val; // toggle val - else - { - double d = CrossProduct(op2->prev->pt, op2->pt, pt); - if (d == 0) return PointInPolygonResult::IsOn; - if ((d < 0) == is_above) val = 1 - val; - } - is_above = !is_above; - op2 = op2->next; - } - - if (is_above != starting_above) - { - double d = CrossProduct(op2->prev->pt, op2->pt, pt); - if (d == 0) return PointInPolygonResult::IsOn; - if ((d < 0) == is_above) val = 1 - val; - } - - if (val == 0) return PointInPolygonResult::IsOutside; - else return PointInPolygonResult::IsInside; - } - - inline bool Path1InsidePath2(OutPt* op1, OutPt* op2) - { - // we need to make some accommodation for rounding errors - // so we won't jump if the first vertex is found outside - int outside_cnt = 0; - OutPt* op = op1; - do - { - PointInPolygonResult result = PointInOpPolygon(op->pt, op2); - if (result == PointInPolygonResult::IsOutside) ++outside_cnt; - else if (result == PointInPolygonResult::IsInside) --outside_cnt; - op = op->next; - } while (op != op1 && std::abs(outside_cnt) < 2); - if (std::abs(outside_cnt) > 1) return (outside_cnt < 0); - // since path1's location is still equivocal, check its midpoint - Point64 mp = GetBounds(op).MidPoint(); - return PointInOpPolygon(mp, op2) == PointInPolygonResult::IsInside; - } - inline bool SetHorzSegHeadingForward(HorzSegment& hs, OutPt* opP, OutPt* opN) { if (opP->pt.x == opN->pt.x) return false; @@ -2051,7 +2167,7 @@ namespace Clipper2Lib { horz_seg_list_.end(), [](HorzSegment& hs) { return UpdateHorzSegment(hs); }); if (j < 2) return; - std::sort(horz_seg_list_.begin(), horz_seg_list_.end(), HorzSegSorter()); + std::stable_sort(horz_seg_list_.begin(), horz_seg_list_.end(), HorzSegSorter()); HorzSegmentList::iterator hs1 = horz_seg_list_.begin(), hs2; HorzSegmentList::iterator hs_end = hs1 +j; @@ -2061,8 +2177,8 @@ namespace Clipper2Lib { { for (hs2 = hs1 + 1; hs2 != hs_end; ++hs2) { - if (hs2->left_op->pt.x >= hs1->right_op->pt.x) break; - if (hs2->left_to_right == hs1->left_to_right || + if ((hs2->left_op->pt.x >= hs1->right_op->pt.x) || + (hs2->left_to_right == hs1->left_to_right) || (hs2->right_op->pt.x <= hs1->left_op->pt.x)) continue; int64_t curr_y = hs1->left_op->pt.y; if (hs1->left_to_right) @@ -2095,6 +2211,17 @@ namespace Clipper2Lib { } } + void MoveSplits(OutRec* fromOr, OutRec* toOr) + { + if (!fromOr->splits) return; + if (!toOr->splits) toOr->splits = new OutRecList(); + OutRecList::iterator orIter = fromOr->splits->begin(); + for (; orIter != fromOr->splits->end(); ++orIter) + toOr->splits->push_back(*orIter); + fromOr->splits->clear(); + } + + void ClipperBase::ProcessHorzJoins() { for (const HorzJoin& j : horz_join_list_) @@ -2109,36 +2236,53 @@ namespace Clipper2Lib { op1b->prev = op2b; op2b->next = op1b; - if (or1 == or2) + if (or1 == or2) // 'join' is really a split { - or2 = new OutRec(); + or2 = NewOutRec(); or2->pts = op1b; FixOutRecPts(or2); + + //if or1->pts has moved to or2 then update or1->pts!! if (or1->pts->outrec == or2) { or1->pts = j.op1; or1->pts->outrec = or1; } - if (using_polytree_) + if (using_polytree_) //#498, #520, #584, D#576, #618 { - if (Path1InsidePath2(or2->pts, or1->pts)) - SetOwner(or2, or1); - else if (Path1InsidePath2(or1->pts, or2->pts)) - SetOwner(or1, or2); - else + if (Path1InsidePath2(or1->pts, or2->pts)) + { + //swap or1's & or2's pts + OutPt* tmp = or1->pts; + or1->pts = or2->pts; + or2->pts = tmp; + FixOutRecPts(or1); + FixOutRecPts(or2); + //or2 is now inside or1 or2->owner = or1; + } + else if (Path1InsidePath2(or2->pts, or1->pts)) + { + or2->owner = or1; + } + else + or2->owner = or1->owner; + + if (!or1->splits) or1->splits = new OutRecList(); + or1->splits->push_back(or2); } else or2->owner = or1; - - outrec_list_.push_back(or2); } else { or2->pts = nullptr; if (using_polytree_) + { SetOwner(or2, or1); + MoveSplits(or2, or1); //#618 + } else or2->owner = or1; } @@ -2335,35 +2479,6 @@ namespace Clipper2Lib { } } - inline bool HorzIsSpike(const Active& horzEdge) - { - Point64 nextPt = NextVertex(horzEdge)->pt; - return (nextPt.y == horzEdge.bot.y) && - (horzEdge.bot.x < horzEdge.top.x) != (horzEdge.top.x < nextPt.x); - } - - inline void TrimHorz(Active& horzEdge, bool preserveCollinear) - { - bool wasTrimmed = false; - Point64 pt = NextVertex(horzEdge)->pt; - while (pt.y == horzEdge.top.y) - { - //always trim 180 deg. spikes (in closed paths) - //but otherwise break if preserveCollinear = true - if (preserveCollinear && - ((pt.x < horzEdge.top.x) != (horzEdge.bot.x < horzEdge.top.x))) - break; - - horzEdge.vertex_top = NextVertex(horzEdge); - horzEdge.top = pt; - wasTrimmed = true; - if (IsMaxima(horzEdge)) break; - pt = NextVertex(horzEdge)->pt; - } - - if (wasTrimmed) SetDx(horzEdge); // +/-infinity - } - void ClipperBase::DoHorizontal(Active& horz) /******************************************************************************* * Notes: Horizontal edges (HEs) at scanline intersections (ie at the top or * @@ -2389,10 +2504,10 @@ namespace Clipper2Lib { else vertex_max = GetCurrYMaximaVertex(horz); - // remove 180 deg.spikes and also simplify - // consecutive horizontals when PreserveCollinear = true - if (vertex_max && !horzIsOpen && vertex_max != horz.vertex_top) - TrimHorz(horz, PreserveCollinear); + //// remove 180 deg.spikes and also simplify + //// consecutive horizontals when PreserveCollinear = true + //if (!horzIsOpen && vertex_max != horz.vertex_top) + // TrimHorz(horz, PreserveCollinear); int64_t horz_left, horz_right; bool is_left_to_right = @@ -2407,7 +2522,6 @@ namespace Clipper2Lib { #endif AddTrialHorzJoin(op); } - OutRec* currHorzOutrec = horz.outrec; while (true) // loop through consec. horizontal edges { @@ -2422,6 +2536,9 @@ namespace Clipper2Lib { if (IsHotEdge(horz) && IsJoined(*e)) Split(*e, e->top); + //if (IsHotEdge(horz) != IsHotEdge(*e)) + // DoError(undefined_error_i); + if (IsHotEdge(horz)) { while (horz.vertex_top != vertex_max) @@ -2476,6 +2593,7 @@ namespace Clipper2Lib { { IntersectEdges(horz, *e, pt); SwapPositionsInAEL(horz, *e); + CheckJoinLeft(*e, pt); horz.curr_x = e->curr_x; e = horz.next_in_ael; } @@ -2483,13 +2601,13 @@ namespace Clipper2Lib { { IntersectEdges(*e, horz, pt); SwapPositionsInAEL(*e, horz); + CheckJoinRight(*e, pt); horz.curr_x = e->curr_x; e = horz.prev_in_ael; } - if (horz.outrec && horz.outrec != currHorzOutrec) + if (horz.outrec) { - currHorzOutrec = horz.outrec; //nb: The outrec containining the op returned by IntersectEdges //above may no longer be associated with horzEdge. AddTrialHorzJoin(GetLastOp(horz)); @@ -2519,14 +2637,16 @@ namespace Clipper2Lib { AddOutPt(horz, horz.top); UpdateEdgeIntoAEL(&horz); - if (PreserveCollinear && !horzIsOpen && HorzIsSpike(horz)) - TrimHorz(horz, true); - is_left_to_right = ResetHorzDirection(horz, vertex_max, horz_left, horz_right); } - if (IsHotEdge(horz)) AddOutPt(horz, horz.top); + if (IsHotEdge(horz)) + { + OutPt* op = AddOutPt(horz, horz.top); + AddTrialHorzJoin(op); + } + UpdateEdgeIntoAEL(&horz); // end of an intermediate horiz. } @@ -2638,10 +2758,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) || - pt.y < e.top.y + 2 || pt.y < prev->top.y + 2) // avoid trivial joins - return; + if (IsOpen(e) || !IsHotEdge(e) || !prev || + IsOpen(*prev) || !IsHotEdge(*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 if (check_curr_x) { @@ -2664,10 +2784,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) || - pt.y < e.top.y +2 || pt.y < next->top.y +2) // avoids trivial joins - return; + if (IsOpen(e) || !IsHotEdge(e) || + !next || IsOpen(*next) || !IsHotEdge(*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 if (check_curr_x) { @@ -2682,6 +2802,7 @@ namespace Clipper2Lib { JoinOutrecPaths(e, *next); else JoinOutrecPaths(*next, e); + e.join_with = JoinWith::Right; next->join_with = JoinWith::Left; } @@ -2752,12 +2873,34 @@ namespace Clipper2Lib { if (!outrec->bounds.IsEmpty()) return true; CleanCollinear(outrec); if (!outrec->pts || - !BuildPath64(outrec->pts, ReverseSolution, false, outrec->path)) - return false; + !BuildPath64(outrec->pts, reverse_solution_, false, outrec->path)){ + return false;} outrec->bounds = GetBounds(outrec->path); return true; } + bool ClipperBase::CheckSplitOwner(OutRec* outrec, OutRecList* splits) + { + for (auto split : *splits) + { + split = GetRealOutRec(split); + if(!split || split == outrec || split->recursive_split == outrec) continue; + split->recursive_split = outrec; // prevent infinite loops + + if (split->splits && CheckSplitOwner(outrec, split->splits)) + return true; + else if (CheckBounds(split) && + IsValidOwner(outrec, split) && + split->bounds.Contains(outrec->bounds) && + Path1InsidePath2(outrec->pts, split->pts)) + { + outrec->owner = split; //found in split + return true; + } + } + return false; + } + void ClipperBase::RecursiveCheckOwners(OutRec* outrec, PolyPath* polypath) { // pre-condition: outrec will have valid bounds @@ -2765,52 +2908,25 @@ namespace Clipper2Lib { if (outrec->polypath || outrec->bounds.IsEmpty()) return; - while (outrec->owner && - (!outrec->owner->pts || !CheckBounds(outrec->owner))) - outrec->owner = outrec->owner->owner; - - if (outrec->owner && !outrec->owner->polypath) - RecursiveCheckOwners(outrec->owner, polypath); - while (outrec->owner) - if (outrec->owner->bounds.Contains(outrec->bounds) && - Path1InsidePath2(outrec->pts, outrec->owner->pts)) - break; // found - owner contain outrec! - else - outrec->owner = outrec->owner->owner; + { + if (outrec->owner->splits && CheckSplitOwner(outrec, outrec->owner->splits)) break; + if (outrec->owner->pts && CheckBounds(outrec->owner) && + outrec->owner->bounds.Contains(outrec->bounds) && + Path1InsidePath2(outrec->pts, outrec->owner->pts)) break; + outrec->owner = outrec->owner->owner; + } if (outrec->owner) + { + if (!outrec->owner->polypath) + RecursiveCheckOwners(outrec->owner, polypath); outrec->polypath = outrec->owner->polypath->AddChild(outrec->path); + } else outrec->polypath = polypath->AddChild(outrec->path); } - void ClipperBase::DeepCheckOwners(OutRec* outrec, PolyPath* polypath) - { - RecursiveCheckOwners(outrec, polypath); - - while (outrec->owner && outrec->owner->splits) - { - OutRec* split = nullptr; - for (auto s : *outrec->owner->splits) - { - split = GetRealOutRec(s); - if (split && split != outrec && - split != outrec->owner && CheckBounds(split) && - split->bounds.Contains(outrec->bounds) && - Path1InsidePath2(outrec->pts, split->pts)) - { - RecursiveCheckOwners(split, polypath); - outrec->owner = split; //found in split - break; // inner 'for' loop - } - else - split = nullptr; - } - if (!split) break; - } - } - void Clipper64::BuildPaths64(Paths64& solutionClosed, Paths64* solutionOpen) { solutionClosed.resize(0); @@ -2832,7 +2948,7 @@ namespace Clipper2Lib { Path64 path; if (solutionOpen && outrec->is_open) { - if (BuildPath64(outrec->pts, ReverseSolution, true, path)) + if (BuildPath64(outrec->pts, reverse_solution_, true, path)) solutionOpen->emplace_back(std::move(path)); } else @@ -2840,7 +2956,7 @@ namespace Clipper2Lib { // nb: CleanCollinear can add to outrec_list_ CleanCollinear(outrec); //closed paths should always return a Positive orientation - if (BuildPath64(outrec->pts, ReverseSolution, false, path)) + if (BuildPath64(outrec->pts, reverse_solution_, false, path)) solutionClosed.emplace_back(std::move(path)); } } @@ -2863,13 +2979,13 @@ namespace Clipper2Lib { if (outrec->is_open) { Path64 path; - if (BuildPath64(outrec->pts, ReverseSolution, true, path)) + if (BuildPath64(outrec->pts, reverse_solution_, true, path)) open_paths.push_back(path); continue; } if (CheckBounds(outrec)) - DeepCheckOwners(outrec, &polytree); + RecursiveCheckOwners(outrec, &polytree); } } @@ -2940,14 +3056,14 @@ namespace Clipper2Lib { PathD path; if (solutionOpen && outrec->is_open) { - if (BuildPathD(outrec->pts, ReverseSolution, true, path, invScale_)) + if (BuildPathD(outrec->pts, reverse_solution_, true, path, invScale_)) solutionOpen->emplace_back(std::move(path)); } else { CleanCollinear(outrec); //closed paths should always return a Positive orientation - if (BuildPathD(outrec->pts, ReverseSolution, false, path, invScale_)) + if (BuildPathD(outrec->pts, reverse_solution_, false, path, invScale_)) solutionClosed.emplace_back(std::move(path)); } } @@ -2960,19 +3076,22 @@ namespace Clipper2Lib { if (has_open_paths_) open_paths.reserve(outrec_list_.size()); - for (OutRec* outrec : outrec_list_) + // outrec_list_.size() is not static here because + // BuildPathD below can indirectly add additional OutRec //#607 + for (size_t i = 0; i < outrec_list_.size(); ++i) { + OutRec* outrec = outrec_list_[i]; if (!outrec || !outrec->pts) continue; if (outrec->is_open) { PathD path; - if (BuildPathD(outrec->pts, ReverseSolution, true, path, invScale_)) + if (BuildPathD(outrec->pts, reverse_solution_, true, path, invScale_)) open_paths.push_back(path); continue; } if (CheckBounds(outrec)) - DeepCheckOwners(outrec, &polytree); + RecursiveCheckOwners(outrec, &polytree); } } diff --git a/CPP/Clipper2Lib/src/clipper.offset.cpp b/CPP/Clipper2Lib/src/clipper.offset.cpp index 78cd8237..0282aa49 100644 --- a/CPP/Clipper2Lib/src/clipper.offset.cpp +++ b/CPP/Clipper2Lib/src/clipper.offset.cpp @@ -1,6 +1,6 @@ /******************************************************************************* * Author : Angus Johnson * -* Date : 22 March 2023 * +* Date : 28 November 2023 * * Website : http://www.angusj.com * * Copyright : Angus Johnson 2010-2023 * * Purpose : Path Offset (Inflate/Shrink) * @@ -20,38 +20,63 @@ const double floating_point_tolerance = 1e-12; // Miscellaneous methods //------------------------------------------------------------------------------ -void GetBoundsAndLowestPolyIdx(const Paths64& paths, Rect64& r, int & idx) +inline bool ToggleBoolIf(bool val, bool condition) { - idx = -1; - r = MaxInvalidRect64; - int64_t lpx = 0; - for (int i = 0; i < static_cast(paths.size()); ++i) - for (const Point64& p : paths[i]) + 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) { - if (p.y >= r.bottom) - { - if (p.y > r.bottom || p.x < lpx) - { - idx = i; - lpx = p.x; - r.bottom = p.y; - } - } - else if (p.y < r.top) r.top = p.y; - if (p.x > r.right) r.right = p.x; - else if (p.x < r.left) r.left = p.x; + 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; } - //if (idx < 0) r = Rect64(0, 0, 0, 0); - //if (r.top == INT64_MIN) r.bottom = r.top; - //if (r.left == INT64_MIN) r.left = r.right; + recList.push_back(r); + } } -bool IsSafeOffset(const Rect64& r, double abs_delta) +bool ValidateBounds(std::vector& recList, double delta) { - return r.left > min_coord + abs_delta && - r.right < max_coord - abs_delta && - r.top > min_coord + abs_delta && - r.bottom < max_coord - abs_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; + 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)) + { + botPt = Point64(r.left, r.bottom); + result = static_cast(i); + } + } + return result; } PointD GetUnitNormal(const Point64& pt1, const Point64& pt2) @@ -78,8 +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; @@ -126,6 +150,44 @@ inline void NegatePath(PathD& path) } } + +//------------------------------------------------------------------------------ +// ClipperOffset::Group methods +//------------------------------------------------------------------------------ + +ClipperOffset::Group::Group(const Paths64& _paths, JoinType _join_type, EndType _end_type): + paths_in(_paths), join_type(_join_type), end_type(_end_type) +{ + bool is_joined = + (end_type == EndType::Polygon) || + (end_type == EndType::Joined); + 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()); + for (const Path64& path : paths_in) + is_hole_list.push_back(Area(path) < 0); + lowest_path_idx = GetLowestClosedPathIdx(bounds_list); + // 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(); + } + else + { + lowest_path_idx = -1; + is_reversed = false; + is_hole_list.resize(paths_in.size()); + } +} + + //------------------------------------------------------------------------------ // ClipperOffset methods //------------------------------------------------------------------------------ @@ -148,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) @@ -201,19 +263,39 @@ PointD IntersectPoint(const PointD& pt1a, const PointD& pt1b, } } -void ClipperOffset::DoSquare(Group& group, const Path64& path, size_t j, size_t k) +void ClipperOffset::DoBevel(const Path64& path, size_t j, size_t k) +{ + PointD pt1, pt2; + if (j == 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); + pt2 = PointD(path[j].x + group_delta_ * norms[j].x, path[j].y + group_delta_ * norms[j].y); + } + path_out.push_back(Point64(pt1)); + path_out.push_back(Point64(pt2)); +} + +void ClipperOffset::DoSquare(const Path64& path, size_t j, size_t k) { PointD vec; if (j == k) - vec = PointD(norms[0].y, -norms[0].x); + vec = PointD(norms[j].y, -norms[j].x); else vec = GetAvgUnitVector( PointD(-norms[k].y, norms[k].x), PointD(norms[j].y, -norms[j].x)); + double abs_delta = std::abs(group_delta_); + // now offset the original vertex delta units along unit vector PointD ptQ = PointD(path[j]); - ptQ = TranslatePoint(ptQ, abs_group_delta_ * vec.x, abs_group_delta_ * vec.y); + ptQ = TranslatePoint(ptQ, abs_delta * vec.x, abs_delta * vec.y); // get perpendicular vertices PointD pt1 = TranslatePoint(ptQ, group_delta_ * vec.y, group_delta_ * -vec.x); PointD pt2 = TranslatePoint(ptQ, group_delta_ * -vec.y, group_delta_ * vec.x); @@ -227,8 +309,8 @@ void ClipperOffset::DoSquare(Group& group, const Path64& path, size_t j, size_t pt.z = ptQ.z; #endif //get the second intersect point through reflecion - group.path.push_back(Point64(ReflectPoint(pt, ptQ))); - group.path.push_back(Point64(pt)); + path_out.push_back(Point64(ReflectPoint(pt, ptQ))); + path_out.push_back(Point64(pt)); } else { @@ -237,57 +319,67 @@ void ClipperOffset::DoSquare(Group& group, const Path64& path, size_t j, size_t #ifdef USINGZ pt.z = ptQ.z; #endif - group.path.push_back(Point64(pt)); + path_out.push_back(Point64(pt)); //get the second intersect point through reflecion - group.path.push_back(Point64(ReflectPoint(pt, ptQ))); + path_out.push_back(Point64(ReflectPoint(pt, ptQ))); } } -void ClipperOffset::DoMiter(Group& group, const Path64& path, size_t j, size_t k, double cos_a) +void ClipperOffset::DoMiter(const Path64& path, size_t j, size_t k, double cos_a) { double q = group_delta_ / (cos_a + 1); #ifdef USINGZ - group.path.push_back(Point64( + path_out.push_back(Point64( path[j].x + (norms[k].x + norms[j].x) * q, path[j].y + (norms[k].y + norms[j].y) * q, path[j].z)); #else - group.path.push_back(Point64( + path_out.push_back(Point64( path[j].x + (norms[k].x + norms[j].x) * q, path[j].y + (norms[k].y + norms[j].y) * q)); #endif } -void ClipperOffset::DoRound(Group& group, const Path64& path, size_t j, size_t k, double angle) +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, + // 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 ? + std::min(abs_delta, arc_tolerance_) : + std::log10(2 + abs_delta) * default_arc_tolerance); + double steps_per_360 = std::min(PI / std::acos(1 - arcTol / abs_delta), abs_delta * PI); + step_sin_ = std::sin(2 * PI / steps_per_360); + step_cos_ = std::cos(2 * PI / steps_per_360); + if (group_delta_ < 0.0) step_sin_ = -step_sin_; + steps_per_rad_ = steps_per_360 / (2 * PI); + } + Point64 pt = path[j]; PointD offsetVec = PointD(norms[k].x * group_delta_, norms[k].y * group_delta_); if (j == k) offsetVec.Negate(); #ifdef USINGZ - group.path.push_back(Point64(pt.x + offsetVec.x, pt.y + offsetVec.y, pt.z)); + path_out.push_back(Point64(pt.x + offsetVec.x, pt.y + offsetVec.y, pt.z)); #else - group.path.push_back(Point64(pt.x + offsetVec.x, pt.y + offsetVec.y)); + path_out.push_back(Point64(pt.x + offsetVec.x, pt.y + offsetVec.y)); #endif - if (angle > -PI + 0.01) // avoid 180deg concave + int steps = static_cast(std::ceil(steps_per_rad_ * std::abs(angle))); // #448, #456 + for (int i = 1; i < steps; ++i) // ie 1 less than steps { - int steps = static_cast(std::ceil(steps_per_rad_ * std::abs(angle))); // #448, #456 - for (int i = 1; i < steps; ++i) // ie 1 less than steps - { - offsetVec = PointD(offsetVec.x * step_cos_ - step_sin_ * offsetVec.y, - offsetVec.x * step_sin_ + offsetVec.y * step_cos_); + offsetVec = PointD(offsetVec.x * step_cos_ - step_sin_ * offsetVec.y, + offsetVec.x * step_sin_ + offsetVec.y * step_cos_); #ifdef USINGZ - group.path.push_back(Point64(pt.x + offsetVec.x, pt.y + offsetVec.y, pt.z)); + path_out.push_back(Point64(pt.x + offsetVec.x, pt.y + offsetVec.y, pt.z)); #else - group.path.push_back(Point64(pt.x + offsetVec.x, pt.y + offsetVec.y)); + path_out.push_back(Point64(pt.x + offsetVec.x, pt.y + offsetVec.y)); #endif - - } } - group.path.push_back(GetPerpendic(path[j], norms[j], group_delta_)); + path_out.push_back(GetPerpendic(path[j], norms[j], group_delta_)); } -void ClipperOffset::OffsetPoint(Group& group, Path64& path, size_t j, size_t& k) +void ClipperOffset::OffsetPoint(Group& group, const Path64& path, size_t j, size_t k) { // Let A = change in angle where edges join // A == 0: ie no change in angle (flat join) @@ -302,50 +394,57 @@ void ClipperOffset::OffsetPoint(Group& group, Path64& path, size_t j, size_t& k) if (sin_a > 1.0) sin_a = 1.0; else if (sin_a < -1.0) sin_a = -1.0; - if (cos_a > 0.99) // almost straight - less than 8 degrees + if (deltaCallback64_) { + group_delta_ = deltaCallback64_(path, norms, j, k); + if (group.is_reversed) group_delta_ = -group_delta_; + } + if (std::fabs(group_delta_) <= floating_point_tolerance) { - group.path.push_back(GetPerpendic(path[j], norms[k], group_delta_)); - if (cos_a < 0.9998) // greater than 1 degree (#424) - group.path.push_back(GetPerpendic(path[j], norms[j], group_delta_)); // (#418) + path_out.push_back(path[j]); + return; } - else if (cos_a > -0.99 && (sin_a * group_delta_ < 0)) + + if (cos_a > -0.99 && (sin_a * group_delta_ < 0)) // test for concavity first (#593) { // is concave - group.path.push_back(GetPerpendic(path[j], norms[k], group_delta_)); + 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 - group.path.push_back(path[j]); // (#405) - group.path.push_back(GetPerpendic(path[j], norms[j], group_delta_)); - } - else if (join_type_ == JoinType::Round) - DoRound(group, path, j, k, std::atan2(sin_a, cos_a)); + // 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_)); + } + else if (cos_a > 0.999 && join_type_ != JoinType::Round) + { + // almost straight - less than 2.5 degree (#424, #482, #526 & #724) + DoMiter(path, j, k, cos_a); + } else if (join_type_ == JoinType::Miter) { - // miter unless the angle is so acute the miter would exceeds ML - if (cos_a > temp_lim_ - 1) DoMiter(group, path, j, k, cos_a); - else DoSquare(group, path, j, k); + // miter unless the angle is sufficiently acute to exceed ML + if (cos_a > temp_lim_ - 1) DoMiter(path, j, k, cos_a); + else DoSquare(path, j, k); } - // don't bother squaring angles that deviate < ~20 degrees because - // squaring will be indistinguishable from mitering and just be a lot slower - else if (cos_a > 0.9) - DoMiter(group, path, j, k, cos_a); + else if (join_type_ == JoinType::Round) + DoRound(path, j, k, std::atan2(sin_a, cos_a)); + else if ( join_type_ == JoinType::Bevel) + DoBevel(path, j, k); else - DoSquare(group, path, j, k); - - k = j; + DoSquare(path, j, k); } -void ClipperOffset::OffsetPolygon(Group& group, Path64& path) +void ClipperOffset::OffsetPolygon(Group& group, const Path64& path) { - for (Path64::size_type i = 0, j = path.size() -1; i < path.size(); j = i, ++i) - OffsetPoint(group, path, i, j); - group.paths_out.push_back(group.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); } -void ClipperOffset::OffsetOpenJoined(Group& group, Path64& path) +void ClipperOffset::OffsetOpenJoined(Group& group, const Path64& path) { OffsetPolygon(group, path); - std::reverse(path.begin(), path.end()); + Path64 reverse_path(path); + std::reverse(reverse_path.begin(), reverse_path.end()); //rebuild normals // BuildNormals(path); std::reverse(norms.begin(), norms.end()); @@ -353,41 +452,36 @@ void ClipperOffset::OffsetOpenJoined(Group& group, Path64& path) norms.erase(norms.begin()); NegatePath(norms); - group.path.clear(); - OffsetPolygon(group, path); + OffsetPolygon(group, reverse_path); } -void ClipperOffset::OffsetOpenPath(Group& group, Path64& path) +void ClipperOffset::OffsetOpenPath(Group& group, const Path64& path) { // do the line start cap - switch (end_type_) + if (deltaCallback64_) group_delta_ = deltaCallback64_(path, norms, 0, 0); + + if (std::fabs(group_delta_) <= floating_point_tolerance) + path_out.push_back(path[0]); + else { - case EndType::Butt: -#ifdef USINGZ - group.path.push_back(Point64( - path[0].x - norms[0].x * group_delta_, - path[0].y - norms[0].y * group_delta_, - path[0].z)); -#else - group.path.push_back(Point64( - path[0].x - norms[0].x * group_delta_, - path[0].y - norms[0].y * group_delta_)); -#endif - group.path.push_back(GetPerpendic(path[0], norms[0], group_delta_)); - break; - case EndType::Round: - DoRound(group, path, 0,0, PI); - break; - default: - DoSquare(group, path, 0, 0); - break; + switch (end_type_) + { + case EndType::Butt: + DoBevel(path, 0, 0); + break; + case EndType::Round: + DoRound(path, 0, 0, PI); + break; + default: + DoSquare(path, 0, 0); + break; + } } - + size_t highI = path.size() - 1; - // offset the left side going forward - for (Path64::size_type i = 1, k = 0; i < highI; ++i) - OffsetPoint(group, path, i, k); + for (Path64::size_type j = 1, k = 0; j < highI; k = j, ++j) + OffsetPoint(group, path, j, k); // reverse normals for (size_t i = highI; i > 0; --i) @@ -395,60 +489,46 @@ void ClipperOffset::OffsetOpenPath(Group& group, Path64& path) norms[0] = norms[highI]; // do the line end cap - switch (end_type_) + if (deltaCallback64_) + group_delta_ = deltaCallback64_(path, norms, highI, highI); + + if (std::fabs(group_delta_) <= floating_point_tolerance) + path_out.push_back(path[highI]); + else { - case EndType::Butt: -#ifdef USINGZ - group.path.push_back(Point64( - path[highI].x - norms[highI].x * group_delta_, - path[highI].y - norms[highI].y * group_delta_, - path[highI].z)); -#else - group.path.push_back(Point64( - path[highI].x - norms[highI].x * group_delta_, - path[highI].y - norms[highI].y * group_delta_)); -#endif - group.path.push_back(GetPerpendic(path[highI], norms[highI], group_delta_)); - break; - case EndType::Round: - DoRound(group, path, highI, highI, PI); - break; - default: - DoSquare(group, path, highI, highI); - break; + switch (end_type_) + { + case EndType::Butt: + DoBevel(path, highI, highI); + break; + case EndType::Round: + DoRound(path, highI, highI, PI); + break; + default: + DoSquare(path, highI, highI); + break; + } } - for (size_t i = highI, k = 0; i > 0; --i) - OffsetPoint(group, path, i, k); - group.paths_out.push_back(group.path); + for (size_t j = highI, k = 0; j > 0; k = j, --j) + OffsetPoint(group, path, j, k); + solution.push_back(path_out); } void ClipperOffset::DoGroupOffset(Group& group) { - Rect64 r; - int idx = -1; - //the lowermost polygon must be an outer polygon. So we can use that as the - //designated orientation for outer polygons (needed for tidy-up clipping) - GetBoundsAndLowestPolyIdx(group.paths_in, r, idx); - if (idx < 0) return; - if (group.end_type == EndType::Polygon) { - double area = Area(group.paths_in[idx]); - //if (area == 0) return; // probably unhelpful (#430) - group.is_reversed = (area < 0); - if (group.is_reversed) group_delta_ = -delta_; - else group_delta_ = delta_; - } - else - { - group.is_reversed = false; - group_delta_ = std::abs(delta_) * 0.5; + // 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_; } - abs_group_delta_ = std::fabs(group_delta_); + else + group_delta_ = std::abs(delta_);// *0.5; - // do range checking - if (!IsSafeOffset(r, abs_group_delta_)) + double abs_delta = std::fabs(group_delta_); + if (!ValidateBounds(group.bounds_list, abs_delta)) { DoError(range_error_i); error_code_ |= range_error_i; @@ -458,80 +538,98 @@ void ClipperOffset::DoGroupOffset(Group& group) join_type_ = group.join_type; end_type_ = group.end_type; - //calculate a sensible number of steps (for 360 deg for the given offset 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 double arcTol = (arc_tolerance_ > floating_point_tolerance ? - std::min(abs_group_delta_, arc_tolerance_) : - std::log10(2 + abs_group_delta_) * default_arc_tolerance); - double steps_per_360 = PI / std::acos(1 - arcTol / abs_group_delta_); - if (steps_per_360 > abs_group_delta_ * PI) - steps_per_360 = abs_group_delta_ * PI; //ie avoids excessive precision + std::min(abs_delta, arc_tolerance_) : + std::log10(2 + abs_delta) * default_arc_tolerance); + double steps_per_360 = std::min(PI / std::acos(1 - arcTol / abs_delta), abs_delta * PI); step_sin_ = std::sin(2 * PI / steps_per_360); step_cos_ = std::cos(2 * PI / steps_per_360); - if (group_delta_ < 0.0) step_sin_ = -step_sin_; - steps_per_rad_ = steps_per_360 / (2 *PI); + if (group_delta_ < 0.0) step_sin_ = -step_sin_; + steps_per_rad_ = steps_per_360 / (2 * PI); } - bool is_joined = - (end_type_ == EndType::Polygon) || - (end_type_ == EndType::Joined); - Paths64::const_iterator path_iter; - for(path_iter = group.paths_in.cbegin(); path_iter != group.paths_in.cend(); ++path_iter) + 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(); + for ( ; path_in_it != group.paths_in.cend(); ++path_in_it, ++path_rect_it, ++is_hole_it) { - Path64 path = StripDuplicates(*path_iter, is_joined); - Path64::size_type cnt = path.size(); - if (cnt == 0 || ((cnt < 3) && group.end_type == EndType::Polygon)) - continue; + if (!path_rect_it->IsValid()) continue; + Path64::size_type pathLen = path_in_it->size(); + path_out.clear(); - group.path.clear(); - if (cnt == 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]; //single vertex so build a circle or square ... if (group.join_type == JoinType::Round) { - double radius = abs_group_delta_; - group.path = Ellipse(path[0], radius, radius); + double radius = abs_delta; + int steps = static_cast(std::ceil(steps_per_rad_ * 2 * PI)); //#617 + path_out = Ellipse(pt, radius, radius, steps); #ifdef USINGZ - for (auto& p : group.path) p.z = path[0].z; + for (auto& p : path_out) p.z = pt.z; #endif } else { - int d = (int)std::ceil(abs_group_delta_); - r = Rect64(path[0].x - d, path[0].y - d, path[0].x + d, path[0].y + d); - group.path = r.AsPath(); + int d = (int)std::ceil(abs_delta); + Rect64 r = Rect64(pt.x - d, pt.y - d, pt.x + d, pt.y + d); + path_out = r.AsPath(); #ifdef USINGZ - for (auto& p : group.path) p.z = path[0].z; + for (auto& p : path_out) p.z = pt.z; #endif } - group.paths_out.push_back(group.path); - } - else - { - if ((cnt == 2) && (group.end_type == EndType::Joined)) - { - if (group.join_type == JoinType::Round) - end_type_ = EndType::Round; - else - end_type_ = EndType::Square; - } + 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); + else if (end_type_ == EndType::Joined) OffsetOpenJoined(group, *path_in_it); + else OffsetOpenPath(group, *path_in_it); + } +} + + +size_t ClipperOffset::CalcSolutionCapacity() +{ + size_t result = 0; + for (const Group& g : groups_) + result += (g.end_type == EndType::Joined) ? g.paths_in.size() * 2 : g.paths_in.size(); + return result; +} - BuildNormals(path); - if (end_type_ == EndType::Polygon) OffsetPolygon(group, path); - else if (end_type_ == EndType::Joined) OffsetOpenJoined(group, path); - else OffsetOpenPath(group, path); +bool ClipperOffset::CheckReverseOrientation() +{ + // nb: this assumes there's consistency in orientation between groups + bool is_reversed_orientation = false; + for (const Group& g : groups_) + if (g.end_type == EndType::Polygon) + { + is_reversed_orientation = g.is_reversed; + break; } - } - solution.reserve(solution.size() + group.paths_out.size()); - copy(group.paths_out.begin(), group.paths_out.end(), back_inserter(solution)); - group.paths_out.clear(); + return is_reversed_orientation; } void ClipperOffset::ExecuteInternal(double delta) @@ -539,29 +637,29 @@ void ClipperOffset::ExecuteInternal(double delta) error_code_ = 0; solution.clear(); if (groups_.size() == 0) return; + solution.reserve(CalcSolutionCapacity()); - if (std::abs(delta) < 0.5) + 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); for (const Group& group : groups_) - { - solution.reserve(solution.size() + group.paths_in.size()); 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_); + return; + } - delta_ = delta; - std::vector::iterator git; - for (git = groups_.begin(); git != groups_.end(); ++git) - { - DoGroupOffset(*git); - if (!error_code_) continue; // all OK - solution.clear(); - } + 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(); } } @@ -572,19 +670,17 @@ void ClipperOffset::Execute(double delta, Paths64& paths) ExecuteInternal(delta); if (!solution.size()) return; - paths = solution; + bool paths_reversed = CheckReverseOrientation(); //clean up self-intersections ... Clipper64 c; - c.PreserveCollinear = false; + c.PreserveCollinear(false); //the solution should retain the orientation of the input - c.ReverseSolution = reverse_solution_ != groups_[0].is_reversed; + c.ReverseSolution(reverse_solution_ != paths_reversed); #ifdef USINGZ - if (zCallback64_) { - c.SetZCallback(zCallback64_); - } + if (zCallback64_) { c.SetZCallback(zCallback64_); } #endif c.AddSubject(solution); - if (groups_[0].is_reversed) + if (paths_reversed) c.Execute(ClipType::Union, FillRule::Negative, paths); else c.Execute(ClipType::Union, FillRule::Positive, paths); @@ -598,21 +694,30 @@ void ClipperOffset::Execute(double delta, PolyTree64& polytree) ExecuteInternal(delta); if (!solution.size()) return; + bool paths_reversed = CheckReverseOrientation(); //clean up self-intersections ... Clipper64 c; - c.PreserveCollinear = false; + c.PreserveCollinear(false); //the solution should retain the orientation of the input - c.ReverseSolution = reverse_solution_ != groups_[0].is_reversed; + c.ReverseSolution (reverse_solution_ != paths_reversed); #ifdef USINGZ if (zCallback64_) { c.SetZCallback(zCallback64_); } #endif c.AddSubject(solution); - if (groups_[0].is_reversed) + + + if (paths_reversed) c.Execute(ClipType::Union, FillRule::Negative, polytree); else c.Execute(ClipType::Union, FillRule::Positive, polytree); } +void ClipperOffset::Execute(DeltaCallback64 delta_cb, Paths64& paths) +{ + deltaCallback64_ = delta_cb; + Execute(1.0, paths); +} + } // namespace diff --git a/CPP/Clipper2Lib/src/clipper.rectclip.cpp b/CPP/Clipper2Lib/src/clipper.rectclip.cpp index 959972b4..9aa0fc0f 100644 --- a/CPP/Clipper2Lib/src/clipper.rectclip.cpp +++ b/CPP/Clipper2Lib/src/clipper.rectclip.cpp @@ -1,6 +1,6 @@ /******************************************************************************* * Author : Angus Johnson * -* Date : 14 February 2023 * +* Date : 8 September 2023 * * Website : http://www.angusj.com * * Copyright : Angus Johnson 2010-2023 * * Purpose : FAST rectangular clipping * @@ -24,11 +24,11 @@ namespace Clipper2Lib { for (const Point64& pt : path2) { PointInPolygonResult pip = PointInPolygon(pt, path1); - switch (pip) + switch (pip) { case PointInPolygonResult::IsOutside: ++io_count; break; - case PointInPolygonResult::IsInside: --io_count; break; - default: continue; + case PointInPolygonResult::IsInside: --io_count; break; + default: continue; } if (std::abs(io_count) > 1) break; } @@ -66,6 +66,56 @@ namespace Clipper2Lib { return true; } + inline bool IsHorizontal(const Point64& pt1, const Point64& pt2) + { + return pt1.y == pt2.y; + } + + inline bool GetSegmentIntersection(const Point64& p1, + const Point64& p2, const Point64& p3, const Point64& p4, Point64& ip) + { + double res1 = CrossProduct(p1, p3, p4); + double res2 = CrossProduct(p2, p3, p4); + if (res1 == 0) + { + ip = p1; + if (res2 == 0) return false; // segments are collinear + else if (p1 == p3 || p1 == p4) return true; + //else if (p2 == p3 || p2 == p4) { ip = p2; return true; } + else if (IsHorizontal(p3, p4)) return ((p1.x > p3.x) == (p1.x < p4.x)); + else return ((p1.y > p3.y) == (p1.y < p4.y)); + } + else if (res2 == 0) + { + ip = p2; + if (p2 == p3 || p2 == p4) return true; + else if (IsHorizontal(p3, p4)) return ((p2.x > p3.x) == (p2.x < p4.x)); + else return ((p2.y > p3.y) == (p2.y < p4.y)); + } + if ((res1 > 0) == (res2 > 0)) return false; + + double res3 = CrossProduct(p3, p1, p2); + double res4 = CrossProduct(p4, p1, p2); + if (res3 == 0) + { + ip = p3; + if (p3 == p1 || p3 == p2) return true; + else if (IsHorizontal(p1, p2)) return ((p3.x > p1.x) == (p3.x < p2.x)); + else return ((p3.y > p1.y) == (p3.y < p2.y)); + } + else if (res4 == 0) + { + ip = p4; + if (p4 == p1 || p4 == p2) return true; + else if (IsHorizontal(p1, p2)) return ((p4.x > p1.x) == (p4.x < p2.x)); + else return ((p4.y > p1.y) == (p4.y < p2.y)); + } + if ((res3 > 0) == (res4 > 0)) return false; + + // segments must intersect to get here + return GetIntersectPoint(p1, p2, p3, p4, ip); + } + inline bool GetIntersection(const Path64& rectPath, const Point64& p, const Point64& p2, Location& loc, Point64& ip) { @@ -74,100 +124,84 @@ namespace Clipper2Lib { switch (loc) { case Location::Left: - if (SegmentsIntersect(p, p2, rectPath[0], rectPath[3], true)) - GetIntersectPoint(p, p2, rectPath[0], rectPath[3], ip); - else if (p.y < rectPath[0].y && - SegmentsIntersect(p, p2, rectPath[0], rectPath[1], true)) + 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)) { - GetIntersectPoint(p, p2, rectPath[0], rectPath[1], ip); loc = Location::Top; + return true; } - else if (SegmentsIntersect(p, p2, rectPath[2], rectPath[3], true)) + else if (GetSegmentIntersection(p, p2, rectPath[2], rectPath[3], ip)) { - GetIntersectPoint(p, p2, rectPath[2], rectPath[3], ip); loc = Location::Bottom; + return true; } else return false; - break; case Location::Top: - if (SegmentsIntersect(p, p2, rectPath[0], rectPath[1], true)) - GetIntersectPoint(p, p2, rectPath[0], rectPath[1], ip); - else if (p.x < rectPath[0].x && - SegmentsIntersect(p, p2, rectPath[0], rectPath[3], true)) + if (GetSegmentIntersection(p, p2, rectPath[0], rectPath[1], ip)) return true; + else if ((p.x < rectPath[0].x) && GetSegmentIntersection(p, p2, rectPath[0], rectPath[3], ip)) { - GetIntersectPoint(p, p2, rectPath[0], rectPath[3], ip); loc = Location::Left; + return true; } - else if (p.x > rectPath[1].x && - SegmentsIntersect(p, p2, rectPath[1], rectPath[2], true)) + else if (GetSegmentIntersection(p, p2, rectPath[1], rectPath[2], ip)) { - GetIntersectPoint(p, p2, rectPath[1], rectPath[2], ip); loc = Location::Right; + return true; } else return false; - break; case Location::Right: - if (SegmentsIntersect(p, p2, rectPath[1], rectPath[2], true)) - GetIntersectPoint(p, p2, rectPath[1], rectPath[2], ip); - else if (p.y < rectPath[0].y && - SegmentsIntersect(p, p2, rectPath[0], rectPath[1], true)) + if (GetSegmentIntersection(p, p2, rectPath[1], rectPath[2], ip)) return true; + else if ((p.y < rectPath[1].y) && GetSegmentIntersection(p, p2, rectPath[0], rectPath[1], ip)) { - GetIntersectPoint(p, p2, rectPath[0], rectPath[1], ip); loc = Location::Top; + return true; } - else if (SegmentsIntersect(p, p2, rectPath[2], rectPath[3], true)) + else if (GetSegmentIntersection(p, p2, rectPath[2], rectPath[3], ip)) { - GetIntersectPoint(p, p2, rectPath[2], rectPath[3], ip); loc = Location::Bottom; + return true; } else return false; - break; case Location::Bottom: - if (SegmentsIntersect(p, p2, rectPath[2], rectPath[3], true)) - GetIntersectPoint(p, p2, rectPath[2], rectPath[3], ip); - else if (p.x < rectPath[3].x && - SegmentsIntersect(p, p2, rectPath[0], rectPath[3], true)) + if (GetSegmentIntersection(p, p2, rectPath[2], rectPath[3], ip)) return true; + else if ((p.x < rectPath[3].x) && GetSegmentIntersection(p, p2, rectPath[0], rectPath[3], ip)) { - GetIntersectPoint(p, p2, rectPath[0], rectPath[3], ip); loc = Location::Left; + return true; } - else if (p.x > rectPath[2].x && - SegmentsIntersect(p, p2, rectPath[1], rectPath[2], true)) + else if (GetSegmentIntersection(p, p2, rectPath[1], rectPath[2], ip)) { - GetIntersectPoint(p, p2, rectPath[1], rectPath[2], ip); loc = Location::Right; + return true; } else return false; - break; default: // loc == rInside - if (SegmentsIntersect(p, p2, rectPath[0], rectPath[3], true)) + if (GetSegmentIntersection(p, p2, rectPath[0], rectPath[3], ip)) { - GetIntersectPoint(p, p2, rectPath[0], rectPath[3], ip); loc = Location::Left; + return true; } - else if (SegmentsIntersect(p, p2, rectPath[0], rectPath[1], true)) + else if (GetSegmentIntersection(p, p2, rectPath[0], rectPath[1], ip)) { - GetIntersectPoint(p, p2, rectPath[0], rectPath[1], ip); loc = Location::Top; + return true; } - else if (SegmentsIntersect(p, p2, rectPath[1], rectPath[2], true)) + else if (GetSegmentIntersection(p, p2, rectPath[1], rectPath[2], ip)) { - GetIntersectPoint(p, p2, rectPath[1], rectPath[2], ip); loc = Location::Right; + return true; } - else if (SegmentsIntersect(p, p2, rectPath[2], rectPath[3], true)) + else if (GetSegmentIntersection(p, p2, rectPath[2], rectPath[3], ip)) { - GetIntersectPoint(p, p2, rectPath[2], rectPath[3], ip); loc = Location::Bottom; + return true; } else return false; - break; } - return true; } inline Location GetAdjacentLocation(Location loc, bool isClockwise) @@ -281,7 +315,7 @@ namespace Clipper2Lib { // RectClip64 //---------------------------------------------------------------------------- - OutPt2* RectClip::Add(Point64 pt, bool start_new) + OutPt2* RectClip64::Add(Point64 pt, bool start_new) { // this method is only called by InternalExecute. // Later splitting & rejoining won't create additional op's, @@ -312,7 +346,7 @@ namespace Clipper2Lib { return result; } - void RectClip::AddCorner(Location prev, Location curr) + void RectClip64::AddCorner(Location prev, Location curr) { if (HeadingClockwise(prev, curr)) Add(rect_as_path_[static_cast(prev)]); @@ -320,7 +354,7 @@ namespace Clipper2Lib { Add(rect_as_path_[static_cast(curr)]); } - void RectClip::AddCorner(Location& loc, bool isClockwise) + void RectClip64::AddCorner(Location& loc, bool isClockwise) { if (isClockwise) { @@ -334,7 +368,7 @@ namespace Clipper2Lib { } } - void RectClip::GetNextLocation(const Path64& path, + void RectClip64::GetNextLocation(const Path64& path, Location& loc, int& i, int highI) { switch (loc) @@ -389,7 +423,7 @@ namespace Clipper2Lib { } //switch } - void RectClip::ExecuteInternal(const Path64& path) + void RectClip64::ExecuteInternal(const Path64& path) { int i = 0, highI = static_cast(path.size()) - 1; Location prev = Location::Inside, loc; @@ -474,7 +508,7 @@ namespace Clipper2Lib { // intersect pt but we'll also need the first intersect pt (ip2) loc = prev; GetIntersection(rect_as_path_, prev_pt, path[i], loc, ip2); - if (crossing_prev != Location::Inside) + if (crossing_prev != Location::Inside && crossing_prev != loc) //579 AddCorner(crossing_prev, loc); if (first_cross_ == Location::Inside) @@ -546,7 +580,7 @@ namespace Clipper2Lib { } } - void RectClip::CheckEdges() + void RectClip64::CheckEdges() { for (size_t i = 0; i < results_.size(); ++i) { @@ -606,7 +640,7 @@ namespace Clipper2Lib { } } - void RectClip::TidyEdges(int idx, OutPt2List& cw, OutPt2List& ccw) + void RectClip64::TidyEdges(int idx, OutPt2List& cw, OutPt2List& ccw) { if (ccw.empty()) return; bool isHorz = ((idx == 1) || (idx == 3)); @@ -619,7 +653,7 @@ namespace Clipper2Lib { p1 = cw[i]; if (!p1 || p1->next == p1->prev) { - cw[i++]->edge = nullptr; + cw[i++] = nullptr; j = 0; continue; } @@ -784,7 +818,7 @@ namespace Clipper2Lib { } } - Path64 RectClip::GetPath(OutPt2*& op) + Path64 RectClip64::GetPath(OutPt2*& op) { if (!op || op->next == op->prev) return Path64(); @@ -814,13 +848,13 @@ namespace Clipper2Lib { return result; } - Paths64 RectClip::Execute(const Paths64& paths, bool convex_only) + Paths64 RectClip64::Execute(const Paths64& paths) { Paths64 result; if (rect_.IsEmpty()) return result; - for (const auto& path : paths) - { + for (const Path64& path : paths) + { if (path.size() < 3) continue; path_bounds_ = GetBounds(path); if (!rect_.Intersects(path_bounds_)) @@ -833,13 +867,10 @@ namespace Clipper2Lib { } ExecuteInternal(path); - if (!convex_only) - { - CheckEdges(); - for (int i = 0; i < 4; ++i) - TidyEdges(i, edges_[i * 2], edges_[i * 2 + 1]); - } - + 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); @@ -850,26 +881,24 @@ namespace Clipper2Lib { //clean up after every loop op_container_ = std::deque(); results_.clear(); - for (OutPt2List edge : edges_) edge.clear(); + for (OutPt2List &edge : edges_) edge.clear(); start_locs_.clear(); } return result; } //------------------------------------------------------------------------------ - // RectClipLines + // RectClipLines64 //------------------------------------------------------------------------------ - Paths64 RectClipLines::Execute(const Paths64& paths) + Paths64 RectClipLines64::Execute(const Paths64& paths) { Paths64 result; if (rect_.IsEmpty()) return result; for (const auto& path : paths) { - if (path.size() < 2) continue; Rect64 pathrec = GetBounds(path); - if (!rect_.Intersects(pathrec)) continue; ExecuteInternal(path); @@ -888,7 +917,7 @@ namespace Clipper2Lib { return result; } - void RectClipLines::ExecuteInternal(const Path64& path) + void RectClipLines64::ExecuteInternal(const Path64& path) { if (rect_.IsEmpty() || path.size() < 2) return; @@ -958,7 +987,7 @@ namespace Clipper2Lib { /////////////////////////////////////////////////// } - Path64 RectClipLines::GetPath(OutPt2*& op) + Path64 RectClipLines64::GetPath(OutPt2*& op) { Path64 result; if (!op || op == op->next) return result; diff --git a/CPP/Examples/Inflate/Inflate.cpp b/CPP/Examples/Inflate/Inflate.cpp index 32e4f21e..b7a7dcc7 100644 --- a/CPP/Examples/Inflate/Inflate.cpp +++ b/CPP/Examples/Inflate/Inflate.cpp @@ -11,38 +11,44 @@ void DoRabbit(); void DoSimpleShapes(); void System(const std::string& filename); + int main(int argc, char* argv[]) { - //DoSimpleShapes(); - DoRabbit(); - std::getchar(); + DoSimpleShapes(); + DoRabbit(); + //std::getchar(); } void DoSimpleShapes() { - //open path offsets Paths64 op1, op2; - FillRule fr2 = FillRule::EvenOdd; SvgWriter svg2; - op1.push_back(MakePath({ 80,60, 20,20, 180,20, 180,80, 20,180, 180,180 })); - op2 = InflatePaths(op1, 20, JoinType::Square, EndType::Butt); + + op1.push_back(MakePath({ 80,60, 20,20, 180,20, 180,70, 25,150, 20,180, 180,180 })); + op2 = InflatePaths(op1, 15, JoinType::Miter, EndType::Square, 3); + SvgAddOpenSubject(svg2, op1, fr2, false); + SvgAddSolution(svg2, TransformPaths(op2), fr2, false); + SvgAddCaption(svg2, "Miter Joins; Square Ends", 20, 210); + + op1 = TranslatePaths(op1, 210, 0); + op2 = InflatePaths(op1, 15, JoinType::Square, EndType::Square); SvgAddOpenSubject(svg2, op1, fr2, false); - SvgAddSolution(svg2, Paths64ToPathsD(op2), fr2, false); - SvgAddCaption(svg2, "Square Joins; Butt Ends", 20, 220); + SvgAddSolution(svg2, TransformPaths(op2), fr2, false); + SvgAddCaption(svg2, "Square Joins; Square Ends", 230, 210); - op1 = TranslatePaths(op1, 250, 0); - op2 = InflatePaths(op1, 20, JoinType::Miter, EndType::Square, 3); + op1 = TranslatePaths(op1, 210, 0); + op2 = InflatePaths(op1, 15, JoinType::Bevel, EndType::Butt, 3); SvgAddOpenSubject(svg2, op1, fr2, false); - SvgAddSolution(svg2, Paths64ToPathsD(op2), fr2, false); - SvgAddCaption(svg2, "Miter Joins; Square Ends", 300, 220); + SvgAddSolution(svg2, TransformPaths(op2), fr2, false); + SvgAddCaption(svg2, "Bevel Joins; Butt Ends", 440, 210); - op1 = TranslatePaths(op1, 250, 0); - op2 = InflatePaths(op1, 20, JoinType::Round, EndType::Round); + op1 = TranslatePaths(op1, 210, 0); + op2 = InflatePaths(op1, 15, JoinType::Round, EndType::Round); SvgAddOpenSubject(svg2, op1, fr2, false); - SvgAddSolution(svg2, Paths64ToPathsD(op2), fr2, false); - SvgAddCaption(svg2, "Round Joins; Round Ends", 580, 220); + SvgAddSolution(svg2, TransformPaths(op2), fr2, false); + SvgAddCaption(svg2, "Round Joins; Round Ends", 650, 210); SvgSaveToFile(svg2, "open_paths.svg", 800, 600, 20); System("open_paths.svg"); @@ -68,15 +74,15 @@ void DoSimpleShapes() //different join types within the same offset operation ClipperOffset co; co.AddPaths(p, JoinType::Miter, EndType::Joined); - p = TranslatePaths(p, 120, 100); + p = TranslatePaths(p, 120, 100); pp.insert(pp.end(), p.begin(), p.end()); co.AddPaths(p, JoinType::Round, EndType::Joined); - co.Execute(20, p); + co.Execute(10, p); pp.insert(pp.end(), p.begin(), p.end()); - FillRule fr = FillRule::EvenOdd; + FillRule fr3 = FillRule::EvenOdd; SvgWriter svg; - SvgAddSolution(svg, Paths64ToPathsD(pp), fr, false); + SvgAddSolution(svg, TransformPaths(pp), fr3, false); SvgSaveToFile(svg, "solution_off.svg", 800, 600, 20); System("solution_off.svg"); } @@ -104,7 +110,7 @@ void DoRabbit() FillRule fr = FillRule::EvenOdd; SvgWriter svg; - SvgAddSolution(svg, solution, fr, false); + SvgAddSolution(svg, solution, fr, false); SvgSaveToFile(svg, "solution_off2.svg", 450, 720, 0); System("solution_off2.svg"); } diff --git a/CPP/Examples/RectClipping/RectClipping.cpp b/CPP/Examples/RectClipping/RectClipping.cpp index 1ad2baca..b39ebdcd 100644 --- a/CPP/Examples/RectClipping/RectClipping.cpp +++ b/CPP/Examples/RectClipping/RectClipping.cpp @@ -1,7 +1,6 @@ #include - #include #include #include @@ -58,7 +57,7 @@ void DoEllipses(int cnt) sub.push_back(MakeRandomEllipse(10, 10, 100, 100, width, height)); ////////////////////////////////// - sol = ExecuteRectClip(rect, sub, true); + sol = RectClip(rect, sub); ////////////////////////////////// FillRule fr = FillRule::EvenOdd; @@ -94,7 +93,7 @@ void DoRectangles(int cnt) for (int i = 0; i < cnt; ++i) sub.push_back(MakeRandomRectangle(10, 10, 100, 100, width, height)); - sol = ExecuteRectClip(rect, sub, true); + sol = RectClip(rect, sub); FillRule fr = FillRule::EvenOdd; SvgWriter svg; @@ -133,7 +132,7 @@ void DoRandomPoly(int count) sub.push_back(MakeRandomPolyD(width, height, count)); ////////////////////////////////// - sol = ExecuteRectClip(rect, sub); + sol = RectClip(rect, sub, false); ////////////////////////////////// FillRule fr = FillRule::EvenOdd; @@ -174,13 +173,9 @@ void MeasurePerformance(int min, int max, int step) { Timer t("RectClip: "); - sol = ExecuteRectClip(rect, sub); + sol = RectClip(rect, sub); } - { - Timer t("RectClip: (convex flagged)"); - sol = ExecuteRectClip(rect, sub, true); - } } SvgWriter svg; diff --git a/CPP/Examples/VariableOffset/VariableOffset.cpp b/CPP/Examples/VariableOffset/VariableOffset.cpp new file mode 100644 index 00000000..62df56e4 --- /dev/null +++ b/CPP/Examples/VariableOffset/VariableOffset.cpp @@ -0,0 +1,165 @@ +#include + +#include "clipper2/clipper.h" +#include "clipper2/clipper.core.h" +#include "../../Utils/clipper.svg.utils.h" +#include "../../Utils/CommonUtils.h" + +using namespace Clipper2Lib; + +void System(const std::string& filename) +{ +#ifdef _WIN32 + system(filename.c_str()); +#else + system(("firefox " + filename).c_str()); +#endif +} + +void test1() { + + int64_t const scale = 10; + double delta = 10 * scale; + + ClipperOffset co; + co.SetDeltaCallback([delta](const Path64& path, + 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; + }); + + 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 }; + + co.AddPaths(subject, JoinType::Miter, EndType::Round); + Paths64 solution; + co.Execute(1.0, solution); + + std::string filename = "test1.svg"; + SvgWriter svg; + SvgAddOpenSubject(svg, subject, FillRule::NonZero); + SvgAddSolution(svg, solution, FillRule::NonZero, false); + SvgSaveToFile(svg, filename, 400, 400); + System(filename); +} + +void test2() { + + int64_t const scale = 10; + double delta = 10 * scale; + + ClipperOffset co; + 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)); + }); + + 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 }; + + co.AddPaths(subject, JoinType::Miter, EndType::Round); + Paths64 solution; + co.Execute(1.0, solution); + + std::string filename = "test2.svg"; + SvgWriter svg; + SvgAddOpenSubject(svg, subject, FillRule::NonZero); + SvgAddSolution(svg, solution, FillRule::NonZero, false); + SvgSaveToFile(svg, filename, 400, 400); + System(filename); +} + +void test3() { + + double radius = 5000.0; + Paths64 subject = { Ellipse(Rect64(-radius, -radius, radius, radius), 200) }; + + ClipperOffset co; + co.AddPaths(subject, JoinType::Miter, EndType::Polygon); + + 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; + }); + + // solution + Paths64 solution; + co.Execute(1.0, solution); + + std::string filename = "test3.svg"; + SvgWriter svg; + SvgAddSubject(svg, subject, FillRule::NonZero); + SvgAddSolution(svg, solution, FillRule::NonZero, false); + SvgSaveToFile(svg, filename, 400, 400); + System(filename); +} + +void test4() { + + int64_t const scale = 100; + Paths64 solution; + Paths64 subject = { Ellipse(ScaleRect(Rect64(10, 10, 50, 50), scale)) }; + + ClipperOffset co; + co.AddPaths(subject, JoinType::Round, EndType::Round); + co.Execute( + [scale](const Path64& path, + const PathD& path_norms, size_t curr_idx, size_t prev_idx) { + //double vertex_sin_a = CrossProduct(path_norms[curr_idx], path_norms[prev_idx]); + //double vertex_cos_a = DotProduct(path_norms[curr_idx], path_norms[prev_idx]); + //double vertex_angle = std::atan2(vertex_sin_a, vertex_cos_a); + //double edge_angle = std::atan2(path_norms[curr_idx].y, path_norms[curr_idx].x); + double sin_edge = path_norms[curr_idx].y; + return Sqr(sin_edge) * 3 * scale; } + , solution); + + std::string filename = "test4.svg"; + SvgWriter svg; + SvgAddOpenSubject(svg, subject, FillRule::NonZero); + SvgAddSolution(svg, solution, FillRule::NonZero, false); + SvgSaveToFile(svg, filename, 400, 400); + System(filename); +} + +void test5() { + + Paths64 solution; + Paths64 subject = { MakePath({0,0, 20,0, 40,0, 60,0, 80,0, 100,0}) }; + + ClipperOffset co; + co.AddPaths(subject, JoinType::Round, EndType::Butt); + co.Execute( + [](const Path64& path, + const PathD& path_norms, size_t curr_idx, size_t prev_idx) { + return double(curr_idx * curr_idx + 10); } + , solution); + + SvgWriter svg; + std::string filename = "test5.svg"; + SvgAddOpenSubject(svg, subject, FillRule::NonZero); + SvgAddSolution(svg, solution, FillRule::NonZero, false); + SvgSaveToFile(svg, filename, 400, 400); + System(filename); +} + + +int main() { + + test1(); + test2(); + test3(); + test4(); + test5(); + return 0; +} \ No newline at end of file diff --git a/CPP/Tests/TestExportHeaders.cpp b/CPP/Tests/TestExportHeaders.cpp new file mode 100644 index 00000000..02ca6655 --- /dev/null +++ b/CPP/Tests/TestExportHeaders.cpp @@ -0,0 +1,220 @@ +#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++; + if (!poly_len) return false; + Path64 path; + path.reserve(poly_len); + for (size_t i = 0; i < poly_len; ++i) + { + 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(); + int64_t* v = tree; + int64_t array_len = *v++, child_count = *v++; + for (size_t i = 0; i < child_count; ++i) + if (!CreatePolyPath64FromCPolyPath(v, result)) return false; + return true; +} + +static bool CreatePolyPathDFromCPolyPath(CPolyPathD& v, PolyPathD& owner) +{ + int64_t poly_len = *v++, child_count = *v++; + if (!poly_len) return false; + PathD path; + path.reserve(poly_len); + for (size_t i = 0; i < poly_len; ++i) + { + int64_t x = *v++, y = *v++; + path.push_back(PointD(x, y)); + } + PolyPathD* new_owner = owner.AddChild(path); + for (size_t i = 0; i < child_count; ++i) + CreatePolyPathDFromCPolyPath(v, *new_owner); + return true; +} + +static bool BuildPolyTreeDFromCPolyTree(CPolyTreeD tree, PolyTreeD& result) +{ + result.Clear(); + double* v = tree; + int64_t array_len = *v++, child_count = *v++; + for (size_t i = 0; i < child_count; ++i) + 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. + // 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)); + 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 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 + // 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 + 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 + // 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 + 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); + } +} diff --git a/CPP/Tests/TestLines.cpp b/CPP/Tests/TestLines.cpp index 1122a425..def6465b 100644 --- a/CPP/Tests/TestLines.cpp +++ b/CPP/Tests/TestLines.cpp @@ -5,8 +5,7 @@ TEST(Clipper2Tests, TestMultipleLines) { std::ifstream ifs("Lines.txt"); - - + //if (!ifs.good()) return; ASSERT_TRUE(ifs); ASSERT_TRUE(ifs.good()); @@ -61,5 +60,6 @@ TEST(Clipper2Tests, TestMultipleLines) { } ++test_number; } + ifs.close(); EXPECT_GE(test_number, 17); } \ No newline at end of file diff --git a/CPP/Tests/TestOffsetOrientation.cpp b/CPP/Tests/TestOffsetOrientation.cpp index 03c1f11a..b668a097 100644 --- a/CPP/Tests/TestOffsetOrientation.cpp +++ b/CPP/Tests/TestOffsetOrientation.cpp @@ -1,21 +1,34 @@ #include #include "clipper2/clipper.offset.h" +#include "ClipFileLoad.h" -TEST(Clipper2Tests, TestOffsettingOrientation) { - Clipper2Lib::ClipperOffset co; +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::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])); +} - const Clipper2Lib::Path64 input = { - Clipper2Lib::Point64(0, 0), - Clipper2Lib::Point64(0, 5), - Clipper2Lib::Point64(5, 5), - Clipper2Lib::Point64(5, 0) - }; +TEST(Clipper2Tests, TestOffsettingOrientation2) { - co.AddPath(input, Clipper2Lib::JoinType::Round, Clipper2Lib::EndType::Polygon); - Clipper2Lib::Paths64 outputs; - co.Execute(1, outputs); + const Clipper2Lib::Paths64 subject = { + Clipper2Lib::MakePath({20, 220, 280, 220, 280, 280, 20, 280}), + Clipper2Lib::MakePath({0, 200, 0, 300, 300, 300, 300, 200}) + }; + Clipper2Lib::ClipperOffset co; + co.ReverseSolution(true); // could also assign using a parameter in ClipperOffset's constructor + 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 + // 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 + // sweep-line algorithm used, paths with larger Y coordinates will likely be listed first. + EXPECT_TRUE(Clipper2Lib::IsPositive(subject[1]) != Clipper2Lib::IsPositive(solution[0])); +} - ASSERT_EQ(outputs.size(), 1); - //when offsetting, output orientation should match input - EXPECT_TRUE(Clipper2Lib::IsPositive(input) == Clipper2Lib::IsPositive(outputs[0])); -} \ No newline at end of file diff --git a/CPP/Tests/TestOffsets.cpp b/CPP/Tests/TestOffsets.cpp index e197d153..d5805e65 100644 --- a/CPP/Tests/TestOffsets.cpp +++ b/CPP/Tests/TestOffsets.cpp @@ -1,33 +1,34 @@ #include #include "clipper2/clipper.offset.h" #include "ClipFileLoad.h" -#include - -TEST(Clipper2Tests, TestOffsets) { +//#include "clipper.svg.utils.h" +using namespace Clipper2Lib; +TEST(Clipper2Tests, TestOffsets) { std::ifstream ifs("Offsets.txt"); + if(!ifs.good()) return; for (int test_number = 1; test_number <= 2; ++test_number) { - Clipper2Lib::ClipperOffset co; + ClipperOffset co; - Clipper2Lib::Paths64 subject, subject_open, clip; - Clipper2Lib::Paths64 solution, solution_open; - Clipper2Lib::ClipType ct = Clipper2Lib::ClipType::None; - Clipper2Lib::FillRule fr = Clipper2Lib::FillRule::NonZero; + 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, Clipper2Lib::JoinType::Round, Clipper2Lib::EndType::Polygon); - Clipper2Lib::Paths64 outputs; + 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 = Clipper2Lib::Area(outputs) > 0; + const auto outer_is_positive = Area(outputs) > 0; // there should be exactly one exterior path - const auto is_positive_func = Clipper2Lib::IsPositive; + const auto is_positive_func = IsPositive; const auto is_positive_count = std::count_if( outputs.begin(), outputs.end(), is_positive_func); const auto is_negative_count = @@ -37,11 +38,13 @@ TEST(Clipper2Tests, TestOffsets) { else EXPECT_EQ(is_negative_count, 1); } + ifs.close(); } -Clipper2Lib::Point64 MidPoint(const Clipper2Lib::Point64& p1, const Clipper2Lib::Point64& p2) + +static Point64 MidPoint(const Point64& p1, const Point64& p2) { - Clipper2Lib::Point64 result; + Point64 result; result.x = (p1.x + p2.x) / 2; result.y = (p1.y + p2.y) / 2; return result; @@ -51,29 +54,29 @@ TEST(Clipper2Tests, TestOffsets2) { // see #448 & #456 double scale = 10, delta = 10 * scale, arc_tol = 0.25 * scale; - Clipper2Lib::Paths64 subject, solution; - Clipper2Lib::ClipperOffset c; - subject.push_back(Clipper2Lib::MakePath({ 50,50, 100,50, 100,150, 50,150, 0,100 })); + Paths64 subject, solution; + ClipperOffset c; + subject.push_back(MakePath({ 50,50, 100,50, 100,150, 50,150, 0,100 })); int err; - subject = Clipper2Lib::ScalePaths(subject, scale, err); + subject = ScalePaths(subject, scale, err); - c.AddPaths(subject, Clipper2Lib::JoinType::Round, Clipper2Lib::EndType::Polygon); + c.AddPaths(subject, JoinType::Round, EndType::Polygon); c.ArcTolerance(arc_tol); c.Execute(delta, solution); double min_dist = delta * 2, max_dist = 0; - for (auto subjPt : subject[0]) + for (const Point64& subjPt : subject[0]) { - Clipper2Lib::Point64 prevPt = solution[0][solution[0].size() - 1]; - for (auto pt : solution[0]) + Point64 prevPt = solution[0][solution[0].size() - 1]; + for (const Point64& pt : solution[0]) { - Clipper2Lib::Point64 mp = MidPoint(prevPt, pt); - double d = Clipper2Lib::Distance(mp, subjPt); + Point64 mp = MidPoint(prevPt, pt); + double d = Distance(mp, subjPt); if (d < delta * 2) { if (d < min_dist) min_dist = d; - if (d > max_dist) max_dist = d; + if (d> max_dist) max_dist = d; } prevPt = pt; } @@ -81,4 +84,583 @@ TEST(Clipper2Tests, TestOffsets2) { // see #448 & #456 EXPECT_GE(min_dist + 1, delta - arc_tol); // +1 for rounding errors EXPECT_LE(solution[0].size(), 21); -} \ No newline at end of file +} + +TEST(Clipper2Tests, TestOffsets3) // see #424 +{ + Paths64 subjects = {{ + {1525311078, 1352369439}, {1526632284, 1366692987}, {1519397110, 1367437476}, + {1520246456, 1380177674}, {1520613458, 1385913385}, {1517383844, 1386238444}, + {1517771817, 1392099983}, {1518233190, 1398758441}, {1518421934, 1401883197}, + {1518694564, 1406612275}, {1520267428, 1430289121}, {1520770744, 1438027612}, + {1521148232, 1443438264}, {1521441833, 1448964260}, {1521683005, 1452518932}, + {1521819320, 1454374912}, {1527943004, 1454154711}, {1527649403, 1448523858}, + {1535901696, 1447989084}, {1535524209, 1442788147}, {1538953052, 1442463089}, + {1541553521, 1442242888}, {1541459149, 1438855987}, {1538764308, 1439076188}, + {1538575565, 1436832236}, {1538764308, 1436832236}, {1536509870, 1405374956}, + {1550497874, 1404347351}, {1550214758, 1402428457}, {1543818445, 1402868859}, + {1543734559, 1402124370}, {1540672717, 1402344571}, {1540473487, 1399995761}, + {1524996506, 1400981422}, {1524807762, 1398223667}, {1530092585, 1397898609}, + {1531675935, 1397783265}, {1531392819, 1394920653}, {1529809469, 1395025510}, + {1529348096, 1388880855}, {1531099218, 1388660654}, {1530826588, 1385158410}, + {1532955197, 1384938209}, {1532661596, 1379003269}, {1532472852, 1376235028}, + {1531277476, 1376350372}, {1530050642, 1361806623}, {1599487345, 1352704983}, + {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}, + {40000, 0}, {40000, 50000}, {0, 50000}, {0, 0}} }; + Paths64 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::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, + JoinType::Round, EndType::Polygon, 2, 100); + //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 = { + MakePath({ + 524,1483, 524,2711, 610,2744, 693,2782, 773,2825, 852,2872, + 927,2924, 999,2980, 1068,3040, 1133,3103, 1195,3171, 1252,3242, + 1305,3316, 1354,3394, 1398,3473, 1437,3556, 1472,3640, 1502,3727, + 1526,3815, 1546,3904, 1560,3994, 1569,4085, 1573,4176, 1571,4267, + 1564,4358, 1552,4449, 1535,4539, 1512,4627, 1485,4714, 1452,4799, + 1414,4883, 1372,4964, 1325,5042, 1274,5117, 1218,5190, 1158,5259, + 1094,5324, 1027,5386, 956,5443, 882,5497, 805,5546, 725,5590, + 643,5630, 559,5665, 524,5677, 524,6906, 610,6939, 693,6977, + 773,7019, 852,7066, 927,7118, 999,7174, 1068,7234, 1133,7298, + 1195,7365, 1252,7436, 1305,7511, 1354,7588, 1398,7668, 1437,7750, + 1472,7835, 1502,7921, 1526,8009, 1546,8098, 1560,8188, 1569,8279, + 1573,8370, 1571,8462, 1564,8553, 1552,8643, 1535,8733, 1512,8821, + 1485,8908, 1452,8994, 1414,9077, 1372,9158, 1325,9236, 1274,9312, + 1218,9384, 1158,9453, 1094,9518, 1027,9580, 956,9638, 882,9691, + 805,9740, 725,9784, 643,9824, 559,9859, 524,9872, 524,11100, + 610,11133, 693,11171, 773,11213, 852,11261, 927,11312, 999,11368, + 1068,11428, 1133,11492, 1195,11560, 1252,11631, 1305,11705, 1354,11782, + 1398,11862, 1437,11945, 1472,12029, 1502,12115, 1526,12203, 1546,12293, + 1560,12383, 1569,12474, 1573,12565, 1571,12656, 1564,12747, 1552,12838, + 1535,12927, 1512,13016, 1485,13103, 1452,13188, 1414,13271, 1372,13352, + 1325,13431, 1274,13506, 1218,13578, 1158,13647, 1094,13713, 1027,13774, + 956,13832, 882,13885, 805,13934, 725,13979, 643,14019, 559,14053, + 524,14066, 524,15295, 610,15327, 693,15365, 773,15408, 852,15455, + 927,15507, 999,15563, 1068,15623, 1133,15687, 1195,15754, 1252,15825, + 1305,15899, 1354,15977, 1398,16057, 1437,16139, 1472,16223, 1502,16310, + 1526,16398, 1546,16487, 1560,16577, 1569,16668, 1573,16759, 1571,16850, + 1564,16942, 1552,17032, 1535,17122, 1512,17210, 1485,17297, 1452,17382, + 1414,17466, 1372,17547, 1325,17625, 1274,17700, 1218,17773, 1158,17842, + 1094,17907, 1027,17969, 956,18026, 882,18080, 805,18129, 725,18173, + 643,18213, 559,18248, 524,18260, 524,19489, 610,19522, 693,19560, + 773,19602, 852,19649, 927,19701, 999,19757, 1068,19817, 1133,19881, + 1195,19948, 1252,20019, 1305,20094, 1354,20171, 1398,20251, 1437,20333, + 1472,20418, 1502,20504, 1526,20592, 1546,20681, 1560,20771, 1569,20862, + 1573,20954, 1571,21045, 1564,21136, 1552,21226, 1535,21316, 1512,21404, + 1485,21492, 1452,21577, 1414,21660, 1372,21741, 1325,21819, 1274,21895, + 1218,21967, 1158,22036, 1094,22101, 1027,22163, 956,22221, 882,22274, + 805,22323, 725,22368, 643,22407, 559,22442, 524,22455, 524,23683, + 610,23716, 693,23754, 773,23797, 852,23844, 927,23895, 999,23951, + 1068,24011, 1133,24075, 1195,24143, 1252,24214, 1305,24288, 1354,24365, + 1398,24445, 1437,24528, 1472,24612, 1502,24698, 1526,24786, 1546,24876, + 1560,24966, 1569,25057, 1573,25148, 1571,25239, 1564,25330, 1552,25421, + 1535,25510, 1512,25599, 1485,25686, 1452,25771, 1414,25854, 1372,25935, + 1325,26014, 1274,26089, 1218,26161, 1158,26230, 1094,26296, 1027,26357, + 956,26415, 882,26468, 805,26517, 725,26562, 643,26602, 559,26636, + 524,26649, 524,27878, 610,27910, 693,27948, 773,27991, 852,28038, + 927,28090, 999,28146, 1068,28206, 1133,28270, 1195,28337, 1252,28408, + 1305,28482, 1354,28560, 1398,28640, 1437,28722, 1472,28806, 1502,28893, + 1526,28981, 1546,29070, 1560,29160, 1569,29251, 1573,29342, 1571,29434, + 1564,29525, 1552,29615, 1535,29705, 1512,29793, 1485,29880, 1452,29965, + 1414,30049, 1372,30130, 1325,30208, 1274,30283, 1218,30356, 1158,30425, + 1094,30490, 1027,30552, 956,30609, 882,30663, 805,30712, 725,30756, + 643,30796, 559,30831, 524,30843, 524,32072, 609,32105, 692,32142, + 773,32185, 851,32232, 926,32283, 998,32339, 1066,32398, 1131,32462, + 1193,32529, 1250,32600, 1303,32674, 1352,32751, 1396,32830, 1436,32912, + 1470,32996, 1483,33031, 3131,33031, 3164,32945, 3202,32862, 3244,32781, + 3291,32703, 3343,32628, 3399,32556, 3459,32487, 3523,32422, 3591,32360, + 3662,32303, 3736,32250, 3813,32201, 3893,32157, 3975,32117, 4060,32083, + 4146,32053, 4234,32028, 4323,32009, 4413,31995, 4504,31986, 4596,31982, + 4687,31984, 4778,31991, 4868,32003, 4958,32020, 5047,32043, 5134,32070, + 5219,32103, 5302,32141, 5383,32183, 5461,32230, 5537,32281, 5609,32337, + 5678,32397, 5744,32460, 5805,32528, 5863,32599, 5916,32673, 5965,32750, + 6010,32830, 6049,32912, 6084,32996, 6097,33031, 7745,33031, 7778,32945, + 7815,32862, 7858,32781, 7905,32703, 7957,32628, 8013,32556, 8073,32487, + 8137,32422, 8204,32360, 8275,32303, 8350,32250, 8427,32201, 8507,32157, + 8589,32117, 8674,32083, 8760,32053, 8848,32028, 8937,32009, 9027,31995, + 9118,31986, 9209,31982, 9301,31984, 9392,31991, 9482,32003, 9572,32020, + 9660,32043, 9747,32070, 9833,32103, 9916,32141, 9997,32183, 10075,32230, + 10151,32281, 10223,32337, 10292,32397, 10357,32460, 10419,32528, 10477,32599, + 10530,32673, 10579,32750, 10623,32830, 10663,32912, 10698,32996, 10711,33031, + 12358,33031, 12391,32945, 12429,32862, 12472,32781, 12519,32703, 12571,32628, + 12627,32556, 12687,32487, 12750,32422, 12818,32360, 12889,32303, 12963,32250, + 13041,32201, 13120,32157, 13203,32117, 13287,32083, 13374,32053, 13462,32028, + 13551,32009, 13641,31995, 13732,31986, 13823,31982, 13914,31984, 14005,31991, + 14096,32003, 14186,32020, 14274,32043, 14361,32070, 14446,32103, 14530,32141, + 14611,32183, 14689,32230, 14764,32281, 14837,32337, 14906,32397, 14971,32460, + 15033,32528, 15090,32599, 15144,32673, 15193,32750, 15237,32830, 15277,32912, + 15312,32996, 15324,33031, 16972,33031, 17005,32945, 17043,32862, 17086,32781, + 17133,32703, 17184,32628, 17240,32556, 17300,32487, 17364,32422, 17432,32360, + 17503,32303, 17577,32250, 17654,32201, 17734,32157, 17817,32117, 17901,32083, + 17987,32053, 18075,32028, 18165,32009, 18255,31995, 18346,31986, 18437,31982, + 18528,31984, 18619,31991, 18710,32003, 18799,32020, 18888,32043, 18975,32070, + 19060,32103, 19143,32141, 19224,32183, 19303,32230, 19378,32281, 19450,32337, + 19519,32397, 19585,32460, 19646,32528, 19704,32599, 19757,32673, 19806,32750, + 19851,32830, 19891,32912, 19926,32996, 19938,33031, 21586,33031, 21619,32945, + 21657,32862, 21699,32781, 21747,32703, 21798,32628, 21854,32556, 21914,32487, + 21978,32422, 22046,32360, 22117,32303, 22191,32250, 22268,32201, 22348,32157, + 22430,32117, 22515,32083, 22601,32053, 22689,32028, 22778,32009, 22869,31995, + 22959,31986, 23051,31982, 23142,31984, 23233,31991, 23324,32003, 23413,32020, + 23502,32043, 23589,32070, 23674,32103, 23757,32141, 23838,32183, 23916,32230, + 23992,32281, 24064,32337, 24133,32397, 24199,32460, 24260,32528, 24318,32599, + 24371,32673, 24420,32750, 24465,32830, 24504,32912, 24539,32996, 24552,33031, + 26200,33031, 26233,32945, 26271,32862, 26313,32781, 26360,32703, 26412,32628, + 26468,32556, 26528,32487, 26592,32422, 26659,32360, 26730,32303, 26805,32250, + 26882,32201, 26962,32157, 27044,32117, 27129,32083, 27215,32053, 27303,32028, + 27392,32009, 27482,31995, 27573,31986, 27664,31982, 27756,31984, 27847,31991, + 27937,32003, 28027,32020, 28115,32043, 28202,32070, 28288,32103, 28371,32141, + 28452,32183, 28530,32230, 28606,32281, 28678,32337, 28747,32397, 28812,32460, + 28874,32528, 28932,32599, 28985,32673, 29034,32750, 29078,32830, 29118,32912, + 29153,32996, 29166,33031, 30814,33031, 30847,32945, 30884,32862, 30927,32781, + 30974,32703, 31026,32628, 31082,32556, 31142,32487, 31206,32422, 31273,32360, + 31344,32303, 31418,32250, 31496,32201, 31576,32157, 31658,32117, 31742,32083, + 31829,32053, 31917,32028, 32006,32009, 32096,31995, 32187,31986, 32278,31982, + 32370,31984, 32461,31991, 32551,32003, 32641,32020, 32729,32043, 32816,32070, + 32902,32103, 32985,32141, 33066,32183, 33144,32230, 33219,32281, 33292,32337, + 33361,32397, 33426,32460, 33488,32528, 33545,32599, 33599,32673, 33648,32750, + 33692,32830, 33732,32912, 33767,32996, 33779,33031, 35427,33031, 35460,32946, + 35498,32863, 35540,32782, 35587,32704, 35639,32629, 35694,32557, 35754,32489, + 35818,32423, 35885,32362, 35956,32305, 36029,32252, 36106,32203, 36186,32159, + 36268,32119, 36352,32084, 36386,32072, 36386,30843, 36301,30810, 36218,30773, + 36137,30730, 36059,30683, 35983,30631, 35911,30575, 35842,30515, 35777,30451, + 35716,30384, 35658,30313, 35605,30239, 35557,30161, 35512,30081, 35473,29999, + 35438,29915, 35409,29828, 35384,29740, 35364,29651, 35350,29561, 35341,29470, + 35338,29379, 35339,29287, 35346,29196, 35358,29106, 35376,29016, 35398,28928, + 35426,28841, 35458,28755, 35496,28672, 35538,28591, 35585,28513, 35637,28438, + 35692,28365, 35752,28296, 35816,28231, 35883,28169, 35954,28112, 36028,28058, + 36105,28009, 36185,27965, 36267,27925, 36352,27890, 36386,27878, 36386,26649, + 36301,26616, 36218,26578, 36137,26536, 36059,26489, 35983,26437, 35911,26381, + 35842,26321, 35777,26257, 35716,26189, 35658,26118, 35605,26044, 35557,25967, + 35512,25887, 35473,25805, 35438,25720, 35409,25634, 35384,25546, 35364,25457, + 35350,25366, 35341,25276, 35338,25184, 35339,25093, 35346,25002, 35358,24912, + 35376,24822, 35398,24733, 35426,24646, 35458,24561, 35496,24478, 35538,24397, + 35585,24319, 35637,24243, 35692,24171, 35752,24102, 35816,24036, 35883,23975, + 35954,23917, 36028,23864, 36105,23815, 36185,23770, 36267,23731, 36352,23696, + 36386,23683, 36386,22455, 36301,22422, 36218,22384, 36137,22341, 36059,22294, + 35983,22243, 35911,22187, 35842,22127, 35777,22063, 35716,21995, 35658,21924, + 35605,21850, 35557,21773, 35512,21693, 35473,21610, 35438,21526, 35409,21440, + 35384,21352, 35364,21262, 35350,21172, 35341,21081, 35338,20990, 35339,20899, + 35346,20808, 35358,20717, 35376,20628, 35398,20539, 35426,20452, 35458,20367, + 35496,20284, 35538,20203, 35585,20124, 35637,20049, 35692,19976, 35752,19907, + 35816,19842, 35883,19780, 35954,19723, 36028,19669, 36105,19620, 36185,19576, + 36267,19536, 36352,19501, 36386,19489, 36386,18260, 36301,18227, 36218,18190, + 36137,18147, 36059,18100, 35983,18048, 35911,17992, 35842,17932, 35777,17868, + 35716,17801, 35658,17730, 35605,17655, 35557,17578, 35512,17498, 35473,17416, + 35438,17332, 35409,17245, 35384,17157, 35364,17068, 35350,16978, 35341,16887, + 35338,16796, 35339,16704, 35346,16613, 35358,16523, 35376,16433, 35398,16345, + 35426,16258, 35458,16172, 35496,16089, 35538,16008, 35585,15930, 35637,15854, + 35692,15782, 35752,15713, 35816,15648, 35883,15586, 35954,15529, 36028,15475, + 36105,15426, 36185,15382, 36267,15342, 36352,15307, 36386,15295, 36386,14066, + 36301,14033, 36218,13995, 36137,13953, 36059,13906, 35983,13854, 35911,13798, + 35842,13738, 35777,13674, 35716,13606, 35658,13535, 35605,13461, 35557,13384, + 35512,13304, 35473,13222, 35438,13137, 35409,13051, 35384,12963, 35364,12874, + 35350,12783, 35341,12693, 35338,12601, 35339,12510, 35346,12419, 35358,12328, + 35376,12239, 35398,12150, 35426,12063, 35458,11978, 35496,11895, 35538,11814, + 35585,11736, 35637,11660, 35692,11588, 35752,11519, 35816,11453, 35883,11392, + 35954,11334, 36028,11281, 36105,11232, 36185,11187, 36267,11148, 36352,11113, + 36386,11100, 36386,9872, 36301,9839, 36218,9801, 36137,9758, 36059,9711, + 35983,9660, 35911,9604, 35842,9544, 35777,9480, 35716,9412, 35658,9341, + 35605,9267, 35557,9190, 35512,9110, 35473,9027, 35438,8943, 35409,8856, + 35384,8769, 35364,8679, 35350,8589, 35341,8498, 35338,8407, 35339,8316, + 35346,8225, 35358,8134, 35376,8045, 35398,7956, 35426,7869, 35458,7784, + 35496,7700, 35538,7620, 35585,7541, 35637,7466, 35692,7393, 35752,7324, + 35816,7259, 35883,7197, 35954,7140, 36028,7086, 36105,7037, 36185,6993, + 36267,6953, 36352,6918, 36386,6906, 36386,5677, 36301,5644, 36218,5607, + 36137,5564, 36059,5517, 35983,5465, 35911,5409, 35842,5349, 35777,5285, + 35716,5218, 35658,5147, 35605,5072, 35557,4995, 35512,4915, 35473,4833, + 35438,4748, 35409,4662, 35384,4574, 35364,4485, 35350,4395, 35341,4304, + 35338,4213, 35339,4121, 35346,4030, 35358,3940, 35376,3850, 35398,3762, + 35426,3675, 35458,3589, 35496,3506, 35538,3425, 35585,3347, 35637,3271, + 35692,3199, 35752,3130, 35816,3065, 35883,3003, 35954,2945, 36028,2892, + 36105,2843, 36185,2799, 36267,2759, 36352,2724, 36386,2711, 36386,1483, + 36301,1450, 36218,1413, 36138,1370, 36060,1323, 35985,1272, 35913,1216, + 35844,1156, 35779,1093, 35718,1026, 35660,955, 35607,881, 35558,804, + 35514,725, 35475,643, 35440,559, 35427,524, 33779,524, 33747,610, + 33709,693, 33666,773, 33619,852, 33567,927, 33511,999, 33451,1068, + 33387,1133, 33320,1195, 33249,1252, 33175,1305, 33097,1354, 33017,1398, + 32935,1437, 32851,1472, 32764,1502, 32676,1526, 32587,1546, 32497,1560, + 32406,1569, 32315,1573, 32223,1571, 32132,1564, 32042,1552, 31952,1535, + 31864,1512, 31777,1485, 31691,1452, 31608,1414, 31527,1372, 31449,1325, + 31374,1274, 31301,1218, 31232,1158, 31167,1094, 31105,1027, 31048,956, + 30994,882, 30945,805, 30901,725, 30861,643, 30826,559, 30814,524, + 29166,524, 29133,610, 29095,693, 29052,773, 29005,852, 28954,927, + 28898,999, 28838,1068, 28774,1133, 28706,1195, 28635,1252, 28561,1305, + 28484,1354, 28404,1398, 28321,1437, 28237,1472, 28150,1502, 28063,1526, + 27973,1546, 27883,1560, 27792,1569, 27701,1573, 27610,1571, 27519,1564, + 27428,1552, 27338,1535, 27250,1512, 27163,1485, 27078,1452, 26994,1414, + 26914,1372, 26835,1325, 26760,1274, 26687,1218, 26618,1158, 26553,1094, + 26491,1027, 26434,956, 26380,882, 26331,805, 26287,725, 26247,643, + 26212,559, 26200,524, 24552,524, 24519,610, 24481,693, 24439,773, + 24391,852, 24340,927, 24284,999, 24224,1068, 24160,1133, 24092,1195, + 24021,1252, 23947,1305, 23870,1354, 23790,1398, 23708,1437, 23623,1472, + 23537,1502, 23449,1526, 23360,1546, 23269,1560, 23178,1569, 23087,1573, + 22996,1571, 22905,1564, 22814,1552, 22725,1535, 22636,1512, 22549,1485, + 22464,1452, 22381,1414, 22300,1372, 22221,1325, 22146,1274, 22074,1218, + 22005,1158, 21939,1094, 21878,1027, 21820,956, 21767,882, 21718,805, + 21673,725, 21633,643, 21599,559, 21586,524, 19938,524, 19905,610, + 19867,693, 19825,773, 19778,852, 19726,927, 19670,999, 19610,1068, + 19546,1133, 19478,1195, 19407,1252, 19333,1305, 19256,1354, 19176,1398, + 19094,1437, 19009,1472, 18923,1502, 18835,1526, 18746,1546, 18656,1560, + 18565,1569, 18473,1573, 18382,1571, 18291,1564, 18201,1552, 18111,1535, + 18022,1512, 17935,1485, 17850,1452, 17767,1414, 17686,1372, 17608,1325, + 17532,1274, 17460,1218, 17391,1158, 17325,1094, 17264,1027, 17206,956, + 17153,882, 17104,805, 17059,725, 17020,643, 16985,559, 16972,524, + 15324,524, 15291,610, 15254,693, 15211,773, 15164,852, 15112,927, + 15056,999, 14996,1068, 14932,1133, 14865,1195, 14794,1252, 14719,1305, + 14642,1354, 14562,1398, 14480,1437, 14395,1472, 14309,1502, 14221,1526, + 14132,1546, 14042,1560, 13951,1569, 13860,1573, 13768,1571, 13677,1564, + 13587,1552, 13497,1535, 13409,1512, 13322,1485, 13236,1452, 13153,1414, + 13072,1372, 12994,1325, 12918,1274, 12846,1218, 12777,1158, 12712,1094, + 12650,1027, 12592,956, 12539,882, 12490,805, 12446,725, 12406,643, + 12371,559, 12358,524, 10711,524, 10678,610, 10640,693, 10597,773, + 10550,852, 10498,927, 10442,999, 10382,1068, 10319,1133, 10251,1195, + 10180,1252, 10106,1305, 10028,1354, 9949,1398, 9866,1437, 9782,1472, + 9695,1502, 9607,1526, 9518,1546, 9428,1560, 9337,1569, 9246,1573, + 9155,1571, 9064,1564, 8973,1552, 8883,1535, 8795,1512, 8708,1485, + 8623,1452, 8539,1414, 8458,1372, 8380,1325, 8305,1274, 8232,1218, + 8163,1158, 8098,1094, 8036,1027, 7979,956, 7925,882, 7876,805, + 7832,725, 7792,643, 7757,559, 7745,524, 6097,524, 6064,610, + 6026,693, 5983,773, 5936,852, 5885,927, 5829,999, 5769,1068, + 5705,1133, 5637,1195, 5566,1252, 5492,1305, 5415,1354, 5335,1398, + 5252,1437, 5168,1472, 5082,1502, 4994,1526, 4904,1546, 4814,1560, + 4723,1569, 4632,1573, 4541,1571, 4450,1564, 4359,1552, 4270,1535, + 4181,1512, 4094,1485, 4009,1452, 3926,1414, 3845,1372, 3766,1325, + 3691,1274, 3619,1218, 3550,1158, 3484,1094, 3423,1027, 3365,956, + 3312,882, 3263,805, 3218,725, 3178,643, 3143,559, 3131,524, + 1483,524, 1450,609, 1413,692, 1370,773, 1323,851, 1272,926, + 1216,998, 1156,1066, 1093,1131, 1026,1193, 955,1250, 881,1303, + 804,1352, 725,1396, 643,1436, 559,1470 + }), + 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}, + {260,-99}, {267,-78}, {272,-61}, {275,-40}, {278,-18}, {276,-39}, + {272,-61}, {267,-79}, {260,-99}, {253,-116}, {245,-133}, {235,-150}, + {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 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 largestInSol; // that define the place that most exceeds the expected offset +}; + + +template +inline PointD GetClosestPointOnSegment(const PointD& offPt, + const Point& seg1, const Point& seg2) +{ + if (seg1.x == seg2.x && seg1.y == seg2.y) return PointD(seg1); + double dx = static_cast(seg2.x - seg1.x); + double dy = static_cast(seg2.y - seg1.y); + double q = ( + (offPt.x - static_cast(seg1.x)) * dx + + (offPt.y - static_cast(seg1.y)) * dy) / + (Sqr(dx) + Sqr(dy)); + q = (q < 0) ? 0 : (q > 1) ? 1 : q; + return PointD( + 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]; + 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(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(); + Point subPrev = subject[subject.size() - 1]; + for (size_t i = 0; i < subject.size(); ++i) + { + PointD closestPt = ::GetClosestPointOnSegment(solPt, subject[i], subPrev); + subPrev = subject[i]; + const double sqrDist = DistanceSqr(closestPt, solPt); + if (sqrDist < closestDistSqr) { + closestDistSqr = sqrDist; + 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 + if (closestDistSqr < smallestSqr) { + smallestSqr = closestDistSqr; + oq.smallestInSub = closestToSolPt; + oq.smallestInSol = solPt; + } + if (closestDistSqr > largestSqr) { + largestSqr = closestDistSqr; + oq.largestInSub = closestToSolPt; + oq.largestInSol = solPt; + } + } + solPrev = solPt0; + } + return oq; +} + +TEST(Clipper2Tests, TestOffsets8) // (#724) +{ + Paths64 subject = { MakePath({ + 91759700, -49711991, 83886095, -50331657, + -872415388, -50331657, -880288993, -49711991, -887968725, -47868251, + -895265482, -44845834, -901999593, -40719165, -908005244, -35589856, + -913134553, -29584205, -917261224, -22850094, -920283639, -15553337, + -922127379, -7873605, -922747045, 0, -922747045, 1434498600, + -922160557, 1442159790, -920414763, 1449642437, -917550346, 1456772156, + -913634061, 1463382794, -908757180, 1469320287, -903033355, 1474446264, + -896595982, 1478641262, -889595081, 1481807519, -882193810, 1483871245, + -876133965, 1484596521, -876145751, 1484713389, -875781839, 1485061090, + -874690056, 1485191762, -874447580, 1485237014, -874341490, 1485264094, + -874171960, 1485309394, -873612294, 1485570372, -873201878, 1485980788, + -872941042, 1486540152, -872893274, 1486720070, -872835064, 1487162210, + -872834788, 1487185500, -872769052, 1487406000, -872297948, 1487583168, + -871995958, 1487180514, -871995958, 1486914040, -871908872, 1486364208, + -871671308, 1485897962, -871301302, 1485527956, -870835066, 1485290396, + -870285226, 1485203310, -868659019, 1485203310, -868548443, 1485188472, + -868239649, 1484791011, -868239527, 1484783879, -838860950, 1484783879, + -830987345, 1484164215, -823307613, 1482320475, -816010856, 1479298059, + -809276745, 1475171390, -803271094, 1470042081, -752939437, 1419710424, + -747810128, 1413704773, -743683459, 1406970662, -740661042, 1399673904, + -738817302, 1391994173, -738197636, 1384120567, -738197636, 1244148246, + -738622462, 1237622613, -739889768, 1231207140, -802710260, 995094494, + -802599822, 995052810, -802411513, 994586048, -802820028, 993050638, + -802879992, 992592029, -802827240, 992175479, -802662144, 991759637, + -802578556, 991608039, -802511951, 991496499, -801973473, 990661435, + -801899365, 990554757, -801842657, 990478841, -801770997, 990326371, + -801946911, 989917545, -801636397, 989501855, -801546099, 989389271, + -800888669, 988625013, -800790843, 988518907, -800082405, 987801675, + -799977513, 987702547, -799221423, 987035738, -799109961, 986944060, + -798309801, 986330832, -798192297, 986247036, -797351857, 985690294, + -797228867, 985614778, -796352124, 985117160, -796224232, 985050280, + -795315342, 984614140, -795183152, 984556216, -794246418, 984183618, + -794110558, 984134924, -793150414, 983827634, -793011528, 983788398, + -792032522, 983547874, -791891266, 983518284, -790898035, 983345662, + -790755079, 983325856, -789752329, 983221956, -789608349, 983212030, + -787698545, 983146276, -787626385, 983145034, -536871008, 983145034, + -528997403, 982525368, -521317671, 980681627, -514020914, 977659211, + -507286803, 973532542, -501281152, 968403233, -496151843, 962397582, + -492025174, 955663471, -489002757, 948366714, -487159017, 940686982, + -486539351, 932813377, -486539351, 667455555, -486537885, 667377141, + -486460249, 665302309, -486448529, 665145917, -486325921, 664057737, + -486302547, 663902657, -486098961, 662826683, -486064063, 662673784, + -485780639, 661616030, -485734413, 661466168, -485372735, 660432552, + -485315439, 660286564, -484877531, 659282866, -484809485, 659141568, + -484297795, 658173402, -484219379, 658037584, -483636768, 657110363, + -483548422, 656980785, -482898150, 656099697, -482800368, 655977081, + -482086070, 655147053, -481979398, 655032087, -481205068, 654257759, + -481090104, 654151087, -480260074, 653436789, -480137460, 653339007, + -479256372, 652688735, -479126794, 652600389, -478199574, 652017779, + -478063753, 651939363, -477095589, 651427672, -476954289, 651359626, + -475950593, 650921718, -475804605, 650864422, -474770989, 650502744, + -474621127, 650456518, -473563373, 650173094, -473410475, 650138196, + -472334498, 649934610, -472179420, 649911236, -471091240, 649788626, + -470934848, 649776906, -468860016, 649699272, -468781602, 649697806, + -385876037, 649697806, -378002432, 649078140, -370322700, 647234400, + -363025943, 644211983, -356291832, 640085314, -350286181, 634956006, + -345156872, 628950354, -341030203, 622216243, -338007786, 614919486, + -336164046, 607239755, -335544380, 599366149, -335544380, 571247184, + -335426942, 571236100, -335124952, 570833446, -335124952, 569200164, + -335037864, 568650330, -334800300, 568184084, -334430294, 567814078, + -333964058, 567576517, -333414218, 567489431, -331787995, 567489431, + -331677419, 567474593, -331368625, 567077133, -331368503, 567070001, + -142068459, 567070001, -136247086, 566711605, -136220070, 566848475, + -135783414, 567098791, -135024220, 567004957, -134451560, 566929159, + -134217752, 566913755, -133983942, 566929159, -133411282, 567004957, + -132665482, 567097135, -132530294, 567091859, -132196038, 566715561, + -132195672, 566711157, -126367045, 567070001, -33554438, 567070001, + -27048611, 566647761, -20651940, 565388127, -14471751, 563312231, + -8611738, 560454902, 36793963, 534548454, 43059832, 530319881, + 48621743, 525200596, 53354240, 519306071, 57150572, 512769270, + 59925109, 505737634, 61615265, 498369779, 62182919, 490831896, + 62182919, 474237629, 62300359, 474226543, 62602349, 473823889, + 62602349, 472190590, 62689435, 471640752, 62926995, 471174516, + 63297005, 470804506, 63763241, 470566946, 64313081, 470479860, + 65939308, 470479860, 66049884, 470465022, 66358678, 470067562, + 66358800, 470060430, 134217752, 470060430, 134217752, 0, + 133598086, -7873605, 131754346, -15553337, 128731929, -22850094, + 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; + //std::cout << "Largest delta : " << largestDist << " (" << largestDist - offset << ")" << std::endl; + //std::cout << "Coords of smallest delta : " << oq.smallestInSub << " and " << oq.smallestInSol << std::endl; + //std::cout << "Coords of largest delta : " << oq.largestInSub << " and " << oq.largestInSol << std::endl; + //std::cout << std::endl; + //SvgWriter svg; + //SvgAddSubject(svg, subject, FillRule::NonZero); + //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 + // 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/Tests/TestPolygons.cpp b/CPP/Tests/TestPolygons.cpp index 1b30999c..068dd318 100644 --- a/CPP/Tests/TestPolygons.cpp +++ b/CPP/Tests/TestPolygons.cpp @@ -52,9 +52,6 @@ TEST(Clipper2Tests, TestMultiplePolygons) const int64_t measured_area = static_cast(Area(solution)); const int64_t measured_count = static_cast(solution.size() + solution_open.size()); - const int64_t count_diff = stored_count <= 0 ? 0 : std::abs(measured_count - stored_count); - const int64_t area_diff = stored_area <= 0 ? 0 : std::abs(measured_area - stored_area); - double area_diff_ratio = (area_diff == 0) ? 0 : std::fabs((double)(area_diff) / measured_area); // check the polytree variant too Clipper2Lib::PolyTree64 solution_polytree; @@ -76,33 +73,33 @@ TEST(Clipper2Tests, TestMultiplePolygons) ; // skip count else if (IsInList(test_number, { 120, 121, 130, 138, 140, 148, 163, 165, 166, 167, 168, 172, 175, 178, 180 })) - EXPECT_LE(count_diff, 5) << " in test " << test_number; + EXPECT_NEAR(measured_count, stored_count, 5) << " in test " << test_number; else if (IsInList(test_number, { 27, 181 })) - EXPECT_LE(count_diff, 2) << " in test " << test_number; + EXPECT_NEAR(measured_count, stored_count, 2) << " in test " << test_number; else if (test_number >= 120 && test_number <= 184) - EXPECT_LE(count_diff, 2) << " in test " << test_number; + EXPECT_NEAR(measured_count, stored_count, 2) << " in test " << test_number; else if (IsInList(test_number, { 23, 45, 87, 102, 111, 113, 191 })) - EXPECT_LE(count_diff, 1) << " in test " << test_number; + EXPECT_NEAR(measured_count, stored_count, 1) << " in test " << test_number; else - EXPECT_EQ(count_diff, 0) << " in test " << test_number; + EXPECT_EQ(measured_count, stored_count) << " in test " << test_number; // check polygon areas if (stored_area <= 0) ; // skip area else if (IsInList(test_number, { 19, 22, 23, 24 })) - EXPECT_LE((double)area_diff_ratio, 0.5) << " in test " << test_number; + EXPECT_NEAR(measured_area, stored_area, 0.5 * measured_area) << " in test " << test_number; else if (test_number == 193) - EXPECT_LE((double)area_diff_ratio, 0.2) << " in test " << test_number; + EXPECT_NEAR(measured_area, stored_area, 0.2 * measured_area) << " in test " << test_number; else if (test_number == 63) - EXPECT_LE((double)area_diff_ratio, 0.1) << " in test " << test_number; + EXPECT_NEAR(measured_area, stored_area, 0.1 * measured_area) << " in test " << test_number; else if (test_number == 16) - EXPECT_LE((double)area_diff_ratio, 0.075) << " in test " << test_number; + EXPECT_NEAR(measured_area, stored_area, 0.075 * measured_area) << " in test " << test_number; else if (test_number == 26) - EXPECT_LE((double)area_diff_ratio, 0.05) << " in test " << test_number; + EXPECT_NEAR(measured_area, stored_area, 0.05 * measured_area) << " in test " << test_number; else if (IsInList(test_number, { 15, 52, 53, 54, 59, 60, 64, 117, 119, 184 })) - EXPECT_LE((double)area_diff_ratio, 0.02) << " in test " << test_number; + EXPECT_NEAR(measured_area, stored_area, 0.02 * measured_area) << " in test " << test_number; else - EXPECT_LE((double)area_diff_ratio, 0.01) << " in test " << test_number; + EXPECT_NEAR(measured_area, stored_area, 0.01 * measured_area) << " in test " << test_number; EXPECT_EQ(measured_count, measured_count_polytree) << " in test " << test_number; @@ -115,5 +112,19 @@ TEST(Clipper2Tests, TestMultiplePolygons) 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}) }; -} \ No newline at end of file + std::cout << paths << std::endl; + + Clipper2Lib::Clipper64 c; + c.AddSubject(paths); + c.Execute(Clipper2Lib::ClipType::Union, Clipper2Lib::FillRule::NonZero, paths); + + EXPECT_GE(paths.size(), 1); +} diff --git a/CPP/Tests/TestPolytreeHoles.cpp b/CPP/Tests/TestPolytreeHoles.cpp new file mode 100644 index 00000000..2ad65551 --- /dev/null +++ b/CPP/Tests/TestPolytreeHoles.cpp @@ -0,0 +1,273 @@ +#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) + { + if (PointInPolygon(pt, pp.Polygon()) != PointInPolygonResult::IsOutside) + { + if (pp.IsHole()) --counter; + else ++counter; + } + } + for (const auto& child : pp) + PolyPathContainsPoint(*child, pt, counter); +} + +bool PolytreeContainsPoint(const PolyPath64& pp, const Point64 pt) +{ + int counter = 0; + for (const auto& child : pp) + PolyPathContainsPoint(*child, pt, counter); + 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; + for (const auto& child : 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) + { + int outside_subject_count = 0; + for (const auto& path : subject) + if (PointInPolygon(poi_outside, path) != PointInPolygonResult::IsOutside) + ++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) + { + int inside_subject_count = 0; + for (const auto& path : subject) + { + if (PointInPolygon(poi_inside, path) != PointInPolygonResult::IsOutside) + ++inside_subject_count; + } + EXPECT_EQ(inside_subject_count, 1); + } + + PolyTree64 solution_tree; + Paths64 solution_open; + Clipper64 c; + c.AddSubject(subject); + 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; + subject.push_back(MakePath({ 1072,501, 1072,501, 1072,539, 1072,539, 1072,539, 870,539, + 870,539, 870,539, 870,520, 894,520, 898,524, 911,524, 915,520, 915,520, 936,520, + 940,524, 953,524, 957,520, 957,520, 978,520, 983,524, 995,524, 1000,520, 1021,520, + 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; + PolyTree64 solution; + Clipper64 c; + subject.push_back(MakePath({ 50,500, 50,300, 100,300, 100,350, 150,350, + 150,250, 200,250, 200,450, 350,450, 350,200, 400,200, 400,225, 450,225, + 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 + // +- Polygon with 3 holes. + // +- Hole with 1 nested polygon. + // +-Polygon + // +- Hole + // +- Hole + EXPECT_TRUE(solution.Count() == 1 && solution[0]->Count() == 3); +} + +TEST(Clipper2Tests, TestPolytreeHoles5) +{ + Paths64 subject, clip; + subject.push_back(MakePath({ 0,30, 400,30, 400,100, 0,100 })); + 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; + subject.push_back(MakePath({ 150,50, 200,50, 200,100, 150,100 })); + subject.push_back(MakePath({ 125,100, 150,100, 150,150, 125,150 })); + subject.push_back(MakePath({ 225,50, 300,50, 300,80, 225,80 })); + subject.push_back(MakePath({ 225,100, 300,100, 300,150, 275,150, 275,175, 260,175, + 260,250, 235,250, 235,300, 275,300, 275,275, 300,275, 300,350, 225,350 })); + subject.push_back(MakePath({ 300,150, 350,150, 350,175, 300,175 })); + 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, + 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); +} + + + + + diff --git a/CPP/Tests/TestPolytreeHoles1.cpp b/CPP/Tests/TestPolytreeHoles1.cpp deleted file mode 100644 index 176ef063..00000000 --- a/CPP/Tests/TestPolytreeHoles1.cpp +++ /dev/null @@ -1,31 +0,0 @@ -#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; - FillRule fr; - int64_t area, count; - - 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)); - -} diff --git a/CPP/Tests/TestPolytreeHoles2.cpp b/CPP/Tests/TestPolytreeHoles2.cpp deleted file mode 100644 index c28c72cb..00000000 --- a/CPP/Tests/TestPolytreeHoles2.cpp +++ /dev/null @@ -1,132 +0,0 @@ -#include -#include "clipper2/clipper.h" -#include "ClipFileLoad.h" - -using namespace Clipper2Lib; - - -void PolyPathContainsPoint(const PolyPath64& pp, const Point64 pt, int& counter) -{ - if (pp.Polygon().size() > 0) - { - if (PointInPolygon(pt, pp.Polygon()) != PointInPolygonResult::IsOutside) - { - if (pp.IsHole()) --counter; - else ++counter; - } - } - for (const auto& child : pp) - PolyPathContainsPoint(*child, pt, counter); -} - -bool PolytreeContainsPoint(const PolyPath64& pp, const Point64 pt) -{ - int counter = 0; - for (const auto& child : pp) - PolyPathContainsPoint(*child, pt, counter); - 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; - for (const auto& child : 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; - FillRule fr; - int64_t area, count; - - 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) - { - int outside_subject_count = 0; - for (const auto& path : subject) - if (PointInPolygon(poi_outside, path) != PointInPolygonResult::IsOutside) - ++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) - { - int inside_subject_count = 0; - for (const auto& path : subject) - { - if (PointInPolygon(poi_inside, path) != PointInPolygonResult::IsOutside) - ++inside_subject_count; - } - EXPECT_EQ(inside_subject_count, 1); - } - - PolyTree64 solution_tree; - Paths64 solution_open; - Clipper64 c; - c.AddSubject(subject); - 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)); - -} \ No newline at end of file diff --git a/CPP/Tests/TestPolytreeHoles3.cpp b/CPP/Tests/TestPolytreeHoles3.cpp deleted file mode 100644 index 561d2ce8..00000000 --- a/CPP/Tests/TestPolytreeHoles3.cpp +++ /dev/null @@ -1,25 +0,0 @@ -#include -#include "clipper2/clipper.h" - -using namespace Clipper2Lib; - -TEST(Clipper2Tests, TestPolytreeHoles3) -{ - - Paths64 subject, clip, sol; - PolyTree64 solution; - Clipper64 c; - subject.push_back(MakePath({ 1072,501, 1072,501, 1072,539, 1072,539, 1072,539, 870,539, - 870,539, 870,539, 870,520, 894,520, 898,524, 911,524, 915,520, 915,520, 936,520, - 940,524, 953,524, 957,520, 957,520, 978,520, 983,524, 995,524, 1000,520, 1021,520, - 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); -} \ No newline at end of file diff --git a/CPP/Tests/TestPolytreeUnion.cpp b/CPP/Tests/TestPolytreeUnion.cpp index ccda169d..4d53da87 100644 --- a/CPP/Tests/TestPolytreeUnion.cpp +++ b/CPP/Tests/TestPolytreeUnion.cpp @@ -20,7 +20,7 @@ TEST(Clipper2Tests, TestPolytreeUnion) { else { //because clipping ops normally return Positive solutions - clipper.ReverseSolution = true; + clipper.ReverseSolution(true); clipper.Execute(ClipType::Union, FillRule::Negative, solution, open_paths); } diff --git a/CPP/Tests/TestRandomPaths.cpp b/CPP/Tests/TestRandomPaths.cpp index f8ff1ab5..6fb0e8df 100644 --- a/CPP/Tests/TestRandomPaths.cpp +++ b/CPP/Tests/TestRandomPaths.cpp @@ -77,6 +77,7 @@ TEST(Clipper2Tests, TestRandomPaths) 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; @@ -84,11 +85,11 @@ TEST(Clipper2Tests, TestRandomPaths) clipper_polytree.AddOpenSubject(subject_open); clipper_polytree.AddClip(clip); clipper_polytree.Execute(ct, fr, solution_polytree, solution_polytree_open); - const auto solution_polytree_paths = PolyTreeToPaths64(solution_polytree); 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 24e493d8..2b56d16b 100644 --- a/CPP/Tests/TestRectClip.cpp +++ b/CPP/Tests/TestRectClip.cpp @@ -10,22 +10,22 @@ TEST(Clipper2Tests, TestRectClip) clp.push_back(rect.AsPath()); sub.push_back(MakePath({ 100,100, 700,100, 700,500, 100,500 })); - sol = ExecuteRectClip(rect, sub); + 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 = ExecuteRectClip(rect, sub); + 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 = ExecuteRectClip(rect, sub); + 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 = ExecuteRectClip(rect, sub); + sol = RectClip(rect, sub); EXPECT_TRUE(Area(sol) == Area(sub)); sub.clear(); @@ -33,26 +33,50 @@ TEST(Clipper2Tests, TestRectClip) rect = Rect64(390, 290, 410, 310); clp.push_back(rect.AsPath()); sub.push_back(MakePath({ 410,290, 500,290, 500,310, 410,310 })); - sol = ExecuteRectClip(rect, sub); + sol = RectClip(rect, sub); EXPECT_TRUE(sol.empty()); sub.clear(); sub.push_back(MakePath({ 430,290, 470,330, 390,330 })); - sol = ExecuteRectClip(rect, sub); + sol = RectClip(rect, sub); EXPECT_TRUE(sol.empty()); sub.clear(); sub.push_back(MakePath({ 450,290, 480,330, 450,330 })); - sol = ExecuteRectClip(rect, sub); + 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 })); rect = Rect64(237, 164, 322, 248); - sol = ExecuteRectClip(rect, sub); + sol = RectClip(rect, sub); 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); + Paths64 subject, clip, solution; + 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); } \ No newline at end of file diff --git a/CPP/Tests/TestSimplifyPath.cpp b/CPP/Tests/TestSimplifyPath.cpp new file mode 100644 index 00000000..61e701d4 --- /dev/null +++ b/CPP/Tests/TestSimplifyPath.cpp @@ -0,0 +1,14 @@ +#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); +} diff --git a/CPP/Tests/TestWindows.cpp b/CPP/Tests/TestWindows.cpp new file mode 100644 index 00000000..73d6d56e --- /dev/null +++ b/CPP/Tests/TestWindows.cpp @@ -0,0 +1,8 @@ +// see: https://github.com/AngusJohnson/Clipper2/issues/601 + +#ifdef WIN32 + +#include +#include "clipper2/clipper.h" + +#endif // WIN32 \ No newline at end of file diff --git a/CPP/Utils/ClipFileSave.cpp b/CPP/Utils/ClipFileSave.cpp index ad0755d8..305484c6 100644 --- a/CPP/Utils/ClipFileSave.cpp +++ b/CPP/Utils/ClipFileSave.cpp @@ -2,13 +2,13 @@ // Functions load clipping operations from text files //------------------------------------------------------------------------------ -#include "ClipFileLoad.h" #include "ClipFileSave.h" #include #include -using namespace std; -using namespace Clipper2Lib; +namespace Clipper2Lib { + + using namespace std; //------------------------------------------------------------------------------ // Boyer Moore Horspool Search @@ -247,8 +247,8 @@ static bool GetInt(string::const_iterator& s_it, } bool SaveTest(const std::string& filename, bool append, - Clipper2Lib::Paths64* subj, Clipper2Lib::Paths64* subj_open, Clipper2Lib::Paths64* clip, - int64_t area, int64_t count, Clipper2Lib::ClipType ct, Clipper2Lib::FillRule fr) + const Paths64* subj, const Paths64* subj_open, const Paths64* clip, + int64_t area, int64_t count, ClipType ct, FillRule fr) { string line; bool found = false; @@ -258,7 +258,7 @@ bool SaveTest(const std::string& filename, bool append, { ifstream file; file.open(filename, std::ios::binary); - if (!file) return false; + if (!file || !file.good()) return false; BMH_Search bmh = BMH_Search(file, "CAPTION:"); while (bmh.FindNext()) ; if (bmh.LastFound()) @@ -322,3 +322,5 @@ bool SaveTest(const std::string& filename, bool append, source.close(); return true; } + +} //end namespace diff --git a/CPP/Utils/ClipFileSave.h b/CPP/Utils/ClipFileSave.h index 9aa253ae..2624b56c 100644 --- a/CPP/Utils/ClipFileSave.h +++ b/CPP/Utils/ClipFileSave.h @@ -5,10 +5,14 @@ #ifndef CLIPPER_TEST_SAVE_H #define CLIPPER_TEST_SAVE_H -#include "clipper2/clipper.h" +#include "ClipFileLoad.h" -bool SaveTest(const std::string& filename, bool append, - Clipper2Lib::Paths64* subj, Clipper2Lib::Paths64* subj_open, Clipper2Lib::Paths64* clip, - int64_t area, int64_t count, Clipper2Lib::ClipType ct, Clipper2Lib::FillRule fr); +namespace Clipper2Lib { + + bool SaveTest(const std::string& filename, bool append, + const Paths64* subj, const Paths64* subj_open, const Paths64* clip, + int64_t area, int64_t count, ClipType ct, FillRule fr); + +} //end namespace #endif //CLIPPER_TEST_SAVE_H diff --git a/CPP/Utils/CommonUtils.h b/CPP/Utils/CommonUtils.h new file mode 100644 index 00000000..c2e2d05c --- /dev/null +++ b/CPP/Utils/CommonUtils.h @@ -0,0 +1,26 @@ +#include +#include "clipper2/clipper.h" +#ifndef __COMMONUTILS_H__ +#define __COMMONUTILS_H__ + +Clipper2Lib::Path64 MakeRandomPoly(int width, int height, unsigned vertCnt) +{ + 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)); + return result; +} + +Clipper2Lib::PathD MakeRandomPolyD(int width, int height, unsigned vertCnt) +{ + 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)); + return result; +} + +#endif \ No newline at end of file diff --git a/CPP/Utils/Timer.h b/CPP/Utils/Timer.h index ee9792ac..c8994c29 100644 --- a/CPP/Utils/Timer.h +++ b/CPP/Utils/Timer.h @@ -5,31 +5,25 @@ #include #include #include +#include /* Timer Usage: The Timer object will start immediately following its construction -(unless "start_paused" is true). It will stop either when its -destructor is called on leaving scope, or when pause() is called. -The timer's pause() and resume() can be called an number of times and -the total interval while unpaused will be reported in standard output. - -The Timer's constructor takes two optional string parameters: - caption : test sent to standard output just as timing commences. - time_text : text to be output to the left of the recorded time interval. +(unless "start_paused" is true). The timer's pause() and resume() can +be called an number of times and the total interval while unpaused +will be returned when elapsed() is called. Example: #include "timer.h" - #include "windows.h" //for Sleep() :) void main() { - { - Timer timer("Starting timer now.", "This operation took "); - Sleep(1000); - } + Timer timer; + Sleep(1000); + std::cout << timer.elapsed_str(); } */ @@ -38,31 +32,23 @@ struct Timer { private: std::streamsize old_precision = std::cout.precision(0); std::ios_base::fmtflags old_flags = std::cout.flags(); - std::chrono::high_resolution_clock::time_point time_started_ = - std::chrono::high_resolution_clock::now(); + std::chrono::high_resolution_clock::time_point time_started_; std::chrono::high_resolution_clock::duration duration_ = {}; bool paused_ = false; - std::string time_text_ = ""; public: - Timer(bool start_paused = false): paused_(start_paused) {} - - explicit Timer(const char time_text[], bool start_paused) : - paused_(start_paused), time_text_(time_text) {} - - explicit Timer(const char caption[], const char time_text[] = "", - bool start_paused = false) : - paused_(start_paused), time_text_(time_text) + Timer(bool start_paused = false): paused_(start_paused) { - std::cout << caption << std::endl; + if (!paused_) time_started_ = + std::chrono::high_resolution_clock::now(); } - explicit Timer(const std::string& caption, const std::string& time_text = "", - bool start_paused = false) : - paused_(start_paused), time_text_(time_text) + void restart() { - if (caption != "") std::cout << caption << std::endl; + paused_ = false; + duration_ = {}; + time_started_ = std::chrono::high_resolution_clock::now(); } void resume() @@ -81,24 +67,31 @@ struct Timer { paused_ = true; } - ~Timer() + int64_t elapsed_nano() // result in nano-seconds + { + if (!paused_) + { + std::chrono::high_resolution_clock::time_point now = + std::chrono::high_resolution_clock::now(); + duration_ += (now - time_started_); + } + return std::chrono::duration_cast(duration_).count(); + } + + std::string elapsed_str() // result as string { - pause(); - int nsecs_log10 = static_cast(std::log10( - std::chrono::duration_cast(duration_).count())); - std::cout << std::fixed << - std::setprecision(static_cast(2.0 - (nsecs_log10 % 3))) << time_text_; - if (nsecs_log10 < 6) - std::cout << std::chrono::duration_cast - (duration_).count() * 1.0e-3 << " microsecs" << std::endl; + int64_t nano_secs = elapsed_nano(); + int nsecs_log10 = static_cast(std::log10(nano_secs)); + std::ostringstream os{}; + os.precision(static_cast(2.0 - (nsecs_log10 % 3))); + os << std::fixed; + if (nsecs_log10 < 6) + os << nano_secs * 1.0e-3 << " microsecs"; else if (nsecs_log10 < 9) - std::cout << std::chrono::duration_cast - (duration_).count() * 1.0e-3 << " millisecs" << std::endl; - else - std::cout << std::chrono::duration_cast - (duration_).count() * 1.0e-3 << " secs" << std::endl; - std::cout.precision(old_precision); - std::cout.flags(old_flags); + os << nano_secs * 1.0e-6 << " millisecs"; + else + os << nano_secs * 1.0e-9 << " secs"; + return os.str(); } }; diff --git a/CPP/Utils/clipper.svg.cpp b/CPP/Utils/clipper.svg.cpp index d8b11653..c0a8754d 100644 --- a/CPP/Utils/clipper.svg.cpp +++ b/CPP/Utils/clipper.svg.cpp @@ -140,7 +140,7 @@ namespace Clipper2Lib { bool SvgWriter::SaveToFile(const std::string &filename, int max_width, int max_height, int margin) { - RectD rec = MaxInvalidRectD; + RectD rec = InvalidRectD; for (const PathInfo* pi : path_infos) for (const PathD& path : pi->paths_) for (const PointD& pt : path){ @@ -248,18 +248,19 @@ namespace Clipper2Lib { } } - //draw red dots at all vertices - useful for debugging + ////draw red dots at all solution vertices - useful for debugging //for (PathInfo* pi : path_infos) - // for (PathD& path : pi->paths_) - // for (PointD& pt : path) - // DrawCircle(file, pt.x * scale + offsetX, pt.y * scale + offsetY, 1); + // if (!(pi->pen_color_ & 0x00FF00FF)) // ie any shade of green only + // for (PathD& path : pi->paths_) + // for (PointD& pt : path) + // DrawCircle(file, pt.x * scale + offsetX, pt.y * scale + offsetY, 1.6); 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 + margin) << "\" y=\"" << (ti->y+margin) << "\">" << + file << " x * scale + offsetX) << "\" y=\"" << (ti->y * scale + offsetY) << "\">" << ti->text << "\n \n\n"; } @@ -410,8 +411,9 @@ namespace Clipper2Lib { bool SvgReader::LoadFromFile(const std::string &filename) { Clear(); - std::ifstream file; - file.open(filename); + std::ifstream file(filename); + if (!file.good()) return false; + std::stringstream xml_buff; xml_buff << file.rdbuf(); file.close(); diff --git a/CPP/clipper.version.in b/CPP/clipper.version.in new file mode 100644 index 00000000..fb75c48d --- /dev/null +++ b/CPP/clipper.version.in @@ -0,0 +1,6 @@ +#ifndef CLIPPER_VERSION_H +#define CLIPPER_VERSION_H + +constexpr auto CLIPPER2_VERSION = "@PROJECT_VERSION@"; + +#endif // CLIPPER_VERSION_H diff --git a/CSharp/Clipper2Lib.Benchmark/Benchmarks.cs b/CSharp/Clipper2Lib.Benchmark/Benchmarks.cs index 84845198..ed1ade11 100644 --- a/CSharp/Clipper2Lib.Benchmark/Benchmarks.cs +++ b/CSharp/Clipper2Lib.Benchmark/Benchmarks.cs @@ -1,5 +1,4 @@ using System; -using System.Collections.Generic; using BenchmarkDotNet.Attributes; using BenchmarkDotNet.Configs; using BenchmarkDotNet.Jobs; diff --git a/CSharp/Clipper2Lib.Examples/ConsoleDemo/Main.cs b/CSharp/Clipper2Lib.Examples/ConsoleDemo/Main.cs index 3388da18..33fd63da 100644 --- a/CSharp/Clipper2Lib.Examples/ConsoleDemo/Main.cs +++ b/CSharp/Clipper2Lib.Examples/ConsoleDemo/Main.cs @@ -1,4 +1,4 @@ -/******************************************************************************* +/******************************************************************************* * Author : Angus Johnson * * Date : 1 January 2023 * * Website : http://www.angusj.com * @@ -6,7 +6,6 @@ * License : http://www.boost.org/LICENSE_1_0.txt * *******************************************************************************/ -using System.Collections.Generic; using System.Reflection; using System.IO; using System; diff --git a/CSharp/Clipper2Lib.Examples/InflateDemo/Main.cs b/CSharp/Clipper2Lib.Examples/InflateDemo/Main.cs index 3b40bcb5..bdb30fed 100644 --- a/CSharp/Clipper2Lib.Examples/InflateDemo/Main.cs +++ b/CSharp/Clipper2Lib.Examples/InflateDemo/Main.cs @@ -1,13 +1,12 @@ -/******************************************************************************* +/******************************************************************************* * Author : Angus Johnson * -* Date : 16 September 2022 * +* Date : 24 September 2023 * * Website : http://www.angusj.com * -* Copyright : Angus Johnson 2010-2022 * +* Copyright : Angus Johnson 2010-2023 * * License : http://www.boost.org/LICENSE_1_0.txt * *******************************************************************************/ using System; -using System.Collections.Generic; using System.IO; using System.Reflection; using System.Runtime.CompilerServices; @@ -23,40 +22,49 @@ public static void Main() { DoSimpleShapes(); DoRabbit(); + DoVariableOffset(); } public static void DoSimpleShapes() { - //triangle offset - with large miter - Paths64 p = new() { Clipper.MakePath(new int[] { 30, 150, 60, 350, 0, 350 }) }; - Paths64 pp = new (); - pp.AddRange(p); + SvgWriter svg = new(); + ClipperOffset co = new(); + //triangle offset - with large miter + Paths64 p0 = new() { Clipper.MakePath(new int[] { 30,150, 60,350, 0,350 }) }; + Paths64 p = new(); for (int i = 0; i < 5; ++i) { - //nb: the following '10' parameter greatly increases miter limit - p = Clipper.InflatePaths(p, 5, JoinType.Miter, EndType.Polygon, 10); - pp.AddRange(p); + //nb: the last parameter here (10) greatly increases miter limit + p0 = Clipper.InflatePaths(p0, 5, JoinType.Miter, EndType.Polygon, 10); + p.AddRange(p0); } + SvgUtils.AddSolution(svg, p, false); + p.Clear(); //rectangle offset - both squared and rounded - p.Clear(); - p.Add(Clipper.MakePath(new int[] { 100, 0, 340, 0, 340, 200, 100, 200 })); - pp.AddRange(p); //nb: using the ClipperOffest class directly here to control //different join types within the same offset operation - ClipperOffset co = new (); + p.Add(Clipper.MakePath(new int[] { 100,0, 340,0, 340,200, 100,200, 100, 0 })); + SvgUtils.AddOpenSubject(svg, p); + co.AddPaths(p, JoinType.Bevel, EndType.Joined); + + p = Clipper.TranslatePaths(p, 60, 50); + SvgUtils.AddOpenSubject(svg, p); co.AddPaths(p, JoinType.Square, EndType.Joined); - p = Clipper.TranslatePaths(p, 120, 100); - pp.AddRange(p); + p = Clipper.TranslatePaths(p, 60, 50); + SvgUtils.AddOpenSubject(svg, p); co.AddPaths(p, JoinType.Round, EndType.Joined); - co.Execute(20, p); - pp.AddRange(p); - SvgWriter svg = new (); - SvgUtils.AddSolution(svg, pp, false); - SvgUtils.SaveToFile(svg, "../../../inflate.svg", FillRule.EvenOdd, 800, 600, 20); - ClipperFileIO.OpenFileWithDefaultApp("../../../inflate.svg"); + co.Execute(10, p); + + string filename = "../../../inflate.svg"; + SvgUtils.AddSolution(svg, p, false); + SvgUtils.AddCaption(svg, "Beveled join", 100, -27); + SvgUtils.AddCaption(svg, "Squared join", 160, 23); + SvgUtils.AddCaption(svg, "Rounded join", 220, 73); + SvgUtils.SaveToFile(svg, filename, FillRule.EvenOdd, 800, 600, 20); + ClipperFileIO.OpenFileWithDefaultApp(filename); } public static void DoRabbit() @@ -70,14 +78,15 @@ public static void DoRabbit() pd = Clipper.InflatePaths(pd, -2.5, JoinType.Round, EndType.Polygon); // SimplifyPaths - is not essential but it not only // speeds up the loop but it also tidies the result - pd = Clipper.SimplifyPaths(pd, 0.2); + pd = Clipper.SimplifyPaths(pd, 0.25); solution.AddRange(pd); } + string filename = "../../../rabbit.svg"; SvgWriter svg = new (); SvgUtils.AddSolution(svg, solution, false); - SvgUtils.SaveToFile(svg, "../../../rabbit2.svg", FillRule.EvenOdd, 450, 720, 10); - ClipperFileIO.OpenFileWithDefaultApp("../../../rabbit2.svg"); + SvgUtils.SaveToFile(svg, filename, FillRule.EvenOdd, 450, 720, 10); + ClipperFileIO.OpenFileWithDefaultApp(filename); } public static PathsD LoadPathsFromResource(string resourceName) @@ -102,7 +111,25 @@ public static PathsD LoadPathsFromResource(string resourceName) } return result; } - + + public static void DoVariableOffset() + { + Paths64 p = new() { Clipper.MakePath(new int[] { 0,50, 20,50, 40,50, 60,50, 80,50, 100,50 }) }; + Paths64 solution = new(); + ClipperOffset co = new(); + co.AddPaths(p, JoinType.Square, EndType.Butt); + co.Execute( + delegate (Path64 path, PathD path_norms, int currPt, int prevPt) + { return currPt* currPt + 10; } , solution); + + string filename = "../../../variable_offset.svg"; + SvgWriter svg = new(); + SvgUtils.AddOpenSubject(svg, p); + SvgUtils.AddSolution(svg, solution, true); + SvgUtils.SaveToFile(svg, filename, FillRule.EvenOdd, 500, 500, 60); + ClipperFileIO.OpenFileWithDefaultApp(filename); + } + } //end Application } //namespace diff --git a/CSharp/Clipper2Lib.Examples/RectClip/Main.cs b/CSharp/Clipper2Lib.Examples/RectClip/Main.cs index b9e24b4f..d8b7748d 100644 --- a/CSharp/Clipper2Lib.Examples/RectClip/Main.cs +++ b/CSharp/Clipper2Lib.Examples/RectClip/Main.cs @@ -83,7 +83,7 @@ public static void DoRandomPoly(bool loadPrevious = false) } ///////////////////////////////////////////////// - sol = Clipper.ExecuteRectClip(rec, sub); + sol = Clipper.RectClip(rec, sub); ///////////////////////////////////////////////// SvgWriter svg = new (FillRule.NonZero); diff --git a/CSharp/Clipper2Lib.Tests/Tests1/Tests/TestOffset.cs b/CSharp/Clipper2Lib.Tests/Tests1/Tests/TestOffset.cs new file mode 100644 index 00000000..54522c74 --- /dev/null +++ b/CSharp/Clipper2Lib.Tests/Tests1/Tests/TestOffset.cs @@ -0,0 +1,19 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace Clipper2Lib.UnitTests +{ + + [TestClass] + public class TestOffsets + { + + [TestMethod] + public void TestOffsetEmpty() + { + Paths64 solution = new(); + + ClipperOffset offset = new ClipperOffset(); + offset.Execute(10, solution); + } + } +} diff --git a/CSharp/Clipper2Lib/Clipper.Core.cs b/CSharp/Clipper2Lib/Clipper.Core.cs index 35e04687..80018577 100644 --- a/CSharp/Clipper2Lib/Clipper.Core.cs +++ b/CSharp/Clipper2Lib/Clipper.Core.cs @@ -1,6 +1,6 @@ /******************************************************************************* * Author : Angus Johnson * -* Date : 21 February 2023 * +* Date : 1 October 2023 * * Website : http://www.angusj.com * * Copyright : Angus Johnson 2010-2023 * * Purpose : Core structures and functions for the Clipper Library * @@ -11,8 +11,6 @@ using System; using System.Collections.Generic; using System.Runtime.CompilerServices; -using System.Security.Cryptography; -using System.Threading; namespace Drecom.Clipper2Lib { @@ -33,9 +31,9 @@ public Point64(Point64 pt) public Point64(Point64 pt, double scale) { - X = (long) Math.Round(pt.X * scale); - Y = (long) Math.Round(pt.Y * scale); - Z = (long) Math.Round(pt.Z * scale); + X = (long) Math.Round(pt.X * scale, MidpointRounding.AwayFromZero); + Y = (long) Math.Round(pt.Y * scale, MidpointRounding.AwayFromZero); + Z = (long) Math.Round(pt.Z * scale, MidpointRounding.AwayFromZero); } public Point64(long x, long y, long z = 0) @@ -47,22 +45,22 @@ public Point64(long x, long y, long z = 0) public Point64(double x, double y, double z = 0.0) { - X = (long) Math.Round(x); - Y = (long) Math.Round(y); - Z = (long) Math.Round(z); + X = (long) Math.Round(x, MidpointRounding.AwayFromZero); + Y = (long) Math.Round(y, MidpointRounding.AwayFromZero); + Z = (long) Math.Round(z, MidpointRounding.AwayFromZero); } public Point64(PointD pt) { - X = (long) Math.Round(pt.x); - Y = (long) Math.Round(pt.y); + X = (long) Math.Round(pt.x, MidpointRounding.AwayFromZero); + Y = (long) Math.Round(pt.y, MidpointRounding.AwayFromZero); Z = pt.z; } public Point64(PointD pt, double scale) { - X = (long) Math.Round(pt.x * scale); - Y = (long) Math.Round(pt.y * scale); + X = (long) Math.Round(pt.x * scale, MidpointRounding.AwayFromZero); + Y = (long) Math.Round(pt.y * scale, MidpointRounding.AwayFromZero); Z = pt.z; } @@ -106,26 +104,26 @@ public Point64(long x, long y) public Point64(double x, double y) { - X = (long) Math.Round(x); - Y = (long) Math.Round(y); + X = (long) Math.Round(x, MidpointRounding.AwayFromZero); + Y = (long) Math.Round(y, MidpointRounding.AwayFromZero); } public Point64(PointD pt) { - X = (long) Math.Round(pt.x); - Y = (long) Math.Round(pt.y); + X = (long) Math.Round(pt.x, MidpointRounding.AwayFromZero); + Y = (long) Math.Round(pt.y, MidpointRounding.AwayFromZero); } public Point64(Point64 pt, double scale) { - X = (long) Math.Round(pt.X * scale); - Y = (long) Math.Round(pt.Y * scale); + X = (long) Math.Round(pt.X * scale, MidpointRounding.AwayFromZero); + Y = (long) Math.Round(pt.Y * scale, MidpointRounding.AwayFromZero); } public Point64(PointD pt, double scale) { - X = (long) Math.Round(pt.x * scale); - Y = (long) Math.Round(pt.y * scale); + X = (long) Math.Round(pt.x * scale, MidpointRounding.AwayFromZero); + Y = (long) Math.Round(pt.y * scale, MidpointRounding.AwayFromZero); } public static bool operator ==(Point64 lhs, Point64 rhs) @@ -147,20 +145,24 @@ public Point64(PointD pt, double scale) { return new Point64(lhs.X - rhs.X, lhs.Y - rhs.Y); } - public override string ToString() + public readonly override string ToString() { return $"{X},{Y} "; // nb: trailing space } #endif - public override bool Equals(object? obj) + public readonly override bool Equals(object? obj) { if (obj != null && obj is Point64 p) return this == p; return false; } - public override int GetHashCode() { return 0; } + public readonly override int GetHashCode() + { + return HashCode.Combine(X, Y); //#599 + } + } public struct PointD @@ -213,9 +215,9 @@ public PointD(double x, double y, long z = 0) this.z = z; } - public override string ToString() + public string ToString(int precision = 2) { - return $"{x:F},{y:F},{z} "; + return string.Format($"{{0:F{precision}}},{{1:F{precision}}},{{2:D}}", x,y,z); } #else @@ -255,9 +257,9 @@ public PointD(double x, double y) this.y = y; } - public override string ToString() + public readonly string ToString(int precision = 2) { - return $"{x:F},{y:F} "; + return string.Format($"{{0:F{precision}}},{{1:F{precision}}}", x,y); } #endif @@ -273,7 +275,7 @@ public override string ToString() !InternalClipper.IsAlmostZero(lhs.y - rhs.y); } - public override bool Equals(object? obj) + public readonly override bool Equals(object? obj) { if (obj != null && obj is PointD p) return this == p; @@ -282,7 +284,11 @@ public override bool Equals(object? obj) public void Negate() { x = -x; y = -y; } - public override int GetHashCode() { return 0; } + public readonly override int GetHashCode() + { + return HashCode.Combine(x, y); //#599 + } + } public struct Rect64 @@ -291,12 +297,9 @@ public struct Rect64 public long top; public long right; public long bottom; - private static readonly string InvalidRect = "Invalid Rect64 assignment"; public Rect64(long l, long t, long r, long b) { - if (r < l || b < t) - throw new Exception(InvalidRect); left = l; top = t; right = r; @@ -325,46 +328,49 @@ public Rect64(Rect64 rec) } public long Width - { - get => right - left; + { readonly get => right - left; set => right = left + value; } public long Height - { - get => bottom - top; + { readonly get => bottom - top; set => bottom = top + value; } - public bool IsEmpty() + public readonly bool IsEmpty() { return bottom <= top || right <= left; } - public Point64 MidPoint() + public readonly bool IsValid() + { + return left < long.MaxValue; + } + + public readonly Point64 MidPoint() { return new Point64((left + right) /2, (top + bottom)/2); } - public bool Contains(Point64 pt) + public readonly bool Contains(Point64 pt) { return pt.X > left && pt.X < right && pt.Y > top && pt.Y < bottom; } - public bool Contains(Rect64 rec) + public readonly bool Contains(Rect64 rec) { return rec.left >= left && rec.right <= right && rec.top >= top && rec.bottom <= bottom; } - public bool Intersects(Rect64 rec) + public readonly bool Intersects(Rect64 rec) { return (Math.Max(left, rec.left) <= Math.Min(right, rec.right)) && (Math.Max(top, rec.top) <= Math.Min(bottom, rec.bottom)); } - public Path64 AsPath() + public readonly Path64 AsPath() { Path64 result = new Path64(4) { @@ -384,12 +390,9 @@ public struct RectD public double top; public double right; public double bottom; - private static readonly string InvalidRect = "Invalid RectD assignment"; public RectD(double l, double t, double r, double b) { - if (r < l || b < t) - throw new Exception(InvalidRect); left = l; top = t; right = r; @@ -417,46 +420,44 @@ public RectD(bool isValid) } } public double Width - { - get => right - left; + { readonly get => right - left; set => right = left + value; } public double Height - { - get => bottom - top; + { readonly get => bottom - top; set => bottom = top + value; } - public bool IsEmpty() + public readonly bool IsEmpty() { return bottom <= top || right <= left; } - public PointD MidPoint() + public readonly PointD MidPoint() { return new PointD((left + right) / 2, (top + bottom) / 2); } - public bool Contains(PointD pt) + public readonly bool Contains(PointD pt) { return pt.x > left && pt.x < right && pt.y > top && pt.y < bottom; } - public bool Contains(RectD rec) + public readonly bool Contains(RectD rec) { return rec.left >= left && rec.right <= right && rec.top >= top && rec.bottom <= bottom; } - public bool Intersects(RectD rec) + public readonly bool Intersects(RectD rec) { return (Math.Max(left, rec.left) < Math.Min(right, rec.right)) && (Math.Max(top, rec.top) < Math.Min(bottom, rec.bottom)); } - public PathD AsPath() + public readonly PathD AsPath() { PathD result = new PathD(4) { @@ -503,11 +504,11 @@ public class PathD : List private PathD() : base() { } public PathD(int capacity = 0) : base(capacity) { } public PathD(IEnumerable path) : base(path) { } - public override string ToString() + public string ToString(int precision = 2) { string s = ""; foreach (PointD p in this) - s = s + p.ToString() + " "; + s = s + p.ToString(precision) + " "; return s; } } @@ -517,11 +518,11 @@ public class PathsD : List private PathsD() : base() { } public PathsD(int capacity = 0) : base(capacity) { } public PathsD(IEnumerable paths) : base(paths) { } - public override string ToString() + public string ToString(int precision = 2) { string s = ""; foreach (PathD p in this) - s = s + p.ToString() + "\n"; + s = s + p.ToString(precision) + "\n"; return s; } } @@ -628,52 +629,39 @@ internal static double DotProduct(PointD vec1, PointD vec2) internal static long CheckCastInt64(double val) { if ((val >= max_coord) || (val <= min_coord)) return Invalid64; - return (long)Math.Round(val); + return (long)Math.Round(val, MidpointRounding.AwayFromZero); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] - internal static bool GetIntersectPt(Point64 ln1a, + public static bool GetIntersectPoint(Point64 ln1a, Point64 ln1b, Point64 ln2a, Point64 ln2b, out Point64 ip) { double dy1 = (ln1b.Y - ln1a.Y); double dx1 = (ln1b.X - ln1a.X); double dy2 = (ln2b.Y - ln2a.Y); double dx2 = (ln2b.X - ln2a.X); - double cp = dy1 * dx2 - dy2 * dx1; - if (cp == 0.0) + double det = dy1 * dx2 - dy2 * dx1; + if (det == 0.0) { ip = new Point64(); return false; } - double qx = dx1 * ln1a.Y - dy1 * ln1a.X; - double qy = dx2 * ln2a.Y - dy2 * ln2a.X; - ip = new Point64( - CheckCastInt64((dx1 * qy - dx2 * qx) / cp), - CheckCastInt64((dy1 * qy - dy2 * qx) / cp)); - return (ip.X != Invalid64 && ip.Y != Invalid64); - } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - internal static bool GetIntersectPoint(Point64 ln1a, - Point64 ln1b, Point64 ln2a, Point64 ln2b, out PointD ip) - { - double dy1 = (ln1b.Y - ln1a.Y); - double dx1 = (ln1b.X - ln1a.X); - double dy2 = (ln2b.Y - ln2a.Y); - double dx2 = (ln2b.X - ln2a.X); - double q1 = dy1 * ln1a.X - dx1 * ln1a.Y; - double q2 = dy2 * ln2a.X - dx2 * ln2a.Y; - double cross_prod = dy1 * dx2 - dy2 * dx1; - if (cross_prod == 0.0) - { - ip = new PointD(); - return false; + double t = ((ln1a.X - ln2a.X) * dy2 - (ln1a.Y - ln2a.Y) * dx2) / det; + if (t <= 0.0) ip = ln1a; + else if (t >= 1.0) ip = ln1b; + else { + // avoid using constructor (and rounding too) as they affect performance //664 + ip.X = (long) (ln1a.X + t * dx1); + ip.Y = (long) (ln1a.Y + t * dy1); +#if USINGZ + ip.Z = 0; +#endif } - ip = new PointD( - (dx2 * q1 - dx1 * q2) / cross_prod, - (dy2 * q1 - dy1 * q2) / cross_prod); return true; } + internal static bool SegsIntersect(Point64 seg1a, Point64 seg1b, Point64 seg2a, Point64 seg2b, bool inclusive = false) { @@ -706,7 +694,10 @@ public static Point64 GetClosestPtOnSegment(Point64 offPt, (offPt.Y - seg1.Y) * dy) / ((dx*dx) + (dy*dy)); if (q < 0) q = 0; else if (q > 1) q = 1; return new Point64( - seg1.X + Math.Round(q * dx), seg1.Y + Math.Round(q* dy)); + // use MidpointRounding.ToEven in order to explicitly match the nearbyint behaviour on the C++ side + seg1.X + Math.Round(q * dx, MidpointRounding.ToEven), + seg1.Y + Math.Round(q * dy, MidpointRounding.ToEven) + ); } public static PointInPolygonResult PointInPolygon(Point64 pt, Path64 polygon) diff --git a/CSharp/Clipper2Lib/Clipper.Engine.cs b/CSharp/Clipper2Lib/Clipper.Engine.cs index 9cd575b4..c9316d1f 100644 --- a/CSharp/Clipper2Lib/Clipper.Engine.cs +++ b/CSharp/Clipper2Lib/Clipper.Engine.cs @@ -1,6 +1,6 @@ /******************************************************************************* * Author : Angus Johnson * -* Date : 3 March 2023 * +* Date : 22 November 2023 * * Website : http://www.angusj.com * * Copyright : Angus Johnson 2010-2023 * * Purpose : This is the main polygon clipping module * @@ -13,6 +13,7 @@ using System; using System.Collections; using System.Collections.Generic; +using System.Reflection.Emit; using System.Runtime.CompilerServices; namespace Drecom.Clipper2Lib @@ -87,7 +88,6 @@ public override int GetHashCode() { return vertex.GetHashCode(); } - }; // IntersectNode: a structure representing 2 intersecting edges. @@ -109,7 +109,7 @@ public IntersectNode(Point64 pt, Active edge1, Active edge2) internal struct LocMinSorter : IComparer { - public int Compare(LocalMinima locMin1, LocalMinima locMin2) + public readonly int Compare(LocalMinima locMin1, LocalMinima locMin2) { return locMin2.vertex.pt.Y.CompareTo(locMin1.vertex.pt.Y); } @@ -151,11 +151,12 @@ internal class OutRec public Path64 path = new Path64(); public bool isOpen; public List? splits = null; + public OutRec? recursiveSplit; }; internal struct HorzSegSorter : IComparer { - public int Compare(HorzSegment? hs1, HorzSegment? hs2) + public readonly int Compare(HorzSegment? hs1, HorzSegment? hs2) { if (hs1 == null || hs2 == null) return 0; if (hs1.rightOp == null) @@ -228,6 +229,134 @@ internal class Active internal JoinWith joinWith; }; + internal static class ClipperEngine + { + internal static void AddLocMin(Vertex vert, PathType polytype, bool isOpen, + List minimaList) + { + // make sure the vertex is added only once ... + if ((vert.flags & VertexFlags.LocalMin) != VertexFlags.None) return; + vert.flags |= VertexFlags.LocalMin; + + LocalMinima lm = new LocalMinima(vert, polytype, isOpen); + minimaList.Add(lm); + } + + 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; + + foreach (Path64 path in paths) + { + Vertex? v0 = null, prev_v = null, curr_v; + foreach (Point64 pt in path) + { + if (v0 == null) + { + v0 = new Vertex(pt, VertexFlags.None, null); + vertexList.Add(v0); + prev_v = v0; + } + else if (prev_v!.pt != pt) // ie skips duplicates + { + curr_v = new Vertex(pt, VertexFlags.None, prev_v); + vertexList.Add(curr_v); + prev_v.next = curr_v; + prev_v = curr_v; + } + } + if (prev_v == null || prev_v.prev == null) continue; + if (!isOpen && prev_v.pt == v0!.pt) prev_v = prev_v.prev; + prev_v.next = v0; + v0!.prev = prev_v; + if (!isOpen && prev_v.next == prev_v) continue; + + // OK, we have a valid path + bool going_up, going_up0; + if (isOpen) + { + curr_v = v0.next; + while (curr_v != v0 && curr_v!.pt.Y == v0.pt.Y) + curr_v = curr_v.next; + going_up = curr_v.pt.Y <= v0.pt.Y; + if (going_up) + { + v0.flags = VertexFlags.OpenStart; + AddLocMin(v0, polytype, true, minimaList); + } + else + v0.flags = VertexFlags.OpenStart | VertexFlags.LocalMax; + } + else // closed path + { + prev_v = v0.prev; + while (prev_v != v0 && prev_v!.pt.Y == v0.pt.Y) + prev_v = prev_v.prev; + if (prev_v == v0) + continue; // only open paths can be completely flat + going_up = prev_v.pt.Y > v0.pt.Y; + } + + going_up0 = going_up; + prev_v = v0; + curr_v = v0.next; + while (curr_v != v0) + { + if (curr_v!.pt.Y > prev_v.pt.Y && going_up) + { + prev_v.flags |= VertexFlags.LocalMax; + going_up = false; + } + else if (curr_v.pt.Y < prev_v.pt.Y && !going_up) + { + going_up = true; + AddLocMin(prev_v, polytype, isOpen, minimaList); + } + prev_v = curr_v; + curr_v = curr_v.next; + } + + if (isOpen) + { + prev_v.flags |= VertexFlags.OpenEnd; + if (going_up) + prev_v.flags |= VertexFlags.LocalMax; + else + AddLocMin(prev_v, polytype, isOpen, minimaList); + } + else if (going_up != going_up0) + { + if (going_up0) AddLocMin(prev_v, polytype, false, minimaList); + else prev_v.flags |= VertexFlags.LocalMax; + } + } + } + } + + public class ReuseableDataContainer64 + { + internal readonly List _minimaList; + internal readonly List _vertexList; + public ReuseableDataContainer64() + { + _minimaList = new List(); + _vertexList = new List(); + } + public void Clear() + { + _minimaList.Clear(); + _vertexList.Clear(); + } + + public void AddPaths(Paths64 paths, PathType pt, bool isOpen) + { + ClipperEngine.AddPathsToVertexList(paths, pt, isOpen, _minimaList, _vertexList); + } + } + public class ClipperBase { private ClipType _cliptype; @@ -379,7 +508,9 @@ private static long TopX(Active ae, long currentY) { if ((currentY == ae.top.Y) || (ae.top.X == ae.bot.X)) return ae.top.X; if (currentY == ae.bot.Y) return ae.bot.X; - return ae.bot.X + (long) Math.Round(ae.dx * (currentY - ae.bot.Y)); + + // use MidpointRounding.ToEven in order to explicitly match the nearbyint behaviour on the C++ side + return ae.bot.X + (long) Math.Round(ae.dx * (currentY - ae.bot.Y), MidpointRounding.ToEven); } [MethodImpl(MethodImplOptions.AggressiveInlining)] @@ -498,7 +629,7 @@ private static bool IsMaxima(Active ae) private struct IntersectListSort : IComparer { - public int Compare(IntersectNode a, IntersectNode b) + public readonly int Compare(IntersectNode a, IntersectNode b) { if (a.pt.Y == b.pt.Y) { @@ -596,6 +727,14 @@ private static double AreaTriangle(Point64 pt1, Point64 pt2, Point64 pt3) return outRec; } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static bool IsValidOwner(OutRec? outRec, OutRec? testOwner) + { + while ((testOwner != null) && (testOwner != outRec)) + testOwner = testOwner.owner; + return testOwner == null; + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] private static void UncoupleOutRec(Active ae) { @@ -709,110 +848,7 @@ private LocalMinima PopLocalMinima() { return _minimaList[_currentLocMin++]; } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private void AddLocMin(Vertex vert, PathType polytype, bool isOpen) - { - // make sure the vertex is added only once ... - if ((vert.flags & VertexFlags.LocalMin) != VertexFlags.None) return; - vert.flags |= VertexFlags.LocalMin; - - LocalMinima lm = new LocalMinima(vert, polytype, isOpen); - _minimaList.Add(lm); - } - - protected void AddPathsToVertexList(Paths64 paths, PathType polytype, bool isOpen) - { - int totalVertCnt = 0; - foreach (Path64 path in paths) totalVertCnt += path.Count; - _vertexList.Capacity = _vertexList.Count + totalVertCnt; - - foreach (Path64 path in paths) - { - Vertex? v0 = null, prev_v = null, curr_v; - foreach (Point64 pt in path) - { - if (v0 == null) - { - v0 = new Vertex(pt, VertexFlags.None, null); - _vertexList.Add(v0); - prev_v = v0; - } - else if (prev_v!.pt != pt) // ie skips duplicates - { - curr_v = new Vertex(pt, VertexFlags.None, prev_v); - _vertexList.Add(curr_v); - prev_v.next = curr_v; - prev_v = curr_v; - } - } - if (prev_v == null || prev_v.prev == null) continue; - if (!isOpen && prev_v.pt == v0!.pt) prev_v = prev_v.prev; - prev_v.next = v0; - v0!.prev = prev_v; - if (!isOpen && prev_v.next == prev_v) continue; - - // OK, we have a valid path - bool going_up, going_up0; - if (isOpen) - { - curr_v = v0.next; - while (curr_v != v0 && curr_v!.pt.Y == v0.pt.Y) - curr_v = curr_v.next; - going_up = curr_v.pt.Y <= v0.pt.Y; - if (going_up) - { - v0.flags = VertexFlags.OpenStart; - AddLocMin(v0, polytype, true); - } - else - v0.flags = VertexFlags.OpenStart | VertexFlags.LocalMax; - } - else // closed path - { - prev_v = v0.prev; - while (prev_v != v0 && prev_v!.pt.Y == v0.pt.Y) - prev_v = prev_v.prev; - if (prev_v == v0) - continue; // only open paths can be completely flat - going_up = prev_v.pt.Y > v0.pt.Y; - } - - going_up0 = going_up; - prev_v = v0; - curr_v = v0.next; - while (curr_v != v0) - { - if (curr_v!.pt.Y > prev_v.pt.Y && going_up) - { - prev_v.flags |= VertexFlags.LocalMax; - going_up = false; - } - else if (curr_v.pt.Y < prev_v.pt.Y && !going_up) - { - going_up = true; - AddLocMin(prev_v, polytype, isOpen); - } - prev_v = curr_v; - curr_v = curr_v.next; - } - - if (isOpen) - { - prev_v.flags |= VertexFlags.OpenEnd; - if (going_up) - prev_v.flags |= VertexFlags.LocalMax; - else - AddLocMin(prev_v, polytype, isOpen); - } - else if (going_up != going_up0) - { - if (going_up0) AddLocMin(prev_v, polytype, false); - else prev_v.flags |= VertexFlags.LocalMax; - } - } - } - + [MethodImpl(MethodImplOptions.AggressiveInlining)] public void AddSubject(Path64 path) { @@ -843,7 +879,22 @@ protected void AddPaths(Paths64 paths, PathType polytype, bool isOpen = false) { if (isOpen) _hasOpenPaths = true; _isSortedMinimaList = false; - AddPathsToVertexList(paths, polytype, isOpen); + ClipperEngine.AddPathsToVertexList(paths, polytype, isOpen, _minimaList, _vertexList); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + protected void AddReuseableData(ReuseableDataContainer64 reuseableData) + { + if (reuseableData._minimaList.Count == 0) return; + // nb: reuseableData will continue to own the vertices, so it's important + // that the reuseableData object isn't destroyed before the Clipper object + // that's using the data. + _isSortedMinimaList = false; + foreach (LocalMinima lm in reuseableData._minimaList) + { + _minimaList.Add(new LocalMinima(lm.vertex, lm.polytype, lm.isOpen)); + if (lm.isOpen) _hasOpenPaths = true; + } } [MethodImpl(MethodImplOptions.AggressiveInlining)] @@ -1476,11 +1527,15 @@ private void UpdateEdgeIntoAEL(Active ae) if (IsJoined(ae)) Split(ae, ae.bot); - if (IsHorizontal(ae)) return; + if (IsHorizontal(ae)) + { + if (!IsOpen(ae)) TrimHorz(ae, PreserveCollinear); + return; + } InsertScanline(ae.top.Y); CheckJoinLeft(ae, ae.bot); - CheckJoinRight(ae, ae.bot); + CheckJoinRight(ae, ae.bot, true); // (#500) } [MethodImpl(MethodImplOptions.AggressiveInlining)] @@ -1839,9 +1894,9 @@ private void DisposeIntersectNodes() [MethodImpl(MethodImplOptions.AggressiveInlining)] private void AddNewIntersectNode(Active ae1, Active ae2, long topY) { - if (!InternalClipper.GetIntersectPt( + if (!InternalClipper.GetIntersectPoint( ae1.bot, ae1.top, ae2.bot, ae2.top, out Point64 ip)) - ip = new Point64(ae1.curX, topY); + ip = new Point64(ae1.curX, topY); if (ip.Y > _currentBotY || ip.Y < topY) { @@ -2027,13 +2082,6 @@ private static bool ResetHorzDirection(Active horz, Vertex? vertexMax, return false; // right to left } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static bool HorzIsSpike(Active horz) - { - Point64 nextPt = NextVertex(horz).pt; - return (horz.bot.X < horz.top.X) != (horz.top.X < nextPt.X); - } - [MethodImpl(MethodImplOptions.AggressiveInlining)] private static void TrimHorz(Active horzEdge, bool preserveCollinear) { @@ -2096,12 +2144,6 @@ private void DoHorizontal(Active horz) GetCurrYMaximaVertex_Open(horz) : GetCurrYMaximaVertex(horz); - // remove 180 deg.spikes and also simplify - // consecutive horizontals when PreserveCollinear = true - if (vertex_max != null && - !horzIsOpen && vertex_max != horz.vertexTop) - TrimHorz(horz, PreserveCollinear); - bool isLeftToRight = ResetHorzDirection(horz, vertex_max, out long leftX, out long rightX); @@ -2114,7 +2156,6 @@ private void DoHorizontal(Active horz) #endif AddToHorzSegList(op); } - OutRec? currOutrec = horz.outrec!; for (; ; ) { @@ -2178,6 +2219,7 @@ private void DoHorizontal(Active horz) { IntersectEdges(horz, ae, pt); SwapPositionsInAEL(horz, ae); + CheckJoinLeft(ae, pt); horz.curX = ae.curX; ae = horz.nextInAEL; } @@ -2185,15 +2227,13 @@ private void DoHorizontal(Active horz) { IntersectEdges(ae, horz, pt); SwapPositionsInAEL(ae, horz); + CheckJoinRight(ae, pt); horz.curX = ae.curX; ae = horz.prevInAEL; } - if (IsHotEdge(horz) && (horz.outrec != currOutrec)) - { - currOutrec = horz.outrec; + if (IsHotEdge(horz)) AddToHorzSegList(GetLastOp(horz)); - } } // we've reached the end of this horizontal @@ -2219,17 +2259,20 @@ private void DoHorizontal(Active horz) //still more horizontals in bound to process ... if (IsHotEdge(horz)) AddOutPt(horz, horz.top); - UpdateEdgeIntoAEL(horz); - if (PreserveCollinear && !horzIsOpen && HorzIsSpike(horz)) - TrimHorz(horz, true); + UpdateEdgeIntoAEL(horz); isLeftToRight = ResetHorzDirection(horz, vertex_max, out leftX, out rightX); } // end for loop and end of (possible consecutive) horizontals - if (IsHotEdge(horz)) AddOutPt(horz, horz.top); + if (IsHotEdge(horz)) + { + OutPt op = AddOutPt(horz, horz.top); + AddToHorzSegList(op); + } + UpdateEdgeIntoAEL(horz); // this is the end of an intermediate horiz. } @@ -2351,9 +2394,10 @@ private void CheckJoinLeft(Active e, { Active? prev = e.prevInAEL; if (prev == null || IsOpen(e) || IsOpen(prev) || - !IsHotEdge(e) || !IsHotEdge(prev) || - pt.Y < e.top.Y + 2 || pt.Y < prev.top.Y + 2) return; - + !IsHotEdge(e) || !IsHotEdge(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) + if (checkCurrX) { if (Clipper.PerpendicDistFromLineSqrd(pt, prev.bot, prev.top) > 0.25) return; @@ -2377,9 +2421,9 @@ private void CheckJoinRight(Active e, { Active? next = e.nextInAEL; if (IsOpen(e) || !IsHotEdge(e) || IsJoined(e) || - next == null || IsOpen(next) || !IsHotEdge(next) || - pt.Y < e.top.Y + 2 || pt.Y < next.top.Y + 2) // avoids trivial joins - return; + next == null || IsOpen(next) || !IsHotEdge(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) if (checkCurrX) { @@ -2499,8 +2543,8 @@ private void ConvertHorzSegsToJoins() for (int j = i + 1; j < k; j++) { HorzSegment hs2 = _horzSegList[j]; - if (hs2.leftOp!.pt.X >= hs1.rightOp!.pt.X) break; - if (hs2.leftToRight == hs1.leftToRight || + if ((hs2.leftOp!.pt.X >= hs1.rightOp!.pt.X) || + (hs2.leftToRight == hs1.leftToRight) || (hs2.rightOp!.pt.X <= hs1.leftOp!.pt.X)) continue; long curr_y = hs1.leftOp.pt.Y; if ((hs1).leftToRight) @@ -2533,22 +2577,32 @@ private void ConvertHorzSegsToJoins() } } + [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static Rect64 GetBounds(OutPt op) + private static Path64 GetCleanPath(OutPt op) { - Rect64 result = new Rect64(op.pt.X, op.pt.Y, op.pt.X, op.pt.Y); - OutPt op2 = op.next!; + Path64 result = new Path64(); + OutPt op2 = op; + while (op2.next != op && + ((op2.pt.X == op2.next!.pt.X && op2.pt.X == op2.prev.pt.X) || + (op2.pt.Y == op2.next.pt.Y && op2.pt.Y == op2.prev.pt.Y))) op2 = op2.next; + result.Add(op2.pt); + OutPt prevOp = op2; + op2 = op2.next; while (op2 != op) { - if (op2.pt.X < result.left) result.left = op2.pt.X; - else if (op2.pt.X > result.right) result.right = op2.pt.X; - if (op2.pt.Y < result.top) result.top = op2.pt.Y; - else if (op2.pt.Y > result.bottom) result.bottom = op2.pt.Y; - op2 = op2.next!; + if ((op2.pt.X != op2.next!.pt.X || op2.pt.X != prevOp.pt.X) && + (op2.pt.Y != op2.next.pt.Y || op2.pt.Y != prevOp.pt.Y)) + { + result.Add(op2.pt); + prevOp = op2; + } + op2 = op2.next; } return result; } + private static PointInPolygonResult PointInOpPolygon(Point64 pt, OutPt op) { if (op == op.next || op.prev == op.next) @@ -2619,19 +2673,30 @@ private static bool Path1InsidePath2(OutPt op1, OutPt op2) { // we need to make some accommodation for rounding errors // so we won't jump if the first vertex is found outside + PointInPolygonResult result; int outside_cnt = 0; OutPt op = op1; do { - PointInPolygonResult result = PointInOpPolygon(op.pt, op2); + result = PointInOpPolygon(op.pt, op2); if (result == PointInPolygonResult.IsOutside) ++outside_cnt; else if (result == PointInPolygonResult.IsInside) --outside_cnt; op = op.next!; } while (op != op1 && Math.Abs(outside_cnt) < 2); if (Math.Abs(outside_cnt) > 1) return (outside_cnt < 0); // since path1's location is still equivocal, check its midpoint - Point64 mp = GetBounds(op).MidPoint(); - return PointInOpPolygon(mp, op2) == PointInPolygonResult.IsInside; + Point64 mp = GetBounds(GetCleanPath(op1)).MidPoint(); + Path64 path2 = GetCleanPath(op2); + return InternalClipper.PointInPolygon(mp, path2) != PointInPolygonResult.IsOutside; + } + + private void MoveSplits(OutRec fromOr, OutRec toOr) + { + if (fromOr.splits == null) return; + toOr.splits ??= new List(); + foreach (int i in fromOr.splits) + toOr.splits.Add(i); + fromOr.splits = null; } private void ProcessHorzJoins() @@ -2648,38 +2713,49 @@ private void ProcessHorzJoins() op1b.prev = op2b; op2b.next = op1b; - if (or1 == or2) + if (or1 == or2) // 'join' is really a split { - or2 = new OutRec - { - pts = op1b - }; + or2 = NewOutRec(); + or2.pts = op1b; FixOutRecPts(or2); + + //if or1->pts has moved to or2 then update or1->pts!! if (or1.pts!.outrec == or2) { or1.pts = j.op1; or1.pts.outrec = or1; } - if (_using_polytree) + if (_using_polytree) //#498, #520, #584, D#576, #618 { - if (Path1InsidePath2(or2.pts, or1.pts)) - SetOwner(or2, or1); - else if (Path1InsidePath2(or1.pts, or2.pts)) - SetOwner(or1, or2); - else + if (Path1InsidePath2(or1.pts, or2.pts)) + { + //swap or1's & or2's pts + (or2.pts, or1.pts) = (or1.pts, or2.pts); + FixOutRecPts(or1); + FixOutRecPts(or2); + //or2 is now inside or1 + or2.owner = or1; + } + else if (Path1InsidePath2(or2.pts, or1.pts)) or2.owner = or1; + else + or2.owner = or1.owner; + + or1.splits ??= new List(); + or1.splits.Add(or2.idx); } else or2.owner = or1; - - _outrecList.Add(or2); } else { or2.pts = null; if (_using_polytree) + { SetOwner(or2, or1); + MoveSplits(or2, or1); //#618 + } else or2.owner = or1; } @@ -2770,8 +2846,7 @@ private void DoSplitOp(OutRec outrec, OutPt splitOp) OutPt result = prevOp; InternalClipper.GetIntersectPoint( - prevOp.pt, splitOp.pt, splitOp.next.pt, nextNextOp.pt, out PointD tmp); - Point64 ip = new Point64(tmp); + prevOp.pt, splitOp.pt, splitOp.next.pt, nextNextOp.pt, out Point64 ip); #if USINGZ if (_zCallback != null) @@ -2787,11 +2862,6 @@ private void DoSplitOp(OutRec outrec, OutPt splitOp) return; } - // nb: area1 is the path's area *before* splitting, whereas area2 is - // the area of the triangle containing splitOp & splitOp.next. - // So the only way for these areas to have the same sign is if - // the split triangle is larger than the path containing prevOp or - // if there's more than one self=intersection. double area2 = AreaTriangle(ip, splitOp.pt, splitOp.next.pt); double absArea2 = Math.Abs(area2); @@ -2804,13 +2874,16 @@ private void DoSplitOp(OutRec outrec, OutPt splitOp) } else { - OutPt newOp2 = new - OutPt(ip, outrec) { prev = prevOp, next = nextNextOp }; - + OutPt newOp2 = new OutPt(ip, outrec) { prev = prevOp, next = nextNextOp }; nextNextOp.prev = newOp2; prevOp.next = newOp2; } + // nb: area1 is the path's area *before* splitting, whereas area2 is + // the area of the triangle containing splitOp & splitOp.next. + // So the only way for these areas to have the same sign is if + // the split triangle is larger than the path containing prevOp or + // if there's more than one self=intersection. if (absArea2 > 1 && (absArea2 > absArea1 || ((area2 > 0) == (area1 > 0)))) @@ -2820,16 +2893,24 @@ private void DoSplitOp(OutRec outrec, OutPt splitOp) splitOp.outrec = newOutRec; splitOp.next.outrec = newOutRec; - if (_using_polytree) - { - outrec.splits ??= new List(); - outrec.splits.Add(newOutRec.idx); - } - OutPt newOp = new OutPt(ip, newOutRec) { prev = splitOp.next, next = splitOp }; newOutRec.pts = newOp; splitOp.prev = newOp; splitOp.next.next = newOp; + + if (_using_polytree) + { + if (Path1InsidePath2(prevOp, newOp)) + { + newOutRec.splits ??= new List(); + newOutRec.splits.Add(outrec.idx); + } + else + { + outrec.splits ??= new List(); + outrec.splits.Add(newOutRec.idx); + } + } } //else { splitOp = null; splitOp.next = null; } } @@ -2954,6 +3035,25 @@ private bool CheckBounds(OutRec outrec) return true; } + private bool CheckSplitOwner(OutRec outrec, List? splits) + { + foreach (int i in 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 (IsValidOwner(outrec, split) && + CheckBounds(split) && + split.bounds.Contains(outrec.bounds) && + Path1InsidePath2(outrec.pts!, split.pts!)) + { + outrec.owner = split; //found in split + return true; + } + } + return false; + } private void RecursiveCheckOwners(OutRec outrec, PolyPathBase polypath) { // pre-condition: outrec will have valid bounds @@ -2961,49 +3061,23 @@ private void RecursiveCheckOwners(OutRec outrec, PolyPathBase polypath) if (outrec.polypath != null || outrec.bounds.IsEmpty()) return; - while (outrec.owner != null && - (outrec.owner.pts == null || !CheckBounds(outrec.owner))) - outrec.owner = outrec.owner.owner; - - if (outrec.owner != null && outrec.owner.polypath == null) - RecursiveCheckOwners(outrec.owner, polypath); - while (outrec.owner != null) - if (outrec.owner.bounds.Contains(outrec.bounds) && - Path1InsidePath2(outrec.pts!, outrec.owner.pts!)) - break; // found - owner contain outrec! - else - outrec.owner = outrec.owner.owner; + { + if (outrec.owner.splits != null && + CheckSplitOwner(outrec, outrec.owner.splits)) break; + else if (outrec.owner.pts != null && CheckBounds(outrec.owner) && + Path1InsidePath2(outrec.pts!, outrec.owner.pts!)) break; + outrec.owner = outrec.owner.owner; + } if (outrec.owner != null) - outrec.polypath = outrec.owner.polypath!.AddChild(outrec.path); - else - outrec.polypath = polypath.AddChild(outrec.path); - } - - private void DeepCheckOwners(OutRec outrec, PolyPathBase polypath) - { - RecursiveCheckOwners(outrec, polypath); - - while (outrec.owner != null && outrec.owner.splits != null) { - OutRec? split = null; - foreach (int i in outrec.owner.splits) - { - split = GetRealOutRec(_outrecList[i]); - if (split != null && split != outrec && - split != outrec.owner && CheckBounds(split) && - split.bounds.Contains(outrec.bounds) && - Path1InsidePath2(outrec.pts!, split.pts!)) - { - RecursiveCheckOwners(split, polypath); - outrec.owner = split; //found in split - break; // inner 'for' loop - } - else split = null; - } - if (split == null) break; + if (outrec.owner.polypath == null) + RecursiveCheckOwners(outrec.owner, polypath); + outrec.polypath = outrec.owner.polypath!.AddChild(outrec.path); } + else + outrec.polypath = polypath.AddChild(outrec.path); } protected void BuildTree(PolyPathBase polytree, Paths64 solutionOpen) @@ -3030,7 +3104,7 @@ protected void BuildTree(PolyPathBase polytree, Paths64 solutionOpen) continue; } if (CheckBounds(outrec)) - DeepCheckOwners(outrec, polytree); + RecursiveCheckOwners(outrec, polytree); } } @@ -3065,6 +3139,12 @@ public class Clipper64 : ClipperBase base.AddPath(path, polytype, isOpen); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public new void AddReuseableData(ReuseableDataContainer64 reuseableData) + { + base.AddReuseableData(reuseableData); + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] internal new void AddPaths(Paths64 paths, PathType polytype, bool isOpen = false) { @@ -3186,7 +3266,7 @@ private void ZCB(Point64 bot1, Point64 top1, // temporarily convert integers to their initial float values // this will slow clipping marginally but will make it much easier // to understand the coordinates passed to the callback function - PointD tmp = new PointD(intersectPt); + PointD tmp = Clipper.ScalePointD(intersectPt, _invScale); //do the callback ZCallback?.Invoke( Clipper.ScalePointD(bot1, _invScale), @@ -3289,11 +3369,12 @@ public bool Execute(ClipType clipType, FillRule fillRule, PathsD solutionClosed) public bool Execute(ClipType clipType, FillRule fillRule, PolyTreeD polytree, PathsD openPaths) { polytree.Clear(); + openPaths.Clear(); + _using_polytree = true; (polytree as PolyPathD).Scale = _scale; #if USINGZ CheckZCallback(); #endif - openPaths.Clear(); Paths64 oPaths = new Paths64(); bool success = true; try @@ -3392,23 +3473,52 @@ private bool GetIsHole() } public int Count => _childs.Count; - internal abstract PolyPathBase AddChild(Path64 p); + public abstract PolyPathBase AddChild(Path64 p); [MethodImpl(MethodImplOptions.AggressiveInlining)] public void Clear() { _childs.Clear(); } - } // PolyPathBase class - public class PolyPath64 : PolyPathBase + internal string ToStringInternal(int idx, int level) + { + string result = "", padding = "", plural = "s"; + if (_childs.Count == 1) plural = ""; + padding = padding.PadLeft(level * 2); + if ((level & 1) == 0) + result += string.Format("{0}+- hole ({1}) contains {2} nested polygon{3}.\n", padding, idx, _childs.Count, plural); + else + result += string.Format("{0}+- polygon ({1}) contains {2} hole{3}.\n", padding, idx, _childs.Count, plural); + + for (int i = 0; i < Count; i++) + if (_childs[i].Count > 0) + result += _childs[i].ToStringInternal(i, level +1); + return result; + } + + public override string ToString() + { + if (Level > 0) return ""; //only accept tree root + string plural = "s"; + if (_childs.Count == 1) plural = ""; + string result = string.Format("Polytree with {0} polygon{1}.\n", _childs.Count, plural); + for (int i = 0; i < Count; i++) + if (_childs[i].Count > 0) + result += _childs[i].ToStringInternal(i, 1); + return result + '\n'; + } + +} // PolyPathBase class + +public class PolyPath64 : PolyPathBase { public Path64? Polygon { get; private set; } // polytree root's polygon == null public PolyPath64(PolyPathBase? parent = null) : base(parent) {} [MethodImpl(MethodImplOptions.AggressiveInlining)] - internal override PolyPathBase AddChild(Path64 p) + public override PolyPathBase AddChild(Path64 p) { PolyPathBase newChild = new PolyPath64(this); (newChild as PolyPath64)!.Polygon = p; @@ -3446,7 +3556,6 @@ public double Area() return result; } } - public class PolyPathD : PolyPathBase { internal double Scale { get; set; } @@ -3455,7 +3564,7 @@ public class PolyPathD : PolyPathBase public PolyPathD(PolyPathBase? parent = null) : base(parent) {} [MethodImpl(MethodImplOptions.AggressiveInlining)] - internal override PolyPathBase AddChild(Path64 p) + public override PolyPathBase AddChild(Path64 p) { PolyPathBase newChild = new PolyPathD(this); (newChild as PolyPathD)!.Scale = Scale; @@ -3464,6 +3573,16 @@ internal override PolyPathBase AddChild(Path64 p) return newChild; } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public PolyPathBase AddChild(PathD p) + { + PolyPathBase newChild = new PolyPathD(this); + (newChild as PolyPathD)!.Scale = Scale; + (newChild as PolyPathD)!.Polygon = p; + _childs.Add(newChild); + return newChild; + } + [IndexerName("Child")] public PolyPathD this[int index] { diff --git a/CSharp/Clipper2Lib/Clipper.Minkowski.cs b/CSharp/Clipper2Lib/Clipper.Minkowski.cs index b841492c..707fc111 100644 --- a/CSharp/Clipper2Lib/Clipper.Minkowski.cs +++ b/CSharp/Clipper2Lib/Clipper.Minkowski.cs @@ -9,7 +9,6 @@ #nullable enable using System; -using System.Collections.Generic; namespace Drecom.Clipper2Lib { diff --git a/CSharp/Clipper2Lib/Clipper.Offset.cs b/CSharp/Clipper2Lib/Clipper.Offset.cs index ec1b6b68..d3a4c10c 100644 --- a/CSharp/Clipper2Lib/Clipper.Offset.cs +++ b/CSharp/Clipper2Lib/Clipper.Offset.cs @@ -1,6 +1,6 @@ /******************************************************************************* * Author : Angus Johnson * -* Date : 22 March 2023 * +* Date : 28 November 2023 * * Website : http://www.angusj.com * * Copyright : Angus Johnson 2010-2023 * * Purpose : Path Offset (Inflate/Shrink) * @@ -15,9 +15,10 @@ namespace Drecom.Clipper2Lib { public enum JoinType { + Miter, Square, - Round, - Miter + Bevel, + Round }; public enum EndType @@ -35,29 +36,68 @@ public class ClipperOffset private class Group { internal Paths64 inPaths; - internal Path64 outPath; - internal Paths64 outPaths; + internal List boundsList; + internal List isHoleList; internal JoinType joinType; internal EndType endType; internal bool pathsReversed; + internal int lowestPathIdx; public Group(Paths64 paths, JoinType joinType, EndType endType = EndType.Polygon) { - inPaths = new Paths64(paths); this.joinType = joinType; this.endType = endType; - outPath = new Path64(); - outPaths = new Paths64(); - pathsReversed = false; + + bool isJoined = ((endType == EndType.Polygon) || (endType == EndType.Joined)); + inPaths = new Paths64(paths.Count); + 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); + + foreach (Path64 path in inPaths) + isHoleList.Add(Clipper.Area(path) < 0); + // 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]; + } + else + { + lowestPathIdx = -1; + isHoleList = new List(new bool[inPaths.Count]); + pathsReversed = false; + } } } + 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 double _group_delta; //*0.5 for open paths; *-1.0 for negative areas + private double _groupDelta; //*0.5 for open paths; *-1.0 for negative areas private double _delta; - private double _abs_group_delta; private double _mitLimSqr; private double _stepsPerRad; private double _stepSin; @@ -69,11 +109,16 @@ public Group(Paths64 paths, JoinType joinType, EndType endType = EndType.Polygon public double MiterLimit { get; set; } public bool PreserveCollinear { get; set; } public bool ReverseSolution { get; set; } + + public delegate double DeltaCallback64(Path64 path, + PathD path_norms, int currPt, int prevPt); + public ClipperOffset.DeltaCallback64? DeltaCallback { get; set; } + #if USINGZ public ClipperBase.ZCallback64? ZCallback { get; set; } #endif - public ClipperOffset(double miterLimit = 2.0, - double arcTolerance = 0.0, bool + public ClipperOffset(double miterLimit = 2.0, + double arcTolerance = 0.0, bool preserveCollinear = false, bool reverseSolution = false) { MiterLimit = miterLimit; @@ -85,7 +130,6 @@ public ClipperOffset(double miterLimit = 2.0, ZCallback = null; #endif } - public void Clear() { _groupList.Clear(); @@ -106,70 +150,88 @@ public void AddPaths(Paths64 paths, JoinType joinType, EndType endType) _groupList.Add(new Group(paths, joinType, endType)); } + private int CalcSolutionCapacity() + { + int result = 0; + foreach (Group g in _groupList) + result += (g.endType == EndType.Joined) ? g.inPaths.Count * 2 : g.inPaths.Count; + return result; + } + private void ExecuteInternal(double delta) { _solution.Clear(); if (_groupList.Count == 0) return; + _solution.Capacity = CalcSolutionCapacity(); + // make sure the offset delta is significant if (Math.Abs(delta) < 0.5) { foreach (Group group in _groupList) foreach (Path64 path in group.inPaths) _solution.Add(path); + return; } - else - { - _delta = delta; - _mitLimSqr = (MiterLimit <= 1 ? - 2.0 : 2.0 / Clipper.Sqr(MiterLimit)); - foreach (Group group in _groupList) - DoGroupOffset(group); - } + _delta = delta; + _mitLimSqr = (MiterLimit <= 1 ? + 2.0 : 2.0 / Clipper.Sqr(MiterLimit)); + + 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(); + FillRule fillRule = pathsReversed ? FillRule.Negative : FillRule.Positive; // clean up self-intersections ... - Clipper64 c = new Clipper64() - { - PreserveCollinear = PreserveCollinear, - // the solution should retain the orientation of the input - ReverseSolution = ReverseSolution != _groupList[0].pathsReversed - }; + Clipper64 c = new Clipper64(); + c.PreserveCollinear = PreserveCollinear; + // the solution should retain the orientation of the input + c.ReverseSolution = ReverseSolution != pathsReversed; #if USINGZ c.ZCallback = ZCallback; #endif c.AddSubject(_solution); - if (_groupList[0].pathsReversed) - c.Execute(ClipType.Union, FillRule.Negative, solution); - else - c.Execute(ClipType.Union, FillRule.Positive, solution); + c.Execute(ClipType.Union, fillRule, solution); } - public void Execute(double delta, PolyTree64 polytree) + public void Execute(double delta, PolyTree64 solutionTree) { - polytree.Clear(); + solutionTree.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() - { - PreserveCollinear = PreserveCollinear, - // the solution should retain the orientation of the input - ReverseSolution = ReverseSolution != _groupList[0].pathsReversed - }; + 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); - if (_groupList[0].pathsReversed) - c.Execute(ClipType.Union, FillRule.Negative, polytree); - else - c.Execute(ClipType.Union, FillRule.Positive, polytree); + c.Execute(ClipType.Union, fillRule, solutionTree); } @@ -187,28 +249,67 @@ internal static PointD GetUnitNormal(Point64 pt1, Point64 pt2) return new PointD(dy, -dx); } - private static void GetBoundsAndLowestPolyIdx(Paths64 paths, - out int index, out Rect64 rec) + public void Execute(DeltaCallback64 deltaCallback, Paths64 solution) { - rec = new Rect64(false); // ie invalid rect - long lpX = long.MinValue; - index = -1; - for (int i = 0; i < paths.Count; i++) - foreach (Point64 pt in paths[i]) + DeltaCallback = deltaCallback; + Execute(1.0, solution); + } + + internal static void GetMultiBounds(Paths64 paths, List boundsList) + { + boundsList.Capacity = paths.Count; + foreach (Path64 path in paths) + { + if (path.Count < 1) { - if (pt.Y >= rec.bottom) - { - if (pt.Y > rec.bottom || pt.X < lpX) - { - index = i; - lpX = pt.X; - rec.bottom = pt.Y; - } - } - else if (pt.Y < rec.top) rec.top = pt.Y; - if (pt.X > rec.right) rec.right = pt.X; - else if (pt.X < rec.left) rec.left = pt.X; + 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) + { + int result = -1; + Point64 botPt = new Point64(long.MaxValue, long.MinValue); + for (int i = 0; i < boundsList.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); + result = i; + } + } + return result; } [MethodImpl(MethodImplOptions.AggressiveInlining)] @@ -292,11 +393,11 @@ private static PointD IntersectPoint(PointD pt1a, PointD pt1b, PointD pt2a, Poin private Point64 GetPerpendic(Point64 pt, PointD norm) { #if USINGZ - return new Point64(pt.X + norm.x * _group_delta, - pt.Y + norm.y * _group_delta, pt.Z); + return new Point64(pt.X + norm.x * _groupDelta, + pt.Y + norm.y * _groupDelta, pt.Z); #else - return new Point64(pt.X + norm.x * _group_delta, - pt.Y + norm.y * _group_delta); + return new Point64(pt.X + norm.x * _groupDelta, + pt.Y + norm.y * _groupDelta); #endif } @@ -304,21 +405,40 @@ private Point64 GetPerpendic(Point64 pt, PointD norm) private PointD GetPerpendicD(Point64 pt, PointD norm) { #if USINGZ - return new PointD(pt.X + norm.x * _group_delta, - pt.Y + norm.y * _group_delta, pt.Z); + return new PointD(pt.X + norm.x * _groupDelta, + pt.Y + norm.y * _groupDelta, pt.Z); #else - return new PointD(pt.X + norm.x * _group_delta, - pt.Y + norm.y * _group_delta); + return new PointD(pt.X + norm.x * _groupDelta, + pt.Y + norm.y * _groupDelta); #endif } [MethodImpl(MethodImplOptions.AggressiveInlining)] - private void DoSquare(Group group, Path64 path, int j, int k) + private void DoBevel(Path64 path, int j, int k) + { + Point64 pt1, pt2; + 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); + } + 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); + } + pathOut.Add(pt1); + pathOut.Add(pt2); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void DoSquare(Path64 path, int j, int k) { PointD vec; if (j == k) { - vec = new PointD(_normals[0].y, -_normals[0].x); + vec = new PointD(_normals[j].y, -_normals[j].x); } else { @@ -326,29 +446,30 @@ private void DoSquare(Group group, Path64 path, int j, int k) new PointD(-_normals[k].y, _normals[k].x), new PointD(_normals[j].y, -_normals[j].x)); } - + + double absDelta = Math.Abs(_groupDelta); // now offset the original vertex delta units along unit vector PointD ptQ = new PointD(path[j]); - ptQ = TranslatePoint(ptQ, _abs_group_delta * vec.x, _abs_group_delta * vec.y); + ptQ = TranslatePoint(ptQ, absDelta * vec.x, absDelta * vec.y); // get perpendicular vertices - PointD pt1 = TranslatePoint(ptQ, _group_delta * vec.y, _group_delta * -vec.x); - PointD pt2 = TranslatePoint(ptQ, _group_delta * -vec.y, _group_delta * vec.x); + PointD pt1 = TranslatePoint(ptQ, _groupDelta * vec.y, _groupDelta * -vec.x); + PointD pt2 = TranslatePoint(ptQ, _groupDelta * -vec.y, _groupDelta * vec.x); // get 2 vertices along one edge offset PointD pt3 = GetPerpendicD(path[k], _normals[k]); if (j == k) { PointD pt4 = new PointD( - pt3.x + vec.x * _group_delta, - pt3.y + vec.y * _group_delta); + pt3.x + vec.x * _groupDelta, + pt3.y + vec.y * _groupDelta); PointD pt = IntersectPoint(pt1, pt2, pt3, pt4); #if USINGZ pt.z = ptQ.z; #endif //get the second intersect point through reflecion - group.outPath.Add(new Point64(ReflectPoint(pt, ptQ))); - group.outPath.Add(new Point64(pt)); + pathOut.Add(new Point64(ReflectPoint(pt, ptQ))); + pathOut.Add(new Point64(pt)); } else { @@ -357,54 +478,66 @@ private void DoSquare(Group group, Path64 path, int j, int k) #if USINGZ pt.z = ptQ.z; #endif - group.outPath.Add(new Point64(pt)); + pathOut.Add(new Point64(pt)); //get the second intersect point through reflecion - group.outPath.Add(new Point64(ReflectPoint(pt, ptQ))); + pathOut.Add(new Point64(ReflectPoint(pt, ptQ))); } } [MethodImpl(MethodImplOptions.AggressiveInlining)] private void DoMiter(Group group, Path64 path, int j, int k, double cosA) { - double q = _group_delta / (cosA + 1); + double q = _groupDelta / (cosA + 1); #if USINGZ - group.outPath.Add(new Point64( + pathOut.Add(new Point64( path[j].X + (_normals[k].x + _normals[j].x) * q, path[j].Y + (_normals[k].y + _normals[j].y) * q, path[j].Z)); #else - group.outPath.Add(new Point64( + pathOut.Add(new Point64( path[j].X + (_normals[k].x + _normals[j].x) * q, path[j].Y + (_normals[k].y + _normals[j].y) * q)); #endif } [MethodImpl(MethodImplOptions.AggressiveInlining)] - private void DoRound(Group group, Path64 path, int j, int k, double angle) + private void DoRound(Path64 path, int j, int k, double angle) { + if (DeltaCallback != null) + { + // when DeltaCallback is assigned, _groupDelta won't be constant, + // so we'll need to do the following calculations for *every* vertex. + double absDelta = Math.Abs(_groupDelta); + double arcTol = ArcTolerance > 0.01 ? + ArcTolerance : + Math.Log10(2 + absDelta) * InternalClipper.defaultArcTolerance; + double stepsPer360 = Math.PI / Math.Acos(1 - arcTol / absDelta); + _stepSin = Math.Sin((2 * Math.PI) / stepsPer360); + _stepCos = Math.Cos((2 * Math.PI) / stepsPer360); + if (_groupDelta < 0.0) _stepSin = -_stepSin; + _stepsPerRad = stepsPer360 / (2 * Math.PI); + } + Point64 pt = path[j]; - PointD offsetVec = new PointD(_normals[k].x * _group_delta, _normals[k].y * _group_delta); + PointD offsetVec = new PointD(_normals[k].x * _groupDelta, _normals[k].y * _groupDelta); if (j == k) offsetVec.Negate(); #if USINGZ - group.outPath.Add(new Point64(pt.X + offsetVec.x, pt.Y + offsetVec.y, pt.Z)); + pathOut.Add(new Point64(pt.X + offsetVec.x, pt.Y + offsetVec.y, pt.Z)); #else - group.outPath.Add(new Point64(pt.X + offsetVec.x, pt.Y + offsetVec.y)); + pathOut.Add(new Point64(pt.X + offsetVec.x, pt.Y + offsetVec.y)); #endif - if (angle > -Math.PI + 0.01) // avoid 180deg concave + int steps = (int) Math.Ceiling(_stepsPerRad * Math.Abs(angle)); + for (int i = 1; i < steps; i++) // ie 1 less than steps { - int steps = (int) Math.Ceiling(_stepsPerRad * Math.Abs(angle)); - for (int i = 1; i < steps; i++) // ie 1 less than steps - { - offsetVec = new PointD(offsetVec.x * _stepCos - _stepSin * offsetVec.y, - offsetVec.x * _stepSin + offsetVec.y * _stepCos); + offsetVec = new PointD(offsetVec.x * _stepCos - _stepSin * offsetVec.y, + offsetVec.x * _stepSin + offsetVec.y * _stepCos); #if USINGZ - group.outPath.Add(new Point64(pt.X + offsetVec.x, pt.Y + offsetVec.y, pt.Z)); + pathOut.Add(new Point64(pt.X + offsetVec.x, pt.Y + offsetVec.y, pt.Z)); #else - group.outPath.Add(new Point64(pt.X + offsetVec.x, pt.Y + offsetVec.y)); + pathOut.Add(new Point64(pt.X + offsetVec.x, pt.Y + offsetVec.y)); #endif - } } - group.outPath.Add(GetPerpendic(pt, _normals[j])); + pathOut.Add(GetPerpendic(pt, _normals[j])); } [MethodImpl(MethodImplOptions.AggressiveInlining)] @@ -431,45 +564,55 @@ private void OffsetPoint(Group group, Path64 path, int j, ref int k) if (sinA > 1.0) sinA = 1.0; else if (sinA < -1.0) sinA = -1.0; - if (cosA > 0.99) // almost straight - less than 8 degrees + if (DeltaCallback != null) + { + _groupDelta = DeltaCallback(path, _normals, j, k); + if (group.pathsReversed) _groupDelta = -_groupDelta; + } + if (Math.Abs(_groupDelta) < Tolerance) { - group.outPath.Add(GetPerpendic(path[j], _normals[k])); - if (cosA < 0.9998) // greater than 1 degree (#424) - group.outPath.Add(GetPerpendic(path[j], _normals[j])); // (#418) + pathOut.Add(path[j]); + return; } - else if (cosA > -0.99 && (sinA * _group_delta < 0)) // is concave + + if (cosA > -0.99 && (sinA * _groupDelta < 0)) // test for concavity first (#593) { - group.outPath.Add(GetPerpendic(path[j], _normals[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 - group.outPath.Add(path[j]); // (#405) - group.outPath.Add(GetPerpendic(path[j], _normals[j])); + pathOut.Add(path[j]); // (#405) + pathOut.Add(GetPerpendic(path[j], _normals[j])); + } + 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); } - else if (_joinType == JoinType.Round) - DoRound(group, path, j, k, Math.Atan2(sinA, cosA)); else if (_joinType == JoinType.Miter) { - // miter unless the angle is so acute the miter would exceeds ML + // miter unless the angle is sufficiently acute to exceed ML if (cosA > _mitLimSqr - 1) DoMiter(group, path, j, k, cosA); - else DoSquare(group, path, j, k); + else DoSquare(path, j, k); } - // don't bother squaring angles that deviate < ~20 degrees because - // squaring will be indistinguishable from mitering and just be a lot slower - else if (cosA > 0.9) - DoMiter(group, path, j, k, cosA); + else if (_joinType == JoinType.Round) + DoRound(path, j, k, Math.Atan2(sinA, cosA)); + else if (_joinType == JoinType.Bevel) + DoBevel(path, j, k); else - DoSquare(group, path, j, k); + DoSquare(path, j, k); + k = j; } [MethodImpl(MethodImplOptions.AggressiveInlining)] private void OffsetPolygon(Group group, Path64 path) { - group.outPath = new Path64(); + pathOut = new Path64(); int cnt = path.Count, prev = cnt - 1; for (int i = 0; i < cnt; i++) OffsetPoint(group, path, i, ref prev); - group.outPaths.Add(group.outPath); + _solution.Add(pathOut); } [MethodImpl(MethodImplOptions.AggressiveInlining)] @@ -483,32 +626,28 @@ private void OffsetOpenJoined(Group group, Path64 path) private void OffsetOpenPath(Group group, Path64 path) { - group.outPath = new Path64(); + pathOut = new Path64(); int highI = path.Count - 1; + if (DeltaCallback != null) + _groupDelta = DeltaCallback(path, _normals, 0, 0); + // do the line start cap - switch (_endType) - { - case EndType.Butt: -#if USINGZ - group.outPath.Add(new Point64( - path[0].X - _normals[0].x * _group_delta, - path[0].Y - _normals[0].y * _group_delta, - path[0].Z)); -#else - group.outPath.Add(new Point64( - path[0].X - _normals[0].x * _group_delta, - path[0].Y - _normals[0].y * _group_delta)); -#endif - group.outPath.Add(GetPerpendic(path[0], _normals[0])); - break; - case EndType.Round: - DoRound(group, path, 0, 0, Math.PI); - break; - default: - DoSquare(group, path, 0, 0); - break; - } + if (Math.Abs(_groupDelta) < Tolerance) + pathOut.Add(path[0]); + else + switch (_endType) + { + case EndType.Butt: + DoBevel(path, 0, 0); + break; + case EndType.Round: + DoRound(path, 0, 0, Math.PI); + break; + default: + DoSquare(path, 0, 0); + break; + } // offset the left side going forward for (int i = 1, k = 0; i < highI; i++) @@ -519,130 +658,131 @@ private void OffsetOpenPath(Group group, Path64 path) _normals[i] = new PointD(-_normals[i - 1].x, -_normals[i - 1].y); _normals[0] = _normals[highI]; + if (DeltaCallback != null) + _groupDelta = DeltaCallback(path, _normals, highI, highI); // do the line end cap - switch (_endType) - { - case EndType.Butt: -#if USINGZ - group.outPath.Add(new Point64( - path[highI].X - _normals[highI].x * _group_delta, - path[highI].Y - _normals[highI].y * _group_delta, - path[highI].Z)); -#else - group.outPath.Add(new Point64( - path[highI].X - _normals[highI].x * _group_delta, - path[highI].Y - _normals[highI].y * _group_delta)); -#endif - group.outPath.Add(GetPerpendic(path[highI], _normals[highI])); - break; - case EndType.Round: - DoRound(group, path, highI, highI, Math.PI); - break; - default: - DoSquare(group, path, highI, highI); - break; - } + if (Math.Abs(_groupDelta) < Tolerance) + pathOut.Add(path[highI]); + else + switch (_endType) + { + case EndType.Butt: + DoBevel(path, highI, highI); + break; + case EndType.Round: + DoRound(path, highI, highI, Math.PI); + break; + default: + DoSquare(path, highI, highI); + break; + } // offset the left side going back for (int i = highI, k = 0; i > 0; i--) OffsetPoint(group, path, i, ref k); - group.outPaths.Add(group.outPath); + _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) { - // the lowermost polygon must be an outer polygon. So we can use that as the - // designated orientation for outer polygons (needed for tidy-up clipping) - GetBoundsAndLowestPolyIdx(group.inPaths, - out int lowestIdx, out Rect64 grpBounds); - if (lowestIdx < 0) return; - double area = Clipper.Area(group.inPaths[lowestIdx]); - //if (area == 0) return; // this is probably unhelpful (#430) - group.pathsReversed = (area < 0); - if (group.pathsReversed) _group_delta = -_delta; - else _group_delta = _delta; + // 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 - { - group.pathsReversed = false; - _group_delta = Math.Abs(_delta) * 0.5; - } - _abs_group_delta = Math.Abs(_group_delta); + _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; - // calculate a sensible number of steps (for 360 deg for the given offset 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 double arcTol = ArcTolerance > 0.01 ? ArcTolerance : - Math.Log10(2 + _abs_group_delta) * InternalClipper.defaultArcTolerance; - double stepsPer360 = Math.PI / Math.Acos(1 - arcTol / _abs_group_delta); + Math.Log10(2 + absDelta) * InternalClipper.defaultArcTolerance; + double stepsPer360 = Math.PI / Math.Acos(1 - arcTol / absDelta); _stepSin = Math.Sin((2 * Math.PI) / stepsPer360); _stepCos = Math.Cos((2 * Math.PI) / stepsPer360); - if (_group_delta < 0.0) _stepSin = -_stepSin; + if (_groupDelta < 0.0) _stepSin = -_stepSin; _stepsPerRad = stepsPer360 / (2 * Math.PI); } - bool isJoined = - (group.endType == EndType.Joined) || - (group.endType == EndType.Polygon); - - foreach (Path64 p in group.inPaths) + 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()) { - Path64 path = Clipper.StripDuplicates(p, isJoined); - int cnt = path.Count; - if ((cnt == 0) || ((cnt < 3) && (_endType == EndType.Polygon))) - continue; + Rect64 pathBounds = boundsIt.Current; + if (!pathBounds.IsValid()) continue; + + Path64 p = pathIt.Current; + bool isHole = isHoleIt.Current; + + pathOut = new Path64(); + int cnt = p.Count; if (cnt == 1) { - group.outPath = new Path64(); + Point64 pt = p[0]; + // single vertex so build a circle or square ... if (group.endType == EndType.Round) { - double r = _abs_group_delta; - group.outPath = Clipper.Ellipse(path[0], r, r); + double r = absDelta; + int steps = (int) Math.Ceiling(_stepsPerRad * 2 * Math.PI); + pathOut = Clipper.Ellipse(pt, r, r, steps); #if USINGZ - group.outPath = InternalClipper.SetZ(group.outPath, path[0].Z); -#endif + pathOut = InternalClipper.SetZ(pathOut, pt.Z); +#endif } else { - int d = (int) Math.Ceiling(_group_delta); - Rect64 r = new Rect64(path[0].X - d, path[0].Y - d, - path[0].X - d, path[0].Y - d); - group.outPath = r.AsPath(); + int d = (int) Math.Ceiling(_groupDelta); + Rect64 r = new Rect64(pt.X - d, pt.Y - d, pt.X + d, pt.Y + d); + pathOut = r.AsPath(); #if USINGZ - group.outPath = InternalClipper.SetZ(group.outPath, path[0].Z); + pathOut = InternalClipper.SetZ(pathOut, pt.Z); #endif } - group.outPaths.Add(group.outPath); - } - else - { - if (cnt == 2 && group.endType == EndType.Joined) - { - if (group.joinType == JoinType.Round) - _endType = EndType.Round; - else - _endType = EndType.Square; - } - BuildNormals(path); - if (_endType == EndType.Polygon) OffsetPolygon(group, path); - else if (_endType == EndType.Joined) OffsetOpenJoined(group, path); - else OffsetOpenPath(group, path); - } + _solution.Add(pathOut); + 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 (((_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); + else if (_endType == EndType.Joined) OffsetOpenJoined(group, p); + else OffsetOpenPath(group, p); } - _solution.AddRange(group.outPaths); - group.outPaths.Clear(); } } diff --git a/CSharp/Clipper2Lib/Clipper.RectClip.cs b/CSharp/Clipper2Lib/Clipper.RectClip.cs index f00ff68f..f0dc0102 100644 --- a/CSharp/Clipper2Lib/Clipper.RectClip.cs +++ b/CSharp/Clipper2Lib/Clipper.RectClip.cs @@ -1,6 +1,6 @@ /******************************************************************************* * Author : Angus Johnson * -* Date : 16 February 2023 * +* Date : 8 September 2023 * * Website : http://www.angusj.com * * Copyright : Angus Johnson 2010-2023 * * Purpose : FAST rectangular clipping * @@ -10,7 +10,6 @@ #nullable enable using System; using System.Collections.Generic; -using System.Linq; using System.Runtime.CompilerServices; namespace Drecom.Clipper2Lib @@ -29,7 +28,7 @@ public OutPt2(Point64 pt) } } - public class RectClip + public class RectClip64 { protected enum Location { @@ -43,7 +42,7 @@ protected enum Location protected List results_; protected List[] edges_; protected int currIdx_ = -1; - internal RectClip(Rect64 rect) + internal RectClip64(Rect64 rect) { currIdx_ = -1; rect_ = rect; @@ -276,6 +275,67 @@ static protected bool GetLocation(Rect64 rec, Point64 pt, out Location loc) return true; } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static bool IsHorizontal(Point64 pt1, Point64 pt2) + { + return pt1.Y == pt2.Y; + } + + private static bool GetSegmentIntersection(Point64 p1, + Point64 p2, Point64 p3, Point64 p4, out Point64 ip) + { + double res1 = InternalClipper.CrossProduct(p1, p3, p4); + double res2 = InternalClipper.CrossProduct(p2, p3, p4); + if (res1 == 0) + { + ip = p1; + if (res2 == 0) return false; // segments are collinear + else if (p1 == p3 || p1 == p4) return true; + //else if (p2 == p3 || p2 == p4) { ip = p2; return true; } + else if (IsHorizontal(p3, p4)) return ((p1.X > p3.X) == (p1.X < p4.X)); + else return ((p1.Y > p3.Y) == (p1.Y < p4.Y)); + } + else if (res2 == 0) + { + ip = p2; + if (p2 == p3 || p2 == p4) return true; + else if (IsHorizontal(p3, p4)) return ((p2.X > p3.X) == (p2.X < p4.X)); + else return ((p2.Y > p3.Y) == (p2.Y < p4.Y)); + } + + if ((res1 > 0) == (res2 > 0)) + { + ip = new Point64(0, 0); + return false; + } + + double res3 = InternalClipper.CrossProduct(p3, p1, p2); + double res4 = InternalClipper.CrossProduct(p4, p1, p2); + if (res3 == 0) + { + ip = p3; + if (p3 == p1 || p3 == p2) return true; + else if (IsHorizontal(p1, p2)) return ((p3.X > p1.X) == (p3.X < p2.X)); + else return ((p3.Y > p1.Y) == (p3.Y < p2.Y)); + } + else if (res4 == 0) + { + ip = p4; + if (p4 == p1 || p4 == p2) return true; + else if (IsHorizontal(p1, p2)) return ((p4.X > p1.X) == (p4.X < p2.X)); + else return ((p4.Y > p1.Y) == (p4.Y < p2.Y)); + } + if ((res3 > 0) == (res4 > 0)) + { + ip = new Point64(0, 0); + return false; + } + + // segments must intersect to get here + return InternalClipper.GetIntersectPoint(p1, p2, p3, p4, out ip); + } + + static protected bool GetIntersection(Path64 rectPath, Point64 p, Point64 p2, ref Location loc, out Point64 ip) { // gets the pt of intersection between rectPath and segment(p, p2) that's closest to 'p' @@ -284,108 +344,88 @@ static protected bool GetIntersection(Path64 rectPath, Point64 p, Point64 p2, re switch (loc) { case Location.left: - if (InternalClipper.SegsIntersect(p, p2, rectPath[0], rectPath[3], true)) - { - InternalClipper.GetIntersectPt(p, p2, rectPath[0], rectPath[3], out ip); - } - else if (p.Y < rectPath[0].Y && - InternalClipper.SegsIntersect(p, p2, rectPath[0], rectPath[1], true)) - { - InternalClipper.GetIntersectPt(p, p2, rectPath[0], rectPath[1], out ip); + if (GetSegmentIntersection(p, p2, rectPath[0], rectPath[3], out ip)) + return true; + else if (p.Y < rectPath[0].Y && GetSegmentIntersection(p, p2, rectPath[0], rectPath[1], out ip)) + { loc = Location.top; + return true; } - else if (InternalClipper.SegsIntersect(p, p2, rectPath[2], rectPath[3], true)) - { - InternalClipper.GetIntersectPt(p, p2, rectPath[2], rectPath[3], out ip); + else if (GetSegmentIntersection(p, p2, rectPath[2], rectPath[3], out ip)) + { loc = Location.bottom; + return true; } else return false; - break; case Location.right: - if (InternalClipper.SegsIntersect(p, p2, rectPath[1], rectPath[2], true)) - { - InternalClipper.GetIntersectPt(p, p2, rectPath[1], rectPath[2], out ip); - } - else if (p.Y < rectPath[0].Y && - InternalClipper.SegsIntersect(p, p2, rectPath[0], rectPath[1], true)) - { - InternalClipper.GetIntersectPt(p, p2, rectPath[0], rectPath[1], out ip); + if (GetSegmentIntersection(p, p2, rectPath[1], rectPath[2], out ip)) + return true; + else if (p.Y < rectPath[0].Y && GetSegmentIntersection(p, p2, rectPath[0], rectPath[1], out ip)) + { loc = Location.top; + return true; } - else if (InternalClipper.SegsIntersect(p, p2, rectPath[2], rectPath[3], true)) - { - InternalClipper.GetIntersectPt(p, p2, rectPath[2], rectPath[3], out ip); + else if (GetSegmentIntersection(p, p2, rectPath[2], rectPath[3], out ip)) + { loc = Location.bottom; + return true; } else return false; - break; case Location.top: - if (InternalClipper.SegsIntersect(p, p2, rectPath[0], rectPath[1], true)) - { - InternalClipper.GetIntersectPt(p, p2, rectPath[0], rectPath[1], out ip); - } - else if (p.X < rectPath[0].X && - InternalClipper.SegsIntersect(p, p2, rectPath[0], rectPath[3], true)) - { - InternalClipper.GetIntersectPt(p, p2, rectPath[0], rectPath[3], out ip); + if (GetSegmentIntersection(p, p2, rectPath[0], rectPath[1], out ip)) + return true; + else if (p.X < rectPath[0].X && GetSegmentIntersection(p, p2, rectPath[0], rectPath[3], out ip)) + { loc = Location.left; + return true; } - else if (p.X > rectPath[1].X && - InternalClipper.SegsIntersect(p, p2, rectPath[1], rectPath[2], true)) - { - InternalClipper.GetIntersectPt(p, p2, rectPath[1], rectPath[2], out ip); + else if (p.X > rectPath[1].X && GetSegmentIntersection(p, p2, rectPath[1], rectPath[2], out ip)) + { loc = Location.right; + return true; } else return false; - break; case Location.bottom: - if (InternalClipper.SegsIntersect(p, p2, rectPath[2], rectPath[3], true)) - { - InternalClipper.GetIntersectPt(p, p2, rectPath[2], rectPath[3], out ip); - } - else if (p.X < rectPath[3].X && - InternalClipper.SegsIntersect(p, p2, rectPath[0], rectPath[3], true)) + if (GetSegmentIntersection(p, p2, rectPath[2], rectPath[3], out ip)) + return true; + else if (p.X < rectPath[3].X && GetSegmentIntersection(p, p2, rectPath[0], rectPath[3], out ip)) { - InternalClipper.GetIntersectPt(p, p2, rectPath[0], rectPath[3], out ip); loc = Location.left; + return true; } - else if (p.X > rectPath[2].X && - InternalClipper.SegsIntersect(p, p2, rectPath[1], rectPath[2], true)) + else if (p.X > rectPath[2].X && GetSegmentIntersection(p, p2, rectPath[1], rectPath[2], out ip)) { - InternalClipper.GetIntersectPt(p, p2, rectPath[1], rectPath[2], out ip); loc = Location.right; + return true; } else return false; - break; - case Location.inside: - if (InternalClipper.SegsIntersect(p, p2, rectPath[0], rectPath[3], true)) + default: + if (GetSegmentIntersection(p, p2, rectPath[0], rectPath[3], out ip)) { - InternalClipper.GetIntersectPt(p, p2, rectPath[0], rectPath[3], out ip); loc = Location.left; + return true; } - else if (InternalClipper.SegsIntersect(p, p2, rectPath[0], rectPath[1], true)) + else if (GetSegmentIntersection(p, p2, rectPath[0], rectPath[1], out ip)) { - InternalClipper.GetIntersectPt(p, p2, rectPath[0], rectPath[1], out ip); loc = Location.top; + return true; } - else if (InternalClipper.SegsIntersect(p, p2, rectPath[1], rectPath[2], true)) + else if (GetSegmentIntersection(p, p2, rectPath[1], rectPath[2], out ip)) { - InternalClipper.GetIntersectPt(p, p2, rectPath[1], rectPath[2], out ip); loc = Location.right; + return true; } - else if (InternalClipper.SegsIntersect(p, p2, rectPath[2], rectPath[3], true)) + else if (GetSegmentIntersection(p, p2, rectPath[2], rectPath[3], out ip)) { - InternalClipper.GetIntersectPt(p, p2, rectPath[2], rectPath[3], out ip); loc = Location.bottom; + return true; } else return false; - break; } - return true; } protected void GetNextLocation(Path64 path, @@ -545,7 +585,7 @@ private void ExecuteInternal(Path64 path) loc = prev; GetIntersection(rectPath_, prevPt, path[i], ref loc, out Point64 ip2); - if (prevCrossLoc != Location.inside) + if (prevCrossLoc != Location.inside && prevCrossLoc != loc) //#597 AddCorner(prevCrossLoc, loc); if (firstCross == Location.inside) @@ -611,7 +651,7 @@ private void ExecuteInternal(Path64 path) } } - public Paths64 Execute(Paths64 paths, bool convexOnly) + public Paths64 Execute(Paths64 paths) { Paths64 result = new Paths64(); if (rect_.IsEmpty()) return result; @@ -628,12 +668,9 @@ public Paths64 Execute(Paths64 paths, bool convexOnly) continue; } ExecuteInternal(path); - if (!convexOnly) - { - CheckEdges(); - for (int i = 0; i < 4; ++i) - TidyEdgePair(i, edges_[i * 2], edges_[i * 2 + 1]); - } + CheckEdges(); + for (int i = 0; i < 4; ++i) + TidyEdgePair(i, edges_[i * 2], edges_[i * 2 + 1]); foreach (OutPt2? op in results_) { @@ -721,7 +758,7 @@ private void TidyEdgePair(int idx, List cw, List ccw) p1 = cw[i]; if (p1 == null || p1.next == p1.prev) { - cw[i++]!.edge = null; + cw[i++] = null; j = 0; continue; } @@ -916,11 +953,11 @@ private Path64 GetPath(OutPt2? op) } // RectClip class - public class RectClipLines : RectClip + public class RectClipLines64 : RectClip64 { - internal RectClipLines(Rect64 rect) : base(rect) { } + internal RectClipLines64(Rect64 rect) : base(rect) { } - public Paths64 Execute(Paths64 paths) + public new Paths64 Execute(Paths64 paths) { Paths64 result = new Paths64(); if (rect_.IsEmpty()) return result; @@ -1005,7 +1042,7 @@ private void ExecuteInternal(Path64 path) if (loc == Location.inside) // path must be entering rect { - Add(ip); + Add(ip, true); } else if (prev != Location.inside) { diff --git a/CSharp/Clipper2Lib/Clipper.cs b/CSharp/Clipper2Lib/Clipper.cs index 4e54ca59..6e1c832b 100644 --- a/CSharp/Clipper2Lib/Clipper.cs +++ b/CSharp/Clipper2Lib/Clipper.cs @@ -1,6 +1,6 @@ /******************************************************************************* * Author : Angus Johnson * -* Date : 23 March 2023 * +* Date : 18 October 2023 * * Website : http://www.angusj.com * * Copyright : Angus Johnson 2010-2023 * * Purpose : This module contains simple functions that will likely cover * @@ -159,60 +159,53 @@ public static PathsD InflatePaths(PathsD paths, double delta, JoinType joinType, return ScalePathsD(tmp, 1 / scale); } - public static Paths64 ExecuteRectClip(Rect64 rect, - Paths64 paths, bool convexOnly = false) + public static Paths64 RectClip(Rect64 rect, Paths64 paths) { if (rect.IsEmpty() || paths.Count == 0) return new Paths64(); - RectClip rc = new RectClip(rect); - return rc.Execute(paths, convexOnly); + RectClip64 rc = new RectClip64(rect); + return rc.Execute(paths); } - public static Paths64 ExecuteRectClip(Rect64 rect, - Path64 path, bool convexOnly = false) + public static Paths64 RectClip(Rect64 rect, Path64 path) { if (rect.IsEmpty() || path.Count == 0) return new Paths64(); - Paths64 tmp = new Paths64(); - tmp.Add(path); - return ExecuteRectClip(rect, tmp, convexOnly); + Paths64 tmp = new Paths64 { path }; + return RectClip(rect, tmp); } - public static PathsD ExecuteRectClip(RectD rect, PathsD paths, - int precision = 2, bool convexOnly = false) + public static PathsD RectClip(RectD rect, PathsD paths, int precision = 2) { InternalClipper.CheckPrecision(precision); if (rect.IsEmpty() || paths.Count == 0) return new PathsD(); double scale = Math.Pow(10, precision); Rect64 r = ScaleRect(rect, scale); Paths64 tmpPath = ScalePaths64(paths, scale); - RectClip rc = new RectClip(r); - tmpPath = rc.Execute(tmpPath, convexOnly); + RectClip64 rc = new RectClip64(r); + tmpPath = rc.Execute(tmpPath); return ScalePathsD(tmpPath, 1 / scale); } - public static PathsD ExecuteRectClip(RectD rect, PathD path, - int precision = 2, bool convexOnly = false) + public static PathsD RectClip(RectD rect, PathD path, int precision = 2) { if (rect.IsEmpty() || path.Count == 0) return new PathsD(); - PathsD tmp = new PathsD(); - tmp.Add(path); - return ExecuteRectClip(rect, tmp, precision, convexOnly); + PathsD tmp = new PathsD { path }; + return RectClip(rect, tmp, precision); } - public static Paths64 ExecuteRectClipLines(Rect64 rect, Paths64 paths) + public static Paths64 RectClipLines(Rect64 rect, Paths64 paths) { if (rect.IsEmpty() || paths.Count == 0) return new Paths64(); - RectClipLines rc = new RectClipLines(rect); + RectClipLines64 rc = new RectClipLines64(rect); return rc.Execute(paths); } - public static Paths64 ExecuteRectClipLines(Rect64 rect, Path64 path) + public static Paths64 RectClipLines(Rect64 rect, Path64 path) { if (rect.IsEmpty() || path.Count == 0) return new Paths64(); - Paths64 tmp = new Paths64(); - tmp.Add(path); - return ExecuteRectClipLines(rect, tmp); + Paths64 tmp = new Paths64 { path }; + return RectClipLines(rect, tmp); } - public static PathsD ExecuteRectClipLines(RectD rect, + public static PathsD RectClipLines(RectD rect, PathsD paths, int precision = 2) { InternalClipper.CheckPrecision(precision); @@ -220,16 +213,15 @@ public static PathsD ExecuteRectClipLines(RectD rect, double scale = Math.Pow(10, precision); Rect64 r = ScaleRect(rect, scale); Paths64 tmpPath = ScalePaths64(paths, scale); - RectClipLines rc = new RectClipLines(r); + RectClipLines64 rc = new RectClipLines64(r); tmpPath = rc.Execute(tmpPath); return ScalePathsD(tmpPath, 1 / scale); } - public static PathsD ExecuteRectClipLines(RectD rect, PathD path, int precision = 2) + public static PathsD RectClipLines(RectD rect, PathD path, int precision = 2) { if (rect.IsEmpty() || path.Count == 0) return new PathsD(); - PathsD tmp = new PathsD(); - tmp.Add(path); - return ExecuteRectClipLines(rect, tmp, precision); + PathsD tmp = new PathsD { path }; + return RectClipLines(rect, tmp, precision); } public static Paths64 MinkowskiSum(Path64 pattern, Path64 path, bool isClosed) { @@ -349,10 +341,10 @@ public static Point64 ScalePoint64(Point64 pt, double scale) { Point64 result = new Point64() { - X = (long) (pt.X * scale), - Y = (long) (pt.Y * scale), + X = (long) Math.Round(pt.X * scale, MidpointRounding.AwayFromZero), + Y = (long) Math.Round(pt.Y * scale, MidpointRounding.AwayFromZero), #if USINGZ - Z = (long) (pt.Z), + Z = pt.Z #endif }; return result; @@ -858,8 +850,8 @@ private static int GetPrior(int current, int high, ref bool[] flags) return current; } - public static Path64 SimplifyPath(Path64 path, - double epsilon, bool isOpenPath = false) + public static Path64 SimplifyPath(Path64 path, + double epsilon, bool isClosedPath = true) { int len = path.Count, high = len - 1; double epsSqr = Sqr(epsilon); @@ -867,17 +859,19 @@ public static Path64 SimplifyPath(Path64 path, bool[] flags = new bool[len]; double[] dsq = new double[len]; - int prev = high, curr = 0, start, next, prior2, next2; - if (isOpenPath) + int curr = 0, prev, start, next, prior2; + + if (isClosedPath) { - dsq[0] = double.MaxValue; - dsq[high] = double.MaxValue; + dsq[0] = PerpendicDistFromLineSqrd(path[0], path[high], path[1]); + dsq[high] = PerpendicDistFromLineSqrd(path[high], path[0], path[high - 1]); } else { - dsq[0] = PerpendicDistFromLineSqrd(path[0], path[high], path[1]); - dsq[high] = PerpendicDistFromLineSqrd(path[high], path[0], path[high - 1]); + dsq[0] = double.MaxValue; + dsq[high] = double.MaxValue; } + for (int i = 1; i < high; ++i) dsq[i] = PerpendicDistFromLineSqrd(path[i], path[i - 1], path[i + 1]); @@ -899,24 +893,21 @@ public static Path64 SimplifyPath(Path64 path, if (dsq[next] < dsq[curr]) { - flags[next] = true; - next = GetNext(next, high, ref flags); - next2 = GetNext(next, high, ref flags); - dsq[curr] = PerpendicDistFromLineSqrd(path[curr], path[prev], path[next]); - if (next != high || !isOpenPath) - dsq[next] = PerpendicDistFromLineSqrd(path[next], path[curr], path[next2]); + prior2 = prev; + prev = curr; curr = next; + next = GetNext(next, high, ref flags); } else - { - flags[curr] = true; - curr = next; - next = GetNext(next, high, ref flags); prior2 = GetPrior(prev, high, ref flags); + + flags[curr] = true; + curr = next; + next = GetNext(next, high, ref flags); + if (isClosedPath || ((curr != high) && (curr != 0))) dsq[curr] = PerpendicDistFromLineSqrd(path[curr], path[prev], path[next]); - if (prev != 0 || !isOpenPath) - dsq[prev] = PerpendicDistFromLineSqrd(path[prev], path[prior2], path[curr]); - } + if (isClosedPath || ((prev != 0) && (prev != high))) + dsq[prev] = PerpendicDistFromLineSqrd(path[prev], path[prior2], path[curr]); } Path64 result = new Path64(len); for (int i = 0; i < len; i++) @@ -925,16 +916,16 @@ public static Path64 SimplifyPath(Path64 path, } public static Paths64 SimplifyPaths(Paths64 paths, - double epsilon, bool isOpenPath = false) + double epsilon, bool isClosedPaths = true) { Paths64 result = new Paths64(paths.Count); foreach (Path64 path in paths) - result.Add(SimplifyPath(path, epsilon, isOpenPath)); + result.Add(SimplifyPath(path, epsilon, isClosedPaths)); return result; } public static PathD SimplifyPath(PathD path, - double epsilon, bool isOpenPath = false) + double epsilon, bool isClosedPath = true) { int len = path.Count, high = len - 1; double epsSqr = Sqr(epsilon); @@ -942,16 +933,16 @@ public static PathD SimplifyPath(PathD path, bool[] flags = new bool[len]; double[] dsq = new double[len]; - int prev = high, curr = 0, start, next, prior2, next2; - if (isOpenPath) + int curr = 0, prev, start, next, prior2; + if (isClosedPath) { - dsq[0] = double.MaxValue; - dsq[high] = double.MaxValue; + dsq[0] = PerpendicDistFromLineSqrd(path[0], path[high], path[1]); + dsq[high] = PerpendicDistFromLineSqrd(path[high], path[0], path[high - 1]); } else { - dsq[0] = PerpendicDistFromLineSqrd(path[0], path[high], path[1]); - dsq[high] = PerpendicDistFromLineSqrd(path[high], path[0], path[high - 1]); + dsq[0] = double.MaxValue; + dsq[high] = double.MaxValue; } for (int i = 1; i < high; ++i) dsq[i] = PerpendicDistFromLineSqrd(path[i], path[i - 1], path[i + 1]); @@ -974,24 +965,21 @@ public static PathD SimplifyPath(PathD path, if (dsq[next] < dsq[curr]) { - flags[next] = true; - next = GetNext(next, high, ref flags); - next2 = GetNext(next, high, ref flags); - dsq[curr] = PerpendicDistFromLineSqrd(path[curr], path[prev], path[next]); - if (next != high || !isOpenPath) - dsq[next] = PerpendicDistFromLineSqrd(path[next], path[curr], path[next2]); - curr = next; - } - else - { - flags[curr] = true; + prior2 = prev; + prev = curr; curr = next; next = GetNext(next, high, ref flags); + } + else prior2 = GetPrior(prev, high, ref flags); + + flags[curr] = true; + curr = next; + next = GetNext(next, high, ref flags); + if (isClosedPath || ((curr != high) && (curr != 0))) dsq[curr] = PerpendicDistFromLineSqrd(path[curr], path[prev], path[next]); - if (prev != 0 || !isOpenPath) - dsq[prev] = PerpendicDistFromLineSqrd(path[prev], path[prior2], path[curr]); - } + if (isClosedPath || ((prev != 0) && (prev != high))) + dsq[prev] = PerpendicDistFromLineSqrd(path[prev], path[prior2], path[curr]); } PathD result = new PathD(len); for (int i = 0; i < len; i++) @@ -1000,11 +988,11 @@ public static PathD SimplifyPath(PathD path, } public static PathsD SimplifyPaths(PathsD paths, - double epsilon, bool isOpenPath = false) + double epsilon, bool isClosedPath = true) { PathsD result = new PathsD(paths.Count); foreach (PathD path in paths) - result.Add(SimplifyPath(path, epsilon, isOpenPath)); + result.Add(SimplifyPath(path, epsilon, isClosedPath)); return result; } @@ -1122,5 +1110,47 @@ public static PathD Ellipse(PointD center, return result; } + private static void ShowPolyPathStructure(PolyPath64 pp, int level) + { + string spaces = new string(' ', level * 2); + string caption = (pp.IsHole ? "Hole " : "Outer "); + if (pp.Count == 0) + { + Console.WriteLine(spaces + caption); + } + else + { + Console.WriteLine(spaces + caption + string.Format("({0})", pp.Count)); + foreach (PolyPath64 child in pp) { ShowPolyPathStructure(child, level + 1); } + } + } + + public static void ShowPolyTreeStructure(PolyTree64 polytree) + { + Console.WriteLine("Polytree Root"); + foreach (PolyPath64 child in polytree) { ShowPolyPathStructure(child, 1); } + } + + private static void ShowPolyPathStructure(PolyPathD pp, int level) + { + string spaces = new string(' ', level * 2); + string caption = (pp.IsHole ? "Hole " : "Outer "); + if (pp.Count == 0) + { + Console.WriteLine(spaces + caption); + } + else + { + Console.WriteLine(spaces + caption + string.Format("({0})", pp.Count)); + foreach (PolyPathD child in pp) { ShowPolyPathStructure(child, level + 1); } + } + } + + public static void ShowPolyTreeStructure(PolyTreeD polytree) + { + Console.WriteLine("Polytree Root"); + foreach (PolyPathD child in polytree) { ShowPolyPathStructure(child, 1); } + } + } // Clipper } // namespace \ No newline at end of file diff --git a/CSharp/Clipper2Lib/Clipper2.snk b/CSharp/Clipper2Lib/Clipper2.snk new file mode 100644 index 00000000..830c8863 Binary files /dev/null and b/CSharp/Clipper2Lib/Clipper2.snk differ diff --git a/CSharp/Clipper2Lib/Clipper2Lib.csproj b/CSharp/Clipper2Lib/Clipper2Lib.csproj index 9762988d..9a4f2c5f 100644 --- a/CSharp/Clipper2Lib/Clipper2Lib.csproj +++ b/CSharp/Clipper2Lib/Clipper2Lib.csproj @@ -1,33 +1,31 @@ - - - - netstandard2.0 - enable - 8 - 1.1.0 - True - Angus Johnson - Polygon Clipping and Offsetting Library - Clipper2 - Clipper2 - http://www.angusj.com/clipper2/Docs/Overview.htm - Copyright © 2010-2022 - git - https://github.com/AngusJohnson/Clipper2 - Major revision to merging polygons in clipping solutions - AnyCPU;x86 - - - - TRACE - - - - TRACE - - - - - - - + + + + netstandard2.0 + enable + 8 + True + Angus Johnson + Polygon Clipping and Offsetting Library + Clipper2 + Clipper2 + http://www.angusj.com/clipper2/Docs/Overview.htm + Copyright © 2010-2023 + git + https://github.com/AngusJohnson/Clipper2 + Major revision to merging polygons in clipping solutions + AnyCPU;x86 + true + Clipper2.snk + false + + + + TRACE + + + + TRACE + + + diff --git a/CSharp/Clipper2Lib/HashCode.cs b/CSharp/Clipper2Lib/HashCode.cs new file mode 100644 index 00000000..d3e6c635 --- /dev/null +++ b/CSharp/Clipper2Lib/HashCode.cs @@ -0,0 +1,126 @@ +using System; +using System.ComponentModel; +using System.Runtime.CompilerServices; +using System.Security.Cryptography; + +namespace Drecom.Clipper2Lib +{ + /* + + Licensed to the .NET Foundation under one or more agreements. + // The .NET Foundation licenses this file to you under the MIT license. + + The xxHash32 implementation is based on the code published by Yann Collet: + https://raw.githubusercontent.com/Cyan4973/xxHash/5c174cfa4e45a42f94082dc0d4539b39696afea1/xxhash.c + + xxHash - Fast Hash algorithm + Copyright (C) 2012-2016, Yann Collet + + BSD 2-Clause License (http://www.opensource.org/licenses/bsd-license.php) + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are + met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following disclaimer + in the documentation and/or other materials provided with the + distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + You can contact the author at : + - xxHash homepage: http://www.xxhash.com + - xxHash source repository : https://github.com/Cyan4973/xxHash + */ + + public struct HashCode + { + private static readonly uint s_seed = GenerateGlobalSeed(); + + private const uint Prime1 = 2654435761U; + private const uint Prime2 = 2246822519U; + private const uint Prime3 = 3266489917U; + private const uint Prime4 = 668265263U; + private const uint Prime5 = 374761393U; + + private static uint GenerateGlobalSeed() + { + using RandomNumberGenerator randomNumberGenerator = RandomNumberGenerator.Create(); + byte[] data = new byte[sizeof(uint)]; + randomNumberGenerator.GetBytes(data); + return BitConverter.ToUInt32(data, 0); + } + + public static int Combine(T1 value1, T2 value2) + { + uint hc1 = (uint) (value1?.GetHashCode() ?? 0); + uint hc2 = (uint) (value2?.GetHashCode() ?? 0); + + uint hash = MixEmptyState(); + hash += 8; + + hash = QueueRound(hash, hc1); + hash = QueueRound(hash, hc2); + + hash = MixFinal(hash); + return (int) hash; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static uint QueueRound(uint hash, uint queuedValue) + { + return RotateLeft(hash + (queuedValue * Prime3), 17) * Prime4; + } + + private static uint MixEmptyState() + { + return s_seed + Prime5; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static uint MixFinal(uint hash) + { + hash ^= hash >> 15; + hash *= Prime2; + hash ^= hash >> 13; + hash *= Prime3; + hash ^= hash >> 16; + return hash; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static uint RotateLeft(uint value, int offset) + { + return (value << offset) | (value >> (32 - offset)); + } + +#pragma warning disable CS0809 // Obsolete member overrides non-obsolete member + [Obsolete("HashCode is a mutable struct and should not be compared with other HashCodes. Use ToHashCode to retrieve the computed hash code.", error: true)] + [EditorBrowsable(EditorBrowsableState.Never)] + public override int GetHashCode() + { + throw new NotSupportedException($"{nameof(HashCode)}.{nameof(GetHashCode)}() is not supported"); + } + + [Obsolete("HashCode is a mutable struct and should not be compared with other HashCodes.", error: true)] + [EditorBrowsable(EditorBrowsableState.Never)] + public override bool Equals(object? obj) + { + throw new NotSupportedException($"{nameof(HashCode)}.{nameof(Equals)}() is not supported"); + } +#pragma warning restore CS0809 // Obsolete member overrides non-obsolete member + } +} \ No newline at end of file diff --git a/CSharp/USINGZ.TestApp/Program.cs b/CSharp/USINGZ.TestApp/Program.cs new file mode 100644 index 00000000..4d290bd7 --- /dev/null +++ b/CSharp/USINGZ.TestApp/Program.cs @@ -0,0 +1,79 @@ +/******************************************************************************* +* Author : Angus Johnson * +* Date : 24 January 2023 * +* Website : http://www.angusj.com * +* Copyright : Angus Johnson 2010-2023 * +* License : http://www.boost.org/LICENSE_1_0.txt * +*******************************************************************************/ + +using System.Collections.Generic; +using System.Reflection; +using System.IO; +using System; +using System.Security.Cryptography; +using System.Xml.Linq; +using System.Runtime.InteropServices; +using System.Diagnostics; + +using Clipper2Lib; + +public class Application +{ + public class MyCallbacks + { + public void MyCallback64(Point64 bot1, Point64 top1, + Point64 bot2, Point64 top2, ref Point64 intersectPt) + { + intersectPt.Z = 1; + } + + public void MyCallbackD(PointD bot1, PointD top1, + PointD bot2, PointD top2, ref PointD intersectPt) + { + intersectPt.z = 1; + } + } + + 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 })); + + ClipperD clipperD = new ClipperD(); + MyCallbacks cb = new MyCallbacks(); + clipperD.ZCallback = cb.MyCallbackD; + clipperD.AddSubject(subject); + clipperD.Execute(ClipType.Union, FillRule.NonZero, solution); + + + Console.WriteLine(solution.ToString(0)); + + SvgWriter svg= new SvgWriter(FillRule.NonZero); + SvgUtils.AddSubject(svg, subject); + SvgUtils.AddSolution(svg, solution, false); + + PathsD ellipses = new PathsD(); + for (int i = 0; i < solution[0].Count; i++) + { + if (solution[0][i].z == 1) + ellipses.Add(Clipper.Ellipse( + new PointD(solution[0][i].x, solution[0][i].y), 4)); + } + 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(); + } + +} diff --git a/CSharp/USINGZ.TestApp/UsingZTestApp.csproj b/CSharp/USINGZ.TestApp/UsingZTestApp.csproj new file mode 100644 index 00000000..11464c72 --- /dev/null +++ b/CSharp/USINGZ.TestApp/UsingZTestApp.csproj @@ -0,0 +1,23 @@ + + + + Exe + net6.0 + enable + enable + + + + $(DefineConstants);USINGZ + + + + + + + + + + + + diff --git a/CSharp/USINGZ.TestApp/UsingZ_TestApp.sln b/CSharp/USINGZ.TestApp/UsingZ_TestApp.sln new file mode 100644 index 00000000..2bac2abb --- /dev/null +++ b/CSharp/USINGZ.TestApp/UsingZ_TestApp.sln @@ -0,0 +1,31 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.4.33205.214 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "UsingZTestApp", "UsingZTestApp.csproj", "{34A93225-8048-4A5A-8741-BC619C13C7B0}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Clipper2LibZ", "..\USINGZ\Clipper2LibZ.csproj", "{C50EB9B5-CE07-49DE-B147-941DBFB4FA4E}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {34A93225-8048-4A5A-8741-BC619C13C7B0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {34A93225-8048-4A5A-8741-BC619C13C7B0}.Debug|Any CPU.Build.0 = Debug|Any CPU + {34A93225-8048-4A5A-8741-BC619C13C7B0}.Release|Any CPU.ActiveCfg = Release|Any CPU + {34A93225-8048-4A5A-8741-BC619C13C7B0}.Release|Any CPU.Build.0 = Release|Any CPU + {C50EB9B5-CE07-49DE-B147-941DBFB4FA4E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C50EB9B5-CE07-49DE-B147-941DBFB4FA4E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C50EB9B5-CE07-49DE-B147-941DBFB4FA4E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C50EB9B5-CE07-49DE-B147-941DBFB4FA4E}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {0E1221D6-32D4-4974-9205-DE97EC5F4937} + EndGlobalSection +EndGlobal diff --git a/CSharp/USINGZ/Clipper2LibZ.csproj b/CSharp/USINGZ/Clipper2LibZ.csproj index b36c9182..98de5d5d 100644 --- a/CSharp/USINGZ/Clipper2LibZ.csproj +++ b/CSharp/USINGZ/Clipper2LibZ.csproj @@ -1,7 +1,7 @@ - net6.0 + netstandard2.0 enable 8 @@ -15,7 +15,12 @@ - + + + + + + + - diff --git a/CSharp/Utils/ClipFileIO/Clipper.FileIO.csproj b/CSharp/Utils/ClipFileIO/Clipper.FileIO.csproj index 9fb89b54..50721f6a 100644 --- a/CSharp/Utils/ClipFileIO/Clipper.FileIO.csproj +++ b/CSharp/Utils/ClipFileIO/Clipper.FileIO.csproj @@ -1,15 +1,15 @@ - - - - netstandard2.0 - enable - 8 - AnyCPU;x86 - - - - - - - - + + + + netstandard2.0 + enable + 8 + AnyCPU;x86 + + + + + + + + diff --git a/CSharp/Utils/Colors/Clipper.Colors.csproj b/CSharp/Utils/Colors/Clipper.Colors.csproj index 35c50604..91f9c196 100644 --- a/CSharp/Utils/Colors/Clipper.Colors.csproj +++ b/CSharp/Utils/Colors/Clipper.Colors.csproj @@ -3,7 +3,7 @@ netstandard2.0 enable - 10 + 8 AnyCPU;x86 diff --git a/CSharp/Utils/SVG/Clipper.SVG.Utils.cs b/CSharp/Utils/SVG/Clipper.SVG.Utils.cs index d594d926..75f68112 100644 --- a/CSharp/Utils/SVG/Clipper.SVG.Utils.cs +++ b/CSharp/Utils/SVG/Clipper.SVG.Utils.cs @@ -6,9 +6,7 @@ * License : http://www.boost.org/LICENSE_1_0.txt * *******************************************************************************/ -using System; using System.IO; -using System.Runtime.InteropServices; namespace Drecom.Clipper2Lib { diff --git a/CSharp/Utils/SVG/Clipper.SVG.cs b/CSharp/Utils/SVG/Clipper.SVG.cs index d1dfa9e0..9fa03ea3 100644 --- a/CSharp/Utils/SVG/Clipper.SVG.cs +++ b/CSharp/Utils/SVG/Clipper.SVG.cs @@ -305,7 +305,7 @@ public bool SaveToFile(string filename, int maxWidth = 0, int maxHeight = 0, int { writer.Write("\n", captionInfo.fontSize, ColorToHtml(captionInfo.fontColor)); - writer.Write("{2}\n\n", captionInfo.posX + margin, captionInfo.posY + margin, captionInfo.text); + writer.Write("{2}\n\n", captionInfo.posX * scale + offsetX, captionInfo.posY * scale + offsetX, captionInfo.text); } writer.Write("\n"); diff --git a/CSharp/Utils/SVG/Clipper2.SVG.csproj b/CSharp/Utils/SVG/Clipper2.SVG.csproj index 27ea7f6c..361a3ec0 100644 --- a/CSharp/Utils/SVG/Clipper2.SVG.csproj +++ b/CSharp/Utils/SVG/Clipper2.SVG.csproj @@ -1,12 +1,14 @@ - - - - netstandard2.0 - AnyCPU;x86 - - - - - - - + + + + netstandard2.0 + enable + 8 + AnyCPU;x86 + + + + + + + diff --git a/DLL/CSharp_TestApp/COMPILE AND ADD Clipper2_64.dll HERE b/DLL/CSharp_TestApp/COMPILE AND ADD Clipper2_64.dll HERE new file mode 100644 index 00000000..e69de29b diff --git a/DLL/CSharp_TestApp/CSharp_TestApp.csproj b/DLL/CSharp_TestApp/CSharp_TestApp.csproj new file mode 100644 index 00000000..fe6a125f --- /dev/null +++ b/DLL/CSharp_TestApp/CSharp_TestApp.csproj @@ -0,0 +1,11 @@ + + + + Exe + net6.0 + enable + enable + x64 + + + diff --git a/DLL/CSharp_TestApp/CSharp_TestApp.sln b/DLL/CSharp_TestApp/CSharp_TestApp.sln new file mode 100644 index 00000000..961e12cb --- /dev/null +++ b/DLL/CSharp_TestApp/CSharp_TestApp.sln @@ -0,0 +1,25 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.6.33801.468 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CSharp_TestApp", "CSharp_TestApp.csproj", "{CFC62F44-8150-4BD9-A65F-2FC3876F561C}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|x64 = Debug|x64 + Release|x64 = Release|x64 + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {CFC62F44-8150-4BD9-A65F-2FC3876F561C}.Debug|x64.ActiveCfg = Debug|x64 + {CFC62F44-8150-4BD9-A65F-2FC3876F561C}.Debug|x64.Build.0 = Debug|x64 + {CFC62F44-8150-4BD9-A65F-2FC3876F561C}.Release|x64.ActiveCfg = Release|x64 + {CFC62F44-8150-4BD9-A65F-2FC3876F561C}.Release|x64.Build.0 = Release|x64 + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {F0327373-C3AF-400F-B7EB-33118D1EEE68} + EndGlobalSection +EndGlobal diff --git a/DLL/CSharp_TestApp/Program.cs b/DLL/CSharp_TestApp/Program.cs new file mode 100644 index 00000000..b0c491f1 --- /dev/null +++ b/DLL/CSharp_TestApp/Program.cs @@ -0,0 +1,291 @@ +/******************************************************************************* +* Author : Angus Johnson * +* Date : 29 October 2023 * +* Website : http://www.angusj.com * +* Copyright : Angus Johnson 2010-2023 * +* License : http://www.boost.org/LICENSE_1_0.txt * +*******************************************************************************/ + +using System; +using System.Runtime.InteropServices; + +namespace ClipperDllDemo +{ + + public class Application + { + + // CreateCPaths: The CPaths structure is defined in + // clipper.export.h and is a simple array of long[] or + // double[] that represents a number of path contours. + + static T[]? CreateCPath(T[] coords) + { + int pathLen = coords.Length / 2; + if (pathLen == 0) return null; + int arrayLen = pathLen * 2 + 2; + T[] result = new T[arrayLen]; + result[0] = (T)Convert.ChangeType(pathLen, typeof(T)); + result[1] = (T)Convert.ChangeType(0, typeof(T)); + coords.CopyTo(result, 2); + return result; + } + + static T[] CreateCPaths(List listOfCPath) + { + int pathCount = listOfCPath.Count(); + int arrayLen = 2; + foreach (T[] path in listOfCPath) + arrayLen += path.Length; + T[] result = new T[arrayLen]; + + result[0] = (T)Convert.ChangeType(arrayLen, typeof(T)); + result[1] = (T)Convert.ChangeType(pathCount, typeof(T)); + + int idx = 2; + foreach (T[] cpath in listOfCPath) + { + cpath.CopyTo(result, idx); + idx += cpath.Length; + } + return result; + } + + // or create a cpaths array that contains just 1 path + static T[] CreateCPaths(T[] coords) + { + int pathLen = coords.Length / 2; + int arrayLen = pathLen *2 + 2 + 2; + T[] result = new T[arrayLen]; + + result[0] = (T)Convert.ChangeType(arrayLen, typeof(T)); + result[1] = (T)Convert.ChangeType(1, typeof(T)); // 1 path + + result[2] = (T)Convert.ChangeType(pathLen, typeof(T)); + result[3] = (T)Convert.ChangeType(0, typeof(T)); + + coords.CopyTo(result, 4); + return result; + } + + public static string XyCoordsAsString(T X, T Y, int precision = 0) + { + if (typeof(T) == typeof(long)) // ignore precision + return $"{X},{Y} "; + else + return string.Format($"{{0:F{precision}}},{{1:F{precision}}} ", X, Y); + } + + public static void DisplayCPath(T[] cpaths, ref int idx, string spaceIndent) + { + int vertexCnt = Convert.ToInt32(cpaths[idx]); + idx += 2; + for (int i = 0; i < vertexCnt; i++) + Console.Write(spaceIndent + + XyCoordsAsString(cpaths[idx++], cpaths[idx++], 2)); + Console.Write("\n"); + } + + public static void DisplayCPaths(T[]? cpaths, string spaceIndent) + { + if (cpaths == null) return; + int pathCnt = Convert.ToInt32(cpaths[1]); + int idx = 2; + for (int i = 0; i < pathCnt; i++) + DisplayCPath(cpaths, ref idx, spaceIndent); + } + + // Note: The CPolyTree structure defined in clipper.export.h is + // a simple array of T that contains any number of nested path contours. + + public static void DisplayPolyPath(T[] polypath, + ref int idx, bool isHole, string spaceIndent, int precision) + { + int polyCnt = Convert.ToInt32(polypath[idx++]); + int childCnt = Convert.ToInt32(polypath[idx++]); + string preamble = isHole ? "Hole: " : (spaceIndent == "") ? + "Polygon: " : "Nested Polygon: "; + Console.Write(spaceIndent + preamble); + spaceIndent += " "; + for (int i = 0; i < polyCnt; i++) + Console.Write(XyCoordsAsString(polypath[idx++], polypath[idx++], precision)); + Console.Write("\n"); + for (int i = 0; i < childCnt; i++) + DisplayPolyPath(polypath, ref idx, !isHole, spaceIndent, precision); + } + + public static void DisplayPolytree(T[] polytree, int precision) + { + int cnt = Convert.ToInt32(polytree[1]); + int idx = 2; + for (int i = 0; i < cnt; i++) + DisplayPolyPath(polytree, ref idx, false, " ", precision); + } + + public static T[]? GetArrayFromIntPtr(IntPtr paths) + { + if (paths == IntPtr.Zero) return null; + if (typeof(T) == typeof(long)) + { + long[] len = new long[1]; + Marshal.Copy(paths, len, 0, 1); + long[] res = new long[(int)len[0]]; + Marshal.Copy(paths, res, 0, (int)len[0]); + return res as T[]; + } + else if (typeof(T) == typeof(double)) + { + double[] len = new double[1]; + Marshal.Copy(paths, len, 0, 1); + double[] res = new double[(int)len[0]]; + Marshal.Copy(paths, res, 0, (int)len[0]); + return res as T[]; + } + else return null; + } + + // DLL exported function definitions ///////////////////// + + const string clipperDll = @"..\..\..\..\Clipper2_64.dll"; + + [DllImport(clipperDll, EntryPoint = + "Version", CallingConvention = CallingConvention.Cdecl)] + static extern IntPtr Version(); + + [DllImport(clipperDll, EntryPoint = + "BooleanOp64", CallingConvention = CallingConvention.Cdecl)] + static extern Int32 BooleanOp64(byte clipType, byte fillRule, + long[] subjects, long[]? openSubs, long[]? clips, + out IntPtr solution, out IntPtr openSol, bool preserveCollinear, bool reverseSolution); + + [DllImport(clipperDll, EntryPoint = + "BooleanOpD", CallingConvention = CallingConvention.Cdecl)] + static extern Int32 BooleanOpD(byte clipType, byte fillRule, + double[] subjects, double[]? openSubs, double[]? clips, + out IntPtr solution, out IntPtr openSol, Int32 precision, bool preserveCollinear, bool reverseSolution); + + [DllImport(clipperDll, EntryPoint = + "DisposeArray64", CallingConvention = CallingConvention.Cdecl)] + static extern void DisposeArray64(ref IntPtr intptr); + + // DisposeExported(): since all these functions behave identically ... + [DllImport(clipperDll, EntryPoint = + "DisposeArrayD", CallingConvention = CallingConvention.Cdecl)] + static extern void DisposeArrayD(ref IntPtr intptr); + + [DllImport(clipperDll, EntryPoint = + "BooleanOp_PolyTree64", CallingConvention = CallingConvention.Cdecl)] + static extern Int32 BooleanOp_PolyTree64(byte cliptype, + byte fillrule, long[] subjects, long[]? openSubs, long[]? clips, + out IntPtr solTree, out IntPtr openSol, + bool preserve_collinear, bool reverse_solution); + + [DllImport(clipperDll, EntryPoint = + "BooleanOp_PolyTreeD", CallingConvention = CallingConvention.Cdecl)] + static extern Int32 BooleanOp_PolyTreeD(byte cliptype, + byte fillrule, double[] subjects, double[]? openSubs, double[]? clips, + out IntPtr solTree, out IntPtr openSol, Int32 precision, + bool preserve_collinear, bool reverse_solution); + + + public static readonly byte None = 0, Intersection = 1, Union = 2, Difference = 3, Xor = 4; + public static readonly byte EvenOdd = 0, NonZero = 1, Positive = 2, Negative = 3; + + + /// Main Entry //////////////////////////////////////////////////////////// + public static void Main() + { + + //string? ver = Marshal.PtrToStringAnsi(Version()); + //Console.WriteLine(ver +"\n"); + + // test BooleanOp64() /////////////////////////////////////////////////// + Console.WriteLine("BooleanOp64:"); + long[] cSubject = CreateCPaths(new long[] { 0, 0, 100, 0, 100, 100, 0, 100 }); + long[] cClip = CreateCPaths(new long[] { 20, 20, 120, 20, 120, 120, 20, 120 }); + + if (BooleanOp64(Intersection, NonZero, cSubject, + null, cClip, out IntPtr cSol, out IntPtr cSolOpen, false, false) != 0) return; + + long[]? cSolution = GetArrayFromIntPtr(cSol); + // clean up unmanaged memory + DisposeArray64(ref cSol); + DisposeArray64(ref cSolOpen); + + DisplayCPaths(cSolution, " "); + ///////////////////////////////////////////////////////////////////////// + + // test BooleanOpD() //////////////////////////////////////////////////// + Console.WriteLine("BooleanOpD:"); + double[] cSubjectD = CreateCPaths(new double[] { 0, 0, 100, 0, 100, 100, 0, 100 }); + double[] cClipD = CreateCPaths(new double[] { 20, 20, 120, 20, 120, 120, 20, 120 }); + int resultD = BooleanOpD(Intersection, NonZero, cSubjectD, + null, cClipD, out IntPtr cSolD, out IntPtr cSolOpenD, 2, false, false); + if (resultD != 0) return; + double[]? cSolutionD = GetArrayFromIntPtr(cSolD); + // clean up unmanaged memory + DisposeArrayD(ref cSolD); + DisposeArrayD(ref cSolOpenD); + + DisplayCPaths(cSolutionD, " "); + ///////////////////////////////////////////////////////////////////////// + + + + // test BooleanOp_PolyTree64() ////////////////////////////////////////// + Console.WriteLine("BooleanOp_PolyTree64:"); + + List subList = new(5); + for (int i = 1; i < 6; ++i) + subList.Add(CreateCPath(new long[] { + -i * 20, -i * 20, i * 20, -i * 20, i * 20, i * 20, -i * 20, i * 20 })!); + + long[] cSubject3 = CreateCPaths(subList); + long[] cClip3 = CreateCPaths(new long[] { -90, -120, 90, -120, 90, 120, -90, 120 }); + + int result3 = BooleanOp_PolyTree64(Intersection, EvenOdd, cSubject3, null, cClip3, + out IntPtr cSol_pt64, out IntPtr cSolOpen_pt64, false, false); + if (result3 != 0) return; + + long[]? cPolyTree64 = GetArrayFromIntPtr(cSol_pt64); + // clean up unmanaged memory + DisposeArray64(ref cSol_pt64); + DisposeArray64(ref cSolOpen_pt64); + + if (cPolyTree64 == null) return; + DisplayPolytree(cPolyTree64, 2); + ///////////////////////////////////////////////////////////////////////// + + + // test BooleanOp_PolyTreeD() /////////////////////////////////////////// + Console.WriteLine("BooleanOp_PolyTreeD:"); + + List subList2 = new(5); + for (int i = 1; i < 6; ++i) + subList2.Add(CreateCPath(new double[] { + -i * 20, -i * 20, i * 20, -i * 20, i * 20, i * 20, -i * 20, i * 20 })!); + + double[] cSubject4 = CreateCPaths(subList2); + double[] cClip4 = CreateCPaths(new double[] { -90, -120, 90, -120, 90, 120, -90, 120 }); + + int result4 = BooleanOp_PolyTreeD(Intersection, EvenOdd, cSubject4, null, cClip4, + out IntPtr cSol_ptD, out IntPtr cSolOpen_ptD, 2, false, false); + if (result4 != 0) return; + + double[]? cPolyTreeD = GetArrayFromIntPtr(cSol_ptD); + + // clean up unmanaged memory + DisposeArrayD(ref cSol_ptD); + DisposeArrayD(ref cSolOpen_ptD); + + if (cPolyTreeD == null) return; + DisplayPolytree(cPolyTreeD, 2); + ///////////////////////////////////////////////////////////////////////// + + + Console.WriteLine("\nPress any key to exit ... "); + Console.ReadKey(); + } + + } //end Application +} //namespace diff --git a/DLL/CSharp_TestApp/polytree_sample.png b/DLL/CSharp_TestApp/polytree_sample.png new file mode 100644 index 00000000..820eb859 Binary files /dev/null and b/DLL/CSharp_TestApp/polytree_sample.png differ diff --git a/DLL/CSharp_TestApp2/COMPILE AND ADD Clipper2_64.dll HERE b/DLL/CSharp_TestApp2/COMPILE AND ADD Clipper2_64.dll HERE new file mode 100644 index 00000000..e69de29b diff --git a/DLL/CSharp_TestApp2/CSharp_TestApp2.csproj b/DLL/CSharp_TestApp2/CSharp_TestApp2.csproj new file mode 100644 index 00000000..85e03793 --- /dev/null +++ b/DLL/CSharp_TestApp2/CSharp_TestApp2.csproj @@ -0,0 +1,16 @@ + + + + Exe + net6.0 + enable + enable + x64 + + + + + + + + diff --git a/DLL/CSharp_TestApp2/CSharp_TestApp2.sln b/DLL/CSharp_TestApp2/CSharp_TestApp2.sln new file mode 100644 index 00000000..3b598060 --- /dev/null +++ b/DLL/CSharp_TestApp2/CSharp_TestApp2.sln @@ -0,0 +1,63 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.6.33801.468 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CSharp_TestApp2", "CSharp_TestApp2.csproj", "{3D611FB4-7D15-4FD4-9EE4-D02E0D37F424}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Clipper2Lib", "..\..\CSharp\Clipper2Lib\Clipper2Lib.csproj", "{401DBA71-BB90-495E-9F91-CC495FEE264D}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Clipper2.SVG", "..\..\CSharp\Utils\SVG\Clipper2.SVG.csproj", "{77C119ED-E0A4-4A76-B858-E7B4AF4DE892}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Debug|x64 = Debug|x64 + Debug|x86 = Debug|x86 + Release|Any CPU = Release|Any CPU + Release|x64 = Release|x64 + Release|x86 = Release|x86 + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {3D611FB4-7D15-4FD4-9EE4-D02E0D37F424}.Debug|Any CPU.ActiveCfg = Debug|x64 + {3D611FB4-7D15-4FD4-9EE4-D02E0D37F424}.Debug|x64.ActiveCfg = Debug|x64 + {3D611FB4-7D15-4FD4-9EE4-D02E0D37F424}.Debug|x64.Build.0 = Debug|x64 + {3D611FB4-7D15-4FD4-9EE4-D02E0D37F424}.Debug|x86.ActiveCfg = Debug|x64 + {3D611FB4-7D15-4FD4-9EE4-D02E0D37F424}.Debug|x86.Build.0 = Debug|x64 + {3D611FB4-7D15-4FD4-9EE4-D02E0D37F424}.Release|Any CPU.ActiveCfg = Release|x64 + {3D611FB4-7D15-4FD4-9EE4-D02E0D37F424}.Release|x64.ActiveCfg = Release|x64 + {3D611FB4-7D15-4FD4-9EE4-D02E0D37F424}.Release|x64.Build.0 = Release|x64 + {3D611FB4-7D15-4FD4-9EE4-D02E0D37F424}.Release|x86.ActiveCfg = Release|x64 + {3D611FB4-7D15-4FD4-9EE4-D02E0D37F424}.Release|x86.Build.0 = Release|x64 + {401DBA71-BB90-495E-9F91-CC495FEE264D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {401DBA71-BB90-495E-9F91-CC495FEE264D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {401DBA71-BB90-495E-9F91-CC495FEE264D}.Debug|x64.ActiveCfg = Debug|Any CPU + {401DBA71-BB90-495E-9F91-CC495FEE264D}.Debug|x64.Build.0 = Debug|Any CPU + {401DBA71-BB90-495E-9F91-CC495FEE264D}.Debug|x86.ActiveCfg = Debug|x86 + {401DBA71-BB90-495E-9F91-CC495FEE264D}.Debug|x86.Build.0 = Debug|x86 + {401DBA71-BB90-495E-9F91-CC495FEE264D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {401DBA71-BB90-495E-9F91-CC495FEE264D}.Release|Any CPU.Build.0 = Release|Any CPU + {401DBA71-BB90-495E-9F91-CC495FEE264D}.Release|x64.ActiveCfg = Release|Any CPU + {401DBA71-BB90-495E-9F91-CC495FEE264D}.Release|x64.Build.0 = Release|Any CPU + {401DBA71-BB90-495E-9F91-CC495FEE264D}.Release|x86.ActiveCfg = Release|x86 + {401DBA71-BB90-495E-9F91-CC495FEE264D}.Release|x86.Build.0 = Release|x86 + {77C119ED-E0A4-4A76-B858-E7B4AF4DE892}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {77C119ED-E0A4-4A76-B858-E7B4AF4DE892}.Debug|Any CPU.Build.0 = Debug|Any CPU + {77C119ED-E0A4-4A76-B858-E7B4AF4DE892}.Debug|x64.ActiveCfg = Debug|Any CPU + {77C119ED-E0A4-4A76-B858-E7B4AF4DE892}.Debug|x64.Build.0 = Debug|Any CPU + {77C119ED-E0A4-4A76-B858-E7B4AF4DE892}.Debug|x86.ActiveCfg = Debug|x86 + {77C119ED-E0A4-4A76-B858-E7B4AF4DE892}.Debug|x86.Build.0 = Debug|x86 + {77C119ED-E0A4-4A76-B858-E7B4AF4DE892}.Release|Any CPU.ActiveCfg = Release|Any CPU + {77C119ED-E0A4-4A76-B858-E7B4AF4DE892}.Release|Any CPU.Build.0 = Release|Any CPU + {77C119ED-E0A4-4A76-B858-E7B4AF4DE892}.Release|x64.ActiveCfg = Release|Any CPU + {77C119ED-E0A4-4A76-B858-E7B4AF4DE892}.Release|x64.Build.0 = Release|Any CPU + {77C119ED-E0A4-4A76-B858-E7B4AF4DE892}.Release|x86.ActiveCfg = Release|x86 + {77C119ED-E0A4-4A76-B858-E7B4AF4DE892}.Release|x86.Build.0 = Release|x86 + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {DAE344E0-A107-49C8-B269-1E1896665E6B} + EndGlobalSection +EndGlobal diff --git a/DLL/CSharp_TestApp2/Program.cs b/DLL/CSharp_TestApp2/Program.cs new file mode 100644 index 00000000..845ac998 --- /dev/null +++ b/DLL/CSharp_TestApp2/Program.cs @@ -0,0 +1,184 @@ +/******************************************************************************* +* Author : Angus Johnson * +* Date : 26 October 2023 * +* Website : http://www.angusj.com * +* Copyright : Angus Johnson 2010-2023 * +* License : http://www.boost.org/LICENSE_1_0.txt * +*******************************************************************************/ + +using System.Diagnostics; +using System.Runtime.InteropServices; +using Clipper2Lib; + +namespace ClipperDllDemo +{ + public class Application + { + + // Define miscellaneous functions //////////////////////////// + public static void OpenFileWithDefaultApp(string filename) + { + string path = Path.GetFullPath(filename); + if (!File.Exists(path)) return; + Process p = new() { StartInfo = new ProcessStartInfo(path) { UseShellExecute = true } }; + p.Start(); + } + + public static Path64 MakePath(int[] arr) + { + int len = arr.Length / 2; + Path64 p = new(len); + for (int i = 0; i < len; i++) + p.Add(new Point64(arr[i * 2], arr[i * 2 + 1])); + return p; + } + + private static Point64 MakeRandomPt(int maxWidth, int maxHeight, Random rand) + { + long x = rand.Next(maxWidth); + long y = rand.Next(maxHeight); + return new Point64(x, y); + } + + public static Path64 MakeRandomPath(int width, int height, int count, Random rand) + { + Path64 result = new(count); + for (int i = 0; i < count; ++i) + result.Add(MakeRandomPt(width, height, rand)); + return result; + } + + static Path64 GetPath64FromCPath(long[] cpaths, ref int idx) + { + int cnt = (int)cpaths[idx]; idx += 2; + Path64 result = new(cnt); + for (int i = 0; i < cnt; i++) + { + long x = cpaths[idx++]; + long y = cpaths[idx++]; + result.Add(new Point64(x, y)); + } + return result; + } + + static Paths64 GetPaths64FromCPaths(long[] cpaths) + { + int cnt = (int)cpaths[1], idx = 2; + Paths64 result = new(cnt); + for (int i = 0; i < cnt; i++) + result.Add(GetPath64FromCPath(cpaths, ref idx)); + return result; + } + + static long[] CreateCPaths64(Paths64 pp) + { + int len = pp.Count, len2 = 2; + for (int i = 0; i < len; i++) + if (pp[i].Count > 0) + len2 += pp[i].Count * 2 + 2; + long[] result = new long[len2]; + result[0] = 0; + result[1] = len; + int rPos = 2; + for (int i = 0; i < len; i++) + { + len2 = pp[i].Count; + if (len2 == 0) continue; + result[rPos++] = len2; + result[rPos++] = 0; + for (int j = 0; j < len2; j++) + { + result[rPos++] = pp[i][j].X; + result[rPos++] = pp[i][j].Y; + } + } + return result; + } + + public static long[]? GetPathsFromIntPtr(IntPtr paths) + { + if (paths == IntPtr.Zero) return null; + long[] len = new long[1]; + Marshal.Copy(paths, len, 0, 1); + long[] result = new long[len[0]]; + Marshal.Copy(paths, result, 0, (int)len[0]); + return result; + } + + // Define DLL exported functions ///////////////////// + + public const string clipperDll = @"..\..\..\..\Clipper2_64.dll"; + + [DllImport(clipperDll, EntryPoint = "Version", CallingConvention = CallingConvention.Cdecl)] + static extern IntPtr Version(); + + [DllImport(clipperDll, EntryPoint = "BooleanOp64", CallingConvention = CallingConvention.Cdecl)] + static extern int BooleanOp64(byte clipType, byte fillRule, + long[] subject, long[]? subOpen, long[]? clip, + out IntPtr solution, out IntPtr solOpen, bool preserveCollinear, bool reverseSolution); + + [DllImport(clipperDll, EntryPoint = "DisposeArray64", CallingConvention = CallingConvention.Cdecl)] + static extern void DisposeArray64(ref IntPtr paths); + + static readonly byte None = 0, Intersection = 1, Union = 2, Difference = 3, Xor = 4; + static readonly byte EvenOdd = 0, NonZero = 1, Positive = 2, Negative = 3; + + public static void Main() + { + + //string? ver = Marshal.PtrToStringAnsi(Version()); + //Console.WriteLine(ver + "\n"); + + long timeMsec; + Random rand = new(); + + //////////////////////////////////////////////////////////////////////// + int edgeCount = 2500; + //////////////////////////////////////////////////////////////////////// + + Paths64 subject = new() { MakeRandomPath(600,400, edgeCount, rand)}; + Paths64 clip = new() { MakeRandomPath(600, 400, edgeCount, rand) }; + + ////////////////////////////////////////////////////////////////////// + // Use Dynamically Linked C++ compiled library (ie use the DLL) + // NB: time will include ALL the overhead of swapping path structures + Stopwatch sw1 = Stopwatch.StartNew(); + long[] cSubject = CreateCPaths64(subject); + long[] cClip = CreateCPaths64(clip); + if (BooleanOp64(Intersection, NonZero, cSubject, + null, cClip, out IntPtr cSol, out IntPtr cSolOpen, false, false) != 0) + return; + + long[]? cSolution = GetPathsFromIntPtr(cSol); + if (cSolution == null) return; + DisposeArray64(ref cSol); + DisposeArray64(ref cSolOpen); + Paths64 solution = GetPaths64FromCPaths(cSolution); + sw1.Stop(); + timeMsec = sw1.ElapsedMilliseconds; + Console.WriteLine($"Time using DLL (C++ code): {timeMsec} ms"); + ////////////////////////////////////////////////////////////////////// + + ////////////////////////////////////////////////////////////////////// + // Use Clipper2's statically linked C# compiled library + Stopwatch sw2 = Stopwatch.StartNew(); + Clipper.Intersect(subject, clip, FillRule.NonZero); + sw2.Stop(); + timeMsec = sw2.ElapsedMilliseconds; + Console.WriteLine($"Time using C# code : {timeMsec} ms"); + ////////////////////////////////////////////////////////////////////// + + string fileName = "../../../clipper2_dll.svg"; + SvgWriter svg = new(FillRule.NonZero); + SvgUtils.AddSubject(svg, subject); + SvgUtils.AddClip(svg, clip); + SvgUtils.AddSolution(svg, solution, false); + svg.SaveToFile(fileName, 800, 600, 20); + OpenFileWithDefaultApp(fileName); + + Console.WriteLine("Press any key to exit ... "); + Console.ReadKey(); + } + + } //end Application +} //namespace diff --git a/DLL/Delphi_TestApp/COMPILE AND ADD Clipper2_64.dll HERE b/DLL/Delphi_TestApp/COMPILE AND ADD Clipper2_64.dll HERE new file mode 100644 index 00000000..e69de29b diff --git a/DLL/Delphi_TestApp/Test_DLL.dpr b/DLL/Delphi_TestApp/Test_DLL.dpr index ea936adb..7869bd03 100644 --- a/DLL/Delphi_TestApp/Test_DLL.dpr +++ b/DLL/Delphi_TestApp/Test_DLL.dpr @@ -13,42 +13,25 @@ uses SysUtils, Clipper in '..\..\Delphi\Clipper2Lib\Clipper.pas', Clipper.Core in '..\..\Delphi\Clipper2Lib\Clipper.Core.pas', + Clipper.Engine in '..\..\Delphi\Clipper2Lib\Clipper.Engine.pas', Clipper.SVG in '..\..\Delphi\Utils\Clipper.SVG.pas', Colors in '..\..\Delphi\Utils\Colors.pas', Timer in '..\..\Delphi\Utils\Timer.pas'; type - - CInt64arr = array[0..$FFFF] of Int64; - CPath64 = ^CInt64arr; - CPath64arr = array[0..$FFFF] of CPath64; - CPaths64 = ^CPath64arr; - CDblarr = array[0..$FFFF] of Double; - CPathD = ^CDblarr; - CPathDarr = array[0..$FFFF] of CPathD; - CPathsD = ^CPathDarr; - - - // nb: Pointer sizes could be 32bit or 64 bits - // and this will depend on how the DLL was compiled - // Obviously, DLLs compiled for 64bits won't work with - // applications compiled for 32bits (and vice versa). - - PCPolyTree64 = ^CPolyTree64; - CPolyTree64 = packed record - polygon : CPath64; //pointer (32bit or 64bit) - isHole : LongBool; //32 bits - childCnt : Int32; //32 bits - childs : PCPolyTree64; //pointer (32bit or 64bit) - end; - - PCPolyTreeD = ^CPolyTreeD; - CPolyTreeD = packed record - polygon : CPathD; //pointer (32bit or 64bit) - isHole : LongBool; //32 bits - childCnt : Int32; //32 bits - childs : PCPolyTreeD; //pointer (32bit or 64bit) - end; + CInt64arr = array[0..$FFFF] of Int64; + PCInt64arr = ^CInt64arr; + CPath64 = PCInt64arr; + CPaths64 = PCInt64arr; + CPolyPath64 = PCInt64arr; + CPolytree64 = PCInt64arr; + + CDblarr = array[0..$FFFF] of Double; + PCDblarr = ^CDblarr; + CPathD = PCDblarr; + CPathsD = PCDblarr; + CPolyPathD = PCDblarr; + CPolytreeD = PCDblarr; const {$IFDEF WIN64} @@ -57,6 +40,7 @@ const CLIPPER2_DLL = 'Clipper2_32.dll'; {$ENDIF} + //////////////////////////////////////////////////////// // Clipper2 DLL functions //////////////////////////////////////////////////////// @@ -64,18 +48,10 @@ const function Version(): PAnsiChar; cdecl; external CLIPPER2_DLL name 'Version'; -procedure DisposeExportedCPath64(cp: CPath64); cdecl; - external CLIPPER2_DLL name 'DisposeExportedCPath64'; -procedure DisposeExportedCPaths64(var cps: CPaths64); cdecl; - external CLIPPER2_DLL name 'DisposeExportedCPaths64'; -procedure DisposeExportedCPathD(cp: CPathD); cdecl; - external CLIPPER2_DLL name 'DisposeExportedCPathD'; -procedure DisposeExportedCPathsD(var cp: CPathsD); cdecl; - external CLIPPER2_DLL name 'DisposeExportedCPathsD'; -procedure DisposeExportedCPolyTree64(var cpt: PCPolyTree64); cdecl; - external CLIPPER2_DLL name 'DisposeExportedCPolyTree64'; -procedure DisposeExportedCPolyTreeD(var cpt: PCPolyTreeD); cdecl; - external CLIPPER2_DLL name 'DisposeExportedCPolyTreeD'; +procedure DisposeExportedArray64(var cps: PCInt64arr); cdecl; + external CLIPPER2_DLL name 'DisposeArray64'; +procedure DisposeExportedArrayD(var cp: PCDblarr); cdecl; + external CLIPPER2_DLL name 'DisposeArrayD'; function BooleanOp64(cliptype: UInt8; fillrule: UInt8; const subjects: CPaths64; const subjects_open: CPaths64; @@ -84,13 +60,13 @@ function BooleanOp64(cliptype: UInt8; fillrule: UInt8; preserve_collinear: boolean = true; reverse_solution: boolean = false): integer; cdecl; external CLIPPER2_DLL name 'BooleanOp64'; -function BooleanOpPt64(cliptype: UInt8; fillrule: UInt8; +function BooleanOp_PolyTree64(cliptype: UInt8; fillrule: UInt8; const subjects: CPaths64; const subjects_open: CPaths64; - const clips: CPaths64; out solution: PCPolyTree64; + const clips: CPaths64; out solution: CPolyTree64; out solution_open: CPaths64; preserve_collinear: boolean = true; reverse_solution: boolean = false): integer; cdecl; - external CLIPPER2_DLL name 'BooleanOpPt64'; + external CLIPPER2_DLL name 'BooleanOp_PolyTree64'; function BooleanOpD(cliptype: UInt8; fillrule: UInt8; const subjects: CPathsD; const subjects_open: CPathsD; @@ -99,13 +75,13 @@ function BooleanOpD(cliptype: UInt8; fillrule: UInt8; preserve_collinear: boolean = true; reverse_solution: boolean = false): integer; cdecl; external CLIPPER2_DLL name 'BooleanOpD'; -function BooleanOpPtD(cliptype: UInt8; fillrule: UInt8; +function BooleanOp_PolyTreeD(cliptype: UInt8; fillrule: UInt8; const subjects: CPathsD; const subjects_open: CPathsD; - const clips: CPathsD; out solution: PCPolyTreeD; out solution_open: CPathsD; + const clips: CPathsD; out solution: CPolyTreeD; out solution_open: CPathsD; precision: integer = 2; preserve_collinear: boolean = true; reverse_solution: boolean = false): integer; cdecl; - external CLIPPER2_DLL name 'BooleanOpPtD'; + external CLIPPER2_DLL name 'BooleanOp_PolyTreeD'; function InflatePaths64(const paths: CPaths64; delta: double; jointype, endtype: UInt8; miter_limit: double = 2.0; arc_tolerance: double = 0.0; @@ -117,261 +93,266 @@ function InflatePathsD(const paths: CPathsD; reverse_solution: Boolean = false): CPathsD; cdecl; external CLIPPER2_DLL name 'InflatePathsD'; -function ExecuteRectClip64(const rect: TRect64; const paths: CPaths64; +function RectClip64(const rect: TRect64; const paths: CPaths64; convexOnly: Boolean = false): CPaths64; cdecl; - external CLIPPER2_DLL name 'ExecuteRectClip64'; -function ExecuteRectClipD(const rect: TRectD; const paths: CPathsD; + external CLIPPER2_DLL name 'RectClip64'; +function RectClipD(const rect: TRectD; const paths: CPathsD; precision: integer = 2; convexOnly: Boolean = false): CPathsD; cdecl; - external CLIPPER2_DLL name 'ExecuteRectClipD'; -function ExecuteRectClipLines64(const rect: TRect64; + external CLIPPER2_DLL name 'RectClipD'; +function RectClipLines64(const rect: TRect64; const paths: CPaths64): CPaths64; cdecl; - external CLIPPER2_DLL name 'ExecuteRectClipLines64'; -function ExecuteRectClipLinesD(const rect: TRectD; + external CLIPPER2_DLL name 'RectClipLines64'; +function RectClipLinesD(const rect: TRectD; const paths: CPathsD; precision: integer = 2): CPathsD; cdecl; - external CLIPPER2_DLL name 'ExecuteRectClipLinesD'; + external CLIPPER2_DLL name 'RectClipLinesD'; + +const + Intersection = 1; Union = 2; Difference =3; Xor_ = 4; + EvenOdd = 0; NonZero = 1; Positive = 2; Negative = 3; //////////////////////////////////////////////////////// // functions related to Clipper2 DLL structures //////////////////////////////////////////////////////// -procedure DisposeLocalCPath64(cp: CPath64); +procedure DisposeLocalArray64(cp: PCInt64arr); begin FreeMem(cp); end; -procedure DisposeLocalCPaths64(var cps: CPaths64); -var - i, cnt: integer; -begin - if cps = nil then Exit; - cnt := cps[0][1]; - for i := 0 to cnt do //cnt +1 - FreeMem(cps[i]); - FreeMem(cps); - cps := nil; -end; - -procedure DisposeLocalCPathD(cp: CPathD); +procedure DisposeLocalArrayD(cp: PCDblarr); begin FreeMem(cp); end; -procedure DisposeLocalCPathsD(var cps: CPathsD); -var - i, cnt: integer; -begin - if not Assigned(cps) then Exit; - cnt := Round(cps[0][1]); - for i := 0 to cnt do //cnt +1 - FreeMem(cps[i]); - FreeMem(cps); - cps := nil; -end; - //////////////////////////////////////////////////////// -// conversion functions +// path format conversion functions //////////////////////////////////////////////////////// -function TPath64ToCPath64(const path: TPath64): CPath64; +function CreateCPaths64(const pp: TPaths64): CPaths64; var - i, len: integer; + i,j, len, len2: integer; + v: PInt64; begin - len := Length(path); - GetMem(Result, (2 + len * 2) * sizeof(Int64)); - Result[0] := len; - Result[1] := 0; + len := Length(pp); + len2 := 2; + for i := 0 to len -1 do + if Length(pp[i]) > 0 then + inc(len2, Length(pp[i]) *2 + 2); + GetMem(Result, len2 * sizeof(Int64)); + Result[0] := len2; + Result[1] := len; + v := @Result[2]; for i := 0 to len -1 do begin - Result[2 + i*2] := path[i].X; - Result[3 + i*2] := path[i].Y; + len2 := Length(pp[i]); + if len2 = 0 then continue; + v^ := len2; inc(v); + v^ := 0; inc(v); + for j := 0 to len2 -1 do + begin + v^ := pp[i][j].X; inc(v); + v^ := pp[i][j].Y; inc(v); + end; end; end; -function TPathDToCPathD(const path: TPathD): CPathD; +function CreateCPathsD(const pp: TPathsD): CPathsD; var - i, len: integer; + i,j, len, len2: integer; + v: PDouble; begin - len := Length(path); - GetMem(Result, (2 + len * 2) * sizeof(Double)); - Result[0] := len; - Result[1] := 0; + len := Length(pp); + len2 := 2; + for i := 0 to len -1 do + if Length(pp[i]) > 0 then + inc(len2, Length(pp[i]) *2 + 2); + GetMem(Result, len2 * sizeof(double)); + Result[0] := len2; + Result[1] := len; + v := @Result[2]; for i := 0 to len -1 do begin - Result[2 + i*2] := path[i].X; - Result[3 + i*2] := path[i].Y; + len2 := Length(pp[i]); + if len2 = 0 then continue; + v^ := len2; inc(v); + v^ := 0; inc(v); + for j := 0 to len2 -1 do + begin + v^ := pp[i][j].X; inc(v); + v^ := pp[i][j].Y; inc(v); + end; end; end; -function CPath64Cntrs(cnt1, cnt2: Int64): CPath64; -begin - GetMem(Result, 2 * sizeof(Int64)); - Result[0] := cnt1; - Result[1] := cnt2; -end; - -function CPathDCntrs(cnt1, cnt2: integer): CPathD; -begin - GetMem(Result, 2 * sizeof(double)); - Result[0] := cnt1; - Result[1] := cnt2; -end; - -function TPaths64ToCPaths64(const pp: TPaths64): CPaths64; +function ConvertToTPaths64(cp: CPaths64): TPaths64; var - i,j, len, len2: integer; + i, j, len, len2: integer; + v: PInt64; begin - len := Length(pp); - len2 := len; - for i := 0 to len -1 do - if Length(pp[i]) = 0 then - dec(len2); - GetMem(Result, (1 + len2) * sizeof(Pointer)); - Result[0] := CPath64Cntrs(0, len2); // add the counter 'path' - j := 1; + Result := nil; + v := PInt64(cp); + inc(v); // ignore array length + len := v^; inc(v); + SetLength(Result, len); for i := 0 to len -1 do begin - if Length(pp[i]) = 0 then continue; - Result[j] := TPath64ToCPath64(pp[i]); - inc(j); + len2 := v^; inc(v, 2); + SetLength(Result[i], len2); + for j := 0 to len2 -1 do + begin + Result[i][j].X := v^; inc(v); + Result[i][j].Y := v^; inc(v); + end; end; end; -function TPathsDToCPathsD(const pp: TPathsD): CPathsD; +function ConvertToTPathsD(cp: CPathsD): TPathsD; var - i,j, len, len2: integer; + i, j, len, len2: integer; + v: PDouble; begin - len := Length(pp); - len2 := len; - for i := 0 to len -1 do - if Length(pp[i]) = 0 then - dec(len2); - - GetMem(Result, (1 + len2) * sizeof(Pointer)); - Result[0] := CPathDCntrs(0, len2); // add the counter 'path' - j := 1; + Result := nil; + v := PDouble(cp); + inc(v); // ignore array length + len := Round(cp[1]); inc(v); + SetLength(Result, len); for i := 0 to len -1 do begin - if Length(pp[i]) = 0 then continue; - Result[j] := TPathDToCPathD(pp[i]); - inc(j); + len2 := Round(v^); inc(v, 2); + SetLength(Result[i], len2); + for j := 0 to len2 -1 do + begin + Result[i][j].X := v^; inc(v); + Result[i][j].Y := v^; inc(v); + end; end; end; -function CPath64ToPath64(cp: CPath64): TPath64; +function GetPolyPath64ArrayLen(const pp: TPolyPath64): integer; var i: integer; - cnt: Int64; begin - if not Assigned(cp) then - cnt := 0 else - cnt := cp[0]; - SetLength(Result, cnt); - for i := 0 to cnt -1 do - begin - Result[i].X := cp[2 + i*2]; - Result[i].Y := cp[3 + i*2]; - end; + Result := 2; // poly_length + child_count + inc(Result, Length(pp.Polygon) * 2); + for i := 0 to pp.Count -1 do + Inc(Result, GetPolyPath64ArrayLen(pp.Child[i])); end; -function CPathDToPathD(cp: CPathD): TPathD; -var - i, cnt: integer; +procedure GetPolytreeCountAndCStorageSize(const tree: TPolyTree64; + out cnt: integer; out arrayLen: integer); begin - if not Assigned(cp) then - cnt := 0 else - cnt := Round(cp[0]); - SetLength(Result, cnt); - for i := 0 to cnt -1 do - begin - Result[i].X := cp[2 + i*2]; - Result[i].Y := cp[3 + i*2]; - end; + cnt := tree.Count; // nb: top level count only + arrayLen := GetPolyPath64ArrayLen(tree); end; -function CPaths64ToPaths64(cps: CPaths64): TPaths64; +procedure CreateCPolyPathD(const pp: TPolyPath64; + var v: PDouble; scale: double); var - i: integer; - cnt: Int64; + i, len: integer; begin - if not Assigned(cps) then - cnt := 0 else - cnt := cps[0][1]; - SetLength(Result, cnt); - for i := 1 to cnt do - Result[i-1] := CPath64ToPath64(cps[i]); + len := Length(pp.Polygon); + v^ := len; inc(v); + v^ := pp.Count; inc(v); + for i := 0 to len -1 do + begin + v^ := pp.Polygon[i].x * scale; + v^ := pp.Polygon[i].y * scale; + end; + for i := 0 to pp.Count -1 do + CreateCPolyPathD(pp.Child[i], v, scale); end; -function CPathsDToPathsD(cps: CPathsD): TPathsD; + +function CreateCPolyTreeD(const tree: TPolyTree64; scale: double): CPolyTreeD; var - i, cnt: integer; + i, cnt, arrayLen: integer; + v: PDouble; begin - if not Assigned(cps) then - cnt := 0 else - cnt := Round(cps[0][1]); - SetLength(Result, cnt); - for i := 1 to cnt do - Result[i-1] := CPathDToPathD(cps[i]); + Result := nil; + GetPolytreeCountAndCStorageSize(tree, cnt, arrayLen); + if cnt = 0 then Exit; + // allocate storage + GetMem(Result, arrayLen * SizeOf(double)); + + v := PDouble(Result); + v^ := arrayLen; inc(v); + v^ := tree.Count; inc(v); + for i := 0 to tree.Count - 1 do + CreateCPolyPathD(tree.Child[i], v, scale); end; -procedure CPt64Internal(cpt: PCPolyTree64; var paths: TPaths64); +function CreatePolyPath64FromCPolyPath(var v: PInt64; owner: TPolyPath64): Boolean; var - i: integer; - child: PCPolyTree64; + i, childCount, len: integer; + path: TPath64; + newOwner: TPolyPath64; begin - if Assigned(cpt.polygon) then - AppendPath(paths, CPath64ToPath64(cpt.polygon)); - if cpt.childCnt = 0 then Exit; - child := cpt.childs; - for i := 0 to cpt.childCnt -1 do + Result := false; + len := v^; inc(v); //polygon length + childCount := v^; inc(v); + if (len = 0) then Exit; + SetLength(path, len); + for i := 0 to len -1 do begin - CPt64Internal(child, paths); - inc(child); + path[i].X := v^; inc(v); + path[i].Y := v^; inc(v); end; + newOwner := TPolyPath64(owner.AddChild(path)); + for i := 0 to childCount -1 do + if not CreatePolyPath64FromCPolyPath(v, newOwner) then Exit; + Result := true; end; -function CPolytree64ToPaths64(cpt: PCPolyTree64): TPaths64; +function BuildPolyTree64FromCPolyTree(tree: CPolyTree64; outTree: TPolyTree64): Boolean; var - i: integer; - child: PCPolyTree64; + v: PInt64; + i, childCount: integer; begin - Result := nil; - if not Assigned(cpt) or (cpt.childCnt = 0) then Exit; - child := cpt.childs; - for i := 0 to cpt.childCnt -1 do - begin - CPt64Internal(child, Result); - inc(child); - end; + Result := false; + outTree.Clear(); + v := PInt64(tree); + inc(v); //skip array size + childCount := v^; inc(v); + for i := 0 to childCount -1 do + if not CreatePolyPath64FromCPolyPath(v, outTree) then Exit; + Result := true; end; -procedure CPtDInternal(cpt: PCPolyTreeD; var paths: TPathsD); +function CreatePolyPathDFromCPolyPath(var v: PDouble; owner: TPolyPathD): Boolean; var - i: integer; - child: PCPolyTreeD; + i, len, childCount: integer; + path: TPathD; + newOwner: TPolyPathD; begin - AppendPath(paths, CPathDToPathD(cpt.polygon)); - if cpt.childCnt = 0 then Exit; - child := cpt.childs; - for i := 0 to cpt.childCnt -1 do + Result := false; + len := Round(v^); inc(v); + childCount := Round(v^); inc(v); + if (len = 0) then Exit; + SetLength(path, len); + for i := 0 to len -1 do begin - CPtDInternal(child, paths); - inc(child); + path[i].X := v^; inc(v); + path[i].Y := v^; inc(v); end; + newOwner := TPolyPathD(owner.AddChild(path)); + for i := 0 to childCount -1 do + if not CreatePolyPathDFromCPolyPath(v, newOwner) then Exit; + Result := true; end; -function CPolytreeDToPathsD(cpt: PCPolyTreeD): TPathsD; +function BuildPolyTreeDFromCPolyTree(tree: CPolyTreeD; outTree: TPolyTreeD): Boolean; var - i: integer; - child: PCPolyTreeD; + v: PDouble; + i, childCount: integer; begin - Result := nil; - if not Assigned(cpt) or (cpt.childCnt = 0) then Exit; - child := cpt.childs; - for i := 0 to cpt.childCnt -1 do - begin - CPtDInternal(child, Result); - inc(child); - end; + Result := false; + outTree.Clear(); + v := PDouble(tree); + inc(v); // ignore array size + childCount := Round(v^); inc(v); + for i := 0 to childCount -1 do + if not CreatePolyPathDFromCPolyPath(v, outTree) then Exit; + Result := true; end; //////////////////////////////////////////////////////// @@ -430,140 +411,14 @@ begin end; end; -procedure WriteCPath64(p: CPath64); -var - i, len: integer; - s: string; -begin - len := p[0]; - if len = 0 then Exit; - s := ''; - for i := 0 to len -1 do - s := s + - inttostr(p[2 + i*2]) + ',' + - inttostr(p[3 + i*2]) + ', '; - WriteLn(s); -end; - -procedure WriteCPaths64(p: CPaths64); -var - i, len: integer; -begin - len := p[0][1]; - for i := 1 to len do - WriteCPath64(p[i]); - WriteLn(''); -end; - -procedure WritePath64(p: TPath64); -var - i,len: integer; - s: string; -begin - len := Length(p); - if len = 0 then Exit; - s := ''; - for i := 0 to len -1 do - s := s + - inttostr(p[i].X) + ',' + - inttostr(p[i].Y) + ', '; - WriteLn(s); -end; - - -procedure WritePaths64(pp: TPaths64); -var - i: integer; -begin - for i := 0 to High(pp) do - WritePath64(pp[i]); -end; - -procedure WritePathD(p: TPathD); -var - i,len: integer; - s: string; -begin - len := Length(p); - if len = 0 then Exit; - s := ''; - for i := 0 to len -1 do - s := Format('%s%1.2f,%1.2f, ', - [s, p[i].X, p[i].Y]); - WriteLn(s); -end; - -procedure WritePathsD(pp: TPathsD); -var - i: integer; -begin - for i := 0 to High(pp) do - WritePathD(pp[i]); -end; - -procedure WriteCPolyTree64(pp: PCPolyTree64); -var - i: integer; - child: PCPolyTree64; -begin - if Assigned(pp.polygon) then - WriteCPath64(pp.polygon); - if pp.childCnt = 0 then Exit; - child := pp.childs; - for i := 0 to pp.childCnt -1 do - begin - WriteCPolyTree64(child); - inc(child); - end; -end; - -procedure WriteCPathD(p: CPathD); -var - i, len: integer; - s: string; -begin - len := round(p[0]); - if len = 0 then Exit; - s := ''; - for i := 0 to len -1 do - s := Format('%s%1.2f,%1.2f, ', - [s, p[2 + i*2], p[3 + i*2]]); - WriteLn(s); -end; - -procedure WriteCPathsD(pp: CPathsD); -var - i, len: integer; -begin - len := Round(pp[0][1]); - for i := 1 to len do - WriteCPathD(pp[i]); -end; - -procedure WriteCPolyTreeD(pp: PCPolyTreeD); -var - i: integer; - child: PCPolyTreeD; -begin - if Assigned(pp.polygon) then - WriteCPathD(pp.polygon); - if pp.childCnt = 0 then Exit; - child := pp.childs; - for i := 0 to pp.childCnt -1 do - begin - WriteCPolyTreeD(child); - inc(child); - end; -end; - procedure ShowSvgImage(const svgFilename: string); begin ShellExecute(0, 'open',PChar(svgFilename), nil, nil, SW_SHOW); end; const - displayWidth = 800; - displayHeight = 600; + displayWidth = 600; + displayHeight = 400; procedure DisplaySVG(const sub, subo, clp, sol, solo: TPathsD; const svgName: string; width: integer = displayWidth; @@ -596,6 +451,7 @@ begin AddSubject(svg, sub); AddOpenSubject(svg, subo); AddClip(svg, clp); + AddSolution(svg, sol); AddOpenSolution(svg, solo); SaveSvg(svg, svgName, width, height); @@ -609,39 +465,6 @@ end; // test procedures //////////////////////////////////////////////////////// -procedure Test_RandIntersect_MegaStress(maxCnt: integer); -var - i: integer; - sub, clp: TPathsD; - csub_local, cclp_local: CPathsD; - csol_extern, csolo_extern: CPathsD; -begin - csol_extern := nil; - csolo_extern := nil; - SetLength(sub, 1); - SetLength(clp, 1); - - for i := 1 to maxCnt do - begin - sub[0] := MakeRandomPathD(displayWidth, displayHeight, 50); - clp[0] := MakeRandomPathD(displayWidth, displayHeight, 50); - csub_local := TPathsDToCPathsD(sub); - cclp_local := TPathsDToCPathsD(clp); - - BooleanOpD(Uint8(TClipType.ctIntersection), - Uint8(TFillRule.frNonZero), - csub_local, nil, cclp_local, - csol_extern, csolo_extern); - - DisposeLocalCPathsD(csub_local); - DisposeLocalCPathsD(cclp_local); - DisposeExportedCPathsD(csol_extern); - DisposeExportedCPathsD(csolo_extern); - if i mod 100 = 0 then Write('.'); - end; - WriteLn(#10'Passed!'); -end; - procedure Test_Version(); begin Write(#10'Clipper2 DLL version: '); @@ -662,26 +485,22 @@ begin SetLength(clp, 1); clp[0] := MakeRandomPath(displayWidth, displayHeight, edgeCnt); // convert paths into DLL structures (will require local clean up) - csub_local := TPaths64ToCPaths64(sub); - cclp_local := TPaths64ToCPaths64(clp); + csub_local := CreateCPaths64(sub); + cclp_local := CreateCPaths64(clp); // do the DLL operation - BooleanOp64(Uint8(TClipType.ctIntersection), - Uint8(TFillRule.frNonZero), + BooleanOp64(Intersection, NonZero, csub_local, nil, cclp_local, csol_extern, csolo_extern); - // optionally display result on the console - //WriteCPaths64(csol_extern); - DisplaySVG(sub, nil, clp, - CPaths64ToPaths64(csol_extern), nil, 'BooleanOp64.svg'); + ConvertToTPaths64(csol_extern), nil, 'BooleanOp64.svg'); // clean up - DisposeLocalCPaths64(csub_local); - DisposeLocalCPaths64(cclp_local); - DisposeExportedCPaths64(csol_extern); - DisposeExportedCPaths64(csolo_extern); + DisposeLocalArray64(csub_local); + DisposeLocalArray64(cclp_local); + DisposeExportedArray64(csol_extern); + DisposeExportedArray64(csolo_extern); end; procedure Test_BooleanOpD(edgeCnt: integer); @@ -698,8 +517,8 @@ begin SetLength(clp, 1); clp[0] := MakeRandomPathD(displayWidth, displayHeight, edgeCnt); // convert paths into DLL structures (will require local clean up) - csub_local := TPathsDToCPathsD(sub); - cclp_local := TPathsDToCPathsD(clp); + csub_local := CreateCPathsD(sub); + cclp_local := CreateCPathsD(clp); // do the DLL operation BooleanOpD(Uint8(TClipType.ctIntersection), @@ -711,50 +530,90 @@ begin //WriteCPaths64(csol_extern); DisplaySVG(sub, nil, clp, - CPathsDToPathsD(csol_extern), nil, 'BooleanOpD.svg'); + ConvertToTPathsD(csol_extern), nil, 'BooleanOpD.svg'); + + DisposeLocalArrayD(csub_local); + DisposeLocalArrayD(cclp_local); + DisposeExportedArrayD(csol_extern); + DisposeExportedArrayD(csolo_extern); +end; + +procedure Test_BooleanOp_Polytree64(edgeCnt: integer); +var + sub, clp, sol: TPaths64; + csub_local, cclp_local: CPaths64; + csol_extern: CPolyTree64; + tree: TPolyTree64; + csol_open_extern: CPaths64; +begin + // setup + WriteLn(#10'Testing BooleanOp_PolyTree64'); + SetLength(sub, 1); + sub[0] := MakeRandomPath(displayWidth, displayHeight, edgeCnt); + SetLength(clp, 1); + clp[0] := MakeRandomPath(displayWidth, displayHeight, edgeCnt); + // convert paths into DLL structures (will require local clean up) + csub_local := CreateCPaths64(sub); + cclp_local := CreateCPaths64(clp); - DisposeLocalCPathsD(csub_local); - DisposeLocalCPathsD(cclp_local); - DisposeExportedCPathsD(csol_extern); - DisposeExportedCPathsD(csolo_extern); + // do the DLL operation + BooleanOp_PolyTree64(Intersection, NonZero, + csub_local, nil, cclp_local, csol_extern, csol_open_extern); + + tree := TPolyTree64.Create; + try + BuildPolyTree64FromCPolyTree(csol_extern, tree); + sol := PolyTreeToPaths64(tree); + finally + tree.Free; + end; + DisposeExportedArray64(csol_extern); + DisposeExportedArray64(csol_open_extern); + + DisposeLocalArray64(csub_local); + DisposeLocalArray64(cclp_local); + + // finally, display and clean up + DisplaySVG(sub, nil, clp, sol, nil, 'BooleanOp_PolyTree64.svg'); end; -procedure Test_BooleanOpPtD(edgeCnt: integer); +procedure Test_BooleanOp_PolytreeD(edgeCnt: integer); var sub, clp, sol: TPathsD; csub_local, cclp_local: CPathsD; - csol_extern: PCPolyTreeD; - csolo_extern: CPathsD; + csol_extern: CPolyTreeD; + tree: TPolyTreeD; + csol_open_extern: CPathsD; begin // setup - csolo_extern := nil; - WriteLn(#10'Testing BooleanOpPtD'); + WriteLn(#10'Testing BooleanOp_PolyTreeD'); SetLength(sub, 1); sub[0] := MakeRandomPathD(displayWidth, displayHeight, edgeCnt); SetLength(clp, 1); clp[0] := MakeRandomPathD(displayWidth, displayHeight, edgeCnt); // convert paths into DLL structures (will require local clean up) - csub_local := TPathsDToCPathsD(sub); - cclp_local := TPathsDToCPathsD(clp); + csub_local := CreateCPathsD(sub); + cclp_local := CreateCPathsD(clp); // do the DLL operation - BooleanOpPtD(Uint8(TClipType.ctIntersection), - Uint8(TFillRule.frNonZero), - csub_local, nil, cclp_local, - csol_extern, csolo_extern); - - // optionally display result on the console - //WriteCPaths64(csol_extern); - - sol := CPolytreeDToPathsD(csol_extern); - DisposeExportedCPolyTreeD(csol_extern); - DisposeExportedCPathsD(csolo_extern); + BooleanOp_PolyTreeD(Intersection, NonZero, + csub_local, nil, cclp_local, csol_extern, csol_open_extern); + + tree := TPolyTreeD.Create; + try + BuildPolyTreeDFromCPolyTree(csol_extern, tree); + sol := PolyTreeToPathsD(tree); + finally + tree.Free; + end; + DisposeExportedArrayD(csol_extern); + DisposeExportedArrayD(csol_open_extern); - DisposeLocalCPathsD(csub_local); - DisposeLocalCPathsD(cclp_local); + DisposeLocalArrayD(csub_local); + DisposeLocalArrayD(cclp_local); // finally, display and clean up - DisplaySVG(sub, nil, clp, sol, nil, 'BooleanOpPtD.svg'); + DisplaySVG(sub, nil, clp, sol, nil, 'BooleanOp_PolyTreeD.svg'); end; procedure Test_InflatePathsD(edgeCnt: integer; delta: double); @@ -769,7 +628,7 @@ begin SetLength(sub, 1); sub[0] := MakeRandomPathD(displayWidth, displayHeight, edgeCnt); // convert path into required DLL structure (also requires local clean up) - csub_local := TPathsDToCPathsD(sub); + csub_local := CreateCPathsD(sub); // and because offsetting self-intersecting paths is unpredictable // we must remove self-intersection via a union operation @@ -785,101 +644,42 @@ begin //WriteCPaths64(csol_extern); DisplaySVG(sub, nil, nil, - CPathsDToPathsD(csol_extern), nil, 'InflatePaths64.svg'); + ConvertToTPathsD(csol_extern), nil, 'InflatePathsD.svg'); - DisposeLocalCPathsD(csub_local); - DisposeExportedCPathsD(csol_extern); - DisposeExportedCPathsD(csolo_extern); + DisposeLocalArrayD(csub_local); + DisposeExportedArrayD(csol_extern); + DisposeExportedArrayD(csolo_extern); end; -function RotatePath(const path: TPathD; - const focalPoint: TPointD; angle: double): TPathD; +procedure Test_RectClipD(edgeCount: integer); var - i: integer; - sinA, cosA, x,y: double; -begin - SetLength(Result, length(path)); - SinCos(angle, sinA, cosA); - for i := 0 to high(path) do - begin - x := path[i].X - focalPoint.X; - y := path[i].Y - focalPoint.Y; - Result[i].X := x * cosA - y * sinA + focalPoint.X; - Result[i].Y := x * sinA + y * cosA + focalPoint.Y; - end; -end; - - -procedure Test_RectClipD(shapeCount: integer); -var - i, sol2_len, rec_margin: Integer; - sub, clp, sol1, sol2: TPathsD; + rec_margin: Integer; + sub, clp, sol: TPathsD; csub_local: CPathsD; csol_extern: CPathsD; - scaleRnd, maxOffX, maxOffY, frac: Double; rec: TRectD; - fillrule: TFillRule; - shapes: array [0..3] of TPathD; -const - w = 300; - h = 300; begin - // four simple concave polygons - shapes[0] := MakePathD([20,20, 20,0, 40,0, 40,20, 60,20, 60,40, - 40,40, 40,60, 20,60, 20,40, 0,40, 0,20]); - shapes[1] := MakePathD([0,0, 60,0, 60,20, 20,20, 20,40, 60,40, - 60,60, 0,60]); - shapes[2] := MakePathD([0,0, 20,0, 20,20, 40,20, 40,0, 60,0, - 60,60, 40,60, 40,40, 20,40, 20,60, 0,60]); - shapes[3] := MakePathD([20,60, 20,20, 0,20, 0,0, 60,0, 60,20, - 40,20, 40,60]); - - fillrule := frNonZero; + WriteLn(#10'Testing RectClipD:'); - // setup - WriteLn(#10'Testing RectClip64:'); - - rec_margin := Min(w,h) div 3; + rec_margin := Min(displayWidth,displayHeight) div 4; rec.Left := rec_margin; rec.Top := rec_margin; - rec.Right := w - rec_margin; - rec.Bottom := h -rec_margin; - - SetLength(sub, shapeCount); - for i := 0 to shapeCount -1 do - begin - scaleRnd := (60 + Random(w div 4)) / 120; - maxOffX := w - (scaleRnd * 60); - maxOffY := h - (scaleRnd * 60); - sub[i] := ScalePathD(shapes[Random(4)], scaleRnd); - sub[i] := TranslatePath(sub[i], - Random(Round(maxOffX)), Random(Round(maxOffY))); - end; - - csub_local := TPathsDToCPathsD(sub); - csol_extern := ExecuteRectClipD(rec, csub_local, 2, true); - sol1 := CPathsDToPathsD(csol_extern); - DisposeExportedCPathsD(csol_extern); + rec.Right := displayWidth - rec_margin; + rec.Bottom := displayHeight -rec_margin; - // do the DLL operation again with ConvexOnly disabled - csol_extern := ExecuteRectClipD(rec, csub_local, 2, false); - sol2 := CPathsDToPathsD(csol_extern); + SetLength(sub, 1); + sub[0] := MakeRandomPathD(displayWidth, displayHeight, edgeCount); + csub_local := CreateCPathsD(sub); - // display and clean up - sol2_len := Length(sol2); - if sol2_len = 0 then - frac := 0 else - frac := 1/sol2_len; + csol_extern := RectClipD(rec, csub_local, 2, true); + sol := ConvertToTPathsD(csol_extern); + DisposeLocalArrayD(csub_local); + DisposeExportedArrayD(csol_extern); SetLength(clp, 1); clp[0] := rec.AsPath; - - DisplaySVG(sub, nil, clp, nil, nil, 'RectClip64_1.svg', w,h); - DisplaySVG(sub, nil, clp, sol1, nil, 'RectClip64_2.svg', w,h); - DisplaySVG(sub, nil, clp, sol2, nil, 'RectClip64_3.svg', w,h); - - DisposeLocalCPathsD(csub_local); - DisposeExportedCPathsD(csol_extern); + DisplaySVG(sub, nil, clp, sol, + nil, 'RectClipD.svg', displayWidth,displayHeight); end; procedure Test_RectClipLines64(edgeCnt: integer); @@ -894,7 +694,7 @@ begin SetLength(sub, 1); sub[0] := MakeRandomPath(displayWidth, displayHeight, edgeCnt); - csub_local := TPaths64ToCPaths64(sub); + csub_local := CreateCPaths64(sub); rec.Left := 80; rec.Top := 80; @@ -902,91 +702,35 @@ begin rec.Bottom := displayHeight -80; // do the DLL operation - csolo_extern := ExecuteRectClipLines64(rec, csub_local); - - // optionally display result on the console - //WriteCPaths64(csol_extern); - - // finally, display and clean up + csolo_extern := RectClipLines64(rec, csub_local); SetLength(clp, 1); clp[0] := rec.AsPath; DisplaySVG(nil, sub, clp, nil, - CPaths64ToPaths64(csolo_extern), 'RectClipLines64.svg'); - - DisposeLocalCPaths64(csub_local); - DisposeExportedCPaths64(csolo_extern); -end; - -procedure Test_Performance(lowThousand, hiThousand: integer); -var - i: integer; - tr: TTimeRec; - sub, clp: TPaths64; - csub_local, cclp_local: CPaths64; - csol_extern, csolo_extern: CPaths64; -const - w = 800; - h = 600; -begin - csolo_extern := nil; - WriteLn(#10'Testing Performance'); - for i := lowThousand to hiThousand do - begin - Write(format(#10' C++ DLL - %d edges: ', [i*1000])); - // setup - SetLength(sub, 1); - sub[0] := MakeRandomPath(w, h, i*1000); - SetLength(clp, 1); - clp[0] := MakeRandomPath(w, h, i*1000); - // convert paths into DLL structures (will require local clean up) - csub_local := TPaths64ToCPaths64(sub); - cclp_local := TPaths64ToCPaths64(clp); - - StartTimer(tr); - // do the DLL operation - BooleanOp64(Uint8(TClipType.ctIntersection), - Uint8(TFillRule.frNonZero), - csub_local, nil, cclp_local, - csol_extern, csolo_extern); - WriteLn(format('%1.3n secs', [EndTimer(tr)])); - - Write(format(' Pure delphi - %d edges: ', [i*1000])); - StartTimer(tr); - Clipper.Intersect(sub, clp, Clipper.frNonZero); - WriteLn(format('%1.3n secs', [EndTimer(tr)])); - - if i = hiThousand then - DisplaySVG(sub, nil, clp, CPaths64ToPaths64(csol_extern), nil, 'Performance.svg'); + ConvertToTPaths64(csolo_extern), 'RectClipLines64.svg'); - DisposeLocalCPaths64(csub_local); - DisposeLocalCPaths64(cclp_local); - DisposeExportedCPaths64(csol_extern); - DisposeExportedCPaths64(csolo_extern); - - end; //bottom of for loop + DisposeLocalArray64(csub_local); + DisposeExportedArray64(csolo_extern); end; - //////////////////////////////////////////////////////// // main entry here //////////////////////////////////////////////////////// -var - s: string; +//var +// s: string; begin Randomize; Test_Version(); - Test_BooleanOp64(50); - Test_BooleanOpD(75); - Test_BooleanOpPtD(20); + Test_BooleanOp64(25); + Test_BooleanOpD(25); + Test_BooleanOp_Polytree64(15); + Test_BooleanOp_PolytreeD(25); Test_InflatePathsD(20, -10); // edgeCount, offsetDist - Test_RectClipD(15); + Test_RectClipD(7); Test_RectClipLines64(25); - Test_Performance(1, 5); // 1000 to 5000 - Test_RandIntersect_MegaStress(10000); - WriteLn(#10'Press Enter to quit.'); - ReadLn(s); +// WriteLn(#10'Press Enter to quit.'); +// ReadLn(s); end. diff --git a/DLL/ReadMe.md b/DLL/ReadMe.md new file mode 100644 index 00000000..13ce205c --- /dev/null +++ b/DLL/ReadMe.md @@ -0,0 +1,7 @@ +# Clipper2 DLL + +The Clipper2 library is written in several languages (C++, C# & Delphi Pascal) and can be compiled and statically linked into applications using those languages. Now developers using other programming languages can also access almost all of Clipper2's features by dynamically linking to a C++ compiled library. Exported functions and their related data structures are well documented in [clipper.export.h](https://github.com/AngusJohnson/Clipper2/blob/main/CPP/Clipper2Lib/include/clipper2/clipper.export.h). And the latest precompiled DLLs can be found in [Clipper2's Releases](https://github.com/AngusJohnson/Clipper2/releases). + +This folder contains: +1. C++ code that compiles **Clipper2** into Windows DLLs (both 32bit and 64bit). +2. Several sample applications that demonstrate how to use these DLLs in non-C++ languages. diff --git a/Delphi/Clipper2Lib/Clipper.Core.pas b/Delphi/Clipper2Lib/Clipper.Core.pas index 09a696c0..1ed1e243 100644 --- a/Delphi/Clipper2Lib/Clipper.Core.pas +++ b/Delphi/Clipper2Lib/Clipper.Core.pas @@ -2,7 +2,7 @@ (******************************************************************************* * Author : Angus Johnson * -* Date : 19 February 2023 * +* Date : 5 November 2023 * * Website : http://www.angusj.com * * Copyright : Angus Johnson 2010-2023 * * Purpose : Core Clipper Library module * @@ -64,6 +64,7 @@ TPointD = record function GetWidth: Int64; {$IFDEF INLINING} inline; {$ENDIF} function GetHeight: Int64; {$IFDEF INLINING} inline; {$ENDIF} function GetIsEmpty: Boolean; {$IFDEF INLINING} inline; {$ENDIF} + function GetIsValid: Boolean; {$IFDEF INLINING} inline; {$ENDIF} function GetMidPoint: TPoint64; {$IFDEF INLINING} inline; {$ENDIF} public Left : Int64; @@ -78,6 +79,7 @@ TPointD = record property Width: Int64 read GetWidth; property Height: Int64 read GetHeight; property IsEmpty: Boolean read GetIsEmpty; + property IsValid: Boolean read GetIsValid; property MidPoint: TPoint64 read GetMidPoint; end; @@ -86,6 +88,7 @@ TPointD = record function GetWidth: double; {$IFDEF INLINING} inline; {$ENDIF} function GetHeight: double; {$IFDEF INLINING} inline; {$ENDIF} function GetIsEmpty: Boolean; {$IFDEF INLINING} inline; {$ENDIF} + function GetIsValid: Boolean; {$IFDEF INLINING} inline; {$ENDIF} function GetMidPoint: TPointD; {$IFDEF INLINING} inline; {$ENDIF} public Left : double; @@ -99,6 +102,7 @@ TPointD = record property Width: double read GetWidth; property Height: double read GetHeight; property IsEmpty: Boolean read GetIsEmpty; + property IsValid: Boolean read GetIsValid; property MidPoint: TPointD read GetMidPoint; end; @@ -120,6 +124,7 @@ TListEx = class protected function UnsafeGet(idx: integer): Pointer; // no range checking procedure UnsafeSet(idx: integer; val: Pointer); + procedure UnsafeDelete(index: integer); virtual; public constructor Create(capacity: integer = 0); virtual; destructor Destroy; override; @@ -334,6 +339,7 @@ procedure CheckPrecisionRange(var precision: integer); const MaxInt64 = 9223372036854775807; + MinInt64 = -MaxInt64; MaxCoord = MaxInt64 div 4; MinCoord = - MaxCoord; invalid64 = MaxInt64; @@ -345,8 +351,16 @@ procedure CheckPrecisionRange(var precision: integer); InvalidPtD : TPointD = (X: invalidD; Y: invalidD); NullRectD : TRectD = (left: 0; top: 0; right: 0; Bottom: 0); + InvalidRect64 : TRect64 = + (left: invalid64; top: invalid64; right: invalid64; bottom: invalid64); + InvalidRectD : TRectD = + (left: invalidD; top: invalidD; right: invalidD; bottom: invalidD); + Tolerance : Double = 1.0E-12; + //https://github.com/AngusJohnson/Clipper2/discussions/564 + MaxDecimalPrecision = 8; + implementation resourcestring @@ -374,6 +388,12 @@ function TRect64.GetIsEmpty: Boolean; end; //------------------------------------------------------------------------------ +function TRect64.GetIsValid: Boolean; +begin + result := left <> invalid64; +end; +//------------------------------------------------------------------------------ + function TRect64.GetMidPoint: TPoint64; begin result := Point64((Left + Right) div 2, (Top + Bottom) div 2); @@ -446,6 +466,12 @@ function TRectD.GetIsEmpty: Boolean; end; //------------------------------------------------------------------------------ +function TRectD.GetIsValid: Boolean; +begin + result := left <> invalidD; +end; +//------------------------------------------------------------------------------ + function TRectD.GetMidPoint: TPointD; begin result := PointD((Left + Right) *0.5, (Top + Bottom) *0.5); @@ -608,6 +634,14 @@ procedure TListEx.UnsafeSet(idx: integer; val: Pointer); end; //------------------------------------------------------------------------------ +procedure TListEx.UnsafeDelete(index: integer); +begin + dec(fCount); + if index < fCount then + Move(fList[index +1], fList[index], (fCount - index) * SizeOf(Pointer)); +end; +//------------------------------------------------------------------------------ + procedure TListEx.Swap(idx1, idx2: integer); var p: Pointer; @@ -623,7 +657,7 @@ procedure TListEx.Swap(idx1, idx2: integer); procedure CheckPrecisionRange(var precision: integer); begin - if (precision < -8) or (precision > 8) then + if (precision < -MaxDecimalPrecision) or (precision > MaxDecimalPrecision) then Raise EClipper2LibException(rsClipper_PrecisonErr); end; //------------------------------------------------------------------------------ @@ -1922,19 +1956,10 @@ function __Trunc(val: double): Int64; {$IFDEF INLINE} inline; {$ENDIF} end; //------------------------------------------------------------------------------ -function CheckCastInt64(val: double): Int64; {$IFDEF INLINE} inline; {$ENDIF} -begin - if (val >= MaxCoord) or (val <= MinCoord) then - Raise EClipper2LibException.Create('overflow error.'); - Result := Trunc(val); - //Result := __Trunc(val); -end; -//------------------------------------------------------------------------------ - function GetIntersectPoint(const ln1a, ln1b, ln2a, ln2b: TPoint64; out ip: TPoint64): Boolean; var - dx1,dy1, dx2,dy2, qx,qy, cp: double; + dx1,dy1, dx2,dy2, t, cp: double; begin // https://en.wikipedia.org/wiki/Line%E2%80%93line_intersection dy1 := (ln1b.y - ln1a.y); @@ -1942,16 +1967,13 @@ function GetIntersectPoint(const ln1a, ln1b, ln2a, ln2b: TPoint64; dy2 := (ln2b.y - ln2a.y); dx2 := (ln2b.x - ln2a.x); cp := dy1 * dx2 - dy2 * dx1; - if (cp = 0.0) then - begin - Result := false; - Exit; - end; - qx := dx1 * ln1a.y - dy1 * ln1a.x; - qy := dx2 * ln2a.y - dy2 * ln2a.x; - ip.X := CheckCastInt64((dx1 * qy - dx2 * qx) / cp); - ip.Y := CheckCastInt64((dy1 * qy - dy2 * qx) / cp); - Result := (ip.x <> invalid64) and (ip.y <> invalid64); + Result := (cp <> 0.0); + if not Result then Exit; + t := ((ln1a.x-ln2a.x) * dy2 - (ln1a.y-ln2a.y) * dx2) / cp; + if t <= 0.0 then ip := ln1a + else if t >= 1.0 then ip := ln1b; + ip.X := Trunc(ln1a.X + t * dx1); + ip.Y := Trunc(ln1a.Y + t * dy1); end; //------------------------------------------------------------------------------ diff --git a/Delphi/Clipper2Lib/Clipper.Engine.pas b/Delphi/Clipper2Lib/Clipper.Engine.pas index 1c0da73e..da38f00a 100644 --- a/Delphi/Clipper2Lib/Clipper.Engine.pas +++ b/Delphi/Clipper2Lib/Clipper.Engine.pas @@ -2,7 +2,7 @@ (******************************************************************************* * Author : Angus Johnson * -* Date : 26 March 2023 * +* Date : 22 November 2023 * * Website : http://www.angusj.com * * Copyright : Angus Johnson 2010-2023 * * Purpose : This is the main polygon clipping module * @@ -53,6 +53,18 @@ TLocMinList = class(TListEx) procedure Clear; override; end; + TReuseableDataContainer64 = class + private + FLocMinList : TLocMinList; + FVertexArrayList : TList; + public + constructor Create; + destructor Destroy; override; + procedure Clear; + procedure AddPaths(const paths: TPaths64; + pathType: TPathType; isOpen: Boolean); + end; + // forward declarations POutRec = ^TOutRec; PHorzSegment = ^THorzSegment; @@ -84,6 +96,7 @@ TOutRec = record pts : POutPt; polypath : TPolyPathBase; splits : TOutRecArray; + recursiveCheck : POutRec; bounds : TRect64; path : TPath64; isOpen : Boolean; @@ -239,7 +252,7 @@ TClipperBase = class procedure ProcessIntersectList; procedure SwapPositionsInAEL(e1, e2: PActive); function AddOutPt(e: PActive; const pt: TPoint64): POutPt; - procedure Split(e: PActive; const currPt: TPoint64); + procedure UndoJoin(e: PActive; const currPt: TPoint64); procedure CheckJoinLeft(e: PActive; const pt: TPoint64; checkCurrX: Boolean = false); {$IFDEF INLINING} inline; {$ENDIF} @@ -257,14 +270,15 @@ TClipperBase = class procedure DoSplitOp(outrec: POutRec; splitOp: POutPt); procedure FixSelfIntersects(outrec: POutRec); function CheckBounds(outrec: POutRec): Boolean; + function CheckSplitOwner(outrec: POutRec; const splits: TOutRecArray): Boolean; procedure RecursiveCheckOwners(outrec: POutRec; polytree: TPolyPathBase); - procedure DeepCheckOwners(outrec: POutRec; polytree: TPolyPathBase); protected FUsingPolytree : Boolean; procedure AddPath(const path: TPath64; pathType: TPathType; isOpen: Boolean); procedure AddPaths(const paths: TPaths64; pathType: TPathType; isOpen: Boolean); + procedure AddReuseableData(const reuseableData: TReuseableDataContainer64); function ClearSolutionOnly: Boolean; procedure ExecuteInternal(clipType: TClipType; fillRule: TFillRule; usingPolytree: Boolean); @@ -289,6 +303,7 @@ TClipperBase = class TClipper64 = class(TClipperBase) // for integer coordinates public + procedure AddReuseableData(const reuseableData: TReuseableDataContainer64); procedure AddSubject(const subject: TPath64); overload; procedure AddSubject(const subjects: TPaths64); overload; procedure AddOpenSubject(const subject: TPath64); overload; @@ -332,10 +347,9 @@ TPolyPathBase = class TPolyPath64 = class(TPolyPathBase) {$IFDEF STRICT}strict{$ENDIF} private FPath : TPath64; - function GetChild64(index: Integer): TPolyPath64; - protected - function AddChild(const path: TPath64): TPolyPathBase; override; + function GetChild64(index: Integer): TPolyPath64; public + function AddChild(const path: TPath64): TPolyPathBase; override; property Child[index: Integer]: TPolyPath64 read GetChild64; default; property Polygon: TPath64 read FPath; end; @@ -388,8 +402,9 @@ TPolyPathD = class(TPolyPathBase) function GetChildD(index: Integer): TPolyPathD; protected FScale : double; - function AddChild(const path: TPath64): TPolyPathBase; override; public + function AddChild(const path: TPath64): TPolyPathBase; overload; override; + function AddChild(const path: TPathD): TPolyPathBase; reintroduce; overload; property Polygon: TPathD read FPath; property Child[index: Integer]: TPolyPathD read GetChildD; default; end; @@ -536,10 +551,9 @@ function IsOpen(e: PActive): Boolean; overload; {$IFDEF INLINING} inline; {$ENDI end; //------------------------------------------------------------------------------ -function IsOpenEnd(e: PActive): Boolean; overload; {$IFDEF INLINING} inline; {$ENDIF} +function IsOpenEnd(v: PVertex): Boolean; overload; {$IFDEF INLINING} inline; {$ENDIF} begin - Result := e.locMin.isOpen and - (e.vertTop.flags * [vfOpenStart, vfOpenEnd] <> []); + Result := (v.flags * [vfOpenStart, vfOpenEnd] <> []); end; //------------------------------------------------------------------------------ @@ -620,6 +634,15 @@ function GetRealOutRec(outRec: POutRec): POutRec; end; //------------------------------------------------------------------------------ +function IsValidOwner(outRec, TestOwner: POutRec): Boolean; + {$IFDEF INLINING} inline; {$ENDIF} +begin + while Assigned(TestOwner) and (outrec <> TestOwner) do + TestOwner := TestOwner.owner; + Result := not Assigned(TestOwner); +end; +//------------------------------------------------------------------------------ + function PtsReallyClose(const pt1, pt2: TPoint64): Boolean; {$IFDEF INLINING} inline; {$ENDIF} begin @@ -817,6 +840,138 @@ function PointCount(pts: POutPt): Integer; {$IFDEF INLINING} inline; {$ENDIF} end; //------------------------------------------------------------------------------ +function GetCleanPath(op: POutPt): TPath64; +var + cnt: integer; + op2, prevOp: POutPt; +begin + cnt := 0; + SetLength(Result, PointCount(op)); + op2 := op; + while ((op2.next <> op) and + (((op2.pt.X = op2.next.pt.X) and (op2.pt.X = op2.prev.pt.X)) or + ((op2.pt.Y = op2.next.pt.Y) and (op2.pt.Y = op2.prev.pt.Y)))) do + op2 := op2.next; + result[cnt] := op2.pt; + inc(cnt); + prevOp := op2; + op2 := op2.next; + while (op2 <> op) do + begin + if (((op2.pt.X <> op2.next.pt.X) or (op2.pt.X <> prevOp.pt.X)) and + ((op2.pt.Y <> op2.next.pt.Y) or (op2.pt.Y <> prevOp.pt.Y))) then + begin + result[cnt] := op2.pt; + inc(cnt); + prevOp := op2; + end; + op2 := op2.next; + end; + SetLength(Result, cnt); +end; + +function PointInOpPolygon(const pt: TPoint64; op: POutPt): TPointInPolygonResult; +var + val: Integer; + op2: POutPt; + isAbove, startingAbove: Boolean; + d: double; // avoids integer overflow +begin + result := pipOutside; + if (op = op.next) or (op.prev = op.next) then Exit; + + op2 := op; + repeat + if (op.pt.Y <> pt.Y) then break; + op := op.next; + until op = op2; + if (op.pt.Y = pt.Y) then Exit; // not a proper polygon + + isAbove := op.pt.Y < pt.Y; + startingAbove := isAbove; + Result := pipOn; + val := 0; + op2 := op.next; + while (op2 <> op) do + begin + if isAbove then + while (op2 <> op) and (op2.pt.Y < pt.Y) do op2 := op2.next + else + while (op2 <> op) and (op2.pt.Y > pt.Y) do op2 := op2.next; + if (op2 = op) then break; + + // must have touched or crossed the pt.Y horizonal + // and this must happen an even number of times + + if (op2.pt.Y = pt.Y) then // touching the horizontal + begin + if (op2.pt.X = pt.X) or ((op2.pt.Y = op2.prev.pt.Y) and + ((pt.X < op2.prev.pt.X) <> (pt.X < op2.pt.X))) then Exit; + op2 := op2.next; + if (op2 = op) then break; + Continue; + end; + + if (pt.X < op2.pt.X) and (pt.X < op2.prev.pt.X) then + // do nothing because + // we're only interested in edges crossing on the left + else if((pt.X > op2.prev.pt.X) and (pt.X > op2.pt.X)) then + val := 1 - val // toggle val + else + begin + d := CrossProduct(op2.prev.pt, op2.pt, pt); + if d = 0 then Exit; // ie point on path + if (d < 0) = isAbove then val := 1 - val; + end; + isAbove := not isAbove; + op2 := op2.next; + end; + + if (isAbove <> startingAbove) then + begin + d := CrossProduct(op2.prev.pt, op2.pt, pt); + if d = 0 then Exit; // ie point on path + if (d < 0) = isAbove then val := 1 - val; + end; + + if val = 0 then + result := pipOutside else + result := pipInside; +end; +//------------------------------------------------------------------------------ + +function Path1InsidePath2(const op1, op2: POutPt): Boolean; +var + op: POutPt; + mp: TPoint64; + path: TPath64; + pipResult: TPointInPolygonResult; + outsideCnt: integer; +begin + // precondition - the twi paths or1 & pr2 don't intersect + // we need to make some accommodation for rounding errors + // so we won't jump if the first vertex is found outside + outsideCnt := 0; + op := op1; + repeat + pipResult := PointInOpPolygon(op.pt, op2); + if pipResult = pipOutside then inc(outsideCnt) + else if pipResult = pipInside then dec(outsideCnt); + op := op.next; + until (op = op1) or (Abs(outsideCnt) = 2); + if (Abs(outsideCnt) < 2) then + begin + // if path1's location is still equivocal then check its midpoint + path := GetCleanPath(op1); + mp := Clipper.Core.GetBounds(path).MidPoint; + path := GetCleanPath(op2); + Result := PointInPolygon(mp, path) <> pipOutside; + end + else + Result := (outsideCnt < 0); +end; +//------------------------------------------------------------------------------ + procedure UncoupleOutRec(e: PActive); var outRec: POutRec; @@ -1181,6 +1336,44 @@ procedure SetOwner(outrec, newOwner: POutRec); outrec.owner := newOwner; end; +//------------------------------------------------------------------------------ +// TReuseableDataContainer64 methods ... +//------------------------------------------------------------------------------ + +constructor TReuseableDataContainer64.Create; +begin + FLocMinList := TLocMinList.Create; + FVertexArrayList := TList.Create; +end; +//------------------------------------------------------------------------------ + +destructor TReuseableDataContainer64.Destroy; +begin + Clear; + FLocMinList.Free; + FVertexArrayList.Free; + inherited; +end; +//------------------------------------------------------------------------------ + +procedure TReuseableDataContainer64.Clear; +var + i: integer; +begin + FLocMinList.Clear; + for i := 0 to FVertexArrayList.Count -1 do + FreeMem(UnsafeGet(FVertexArrayList, i)); + FVertexArrayList.Clear; +end; +//------------------------------------------------------------------------------ + +procedure TReuseableDataContainer64.AddPaths(const paths: TPaths64; + pathType: TPathType; isOpen: Boolean); +begin + AddPathsToVertexList(paths, pathType, isOpen, + FVertexArrayList, FLocMinList); +end; + //------------------------------------------------------------------------------ // TClipperBase methods ... //------------------------------------------------------------------------------ @@ -1402,6 +1595,29 @@ procedure TClipperBase.AddPaths(const paths: TPaths64; end; //------------------------------------------------------------------------------ +procedure TClipperBase.AddReuseableData(const reuseableData: TReuseableDataContainer64); +var + i: integer; + lm: PLocalMinima; +begin + if reuseableData.FLocMinList.Count = 0 then Exit; + // nb: reuseableData will continue to own the vertices + // and will remain responsible for their clean up. + // Consequently, it's important that the reuseableData object isn't + // destroyed before the Clipper object that's using the data. + FLocMinListSorted := false; + for i := 0 to reuseableData.FLocMinList.Count -1 do + with PLocalMinima(reuseableData.FLocMinList[i])^ do + begin + lm := self.FLocMinList.Add; + lm.vertex := vertex; + lm.polytype := polytype; + lm.isOpen := isOpen; + if isOpen then FHasOpenPaths := true; + end; +end; +//------------------------------------------------------------------------------ + function TClipperBase.IsContributingClosed(e: PActive): Boolean; begin Result := false; @@ -1954,11 +2170,6 @@ procedure TClipperBase.DoSplitOp(outrec: POutRec; splitOp: POutPt); Exit; end; - // nb: area1 is the path's area *before* splitting, whereas area2 is - // the area of the triangle containing splitOp & splitOp.next. - // So the only way for these areas to have the same sign is if - // the split triangle is larger than the path containing prevOp or - // if there's more than one self=intersection. area2 := AreaTriangle(ip, splitOp.pt, splitOp.next.pt); absArea2 := abs(area2); @@ -1976,6 +2187,11 @@ procedure TClipperBase.DoSplitOp(outrec: POutRec; splitOp: POutPt); prevOp.next := newOp2; end; + // nb: area1 is the path's area *before* splitting, whereas area2 is + // the area of the triangle containing splitOp & splitOp.next. + // So the only way for these areas to have the same sign is if + // the split triangle is larger than the path containing prevOp or + // if there's more than one self-intersection. if (absArea2 > 1) and ((absArea2 > absArea1) or ((area2 > 0) = (area1 > 0))) then @@ -1983,15 +2199,20 @@ procedure TClipperBase.DoSplitOp(outrec: POutRec; splitOp: POutPt); newOutRec := FOutRecList.Add; newOutRec.owner := outrec.owner; - if FUsingPolytree then - AddSplit(outrec, newOutRec); - splitOp.outrec := newOutRec; splitOp.next.outrec := newOutRec; newOp := NewOutPt(ip, newOutRec, splitOp.next, splitOp); splitOp.prev := newOp; splitOp.next.next := newOp; newOutRec.pts := newOp; + + if FUsingPolytree then + begin + if (Path1InsidePath2(prevOp, newOp)) then + AddSplit(newOutRec, outrec) else + AddSplit(outrec, newOutRec); + end; + end else begin Dispose(splitOp.next); @@ -2030,14 +2251,14 @@ function TClipperBase.AddLocalMaxPoly(e1, e2: PActive; const pt: TPoint64): POut outRec: POutRec; begin - if IsJoined(e1) then Split(e1, pt); - if IsJoined(e2) then Split(e2, pt); + if IsJoined(e1) then UndoJoin(e1, pt); + if IsJoined(e2) then UndoJoin(e2, pt); if (IsFront(e1) = IsFront(e2)) then begin - if IsOpenEnd(e1) then + if IsOpenEnd(e1.vertTop) then SwapFrontBackSides(e1.outrec) - else if IsOpenEnd(e2) then + else if IsOpenEnd(e2.vertTop) then SwapFrontBackSides(e2.outrec) else begin @@ -2116,13 +2337,14 @@ procedure TClipperBase.JoinOutrecPaths(e1, e2: PActive); e2.outrec.frontE := nil; e2.outrec.backE := nil; e2.outrec.pts := nil; - SetOwner(e2.outrec, e1.outrec); - if IsOpenEnd(e1) then + if IsOpenEnd(e1.vertTop) then begin e2.outrec.pts := e1.outrec.pts; e1.outrec.pts := nil; - end; + end + else + SetOwner(e2.outrec, e1.outrec); // and e1 and e2 are maxima and are about to be dropped from the Actives list. e1.outrec := nil; @@ -2130,7 +2352,7 @@ procedure TClipperBase.JoinOutrecPaths(e1, e2: PActive); end; //------------------------------------------------------------------------------ -procedure TClipperBase.Split(e: PActive; const currPt: TPoint64); +procedure TClipperBase.UndoJoin(e: PActive; const currPt: TPoint64); begin if e.joinedWith = jwRight then begin @@ -2153,8 +2375,10 @@ procedure TClipperBase.CheckJoinLeft(e: PActive; begin prev := e.prevInAEL; if IsOpen(e) or not IsHotEdge(e) or not Assigned(prev) or - IsOpen(prev) or not IsHotEdge(prev) or - (pt.Y < e.top.Y +2) or (pt.Y < prev.top.Y +2) then Exit; + IsOpen(prev) or not IsHotEdge(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) + if checkCurrX then begin if DistanceFromLineSqrd(pt, prev.bot, prev.top) > 0.25 then Exit @@ -2180,8 +2404,9 @@ procedure TClipperBase.CheckJoinRight(e: PActive; begin next := e.nextInAEL; if IsOpen(e) or not IsHotEdge(e) or not Assigned(next) or - IsOpen(next) or not IsHotEdge(next) or - (pt.Y < e.top.Y +2) or (pt.Y < next.top.Y +2) then Exit; + not IsHotEdge(next) 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) if (checkCurrX) then begin @@ -2255,6 +2480,31 @@ function TClipperBase.StartOpenPath(e: PActive; const pt: TPoint64): POutPt; end; //------------------------------------------------------------------------------ +procedure TrimHorz(horzEdge: PActive; preserveCollinear: Boolean); +var + pt: TPoint64; + wasTrimmed: Boolean; +begin + wasTrimmed := false; + pt := NextVertex(horzEdge).pt; + while (pt.Y = horzEdge.top.Y) do + begin + // always trim 180 deg. spikes (in closed paths) + // but otherwise break if preserveCollinear = true + if preserveCollinear and + ((pt.X < horzEdge.top.X) <> (horzEdge.bot.X < horzEdge.top.X)) then + break; + + horzEdge.vertTop := NextVertex(horzEdge); + horzEdge.top := pt; + wasTrimmed := true; + if IsMaxima(horzEdge) then Break; + pt := NextVertex(horzEdge).pt; + end; + if wasTrimmed then SetDx(horzEdge); // +/-infinity +end; +//------------------------------------------------------------------------------ + procedure TClipperBase.UpdateEdgeIntoAEL(var e: PActive); begin e.bot := e.top; @@ -2263,13 +2513,17 @@ procedure TClipperBase.UpdateEdgeIntoAEL(var e: PActive); e.currX := e.bot.X; SetDx(e); - if IsJoined(e) then Split(e, e.bot); + if IsJoined(e) then UndoJoin(e, e.bot); - if IsHorizontal(e) then Exit; + if IsHorizontal(e) then + begin + if not IsOpen(e) then TrimHorz(e, PreserveCollinear); + Exit; + end; InsertScanLine(e.top.Y); CheckJoinLeft(e, e.bot); - CheckJoinRight(e, e.bot); + CheckJoinRight(e, e.bot, true); // (#500) end; //------------------------------------------------------------------------------ @@ -2315,7 +2569,7 @@ function TClipperBase.IntersectEdges(e1, e2: PActive; pt: TPoint64): POutPt; // e1 is open and e2 is closed - if IsJoined(e2) then Split(e2, pt); // needed for safety + if IsJoined(e2) then UndoJoin(e2, pt); // needed for safety case FClipType of ctUnion: if not IsHotEdge(e2) then Exit; @@ -2354,11 +2608,9 @@ function TClipperBase.IntersectEdges(e1, e2: PActive; pt: TPoint64): POutPt; SetSides(e3.outrec, e3, e1); Result := e3.outrec.pts; Exit; - end - else + end else Result := StartOpenPath(e1, pt); - end - else + end else Result := StartOpenPath(e1, pt); {$IFDEF USINGZ} @@ -2368,8 +2620,8 @@ function TClipperBase.IntersectEdges(e1, e2: PActive; pt: TPoint64): POutPt; end; // MANAGING CLOSED PATHS FROM HERE ON - if IsJoined(e1) then Split(e1, pt); - if IsJoined(e2) then Split(e2, pt); + if IsJoined(e1) then UndoJoin(e1, pt); + if IsJoined(e2) then UndoJoin(e2, pt); // FIRST, UPDATE WINDING COUNTS if IsSamePolyType(e1, e2) then @@ -2608,7 +2860,7 @@ procedure TClipperBase.ExecuteInternal(clipType: TClipType; FBotY := Y; // FBotY == bottom of current scanbeam if not PopScanLine(Y) then Break; // Y == top of current scanbeam DoIntersections(Y); - DoTopOfScanbeam(Y); + DoTopOfScanbeam(Y); while PopHorz(e) do DoHorizontal(e); end; if Succeeded then ProcessHorzJoins; @@ -2641,120 +2893,6 @@ procedure SetOutRecPts(op: POutPt; newOR: POutrec); overload; end; //------------------------------------------------------------------------------ -function GetBounds(op: POutPt): TRect64; -var - op2: POutPt; -begin - result.Left := op.pt.X; - result.Right := op.pt.X; - result.Top := op.pt.Y; - result.Bottom := op.pt.Y; - op2 := op.next; - while op2 <> op do - begin - if op2.pt.X < result.Left then result.Left := op2.pt.X - else if op2.pt.X > result.Right then result.Right := op2.pt.X; - if op2.pt.Y < result.Top then result.Top := op2.pt.Y - else if op2.pt.Y > result.Bottom then result.Bottom := op2.pt.Y; - op2 := op2.next; - end; -end; -//------------------------------------------------------------------------------ - -function PointInOpPolygon(const pt: TPoint64; op: POutPt): TPointInPolygonResult; -var - val: Integer; - op2: POutPt; - isAbove, startingAbove: Boolean; - d: double; // avoids integer overflow -begin - result := pipOutside; - if (op = op.next) or (op.prev = op.next) then Exit; - - op2 := op; - repeat - if (op.pt.Y <> pt.Y) then break; - op := op.next; - until op = op2; - if (op.pt.Y = pt.Y) then Exit; // not a proper polygon - - isAbove := op.pt.Y < pt.Y; - startingAbove := isAbove; - Result := pipOn; - val := 0; - op2 := op.next; - while (op2 <> op) do - begin - if isAbove then - while (op2 <> op) and (op2.pt.Y < pt.Y) do op2 := op2.next - else - while (op2 <> op) and (op2.pt.Y > pt.Y) do op2 := op2.next; - if (op2 = op) then break; - - // must have touched or crossed the pt.Y horizonal - // and this must happen an even number of times - - if (op2.pt.Y = pt.Y) then // touching the horizontal - begin - if (op2.pt.X = pt.X) or ((op2.pt.Y = op2.prev.pt.Y) and - ((pt.X < op2.prev.pt.X) <> (pt.X < op2.pt.X))) then Exit; - op2 := op2.next; - if (op2 = op) then break; - Continue; - end; - - if (pt.X < op2.pt.X) and (pt.X < op2.prev.pt.X) then - // do nothing because - // we're only interested in edges crossing on the left - else if((pt.X > op2.prev.pt.X) and (pt.X > op2.pt.X)) then - val := 1 - val // toggle val - else - begin - d := CrossProduct(op2.prev.pt, op2.pt, pt); - if d = 0 then Exit; // ie point on path - if (d < 0) = isAbove then val := 1 - val; - end; - isAbove := not isAbove; - op2 := op2.next; - end; - - if (isAbove <> startingAbove) then - begin - d := CrossProduct(op2.prev.pt, op2.pt, pt); - if d = 0 then Exit; // ie point on path - if (d < 0) = isAbove then val := 1 - val; - end; - - if val = 0 then - result := pipOutside else - result := pipInside; -end; -//------------------------------------------------------------------------------ - -function Path1InsidePath2(const op1, op2: POutPt): Boolean; -var - op: POutPt; - pipResult: TPointInPolygonResult; - outsideCnt: integer; -begin - // precondition - the twi paths or1 & pr2 don't intersect - // we need to make some accommodation for rounding errors - // so we won't jump if the first vertex is found outside - outsideCnt := 0; - op := op1; - repeat - pipResult := PointInOpPolygon(op.pt, op2); - if pipResult = pipOutside then inc(outsideCnt) - else if pipResult = pipInside then dec(outsideCnt); - op := op.next; - until (op = op1) or (Abs(outsideCnt) = 2); - // if path1's location is still equivocal then check its midpoint - if Abs(outsideCnt) > 1 then - Result := outsideCnt < 0 else - Result := PointInOpPolygon(GetBounds(op).MidPoint, op2) = pipInside; -end; -//------------------------------------------------------------------------------ - function HorzOverlapWithLRSet(const left1, right1, left2, right2: TPoint64): boolean; {$IFDEF INLINING} inline; {$ENDIF} begin @@ -2870,8 +3008,8 @@ procedure TClipperBase.ConvertHorzSegsToJoins; begin hs2 := FHorzSegList.UnsafeGet(j); - if hs2.leftOp.pt.X >= hs1.rightOp.pt.X then Break - else if (hs2.leftToRight = hs1.leftToRight) or + if (hs2.leftOp.pt.X >= hs1.rightOp.pt.X) or + (hs2.leftToRight = hs1.leftToRight) or (hs2.rightOp.pt.X <= hs1.leftOp.pt.X) then Continue; currY := hs1.leftOp.pt.Y; @@ -2903,11 +3041,22 @@ procedure TClipperBase.ConvertHorzSegsToJoins; end; //------------------------------------------------------------------------------ +procedure MoveSplits(fromOr, toOr: POutRec); +var + i: integer; +begin + if not assigned(fromOr.splits) then Exit; + for i := 0 to High(fromOr.splits) do + AddSplit(toOr, fromOr.splits[i]); + fromOr.splits := nil; +end; +//------------------------------------------------------------------------------ + procedure TClipperBase.ProcessHorzJoins; var i: integer; or1, or2: POutRec; - op1b, op2b: POutPt; + op1b, op2b, tmp: POutPt; begin for i := 0 to FHorzJoinList.Count -1 do with PHorzJoin(FHorzJoinList[i])^ do @@ -2924,33 +3073,51 @@ procedure TClipperBase.ProcessHorzJoins; op1b.prev := op2b; op2b.next := op1b; - if or1 = or2 then + if or1 = or2 then // 'join' is really a split begin or2 := FOutRecList.Add; or2.pts := op1b; FixOutRecPts(or2); + + //if or1->pts has moved to or2 then update or1->pts!! if or1.pts.outrec = or2 then begin or1.pts := op1; or1.pts.outrec := or1; end; - if FUsingPolytree then + if FUsingPolytree then //#498, #520, #584, D#576, #618 begin - if Path1InsidePath2(or2.pts, or1.pts) then - SetOwner(or2, or1) - else if Path1InsidePath2(or1.pts, or2.pts) then - SetOwner(or1, or2) - else + if Path1InsidePath2(or1.pts, or2.pts) then + begin + //swap or1's & or2's pts + tmp := or1.pts; + or1.pts := or2.pts; + or2.pts := tmp; + FixOutRecPts(or1); + FixOutRecPts(or2); + //or2 is now inside or1 or2.owner := or1; - end else + end + else if Path1InsidePath2(or2.pts, or1.pts) then + begin + or2.owner := or1; + end + else + or2.owner := or1.owner; + + AddSplit(or1, or2); + end + else or2.owner := or1; end else begin or2.pts := nil; - if FUsingPolytree then - SetOwner(or2, or1) - else + if FUsingPolytree then + begin + SetOwner(or2, or1); + MoveSplits(or2, or1); //#618 + end else or2.owner := or1; end; end; @@ -3195,41 +3362,6 @@ procedure TClipperBase.SwapPositionsInAEL(e1, e2: PActive); end; //------------------------------------------------------------------------------ -function HorzIsSpike(horzEdge: PActive): Boolean; -var - nextPt: TPoint64; -begin - nextPt := NextVertex(horzEdge).pt; - Result := (nextPt.Y = horzEdge.top.Y) and - (horzEdge.bot.X < horzEdge.top.X) <> (horzEdge.top.X < nextPt.X); -end; -//------------------------------------------------------------------------------ - -procedure TrimHorz(horzEdge: PActive; preserveCollinear: Boolean); -var - pt: TPoint64; - wasTrimmed: Boolean; -begin - wasTrimmed := false; - pt := NextVertex(horzEdge).pt; - while (pt.Y = horzEdge.top.Y) do - begin - // always trim 180 deg. spikes (in closed paths) - // but otherwise break if preserveCollinear = true - if preserveCollinear and - ((pt.X < horzEdge.top.X) <> (horzEdge.bot.X < horzEdge.top.X)) then - break; - - horzEdge.vertTop := NextVertex(horzEdge); - horzEdge.top := pt; - wasTrimmed := true; - if IsMaxima(horzEdge) then Break; - pt := NextVertex(horzEdge).pt; - end; - if wasTrimmed then SetDx(horzEdge); // +/-infinity -end; -//------------------------------------------------------------------------------ - function GetLastOp(hotEdge: PActive): POutPt; {$IFDEF INLINING} inline; {$ENDIF} var @@ -3281,7 +3413,6 @@ procedure TClipperBase.DoHorizontal(horzEdge: PActive); e: PActive; pt: TPoint64; op: POutPt; - currOr: POutRec; isLeftToRight, horzIsOpen: Boolean; begin (******************************************************************************* @@ -3307,10 +3438,6 @@ procedure TClipperBase.DoHorizontal(horzEdge: PActive); maxVertex := GetCurrYMaximaVertexOpen(horzEdge) else maxVertex := GetCurrYMaximaVertex(horzEdge); - if Assigned(maxVertex) and not horzIsOpen and - (maxVertex <> horzEdge.vertTop) then - TrimHorz(horzEdge, FPreserveCollinear); - isLeftToRight := ResetHorzDirection; // nb: TrimHorz above hence not using Bot.X here @@ -3323,7 +3450,6 @@ procedure TClipperBase.DoHorizontal(horzEdge: PActive); {$ENDIF} FHorzSegList.Add(op); end; - currOr := horzEdge.outrec; while true do // loop through consec. horizontal edges begin @@ -3336,7 +3462,7 @@ procedure TClipperBase.DoHorizontal(horzEdge: PActive); if (e.vertTop = maxVertex) then begin if IsHotEdge(horzEdge) and IsJoined(e) then - Split(e, e.top); + UndoJoin(e, e.top); if IsHotEdge(horzEdge) then begin @@ -3359,7 +3485,7 @@ procedure TClipperBase.DoHorizontal(horzEdge: PActive); // if horzEdge is a maxima, keep going until we reach // its maxima pair, otherwise check for Break conditions - if (maxVertex <> horzEdge.vertTop) or IsOpenEnd(horzEdge) then + if (maxVertex <> horzEdge.vertTop) or IsOpenEnd(horzEdge.vertTop) then begin // otherwise stop when 'e' is beyond the end of the horizontal line if (isLeftToRight and (e.currX > horzRight)) or @@ -3391,18 +3517,19 @@ procedure TClipperBase.DoHorizontal(horzEdge: PActive); begin IntersectEdges(horzEdge, e, pt); SwapPositionsInAEL(horzEdge, e); + CheckJoinLeft(e, pt); horzEdge.currX := e.currX; e := horzEdge.nextInAEL; end else begin IntersectEdges(e, horzEdge, pt); SwapPositionsInAEL(e, horzEdge); + CheckJoinRight(e, pt); horzEdge.currX := e.currX; e := horzEdge.prevInAEL; end; - if IsHotEdge(horzEdge) and (horzEdge.outrec <> currOr) then + if IsHotEdge(horzEdge) then begin - currOr := horzEdge.outrec; //nb: The outrec containining the op returned by IntersectEdges //above may no longer be associated with horzEdge. FHorzSegList.Add(GetLastOp(horzEdge)); @@ -3410,7 +3537,7 @@ procedure TClipperBase.DoHorizontal(horzEdge: PActive); end; // we've reached the end of this horizontal // check if we've finished looping through consecutive horizontals - if horzIsOpen and IsOpenEnd(horzEdge) then + if horzIsOpen and IsOpenEnd(horzEdge.vertTop) then begin if IsHotEdge(horzEdge) then begin @@ -3432,16 +3559,14 @@ procedure TClipperBase.DoHorizontal(horzEdge: PActive); if IsHotEdge(horzEdge) then AddOutPt(horzEdge, horzEdge.top); UpdateEdgeIntoAEL(horzEdge); - - if PreserveCollinear and - not horzIsOpen and HorzIsSpike(horzEdge) then - TrimHorz(horzEdge, true); - isLeftToRight := ResetHorzDirection; end; // end while horizontal if IsHotEdge(horzEdge) then - AddOutPt(horzEdge, horzEdge.top); + begin + op := AddOutPt(horzEdge, horzEdge.top); + FHorzSegList.Add(op); // Disc.#546 + end; UpdateEdgeIntoAEL(horzEdge); // this is the end of an intermediate horiz. end; @@ -3488,7 +3613,7 @@ function TClipperBase.DoMaxima(e: PActive): PActive; eNext := e.nextInAEL; Result := eNext; - if IsOpenEnd(e) then + if IsOpenEnd(e.vertTop) then begin if IsHotEdge(e) then AddOutPt(e, e.top); if not IsHorizontal(e) then @@ -3509,8 +3634,8 @@ function TClipperBase.DoMaxima(e: PActive): PActive; if not assigned(eMaxPair) then Exit; // EMaxPair is a horizontal ... end; - if IsJoined(e) then Split(e, e.top); - if IsJoined(eMaxPair) then Split(eMaxPair, eMaxPair.top); + if IsJoined(e) then UndoJoin(e, e.top); + if IsJoined(eMaxPair) then UndoJoin(eMaxPair, eMaxPair.top); // only non-horizontal maxima here. // process any edges between maxima pair ... @@ -3607,6 +3732,37 @@ function TClipperBase.CheckBounds(outrec: POutRec): Boolean; end; //------------------------------------------------------------------------------ +function TClipperBase.CheckSplitOwner(outrec: POutRec; const splits: TOutRecArray): Boolean; +var + i : integer; + split : POutrec; +begin + // returns true if a valid owner is found in splits + // (and also assigns it to outrec.owner) + Result := true; + for i := 0 to High(splits) do + begin + split := GetRealOutRec(splits[i]); + if (split = nil) or + (split = outrec) or + (split.recursiveCheck = outrec) then Continue; + + split.recursiveCheck := outrec; // prevent infinite loops + if Assigned(split.splits) and + CheckSplitOwner(outrec, split.splits) then Exit + else if IsValidOwner(outrec, split) and + CheckBounds(split) and + (split.bounds.Contains(outrec.bounds) and + Path1InsidePath2(outrec.pts, split.pts)) then + begin + outrec.owner := split; + Exit; + end; + end; + Result := false; +end; +//------------------------------------------------------------------------------ + procedure TClipperBase.RecursiveCheckOwners(outrec: POutRec; polytree: TPolyPathBase); begin // pre-condition: outrec will have valid bounds @@ -3616,54 +3772,24 @@ procedure TClipperBase.RecursiveCheckOwners(outrec: POutRec; polytree: TPolyPath outrec.bounds.IsEmpty then Exit; - while Assigned(outrec.owner) and - (not Assigned(outrec.owner.pts) or - not CheckBounds(outrec.owner)) do - outrec.owner := outrec.owner.owner; - - if Assigned(outrec.owner) and - not Assigned(outrec.owner.polypath) then - RecursiveCheckOwners(outrec.owner, polytree); - while Assigned(outrec.owner) do - if (outrec.owner.bounds.Contains(outrec.bounds) and - Path1InsidePath2(outrec.pts, outrec.owner.pts)) then - break // found - owner contain outrec! - else - outrec.owner := outrec.owner.owner; + begin + if Assigned(outrec.owner.splits) and + CheckSplitOwner(outrec, outrec.owner.splits) then Break; + if Assigned(outrec.owner.pts) and + CheckBounds(outrec.owner) and + (outrec.owner.bounds.Contains(outrec.bounds) and + Path1InsidePath2(outrec.pts, outrec.owner.pts)) then break; + outrec.owner := outrec.owner.owner; + end; if Assigned(outrec.owner) then - outrec.polypath := outrec.owner.polypath.AddChild(outrec.path) else - outrec.polypath := polytree.AddChild(outrec.path); -end; -//------------------------------------------------------------------------------ - -procedure TClipperBase.DeepCheckOwners(outrec: POutRec; polytree: TPolyPathBase); -var - i : integer; - split : POutrec; -begin - RecursiveCheckOwners(outrec, polytree); - - while Assigned(outrec.owner) and Assigned(outrec.owner.splits) do begin - split := nil; - for i := 0 to High(outrec.owner.splits) do - begin - split := GetRealOutRec(outrec.owner.splits[i]); - if Assigned(split) and (split <> outrec) and - (split <> outrec.owner) and CheckBounds(split) and - (split.bounds.Contains(outrec.bounds) and - Path1InsidePath2(outrec.pts, split.pts)) then - begin - RecursiveCheckOwners(split, polytree); - outrec.owner := split; //found in split - break; // inner 'for' loop - end else - split := nil; - end; - if not Assigned(split) then break; - end; + if not Assigned(outrec.owner.polypath) then + RecursiveCheckOwners(outrec.owner, polytree); + outrec.polypath := outrec.owner.polypath.AddChild(outrec.path) + end else + outrec.polypath := polytree.AddChild(outrec.path); end; //------------------------------------------------------------------------------ @@ -3704,7 +3830,7 @@ function TClipperBase.BuildTree(polytree: TPolyPathBase; end; if CheckBounds(outrec) then - DeepCheckOwners(outrec, polytree); + RecursiveCheckOwners(outrec, polytree); end; setLength(openPaths, cntOpen); Result := FSucceeded; @@ -3739,6 +3865,12 @@ function TClipperBase.GetBounds: TRect64; // TClipper methods //------------------------------------------------------------------------------ +procedure TClipper64.AddReuseableData(const reuseableData: TReuseableDataContainer64); +begin + inherited AddReuseableData(reuseableData); +end; +//------------------------------------------------------------------------------ + procedure TClipper64.AddSubject(const subject: TPath64); begin AddPath(subject, ptSubject, false); @@ -4105,6 +4237,16 @@ function TPolyPathD.AddChild(const path: TPath64): TPolyPathBase; end; //------------------------------------------------------------------------------ +function TPolyPathD.AddChild(const path: TPathD): TPolyPathBase; +begin + Result := TPolyPathD.Create; + Result.Parent := self; + TPolyPathD(Result).fScale := fScale; + TPolyPathD(Result).FPath := path; + ChildList.Add(Result); +end; +//------------------------------------------------------------------------------ + function TPolyPathD.GetChildD(index: Integer): TPolyPathD; begin Result := TPolyPathD(GetChild(index)); diff --git a/Delphi/Clipper2Lib/Clipper.Offset.pas b/Delphi/Clipper2Lib/Clipper.Offset.pas index 1dd5210e..0aa35138 100644 --- a/Delphi/Clipper2Lib/Clipper.Offset.pas +++ b/Delphi/Clipper2Lib/Clipper.Offset.pas @@ -2,7 +2,7 @@ (******************************************************************************* * Author : Angus Johnson * -* Date : 26 March 2023 * +* Date : 28 November 2023 * * Website : http://www.angusj.com * * Copyright : Angus Johnson 2010-2023 * * Purpose : Path Offset (Inflate/Shrink) * @@ -18,7 +18,10 @@ interface type - TJoinType = (jtSquare, jtRound, jtMiter); + TJoinType = (jtMiter, jtSquare, jtBevel, jtRound); + //jtSquare: Joins are 'squared' at exactly the offset distance (complex code) + //jtBevel : offset distances vary depending on the angle (simple code, faster) + TEndType = (etPolygon, etJoined, etButt, etSquare, etRound); // etButt : offsets both sides of a path, with square blunt ends // etSquare : offsets both sides of a path, with square extended ends @@ -26,19 +29,27 @@ interface // etJoined : offsets both sides of a path, with joined ends // etPolygon: offsets only one side of a closed path + TDeltaCallback64 = function (const path: TPath64; + const path_norms: TPathD; currIdx, prevIdx: integer): double of object; + + TRect64Array = array of TRect64; + BooleanArray = array of Boolean; + TGroup = class paths : TPaths64; - reversed : Boolean; joinType : TJoinType; endType : TEndType; - constructor Create(jt: TJoinType; et: TEndType); + reversed : Boolean; + lowestPathIdx: integer; + boundsList: TRect64Array; + isHoleList: BooleanArray; + constructor Create(const pathsIn: TPaths64; jt: TJoinType; et: TEndType); end; TClipperOffset = class private fDelta : Double; fGroupDelta : Double; //*0.5 for open paths; *-1.0 for neg areas - fAbsGrpDelta : Double; fMinLenSqrd : double; fJoinType : TJoinType; fEndType : TEndType; @@ -52,11 +63,13 @@ TClipperOffset = class fGroupList : TListEx; fInPath : TPath64; fOutPath : TPath64; - fOutPaths : TPaths64; fOutPathLen : Integer; fSolution : TPaths64; + fSolutionLen : Integer; + fSolutionTree : TPolyTree64; fPreserveCollinear : Boolean; fReverseSolution : Boolean; + fDeltaCallback64 : TDeltaCallback64; {$IFDEF USINGZ} fZCallback64 : TZCallback64; procedure AddPoint(x,y: double; z: Int64); overload; @@ -66,6 +79,7 @@ TClipperOffset = class procedure AddPoint(const pt: TPoint64); overload; {$IFDEF INLINING} inline; {$ENDIF} procedure DoSquare(j, k: Integer); + procedure DoBevel(j, k: Integer); procedure DoMiter(j, k: Integer; cosA: Double); procedure DoRound(j, k: integer; angle: double); procedure OffsetPoint(j: Integer; var k: integer); @@ -75,6 +89,10 @@ TClipperOffset = class procedure OffsetPolygon; procedure OffsetOpenJoined; procedure OffsetOpenPath; + function CalcSolutionCapacity: integer; + procedure UpdateSolution; {$IFDEF INLINING} inline; {$ENDIF} + + function CheckReverseOrientation: Boolean; procedure ExecuteInternal(delta: Double); public constructor Create(miterLimit: double = 2.0; @@ -89,6 +107,7 @@ TClipperOffset = class procedure Clear; procedure Execute(delta: Double; out solution: TPaths64); overload; procedure Execute(delta: Double; polytree: TPolyTree64); overload; + procedure Execute(DeltaCallback: TDeltaCallback64; out solution: TPaths64); overload; // MiterLimit: needed for mitered offsets (see offset_triginometry3.svg) property MiterLimit: Double read fMiterLimit write fMiterLimit; @@ -98,6 +117,8 @@ TClipperOffset = class read fPreserveCollinear write fPreserveCollinear; property ReverseSolution: Boolean read fReverseSolution write fReverseSolution; + property DeltaCallback: TDeltaCallback64 read + fDeltaCallback64 write fDeltaCallback64; {$IFDEF USINGZ} property ZCallback: TZCallback64 read fZCallback64 write fZCallback64; {$ENDIF} @@ -108,6 +129,10 @@ implementation uses Math; +resourcestring + rsClipper_CoordRangeError = + 'Offsetting will exceed the valid coordinate range'; + const TwoPi : Double = 2 * PI; InvTwoPi : Double = 1/(2 * PI); @@ -116,6 +141,79 @@ 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 @@ -204,10 +302,46 @@ function UnsafeGet(List: TList; Index: Integer): Pointer; // TGroup methods //------------------------------------------------------------------------------ -constructor TGroup.Create(jt: TJoinType; et: TEndType); +constructor TGroup.Create(const pathsIn: TPaths64; jt: TJoinType; et: TEndType); +var + i, len: integer; + isJoined: boolean; + pb: PBoolean; begin Self.joinType := jt; Self.endType := et; + + isJoined := et in [etPolygon, etJoined]; + len := Length(pathsIn); + SetLength(paths, len); + for i := 0 to len -1 do + paths[i] := StripDuplicates(pathsIn[i], isJoined); + + reversed := false; + SetLength(isHoleList, len); + SetLength(boundsList, len); + GetMultiBounds(paths, boundsList); + 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); + 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. + 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; + end else + lowestPathIdx := -1; end; //------------------------------------------------------------------------------ @@ -242,6 +376,7 @@ procedure TClipperOffset.Clear; TGroup(fGroupList[i]).Free; fGroupList.Clear; fSolution := nil; + fSolutionLen := 0; end; //------------------------------------------------------------------------------ @@ -263,8 +398,7 @@ procedure TClipperOffset.AddPaths(const paths: TPaths64; group: TGroup; begin if Length(paths) = 0 then Exit; - group := TGroup.Create(joinType, endType); - AppendPaths(group.paths, paths); + group := TGroup.Create(paths, joinType, endType); fGroupList.Add(group); end; //------------------------------------------------------------------------------ @@ -289,118 +423,119 @@ 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, lowestIdx: Integer; - r, stepsPer360, arcTol, area: Double; + i,j, len, steps: Integer; + r, stepsPer360, arcTol: Double; + absDelta: double; rec: TRect64; - isJoined: Boolean; + pt0: TPoint64; begin + if group.endType = etPolygon then begin - // the lowermost polygon must be an outer polygon. So we can use that as the - // designated orientation for outer polygons (needed for tidy-up clipping) - lowestIdx := GetLowestPolygonIdx(group.paths); - if lowestIdx < 0 then Exit; - // nb: don't use the default orientation here ... - area := Clipper.Core.Area(group.paths[lowestIdx]); - //if area = 0 then Exit; // this is probably unhelpful (#430) - group.reversed := (area < 0); - if group.reversed then fGroupDelta := -fDelta - else fGroupDelta := fDelta; + if (group.lowestPathIdx < 0) then fDelta := Abs(fDelta); + if group.reversed then + fGroupDelta := -fDelta else + fGroupDelta := fDelta; end else begin - group.reversed := false; - fGroupDelta := Abs(fDelta) * 0.5; + fGroupDelta := Abs(fDelta);// * 0.5; end; - fAbsGrpDelta := Abs(fGroupDelta); + + absDelta := Abs(fGroupDelta); + if not ValidateBounds(group.boundsList, absDelta) then + Raise EClipper2LibException(rsClipper_CoordRangeError); + 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 - // 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 if fArcTolerance > 0.01 then - arcTol := Min(fAbsGrpDelta, fArcTolerance) else - arcTol := Log10(2 + fAbsGrpDelta) * 0.25; // empirically derived + arcTol := Min(absDelta, fArcTolerance) else + arcTol := Log10(2 + absDelta) * 0.25; // empirically derived //http://www.angusj.com/clipper2/Docs/Trigonometry.htm - stepsPer360 := Pi / ArcCos(1 - arcTol / fAbsGrpDelta); - if (stepsPer360 > fAbsGrpDelta * Pi) then - stepsPer360 := fAbsGrpDelta * Pi; // avoid excessive precision + stepsPer360 := Pi / ArcCos(1 - arcTol / absDelta); + if (stepsPer360 > absDelta * Pi) then + stepsPer360 := absDelta * Pi; // avoid excessive precision fStepSin := sin(TwoPi/stepsPer360); fStepCos := cos(TwoPi/stepsPer360); if (fGroupDelta < 0.0) then fStepSin := -fStepSin; fStepsPerRad := stepsPer360 / TwoPi; end; - fOutPaths := nil; - isJoined := fEndType in [etPolygon, etJoined]; for i := 0 to High(group.paths) do begin - fInPath := StripDuplicates(group.paths[i], IsJoined); - len := Length(fInPath); - if (len = 0) or ((len < 3) and (fEndType = etPolygon)) then - Continue; + if not group.boundsList[i].IsValid then Continue; + fInPath := group.paths[i]; fNorms := nil; - fOutPath := nil; - fOutPathLen := 0; + len := Length(fInPath); //if a single vertex then build a circle or a square ... if len = 1 then begin if fGroupDelta < 1 then Continue; + pt0 := fInPath[0]; if (group.endType = etRound) then begin - r := fAbsGrpDelta; - with fInPath[0] do - begin - fOutPath := Path64(Ellipse(RectD(X-r, Y-r, X+r, Y+r))); + r := absDelta; + steps := Ceil(fStepsPerRad * TwoPi); //#617 + fOutPath := Path64(Ellipse( + RectD(pt0.X-r, pt0.Y-r, pt0.X+r, pt0.Y+r), steps)); {$IFDEF USINGZ} - for j := 0 to high(fOutPath) do - fOutPath[j].Z := Z; + for j := 0 to high(fOutPath) do + fOutPath[j].Z := pt0.Z; {$ENDIF} - end; end else begin - j := Round(fGroupDelta); - with fInPath[0] do - begin - rec := Rect64(X -j, Y -j, X+j, Y+j); - fOutPath := rec.AsPath; + j := Round(absDelta); + rec := Rect64(pt0.X -j, pt0.Y -j, pt0.X+j, pt0.Y+j); + fOutPath := rec.AsPath; {$IFDEF USINGZ} - for j := 0 to high(fOutPath) do - fOutPath[j].Z := Z; + for j := 0 to high(fOutPath) do + fOutPath[j].Z := pt0.Z; {$ENDIF} - end end; - AppendPath(fOutPaths, fOutPath); + UpdateSolution; Continue; - end else - begin - if (len = 2) and (group.endType = etJoined) then - begin - if fJoinType = jtRound then - fEndType := etRound else - fEndType := etSquare; - end; + end; // end of offsetting a single point - BuildNormals; - if fEndType = etPolygon then OffsetPolygon - else if fEndType = etJoined then OffsetOpenJoined - else OffsetOpenPath; + // 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 + fEndType := etRound else + fEndType := etSquare; end; - if fOutPathLen = 0 then Continue; - SetLength(fOutPath, fOutPathLen); - AppendPath(fOutPaths, fOutPath); + BuildNormals; + if fEndType = etPolygon then OffsetPolygon + else if fEndType = etJoined then OffsetOpenJoined + else OffsetOpenPath; end; - // finally copy the working 'outPaths' to the solution - AppendPaths(fSolution, fOutPaths); end; //------------------------------------------------------------------------------ @@ -416,6 +551,30 @@ procedure TClipperOffset.BuildNormals; end; //------------------------------------------------------------------------------ +procedure TClipperOffset.UpdateSolution; +begin + if fOutPathLen = 0 then Exit; + SetLength(fOutPath, fOutPathLen); + fSolution[fSolutionLen] := fOutPath; + inc(fSolutionLen); + fOutPath := nil; + fOutPathLen := 0; +end; +//------------------------------------------------------------------------------ + +function TClipperOffset.CalcSolutionCapacity: integer; +var + i: integer; +begin + Result := 0; + for i := 0 to fGroupList.Count -1 do + with TGroup(fGroupList[i]) do + if endType = etJoined then + inc(Result, Length(paths) *2) else + inc(Result, Length(paths)); +end; +//------------------------------------------------------------------------------ + procedure TClipperOffset.OffsetPolygon; var i,j: integer; @@ -423,18 +582,14 @@ procedure TClipperOffset.OffsetPolygon; j := high(fInPath); for i := 0 to high(fInPath) do OffsetPoint(i, j); + UpdateSolution; end; //------------------------------------------------------------------------------ procedure TClipperOffset.OffsetOpenJoined; begin OffsetPolygon; - SetLength(fOutPath, fOutPathLen); - AppendPath(fOutPaths, fOutPath); - fOutPath := nil; - fOutPathLen := 0; fInPath := ReversePath(fInPath); - // Rebuild normals // BuildNormals; fNorms := ReversePath(fNorms); fNorms := ShiftPath(fNorms, 1); @@ -450,22 +605,16 @@ procedure TClipperOffset.OffsetOpenPath; begin highI := high(fInPath); - // do the line start cap + if Assigned(fDeltaCallback64) then + fGroupDelta := fDeltaCallback64(fInPath, fNorms, 0, 0); + + // do the line start cap + if Abs(fGroupDelta) < Tolerance then + begin + AddPoint(fInPath[0]); + end else case fEndType of - etButt: - begin -{$IFDEF USINGZ} - with fInPath[0] do AddPoint(Point64( - X - fNorms[0].X * fGroupDelta, - Y - fNorms[0].Y * fGroupDelta, - Z)); -{$ELSE} - with fInPath[0] do AddPoint(Point64( - X - fNorms[0].X * fGroupDelta, - Y - fNorms[0].Y * fGroupDelta)); -{$ENDIF} - AddPoint(GetPerpendic(fInPath[0], fNorms[0], fGroupDelta)); - end; + etButt: DoBevel(0, 0); etRound: DoRound(0,0, PI); else DoSquare(0, 0); end; @@ -484,21 +633,15 @@ procedure TClipperOffset.OffsetOpenPath; fNorms[0] := fNorms[highI]; // do the line end cap + + if Assigned(fDeltaCallback64) then + fGroupDelta := fDeltaCallback64(fInPath, fNorms, highI, highI); + if Abs(fGroupDelta) < Tolerance then + begin + AddPoint(fInPath[highI]); + end else case fEndType of - etButt: - begin -{$IFDEF USINGZ} - with fInPath[highI] do AddPoint(Point64( - X - fNorms[highI].X *fGroupDelta, - Y - fNorms[highI].Y *fGroupDelta, - Z)); -{$ELSE} - with fInPath[highI] do AddPoint(Point64( - X - fNorms[highI].X *fGroupDelta, - Y - fNorms[highI].Y *fGroupDelta)); -{$ENDIF} - AddPoint(GetPerpendic(fInPath[highI], fNorms[highI], fGroupDelta)); - end; + etButt: DoBevel(highI, highI); etRound: DoRound(highI,highI, PI); else DoSquare(highI, highI); end; @@ -507,16 +650,23 @@ procedure TClipperOffset.OffsetOpenPath; k := 0; for i := highI downto 1 do //and stop at 1! OffsetPoint(i, k); + + UpdateSolution; end; //------------------------------------------------------------------------------ procedure TClipperOffset.ExecuteInternal(delta: Double); var - i: integer; + i,j: integer; group: TGroup; + pathsReversed: Boolean; + fillRule: TFillRule; + dummy: TPaths64; begin fSolution := nil; + fSolutionLen := 0; if fGroupList.Count = 0 then Exit; + SetLength(fSolution, CalcSolutionCapacity); fMinLenSqrd := 1; if abs(delta) < Tolerance then @@ -525,7 +675,11 @@ procedure TClipperOffset.ExecuteInternal(delta: Double); for i := 0 to fGroupList.Count -1 do begin group := TGroup(fGroupList[i]); - AppendPaths(fSolution, group.paths); + for j := 0 to High(group.paths) do + begin + fSolution[fSolutionLen] := group.paths[i]; + inc(fSolutionLen); + end; end; Exit; end; @@ -542,75 +696,69 @@ procedure TClipperOffset.ExecuteInternal(delta: Double); group := TGroup(fGroupList[i]); DoGroupOffset(group); end; + SetLength(fSolution, fSolutionLen); + + pathsReversed := CheckReverseOrientation(); + if pathsReversed then + fillRule := frNegative else + fillRule := frPositive; // clean up self-intersections ... with TClipper64.Create do try PreserveCollinear := fPreserveCollinear; // the solution should retain the orientation of the input - ReverseSolution := - fReverseSolution <> TGroup(fGroupList[0]).reversed; + ReverseSolution := fReverseSolution <> pathsReversed; AddSubject(fSolution); - if TGroup(fGroupList[0]).reversed then - Execute(ctUnion, frNegative, fSolution) else - Execute(ctUnion, frPositive, fSolution); + if assigned(fSolutionTree) then + Execute(ctUnion, fillRule, fSolutionTree, dummy); + Execute(ctUnion, fillRule, fSolution); finally free; end; end; //------------------------------------------------------------------------------ -procedure TClipperOffset.Execute(delta: Double; out solution: TPaths64); +function TClipperOffset.CheckReverseOrientation: Boolean; var i: integer; - group: TGroup; begin - fSolution := nil; + Result := false; + // find the orientation of the first closed path + for i := 0 to fGroupList.Count -1 do + with TGroup(fGroupList[i]) do + if endType = etPolygon then + begin + Result := reversed; + break; + end; +end; +//------------------------------------------------------------------------------ + +procedure TClipperOffset.Execute(delta: Double; out solution: TPaths64); +begin + solution := nil; + fSolutionTree := nil; + if fGroupList.Count = 0 then Exit; ExecuteInternal(delta); + solution := fSolution; +end; +//------------------------------------------------------------------------------ - // clean up self-intersections ... - with TClipper64.Create do - try - PreserveCollinear := fPreserveCollinear; - // the solution should retain the orientation of the input - ReverseSolution := - fReverseSolution <> TGroup(fGroupList[0]).reversed; - AddSubject(fSolution); - if TGroup(fGroupList[0]).reversed then - Execute(ctUnion, frNegative, solution) else - Execute(ctUnion, frPositive, solution); - finally - free; - end; +procedure TClipperOffset.Execute(DeltaCallback: TDeltaCallback64; out solution: TPaths64); +begin + fDeltaCallback64 := DeltaCallback; + Execute(1.0, solution); end; //------------------------------------------------------------------------------ procedure TClipperOffset.Execute(delta: Double; polytree: TPolyTree64); -var - i: integer; - group: TGroup; - dummy: TPaths64; begin - fSolution := nil; if not Assigned(polytree) then Raise EClipper2LibException(rsClipper_PolyTreeErr); - + fSolutionTree := polytree; + fSolutionTree.Clear; ExecuteInternal(delta); - - // clean up self-intersections ... - with TClipper64.Create do - try - PreserveCollinear := fPreserveCollinear; - // the solution should retain the orientation of the input - ReverseSolution := - fReverseSolution <> TGroup(fGroupList[0]).reversed; - AddSubject(fSolution); - if TGroup(fGroupList[0]).reversed then - Execute(ctUnion, frNegative, polytree, dummy) else - Execute(ctUnion, frPositive, polytree, dummy); - finally - free; - end; end; //------------------------------------------------------------------------------ @@ -691,14 +839,40 @@ function ReflectPoint(const pt, pivot: TPointD): TPointD; end; //------------------------------------------------------------------------------ +procedure TClipperOffset.DoBevel(j, k: Integer); +var + absDelta: double; +begin + if k = j then + begin + absDelta := abs(fGroupDelta); + 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); + end else + begin + 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); + end; +end; +//------------------------------------------------------------------------------ + procedure TClipperOffset.DoSquare(j, k: Integer); var vec, pt1,pt2,pt3,pt4, pt,ptQ : TPointD; + absDelta: double; begin if k = j then begin - vec.X := fNorms[0].Y; //squaring a line end - vec.Y := -fNorms[0].X; + vec.X := fNorms[j].Y; //squaring a line end + vec.Y := -fNorms[j].X; end else begin // using the reciprocal of unit normals (as unit vectors) @@ -708,9 +882,10 @@ procedure TClipperOffset.DoSquare(j, k: Integer); PointD(fNorms[j].Y, -fNorms[j].X)); end; + absDelta := Abs(fGroupDelta); // now offset the original vertex delta units along unit vector ptQ := PointD(fInPath[j]); - ptQ := TranslatePoint(ptQ, fAbsGrpDelta * vec.X, fAbsGrpDelta * vec.Y); + ptQ := TranslatePoint(ptQ, absDelta * vec.X, absDelta * vec.Y); // get perpendicular vertices pt1 := TranslatePoint(ptQ, fGroupDelta * vec.Y, fGroupDelta * -vec.X); @@ -770,9 +945,29 @@ procedure TClipperOffset.DoMiter(j, k: Integer; cosA: Double); procedure TClipperOffset.DoRound(j, k: Integer; angle: double); var i, steps: Integer; + absDelta, arcTol, stepsPer360: double; pt: TPoint64; offDist: TPointD; begin + + if Assigned(fDeltaCallback64) then + begin + // 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 + //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 + fStepSin := sin(TwoPi/stepsPer360); + fStepCos := cos(TwoPi/stepsPer360); + if (fGroupDelta < 0.0) then fStepSin := -fStepSin; + fStepsPerRad := stepsPer360 / TwoPi; + end; + // nb: angles may be negative but this will always be a convex join pt := fInPath[j]; offDist := ScalePoint(fNorms[k], fGroupDelta); @@ -817,34 +1012,43 @@ procedure TClipperOffset.OffsetPoint(j: Integer; var k: integer); if (sinA > 1.0) then sinA := 1.0 else if (sinA < -1.0) then sinA := -1.0; + if Assigned(fDeltaCallback64) then + begin + fGroupDelta := fDeltaCallback64(fInPath, fNorms, j, k); + if TGroup(fGroupList[0]).reversed then fGroupDelta := -fGroupDelta; + end; - if (cosA > 0.99) then // almost straight - less than 8 degrees + if Abs(fGroupDelta) <= Tolerance then begin - AddPoint(GetPerpendic(fInPath[j], fNorms[k], fGroupDelta)); - if (cosA < 0.9998) then // greater than 1 degree (#424) - AddPoint(GetPerpendic(fInPath[j], fNorms[j], fGroupDelta)); // (#418) - end - else if (cosA > -0.99) and (sinA * fGroupDelta < 0) then // is concave + AddPoint(fInPath[j]); + Exit; + end; + + //test for concavity first (#593) + if (cosA > -0.99) and (sinA * fGroupDelta < 0) then begin + // is concave AddPoint(GetPerpendic(fInPath[j], fNorms[k], fGroupDelta)); // this extra point is the only (simple) way to ensure that // path reversals are fully cleaned with the trailing clipper AddPoint(fInPath[j]); // (#405) AddPoint(GetPerpendic(fInPath[j], fNorms[j], fGroupDelta)); end - //else convex of one sort or another - else if (fJoinType = jtRound) then - DoRound(j, k, ArcTan2(sinA, cosA)) + else if (cosA > 0.999) and (fJoinType <> jtRound) then + begin + // almost straight - less than 2.5 degree (#424, #482, #526 & #724) + DoMiter(j, k, cosA); + end else if (fJoinType = jtMiter) then begin - // miter unless the angle is so acute the miter would exceeds ML + // miter unless the angle is sufficiently acute to exceed ML if (cosA > fTmpLimit -1) then DoMiter(j, k, cosA) else DoSquare(j, k); end - // don't bother squaring angles that deviate < ~20 degrees because - // squaring will be indistinguishable from mitering and just be a lot slower - else if (cosA > 0.9) then - DoMiter(j, k, cosA) + else if (fJoinType = jtRound) then + DoRound(j, k, ArcTan2(sinA, cosA)) + else if (fJoinType = jtBevel) then + DoBevel(j, k) else DoSquare(j, k); diff --git a/Delphi/Clipper2Lib/Clipper.RectClip.pas b/Delphi/Clipper2Lib/Clipper.RectClip.pas index d332b6ae..c687a1fc 100644 --- a/Delphi/Clipper2Lib/Clipper.RectClip.pas +++ b/Delphi/Clipper2Lib/Clipper.RectClip.pas @@ -2,7 +2,7 @@ (******************************************************************************* * Author : Angus Johnson * -* Date : 13 February 2023 * +* Date : 9 September 2023 * * Website : http://www.angusj.com * * Copyright : Angus Johnson 2010-2023 * * Purpose : FAST rectangular clipping * @@ -32,7 +32,7 @@ TOutPt2 = record prev: POutPt2; end; - TRectClip = class + TRectClip64 = class procedure ExecuteInternal(const path: TPath64); function GetPath(resultIdx: integer): TPath64; protected @@ -57,11 +57,10 @@ TRectClip = class public constructor Create(const rect: TRect64); destructor Destroy; override; - function Execute(const paths: TPaths64; - convexOnly: Boolean = false): TPaths64; + function Execute(const paths: TPaths64): TPaths64; end; - TRectClipLines = class(TRectClip) + TRectClipLines64 = class(TRectClip64) private procedure ExecuteInternal(const path: TPath64); function GetPath(resultIdx: integer): TPath64; @@ -115,101 +114,152 @@ function GetLocation(const rec: TRect64; const pt: TPoint64; end; //------------------------------------------------------------------------------ +function IsHorizontal(pt1: TPoint64; pt2: TPoint64): Boolean; + {$IFDEF INLINING} inline; {$ENDIF} +begin + Result := pt1.Y = pt2.Y; +end; +//------------------------------------------------------------------------------ + +function GetSegmentIntersection(p1: TPoint64; +p2: TPoint64; p3: TPoint64; p4: TPoint64; out ip: TPoint64): Boolean; +var + res1, res2, res3, res4: double; +begin + res1 := CrossProduct(p1, p3, p4); + res2 := CrossProduct(p2, p3, p4); + if (res1 = 0) then + begin + ip := p1; + if (res2 = 0) then + result := false // segments are collinear + else if PointsEqual(p1, p3) or PointsEqual(p1, p4) then + result := true + else if (IsHorizontal(p3, p4)) then + result := ((p1.X > p3.X) = (p1.X < p4.X)) + else + result := (p1.Y > p3.Y) = (p1.Y < p4.Y); + Exit; + end; + + if (res2 = 0) then + begin + ip := p2; + if PointsEqual(p2, p3) or PointsEqual(p2, p4) then + Result := true + else if (IsHorizontal(p3, p4)) then + Result := ((p2.X > p3.X) = (p2.X < p4.X)) + else Result := ((p2.Y > p3.Y) = (p2.Y < p4.Y)); + Exit; + end; + + if ((res1 > 0) = (res2 > 0)) then + begin + //ip := Point64(0, 0); + Result := false; + Exit; + end; + + res3 := CrossProduct(p3, p1, p2); + res4 := CrossProduct(p4, p1, p2); + if (res3 = 0) then + begin + ip := p3; + if PointsEqual(p3, p1) or PointsEqual(p3, p2) then + Result := true + else if (IsHorizontal(p1, p2)) then + Result := (p3.X > p1.X) = (p3.X < p2.X) + else + Result := (p3.Y > p1.Y) = (p3.Y < p2.Y); + end + else if (res4 = 0) then + begin + ip := p4; + if PointsEqual(p4, p1) or PointsEqual(p4, p2) then + Result := true + else if (IsHorizontal(p1, p2)) then + Result := (p4.X > p1.X) = (p4.X < p2.X) + else + Result := (p4.Y > p1.Y) = (p4.Y < p2.Y); + end + else if ((res3 > 0) = (res4 > 0)) then + begin + //ip := Point64(0, 0); + Result := false; + end + else + // segments must intersect to get here + Result := GetIntersectPoint(p1, p2, p3, p4, ip); +end; +//------------------------------------------------------------------------------ + function GetIntersection(const rectPath: TPath64; const p, p2: TPoint64; var loc: TLocation; out ip: TPoint64): Boolean; begin // gets the intersection closest to 'p' // when Result = false, loc will remain unchanged - Result := false; + Result := True; case loc of locLeft: - if SegmentsIntersect(p, p2, rectPath[0], rectPath[3], true) then - GetIntersectPoint(p, p2, rectPath[0], rectPath[3], ip) + if GetSegmentIntersection(p, p2, rectPath[0], rectPath[3], ip) then + //Result := True else if (p.Y < rectPath[0].Y) and - SegmentsIntersect(p, p2, rectPath[0], rectPath[1], true) then - begin - GetIntersectPoint(p, p2, rectPath[0], rectPath[1], ip); - loc := locTop; - end - else if SegmentsIntersect(p, p2, rectPath[2], rectPath[3], true) then - begin - GetIntersectPoint(p, p2, rectPath[2], rectPath[3], ip); - loc := locBottom; - end - else Exit; + GetSegmentIntersection(p, p2, rectPath[0], rectPath[1], ip) then + loc := locTop + else if GetSegmentIntersection(p, p2, rectPath[2], rectPath[3], ip) then + loc := locBottom + else + Result := False; + locRight: - if SegmentsIntersect(p, p2, rectPath[1], rectPath[2], true) then - GetIntersectPoint(p, p2, rectPath[1], rectPath[2], ip) + if GetSegmentIntersection(p, p2, rectPath[1], rectPath[2], ip) then + //Result := True else if (p.Y < rectPath[0].Y) and - SegmentsIntersect(p, p2, rectPath[0], rectPath[1], true) then - begin - GetIntersectPoint(p, p2, rectPath[0], rectPath[1], ip); - loc := locTop; - end - else if SegmentsIntersect(p, p2, rectPath[2], rectPath[3], true) then - begin - GetIntersectPoint(p, p2, rectPath[2], rectPath[3], ip); - loc := locBottom; - end - else Exit; + GetSegmentIntersection(p, p2, rectPath[0], rectPath[1], ip) then + loc := locTop + else if GetSegmentIntersection(p, p2, rectPath[2], rectPath[3], ip) then + loc := locBottom + else + Result := False; + locTop: - if SegmentsIntersect(p, p2, rectPath[0], rectPath[1], true) then - GetIntersectPoint(p, p2, rectPath[0], rectPath[1], ip) + if GetSegmentIntersection(p, p2, rectPath[0], rectPath[1], ip) then + //Result := True else if (p.X < rectPath[0].X) and - SegmentsIntersect(p, p2, rectPath[0], rectPath[3], true) then - begin - GetIntersectPoint(p, p2, rectPath[0], rectPath[3], ip); - loc := locLeft; - end + GetSegmentIntersection(p, p2, rectPath[0], rectPath[3], ip) then + loc := locLeft else if (p.X > rectPath[1].X) and - SegmentsIntersect(p, p2, rectPath[1], rectPath[2], true) then - begin - GetIntersectPoint(p, p2, rectPath[1], rectPath[2], ip); - loc := locRight; - end - else Exit; + GetSegmentIntersection(p, p2, rectPath[1], rectPath[2], ip) then + loc := locRight + else + Result := False; + locBottom: - if SegmentsIntersect(p, p2, rectPath[2], rectPath[3], true) then - GetIntersectPoint(p, p2, rectPath[2], rectPath[3], ip) + if GetSegmentIntersection(p, p2, rectPath[2], rectPath[3], ip) then + //Result := True else if (p.X < rectPath[3].X) and - SegmentsIntersect(p, p2, rectPath[0], rectPath[3], true) then - begin - GetIntersectPoint(p, p2, rectPath[0], rectPath[3], ip); - loc := locLeft; - end + GetSegmentIntersection(p, p2, rectPath[0], rectPath[3], ip) then + loc := locLeft else if (p.X > rectPath[2].X) and - SegmentsIntersect(p, p2, rectPath[1], rectPath[2], true) then - begin - GetIntersectPoint(p, p2, rectPath[1], rectPath[2], ip); - loc := locRight; - end - else Exit; + GetSegmentIntersection(p, p2, rectPath[1], rectPath[2], ip) then + loc := locRight + else + Result := False; else // loc = rInside begin - if SegmentsIntersect(p, p2, rectPath[0], rectPath[3], true) then - begin - GetIntersectPoint(p, p2, rectPath[0], rectPath[3], ip); - loc := locLeft; - end else if SegmentsIntersect(p, p2, rectPath[0], rectPath[1], true) then - begin - GetIntersectPoint(p, p2, rectPath[0], rectPath[1], ip); - loc := locTop; - end - else if SegmentsIntersect(p, p2, rectPath[1], rectPath[2], true) then - begin - GetIntersectPoint(p, p2, rectPath[1], rectPath[2], ip); - loc := locRight; - end - else if SegmentsIntersect(p, p2, rectPath[2], rectPath[3], true) then - begin - GetIntersectPoint(p, p2, rectPath[2], rectPath[3], ip); - loc := locBottom; - end - else Exit; + if GetSegmentIntersection(p, p2, rectPath[0], rectPath[3], ip) then + loc := locLeft + else if GetSegmentIntersection(p, p2, rectPath[0], rectPath[1], ip) then + loc := locTop + else if GetSegmentIntersection(p, p2, rectPath[1], rectPath[2], ip) then + loc := locRight + else if GetSegmentIntersection(p, p2, rectPath[2], rectPath[3], ip) then + loc := locBottom + else + Result := False; end; end; - Result := true; end; //------------------------------------------------------------------------------ @@ -377,10 +427,10 @@ function IsHeadingClockwise(const pt1, pt2: TPoint64; edgeIdx: integer): Boolean end; //------------------------------------------------------------------------------ -// TRectClip class +// TRectClip64 class //------------------------------------------------------------------------------ -constructor TRectClip.Create(const rect: TRect64); +constructor TRectClip64.Create(const rect: TRect64); begin fResults := TList.Create; @@ -392,7 +442,7 @@ constructor TRectClip.Create(const rect: TRect64); end; //------------------------------------------------------------------------------ -destructor TRectClip.Destroy; +destructor TRectClip64.Destroy; begin fStartLocs.Free; fResults.Free; @@ -414,7 +464,7 @@ procedure DisposeOps(op: POutPt2); end; //------------------------------------------------------------------------------ -procedure TRectClip.DisposeResults; +procedure TRectClip64.DisposeResults; var i: integer; begin @@ -424,7 +474,7 @@ procedure TRectClip.DisposeResults; end; //------------------------------------------------------------------------------ -function TRectClip.Add(const pt: TPoint64; startNewPath: Boolean): POutPt2; +function TRectClip64.Add(const pt: TPoint64; startNewPath: Boolean): POutPt2; var currIdx: integer; prevOp: POutPt2; @@ -462,7 +512,7 @@ function TRectClip.Add(const pt: TPoint64; startNewPath: Boolean): POutPt2; end; //------------------------------------------------------------------------------ -procedure TRectClip.AddCorner(prev, curr: TLocation); +procedure TRectClip64.AddCorner(prev, curr: TLocation); var cnrIdx: integer; begin @@ -474,7 +524,7 @@ procedure TRectClip.AddCorner(prev, curr: TLocation); end; //------------------------------------------------------------------------------ -procedure TRectClip.AddCorner(var loc: TLocation; isClockwise: Boolean); +procedure TRectClip64.AddCorner(var loc: TLocation; isClockwise: Boolean); begin if (isClockwise) then begin @@ -488,7 +538,7 @@ procedure TRectClip.AddCorner(var loc: TLocation; isClockwise: Boolean); end; //------------------------------------------------------------------------------ -procedure TRectClip.GetNextLocation(const path: TPath64; +procedure TRectClip64.GetNextLocation(const path: TPath64; var loc: TLocation; var i: integer; highI: integer); begin case loc of @@ -564,7 +614,7 @@ function Path1ContainsPath2(const path1, path2: TPath64): Boolean; end; //------------------------------------------------------------------------------ -function TRectClip.Execute(const paths: TPaths64; convexOnly: Boolean): TPaths64; +function TRectClip64.Execute(const paths: TPaths64): TPaths64; var i,j, len: integer; path: TPath64; @@ -587,12 +637,9 @@ function TRectClip.Execute(const paths: TPaths64; convexOnly: Boolean): TPaths64 end; ExecuteInternal(path); - if not convexOnly then - begin - CheckEdges; - for j := 0 to 3 do - TidyEdgePair(j, fEdges[j*2], fEdges[j*2 +1]); - end; + CheckEdges; + for j := 0 to 3 do + TidyEdgePair(j, fEdges[j*2], fEdges[j*2 +1]); for j := 0 to fResults.Count -1 do AppendPath(Result, GetPath(j)); @@ -605,7 +652,7 @@ function TRectClip.Execute(const paths: TPaths64; convexOnly: Boolean): TPaths64 end; //------------------------------------------------------------------------------ -procedure TRectClip.ExecuteInternal(const path: TPath64); +procedure TRectClip64.ExecuteInternal(const path: TPath64); var i,highI : integer; prevPt,ip,ip2 : TPoint64; @@ -704,7 +751,7 @@ procedure TRectClip.ExecuteInternal(const path: TPath64); // intersect pt but we'll also need the first intersect pt (ip2) loc := prevLoc; GetIntersection(fRectPath, prevPt, path[i], loc, ip2); - if (prevCrossLoc <> locInside) then + if (prevCrossLoc <> locInside) and (prevCrossLoc <> loc) then //#579 AddCorner(prevCrossLoc, loc); if (firstCrossLoc = locInside) then @@ -782,7 +829,7 @@ procedure TRectClip.ExecuteInternal(const path: TPath64); end; //------------------------------------------------------------------------------ -procedure TRectClip.CheckEdges; +procedure TRectClip64.CheckEdges; var i,j: integer; edgeSet1, edgeSet2, combinedSet: Cardinal; @@ -841,7 +888,7 @@ procedure TRectClip.CheckEdges; end; //------------------------------------------------------------------------------ -procedure TRectClip.TidyEdgePair(idx: integer; var cw, ccw: TOutPtArray); +procedure TRectClip64.TidyEdgePair(idx: integer; var cw, ccw: TOutPtArray); var isHorz, cwIsTowardLarger: Boolean; i, j, highJ, newIdx: integer; @@ -862,7 +909,7 @@ procedure TRectClip.TidyEdgePair(idx: integer; var cw, ccw: TOutPtArray); p1 := cw[i]; if not Assigned(p1) or (p1.next = p1.prev) then begin - cw[i].edge := nil; + cw[i] := nil; inc(i); j := 0; Continue; @@ -1025,7 +1072,7 @@ procedure TRectClip.TidyEdgePair(idx: integer; var cw, ccw: TOutPtArray); end; //------------------------------------------------------------------------------ -function TRectClip.GetPath(resultIdx: integer): TPath64; +function TRectClip64.GetPath(resultIdx: integer): TPath64; var i, len: integer; op, op2: POutPt2; @@ -1057,10 +1104,10 @@ function TRectClip.GetPath(resultIdx: integer): TPath64; end; //------------------------------------------------------------------------------ -// TRectClipLines +// TRectClipLines64 //------------------------------------------------------------------------------ -function TRectClipLines.Execute(const paths: TPaths64): TPaths64; +function TRectClipLines64.Execute(const paths: TPaths64): TPaths64; var i,j, len: integer; pathrec: TRect64; @@ -1089,7 +1136,7 @@ function TRectClipLines.Execute(const paths: TPaths64): TPaths64; end; //------------------------------------------------------------------------------ -procedure TRectClipLines.ExecuteInternal(const path: TPath64); +procedure TRectClipLines64.ExecuteInternal(const path: TPath64); var i, highI : integer; prevPt,ip,ip2 : TPoint64; @@ -1155,7 +1202,7 @@ procedure TRectClipLines.ExecuteInternal(const path: TPath64); end; //------------------------------------------------------------------------------ -function TRectClipLines.GetPath(resultIdx: integer): TPath64; +function TRectClipLines64.GetPath(resultIdx: integer): TPath64; var i, len: integer; op: POutPt2; diff --git a/Delphi/Clipper2Lib/Clipper.inc b/Delphi/Clipper2Lib/Clipper.inc index 066d5dc7..5b15f920 100644 --- a/Delphi/Clipper2Lib/Clipper.inc +++ b/Delphi/Clipper2Lib/Clipper.inc @@ -14,21 +14,21 @@ {$DEFINE INLINING} {$MODE DELPHI} {$ELSE} - {$IF CompilerVersion < 14} + {$IF COMPILERVERSION < 14} Requires Delphi version 6 or above. {$IFEND} - {$IF CompilerVersion >= 18} //Delphi 2007 + {$IF COMPILERVERSION >= 18} //Delphi 2007 {$DEFINE RECORD_METHODS} {$DEFINE STRICT} - {$IF CompilerVersion >= 19} //Delphi 2009 + {$IF COMPILERVERSION >= 19} //Delphi 2009 //While "inlining" is supported from D2005, it's buggy (see QC41166) until D2009 {$DEFINE INLINING} - {$IFEND} - {$IF COMPILERVERSION >= 23} //Delphi XE2+ - {$DEFINE XPLAT_GENERICS} - {$DEFINE ROUNDINGMODE} - {$IF COMPILERVERSION >= 24} //Delphi XE3+ - {$LEGACYIFEND ON} + {$IF COMPILERVERSION >= 23} //Delphi XE2+ + {$DEFINE XPLAT_GENERICS} + {$DEFINE ROUNDINGMODE} + {$IF COMPILERVERSION >= 24} //Delphi XE3+ + {$LEGACYIFEND ON} + {$IFEND} {$IFEND} {$IFEND} {$IFEND} diff --git a/Delphi/Clipper2Lib/Clipper.pas b/Delphi/Clipper2Lib/Clipper.pas index 0abb2b84..1d34c92b 100644 --- a/Delphi/Clipper2Lib/Clipper.pas +++ b/Delphi/Clipper2Lib/Clipper.pas @@ -2,7 +2,7 @@ (******************************************************************************* * Author : Angus Johnson * -* Date : 23 March 2023 * +* Date : 18 November 2023 * * Website : http://www.angusj.com * * Copyright : Angus Johnson 2010-2023 * * Purpose : This module provides a simple interface to the Clipper Library * @@ -14,12 +14,11 @@ interface {$I Clipper.inc} uses - Math, SysUtils, + Math, SysUtils, Classes, Clipper.Core, Clipper.Engine, Clipper.Offset, Clipper.RectClip; -// Redeclare here a number of structures defined in -// other units so those units won't need to be declared -// just to use the following functions. +// A number of structures defined in other units are redeclared here +// so those units won't also need to be declared in your own units clauses. type TClipper = Clipper.Engine.TClipper64; TClipper64 = Clipper.Engine.TClipper64; @@ -96,23 +95,18 @@ function InflatePaths(const paths: TPathsD; delta: Double; ArcTolerance: double = 0.0): TPathsD; overload; // RectClip: for closed paths only (otherwise use RectClipLines) -// much faster when only clipping convex polygons -function ExecuteRectClip(const rect: TRect64; const path: TPath64; - convexOnly: Boolean = false): TPath64; overload; -function ExecuteRectClip(const rect: TRect64; const paths: TPaths64; - convexOnly: Boolean = false): TPaths64; overload; -function ExecuteRectClip(const rect: TRectD; const path: TPathD; - convexOnly: Boolean = false; precision: integer = 2): TPathD; overload; -function ExecuteRectClip(const rect: TRectD; const paths: TPathsD; - convexOnly: Boolean = false; precision: integer = 2): TPathsD; overload; - -function ExecuteRectClipLines(const rect: TRect64; +function RectClip(const rect: TRect64; const path: TPath64): TPath64; overload; +function RectClip(const rect: TRect64; const paths: TPaths64): TPaths64; overload; +function RectClip(const rect: TRectD; const path: TPathD; precision: integer = 2): TPathD; overload; +function RectClip(const rect: TRectD; const paths: TPathsD; precision: integer = 2): TPathsD; overload; + +function RectClipLines(const rect: TRect64; const path: TPath64): TPaths64; overload; -function ExecuteRectClipLines(const rect: TRect64; +function RectClipLines(const rect: TRect64; const paths: TPaths64): TPaths64; overload; -function ExecuteRectClipLines(const rect: TRectD; const path: TPathD; +function RectClipLines(const rect: TRectD; const path: TPathD; precision: integer = 2): TPathsD; overload; -function ExecuteRectClipLines(const rect: TRectD; const paths: TPathsD; +function RectClipLines(const rect: TRectD; const paths: TPathsD; precision: integer = 2): TPathsD; overload; function TranslatePath(const path: TPath64; dx, dy: Int64): TPath64; overload; @@ -128,6 +122,19 @@ function MinkowskiSum(const pattern, path: TPathD; function PolyTreeToPaths64(PolyTree: TPolyTree64): TPaths64; function PolyTreeToPathsD(PolyTree: TPolyTreeD): TPathsD; +function PathToString(const p: TPath64; + indentSpaces: integer = 0; pointsPerRow: integer = 0): string; overload; +function PathToString(const p: TPathD; decimals: integer; + indentSpaces: integer = 0; pointsPerRow: integer = 0): string; overload; +function PathsToString(const p: TPaths64; + indentSpaces: integer = 0; pointsPerRow: integer = 0): string; overload; +function PathsToString(const p: TPathsD; decimals: integer; + indentSpaces: integer = 0; pointsPerRow: integer = 0): string; overload; + +//ShowPolyTreeStructure: only useful when debugging +procedure ShowPolyTreeStructure(polytree: TPolyTree64; strings: TStrings); overload; +procedure ShowPolyTreeStructure(polytree: TPolyTreeD; strings: TStrings); overload; + function MakePath(const ints: array of Int64): TPath64; overload; function MakePathD(const dbls: array of double): TPathD; overload; @@ -137,14 +144,16 @@ function TrimCollinear(const path: TPathD; precision: integer; isOpenPath: Boolean = false): TPathD; overload; function PointInPolygon(const pt: TPoint64; const polygon: TPath64): - TPointInPolygonResult; {$IFDEF INLINE} inline; {$ENDIF} + TPointInPolygonResult; function SimplifyPath(const path: TPath64; - epsilon: double; isOpenPath: Boolean = false): TPath64; - {$IFDEF INLINE} inline; {$ENDIF} + shapeTolerance: double; isClosedPath: Boolean = true): TPath64; overload; function SimplifyPaths(const paths: TPaths64; - epsilon: double; isOpenPath: Boolean = false): TPaths64; - {$IFDEF INLINE} inline; {$ENDIF} + shapeTolerance: double; isClosedPath: Boolean = true): TPaths64; overload; +function SimplifyPath(const path: TPathD; shapeTolerance: double; + isClosedPath: Boolean = true; decimalPrecision: integer = 2): TPathD; overload; +function SimplifyPaths(const paths: TPathsD; shapeTolerance: double; + isClosedPath: Boolean = true; decimalPrecision: integer = 2): TPathsD; overload; implementation @@ -154,6 +163,38 @@ implementation //------------------------------------------------------------------------------ //------------------------------------------------------------------------------ +{$IFDEF USINGZ} +function MakePath(const ints: array of Int64): TPath64; +var + i, len: integer; +begin + len := length(ints) div 3; + SetLength(Result, len); + for i := 0 to len -1 do + begin + Result[i].X := ints[i*3]; + Result[i].Y := ints[i*3 +1]; + Result[i].z := ints[i*3 +2]; + end; +end; +//------------------------------------------------------------------------------ + +function MakePathD(const dbls: array of double): TPathD; overload; +var + i, len: integer; +begin + len := length(dbls) div 3; + SetLength(Result, len); + for i := 0 to len -1 do + begin + Result[i].X := dbls[i*3]; + Result[i].Y := dbls[i*3 +1]; + Result[i].Z := Round(dbls[i*3 +2]); + end; +end; +//------------------------------------------------------------------------------ +{$ELSE} + function MakePath(const ints: array of Int64): TPath64; var i, len: integer; @@ -181,6 +222,7 @@ function MakePathD(const dbls: array of double): TPathD; overload; end; end; //------------------------------------------------------------------------------ +{$ENDIF} procedure AddPolyNodeToPaths(Poly: TPolyPath64; var Paths: TPaths64); var @@ -376,36 +418,34 @@ function InflatePaths(const paths: TPathsD; delta: Double; end; //------------------------------------------------------------------------------ -function ExecuteRectClip(const rect: TRect64; - const path: TPath64; convexOnly: Boolean): TPath64; +function RectClip(const rect: TRect64; + const path: TPath64): TPath64; var paths: TPaths64; begin SetLength(paths, 1); paths[0] := path; - paths := ExecuteRectClip(rect, paths, convexOnly); + paths := RectClip(rect, paths); if Assigned(paths) then Result := paths[0] else Result := nil; end; //------------------------------------------------------------------------------ -function ExecuteRectClip(const rect: TRect64; - const paths: TPaths64; convexOnly: Boolean): TPaths64; +function RectClip(const rect: TRect64; const paths: TPaths64): TPaths64; begin Result := nil; if rect.IsEmpty then Exit; - with TRectClip.Create(rect) do + with TRectClip64.Create(rect) do try - Result := Execute(paths, convexOnly); + Result := Execute(paths); finally Free; end; end; //------------------------------------------------------------------------------ -function ExecuteRectClip(const rect: TRectD; const path: TPathD; - convexOnly: Boolean; precision: integer): TPathD; +function RectClip(const rect: TRectD; const path: TPathD; precision: integer): TPathD; var scale: double; tmpPath: TPath64; @@ -417,13 +457,12 @@ function ExecuteRectClip(const rect: TRectD; const path: TPathD; scale := Math.Power(10, precision); rec := Rect64(ScaleRect(rect, scale)); tmpPath := ScalePath(path, scale); - tmpPath := ExecuteRectClip(rec, tmpPath, convexOnly); + tmpPath := RectClip(rec, tmpPath); Result := ScalePathD(tmpPath, 1/scale); end; //------------------------------------------------------------------------------ -function ExecuteRectClip(const rect: TRectD; const paths: TPathsD; - convexOnly: Boolean; precision: integer): TPathsD; +function RectClip(const rect: TRectD; const paths: TPathsD; precision: integer): TPathsD; var scale: double; tmpPaths: TPaths64; @@ -434,7 +473,7 @@ function ExecuteRectClip(const rect: TRectD; const paths: TPathsD; rec := Rect64(ScaleRect(rect, scale)); tmpPaths := ScalePaths(paths, scale); - with TRectClip.Create(rec) do + with TRectClip64.Create(rec) do try tmpPaths := Execute(tmpPaths); finally @@ -444,14 +483,14 @@ function ExecuteRectClip(const rect: TRectD; const paths: TPathsD; end; //------------------------------------------------------------------------------ -function ExecuteRectClipLines(const rect: TRect64; const path: TPath64): TPaths64; +function RectClipLines(const rect: TRect64; const path: TPath64): TPaths64; var tmp: TPaths64; begin Result := nil; SetLength(tmp, 1); tmp[0] := path; - with TRectClipLines.Create(rect) do + with TRectClipLines64.Create(rect) do try Result := Execute(tmp); finally @@ -460,11 +499,11 @@ function ExecuteRectClipLines(const rect: TRect64; const path: TPath64): TPaths6 end; //------------------------------------------------------------------------------ -function ExecuteRectClipLines(const rect: TRect64; const paths: TPaths64): TPaths64; +function RectClipLines(const rect: TRect64; const paths: TPaths64): TPaths64; begin Result := nil; if rect.IsEmpty then Exit; - with TRectClipLines.Create(rect) do + with TRectClipLines64.Create(rect) do try Result := Execute(paths); finally @@ -473,7 +512,7 @@ function ExecuteRectClipLines(const rect: TRect64; const paths: TPaths64): TPath end; //------------------------------------------------------------------------------ -function ExecuteRectClipLines(const rect: TRectD; +function RectClipLines(const rect: TRectD; const path: TPathD; precision: integer): TPathsD; var scale: double; @@ -487,12 +526,12 @@ function ExecuteRectClipLines(const rect: TRectD; scale := Math.Power(10, precision); rec := Rect64(ScaleRect(rect, scale)); tmpPath := ScalePath(path, scale); - tmpPaths := ExecuteRectClipLines(rec, tmpPath); + tmpPaths := RectClipLines(rec, tmpPath); Result := ScalePathsD(tmpPaths, 1/scale); end; //------------------------------------------------------------------------------ -function ExecuteRectClipLines(const rect: TRectD; const paths: TPathsD; +function RectClipLines(const rect: TRectD; const paths: TPathsD; precision: integer = 2): TPathsD; var scale: double; @@ -505,7 +544,7 @@ function ExecuteRectClipLines(const rect: TRectD; const paths: TPathsD; scale := Math.Power(10, precision); rec := Rect64(ScaleRect(rect, scale)); tmpPaths := ScalePaths(paths, scale); - with TRectClipLines.Create(rec) do + with TRectClipLines64.Create(rec) do try tmpPaths := Execute(tmpPaths); finally @@ -583,6 +622,127 @@ function MinkowskiSum(const pattern, path: TPathD; end; //------------------------------------------------------------------------------ +function PathToString(const p: TPath64; + indentSpaces: integer; pointsPerRow: integer): string; +var + i, highI: Integer; + spaces: string; +begin + spaces := StringOfChar(' ', indentSpaces); + Result := spaces; + highI := high(p); + if highI < 0 then Exit; + for i := 0 to highI -1 do + begin + Result := Result + format('%d,%d, ',[p[i].X,p[i].Y]); + if (pointsPerRow > 0) and ((i + 1) mod pointsPerRow = 0) then + Result := Result + #10 + spaces; + end; + Result := Result + format('%d,%d',[p[highI].X,p[highI].Y]); +end; +//------------------------------------------------------------------------------ + +function PathToString(const p: TPathD; decimals: integer; + indentSpaces: integer; pointsPerRow: integer): string; +var + i, highI: Integer; + spaces: string; +begin + spaces := StringOfChar(' ', indentSpaces); + Result := ''; + highI := high(p); + if highI < 0 then Exit; + for i := 0 to highI -1 do + Result := Result + format('%1.*n,%1.*n, ', + [decimals, p[i].X, decimals, p[i].Y]); + Result := Result + format('%1.*n,%1.*n',[ + decimals, p[highI].X, decimals, p[highI].Y]); +end; +//------------------------------------------------------------------------------ + +function PathsToString(const p: TPaths64; + indentSpaces: integer = 0; pointsPerRow: integer = 0): string; +var + i: integer; +begin + Result := ''; + for i := 0 to High(p) do + Result := Result + PathToString(p[i], indentSpaces, pointsPerRow) + #10#10; +end; +//------------------------------------------------------------------------------ + +function PathsToString(const p: TPathsD; decimals: integer; + indentSpaces: integer = 0; pointsPerRow: integer = 0): string; +var + i: integer; +begin + Result := ''; + for i := 0 to High(p) do + Result := Result + PathToString(p[i], indentSpaces, pointsPerRow) + #10#10; +end; +//------------------------------------------------------------------------------ + +procedure ShowPolyPathStructure64(pp: TPolyPath64; level: integer; + strings: TStrings); +var + i: integer; + spaces, plural: string; +begin + spaces := StringOfChar(' ', level * 2); + if pp.Count = 1 then plural := '' else plural := 's'; + if pp.IsHole then + strings.Add(Format('%sA hole containing %d polygon%s', [spaces, pp.Count, plural])) + else + strings.Add(Format('%sA polygon containing %d hole%s', [spaces, pp.Count, plural])); + for i := 0 to pp.Count -1 do + if pp.child[i].Count> 0 then + ShowPolyPathStructure64(pp.child[i], level + 1, strings); +end; +//------------------------------------------------------------------------------ + +procedure ShowPolyTreeStructure(polytree: TPolyTree64; strings: TStrings); +var + i: integer; +begin + if polytree.Count = 1 then + strings.Add('Polytree with just 1 polygon.') else + strings.Add(Format('Polytree with just %d polygons.', [polytree.Count])); + for i := 0 to polytree.Count -1 do + if polytree[i].Count > 0 then + ShowPolyPathStructure64(polytree[i], 1, strings); +end; +//------------------------------------------------------------------------------ + +procedure ShowPolyPathStructureD(pp: TPolyPathD; level: integer; strings: TStrings); +var + i: integer; + spaces, plural: string; +begin + spaces := StringOfChar(' ', level * 2); + if pp.Count = 1 then plural := '' else plural := 's'; + if pp.IsHole then + strings.Add(Format('%sA hole containing %d polygon%s', [spaces, pp.Count, plural])) + else + strings.Add(Format('%sA polygon containing %d hole%s', [spaces, pp.Count, plural])); + for i := 0 to pp.Count -1 do + if pp.child[i].Count> 0 then + ShowPolyPathStructureD(pp.child[i], level + 1, strings); +end; +//------------------------------------------------------------------------------ + +procedure ShowPolyTreeStructure(polytree: TPolyTreeD; strings: TStrings); +var + i: integer; +begin + if polytree.Count = 1 then + strings.Add('Polytree with just 1 polygon.') else + strings.Add(Format('Polytree with just %d polygons.', [polytree.Count])); + for i := 0 to polytree.Count -1 do + if polytree[i].Count > 0 then + ShowPolyPathStructureD(polytree[i], 1, strings); +end; +//------------------------------------------------------------------------------ + function TrimCollinear(const p: TPath64; isOpenPath: Boolean = false): TPath64; var i,j, len: integer; @@ -655,7 +815,19 @@ function PointInPolygon(const pt: TPoint64; end; //------------------------------------------------------------------------------ -function PerpendicDistFromLineSqrd(const pt, line1, line2: TPoint64): double; +function DistanceSqrd(const pt1, pt2: TPoint64): double; + {$IFDEF INLINE} inline; {$ENDIF} +var + x1,y1,x2,y2: double; +begin + // nb: older versions of Delphi don't allow explicit typcasting + x1 := pt1.X; y1 := pt1.Y; + x2 := pt2.X; y2 := pt2.Y; + result := Sqr(x1 - x2) + Sqr(y1 - y2); +end; +//------------------------------------------------------------------------------ + +function PerpendicDistSqrd(const pt, line1, line2: TPoint64): double; {$IFDEF INLINE} inline; {$ENDIF} var a,b,c,d: double; @@ -668,121 +840,135 @@ function PerpendicDistFromLineSqrd(const pt, line1, line2: TPoint64): double; result := 0 else result := Sqr(a * d - c * b) / (c * c + d * d); end; -//------------------------------------------------------------------------------ -function GetNext(current, high: integer; var flags: array of Boolean): integer; - {$IFDEF INLINE} inline; {$ENDIF} -begin - Result := current +1; - while (Result <= high) and flags[Result] do inc(Result); - if (Result <= high) then Exit; - Result := 0; - while (flags[Result]) do inc(Result); -end; +//------------------------------------------------------------------------------ +//------------------------------------------------------------------------------ -function GetPrior(current, high: integer; var flags: array of Boolean): integer; - {$IFDEF INLINE} inline; {$ENDIF} -begin - Result := current; - if (Result = 0) then Result := high - else dec(Result); - while (Result > 0) and flags[Result] do dec(Result); - if not flags[Result] then Exit; - Result := high; - while flags[Result] do dec(Result); -end; +type + PSimplifyRec = ^TSimplifyRec; + TSimplifyRec = record + pt : TPoint64; + pdSqrd : double; + prev : PSimplifyRec; + next : PSimplifyRec; + //isEnd : Boolean; + end; function SimplifyPath(const path: TPath64; - epsilon: double; isOpenPath: Boolean = false): TPath64; + shapeTolerance: double; isClosedPath: Boolean): TPath64; var - i,j, len, high: integer; - curr, prev, start, prev2, next, next2: integer; - epsSqr: double; - flags: array of boolean; - dsq: array of double; + i, highI, minHigh: integer; + tolSqrd: double; + srArray: array of TSimplifyRec; + first, last: PSimplifyRec; begin Result := nil; - len := Length(path); - if (len < 4) then Exit;; - high := len -1; - epsSqr := Sqr(epsilon); - SetLength(flags, len); - SetLength(dsq, len); - - curr := 0; - if (isOpenPath) then - begin - dsq[0] := MaxDouble; - dsq[high] := MaxDouble; - end else + highI := High(path); + + if isClosedPath then minHigh := 2 else minHigh := 1; + if highI < minHigh then Exit; + + SetLength(srArray, highI +1); + with srArray[0] do begin - dsq[0] := PerpendicDistFromLineSqrd(path[0], path[high], path[1]); - dsq[high] := PerpendicDistFromLineSqrd(path[high], path[0], path[high - 1]); + pt := path[0]; + prev := @srArray[highI]; + next := @srArray[1]; + if isClosedPath then + pdSqrd := PerpendicDistSqrd(path[0], path[highI], path[1]) else + pdSqrd := invalidD; end; - for i := 1 to high -1 do - dsq[i] := PerpendicDistFromLineSqrd(path[i], path[i - 1], path[i + 1]); - - while true do + with srArray[highI] do begin - if (dsq[curr] > epsSqr) then + 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; + end; + + for i := 1 to highI -1 do + with srArray[i] do begin - start := curr; - repeat - curr := GetNext(curr, high, flags); - until (curr = start) or (dsq[curr] < epsSqr); - if (curr = start) then break; + pt := path[i]; + prev := @srArray[i-1]; + next := @srArray[i+1]; + pdSqrd := PerpendicDistSqrd(path[i], path[i-1], path[i+1]); end; - prev := GetPrior(curr, high, flags); - next := GetNext(curr, high, flags); - if (next = prev) then break; + first := @srArray[0]; + last := first.prev; - if (dsq[next] < dsq[curr]) then - begin - flags[next] := true; - next := GetNext(next, high, flags); - next2 := GetNext(next, high, flags); - dsq[curr] := PerpendicDistFromLineSqrd( - path[curr], path[prev], path[next]); - if (next <> high) or not isOpenPath then - dsq[next] := PerpendicDistFromLineSqrd( - path[next], path[curr], path[next2]); - curr := next; - end else + tolSqrd := Sqr(shapeTolerance); + while first <> last do + begin + if (first.pdSqrd > tolSqrd) or + (first.next.pdSqrd < first.pdSqrd) then begin - flags[curr] := true; - curr := next; - next := GetNext(next, high, flags); - prev2 := GetPrior(prev, high, flags); - dsq[curr] := PerpendicDistFromLineSqrd( - path[curr], path[prev], path[next]); - if (prev <> 0) or not isOpenPath then - dsq[prev] := PerpendicDistFromLineSqrd( - path[prev], path[prev2], path[curr]); + first := first.next; + Continue; end; + dec(highI); + first.prev.next := first.next; + first.next.prev := first.prev; + last := first.prev; + first := last.next; + if first.next = first.prev then break; + last.pdSqrd := PerpendicDistSqrd(last.pt, last.prev.pt, first.pt); + first.pdSqrd := PerpendicDistSqrd(first.pt, last.pt, first.next.pt); + end; + if highI < minHigh then Exit; + if not isClosedPath then first := @srArray[0]; + SetLength(Result, highI +1); + for i := 0 to HighI do + begin + Result[i] := first.pt; + first := first.next; end; - j := 0; - SetLength(Result, len); - for i := 0 to High do - if not flags[i] then - begin - Result[j] := path[i]; - inc(j); - end; - SetLength(Result, j); end; +//------------------------------------------------------------------------------ function SimplifyPaths(const paths: TPaths64; - epsilon: double; isOpenPath: Boolean = false): TPaths64; + shapeTolerance: double; isClosedPath: Boolean): TPaths64; var i, len: integer; begin len := Length(paths); SetLength(Result, len); for i := 0 to len -1 do - result[i] := SimplifyPath(paths[i], epsilon, isOpenPath); + result[i] := SimplifyPath(paths[i], shapeTolerance, isClosedPath); +end; +//------------------------------------------------------------------------------ + +function SimplifyPath(const path: TPathD; shapeTolerance: double; + isClosedPath: Boolean; decimalPrecision: integer): TPathD; +var + p: TPath64; + scale: double; +begin + scale := power(10, decimalPrecision); + p := ScalePath(path, scale); + p := SimplifyPath(p, shapeTolerance, isClosedPath); + Result := ScalePathD(p, 1/scale); end; +//------------------------------------------------------------------------------ + +function SimplifyPaths(const paths: TPathsD; shapeTolerance: double; + isClosedPath: Boolean; decimalPrecision: integer): TPathsD; +var + pp: TPaths64; + scale: double; +begin + scale := power(10, decimalPrecision); + pp := ScalePaths(paths, scale); + pp := SimplifyPaths(pp, shapeTolerance, isClosedPath); + Result := ScalePathsD(pp, 1/scale); +end; +//------------------------------------------------------------------------------ + end. + diff --git a/Delphi/Examples/Example1/Example1.dpr b/Delphi/Examples/Example1/Example1.dpr index 068ec729..c3c067a8 100644 --- a/Delphi/Examples/Example1/Example1.dpr +++ b/Delphi/Examples/Example1/Example1.dpr @@ -48,4 +48,37 @@ begin end; ShellExecute(0, 'open','Sample1.svg', nil, nil, SW_SHOW); + setLength(subj, 1); + subj[0] := MakePath([40,60, 20,20, 180,20, 180,70, 25,150, 20,180, 180,180]); + solution := InflatePaths(subj, 20, jtMiter, etSquare, 3); + + with TSvgWriter.Create(fillRule) do + try + AddPaths(subj, true, $2000BBFF, $800033FF, 0.8); + AddPaths( solution, false, $2000FF00, $FF006600, 1.2, false); + AddText('Miter Joins; Square Ends', 10, 210); + + subj := Clipper.TranslatePaths(subj, 210, 0); + solution := InflatePaths(subj, 20, jtSquare, etSquare, 3); + AddPaths(subj, true, $2000BBFF, $800033FF, 0.8); + AddPaths( solution, false, $2000FF00, $FF006600, 1.2, false); + AddText('Square Joins; Square Ends', 220, 210); + + subj := Clipper.TranslatePaths(subj, 210, 0); + solution := InflatePaths(subj, 20, jtBevel, etButt, 3); + AddPaths(subj, true, $2000BBFF, $800033FF, 0.8); + AddPaths( solution, false, $2000FF00, $FF006600, 1.2, false); + AddText('Bevel Joins; Butt Ends', 430, 210); + + subj := Clipper.TranslatePaths(subj, 210, 0); + solution := InflatePaths(subj, 20, jtRound, etRound, 3); + AddPaths(subj, true, $2000BBFF, $800033FF, 0.8); + AddPaths( solution, false, $2000FF00, $FF006600, 1.2, false); + AddText('Round Joins; Round Ends', 640, 210); + SaveToFile('offsets.svg', 800,600, 40); + finally + free; + end; + ShellExecute(0, 'open','offsets.svg', nil, nil, SW_SHOW); + end. diff --git a/Delphi/Examples/RectClip/RectClip_Demo.dpr b/Delphi/Examples/RectClip/RectClip_Demo.dpr index 26dbc6b9..aebbe157 100644 --- a/Delphi/Examples/RectClip/RectClip_Demo.dpr +++ b/Delphi/Examples/RectClip/RectClip_Demo.dpr @@ -39,7 +39,7 @@ const rec := Rect64(margin, margin, width - margin, height - margin); clp[0] := rec.AsPath; - sol := ExecuteRectClip(rec, sub); + sol := RectClip(rec, sub); //display with TSvgWriter.Create(fillrule) do @@ -66,7 +66,7 @@ const clp[0] := rec.AsPath; SetLength(sub, 1); sub[0] := PathD(MakeRandomPath(width, height, count)); - sol := ExecuteRectClip(rec, sub); + sol := RectClip(rec, sub); //display with TSvgWriter.Create(fillrule) do @@ -95,7 +95,7 @@ const SetLength(sub, 1); sub[0] := MakeRandomPathD(width, height, lineLength); - sol := ExecuteRectClipLines(rec, sub); + sol := RectClipLines(rec, sub); //display with TSvgWriter.Create(fillrule) do diff --git a/README.md b/README.md index 611e62f6..42c23922 100644 --- a/README.md +++ b/README.md @@ -4,13 +4,15 @@ [![Nuget](https://img.shields.io/nuget/v/Clipper2?color=green)](https://www.nuget.org/packages/Clipper2) [![documentation](https://user-images.githubusercontent.com/5280692/187832279-b2a43890-da80-4888-95fe-793f092be372.svg)](http://www.angusj.com/clipper2/Docs/Overview.htm) -The Clipper2 library performs **intersection**, **union**, **difference** and **XOR** boolean operations on both simple and complex polygons. It also performs polygon offsetting. This is a major update of my original Clipper library that was written over 10 years ago. That library I'm now calling Clipper1 and while it still works very well, Clipper2 is [better](http://www.angusj.com/clipper2/Docs/Changes.htm) in just about every way. +The Clipper2 library performs **intersection**, **union**, **difference** and **XOR** boolean operations on both simple and complex polygons. It also performs polygon offsetting. This is a major update of my original Clipper library that was written over 10 years ago. That library I'm now calling Clipper1, and while it still works very well, Clipper2 is [better](http://www.angusj.com/clipper2/Docs/Changes.htm) in just about every way. ### Compilers - -C++: Requires C++17 (but could be modified to C++11 with only minor changes)
-C#: The core library uses Standard Library 2.0 but the sample code uses .NET6
-Delphi: Compiles with any version of Delphi back to Delphi 7. +Clipper2 can be compiled using either C++, or C#, or Delphi Pascal. The library can also be accessed from other programming languages by dynamically linking to exported functions in the [C++ compiled Clipper2 library](https://github.com/AngusJohnson/Clipper2/tree/main/DLL). (Since the C++ compiled code is [measurably](https://www.angusj.com/clipper2/Docs/Changes.htm) faster, C# and Delphi developers may also prefer this approach in applications where the library's performance is critical.) +| Lang. | Requirements | +| --- | --- | +| [**C++:**](https://github.com/AngusJohnson/Clipper2/tree/main/CPP) | Requires C++17 (could be modified to C++11 with relatively minor changes), **or**| +| [**C#:**](https://github.com/AngusJohnson/Clipper2/tree/main/CSharp) | The library uses Standard Library 2.0 but the sample code uses .NET6, **or**| +| [**Delphi:**](https://github.com/AngusJohnson/Clipper2/tree/main/Delphi) | Compiles with any version of Delphi from version 7 to current.| ### Documentation @@ -45,5 +47,6 @@ Delphi: Compiles with any version of Delphi back to Delphi 7.
### Ports to other languages -**Java**: https://github.com/micycle1/Clipper2-java/ - +**Java**: https://github.com/micycle1/Clipper2-java/
+**Kotlin**: https://github.com/Monkey-Maestro/clipper2-kotlin
+**golang**: https://github.com/epit3d/goclipper2