From 205de9ce633dba932e03c4d441fe074de09341a6 Mon Sep 17 00:00:00 2001 From: HanifKoh <76276251+HanifKoh@users.noreply.github.com> Date: Fri, 2 Oct 2026 20:13:06 +0800 Subject: [PATCH 1/6] Keep User Preset Values on Extruder Variants They Don't List (#16046) * Keep User Preset Values on Extruder Variants They Don't List A user preset stores the variant list its parent had when it was saved. When the parent later gains variants, update_diff_values_to_child_config matched variants by name only and left the new ones at the parent's value, so the user's settings were silently replaced there, and a re-save wrote the system values into the user's file. An unmatched parent variant now takes the child's first variant of the same extruder, the rule slicing already uses in get_config_index_base. A child without a variant list covers the parent's first extruder. The name match also no longer indexes the child's extruder ids when it has none. * Share One Variant Column Rule Between Slicing, User Presets and Projects Three places chose which variant column a value comes from, each with its own copy of "the same variant and owner, else the owner's first column": get_config_index_base when slicing, the user preset merge in update_diff_values_to_child_config, and normalize_filament_values_to_variants for projects and the CLI. find_variant_column now holds that rule and map_variant_columns applies it to a variant list, so a change to how missing variants are filled reaches all three. Each caller keeps its own copy step. There is no behaviour change: G-code is identical before and after. The one relaxation is that get_config_index_base no longer reads past a short id list when its two lists differ in length, which its assert already rules out. * Rename variant column helpers to variant index --------- Co-authored-by: SoftFever --- src/libslic3r/PrintConfig.cpp | 86 +++++++++++++++++--------- src/libslic3r/PrintConfig.hpp | 15 ++++- tests/libslic3r/test_config.cpp | 103 ++++++++++++++++++++++++++++++++ 3 files changed, 175 insertions(+), 29 deletions(-) diff --git a/src/libslic3r/PrintConfig.cpp b/src/libslic3r/PrintConfig.cpp index d4d0c90272..fbd72117cd 100644 --- a/src/libslic3r/PrintConfig.cpp +++ b/src/libslic3r/PrintConfig.cpp @@ -8,6 +8,7 @@ #include "format.hpp" #include "GCode/Thumbnails.hpp" +#include #include #include #include @@ -671,19 +672,46 @@ std::string get_extruder_variant_string(ExtruderType extruder_type, NozzleVolume return variant_string; } +int find_variant_index(const std::string& variant, int variant_id_1based, const std::vector& variant_list, const std::vector& variant_ids_1based) +{ + const int count = int(variant_list.empty() ? variant_ids_1based.size() : variant_list.size()); + if (count == 0) + return 0; + auto same_id = [&](int index) { + return variant_id_1based < 0 || variant_ids_1based.empty() || (index < int(variant_ids_1based.size()) && variant_ids_1based[index] == variant_id_1based); + }; + for (int index = 0; index < int(variant_list.size()); ++index) + if (variant_list[index] == variant && same_id(index)) + return index; + // Without this variant, use the id's own first variant (usually Standard), not variant index 0, + // which belongs to the first filament or extruder. + for (int index = 0; index < count; ++index) + if (same_id(index)) + return index; + return -1; +} + +std::vector map_variant_indices(const std::vector& variants, const std::vector& ids, + const std::vector& from_variants, const std::vector& from_ids) +{ + const size_t count = variants.empty() ? ids.size() : variants.size(); + std::vector variant_index(count); + for (size_t index = 0; index < count; ++index) { + if (!ids.empty() && index >= ids.size()) { + variant_index[index] = -1; + continue; + } + variant_index[index] = find_variant_index(index < variants.size() ? variants[index] : std::string(), + ids.empty() ? -1 : ids[index], from_variants, from_ids); + } + return variant_index; +} + int get_config_index_base(NozzleVolumeType volume_type, ExtruderType extruder_type, int variant_id_1based, const std::vector& variant_list, const std::vector& variant_ids_1based) { assert(variant_list.size() == variant_ids_1based.size()); - std::string extruder_variant = get_extruder_variant_string(extruder_type, volume_type); - for (int index = 0; index < int(variant_list.size()); ++index) { - if (extruder_variant == variant_list[index] && variant_ids_1based[index] == variant_id_1based) { return index; } - } - // Without this variant, use the id's own first variant (usually Standard), not variant index 0, - // which belongs to the first filament or extruder. - for (int index = 0; index < int(variant_list.size()); ++index) { - if (variant_ids_1based[index] == variant_id_1based) { return index; } - } - return 0; + const int index = find_variant_index(get_extruder_variant_string(extruder_type, volume_type), variant_id_1based, variant_list, variant_ids_1based); + return std::max(index, 0); } std::set get_extruder_supported_nozzle_volume_types(const DynamicPrintConfig &printer_config, int extruder_id) @@ -10741,14 +10769,23 @@ void normalize_filament_values_to_variants(DynamicPrintConfig &config) const int filament_count = *std::max_element(self_index->values.begin(), self_index->values.end()); if (filament_count <= 0 || size_t(filament_count) >= self_index->size()) return; + // The values are one per filament, without variant strings, or a single value for all of them. The + // variant strings do not change today's mapping; they are passed so a rule that reads them applies here too. + const auto *variants = config.option("filament_extruder_variant"); + const std::vector variant_list = variants && variants->size() == self_index->size() ? variants->values : std::vector(); + std::vector filament_ids(filament_count); + std::iota(filament_ids.begin(), filament_ids.end(), 1); + const std::vector from_filaments = map_variant_indices(variant_list, self_index->values, {}, filament_ids); + const std::vector from_single = map_variant_indices(variant_list, self_index->values, {}, {}); for (const std::string &key : filament_options_with_variant) { auto *opt = dynamic_cast(config.option(key)); if (opt == nullptr || (opt->size() != size_t(filament_count) && opt->size() != 1)) continue; - std::unique_ptr per_filament(opt->clone()); - // set_at() takes the first value for a filament past the end of a single-value vector + const std::vector &variant_index = opt->size() == size_t(filament_count) ? from_filaments : from_single; + std::unique_ptr source(opt->clone()); + // -1 and a single-value source both resolve to the first value through get_at() for (size_t variant = 0; variant < self_index->size(); ++variant) - opt->set_at(per_filament.get(), variant, self_index->values[variant] - 1); + opt->set_at(source.get(), variant, variant_index[variant]); } } @@ -11582,8 +11619,14 @@ void DynamicPrintConfig::update_diff_values_to_child_config(DynamicPrintConfig& else variant_index.resize(1, 0); + // A parent variant the child does not list (the parent gained it after the child was saved, or the + // child lists none) takes the child's first variant of the same extruder, as slicing does. + // Left unmatched, the parent's value would silently replace the user's. if (target_variant_count == 0) { - variant_index[0] = 0; + // The child's one value belongs to the extruder of the parent's first variant. + if (cur_variant_count > 0) + variant_index = map_variant_indices(cur_extruder_variants, cur_extruder_ids, {}, + cur_extruder_ids.empty() ? std::vector() : std::vector{cur_extruder_ids[0]}); } else if ((cur_extruder_ids.size() > 0) && cur_variant_count != cur_extruder_ids.size()){ //should not happen @@ -11595,19 +11638,8 @@ void DynamicPrintConfig::update_diff_values_to_child_config(DynamicPrintConfig& BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << boost::format(" size of %1% = %2%, not equal to size of %3% = %4%") %extruder_variant_name %target_variant_count %extruder_id_name %target_extruder_ids.size(); } - else { - for (int i = 0; i < cur_variant_count; i++) - { - for (int j = 0; j < target_variant_count; j++) - { - if ((cur_extruder_variants[i] == target_extruder_variants[j]) - &&(cur_extruder_ids.empty() || (cur_extruder_ids[i] == target_extruder_ids[j]))) - { - variant_index[i] = j; - break; - } - } - } + else if (cur_variant_count > 0) { + variant_index = map_variant_indices(cur_extruder_variants, cur_extruder_ids, target_extruder_variants, target_extruder_ids); } const t_config_option_keys &keys = new_config.keys(); diff --git a/src/libslic3r/PrintConfig.hpp b/src/libslic3r/PrintConfig.hpp index 3430de96fb..b1c407e3b6 100644 --- a/src/libslic3r/PrintConfig.hpp +++ b/src/libslic3r/PrintConfig.hpp @@ -556,8 +556,19 @@ enum PrimeVolumeMode { extern std::string get_extruder_variant_string(ExtruderType extruder_type, NozzleVolumeType nozzle_volume_type); -// Base slot lookup: scans a variant list (paired with its 1-based extruder/filament ids) for the -// entry matching the given extruder/volume type and id. Returns 0 when no entry matches. +// The variant index a value is taken from: in a variant list paired with its 1-based extruder or +// filament ids, the variant with the same variant string and id, else that id's first variant, else -1. +// variant_id_1based < 0 or empty variant_ids_1based match any id. A list without variant strings has +// one variant per id, and one with neither variant strings nor ids has a single variant. +extern int find_variant_index(const std::string& variant, int variant_id_1based, const std::vector& variant_list, const std::vector& variant_ids_1based); +// find_variant_index for every variant of a list paired with its ids, into from_variants/from_ids. +// A variant past the end of a shorter id list has no id and gets -1. +extern std::vector map_variant_indices(const std::vector& variants, const std::vector& ids, + const std::vector& from_variants, const std::vector& from_ids); + +// Variant index lookup: scans a variant list (paired with its 1-based extruder/filament ids) for the +// entry matching the given extruder/volume type and id, as find_variant_index. Returns 0 when the id +// has no variant. extern int get_config_index_base(NozzleVolumeType volume_type, ExtruderType extruder_type, int variant_id_1based, const std::vector& variant_list, const std::vector& variant_ids_1based); static std::set get_valid_nozzle_volume_type() { diff --git a/tests/libslic3r/test_config.cpp b/tests/libslic3r/test_config.cpp index 00c4d6d170..d7803f300b 100644 --- a/tests/libslic3r/test_config.cpp +++ b/tests/libslic3r/test_config.cpp @@ -421,6 +421,109 @@ SCENARIO("update_diff_values_to_child_config tolerates legacy machine-limit vect } } +TEST_CASE("A variant index comes from the same variant and id, else the id's first variant", "[Config][Variant]") { + const std::vector variant_list{"Direct Drive Standard", "Direct Drive High Flow", "Direct Drive Standard"}; + const std::vector variant_ids{1, 1, 2}; + + SECTION("same variant and id") { + CHECK(Slic3r::find_variant_index("Direct Drive High Flow", 1, variant_list, variant_ids) == 1); + CHECK(Slic3r::find_variant_index("Direct Drive Standard", 2, variant_list, variant_ids) == 2); + } + SECTION("a variant the id lacks falls back to the id's first variant") { + CHECK(Slic3r::find_variant_index("Bowden Standard", 1, variant_list, variant_ids) == 0); + CHECK(Slic3r::find_variant_index("Direct Drive High Flow", 2, variant_list, variant_ids) == 2); + } + SECTION("an id with no variants matches none") { + CHECK(Slic3r::find_variant_index("Direct Drive Standard", 3, variant_list, variant_ids) == -1); + } + SECTION("a negative id or a list without ids matches any id") { + CHECK(Slic3r::find_variant_index("Direct Drive High Flow", -1, variant_list, variant_ids) == 1); + CHECK(Slic3r::find_variant_index("Direct Drive High Flow", 2, variant_list, {}) == 1); + } + SECTION("a variant past a shorter id list gets no variant index") { + CHECK(Slic3r::map_variant_indices(variant_list, {1}, variant_list, variant_ids) == std::vector{0, -1, -1}); + } + SECTION("a list without variant strings has one variant per id, and an empty one a single variant") { + CHECK(Slic3r::map_variant_indices(variant_list, variant_ids, {}, {1, 2}) == std::vector{0, 0, 1}); + CHECK(Slic3r::map_variant_indices(variant_list, variant_ids, {}, {}) == std::vector{0, 0, 0}); + } +} + +SCENARIO("update_diff_values_to_child_config keeps a child's values on variants it does not list", + "[Config][Variant]") { + std::set no_keys; + auto variants = [](std::initializer_list names) { return new Slic3r::ConfigOptionStrings(names); }; + + GIVEN("A filament parent with three variants") { + Slic3r::DynamicPrintConfig parent; + parent.set_key_value("filament_extruder_variant", + variants({"Direct Drive Standard", "Bowden Standard", "Direct Drive High Flow"})); + parent.set_deserialize_strict("nozzle_temperature", "220,220,220"); + + WHEN("the child was saved when the parent had only its first variant") { + Slic3r::DynamicPrintConfig child; + child.set_key_value("filament_extruder_variant", variants({"Direct Drive Standard"})); + child.set_deserialize_strict("nozzle_temperature", "199"); + parent.update_diff_values_to_child_config(child, "", "filament_extruder_variant", + Slic3r::filament_options_with_variant, no_keys); + THEN("the child's value applies to every variant") { + REQUIRE(parent.opt_serialize("nozzle_temperature") == "199,199,199"); + } + } + WHEN("the child lists every variant, in another order") { + Slic3r::DynamicPrintConfig child; + child.set_key_value("filament_extruder_variant", + variants({"Bowden Standard", "Direct Drive High Flow", "Direct Drive Standard"})); + child.set_deserialize_strict("nozzle_temperature", "190,205,199"); + parent.update_diff_values_to_child_config(child, "", "filament_extruder_variant", + Slic3r::filament_options_with_variant, no_keys); + THEN("each variant keeps its own value") { + REQUIRE(parent.opt_serialize("nozzle_temperature") == "199,190,205"); + } + } + WHEN("the child lists no variants") { + Slic3r::DynamicPrintConfig child; + child.set_deserialize_strict("nozzle_temperature", "199"); + parent.update_diff_values_to_child_config(child, "", "filament_extruder_variant", + Slic3r::filament_options_with_variant, no_keys); + THEN("the child's value applies to every variant") { + REQUIRE(parent.opt_serialize("nozzle_temperature") == "199,199,199"); + } + } + } + + GIVEN("A two-extruder printer parent with two variants per extruder") { + Slic3r::DynamicPrintConfig parent; + parent.set_key_value("printer_extruder_variant", + variants({"Direct Drive Standard", "Direct Drive High Flow", "Direct Drive Standard", "Direct Drive High Flow"})); + parent.set_key_value("printer_extruder_id", new Slic3r::ConfigOptionInts({1, 1, 2, 2})); + parent.set_deserialize_strict("retraction_length", "0.8,0.8,0.8,0.8"); + + WHEN("the child lists only the Standard variant of each extruder") { + Slic3r::DynamicPrintConfig child; + child.set_key_value("printer_extruder_variant", variants({"Direct Drive Standard", "Direct Drive Standard"})); + child.set_key_value("printer_extruder_id", new Slic3r::ConfigOptionInts({1, 2})); + child.set_deserialize_strict("retraction_length", "1.1,2.2"); + parent.update_diff_values_to_child_config(child, "printer_extruder_id", "printer_extruder_variant", + Slic3r::printer_options_with_variant_1, + Slic3r::printer_options_with_variant_2); + THEN("each extruder's High Flow variant takes that extruder's value") { + REQUIRE(parent.opt_serialize("retraction_length") == "1.1,1.1,2.2,2.2"); + } + } + WHEN("the child lists no variants") { + Slic3r::DynamicPrintConfig child; + child.set_deserialize_strict("retraction_length", "1.1"); + parent.update_diff_values_to_child_config(child, "printer_extruder_id", "printer_extruder_variant", + Slic3r::printer_options_with_variant_1, + Slic3r::printer_options_with_variant_2); + THEN("only the first extruder's variants take the child's value") { + REQUIRE(parent.opt_serialize("retraction_length") == "1.1,1.1,0.8,0.8"); + } + } + } +} + // SCENARIO("DynamicPrintConfig JSON serialization", "[Config]") { // WHEN("DynamicPrintConfig is serialized and deserialized") { // auto now = std::chrono::high_resolution_clock::now(); From 6c6e8be43df2ce4427bdd0b944d507add51767fd Mon Sep 17 00:00:00 2001 From: Noisyfox Date: Fri, 2 Oct 2026 20:46:24 +0800 Subject: [PATCH 2/6] Fix debug build cmake errors (#14593) * Fix debug build after qhull upgrade We upgraded qhull from 8.0.1 to 8.0.2 in 504a5d3b704fba8c0fac410cf9702f7bcf611f95, which contains a commit qhull/qhull@16159c648c36d79effbd537c1375b20e63398ed8 `use same CMake target name for Debug and non-Debug`, so this target name check is no longer required * Fix issue like `IMPORTED_LOCATION not set for imported target "opencv_world" configuration "RelWithDebInfo".` when build Debug config --- CMakeLists.txt | 4 +++- deps_src/qhull/CMakeLists.txt | 6 +----- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 821d7e40c2..c431bff558 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -823,7 +823,9 @@ if(SLIC3R_STATIC) set(TBB_STATIC 1) endif() set(TBB_DEBUG 1) -set(CMAKE_MAP_IMPORTED_CONFIG_RELWITHDEBINFO RelWithDebInfo Release "") +if ("${CMAKE_BUILD_TYPE}" STREQUAL "RelWithDebInfo" OR MSVC) + set(CMAKE_MAP_IMPORTED_CONFIG_RELWITHDEBINFO RelWithDebInfo Release "") +endif() find_package(TBB REQUIRED) # include_directories(SYSTEM ${TBB_INCLUDE_DIRS}) # add_definitions(${TBB_DEFINITIONS}) diff --git a/deps_src/qhull/CMakeLists.txt b/deps_src/qhull/CMakeLists.txt index 9cead64e2c..82a7bf627e 100644 --- a/deps_src/qhull/CMakeLists.txt +++ b/deps_src/qhull/CMakeLists.txt @@ -19,11 +19,7 @@ if(Qhull_FOUND) message(STATUS "Using qhull from system.") if(SLIC3R_STATIC) slic3r_remap_configs("Qhull::qhullcpp;Qhull::qhullstatic_r" RelWithDebInfo Release) - if ("${CMAKE_BUILD_TYPE}" STREQUAL "Debug") - target_link_libraries(qhull INTERFACE Qhull::qhullcpp_d Qhull::qhullstatic_rd) - else() - target_link_libraries(qhull INTERFACE Qhull::qhullcpp Qhull::qhullstatic_r) - endif() + target_link_libraries(qhull INTERFACE Qhull::qhullcpp Qhull::qhullstatic_r) else() slic3r_remap_configs("Qhull::qhullcpp;Qhull::qhull_r" RelWithDebInfo Release) target_link_libraries(qhull INTERFACE Qhull::qhullcpp Qhull::qhull_r) From a1ad2b44253929eee183e6429f0693c21e8428af Mon Sep 17 00:00:00 2001 From: Ian Bassi Date: Fri, 2 Oct 2026 11:50:43 -0300 Subject: [PATCH 3/6] Add section view feature for 3D canvas (#15879) --- docs/HLSD/section-view.md | 103 ++++++ resources/images/canvas_section.svg | 1 + resources/images/canvas_section_active.svg | 1 + .../images/canvas_section_active_dark.svg | 1 + .../canvas_section_active_dark_hover.svg | 1 + .../images/canvas_section_active_hover.svg | 1 + resources/images/canvas_section_dark.svg | 1 + .../images/canvas_section_dark_hover.svg | 1 + resources/images/canvas_section_hover.svg | 1 + src/libvgcode/include/Viewer.hpp | 4 + src/libvgcode/src/Shaders.hpp | 65 +++- src/libvgcode/src/ShadersES.hpp | 71 ++++- src/libvgcode/src/Viewer.cpp | 5 + src/libvgcode/src/ViewerImpl.cpp | 6 + src/libvgcode/src/ViewerImpl.hpp | 7 + src/slic3r/GUI/GCodeViewer.cpp | 9 + src/slic3r/GUI/GCodeViewer.hpp | 2 + src/slic3r/GUI/GLCanvas3D.cpp | 299 ++++++++++++++---- src/slic3r/GUI/GLCanvas3D.hpp | 28 ++ src/slic3r/GUI/Gizmos/GLGizmoBrimEars.cpp | 42 +-- src/slic3r/GUI/Gizmos/GLGizmoFdmSupports.cpp | 39 +-- src/slic3r/GUI/Gizmos/GLGizmoFuzzySkin.cpp | 47 +-- .../GUI/Gizmos/GLGizmoMmuSegmentation.cpp | 36 +-- src/slic3r/GUI/Gizmos/GLGizmoPainterBase.cpp | 14 - src/slic3r/GUI/Gizmos/GLGizmoSeam.cpp | 35 +- src/slic3r/GUI/Gizmos/GLGizmosCommon.cpp | 188 +---------- src/slic3r/GUI/Gizmos/GLGizmosCommon.hpp | 123 +------ src/slic3r/GUI/Gizmos/GLGizmosManager.cpp | 58 ++-- src/slic3r/GUI/Gizmos/GLGizmosManager.hpp | 18 +- src/slic3r/GUI/KBShortcutsDialog.cpp | 3 +- src/slic3r/GUI/Plater.cpp | 1 + 31 files changed, 599 insertions(+), 612 deletions(-) create mode 100644 docs/HLSD/section-view.md create mode 100644 resources/images/canvas_section.svg create mode 100644 resources/images/canvas_section_active.svg create mode 100644 resources/images/canvas_section_active_dark.svg create mode 100644 resources/images/canvas_section_active_dark_hover.svg create mode 100644 resources/images/canvas_section_active_hover.svg create mode 100644 resources/images/canvas_section_dark.svg create mode 100644 resources/images/canvas_section_dark_hover.svg create mode 100644 resources/images/canvas_section_hover.svg diff --git a/docs/HLSD/section-view.md b/docs/HLSD/section-view.md new file mode 100644 index 0000000000..d37a09db00 --- /dev/null +++ b/docs/HLSD/section-view.md @@ -0,0 +1,103 @@ +# Section view — High Level Design + +## Purpose and scope + +Section view hides whatever lies between the camera and a plane, so the user can look inside +objects in Prepare and in the assembly view, and inside the toolpaths in Preview. It is a view +setting: it changes nothing in the model, the slice or the project file, and it does not reach +plate thumbnails. + +The user controls it from the section button of the canvas toolbar in the bottom left corner of +the 3D view. The button opens a panel above it with a slider for the depth of the cut, a "Set +viewing angle" button that turns the plane to face the camera at the same depth, and a button that +resets the depth to zero. The panel is an ordinary overlay window, not a popup, so the scene keeps +taking clicks and drags while it is open; the button or Esc closes it again. Esc closes the panel +before it closes a gizmo or clears the selection. The button is highlighted +while a section cuts the scene and has shortcuts of its own: the mouse wheel over it moves the +plane, a right click switches the section off and back on, and a middle click sets the viewing +angle. Alt + mouse wheel moves the plane anywhere in the 3D view, with or without a gizmo open. + +## State + +Prepare and Preview share one section view, so a cut made in either tab is the same cut in the +other. The assembly view, whose objects sit apart from their places on the plate, and the Design +tab keep their own. The section itself is two values. + +- **Ratio**, from 0 to 1. At 0 the section is off. As the ratio grows, the plane sweeps the + sphere around the objects, from its side facing the camera to the opposite side, so at 1 + everything is cut away. +- **Normal**, taken from the camera direction the first time the section is switched on, and + again whenever the user sets the viewing angle. The plane keeps that orientation while the + camera orbits and while the section is off, so the cut face can be seen from any side and + bringing the depth back to 0 does not lose the angle. + +The ratio in use when the section is switched off is kept, and the right click on the button +brings the section back at that ratio, which restores the same cut. Whether the panel is open is +shared along with the section. + +The sphere is recomputed every frame from the volumes of the canvas the section view belongs to: +the objects on the current plate, or every object when that plate is empty. Preview holds no +objects of its own, so it places the plane across the volumes of Prepare, which makes it cut the +toolpaths exactly where Prepare cuts the objects. Only G-code opened on its own, without objects, +is measured by its toolpaths. In the assembly view the sphere is around the whole assembly. The +ratio therefore keeps its meaning when objects move or the user switches plates. It is not a +fixed position in world space. + +Only the tab on screen can change the section, and switching tabs redraws the whole scene and +closes the open gizmo, so neither tab ever shows a stale cut. + +## Where the plane applies + +`GLCanvas3D::_get_section_view_plane()` turns the state into a plane in the convention of +`ClippingPlane::is_point_clipped()`. Everything that draws or picks the scene reads that plane. + +- **Volumes.** The plane goes to the `clipping_plane` uniform of the volume shaders. The same + uniform serves the gouraud, phong and X-ray passes and the colour picking pass. +- **Cut faces.** Clipping only discards fragments, which would leave the cut volumes hollow. + `_render_section_view_caps()` draws their cut faces with one `MeshClipper` per model part the + plane passes through. A clipper recomputes its face only when the plane or the volume moves. + Modifiers, the wipe tower and SLA auxiliaries get no face. +- **Toolpaths.** libvgcode takes the plane through `Viewer::set_clipping_plane()`. It draws each + extrusion as only the faces of a diamond-section prism that turn towards the camera, so + discarding the fragments on the clipped side would leave open shells. Instead, the segment + shader follows the view ray from a fragment that is cut away to the plane. When the + extrusion's diamond section still holds that point, the fragment is shaded as the cut face, lit + as the plane faces; otherwise it is discarded. The cut face keeps the depth of the fragment it + replaces, which is safe: along that ray everything else still shown lies behind the plane. The + shader writes no `gl_FragDepth`, so early depth testing survives. Option markers are cut away + whole, by their centres. The shadow casters share the segment shader, so what is cut away casts + no shadow either. Preview shells are drawn by another shader and are not clipped. +- **Picking.** `get_raycaster_clipping_plane()` returns the same plane, so hover, selection and + the perspective pan anchor ignore what the user cannot see. + +## Gizmos + +A gizmo that clips its object itself owns the gizmo data pool's `ObjectClipper`, and its plane +replaces the canvas section while the gizmo is open. `GLGizmosManager::get_clipping_plane()` +reports that plane, or nothing when no open gizmo has a clipper. There are two cases. + +- **Painting tools and brim ears** show the canvas section on the object they edit. + `GLGizmosManager::update_section_view()` copies the ratio and normal into their clipper + whenever the pool is updated or the section changes. The clipper then places the plane across + the edited instance, which is the only object shown. The painting tools keep clipping their + own triangles, raycasts and cut face through it. Brim ears always cut horizontally from the + top, because the ears sit on the plate. At ratio 0 the clipper holds no plane at all, so the + raycasts are not clipped. +- **Cut and mesh boolean** use the clipper for their own purposes, so the canvas section is + suspended while they are open. + +Every other gizmo, including move, rotate and scale, leaves the canvas section in place. + +## Alt + mouse wheel + +The canvas handles Alt + wheel after the gizmos had their turn, so it works the same in every +tab and with any gizmo open. On Windows, releasing Alt when no key was pressed since it went down +opens the window menu, and a wheel turn does not count as a key. Under the custom title bar that +menu is invisible, yet it takes the keyboard and the next click, which looks like a frozen 3D +view. After Alt + wheel the canvas therefore consumes the Alt release instead of passing it on. + +## Redraw + +The button and the panel are part of the ImGui overlay, which is built after the frame's scene is +drawn. A change to the section therefore marks the scene dirty and asks for one more frame. The +cached scene is never reused across a change. diff --git a/resources/images/canvas_section.svg b/resources/images/canvas_section.svg new file mode 100644 index 0000000000..553c01b22c --- /dev/null +++ b/resources/images/canvas_section.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/resources/images/canvas_section_active.svg b/resources/images/canvas_section_active.svg new file mode 100644 index 0000000000..1d8351730b --- /dev/null +++ b/resources/images/canvas_section_active.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/resources/images/canvas_section_active_dark.svg b/resources/images/canvas_section_active_dark.svg new file mode 100644 index 0000000000..45cc5a09c6 --- /dev/null +++ b/resources/images/canvas_section_active_dark.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/resources/images/canvas_section_active_dark_hover.svg b/resources/images/canvas_section_active_dark_hover.svg new file mode 100644 index 0000000000..9472dd39cd --- /dev/null +++ b/resources/images/canvas_section_active_dark_hover.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/resources/images/canvas_section_active_hover.svg b/resources/images/canvas_section_active_hover.svg new file mode 100644 index 0000000000..8664ba71d9 --- /dev/null +++ b/resources/images/canvas_section_active_hover.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/resources/images/canvas_section_dark.svg b/resources/images/canvas_section_dark.svg new file mode 100644 index 0000000000..3f5660f889 --- /dev/null +++ b/resources/images/canvas_section_dark.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/resources/images/canvas_section_dark_hover.svg b/resources/images/canvas_section_dark_hover.svg new file mode 100644 index 0000000000..b0d38d38e5 --- /dev/null +++ b/resources/images/canvas_section_dark_hover.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/resources/images/canvas_section_hover.svg b/resources/images/canvas_section_hover.svg new file mode 100644 index 0000000000..6bd98eda77 --- /dev/null +++ b/resources/images/canvas_section_hover.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/libvgcode/include/Viewer.hpp b/src/libvgcode/include/Viewer.hpp index 179105032d..7eadc16de0 100644 --- a/src/libvgcode/include/Viewer.hpp +++ b/src/libvgcode/include/Viewer.hpp @@ -76,6 +76,10 @@ public: // caller decides which of the two it varies with the realistic view setting. // void set_tone(float exposure, float saturation); + // + // ORCA: section view. Cuts away where dot(plane, (x, y, z, 1)) < 0; { 0, 0, 0, 1 } keeps all. + // + void set_clipping_plane(const std::array& plane); // // ************************************************************************ diff --git a/src/libvgcode/src/Shaders.hpp b/src/libvgcode/src/Shaders.hpp index 9f546d6dc8..b4980885b6 100644 --- a/src/libvgcode/src/Shaders.hpp +++ b/src/libvgcode/src/Shaders.hpp @@ -38,6 +38,8 @@ static const char* Segments_Vertex_Shader = // rejection discards most of the hidden fragments; set when the camera looks down on the print "uniform int reverse_order;\n" "uniform int instance_count;\n" +// ORCA: section view +"uniform vec4 clipping_plane;\n" "in int vertex_id;\n" "out vec3 color;\n" "// ORCA: realistic view - the light the shadow map is able to block, kept apart from the\n" @@ -48,6 +50,9 @@ static const char* Segments_Vertex_Shader = "// needs its world position and, for the depth bias, its eye space normal.\n" "out vec3 world_position;\n" "out vec3 shadow_normal;\n" +// ORCA: section view - the segment and the light on its cut face +"flat out int segment_id;\n" +"out vec3 cut_color_direct;\n" "vec3 decode_color(float color) {\n" " int c = int(round(color));\n" " int r = (c >> 16) & 0xFF;\n" @@ -159,6 +164,9 @@ static const char* Segments_Vertex_Shader = " color_direct = color_base * direct_lighting(eye_position, eye_normal);\n" " world_position = pos;\n" " shadow_normal = eye_normal;\n" +" segment_id = id_a;\n" +" cut_color_direct = clipping_plane.xyz == vec3(0.0) ? vec3(0.0) :\n" +" color_base * direct_lighting(eye_position, mat3(view_matrix) * -clipping_plane.xyz);\n" " gl_Position = projection_matrix * vec4(eye_position, 1.0);\n" "}\n"; @@ -178,23 +186,33 @@ static const char* Segments_Fragment_Shader = // passes in each mode is decided in GLCanvas3D::_render_gcode, not here. "uniform float exposure;\n" "uniform float saturation;\n" +// ORCA: section view - only camera-facing faces are drawn, so cut faces are ray cast +"uniform vec4 clipping_plane;\n" +"uniform mat4 view_matrix;\n" +"uniform mat4 projection_matrix;\n" +"uniform vec3 camera_position;\n" +"uniform samplerBuffer position_tex;\n" +"uniform samplerBuffer height_width_angle_tex;\n" +"const vec3 UP = vec3(0, 0, 1);\n" "const vec3 LUMA = vec3(0.2126, 0.7152, 0.0722);\n" "in vec3 color;\n" "in vec3 color_direct;\n" "in vec3 world_position;\n" "in vec3 shadow_normal;\n" +"flat in int segment_id;\n" +"in vec3 cut_color_direct;\n" "out vec4 fragment_color;\n" -"float shadow_shade() {\n" +"float shadow_shade(vec3 position, vec3 normal) {\n" " if (shadow_intensity <= 0.0)\n" " return 1.0;\n" -" vec4 lp = shadow_light_vp * vec4(world_position, 1.0);\n" +" vec4 lp = shadow_light_vp * vec4(position, 1.0);\n" " vec3 proj = lp.xyz / lp.w;\n" " proj = proj * 0.5 + 0.5;\n" " if (proj.z > 1.0)\n" " return 1.0;\n" " // Slope-scaled bias, as in gouraud.fs. An extrusion is only a handful of shadow-map texels\n" " // wide, so grazing faces need the larger bias to keep self-shadow acne off the top surfaces.\n" -" float NdotL = dot(normalize(shadow_normal), SHADOW_LIGHT_DIR);\n" +" float NdotL = dot(normalize(normal), SHADOW_LIGHT_DIR);\n" " float bias = mix(0.0004, 0.004, clamp(1.0 - NdotL, 0.0, 1.0));\n" " float sum = 0.0;\n" " for (int x = -2; x <= 2; ++x) {\n" @@ -205,8 +223,39 @@ static const char* Segments_Fragment_Shader = " }\n" " return 1.0 - shadow_intensity * (sum / 25.0);\n" "}\n" +// ORCA: section view - where the view ray meets the plane, if inside the extrusion's diamond section +"bool find_cut(out vec3 cut) {\n" +// parallel rays for an orthographic projection +" vec3 ray = projection_matrix[3][3] == 1.0 ? -vec3(view_matrix[0][2], view_matrix[1][2], view_matrix[2][2]) :\n" +" normalize(world_position - camera_position);\n" +" float approach = dot(clipping_plane.xyz, ray);\n" +" if (approach <= 0.0)\n" +" return false;\n" +" cut = world_position - dot(vec4(world_position, 1.0), clipping_plane) / approach * ray;\n" +" vec3 pos_a = texelFetch(position_tex, segment_id).xyz;\n" +" vec3 line = texelFetch(position_tex, segment_id + 1).xyz - pos_a;\n" +" float length_sq = dot(line, line);\n" +" float along = length_sq < 1e-8 ? -1.0 : dot(cut - pos_a, line) / length_sq;\n" +" if (along < 0.0 || along > 1.0)\n" +" return false;\n" +" vec3 line_dir = line * inversesqrt(length_sq);\n" +" vec3 right_dir = abs(dot(line_dir, UP)) > 0.9 ? normalize(cross(vec3(1, 0, 0), line_dir)) : normalize(cross(line_dir, UP));\n" +" vec3 offset = cut - pos_a - along * line;\n" +" vec2 half_height_width = 0.5 * mix(texelFetch(height_width_angle_tex, segment_id).xy,\n" +" texelFetch(height_width_angle_tex, segment_id + 1).xy, along);\n" +" return abs(dot(offset, right_dir)) / half_height_width.y + abs(dot(offset, cross(right_dir, line_dir))) / half_height_width.x <= 1.0;\n" +"}\n" "void main() {\n" -" vec3 c = (color + color_direct * shadow_shade()) * exposure;\n" +" vec3 c;\n" +" if (dot(vec4(world_position, 1.0), clipping_plane) >= 0.0)\n" +" c = color + color_direct * shadow_shade(world_position, shadow_normal);\n" +" else {\n" +" vec3 cut;\n" +" if (!find_cut(cut))\n" +" discard;\n" +" c = color + cut_color_direct * shadow_shade(cut, mat3(view_matrix) * -clipping_plane.xyz);\n" +" }\n" +" c *= exposure;\n" " c = mix(vec3(dot(c, LUMA)), c, saturation);\n" " fragment_color = vec4(clamp(c, 0.0, 1.0), 1.0);\n" "}\n"; @@ -234,9 +283,12 @@ static const char* Options_Vertex_Shader = "uniform samplerBuffer height_width_angle_tex;\n" "uniform samplerBuffer color_tex;\n" "uniform usamplerBuffer segment_index_tex;\n" +// ORCA: section view +"uniform vec4 clipping_plane;\n" "in vec3 in_position;\n" "in vec3 in_normal;\n" "out vec3 color;\n" +"out float clipping_plane_dot;\n" "vec3 decode_color(float color) {\n" " int c = int(round(color));\n" " int r = (c >> 16) & 0xFF;\n" @@ -263,6 +315,8 @@ static const char* Options_Vertex_Shader = " height_width.y, 0.0, 0.0,\n" " 0.0, height_width.y, 0.0,\n" " 0.0, 0.0, height_width.x);\n" +// ORCA: section view - markers are cut away whole, by their centre +" clipping_plane_dot = dot(vec4(offset, 1.0), clipping_plane);\n" " vec3 eye_position = (view_matrix * vec4(scale_matrix * in_position + offset, 1.0)).xyz;\n" " // ORCA: Apply bias to z-position to avoid z-fighting\n" " eye_position.z += bias;\n" @@ -275,8 +329,11 @@ static const char* Options_Vertex_Shader = static const char* Options_Fragment_Shader = "#version 150\n" "in vec3 color;\n" +"in float clipping_plane_dot;\n" "out vec4 fragment_color;\n" "void main() {\n" +" if (clipping_plane_dot < 0.0)\n" +" discard;\n" " fragment_color = vec4(color, 1.0);\n" "}\n"; diff --git a/src/libvgcode/src/ShadersES.hpp b/src/libvgcode/src/ShadersES.hpp index 4194b836e3..1a88e18f96 100644 --- a/src/libvgcode/src/ShadersES.hpp +++ b/src/libvgcode/src/ShadersES.hpp @@ -32,6 +32,8 @@ static const char* Segments_Vertex_Shader_ES = "uniform sampler2D height_width_angle_tex;\n" "uniform sampler2D color_tex;\n" "uniform usampler2D segment_index_tex;\n" +// ORCA: section view +"uniform vec4 clipping_plane;\n" "in float vertex_id_float;\n" "out vec3 color;\n" "// ORCA: realistic view - the light the shadow map is able to block, kept apart from the\n" @@ -42,6 +44,9 @@ static const char* Segments_Vertex_Shader_ES = "// needs its world position and, for the depth bias, its eye space normal.\n" "out vec3 world_position;\n" "out vec3 shadow_normal;\n" +// ORCA: section view - the segment and the light on its cut face +"flat out int segment_id;\n" +"out vec3 cut_color_direct;\n" "vec3 decode_color(float color) {\n" " int c = int(round(color));\n" " int r = (c >> 16) & 0xFF;\n" @@ -156,12 +161,17 @@ static const char* Segments_Vertex_Shader_ES = " color_direct = color_base * direct_lighting(eye_position, eye_normal);\n" " world_position = pos;\n" " shadow_normal = eye_normal;\n" +" segment_id = id_a;\n" +" cut_color_direct = clipping_plane.xyz == vec3(0.0) ? vec3(0.0) :\n" +" color_base * direct_lighting(eye_position, mat3(view_matrix) * -clipping_plane.xyz);\n" " gl_Position = projection_matrix * vec4(eye_position, 1.0);\n" "}\n"; static const char* Segments_Fragment_Shader_ES = "#version 300 es\n" "precision highp float;\n" +// ORCA: section view - segment ids exceed mediump +"precision highp int;\n" "// ORCA: sampler2D defaults to lowp in an ES fragment shader, far too coarse to compare\n" "// shadow map depths against.\n" "precision highp sampler2D;\n" @@ -179,23 +189,33 @@ static const char* Segments_Fragment_Shader_ES = // passes in each mode is decided in GLCanvas3D::_render_gcode, not here. "uniform float exposure;\n" "uniform float saturation;\n" +// ORCA: section view - cut faces are ray cast; data samplers stay lowp as in the vertex shader +"uniform vec4 clipping_plane;\n" +"uniform mat4 view_matrix;\n" +"uniform mat4 projection_matrix;\n" +"uniform vec3 camera_position;\n" +"uniform lowp sampler2D position_tex;\n" +"uniform lowp sampler2D height_width_angle_tex;\n" +"const vec3 UP = vec3(0, 0, 1);\n" "const vec3 LUMA = vec3(0.2126, 0.7152, 0.0722);\n" "in vec3 color;\n" "in vec3 color_direct;\n" "in vec3 world_position;\n" "in vec3 shadow_normal;\n" +"flat in int segment_id;\n" +"in vec3 cut_color_direct;\n" "out vec4 fragment_color;\n" -"float shadow_shade() {\n" +"float shadow_shade(vec3 position, vec3 normal) {\n" " if (shadow_intensity <= 0.0)\n" " return 1.0;\n" -" vec4 lp = shadow_light_vp * vec4(world_position, 1.0);\n" +" vec4 lp = shadow_light_vp * vec4(position, 1.0);\n" " vec3 proj = lp.xyz / lp.w;\n" " proj = proj * 0.5 + 0.5;\n" " if (proj.z > 1.0)\n" " return 1.0;\n" " // Slope-scaled bias, as in gouraud.fs. An extrusion is only a handful of shadow-map texels\n" " // wide, so grazing faces need the larger bias to keep self-shadow acne off the top surfaces.\n" -" float NdotL = dot(normalize(shadow_normal), SHADOW_LIGHT_DIR);\n" +" float NdotL = dot(normalize(normal), SHADOW_LIGHT_DIR);\n" " float bias = mix(0.0004, 0.004, clamp(1.0 - NdotL, 0.0, 1.0));\n" " float sum = 0.0;\n" " for (int x = -2; x <= 2; ++x) {\n" @@ -206,8 +226,43 @@ static const char* Segments_Fragment_Shader_ES = " }\n" " return 1.0 - shadow_intensity * (sum / 25.0);\n" "}\n" +"vec4 fetch(lowp sampler2D sampler, int id) {\n" +" ivec2 tex_size = textureSize(sampler, 0);\n" +" return texelFetch(sampler, (tex_size.y == 1) ? ivec2(id, 0) : ivec2(id % tex_size.x, id / tex_size.x), 0);\n" +"}\n" +// ORCA: section view - where the view ray meets the plane, if inside the extrusion's diamond section +"bool find_cut(out vec3 cut) {\n" +// parallel rays for an orthographic projection +" vec3 ray = projection_matrix[3][3] == 1.0 ? -vec3(view_matrix[0][2], view_matrix[1][2], view_matrix[2][2]) :\n" +" normalize(world_position - camera_position);\n" +" float approach = dot(clipping_plane.xyz, ray);\n" +" if (approach <= 0.0)\n" +" return false;\n" +" cut = world_position - dot(vec4(world_position, 1.0), clipping_plane) / approach * ray;\n" +" vec3 pos_a = fetch(position_tex, segment_id).xyz;\n" +" vec3 line = fetch(position_tex, segment_id + 1).xyz - pos_a;\n" +" float length_sq = dot(line, line);\n" +" float along = length_sq < 1e-8 ? -1.0 : dot(cut - pos_a, line) / length_sq;\n" +" if (along < 0.0 || along > 1.0)\n" +" return false;\n" +" vec3 line_dir = line * inversesqrt(length_sq);\n" +" vec3 right_dir = abs(dot(line_dir, UP)) > 0.9 ? normalize(cross(vec3(1, 0, 0), line_dir)) : normalize(cross(line_dir, UP));\n" +" vec3 offset = cut - pos_a - along * line;\n" +" vec2 half_height_width = 0.5 * mix(fetch(height_width_angle_tex, segment_id).xy,\n" +" fetch(height_width_angle_tex, segment_id + 1).xy, along);\n" +" return abs(dot(offset, right_dir)) / half_height_width.y + abs(dot(offset, cross(right_dir, line_dir))) / half_height_width.x <= 1.0;\n" +"}\n" "void main() {\n" -" vec3 c = (color + color_direct * shadow_shade()) * exposure;\n" +" vec3 c;\n" +" if (dot(vec4(world_position, 1.0), clipping_plane) >= 0.0)\n" +" c = color + color_direct * shadow_shade(world_position, shadow_normal);\n" +" else {\n" +" vec3 cut;\n" +" if (!find_cut(cut))\n" +" discard;\n" +" c = color + cut_color_direct * shadow_shade(cut, mat3(view_matrix) * -clipping_plane.xyz);\n" +" }\n" +" c *= exposure;\n" " c = mix(vec3(dot(c, LUMA)), c, saturation);\n" " fragment_color = vec4(clamp(c, 0.0, 1.0), 1.0);\n" "}\n"; @@ -230,9 +285,12 @@ static const char* Options_Vertex_Shader_ES = "uniform sampler2D height_width_angle_tex;\n" "uniform sampler2D color_tex;\n" "uniform usampler2D segment_index_tex;\n" +// ORCA: section view +"uniform vec4 clipping_plane;\n" "in vec3 in_position;\n" "in vec3 in_normal;\n" "out vec3 color;\n" +"out float clipping_plane_dot;\n" "vec3 decode_color(float color) {\n" " int c = int(round(color));\n" " int r = (c >> 16) & 0xFF;\n" @@ -264,6 +322,8 @@ static const char* Options_Vertex_Shader_ES = " height_width.y, 0.0, 0.0,\n" " 0.0, height_width.y, 0.0,\n" " 0.0, 0.0, height_width.x);\n" +// ORCA: section view - markers are cut away whole, by their centre +" clipping_plane_dot = dot(vec4(offset, 1.0), clipping_plane);\n" " vec3 eye_position = (view_matrix * vec4(scale_matrix * in_position + offset, 1.0)).xyz;\n" " vec3 eye_normal = (view_matrix * vec4(in_normal, 0.0)).xyz;\n" " vec3 color_base = decode_color(texelFetch(color_tex, tex_coord(color_tex, id), 0).r);\n" @@ -275,8 +335,11 @@ static const char* Options_Fragment_Shader_ES = "#version 300 es\n" "precision highp float;\n" "in vec3 color;\n" +"in float clipping_plane_dot;\n" "out vec4 fragment_color;\n" "void main() {\n" +" if (clipping_plane_dot < 0.0)\n" +" discard;\n" " fragment_color = vec4(color, 1.0);\n" "}\n"; diff --git a/src/libvgcode/src/Viewer.cpp b/src/libvgcode/src/Viewer.cpp index 61d7310508..4d5cc29b7c 100644 --- a/src/libvgcode/src/Viewer.cpp +++ b/src/libvgcode/src/Viewer.cpp @@ -57,6 +57,11 @@ void Viewer::set_tone(float exposure, float saturation) m_impl->set_tone(exposure, saturation); } +void Viewer::set_clipping_plane(const std::array& plane) +{ + m_impl->set_clipping_plane(plane); +} + EViewType Viewer::get_view_type() const { return m_impl->get_view_type(); diff --git a/src/libvgcode/src/ViewerImpl.cpp b/src/libvgcode/src/ViewerImpl.cpp index 8212717931..47e2d43f01 100644 --- a/src/libvgcode/src/ViewerImpl.cpp +++ b/src/libvgcode/src/ViewerImpl.cpp @@ -773,6 +773,8 @@ void ViewerImpl::init(const std::string& opengl_context_version) m_uni_segments_exposure_id = glGetUniformLocation(m_segments_shader_id, "exposure"); m_uni_segments_saturation_id = glGetUniformLocation(m_segments_shader_id, "saturation"); m_uni_segments_bias_scale_id = glGetUniformLocation(m_segments_shader_id, "bias_scale"); + // ORCA: section view + m_uni_segments_clipping_plane_id = glGetUniformLocation(m_segments_shader_id, "clipping_plane"); glcheck(); assert(m_uni_segments_view_matrix_id != -1 && m_uni_segments_projection_matrix_id != -1 && @@ -797,6 +799,8 @@ void ViewerImpl::init(const std::string& opengl_context_version) m_uni_options_height_width_angle_tex_id = glGetUniformLocation(m_options_shader_id, "height_width_angle_tex"); m_uni_options_colors_tex_id = glGetUniformLocation(m_options_shader_id, "color_tex"); m_uni_options_segment_index_tex_id = glGetUniformLocation(m_options_shader_id, "segment_index_tex"); + // ORCA: section view + m_uni_options_clipping_plane_id = glGetUniformLocation(m_options_shader_id, "clipping_plane"); glcheck(); assert(m_uni_options_view_matrix_id != -1 && m_uni_options_projection_matrix_id != -1 && @@ -2110,6 +2114,7 @@ void ViewerImpl::render_segments(const Mat4x4& view_matrix, const Mat4x4& projec glsafe(glUniform1f(m_uni_segments_exposure_id, m_exposure)); glsafe(glUniform1f(m_uni_segments_saturation_id, m_saturation)); glsafe(glUniform1f(m_uni_segments_bias_scale_id, m_rendering_shadow_casters ? 0.0f : 1.0f)); + glsafe(glUniform4fv(m_uni_segments_clipping_plane_id, 1, m_clipping_plane.data())); glsafe(glDisable(GL_CULL_FACE)); @@ -2197,6 +2202,7 @@ void ViewerImpl::render_options(const Mat4x4& view_matrix, const Mat4x4& project glsafe(glUniform1i(m_uni_options_segment_index_tex_id, 3)); glsafe(glUniformMatrix4fv(m_uni_options_view_matrix_id, 1, GL_FALSE, view_matrix.data())); glsafe(glUniformMatrix4fv(m_uni_options_projection_matrix_id, 1, GL_FALSE, projection_matrix.data())); + glsafe(glUniform4fv(m_uni_options_clipping_plane_id, 1, m_clipping_plane.data())); glsafe(glEnable(GL_CULL_FACE)); diff --git a/src/libvgcode/src/ViewerImpl.hpp b/src/libvgcode/src/ViewerImpl.hpp index 707f0f6bb7..bf8f8558d9 100644 --- a/src/libvgcode/src/ViewerImpl.hpp +++ b/src/libvgcode/src/ViewerImpl.hpp @@ -89,6 +89,8 @@ public: // caller decides which of the two it varies with the realistic view setting. // void set_tone(float exposure, float saturation); + // ORCA: section view, see Viewer::set_clipping_plane() + void set_clipping_plane(const std::array& plane) { m_clipping_plane = plane; } EViewType get_view_type() const { return m_settings.view_type; } void set_view_type(EViewType type); @@ -371,6 +373,7 @@ private: int m_uni_segments_exposure_id{ -1 }; int m_uni_segments_saturation_id{ -1 }; int m_uni_segments_bias_scale_id{ -1 }; + int m_uni_segments_clipping_plane_id{ -1 }; // // Caches for OpenGL uniforms id for options shader // @@ -380,6 +383,7 @@ private: int m_uni_options_height_width_angle_tex_id{ -1 }; int m_uni_options_colors_tex_id{ -1 }; int m_uni_options_segment_index_tex_id{ -1 }; + int m_uni_options_clipping_plane_id{ -1 }; #if VGCODE_ENABLE_COG_AND_TOOL_MARKERS // // Caches for OpenGL uniforms id for cog marker shader @@ -530,6 +534,9 @@ private: float m_exposure{ 1.0f }; float m_saturation{ 1.0f }; + // ORCA: section view + std::array m_clipping_plane{ 0.0f, 0.0f, 0.0f, 1.0f }; + void apply_pending_updates(); void update_view_full_range(); void update_color_ranges(); diff --git a/src/slic3r/GUI/GCodeViewer.cpp b/src/slic3r/GUI/GCodeViewer.cpp index 04c930dcfe..dab9cf145b 100644 --- a/src/slic3r/GUI/GCodeViewer.cpp +++ b/src/slic3r/GUI/GCodeViewer.cpp @@ -1782,6 +1782,15 @@ void GCodeViewer::set_tone(float exposure, float saturation) m_viewer.set_tone(exposure, saturation); } +void GCodeViewer::set_clipping_plane(const ClippingPlane& plane) +{ + // Flipped to match ClippingPlane::distance(). + const Vec3f normal = -plane.get_normal().cast(); + m_viewer.set_clipping_plane(plane.is_active() ? + std::array{ normal.x(), normal.y(), normal.z(), float(plane.get_offset()) } : + std::array{ 0.0f, 0.0f, 0.0f, 1.0f }); +} + void GCodeViewer::render_overlay(int canvas_width, int canvas_height, int right_margin) { if (m_viewer.get_extrusion_roles().empty()) diff --git a/src/slic3r/GUI/GCodeViewer.hpp b/src/slic3r/GUI/GCodeViewer.hpp index 5154726aba..291891ccc2 100644 --- a/src/slic3r/GUI/GCodeViewer.hpp +++ b/src/slic3r/GUI/GCodeViewer.hpp @@ -300,6 +300,8 @@ public: // ORCA: tone applied to the shaded toolpaths, paying back the light the lighting term, // the shadow and the SSAO pass each take off. 1.0/1.0 is a no-op. void set_tone(float exposure, float saturation); + // ORCA: section view + void set_clipping_plane(const ClippingPlane& plane); //BBS // void _render_calibration_thumbnail_internal(ThumbnailData& thumbnail_data, const ThumbnailsParams& thumbnail_params, PartPlateList& partplate_list, OpenGLManager& opengl_manager); // void _render_calibration_thumbnail_framebuffer(ThumbnailData& thumbnail_data, unsigned int w, unsigned int h, const ThumbnailsParams& thumbnail_params, PartPlateList& partplate_list, OpenGLManager& opengl_manager); diff --git a/src/slic3r/GUI/GLCanvas3D.cpp b/src/slic3r/GUI/GLCanvas3D.cpp index ea116587ac..9462f2e2ed 100644 --- a/src/slic3r/GUI/GLCanvas3D.cpp +++ b/src/slic3r/GUI/GLCanvas3D.cpp @@ -1324,8 +1324,11 @@ GLCanvas3D::~GLCanvas3D() m_shadow_map_fbo = 0; } m_plate_shadow_mask.reset(); + m_section_view_caps.clear(); } m_plate_shadow_mask_key.clear(); + if (m_section_view->owner == this) + m_section_view->owner = nullptr; reset_volumes(); @@ -1474,6 +1477,7 @@ void GLCanvas3D::reset_volumes() m_selection.clear(); m_volumes.clear(); + m_section_view_caps.clear(); m_dirty = true; _set_warning_notification(EWarning::ObjectOutside, false); @@ -1624,6 +1628,42 @@ ModelInstanceEPrintVolumeState GLCanvas3D::check_volumes_outside_state(ObjectFil return state; } +void GLCanvas3D::set_section_view_ratio(double ratio) +{ + ratio = std::clamp(ratio, 0., 1.); + if (ratio == m_section_view->ratio) + return; + + if (ratio == 0.) + m_section_view->last_ratio = m_section_view->ratio; + m_section_view->ratio = ratio; + // Only the first cut faces the camera; later ones keep their angle until it is set again. + if (m_section_view->normal.isZero()) + align_section_view_to_camera(); + else + _on_section_view_changed(); +} + +void GLCanvas3D::toggle_section_view() +{ + const double last_ratio = m_section_view->last_ratio; + set_section_view_ratio(is_section_view_active() ? 0. : last_ratio > 0. ? last_ratio : 0.5); +} + +void GLCanvas3D::align_section_view_to_camera() +{ + m_section_view->normal = -wxGetApp().plater()->get_camera().get_dir_forward(); + _on_section_view_changed(); +} + +void GLCanvas3D::_on_section_view_changed() +{ + m_gizmos.update_section_view(); + // Set from the overlay, after this frame's scene was drawn. + set_as_dirty(); + request_extra_frame(); +} + void GLCanvas3D::toggle_world_axes_visibility(bool force_show) { if (force_show) { @@ -2339,6 +2379,8 @@ void GLCanvas3D::_render_scene(const Camera& camera, const Size& cnv_size) } /* preview render */ else if (m_canvas_type == ECanvasType::CanvasPreview && m_render_preview) { + // Before the shadow pass, so the cut away part casts no shadow. + m_gcode_viewer.set_clipping_plane(_get_section_view_plane()); _render_objects(GLVolumeCollection::ERenderType::Opaque, !m_gizmos.is_running()); _render_sla_slices(); _render_selection(); @@ -3121,7 +3163,6 @@ void GLCanvas3D::reload_scene(bool refresh_immediately, bool force_full_scene_re m_scene_raycaster.remove_raycasters(SceneRaycaster::EType::Bed); m_scene_raycaster.remove_raycasters(SceneRaycaster::EType::Volume); m_gizmos.update_data(); - m_gizmos.update_assemble_view_data(); m_gizmos.refresh_on_off_state(); // Update the toolbar @@ -3837,12 +3878,18 @@ void GLCanvas3D::on_key(wxKeyEvent& evt) m_key_down = { keyCode, key_repeats(keyCode) }; else if (evt.GetEventType() == wxEVT_KEY_UP) key_released(keyCode); + const bool alt_wheel_released = evt.GetEventType() == wxEVT_KEY_UP && keyCode == WXK_ALT && std::exchange(m_alt_wheel_used, false); auto imgui = wxGetApp().imgui(); if (imgui->update_key_data(evt)) render(); else { + // Before the gizmos and the selection, which Esc also closes. + if (evt.GetEventType() == wxEVT_KEY_DOWN && keyCode == WXK_ESCAPE && std::exchange(m_section_view->panel_open, false)) { + _set_overlay_as_dirty(); + return; + } if (!m_gizmos.on_key(evt)) { if (evt.GetEventType() == wxEVT_KEY_UP) { if (evt.ShiftDown() && evt.ControlDown() && keyCode == WXK_SPACE) { @@ -3911,7 +3958,8 @@ void GLCanvas3D::on_key(wxKeyEvent& evt) && keyCode != WXK_LEFT && keyCode != WXK_UP && keyCode != WXK_RIGHT - && keyCode != WXK_DOWN) { + && keyCode != WXK_DOWN + && !alt_wheel_released) { evt.Skip(); // Needed to have EVT_CHAR generated as well } } @@ -3931,6 +3979,8 @@ void GLCanvas3D::on_mouse_wheel(wxMouseEvent& evt) if (evt.MiddleIsDown()) return; + m_alt_wheel_used |= evt.AltDown(); + #if ENABLE_RETINA_GL const float scale = m_retina_helper->get_scale_factor(); evt.SetX(evt.GetX() * scale); @@ -3976,25 +4026,21 @@ void GLCanvas3D::on_mouse_wheel(wxMouseEvent& evt) if (m_gizmos.on_mouse_wheel(evt)) return; - if (m_canvas_type == CanvasAssembleView && (evt.AltDown() || evt.CmdDown()) && m_gizmos.m_assemble_view_data != nullptr) { + if (evt.AltDown()) { + set_section_view_ratio(get_section_view_ratio() + (evt.GetWheelRotation() < 0 ? -0.01 : 0.01)); + return; + } + + if (m_canvas_type == CanvasAssembleView && evt.CmdDown()) { float rotation = (float)evt.GetWheelRotation() / (float)evt.GetWheelDelta(); - if (evt.AltDown()) { - auto clp_dist = m_gizmos.m_assemble_view_data->model_objects_clipper()->get_position(); - clp_dist = rotation < 0.f - ? std::max(0., clp_dist - 0.01) - : std::min(1., clp_dist + 0.01); - m_gizmos.m_assemble_view_data->model_objects_clipper()->set_position(clp_dist, true); - } - else if (evt.CmdDown()) { - m_explosion_ratio = rotation < 0.f - ? std::max(1., m_explosion_ratio - 0.01) - : std::min(3., m_explosion_ratio + 0.01); - if (m_explosion_ratio != GLVolume::explosion_ratio) { - for (GLVolume* volume : m_volumes.volumes) { - volume->set_bounding_boxes_as_dirty(); - } - GLVolume::explosion_ratio = m_explosion_ratio; + m_explosion_ratio = rotation < 0.f + ? std::max(1., m_explosion_ratio - 0.01) + : std::min(3., m_explosion_ratio + 0.01); + if (m_explosion_ratio != GLVolume::explosion_ratio) { + for (GLVolume* volume : m_volumes.volumes) { + volume->set_bounding_boxes_as_dirty(); } + GLVolume::explosion_ratio = m_explosion_ratio; } return; } @@ -8556,7 +8602,7 @@ void GLCanvas3D::_render_objects(GLVolumeCollection::ERenderType type, bool with glsafe(::glEnable(GL_DEPTH_TEST)); - m_camera_clipping_plane = m_gizmos.get_clipping_plane(); + m_camera_clipping_plane = _get_volumes_clipping_plane(); if (m_picking_enabled) // Update the layer editing selection to the first object selected, update the current object maximum Z. @@ -8599,10 +8645,7 @@ void GLCanvas3D::_render_objects(GLVolumeCollection::ERenderType type, bool with GLGizmosManager& gm = get_gizmos_manager(); GLGizmoBase* current_gizmo = gm.get_current(); - if (m_canvas_type == CanvasAssembleView) { - m_volumes.set_clipping_plane(m_gizmos.get_assemble_view_clipping_plane().get_data()); - } - else if (current_gizmo && !current_gizmo->apply_clipping_plane()) { + if (current_gizmo && !current_gizmo->apply_clipping_plane()) { m_volumes.set_clipping_plane(ClippingPlane::ClipsNothing().get_data()); } else { @@ -8711,6 +8754,7 @@ void GLCanvas3D::_render_objects(GLVolumeCollection::ERenderType type, bool with } } + _render_section_view_caps(); break; } case GLVolumeCollection::ERenderType::Transparent: @@ -8732,12 +8776,6 @@ void GLCanvas3D::_render_objects(GLVolumeCollection::ERenderType type, bool with } }, partly_inside_enable, printable_heights); - if (m_canvas_type == CanvasAssembleView && m_gizmos.m_assemble_view_data->model_objects_clipper()->get_position() > 0) { - const GLGizmosManager& gm = get_gizmos_manager(); - shader->stop_using(); - gm.render_painter_assemble_view(); - shader->start_using(); - } break; } } @@ -8758,6 +8796,42 @@ void GLCanvas3D::_render_objects(GLVolumeCollection::ERenderType type, bool with m_camera_clipping_plane = ClippingPlane::ClipsNothing(); } +void GLCanvas3D::_render_section_view_caps() +{ + // A gizmo with its own clipper draws its own cut faces. + const ClippingPlane plane = m_gizmos.get_clipping_plane().has_value() ? ClippingPlane::ClipsNothing() : _get_section_view_plane(); + if (!plane.is_active() || m_model == nullptr) { + m_section_view_caps.clear(); + return; + } + + // Last frame's clippers are reused, so unchanged cuts are not recomputed. + std::map caps; + for (const GLVolume* volume : m_volumes.volumes) { + if (!volume->is_active || volume->is_modifier || volume->is_wipe_tower || volume->volume_idx() < 0 || + volume->object_idx() >= int(m_model->objects.size())) + continue; + const ModelObject* object = m_model->objects[volume->object_idx()]; + if (volume->volume_idx() >= int(object->volumes.size())) + continue; + // Skip volumes the plane misses. + const BoundingBoxf3& box = volume->transformed_bounding_box(); + if (std::abs(plane.distance(box.center())) > 0.5 * box.size().dot(plane.get_normal().cwiseAbs())) + continue; + + auto node = m_section_view_caps.extract(volume); + MeshClipper& clipper = node ? caps.insert(std::move(node)).position->second : caps[volume]; + clipper.set_mesh(object->volumes[volume->volume_idx()]->mesh().its); + clipper.set_plane(plane); + clipper.set_transformation(Geometry::Transformation(volume->world_matrix())); + // No cut face below the plate. + if (m_canvas_type != CanvasAssembleView) + clipper.set_limiting_plane(ClippingPlane(Vec3d::UnitZ(), -SINKING_Z_THRESHOLD)); + clipper.render_cut({ 0.25f, 0.25f, 0.25f, 1.0f }); + } + m_section_view_caps = std::move(caps); +} + bool GLCanvas3D::_is_xray_view_active() const { if (m_canvas_type == ECanvasType::CanvasPreview || !wxGetApp().plater()->is_show_xray()) @@ -9846,7 +9920,31 @@ void GLCanvas3D::_render_canvas_toolbar() ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding , padding); // without padding images clipping imgui.begin(_L("Canvas Toolbar"), ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoBackground | ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoMove | - ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoCollapse);// + ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoScrollWithMouse);// + + // Section view, on top so its panel opens above the toolbar. + const bool section_view = is_section_view_active(); + ImTextureID s_normal_id = m_gizmos.get_icon_texture_id(section_view ? + (m_is_dark ? GLGizmosManager::MENU_ICON_NAME::IC_CANVAS_SECTION_ACTIVE_DARK : GLGizmosManager::MENU_ICON_NAME::IC_CANVAS_SECTION_ACTIVE) : + (m_is_dark ? GLGizmosManager::MENU_ICON_NAME::IC_CANVAS_SECTION_DARK : GLGizmosManager::MENU_ICON_NAME::IC_CANVAS_SECTION)); + ImTextureID s_hover_id = m_gizmos.get_icon_texture_id(section_view ? + (m_is_dark ? GLGizmosManager::MENU_ICON_NAME::IC_CANVAS_SECTION_ACTIVE_DARK_HOVER : GLGizmosManager::MENU_ICON_NAME::IC_CANVAS_SECTION_ACTIVE_HOVER) : + (m_is_dark ? GLGizmosManager::MENU_ICON_NAME::IC_CANVAS_SECTION_DARK_HOVER : GLGizmosManager::MENU_ICON_NAME::IC_CANVAS_SECTION_HOVER)); + + if (ImGui::ImageButton3(s_normal_id, s_hover_id, btn_size)) + m_section_view->panel_open = !m_section_view->panel_open; + else if (ImGui::IsItemHovered()) { + if (const float wheel = ImGui::GetIO().MouseWheel; wheel != 0.f) + set_section_view_ratio(get_section_view_ratio() + (wheel < 0.f ? -0.01 : 0.01)); + if (ImGui::IsMouseClicked(ImGuiMouseButton_Right)) + toggle_section_view(); + else if (ImGui::IsMouseClicked(ImGuiMouseButton_Middle)) + align_section_view_to_camera(); + imgui.tooltip(_L("Section view"), ImGui::GetFontSize() * 20.0f); + } + const ImVec2 section_panel_pos = ImGui::GetItemRectMin() - ImVec2(0.f, spacing.y); + + ImGui::Dummy({ 0, spacing.y}); ImTextureID m_normal_id = m_gizmos.get_icon_texture_id(m_is_dark ? GLGizmosManager::MENU_ICON_NAME::IC_CANVAS_MENU_DARK : GLGizmosManager::MENU_ICON_NAME::IC_CANVAS_MENU); ImTextureID m_hover_id = m_gizmos.get_icon_texture_id(m_is_dark ? GLGizmosManager::MENU_ICON_NAME::IC_CANVAS_MENU_DARK_HOVER : GLGizmosManager::MENU_ICON_NAME::IC_CANVAS_MENU_HOVER); @@ -10012,6 +10110,76 @@ void GLCanvas3D::_render_canvas_toolbar() ImGui::PopStyleVar(6); imgui.end(); + + _render_section_view_panel(section_panel_pos); +} + +void GLCanvas3D::_render_section_view_panel(const ImVec2& bottom_left) +{ + if (!m_section_view->panel_open) + return; + + ImGuiWrapper& imgui = *wxGetApp().imgui(); + const float sc = get_scale(); + + ImGuiWrapper::push_toolbar_style(sc); + ImGui::PushStyleColor(ImGuiCol_WindowBg, m_is_dark ? ImGuiWrapper::COL_TOOLBAR_BG_DARK : ImGuiWrapper::COL_TOOLBAR_BG); + ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 8.f * sc); + ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(12.f, 8.f) * sc); + ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(6.f, 4.f) * sc); + + ImGui::SetNextWindowPos(bottom_left, ImGuiCond_Always, ImVec2(0.f, 1.f)); + imgui.begin(std::string("##CanvasSectionView"), ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoMove | + ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoScrollbar); + + const wxString alt = GUI::shortkey_alt_prefix(); + const std::vector> tips = { + { alt + _L("Mouse wheel"), _L("Move section plane") }, + { _L("Mouse wheel on icon"), _L("Move section plane") }, + { _L("Right click on icon"), _L("Toggle section view") }, + { _L("Middle click on icon"), _L("Set viewing angle") }, + }; + // render_tooltip_button() puts the tips below y plus the panel height; they go above the panel. + const ImGuiStyle& style = ImGui::GetStyle(); + const float tips_h = tips.size() * (ImGui::GetTextLineHeight() + style.ItemSpacing.y) - style.ItemSpacing.y + 2.f * style.WindowPadding.y; + GLGizmoUtils::render_tooltip_button(&imgui, *this, tips, ImGui::GetWindowPos().x, + ImGui::GetWindowPos().y - style.ItemSpacing.y - tips_h - ImGui::GetContentRegionMax().y - ImGui::GetFrameHeight()); + + ImGui::SameLine(); + ImGui::AlignTextToFramePadding(); + imgui.text(_L("Section view")); + + float ratio = float(get_section_view_ratio()); + ImGui::SameLine(); + ImGui::SetNextItemWidth(imgui.scaled(7.f)); + bool changed = imgui.bbl_slider_float_style("##section_view", &ratio, 0.f, 1.f, "%.2f", 1.0f, true); + ImGui::SameLine(); + ImGui::SetNextItemWidth(1.5f * imgui.get_slider_icon_size().x); + changed |= ImGui::BBLDragFloat("##section_view_input", &ratio, 0.05f, 0.0f, 0.0f, "%.2f"); + if (changed) + set_section_view_ratio(ratio); + + ImGui::SameLine(); + if (imgui.button(_L("Set viewing angle"))) + align_section_view_to_camera(); + + imgui.disabled_begin(!is_section_view_active()); + ImGui::SameLine(); + ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.f, 0.f, 0.f, 0.f)); + ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.f, 0.f, 0.f, 0.f)); + ImGui::PushStyleColor(ImGuiCol_ButtonActive, ImVec4(0.f, 0.f, 0.f, 0.f)); + ImGui::PushStyleVar(ImGuiStyleVar_FrameBorderSize, 0.f); + if (imgui.button(wxString(ImGui::RevertBtn) + "##section_view_reset", _L("Reset"))) + set_section_view_ratio(0.); + ImGui::PopStyleVar(); + ImGui::PopStyleColor(3); + imgui.disabled_end(); + + imgui.end(); + + ImGui::PopStyleVar(3); + ImGui::PopStyleColor(); + ImGuiWrapper::pop_toolbar_style(); } void GLCanvas3D::_render_separator_toolbar_right() const @@ -10259,17 +10427,12 @@ float GLCanvas3D::_render_assembly_tooltip_button(ImGuiWrapper* imgui_wrapper) c //BBS void GLCanvas3D::_render_assemble_control() { - if(m_gizmos.m_assemble_view_data == nullptr) - return; - if (m_canvas_type != ECanvasType::CanvasAssembleView) { GLVolume::explosion_ratio = m_explosion_ratio = 1.0; return; } - if (m_gizmos.get_current_type() == GLGizmosManager::EType::MmSegmentation) { - m_gizmos.m_assemble_view_data->model_objects_clipper()->set_position(0.0, true); + if (m_gizmos.get_current_type() == GLGizmosManager::EType::MmSegmentation) return; - } ImGuiWrapper* imgui = wxGetApp().imgui(); @@ -10279,8 +10442,6 @@ void GLCanvas3D::_render_assemble_control() auto canvas_h = float(get_canvas_size().get_height()); const float text_padding = 7.0f; - const float text_size_x = std::max(imgui->calc_text_size(_L("Reset direction")).x + 2 * ImGui::GetStyle().FramePadding.x, - std::max(imgui->calc_text_size(_L("Explosion Ratio")).x, imgui->calc_text_size(_L("Section View")).x)); const float slider_width = 60.0f; const float value_size = imgui->calc_text_size("3.00"sv).x + text_padding * 2; const float item_spacing = imgui->get_item_spacing().x; @@ -10295,34 +10456,6 @@ void GLCanvas3D::_render_assemble_control() tooltip_button_width = _render_assembly_tooltip_button(imgui); } float same_line_width = tooltip_button_width; - { - float clp_dist = m_gizmos.m_assemble_view_data->model_objects_clipper()->get_position(); - if (clp_dist == 0.f) { - ImGui::AlignTextToFramePadding(); - imgui->text(_L("Section View")); - } - else { - if (imgui->button(_L("Reset direction"))) { - wxGetApp().CallAfter([this]() { - m_gizmos.m_assemble_view_data->model_objects_clipper()->set_position(-1., false); - }); - } - } - same_line_width += (text_size_x + item_spacing); - ImGui::SameLine(same_line_width); - ImGui::PushItemWidth(slider_width); - bool view_slider_changed = imgui->bbl_slider_float_style("##clp_dist", &clp_dist, 0.f, 1.f, "%.2f", 1.0f, true); - - same_line_width += (slider_width + item_spacing); - ImGui::SameLine(same_line_width); - ImGui::PushItemWidth(value_size); - bool view_input_changed = ImGui::BBLDragFloat("##clp_dist_input", &clp_dist, 0.05f, 0.0f, 0.0f, "%.2f"); - - if (view_slider_changed || view_input_changed) - m_gizmos.m_assemble_view_data->model_objects_clipper()->set_position(clp_dist, true); - - same_line_width += (value_size + item_spacing * 2); - } { auto temp_x = imgui->calc_text_size(_L("Explosion Ratio")).x; ImGui::SameLine(same_line_width); @@ -10746,12 +10879,42 @@ Vec3d GLCanvas3D::_mouse_to_bed_3d(const Point& mouse_pos) return mouse_ray(mouse_pos).intersect_plane(0.0); } +ClippingPlane GLCanvas3D::_get_section_view_plane() const +{ + const SectionView& section_view = *m_section_view; + if (section_view.ratio == 0.) + return ClippingPlane::ClipsNothing(); + + // The owner's objects, so Preview cuts where Prepare does. + const GLCanvas3D& owner = section_view.owner != nullptr ? *section_view.owner : *this; + BoundingBoxf3 box = owner.volumes_bounding_box(true); + if (!box.defined) + box = owner.volumes_bounding_box(); + // G-code without objects: its toolpaths, merged by corner as a flat box is undefined. + if (!box.defined && m_gcode_viewer.has_data()) { + box.merge(m_gcode_viewer.get_paths_bounding_box().min); + box.merge(m_gcode_viewer.get_paths_bounding_box().max); + } + if (!box.defined) + return ClippingPlane::ClipsNothing(); + + // Sweeps the bounding sphere from the camera side (0) to the far side (1), as ObjectClipper does. + const double radius = box.radius(); + return ClippingPlane(section_view.normal, section_view.normal.dot(box.center()) + radius - 2. * radius * section_view.ratio); +} + +ClippingPlane GLCanvas3D::_get_volumes_clipping_plane() const +{ + const std::optional gizmo_plane = m_gizmos.get_clipping_plane(); + return gizmo_plane.has_value() ? *gizmo_plane : _get_section_view_plane().inverted_normal(); +} + ClippingPlane GLCanvas3D::get_raycaster_clipping_plane() const { // Orca: Ignore the gizmo clipping plane when the active tool does not apply it, and // invert the result into the convention expected by SceneRaycaster. GLGizmoBase* current_gizmo = m_gizmos.get_current(); - return ((!current_gizmo || current_gizmo->apply_clipping_plane()) ? m_gizmos.get_clipping_plane() : + return ((!current_gizmo || current_gizmo->apply_clipping_plane()) ? _get_volumes_clipping_plane() : ClippingPlane::ClipsNothing()) .inverted_normal(); } diff --git a/src/slic3r/GUI/GLCanvas3D.hpp b/src/slic3r/GUI/GLCanvas3D.hpp index f861e15ca1..9dffe195c4 100644 --- a/src/slic3r/GUI/GLCanvas3D.hpp +++ b/src/slic3r/GUI/GLCanvas3D.hpp @@ -619,6 +619,19 @@ private: std::array m_clipping_planes; ClippingPlane m_camera_clipping_plane; bool m_use_clipping_planes; + struct SectionView + { + explicit SectionView(const GLCanvas3D* owner) : owner(owner) {} + double ratio{ 0. }; // 0 = off + double last_ratio{ 0. }; // before it was switched off + Vec3d normal{ Vec3d::Zero() }; // zero until first aimed + bool panel_open{ false }; + const GLCanvas3D* owner; // whose objects place the plane + }; + std::shared_ptr m_section_view{ std::make_shared(this) }; + std::map m_section_view_caps; + // Its release would open the hidden window menu on Windows. + bool m_alt_wheel_used{ false }; std::array m_sla_caps; std::string m_sidebar_field; // when true renders an extra frame by not resetting m_dirty to false @@ -943,6 +956,14 @@ public: void set_color_clip_plane(const Vec3d& cp_normal, double offset) { m_volumes.set_color_clip_plane(cp_normal, offset); } void set_color_clip_plane_colors(const std::array& colors) { m_volumes.set_color_clip_plane_colors(colors); } + bool is_section_view_active() const { return m_section_view->ratio > 0.; } + double get_section_view_ratio() const { return m_section_view->ratio; } + const Vec3d& get_section_view_normal() const { return m_section_view->normal; } + void set_section_view_ratio(double ratio); + void toggle_section_view(); + void align_section_view_to_camera(); + void share_section_view(const GLCanvas3D& owner) { m_section_view = owner.m_section_view; } + void toggle_world_axes_visibility(bool force_show = false); void refresh_camera_scene_box(); void set_color_by(const std::string& value); @@ -1392,6 +1413,7 @@ private: void _render_cad_grid(const Transform3d& view_matrix, const Transform3d& projection_matrix); //BBS: add outline drawing logic void _render_objects(GLVolumeCollection::ERenderType type, bool with_outline = true); + void _render_section_view_caps(); void _render_wireframe_overlay(); bool _is_xray_view_active() const; void _render_xray_volumes(); @@ -1418,6 +1440,7 @@ private: void _render_assemble_view_toolbar() const; void _render_return_toolbar() const; void _render_canvas_toolbar(); + void _render_section_view_panel(const ImVec2& bottom_left); void _render_separator_toolbar_right() const; void _render_separator_toolbar_left() const; void _render_collapse_toolbar() const; @@ -1449,6 +1472,11 @@ private: Gesture }; + void _on_section_view_changed(); + // In the ClippingPlane::is_point_clipped() convention. + ClippingPlane _get_section_view_plane() const; + // In the volume shaders' convention: the open gizmo's own plane, else the section view's. + ClippingPlane _get_volumes_clipping_plane() const; // Orca: These helpers keep clipping, orbit pivots, and perspective-pan depth selection consistent. ClippingPlane get_raycaster_clipping_plane() const; bool is_bed_visible() const; diff --git a/src/slic3r/GUI/Gizmos/GLGizmoBrimEars.cpp b/src/slic3r/GUI/Gizmos/GLGizmoBrimEars.cpp index 7f52b2cc0c..944cfaad7a 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoBrimEars.cpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoBrimEars.cpp @@ -85,7 +85,6 @@ bool GLGizmoBrimEars::on_init() m_shortcut = Shortcut::GizmoBrimEars; const wxString ctrl = GUI::shortkey_ctrl_prefix(); - const wxString alt = GUI::shortkey_alt_prefix(); m_desc["brim_ear_radius"] = _L("Brim ear radius"); m_desc["max_angle"] = _L("Max angle"); @@ -96,13 +95,11 @@ bool GLGizmoBrimEars::on_init() m_desc["create"] = _L("Create"); m_desc["auto_generate"] = _L("Auto-generate"); m_desc["auto-generate-tooltip"] = _L("Generate brim ears using Max angle and Detection radius"); - m_desc["section_view"] = _L("Section view"); m_shortcuts = { {_L("Left mouse button"), _L("Add or Select")}, {_L("Right mouse button"), _L("Remove")}, {ctrl + _L("Mouse wheel"), m_desc["brim_ear_radius"]}, - {alt + _L("Mouse wheel"), m_desc["section_view"]}, }; return true; @@ -551,26 +548,6 @@ bool GLGizmoBrimEars::gizmo_event(SLAGizmoEventType action, const Vec2d &mouse_p apply_radius_change(); } - if (action == SLAGizmoEventType::MouseWheelUp && alt_down) { - double pos = m_c->object_clipper()->get_position(); - pos = std::min(1., pos + 0.01); - m_c->object_clipper()->set_position_by_ratio(pos, false, true); - return true; - } - - if (action == SLAGizmoEventType::MouseWheelDown && alt_down) { - double pos = m_c->object_clipper()->get_position(); - pos = std::max(0., pos - 0.01); - m_c->object_clipper()->set_position_by_ratio(pos, false, true); - return true; - } - - // reset clipper position - if (action == SLAGizmoEventType::ResetClippingPlane) { - m_c->object_clipper()->set_position_by_ratio(-1., false); - return true; - } - return false; } @@ -692,8 +669,8 @@ void GLGizmoBrimEars::on_render_input_window(float x, float y, float bottom_limi ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoTitleBar); float space_size = m_imgui->get_style_scaling() * 8; - std::vector text_list = {m_desc["brim_ear_radius"], m_desc["max_angle"], m_desc["detection_radius"], m_desc["clipping_of_view"], - m_desc["create"], m_desc["remove"]}; + std::vector text_list = {m_desc["brim_ear_radius"], m_desc["max_angle"], m_desc["detection_radius"], m_desc["create"], + m_desc["remove"]}; float widest_text = m_imgui->find_widest_text(text_list); float caption_size = widest_text + space_size + ImGui::GetStyle().WindowPadding.x; float input_text_size = m_imgui->scaled(10.0f); @@ -782,21 +759,6 @@ void GLGizmoBrimEars::on_render_input_window(float x, float y, float bottom_limi } } m_imgui->disabled_end(); - - ImGui::Separator(); - - ImGui::AlignTextToFramePadding(); - float clp_dist = float(m_c->object_clipper()->get_position()); - m_imgui->text(m_desc["section_view"]); - ImGui::SameLine(caption_size); - ImGui::PushItemWidth(slider_width); - bool slider_clp_dist = m_imgui->bbl_slider_float_style("##section_view", &clp_dist, 0.f, 1.f, "%.2f", 1.0f, true); - ImGui::SameLine(drag_left_width); - ImGui::PushItemWidth(1.5 * slider_icon_width); - bool b_clp_dist_input = ImGui::BBLDragFloat("##section_view_input", &clp_dist, 0.05f, 0.0f, 0.0f, "%.2f"); - if (slider_clp_dist || b_clp_dist_input) { - m_c->object_clipper()->set_position_by_ratio(clp_dist, false, true); - } ImGui::Separator(); diff --git a/src/slic3r/GUI/Gizmos/GLGizmoFdmSupports.cpp b/src/slic3r/GUI/Gizmos/GLGizmoFdmSupports.cpp index 6a6288a71c..7ed567ac33 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoFdmSupports.cpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoFdmSupports.cpp @@ -112,36 +112,30 @@ bool GLGizmoFdmSupports::on_init() m_desc["highlight_by_angle"] = _L("Highlight overhangs"); m_desc["tool_type"] = _L("Tool type"); m_desc["gap_fill"] = _L("Gap fill"); - m_desc["reset_direction"] = _L("Reset direction"); - m_desc["clipping_of_view"] = _L("Section view"); m_desc["cursor_size"] = _L("Brush size"); m_desc["smart_fill_angle"] = _L("Smart fill angle"); m_desc["gap_area"] = _L("Gap area"); const wxString ctrl = GUI::shortkey_ctrl_prefix(); - const wxString alt = GUI::shortkey_alt_prefix(); const wxString shift = GUI::shortkey_shift_prefix(); std::pair enforce_shortcut = {_L("Left mouse button"), _L("Enforce supports")}; std::pair block_shortcut = {_L("Right mouse button"), _L("Block supports")}; std::pair remove_shortcut = {shift + _L("Left mouse button"), _L("Erase")}; - std::pair clipping_shortcut = {alt + _L("Mouse wheel"), m_desc["clipping_of_view"]}; m_shortcuts_brush = { enforce_shortcut, block_shortcut, remove_shortcut, - {ctrl + _L("Mouse wheel"), m_desc["cursor_size"]}, - clipping_shortcut + {ctrl + _L("Mouse wheel"), m_desc["cursor_size"]} }; m_shortcuts_bucket_fill = { enforce_shortcut, block_shortcut, remove_shortcut, - {ctrl + _L("Mouse wheel"), m_desc["smart_fill_angle"]}, - clipping_shortcut + {ctrl + _L("Mouse wheel"), m_desc["smart_fill_angle"]} }; m_shortcuts_gap_fill = { @@ -237,11 +231,9 @@ void GLGizmoFdmSupports::on_render_input_window(float x, float y, float bottom_l // First calculate width of all the texts that are could possibly be shown. We will decide set the dialog width based on that: const float space_size = m_imgui->get_style_scaling() * 8; - const float clipping_slider_left = m_imgui->calc_text_size(m_desc.at("clipping_of_view")).x + m_imgui->scaled(1.5f); const float cursor_slider_left = m_imgui->calc_text_size(m_desc.at("cursor_size")).x + m_imgui->scaled(1.5f); const float gap_fill_slider_left = m_imgui->calc_text_size(m_desc.at("gap_fill")).x + m_imgui->scaled(1.5f); const float highlight_slider_left = m_imgui->calc_text_size(m_desc.at("highlight_by_angle")).x + m_imgui->scaled(1.5f); - const float reset_button_slider_left = m_imgui->calc_text_size(m_desc.at("reset_direction")).x + m_imgui->scaled(1.5f) + ImGui::GetStyle().FramePadding.x * 2; const float on_overhangs_only_width = m_imgui->calc_text_size(m_desc["on_overhangs_only"]).x + m_imgui->scaled(1.5f); const float filter_btn_width = m_imgui->calc_text_size(m_desc.at("perform")).x + m_imgui->scaled(1.5f); const float gap_area_txt_width = m_imgui->calc_text_size(m_desc.at("gap_area")).x + m_imgui->scaled(1.5f); @@ -254,14 +246,14 @@ void GLGizmoFdmSupports::on_render_input_window(float x, float y, float bottom_l float caption_max = 0.f; float total_text_max = 0.f; - for (const auto &t : std::array{"enforce", "block", "remove", "cursor_size", "clipping_of_view"}) { + for (const auto &t : std::array{"enforce", "block", "remove", "cursor_size"}) { caption_max = std::max(caption_max, m_imgui->calc_text_size(m_desc[t + "_caption"]).x); total_text_max = std::max(total_text_max, m_imgui->calc_text_size(m_desc[t]).x); } total_text_max += caption_max + m_imgui->scaled(1.f); caption_max += m_imgui->scaled(1.f); - const float sliders_left_width = std::max(gap_area_txt_width, std::max(smart_fill_angle_txt_width, std::max(reset_button_slider_left, std::max(std::max(cursor_slider_left, clipping_slider_left), std::max(highlight_slider_left, gap_fill_slider_left))))); + const float sliders_left_width = std::max(gap_area_txt_width, std::max(smart_fill_angle_txt_width, std::max(cursor_slider_left, std::max(highlight_slider_left, gap_fill_slider_left)))); const float slider_icon_width = m_imgui->get_slider_icon_size().x; const float max_tooltip_width = ImGui::GetFontSize() * 20.0f; @@ -458,29 +450,6 @@ void GLGizmoFdmSupports::on_render_input_window(float x, float y, float bottom_l ImGui::PushItemWidth(1.5 * slider_icon_width); ImGui::BBLDragFloat("##angle_threshold_deg_input", &m_highlight_by_angle_threshold_deg, 0.05f, 0.0f, 0.0f, "%.2f"); - ImGui::Separator(); - if (m_c->object_clipper()->get_position() == 0.f) { - ImGui::AlignTextToFramePadding(); - m_imgui->text(m_desc.at("clipping_of_view")); - } - else { - if (m_imgui->button(m_desc.at("reset_direction"))) { - wxGetApp().CallAfter([this]() { - m_c->object_clipper()->set_position_by_ratio(-1., false); - }); - } - } - auto clp_dist = float(m_c->object_clipper()->get_position()); - ImGui::SameLine(sliders_left_width); - ImGui::PushItemWidth(sliders_width); - bool b_bbl_slider_float = m_imgui->bbl_slider_float_style("##clp_dist", &clp_dist, 0.f, 1.f, "%.2f", 1.0f, true); - - ImGui::SameLine(drag_left_width + sliders_left_width); - ImGui::PushItemWidth(1.5 * slider_icon_width); - bool b_drag_input = ImGui::BBLDragFloat("##clp_dist_input", &clp_dist, 0.05f, 0.0f, 0.0f, "%.2f"); - - if (b_bbl_slider_float || b_drag_input) m_c->object_clipper()->set_position_by_ratio(clp_dist, true); - ImGui::Separator(); render_tooltip_button(x, y); diff --git a/src/slic3r/GUI/Gizmos/GLGizmoFuzzySkin.cpp b/src/slic3r/GUI/Gizmos/GLGizmoFuzzySkin.cpp index 7f36e85e80..e03912456a 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoFuzzySkin.cpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoFuzzySkin.cpp @@ -51,10 +51,8 @@ bool GLGizmoFuzzySkin::on_init() m_shortcut = Shortcut::GizmoFuzzySkin; const wxString ctrl = GUI::shortkey_ctrl_prefix(); - const wxString alt = GUI::shortkey_alt_prefix(); const wxString shift = GUI::shortkey_shift_prefix(); - m_desc["reset_direction"] = _L("Reset direction"); m_desc["remove_all"] = _L("Erase all"); m_desc["circle"] = _L("Circle"); m_desc["sphere"] = _L("Sphere"); @@ -62,7 +60,6 @@ bool GLGizmoFuzzySkin::on_init() m_desc["tool_type"] = _L("Tool type"); m_desc["tool_brush"] = _L("Brush"); m_desc["tool_smart_fill"] = _L("Smart fill"); - m_desc["clipping_of_view"] = _L("Section view"); m_desc["cursor_size"] = _L("Brush size"); m_desc["add_fuzzy_skin"] = _L("Add fuzzy skin"); m_desc["remove_fuzzy_skin"] = _L("Remove fuzzy skin"); @@ -70,26 +67,22 @@ bool GLGizmoFuzzySkin::on_init() std::pair add_fuzzy_skin_shortcut = {_L("Left mouse button"), m_desc["add_fuzzy_skin"]}; std::pair remove_fuzzy_skin_shortcut = {shift + _L("Left mouse button"), m_desc["remove_fuzzy_skin"]}; - std::pair clipping_shortcut = {alt + _L("Mouse wheel"), m_desc["clipping_of_view"]}; m_shortcuts_brush = { add_fuzzy_skin_shortcut, remove_fuzzy_skin_shortcut, - {ctrl + _L("Mouse wheel"), m_desc["cursor_size"]}, - clipping_shortcut + {ctrl + _L("Mouse wheel"), m_desc["cursor_size"]} }; m_shortcuts_triangle = { add_fuzzy_skin_shortcut, - remove_fuzzy_skin_shortcut, - clipping_shortcut + remove_fuzzy_skin_shortcut }; m_shortcuts_smart_fill = { add_fuzzy_skin_shortcut, remove_fuzzy_skin_shortcut, - {ctrl + _L("Mouse wheel"), m_desc["smart_fill_angle"]}, - clipping_shortcut + {ctrl + _L("Mouse wheel"), m_desc["smart_fill_angle"]} }; return true; @@ -162,8 +155,6 @@ void GLGizmoFuzzySkin::on_render_input_window(float x, float y, float bottom_lim // First calculate width of all the texts that are could possibly be shown. We will decide set the dialog width based on that: const float space_size = m_imgui->get_style_scaling() * 8; - const float clipping_slider_left = std::max(m_imgui->calc_text_size(m_desc.at("clipping_of_view")).x + m_imgui->scaled(1.5f), - m_imgui->calc_text_size(m_desc.at("reset_direction")).x + m_imgui->scaled(1.5f) + ImGui::GetStyle().FramePadding.x * 2); const float cursor_slider_left = m_imgui->calc_text_size(m_desc.at("cursor_size")).x + m_imgui->scaled(1.5f); const float smart_fill_slider_left = m_imgui->calc_text_size(m_desc.at("smart_fill_angle")).x + m_imgui->scaled(1.5f); @@ -187,9 +178,7 @@ void GLGizmoFuzzySkin::on_render_input_window(float x, float y, float bottom_lim total_text_max += caption_max + m_imgui->scaled(1.f); caption_max += m_imgui->scaled(1.f); - const float circle_max_width = std::max(clipping_slider_left, cursor_slider_left); - - const float sliders_left_width = std::max(smart_fill_slider_left, std::max(cursor_slider_left, clipping_slider_left)); + const float sliders_left_width = std::max(smart_fill_slider_left, cursor_slider_left); const float slider_icon_width = m_imgui->get_slider_icon_size().x; float window_width = minimal_slider_width + sliders_left_width + slider_icon_width; const float empty_button_width = m_imgui->calc_button_size("").x; @@ -259,10 +248,10 @@ void GLGizmoFuzzySkin::on_render_input_window(float x, float y, float bottom_lim ImGui::AlignTextToFramePadding(); m_imgui->text(m_desc.at("cursor_size")); - ImGui::SameLine(circle_max_width); + ImGui::SameLine(cursor_slider_left); ImGui::PushItemWidth(sliders_width); m_imgui->bbl_slider_float_style("##cursor_radius", &m_cursor_radius, CursorRadiusMin, CursorRadiusMax, "%.2f", 1.0f, true); - ImGui::SameLine(drag_left_width + circle_max_width); + ImGui::SameLine(drag_left_width + cursor_slider_left); ImGui::PushItemWidth(1.5 * slider_icon_width); ImGui::BBLDragFloat("##cursor_radius_input", &m_cursor_radius, 0.05f, 0.0f, 0.0f, "%.2f"); } else if (m_current_tool == ImGui::TriangleButtonIcon) { @@ -291,30 +280,6 @@ void GLGizmoFuzzySkin::on_render_input_window(float x, float y, float bottom_lim ImGui::BBLDragFloat("##smart_fill_angle_input", &m_smart_fill_angle, 0.05f, 0.0f, 0.0f, "%.2f"); } - ImGui::Separator(); - if (m_c->object_clipper()->get_position() == 0.f) { - ImGui::AlignTextToFramePadding(); - m_imgui->text(m_desc.at("clipping_of_view")); - } - else { - if (m_imgui->button(m_desc.at("reset_direction"))) { - wxGetApp().CallAfter([this](){ - m_c->object_clipper()->set_position_by_ratio(-1., false); - }); - } - } - - auto clp_dist = float(m_c->object_clipper()->get_position()); - ImGui::SameLine(sliders_left_width); - - ImGui::PushItemWidth(sliders_width); - bool slider_clp_dist = m_imgui->bbl_slider_float_style("##clp_dist", &clp_dist, 0.f, 1.f, "%.2f", 1.0f, true); - - ImGui::SameLine(drag_left_width + sliders_left_width); - ImGui::PushItemWidth(1.5 * slider_icon_width); - bool b_clp_dist_input = ImGui::BBLDragFloat("##clp_dist_input", &clp_dist, 0.05f, 0.0f, 0.0f, "%.2f"); - if (slider_clp_dist || b_clp_dist_input) { m_c->object_clipper()->set_position_by_ratio(clp_dist, true); } - ImGui::Separator(); render_tooltip_button(x, y); diff --git a/src/slic3r/GUI/Gizmos/GLGizmoMmuSegmentation.cpp b/src/slic3r/GUI/Gizmos/GLGizmoMmuSegmentation.cpp index 2a19cf3ee7..a08ded3003 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoMmuSegmentation.cpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoMmuSegmentation.cpp @@ -118,8 +118,6 @@ bool GLGizmoMmuSegmentation::on_init() const wxString alt = GUI::shortkey_alt_prefix(); const wxString shift = GUI::shortkey_shift_prefix(); - m_desc["clipping_of_view"] = _L("Section view"); - m_desc["reset_direction"] = _L("Reset direction"); m_desc["cursor_size"] = _L("Brush size"); m_desc["cursor_type"] = _L("Brush shape"); m_desc["paint"] = _L("Paint"); @@ -146,14 +144,12 @@ bool GLGizmoMmuSegmentation::on_init() std::pair paint_shortcut = {_L("Left mouse button"), m_desc["paint"]}; std::pair erase_shortcut = {shift + _L("Left mouse button"), m_desc["erase"]}; - std::pair clipping_shortcut = {alt + _L("Mouse wheel"), m_desc["clipping_of_view"]}; std::pair toggle_wireframe_shortcut = {alt + shift + _L_CONTEXT("Enter", "Keyboard Shortcut"), m_desc["toggle_wireframe"]}; m_shortcuts_brush = { paint_shortcut, erase_shortcut, {ctrl + _L("Mouse wheel"), m_desc["cursor_size"]}, - clipping_shortcut, toggle_wireframe_shortcut }; @@ -161,7 +157,6 @@ bool GLGizmoMmuSegmentation::on_init() paint_shortcut, erase_shortcut, {ctrl + _L("Mouse wheel"), m_desc["smart_fill_angle"]}, - clipping_shortcut, toggle_wireframe_shortcut }; @@ -397,8 +392,6 @@ void GLGizmoMmuSegmentation::on_render_input_window(float x, float y, float bott // First calculate width of all the texts that are could possibly be shown. We will decide set the dialog width based on that: const float space_size = m_imgui->get_style_scaling() * 8; - const float clipping_slider_left = std::max(m_imgui->calc_text_size(m_desc.at("clipping_of_view")).x + m_imgui->scaled(1.5f), - m_imgui->calc_text_size(m_desc.at("reset_direction")).x + m_imgui->scaled(1.5f) + ImGui::GetStyle().FramePadding.x * 2); const float cursor_slider_left = m_imgui->calc_text_size(m_desc.at("cursor_size")).x + m_imgui->scaled(1.5f); const float smart_fill_slider_left = m_imgui->calc_text_size(m_desc.at("smart_fill_angle")).x + m_imgui->scaled(1.5f); const float edge_detect_slider_left = m_imgui->calc_text_size(m_desc.at("edge_detection")).x + m_imgui->scaled(1.f); @@ -413,18 +406,15 @@ void GLGizmoMmuSegmentation::on_render_input_window(float x, float y, float bott float caption_max = 0.f; float total_text_max = 0.f; - for (const auto &t : std::array{"paint", "erase", "cursor_size", "smart_fill_angle", "height_range", "clipping_of_view"}) { + for (const auto &t : std::array{"paint", "erase", "cursor_size", "smart_fill_angle", "height_range"}) { caption_max = std::max(caption_max, m_imgui->calc_text_size(m_desc[t + "_caption"]).x); total_text_max = std::max(total_text_max, m_imgui->calc_text_size(m_desc[t]).x); } total_text_max += caption_max + m_imgui->scaled(1.f); caption_max += m_imgui->scaled(1.f); - const float circle_max_width = std::max(clipping_slider_left,cursor_slider_left); - const float height_max_width = std::max(clipping_slider_left,height_range_slider_left); const float sliders_left_width = std::max(smart_fill_slider_left, - std::max(cursor_slider_left, std::max(edge_detect_slider_left, std::max(gap_area_slider_left, std::max(height_range_slider_left, - clipping_slider_left))))) + space_size; + std::max(cursor_slider_left, std::max(edge_detect_slider_left, std::max(gap_area_slider_left, height_range_slider_left)))) + space_size; const float slider_icon_width = m_imgui->get_slider_icon_size().x; float window_width = minimal_slider_width + sliders_left_width + slider_icon_width; const int max_filament_items_per_line = 8; @@ -658,28 +648,6 @@ void GLGizmoMmuSegmentation::on_render_input_window(float x, float y, float bott } } - ImGui::Separator(); - if (m_c->object_clipper()->get_position() == 0.f) { - ImGui::AlignTextToFramePadding(); - m_imgui->text(m_desc.at("clipping_of_view")); - } else { - if (m_imgui->button(m_desc.at("reset_direction"))) { - wxGetApp().CallAfter([this]() { m_c->object_clipper()->set_position_by_ratio(-1., false); }); - } - } - - auto clp_dist = float(m_c->object_clipper()->get_position()); - ImGui::SameLine(sliders_left_width); - ImGui::PushItemWidth(sliders_width); - bool slider_clp_dist = m_imgui->bbl_slider_float_style("##clp_dist", &clp_dist, 0.f, 1.f, "%.2f", 1.0f, true); - ImGui::SameLine(drag_left_width + sliders_left_width); - ImGui::PushItemWidth(1.5 * slider_icon_width); - bool b_clp_dist_input = ImGui::BBLDragFloat("##clp_dist_input", &clp_dist, 0.05f, 0.0f, 0.0f, "%.2f"); - - if (slider_clp_dist || b_clp_dist_input) { - m_c->object_clipper()->set_position_by_ratio(clp_dist, true); - } - ImGui::Separator(); render_tooltip_button(x, y); diff --git a/src/slic3r/GUI/Gizmos/GLGizmoPainterBase.cpp b/src/slic3r/GUI/Gizmos/GLGizmoPainterBase.cpp index c9847afcde..75d2905a86 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoPainterBase.cpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoPainterBase.cpp @@ -750,20 +750,6 @@ bool GLGizmoPainterBase::gizmo_event(SLAGizmoEventType action, const Vec2d& mous return true; } } - else if (alt_down) { - // BBS - double pos = m_c->object_clipper()->get_position(); - pos = action == SLAGizmoEventType::MouseWheelDown - ? std::max(0., pos - 0.01) - : std::min(1., pos + 0.01); - m_c->object_clipper()->set_position_by_ratio(pos, true); - return true; - } - } - - if (action == SLAGizmoEventType::ResetClippingPlane) { - m_c->object_clipper()->set_position_by_ratio(-1., false); - return true; } if (action == SLAGizmoEventType::LeftDown diff --git a/src/slic3r/GUI/Gizmos/GLGizmoSeam.cpp b/src/slic3r/GUI/Gizmos/GLGizmoSeam.cpp index 3ae5604226..1d6d31a047 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoSeam.cpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoSeam.cpp @@ -46,11 +46,8 @@ bool GLGizmoSeam::on_init() m_shortcut = Shortcut::GizmoSeam; const wxString ctrl = GUI::shortkey_ctrl_prefix(); - const wxString alt = GUI::shortkey_alt_prefix(); const wxString shift = GUI::shortkey_shift_prefix(); - m_desc["clipping_of_view"] = _L("Section view"); - m_desc["reset_direction"] = _L("Reset direction"); m_desc["cursor_size"] = _L("Brush size"); m_desc["tool_type"] = _L("Tool type"); m_desc["enforce"] = _L("Enforce seam"); @@ -64,8 +61,7 @@ bool GLGizmoSeam::on_init() {_L("Left mouse button"), m_desc["enforce"]}, {_L("Right mouse button"), m_desc["block"]}, {shift + _L("Left mouse button"), m_desc["remove"]}, - {ctrl + _L("Mouse wheel"), m_desc["cursor_size"]}, - {alt + _L("Mouse wheel"), m_desc["clipping_of_view"]} + {ctrl + _L("Mouse wheel"), m_desc["cursor_size"]} }; return true; @@ -155,20 +151,17 @@ void GLGizmoSeam::on_render_input_window(float x, float y, float bottom_limit) // First calculate width of all the texts that are could possibly be shown. We will decide set the dialog width based on that: const float space_size = m_imgui->get_style_scaling() * 8; - const float clipping_slider_left = std::max(m_imgui->calc_text_size(m_desc.at("clipping_of_view")).x, - m_imgui->calc_text_size(m_desc.at("reset_direction")).x + ImGui::GetStyle().FramePadding.x * 2) - + m_imgui->scaled(1.5f); const float cursor_size_slider_left = m_imgui->calc_text_size(m_desc.at("cursor_size")).x + m_imgui->scaled(1.f); const float empty_button_width = m_imgui->calc_button_size("").x; float caption_max = 0.f; float total_text_max = 0.f; - for (const auto &t : std::array{"enforce", "block", "remove", "cursor_size", "clipping_of_view"}) { + for (const auto &t : std::array{"enforce", "block", "remove", "cursor_size"}) { caption_max = std::max(caption_max, m_imgui->calc_text_size(m_desc[t + "_caption"]).x); total_text_max = std::max(total_text_max, m_imgui->calc_text_size(m_desc[t]).x); } - const float sliders_left_width = std::max(cursor_size_slider_left, clipping_slider_left); + const float sliders_left_width = cursor_size_slider_left; const float slider_icon_width = m_imgui->get_slider_icon_size().x; const float sliders_width = m_imgui->scaled(7.0f); @@ -242,28 +235,6 @@ void GLGizmoSeam::on_render_input_window(float x, float y, float bottom_limit) m_imgui->bbl_checkbox(_L("Vertical"), m_vertical_only); - ImGui::Separator(); - if (m_c->object_clipper()->get_position() == 0.f) { - ImGui::AlignTextToFramePadding(); - m_imgui->text(m_desc.at("clipping_of_view")); - } - else { - if (m_imgui->button(m_desc.at("reset_direction"))) { - wxGetApp().CallAfter([this](){ - m_c->object_clipper()->set_position_by_ratio(-1., false); - }); - } - } - - auto clp_dist = float(m_c->object_clipper()->get_position()); - ImGui::SameLine(sliders_left_width); - ImGui::PushItemWidth(sliders_width); - bool slider_clp_dist = m_imgui->bbl_slider_float_style("##clp_dist", &clp_dist, 0.f, 1.f, "%.2f", 1.0f, true); - ImGui::SameLine(drag_left_width + sliders_left_width); - ImGui::PushItemWidth(1.5 * slider_icon_width); - bool b_clp_dist_input = ImGui::BBLDragFloat("##clp_dist_input", &clp_dist, 0.05f, 0.0f, 0.0f, "%.2f"); - if (slider_clp_dist || b_clp_dist_input) { m_c->object_clipper()->set_position_by_ratio(clp_dist, true); } - ImGui::Separator(); GLGizmoUtils::render_tooltip_button(m_imgui, m_parent, m_shortcuts, x, y); diff --git a/src/slic3r/GUI/Gizmos/GLGizmosCommon.cpp b/src/slic3r/GUI/Gizmos/GLGizmosCommon.cpp index 18308b81b2..9d684cdb88 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmosCommon.cpp +++ b/src/slic3r/GUI/Gizmos/GLGizmosCommon.cpp @@ -380,22 +380,19 @@ std::vector ObjectClipper::point_per_contour() const } -void ObjectClipper::set_position_by_ratio(double pos, bool keep_normal, bool vertical_normal) +void ObjectClipper::set_position_by_ratio(double pos, const Vec3d& normal) { const ModelObject* mo = get_pool()->selection_info()->model_object(); int active_inst = get_pool()->selection_info()->get_active_instance(); double z_shift = get_pool()->selection_info()->get_sla_shift(); - - Vec3d normal; - if(vertical_normal) { - normal = {0, 0, 1}; - }else { - //Vec3d camera_dir = wxGetApp().plater()->get_camera().get_dir_forward(); - //if (abs(camera_dir(0)) > EPSILON || abs(camera_dir(1)) > EPSILON) - // camera_dir(2) = 0; - - normal = (keep_normal && m_clp) ? m_clp->get_normal() : /*-camera_dir;*/ -wxGetApp().plater()->get_camera().get_dir_forward(); + if (pos == 0. || mo == nullptr || active_inst < 0) { + // No plane, so the raycasts are not clipped. + m_clp_ratio = 0.; + m_clp.reset(); + get_pool()->get_canvas()->set_as_dirty(); + return; } + Vec3d center; if (get_pool()->get_canvas()->get_canvas_type() == GLCanvas3D::CanvasAssembleView) { @@ -410,9 +407,6 @@ void ObjectClipper::set_position_by_ratio(double pos, bool keep_normal, bool ver float dist = normal.dot(center); - if (pos < 0.) - pos = m_clp_ratio; - m_clp_ratio = pos; m_clp.reset(new ClippingPlane(normal, (dist - (-m_active_inst_bb_radius) - m_clp_ratio * 2 * m_active_inst_bb_radius))); @@ -445,171 +439,5 @@ void ObjectClipper::set_behavior(bool hide_clipped, bool fill_cut, double contou } -using namespace AssembleViewDataObjects; -AssembleViewDataPool::AssembleViewDataPool(GLCanvas3D* canvas) - : m_canvas(canvas) -{ - using c = AssembleViewDataID; - m_data[c::ModelObjectsInfo].reset(new ModelObjectsInfo(this)); - m_data[c::ModelObjectsClipper].reset(new ModelObjectsClipper(this)); -} - -void AssembleViewDataPool::update(AssembleViewDataID required) -{ - assert(check_dependencies(required)); - for (auto& [id, data] : m_data) { - if (int(required) & int(AssembleViewDataID(id))) - data->update(); - else - if (data->is_valid()) - data->release(); - } -} - - -ModelObjectsInfo* AssembleViewDataPool::model_objects_info() const -{ - ModelObjectsInfo* sel_info = dynamic_cast(m_data.at(AssembleViewDataID::ModelObjectsInfo).get()); - assert(sel_info); - return sel_info->is_valid() ? sel_info : nullptr; -} - - -ModelObjectsClipper* AssembleViewDataPool::model_objects_clipper() const -{ - ModelObjectsClipper* oc = dynamic_cast(m_data.at(AssembleViewDataID::ModelObjectsClipper).get()); - // ObjectClipper is used from outside the gizmos to report current clipping plane. - // This function can be called when oc is nullptr. - return (oc && oc->is_valid()) ? oc : nullptr; -} - -#ifndef NDEBUG -// Check the required resources one by one and return true if all -// dependencies are met. -bool AssembleViewDataPool::check_dependencies(AssembleViewDataID required) const -{ - // This should iterate over currently required data. Each of them should - // be asked about its dependencies and it must check that all dependencies - // are also in required and before the current one. - for (auto& [id, data] : m_data) { - // in case we don't use this, the deps are irrelevant - if (!(int(required) & int(AssembleViewDataID(id)))) - continue; - - - AssembleViewDataID deps = data->get_dependencies(); - assert(int(deps) == (int(deps) & int(required))); - } - - -return true; -} -#endif // NDEBUG - - - - -void ModelObjectsInfo::on_update() -{ - if (!get_pool()->get_canvas()->get_model()->objects.empty()) { - m_model_objects = get_pool()->get_canvas()->get_model()->objects; - } - else { - m_model_objects.clear(); - } -} - -void ModelObjectsInfo::on_release() -{ - m_model_objects.clear(); -} - -//int ModelObjectsInfo::get_active_instance() const -//{ -// const Selection& selection = get_pool()->get_canvas()->get_selection(); -// return selection.get_instance_idx(); -//} - - -void ModelObjectsClipper::on_update() -{ - const ModelObjectPtrs model_objects = get_pool()->model_objects_info()->model_objects(); - if (model_objects.empty()) - return; - - // which mesh should be cut? - std::vector meshes; - - if (meshes.empty()) - for (auto mo : model_objects) { - for (const ModelVolume* mv : mo->volumes) - meshes.push_back(&mv->mesh()); - } - - if (meshes != m_old_meshes) { - m_clippers.clear(); - for (const TriangleMesh* mesh : meshes) { - m_clippers.emplace_back(new MeshClipper); - m_clippers.back()->set_mesh(mesh->its); - } - m_old_meshes = meshes; - - m_active_inst_bb_radius = get_pool()->get_canvas()->volumes_bounding_box().radius(); - } -} - - -void ModelObjectsClipper::on_release() -{ - m_clippers.clear(); - m_old_meshes.clear(); - m_clp.reset(); - m_clp_ratio = 0.; - -} - -void ModelObjectsClipper::render_cut() const -{ - if (m_clp_ratio == 0.) - return; - const ModelObjectPtrs model_objects = get_pool()->model_objects_info()->model_objects(); - - size_t clipper_id = 0; - for (const ModelObject* mo : model_objects) { - Geometry::Transformation assemble_objects_trafo = mo->instances[0]->get_assemble_transformation(); - auto offset_to_assembly = mo->instances[0]->get_offset_to_assembly(); - for (const ModelVolume* mv : mo->volumes) { - Geometry::Transformation vol_trafo = mv->get_transformation(); - Geometry::Transformation trafo = assemble_objects_trafo * vol_trafo; - trafo.set_offset(trafo.get_offset() + vol_trafo.get_offset() * (GLVolume::explosion_ratio - 1.0) + offset_to_assembly * (GLVolume::explosion_ratio - 1.0)); - - auto& clipper = m_clippers[clipper_id]; - clipper->set_plane(*m_clp); - clipper->set_transformation(trafo); - // BBS - clipper->render_cut({0.25f, 0.25f, 0.25f, 1.0f}); - - ++clipper_id; - } - } -} - - -void ModelObjectsClipper::set_position(double pos, bool keep_normal) -{ - Vec3d normal = (keep_normal && m_clp) ? m_clp->get_normal() : -wxGetApp().plater()->get_camera().get_dir_forward(); - const Vec3d& center = get_pool()->get_canvas()->volumes_bounding_box().center(); - float dist = normal.dot(center); - - if (pos < 0.) - pos = m_clp_ratio; - - m_clp_ratio = pos; - m_clp.reset(new ClippingPlane(normal, (dist - (-m_active_inst_bb_radius * GLVolume::explosion_ratio) - m_clp_ratio * 2 * m_active_inst_bb_radius * GLVolume::explosion_ratio))); - get_pool()->get_canvas()->set_as_dirty(); - -} - - } // namespace GUI } // namespace Slic3r diff --git a/src/slic3r/GUI/Gizmos/GLGizmosCommon.hpp b/src/slic3r/GUI/Gizmos/GLGizmosCommon.hpp index d2eeda4f64..f89914e445 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmosCommon.hpp +++ b/src/slic3r/GUI/Gizmos/GLGizmosCommon.hpp @@ -52,7 +52,6 @@ enum class SLAGizmoEventType : unsigned char { class CommonGizmosDataBase; -class AssembleViewDataBase; namespace CommonGizmosDataObjects { class SelectionInfo; class InstancesHider; @@ -62,11 +61,6 @@ namespace CommonGizmosDataObjects { class SupportsClipper; } -namespace AssembleViewDataObjects { - class ModelObjectsInfo; - class ModelObjectsClipper; -} - // Some of the gizmos use the same data that need to be updated ocassionally. // It is also desirable that the data are not recalculated when the gizmos // are just switched, but on the other hand, they should be released when @@ -245,7 +239,7 @@ public: void set_position_to_init_layer(); const ClippingPlane* get_clipping_plane(bool ignore_hide_clipped = false) const; void render_cut(const std::vector* ignore_idxs = nullptr) const; - void set_position_by_ratio(double pos, bool keep_normal, bool vertical_normal=false); + void set_position_by_ratio(double pos, const Vec3d& normal); void set_range_and_pos(const Vec3d& cpl_normal, double cpl_offset, double pos); void set_behavior(bool hide_clipped, bool fill_cut, double contour_width); @@ -272,121 +266,6 @@ private: } // namespace CommonGizmosDataObjects -enum class AssembleViewDataID { - None = 0, - ModelObjectsInfo = 1 << 0, - ModelObjectsClipper = 1 << 4, -}; - -class AssembleViewDataPool { -public: - AssembleViewDataPool(GLCanvas3D* canvas); - - // Update all resources and release what is not used. - // Accepts a bitmask of currently required resources. - void update(AssembleViewDataID required); - - // Getters for the data that need to be accessed from the gizmos directly. - AssembleViewDataObjects::ModelObjectsInfo* model_objects_info() const; - AssembleViewDataObjects::ModelObjectsClipper* model_objects_clipper() const; - - GLCanvas3D* get_canvas() const { return m_canvas; } - -private: - std::map> m_data; - GLCanvas3D* m_canvas; - -#ifndef NDEBUG - bool check_dependencies(AssembleViewDataID required) const; -#endif -}; - -// Base class for a wrapper object managing a single resource. -// Each of the enum values above (safe None) will have an object of this kind. -class AssembleViewDataBase { -public: - // Pass a backpointer to the pool, so the individual - // objects can communicate with one another. - explicit AssembleViewDataBase(AssembleViewDataPool* cgdp) - : m_common{ cgdp } {} - virtual ~AssembleViewDataBase() {} - - // Update the resource. - void update() { on_update(); m_is_valid = true; } - - // Release any data that are stored internally. - void release() { on_release(); m_is_valid = false; } - - // Returns whether the resource is currently maintained. - bool is_valid() const { return m_is_valid; } - -#ifndef NDEBUG - // Return a bitmask of all resources that this one relies on. - // The dependent resource must have higher ID than the one - // it depends on. - virtual AssembleViewDataID get_dependencies() const { return AssembleViewDataID::None; } -#endif // NDEBUG - -protected: - virtual void on_release() = 0; - virtual void on_update() = 0; - AssembleViewDataPool* get_pool() const { return m_common; } - - -private: - bool m_is_valid = false; - AssembleViewDataPool* m_common = nullptr; -}; - -namespace AssembleViewDataObjects -{ -class ModelObjectsInfo : public AssembleViewDataBase -{ -public: - explicit ModelObjectsInfo(AssembleViewDataPool* cgdp) - : AssembleViewDataBase(cgdp) {} - - ModelObjectPtrs model_objects() const { return m_model_objects; } - //int get_active_instance() const; - float get_sla_shift() const { return m_z_shift; } - -protected: - void on_update() override; - void on_release() override; - -private: - ModelObjectPtrs m_model_objects; - float m_z_shift = 0.f; -}; - -class ModelObjectsClipper : public AssembleViewDataBase -{ -public: - explicit ModelObjectsClipper(AssembleViewDataPool* cgdp) - : AssembleViewDataBase(cgdp) {} -#ifndef NDEBUG - AssembleViewDataID get_dependencies() const override { return AssembleViewDataID::ModelObjectsInfo; } -#endif // NDEBUG - - void set_position(double pos, bool keep_normal); - double get_position() const { return m_clp_ratio; } - ClippingPlane* get_clipping_plane() const { return m_clp.get(); } - void render_cut() const; - - -protected: - void on_update() override; - void on_release() override; - -private: - std::vector m_old_meshes; - std::vector> m_clippers; - std::unique_ptr m_clp; - double m_clp_ratio = 0.; - double m_active_inst_bb_radius = 0.; -}; -} - } // namespace GUI } // namespace Slic3r diff --git a/src/slic3r/GUI/Gizmos/GLGizmosManager.cpp b/src/slic3r/GUI/Gizmos/GLGizmosManager.cpp index f9d88b67b8..3eaaf814b6 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmosManager.cpp +++ b/src/slic3r/GUI/Gizmos/GLGizmosManager.cpp @@ -278,8 +278,6 @@ bool GLGizmosManager::init() //m_gizmos.emplace_back(new GLGizmoHollow(m_parent, "hollow.svg", sprite_id++)); m_common_gizmos_data.reset(new CommonGizmosDataPool(&m_parent)); - if(!m_assemble_view_data) - m_assemble_view_data.reset(new AssembleViewDataPool(&m_parent)); for (auto& gizmo : m_gizmos) { if (! gizmo->init()) { @@ -374,7 +372,17 @@ bool GLGizmosManager::init_icon_textures() icon_list.insert(std::make_pair((int) IC_CANVAS_ZOOM_DARK_HOVER, texture_id)); else return false; - + + for (const auto& [icon, name] : std::initializer_list>{ + { IC_CANVAS_SECTION, "canvas_section" }, { IC_CANVAS_SECTION_HOVER, "canvas_section_hover" }, + { IC_CANVAS_SECTION_DARK, "canvas_section_dark" }, { IC_CANVAS_SECTION_DARK_HOVER, "canvas_section_dark_hover" }, + { IC_CANVAS_SECTION_ACTIVE, "canvas_section_active" }, { IC_CANVAS_SECTION_ACTIVE_HOVER, "canvas_section_active_hover" }, + { IC_CANVAS_SECTION_ACTIVE_DARK, "canvas_section_active_dark" }, { IC_CANVAS_SECTION_ACTIVE_DARK_HOVER, "canvas_section_active_dark_hover" } }) { + if (!IMTexture::load_from_svg_file(Slic3r::resources_dir() + "/images/" + name + ".svg", 72, 72, texture_id)) + return false; + icon_list.insert(std::make_pair((int) icon, texture_id)); + } + return true; } @@ -476,14 +484,17 @@ void GLGizmosManager::set_hover_id(int id) m_gizmos[m_current]->set_hover_id(id); } -void GLGizmosManager::update_assemble_view_data() +void GLGizmosManager::update_section_view() { - if (m_assemble_view_data) { - if (!wxGetApp().plater()->get_assmeble_canvas3D()->get_wxglcanvas()->IsShown()) - m_assemble_view_data->update(AssembleViewDataID(0)); - else - m_assemble_view_data->update(AssembleViewDataID((int)AssembleViewDataID::ModelObjectsInfo | (int)AssembleViewDataID::ModelObjectsClipper)); - } + if (m_current != FdmSupports && m_current != Seam && m_current != MmSegmentation && m_current != FuzzySkin && m_current != BrimEars) + return; + + CommonGizmosDataObjects::ObjectClipper* clipper = m_common_gizmos_data ? m_common_gizmos_data->object_clipper() : nullptr; + if (clipper == nullptr) + return; + + // Brim ears always cut horizontally. + clipper->set_position_by_ratio(m_parent.get_section_view_ratio(), m_current == BrimEars ? Vec3d::UnitZ() : m_parent.get_section_view_normal()); } void GLGizmosManager::update_data() @@ -496,6 +507,7 @@ void GLGizmosManager::update_data() m_common_gizmos_data->update(get_current() ? get_current()->get_requirements() : CommonGizmosDataID(0)); + update_section_view(); if (m_current != Undefined) m_gizmos[m_current]->data_changed(m_serializing); // Orca: hack: Fix issue that flatten gizmo faces not updated after reload from disk @@ -609,11 +621,11 @@ bool GLGizmosManager::is_allow_select_all() { return false; } -ClippingPlane GLGizmosManager::get_clipping_plane() const +std::optional GLGizmosManager::get_clipping_plane() const { - if (! m_common_gizmos_data - || ! m_common_gizmos_data->object_clipper() - || m_common_gizmos_data->object_clipper()->get_position() == 0.) + if (! m_common_gizmos_data || ! m_common_gizmos_data->object_clipper()) + return std::nullopt; + else if (m_common_gizmos_data->object_clipper()->get_position() == 0.) return ClippingPlane::ClipsNothing(); else { const ClippingPlane& clp = *m_common_gizmos_data->object_clipper()->get_clipping_plane(); @@ -621,18 +633,6 @@ ClippingPlane GLGizmosManager::get_clipping_plane() const } } -ClippingPlane GLGizmosManager::get_assemble_view_clipping_plane() const -{ - if (!m_assemble_view_data - || !m_assemble_view_data->model_objects_clipper() - || m_assemble_view_data->model_objects_clipper()->get_position() == 0.) - return ClippingPlane::ClipsNothing(); - else { - const ClippingPlane& clp = *m_assemble_view_data->model_objects_clipper()->get_clipping_plane(); - return ClippingPlane(-clp.get_normal(), clp.get_data()[3]); - } -} - bool GLGizmosManager::wants_reslice_supports_on_undo() const { return false; @@ -665,12 +665,6 @@ void GLGizmosManager::render_painter_gizmo() gizmo->render_painter_gizmo(); } -void GLGizmosManager::render_painter_assemble_view() const -{ - if (m_assemble_view_data && m_assemble_view_data->model_objects_clipper()) - m_assemble_view_data->model_objects_clipper()->render_cut(); -} - // The icon bar, drawn with GL. void GLGizmosManager::render_overlay() { diff --git a/src/slic3r/GUI/Gizmos/GLGizmosManager.hpp b/src/slic3r/GUI/Gizmos/GLGizmosManager.hpp index 63207f741b..855e26c102 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmosManager.hpp +++ b/src/slic3r/GUI/Gizmos/GLGizmosManager.hpp @@ -22,6 +22,7 @@ #include #include #include +#include //BBS: GUI refactor: to support top layout #define BBS_TOOLBAR_ON_TOP 1 @@ -181,7 +182,6 @@ private: bool gizmos_toolbar_on_mouse(const wxMouseEvent &mouse_event); public: - std::unique_ptr m_assemble_view_data; enum MENU_ICON_NAME { IC_TOOLBAR_RESET = 0, IC_TOOLBAR_RESET_HOVER, @@ -198,6 +198,14 @@ public: IC_CANVAS_ZOOM_HOVER, IC_CANVAS_ZOOM_DARK, IC_CANVAS_ZOOM_DARK_HOVER, + IC_CANVAS_SECTION, + IC_CANVAS_SECTION_HOVER, + IC_CANVAS_SECTION_DARK, + IC_CANVAS_SECTION_DARK_HOVER, + IC_CANVAS_SECTION_ACTIVE, + IC_CANVAS_SECTION_ACTIVE_HOVER, + IC_CANVAS_SECTION_ACTIVE_DARK, + IC_CANVAS_SECTION_ACTIVE_DARK_HOVER, }; explicit GLGizmosManager(GLCanvas3D& parent); @@ -266,7 +274,8 @@ public: /// Should be called when selection changed /// void update_data(); - void update_assemble_view_data(); + // Passes the canvas' section view to the object clipper of the painting and brim ears gizmos. + void update_section_view(); EType get_current_type() const { return m_current; } GLGizmoBase* get_current() const; @@ -297,8 +306,8 @@ public: bool is_paint_gizmo(); bool is_allow_select_all(); - ClippingPlane get_clipping_plane() const; - ClippingPlane get_assemble_view_clipping_plane() const; + // Empty when the open gizmo has no object clipper. + std::optional get_clipping_plane() const; bool wants_reslice_supports_on_undo() const; bool is_in_editing_mode(bool error_notification = false) const; @@ -307,7 +316,6 @@ public: void on_change_color_mode(bool is_dark); void render_current_gizmo() const; void render_painter_gizmo(); - void render_painter_assemble_view() const; void render_overlay(); void render_overlay_input_window(); diff --git a/src/slic3r/GUI/KBShortcutsDialog.cpp b/src/slic3r/GUI/KBShortcutsDialog.cpp index f7c0f23ae8..92051d116e 100644 --- a/src/slic3r/GUI/KBShortcutsDialog.cpp +++ b/src/slic3r/GUI/KBShortcutsDialog.cpp @@ -196,13 +196,13 @@ void KBShortcutsDialog::fill_pages() mouse(Section::Camera, _L("Middle mouse"), "middle_mouse_drag_action"), mouse(Section::Camera, _L("Right mouse"), "right_mouse_drag_action"), fixed(Section::Camera, { wheel }, L("Zoom View")), + fixed(Section::Display, { alt, wheel }, L("Move section plane")), }); page(_L("Painting"), _L("Available while a painting gizmo is open: supports, seam, fuzzy skin or color painting."), ShortcutContext::Painting, { fixed(Section::Gizmos, { esc }, L("Deselect All")), fixed(Section::Gizmos, { shift, left_button }, L("Move: press to snap by 1mm")), fixed(Section::PaintingTools, { ctrl, wheel }, L("Support/Color Painting: adjust pen radius")), - fixed(Section::PaintingTools, { alt, wheel }, L("Support/Color Painting: adjust section position")), }); page(_L("Objects list"), _L("Available while the object list has focus."), ShortcutContext::ObjectList, { @@ -216,6 +216,7 @@ void KBShortcutsDialog::fill_pages() page(_L("Preview"), _L("Available while the 3D view on the Preview tab has focus."), ShortcutContext::Preview, { fixed(Section::Sliders, { shift_ctrl, any_key }, L("Move slider 5x faster")), fixed(Section::Sliders, { shift_ctrl, wheel }, L("Scroll slider 5x faster")), + fixed(Section::Display, { alt, wheel }, L("Move section plane")), }); } diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index 142e4e2482..89616345eb 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -7891,6 +7891,7 @@ Plater::priv::priv(Plater *q, MainFrame *main_frame) view3D = new View3D(panel_3d, bed, &model, config, &background_process); //BBS: use partplater's gcode preview = new Preview(panel_3d, bed, &model, config, &background_process, partplate_list.get_current_slice_result(), [this]() { schedule_background_process(); }); + preview->get_canvas3d()->share_section_view(*view3D->get_canvas3d()); assemble_view = new AssembleView(panel_3d, bed, &model, config, &background_process); From 4ffba132108b9dac0482f93220c9e2a997f30d93 Mon Sep 17 00:00:00 2001 From: Kris Austin Date: Fri, 2 Oct 2026 12:40:51 -0500 Subject: [PATCH 4/6] ci: time out macOS notarization after 30 minutes (#16087) notarytool submit --wait has no timeout. On 2026-10-02 it hung for over 5 hours in a main build. Since #16044 a new push no longer cancels a running main build, so nothing stopped it and six waiting main runs were replaced without starting. Over the last 30 days the step succeeded 206 times, with a median of 4.3 minutes and a maximum of 14.8. --- .github/workflows/build_orca.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/build_orca.yml b/.github/workflows/build_orca.yml index 8c7674f4d7..98f0d66032 100644 --- a/.github/workflows/build_orca.yml +++ b/.github/workflows/build_orca.yml @@ -259,6 +259,7 @@ jobs: # Thanks to RaySajuuk, it's working now - name: Sign app and notary if: github.repository == 'OrcaSlicer/OrcaSlicer' && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/belt-printer' || startsWith(github.ref, 'refs/heads/release/')) && runner.os == 'macOS' && inputs.macos-combine-only + timeout-minutes: 30 working-directory: ${{ github.workspace }} env: BUILD_CERTIFICATE_BASE64: ${{ secrets.BUILD_CERTIFICATE_BASE64 }} From 70bc02467bae5e8e68b3ef7e8e769430fd67d34c Mon Sep 17 00:00:00 2001 From: Ian Bassi Date: Fri, 2 Oct 2026 14:45:58 -0300 Subject: [PATCH 5/6] Faster Preview View (#15884) Co-authored-by: Kris Austin --- docs/HLSD/scene-benchmark.md | 93 ++++ docs/HLSD/section-view.md | 6 +- localization/i18n/list.txt | 1 + resources/shaders/110/gouraud.fs | 6 +- resources/shaders/110/gouraud.vs | 8 +- resources/shaders/110/phong.fs | 10 +- resources/shaders/110/printbed_shadow.fs | 4 + resources/shaders/140/gouraud.fs | 6 +- resources/shaders/140/gouraud.vs | 8 +- resources/shaders/140/phong.fs | 10 +- resources/shaders/140/printbed_shadow.fs | 4 + src/libslic3r/AppConfig.cpp | 8 +- src/libslic3r/AppConfig.hpp | 3 + src/libvgcode/include/Types.hpp | 2 + src/libvgcode/include/Viewer.hpp | 4 + src/libvgcode/src/Shaders.hpp | 110 +++- src/libvgcode/src/ShadersES.hpp | 112 ++++- src/libvgcode/src/Viewer.cpp | 5 + src/libvgcode/src/ViewerImpl.cpp | 290 ++++++----- src/libvgcode/src/ViewerImpl.hpp | 64 ++- src/slic3r/CMakeLists.txt | 4 + src/slic3r/GUI/3DScene.cpp | 11 +- src/slic3r/GUI/FrameProfiler.cpp | 153 ++++++ src/slic3r/GUI/FrameProfiler.hpp | 66 +++ src/slic3r/GUI/GCodeViewer.cpp | 31 +- src/slic3r/GUI/GCodeViewer.hpp | 7 + src/slic3r/GUI/GLCanvas3D.cpp | 298 ++++++++--- src/slic3r/GUI/GLCanvas3D.hpp | 17 + src/slic3r/GUI/GUI_App.cpp | 4 +- src/slic3r/GUI/MainFrame.cpp | 5 + src/slic3r/GUI/NativeCommands.cpp | 5 + src/slic3r/GUI/Preferences.cpp | 32 +- src/slic3r/GUI/SceneBenchmark.cpp | 555 +++++++++++++++++++++ src/slic3r/GUI/SceneBenchmark.hpp | 27 + tests/slic3rutils/CMakeLists.txt | 1 + tests/slic3rutils/test_action_source.cpp | 3 +- tests/slic3rutils/test_scene_benchmark.cpp | 57 +++ 37 files changed, 1740 insertions(+), 290 deletions(-) create mode 100644 docs/HLSD/scene-benchmark.md create mode 100644 src/slic3r/GUI/FrameProfiler.cpp create mode 100644 src/slic3r/GUI/FrameProfiler.hpp create mode 100644 src/slic3r/GUI/SceneBenchmark.cpp create mode 100644 src/slic3r/GUI/SceneBenchmark.hpp create mode 100644 tests/slic3rutils/test_scene_benchmark.cpp diff --git a/docs/HLSD/scene-benchmark.md b/docs/HLSD/scene-benchmark.md new file mode 100644 index 0000000000..21e4ec574c --- /dev/null +++ b/docs/HLSD/scene-benchmark.md @@ -0,0 +1,93 @@ +# 3D Scene Benchmark: High Level Design + +## Why it exists + +Rendering changes, such as the realistic view, shadows or SSAO, need a number to compare +before and after, and user reports of a slow viewport need a way to say how slow. The FPS +overlay and the render timings overlay show live values while someone drags the camera, +which varies from run to run with the model, the path of the mouse and the view. + +The benchmark renders a fixed model along a fixed camera path in both 3D views, so two +runs on the same machine differ only by the code or the settings, and prints a report that +can be pasted into an issue. + +## What it does + +`run_scene_benchmark()` in `src/slic3r/GUI/SceneBenchmark.cpp` is reached from Help > +Benchmark 3D Scene, the command palette and Preferences > Graphics. After a confirmation +it starts a new project, which asks to save the current one if needed, loads the +OrcaSliced Combo handy model and arranges it. A small dialog in a corner of the 3D view +then shows the progress; every other window is disabled until the run ends, so a click +cannot change the scene being measured. Cancel or Esc stops the run. + +The run goes through these stages, driven by a timer while it waits and by idle events +while it renders: + +1. Loading: waits until the UI job worker is idle, so the arrange job has moved the + objects. The orbit target is the center of the objects on the current plate, and the + base zoom fits their bounding box in the viewport. +2. Prepare: renders the scene in the Prepare view. +3. Slicing: slices the plate and switches to Preview, then waits for the G-code preview + to load. If slicing fails, the report holds Prepare alone. +4. Preview: renders the scene in the Preview view, with the slicing progress notification + hidden. +5. Layers: renders the Preview view again while the layer slider moves, which is what + makes dragging it feel slow on large prints. + +The dialog then shows the report, with a button to copy it. A scene cut short, because its +view was hidden, is left out of the report. + +## Rendering a scene + +Each scene renders 30 warm-up frames, then the camera path twice, 360 frames each time. + +- The first pass times the frames. A frame's time is the interval between the starts of + consecutive benchmark frames, so it includes the event loop between them. +- The second pass averages the render timings. The frame profiler flushes the GL command + queue after each section, which slows a frame down, so it only runs in this pass. + `FrameProfiler::start_averaging()` flags every frame begun afterwards, and + `finish_averaging()` waits for the flagged frames still on the GPU and returns the mean + CPU and GPU time of each section per profiled frame. +- A section's GPU time is taken between a timestamp before its commands and one after + them. The first is only sent along with those commands, so when the GPU finishes a + section before the CPU has issued the next one, the wait counts in neither. + +The dialog renders one frame per idle event by calling `GLCanvas3D::render()`, which +redraws the whole scene. While `GLCanvas3D::set_benchmarking()` is on, the canvas does not +render from its own idle handler, so no other frame is drawn in between, and it skips the +picking pass and the FPS and render timings overlays, which depend on the mouse and on +preferences. The FPS cap does not apply, since it only paces idle redraws. + +VSync is turned off for the scene through `wxGLCanvas::SetSwapInterval(0)`, so the frame +rate is what the GPU and CPU can reach rather than the display's refresh rate, and the +previous interval is restored afterwards. When the platform cannot report the current +interval (EGL), it is left as it is and the report says so. + +The camera path makes two turns around the target while the view rises three times from +25 degrees below the plate to 85 degrees above it and the zoom goes twice between 0.6 and +1.4 times the base zoom. The camera stays at the default distance, so the perspective is +the same in every run. The camera the scene started with is restored at its end. + +The Layers scene holds the camera at the start of that path and moves the top of the layer +slider instead, from the last layer down to the first and back up in each pass. It goes +through `IMSlider::SetHigherValue()`, as a drag does, so every frame applies a new layer +range to the toolpaths and the objects before drawing them, including a new shadow map when +the shadows are static. Its warm-up frames lead into the start of the path, so the slider +moves in every frame. The slider position it started from is restored at its end. + +## The report + +The report is plain English text, so it reads the same in every language: + +- The version and build commit, the GPU and OpenGL version, the viewport size and camera + type, and the graphics settings that change the cost of a frame: MSAA samples as read + from the framebuffer, FXAA, the scene cache, VSync and the realistic view options. +- The printer and process presets the model was sliced with, marked when they have + unsaved changes, and the toolpath vertices and layers they produced, since the Preview + scenes cost more with more toolpaths. +- For each scene, the average FPS and the average, median, 95th percentile, 99th + percentile and maximum frame time. Percentiles are nearest-rank, so each is a measured + frame (`frame_time_stats()`). +- For each scene, the render timings table: the CPU and GPU milliseconds of each section + of a frame, and their total. Without timer queries (OpenGL 3.3 or `ARB_timer_query`) the + table says that the driver does not support them. diff --git a/docs/HLSD/section-view.md b/docs/HLSD/section-view.md index d37a09db00..1cc1fd726e 100644 --- a/docs/HLSD/section-view.md +++ b/docs/HLSD/section-view.md @@ -65,8 +65,10 @@ closes the open gizmo, so neither tab ever shows a stale cut. as the plane faces; otherwise it is discarded. The cut face keeps the depth of the fragment it replaces, which is safe: along that ray everything else still shown lies behind the plane. The shader writes no `gl_FragDepth`, so early depth testing survives. Option markers are cut away - whole, by their centres. The shadow casters share the segment shader, so what is cut away casts - no shadow either. Preview shells are drawn by another shader and are not clipped. + whole, by their centres. The shadow casters draw with a program of their own, which takes the + plane and discards the fragments on the clipped side, so what is cut away casts no shadow either. + Their cut faces are not drawn, since the part left behind casts the shadow of its own section. + Preview shells are drawn by another shader and are not clipped. - **Picking.** `get_raycaster_clipping_plane()` returns the same plane, so hover, selection and the perspective pan anchor ignore what the user cannot see. diff --git a/localization/i18n/list.txt b/localization/i18n/list.txt index 296829814a..d9f2049565 100644 --- a/localization/i18n/list.txt +++ b/localization/i18n/list.txt @@ -180,6 +180,7 @@ src/slic3r/GUI/PrivacyUpdateDialog.cpp src/slic3r/GUI/PublishDialog.cpp src/slic3r/GUI/PublishSettingsDialog.cpp src/slic3r/GUI/SavePresetDialog.cpp +src/slic3r/GUI/SceneBenchmark.cpp src/slic3r/GUI/Search.cpp src/slic3r/GUI/SettingsIndex.cpp src/slic3r/GUI/SpeedDialDialog.cpp diff --git a/resources/shaders/110/gouraud.fs b/resources/shaders/110/gouraud.fs index 59c4e4d410..c5efeef277 100644 --- a/resources/shaders/110/gouraud.fs +++ b/resources/shaders/110/gouraud.fs @@ -63,6 +63,10 @@ uniform float shadow_map_texel; // LIGHT_TOP_DIR in eye space (matches the diffuse light used for shading in gouraud.vs). const vec3 SHADOW_LIGHT_DIR = vec3(-0.4574957, 0.4574957, 0.7624929); +// ORCA: realistic view - static shadows also light the scene from their fixed light. +uniform bool use_static_light; +uniform vec3 static_light_dir; +vec3 top_light_dir() { return use_static_light ? static_light_dir : SHADOW_LIGHT_DIR; } varying vec3 clipping_planes_dots; varying float color_clip_plane_dot; @@ -155,7 +159,7 @@ float shadow_shade() // Slope-scaled depth bias: larger where the surface grazes / faces away from the light. This // suppresses self-shadow acne without discarding real shadows cast by other objects onto // back-facing surfaces (e.g. the shaded back/tip of a cone sitting inside a larger shadow). - float NdotL = dot(normalize(eye_normal), SHADOW_LIGHT_DIR); + float NdotL = dot(normalize(eye_normal), top_light_dir()); float bias = mix(0.0004, 0.004, clamp(1.0 - NdotL, 0.0, 1.0)); // 5x5 PCF: softens shadow edges into a smooth penumbra and blurs residual facet acne. float sum = 0.0; diff --git a/resources/shaders/110/gouraud.vs b/resources/shaders/110/gouraud.vs index 184529be82..37be529b09 100644 --- a/resources/shaders/110/gouraud.vs +++ b/resources/shaders/110/gouraud.vs @@ -39,6 +39,10 @@ uniform vec2 z_range; uniform vec4 clipping_plane; // Color clip plane - general orientation. Used by the cut gizmo. uniform vec4 color_clip_plane; +// ORCA: realistic view - static shadows also light the scene from their fixed light. +uniform bool use_static_light; +uniform vec3 static_light_dir; +vec3 top_light_dir() { return use_static_light ? static_light_dir : LIGHT_TOP_DIR; } attribute vec3 v_position; attribute vec3 v_normal; @@ -60,11 +64,11 @@ void main() // Compute the cos of the angle between the normal and lights direction. The light is directional so the direction is constant for every vertex. // Since these two are normalized the cosine is the dot product. We also need to clamp the result to the [0,1] range. - float NdotL = max(dot(eye_normal, LIGHT_TOP_DIR), 0.0); + float NdotL = max(dot(eye_normal, top_light_dir()), 0.0); intensity.x = INTENSITY_AMBIENT + NdotL * LIGHT_TOP_DIFFUSE; vec4 position = view_model_matrix * vec4(v_position, 1.0); - intensity.y = LIGHT_TOP_SPECULAR * pow(max(dot(-normalize(position.xyz), reflect(-LIGHT_TOP_DIR, eye_normal)), 0.0), LIGHT_TOP_SHININESS); + intensity.y = LIGHT_TOP_SPECULAR * pow(max(dot(-normalize(position.xyz), reflect(-top_light_dir(), eye_normal)), 0.0), LIGHT_TOP_SHININESS); // Perform the same lighting calculation for the 2nd light source (no specular applied). NdotL = max(dot(eye_normal, LIGHT_FRONT_DIR), 0.0); diff --git a/resources/shaders/110/phong.fs b/resources/shaders/110/phong.fs index 5fedf7edff..7b9a522eaa 100644 --- a/resources/shaders/110/phong.fs +++ b/resources/shaders/110/phong.fs @@ -70,6 +70,10 @@ uniform sampler2D shadow_map; uniform mat4 shadow_light_vp; uniform float shadow_intensity; uniform float shadow_map_texel; +// ORCA: realistic view - static shadows also light the scene from their fixed light. +uniform bool use_static_light; +uniform vec3 static_light_dir; +vec3 top_light_dir() { return use_static_light ? static_light_dir : LIGHT_TOP_DIR; } varying vec3 clipping_planes_dots; varying float color_clip_plane_dot; @@ -193,7 +197,7 @@ float shadow_shade() // Slope-scaled depth bias: larger where the surface grazes / faces away from the light. This // suppresses self-shadow acne without discarding real shadows cast by other objects onto // back-facing surfaces (e.g. the shaded back/tip of a cone sitting inside a larger shadow). - float NdotL = dot(normalize(eye_normal), LIGHT_TOP_DIR); + float NdotL = dot(normalize(eye_normal), top_light_dir()); float bias = mix(0.0004, 0.004, clamp(1.0 - NdotL, 0.0, 1.0)); // 5x5 PCF: softens shadow edges into a smooth penumbra and blurs residual facet acne. float sum = 0.0; @@ -248,9 +252,9 @@ void main() vec3 normal = normalize(eye_normal); vec3 view_dir = normalize(-eye_position); - float NdotL_top = max(dot(normal, LIGHT_TOP_DIR), 0.0); + float NdotL_top = max(dot(normal, top_light_dir()), 0.0); float diffuse = INTENSITY_AMBIENT + NdotL_top * LIGHT_TOP_DIFFUSE; - vec3 half_top = normalize(LIGHT_TOP_DIR + view_dir); + vec3 half_top = normalize(top_light_dir() + view_dir); float specular = LIGHT_TOP_SPECULAR * pow(max(dot(normal, half_top), 0.0), LIGHT_TOP_SHININESS); float NdotL_front = max(dot(normal, LIGHT_FRONT_DIR), 0.0); diff --git a/resources/shaders/110/printbed_shadow.fs b/resources/shaders/110/printbed_shadow.fs index a4fd6801dd..394cfac5ee 100644 --- a/resources/shaders/110/printbed_shadow.fs +++ b/resources/shaders/110/printbed_shadow.fs @@ -6,6 +6,8 @@ uniform sampler2D shadow_map; uniform mat4 shadow_light_vp; uniform float shadow_intensity; uniform float shadow_map_texel; +// Plate area a shadow can reach, min xy then max xy; the rest is skipped before any lookup. +uniform vec4 shadow_bounds; varying vec4 world_pos; @@ -33,6 +35,8 @@ float shadow_occlusion() void main() { + if (any(lessThan(world_pos.xy, shadow_bounds.xy)) || any(greaterThan(world_pos.xy, shadow_bounds.zw))) + discard; float occ = shadow_occlusion(); if (occ <= 0.0) discard; diff --git a/resources/shaders/140/gouraud.fs b/resources/shaders/140/gouraud.fs index e099984b46..7b315f90c0 100644 --- a/resources/shaders/140/gouraud.fs +++ b/resources/shaders/140/gouraud.fs @@ -72,6 +72,10 @@ uniform float shadow_map_texel; // LIGHT_TOP_DIR in eye space (matches the diffuse light used for shading in gouraud.vs). const vec3 SHADOW_LIGHT_DIR = vec3(-0.4574957, 0.4574957, 0.7624929); +// ORCA: realistic view - static shadows also light the scene from their fixed light. +uniform bool use_static_light; +uniform vec3 static_light_dir; +vec3 top_light_dir() { return use_static_light ? static_light_dir : SHADOW_LIGHT_DIR; } in vec3 clipping_planes_dots; in float color_clip_plane_dot; @@ -206,7 +210,7 @@ float shadow_shade() // Slope-scaled depth bias: larger where the surface grazes / faces away from the light. This // suppresses self-shadow acne without discarding real shadows cast by other objects onto // back-facing surfaces (e.g. the shaded back/tip of a cone sitting inside a larger shadow). - float NdotL = dot(normalize(eye_normal), SHADOW_LIGHT_DIR); + float NdotL = dot(normalize(eye_normal), top_light_dir()); float bias = mix(0.0004, 0.004, clamp(1.0 - NdotL, 0.0, 1.0)); // 5x5 PCF: softens shadow edges into a smooth penumbra and blurs residual facet acne. float sum = 0.0; diff --git a/resources/shaders/140/gouraud.vs b/resources/shaders/140/gouraud.vs index a31e751964..11fc4b70c8 100644 --- a/resources/shaders/140/gouraud.vs +++ b/resources/shaders/140/gouraud.vs @@ -39,6 +39,10 @@ uniform vec2 z_range; uniform vec4 clipping_plane; // Color clip plane - general orientation. Used by the cut gizmo. uniform vec4 color_clip_plane; +// ORCA: realistic view - static shadows also light the scene from their fixed light. +uniform bool use_static_light; +uniform vec3 static_light_dir; +vec3 top_light_dir() { return use_static_light ? static_light_dir : LIGHT_TOP_DIR; } in vec3 v_position; in vec3 v_normal; @@ -60,11 +64,11 @@ void main() // Compute the cos of the angle between the normal and lights direction. The light is directional so the direction is constant for every vertex. // Since these two are normalized the cosine is the dot product. We also need to clamp the result to the [0,1] range. - float NdotL = max(dot(eye_normal, LIGHT_TOP_DIR), 0.0); + float NdotL = max(dot(eye_normal, top_light_dir()), 0.0); intensity.x = INTENSITY_AMBIENT + NdotL * LIGHT_TOP_DIFFUSE; vec4 position = view_model_matrix * vec4(v_position, 1.0); - intensity.y = LIGHT_TOP_SPECULAR * pow(max(dot(-normalize(position.xyz), reflect(-LIGHT_TOP_DIR, eye_normal)), 0.0), LIGHT_TOP_SHININESS); + intensity.y = LIGHT_TOP_SPECULAR * pow(max(dot(-normalize(position.xyz), reflect(-top_light_dir(), eye_normal)), 0.0), LIGHT_TOP_SHININESS); // Perform the same lighting calculation for the 2nd light source (no specular applied). NdotL = max(dot(eye_normal, LIGHT_FRONT_DIR), 0.0); diff --git a/resources/shaders/140/phong.fs b/resources/shaders/140/phong.fs index f847763658..03ffbb3e5c 100644 --- a/resources/shaders/140/phong.fs +++ b/resources/shaders/140/phong.fs @@ -80,6 +80,10 @@ uniform sampler2D shadow_map; uniform mat4 shadow_light_vp; uniform float shadow_intensity; uniform float shadow_map_texel; +// ORCA: realistic view - static shadows also light the scene from their fixed light. +uniform bool use_static_light; +uniform vec3 static_light_dir; +vec3 top_light_dir() { return use_static_light ? static_light_dir : LIGHT_TOP_DIR; } in vec3 clipping_planes_dots; in float color_clip_plane_dot; @@ -250,7 +254,7 @@ float shadow_shade() // Slope-scaled depth bias: larger where the surface grazes / faces away from the light. This // suppresses self-shadow acne without discarding real shadows cast by other objects onto // back-facing surfaces (e.g. the shaded back/tip of a cone sitting inside a larger shadow). - float NdotL = dot(normalize(eye_normal), LIGHT_TOP_DIR); + float NdotL = dot(normalize(eye_normal), top_light_dir()); float bias = mix(0.0004, 0.004, clamp(1.0 - NdotL, 0.0, 1.0)); // 5x5 PCF: softens shadow edges into a smooth penumbra and blurs residual facet acne. float sum = 0.0; @@ -305,9 +309,9 @@ void main() vec3 normal = normalize(eye_normal); vec3 view_dir = normalize(-eye_position); - float NdotL_top = max(dot(normal, LIGHT_TOP_DIR), 0.0); + float NdotL_top = max(dot(normal, top_light_dir()), 0.0); float diffuse = INTENSITY_AMBIENT + NdotL_top * LIGHT_TOP_DIFFUSE; - vec3 half_top = normalize(LIGHT_TOP_DIR + view_dir); + vec3 half_top = normalize(top_light_dir() + view_dir); float specular = LIGHT_TOP_SPECULAR * pow(max(dot(normal, half_top), 0.0), LIGHT_TOP_SHININESS); float NdotL_front = max(dot(normal, LIGHT_FRONT_DIR), 0.0); diff --git a/resources/shaders/140/printbed_shadow.fs b/resources/shaders/140/printbed_shadow.fs index 650d88950e..8804806d64 100644 --- a/resources/shaders/140/printbed_shadow.fs +++ b/resources/shaders/140/printbed_shadow.fs @@ -6,6 +6,8 @@ uniform sampler2D shadow_map; uniform mat4 shadow_light_vp; uniform float shadow_intensity; uniform float shadow_map_texel; +// Plate area a shadow can reach, min xy then max xy; the rest is skipped before any lookup. +uniform vec4 shadow_bounds; in vec4 world_pos; @@ -35,6 +37,8 @@ float shadow_occlusion() void main() { + if (any(lessThan(world_pos.xy, shadow_bounds.xy)) || any(greaterThan(world_pos.xy, shadow_bounds.zw))) + discard; float occ = shadow_occlusion(); if (occ <= 0.0) discard; diff --git a/src/libslic3r/AppConfig.cpp b/src/libslic3r/AppConfig.cpp index d639604f51..061665fbe6 100644 --- a/src/libslic3r/AppConfig.cpp +++ b/src/libslic3r/AppConfig.cpp @@ -304,6 +304,9 @@ void AppConfig::set_defaults() if (get(SETTING_OPENGL_SHOW_FPS_OVERLAY).empty()) set_bool(SETTING_OPENGL_SHOW_FPS_OVERLAY, false); + if (get(SETTING_OPENGL_SHOW_RENDER_TIMINGS).empty()) + set_bool(SETTING_OPENGL_SHOW_RENDER_TIMINGS, false); + if (get(SETTING_OPENGL_REALISTIC_MODE).empty()) set_bool(SETTING_OPENGL_REALISTIC_MODE, false); @@ -316,8 +319,9 @@ void AppConfig::set_defaults() if (get(SETTING_OPENGL_SHADING_MODEL).empty()) set(SETTING_OPENGL_SHADING_MODEL, "gouraud"); - if (get(SETTING_OPENGL_PHONG_BASIC_PLATE_SHADOWS).empty()) - set_bool(SETTING_OPENGL_PHONG_BASIC_PLATE_SHADOWS, false); + // Replaces the on/off setting, whose shadows turned with the camera. + if (get(SETTING_OPENGL_REALISTIC_SHADOWS).empty()) + set(SETTING_OPENGL_REALISTIC_SHADOWS, get_bool(SETTING_OPENGL_PHONG_BASIC_PLATE_SHADOWS) ? "orbit" : "off"); if (get(SETTING_OPENGL_PHONG_SMOOTH_NORMALS).empty()) set_bool(SETTING_OPENGL_PHONG_SMOOTH_NORMALS, false); diff --git a/src/libslic3r/AppConfig.hpp b/src/libslic3r/AppConfig.hpp index 1bf6e730d0..1486cb6f9f 100644 --- a/src/libslic3r/AppConfig.hpp +++ b/src/libslic3r/AppConfig.hpp @@ -37,10 +37,13 @@ using namespace nlohmann; #define SETTING_OPENGL_SCENE_CACHE "opengl_scene_cache" #define SETTING_OPENGL_SKIP_IDENTICAL_FRAMES "opengl_skip_identical_frames" #define SETTING_OPENGL_SHOW_FPS_OVERLAY "opengl_show_fps_overlay" +#define SETTING_OPENGL_SHOW_RENDER_TIMINGS "opengl_show_render_timings" #define SETTING_OPENGL_REALISTIC_MODE "opengl_realistic_mode" #define SETTING_OPENGL_REALISTIC_PHONG "opengl_realistic_phong" #define SETTING_OPENGL_SHADING_MODEL "opengl_shading_model" #define SETTING_OPENGL_PHONG_BASIC_PLATE_SHADOWS "opengl_phong_basic_plate_shadows" +// off, static (light fixed in the world) or orbit (light turning with the camera) +#define SETTING_OPENGL_REALISTIC_SHADOWS "opengl_realistic_shadows" #define SETTING_OPENGL_PHONG_SSAO "opengl_phong_ssao" #define SETTING_OPENGL_PHONG_SMOOTH_NORMALS "opengl_phong_smooth_normals" #define SETTING_OPENGL_REALISTIC_PREVIEW "opengl_realistic_preview" diff --git a/src/libvgcode/include/Types.hpp b/src/libvgcode/include/Types.hpp index 2187238ef0..099f622dc3 100644 --- a/src/libvgcode/include/Types.hpp +++ b/src/libvgcode/include/Types.hpp @@ -37,6 +37,8 @@ static constexpr float MAX_WIPES_RADIUS_MM = 1.0f; // Used for positions, displacements and so on. // using Vec3 = std::array; +// ORCA: the per vertex data uploaded as GL_RGBA32F, see extract_pos_and_or_hwa(). +using Vec4 = std::array; // // 4x4 square matrix with elements in column-major order: diff --git a/src/libvgcode/include/Viewer.hpp b/src/libvgcode/include/Viewer.hpp index 7eadc16de0..458bc4b69f 100644 --- a/src/libvgcode/include/Viewer.hpp +++ b/src/libvgcode/include/Viewer.hpp @@ -80,6 +80,10 @@ public: // ORCA: section view. Cuts away where dot(plane, (x, y, z, 1)) < 0; { 0, 0, 0, 1 } keeps all. // void set_clipping_plane(const std::array& plane); + // + // ORCA: direction to the top light in eye space, shading the toolpaths and biasing their shadow lookup. + // + void set_light_top_dir(const Vec3& direction); // // ************************************************************************ diff --git a/src/libvgcode/src/Shaders.hpp b/src/libvgcode/src/Shaders.hpp index b4980885b6..eaf143f583 100644 --- a/src/libvgcode/src/Shaders.hpp +++ b/src/libvgcode/src/Shaders.hpp @@ -14,7 +14,7 @@ static const char* Segments_Vertex_Shader = "#version 150\n" "#define POINTY_CAPS\n" "#define FIX_TWISTING\n" -"const vec3 light_top_dir = vec3(-0.4574957, 0.4574957, 0.7624929);\n" +"uniform vec3 light_top_dir;\n" "const float light_top_diffuse = 0.6 * 0.8;\n" // ORCA: the specular was 0.6 * 0.125, too faint to give the filament any sheen. "const float light_top_specular = 0.6 * 0.25;\n" @@ -31,13 +31,9 @@ static const char* Segments_Vertex_Shader = "uniform samplerBuffer height_width_angle_tex;\n" "uniform samplerBuffer color_tex;\n" "uniform usamplerBuffer segment_index_tex;\n" -// ORCA: 0 during the shadow caster pass - the bias below shifts eye_position but not -// world_position, so the caster would write a depth the receiver never looks up. -"uniform float bias_scale;\n" -// draw the instances last to first, top layers before the ones they hide, so that early depth -// rejection discards most of the hidden fragments; set when the camera looks down on the print -"uniform int reverse_order;\n" -"uniform int instance_count;\n" +// ORCA: draw last segment first, so looking down the top layers hide the rest from the fragment shader. +"uniform bool reverse_order;\n" +"uniform int instances_count;\n" // ORCA: section view "uniform vec4 clipping_plane;\n" "in int vertex_id;\n" @@ -53,13 +49,22 @@ static const char* Segments_Vertex_Shader = // ORCA: section view - the segment and the light on its cut face "flat out int segment_id;\n" "out vec3 cut_color_direct;\n" -"vec3 decode_color(float color) {\n" +// ORCA: the layers greyed or dimmed around the one the sliders show, see ViewerImpl::set_layer_colors(). +"uniform ivec2 lit_layers;\n" +"uniform int grey_below_layer;\n" +"uniform float dim_brightness;\n" +"uniform int kept_vertex;\n" +"vec3 decode_color(float color, int id, int layer) {\n" " int c = int(round(color));\n" -" int r = (c >> 16) & 0xFF;\n" -" int g = (c >> 8) & 0xFF;\n" -" int b = (c >> 0) & 0xFF;\n" +" vec3 rgb = vec3((c >> 16) & 0xFF, (c >> 8) & 0xFF, c & 0xFF);\n" +" if (id != kept_vertex && layer != lit_layers.x && layer != lit_layers.y) {\n" +" if (dim_brightness >= 0.0)\n" +" rgb = floor(rgb * dim_brightness);\n" +" else if (layer < grey_below_layer)\n" +" rgb = vec3(64.0);\n" +" }\n" " float f = 1.0 / 255.0f;\n" -" return f * vec3(r, g, b);\n" +" return f * rgb;\n" "}\n" "float direct_lighting(vec3 eye_position, vec3 eye_normal) {\n" " float top_diffuse = light_top_diffuse * max(dot(eye_normal, light_top_dir), 0.0);\n" @@ -68,7 +73,7 @@ static const char* Segments_Vertex_Shader = " return top_diffuse + front_diffuse + top_specular;\n" "}\n" "void main() {\n" -" int instance = (reverse_order != 0) ? instance_count - 1 - gl_InstanceID : gl_InstanceID;\n" +" int instance = reverse_order ? instances_count - 1 - gl_InstanceID : gl_InstanceID;\n" " int id_a = int(texelFetch(segment_index_tex, instance).r);\n" " int id_b = id_a + 1;\n" " vec3 pos_a = texelFetch(position_tex, id_a).xyz;\n" @@ -157,9 +162,9 @@ static const char* Segments_Vertex_Shader = " }\n" " vec3 eye_position = (view_matrix * vec4(pos, 1.0)).xyz;\n" " // ORCA: Apply bias to z-position to avoid z-fighting\n" -" eye_position.z += bias * bias_scale;\n" +" eye_position.z += bias;\n" " vec3 eye_normal = (view_matrix * vec4(normalize(pos - endpoint_pos), 0.0)).xyz;\n" -" vec3 color_base = decode_color(texelFetch(color_tex, id).r);\n" +" vec3 color_base = decode_color(texelFetch(color_tex, id).r, id, int(texelFetch(position_tex, id).w));\n" " color = color_base * (ambient + emission);\n" " color_direct = color_base * direct_lighting(eye_position, eye_normal);\n" " world_position = pos;\n" @@ -170,12 +175,60 @@ static const char* Segments_Vertex_Shader = " gl_Position = projection_matrix * vec4(eye_position, 1.0);\n" "}\n"; +// ORCA: realistic view - shadow caster, one light facing ribbon per segment, not instanced as tiny instances are slow. +static const char* Segments_Shadow_Caster_Vertex_Shader = +"#version 150\n" +"const vec3 UP = vec3(0, 0, 1);\n" +"const int CORNERS[6] = int[](0, 1, 2, 0, 2, 3);\n" +"uniform mat4 view_matrix;\n" +"uniform mat4 projection_matrix;\n" +"uniform samplerBuffer position_tex;\n" +"uniform samplerBuffer height_width_angle_tex;\n" +"uniform usamplerBuffer segment_index_tex;\n" +"uniform bool reverse_order;\n" +"uniform int instances_count;\n" +"out vec3 world_position;\n" +"void main() {\n" +" int segment = gl_VertexID / 6;\n" +" int corner = CORNERS[gl_VertexID - 6 * segment];\n" +" int instance = reverse_order ? instances_count - 1 - segment : segment;\n" +" int id_a = int(texelFetch(segment_index_tex, instance).r);\n" +" int id = corner < 2 ? id_a : id_a + 1;\n" +" vec3 pos_a = texelFetch(position_tex, id_a).xyz;\n" +" vec3 pos_b = texelFetch(position_tex, id_a + 1).xyz;\n" +" vec3 line = pos_b - pos_a;\n" +" float line_len = length(line);\n" +" vec3 line_dir = line_len < 1e-4 ? vec3(1.0, 0.0, 0.0) : line / line_len;\n" +" vec3 line_right_dir = abs(dot(line_dir, UP)) > 0.9 ? normalize(cross(vec3(1, 0, 0), line_dir)) : normalize(cross(line_dir, UP));\n" +" vec3 line_up_dir = normalize(cross(line_right_dir, line_dir));\n" +" vec3 to_light = vec3(view_matrix[0][2], view_matrix[1][2], view_matrix[2][2]);\n" +" vec3 side_dir = cross(to_light, line_dir);\n" +" side_dir = dot(side_dir, side_dir) < 1e-8 ? line_right_dir : normalize(side_dir);\n" +" vec2 height_width = texelFetch(height_width_angle_tex, id).xy;\n" +" float half_extent = 0.5 * (height_width.y * abs(dot(line_right_dir, side_dir)) + height_width.x * abs(dot(line_up_dir, side_dir)));\n" +" float along = (corner < 2 ? -0.5 : 0.5) * height_width.y;\n" +" float side = (corner == 0 || corner == 3) ? -1.0 : 1.0;\n" +" vec3 pos = (corner < 2 ? pos_a : pos_b) + along * line_dir + side * half_extent * side_dir;\n" +" world_position = pos;\n" +" gl_Position = projection_matrix * (view_matrix * vec4(pos, 1.0));\n" +"}\n"; + +// ORCA: realistic view - the shadow caster writes depth only. What a section cuts away casts no shadow. +static const char* Segments_Shadow_Caster_Fragment_Shader = +"#version 150\n" +"uniform vec4 clipping_plane;\n" +"in vec3 world_position;\n" +"void main() {\n" +" if (dot(vec4(world_position, 1.0), clipping_plane) < 0.0)\n" +" discard;\n" +"}\n"; + static const char* Segments_Fragment_Shader = "#version 150\n" "// ORCA: realistic view - object-on-object and self shadows, read from the same depth map the\n" "// rest of the 3D scene samples. shadow_intensity == 0, the default, short-circuits the lookup,\n" "// so the toolpaths shade exactly as before whenever realistic view is off.\n" -"const vec3 SHADOW_LIGHT_DIR = vec3(-0.4574957, 0.4574957, 0.7624929);\n" +"uniform vec3 light_top_dir;\n" "uniform sampler2D shadow_map;\n" "uniform mat4 shadow_light_vp;\n" "uniform float shadow_intensity;\n" @@ -212,7 +265,7 @@ static const char* Segments_Fragment_Shader = " return 1.0;\n" " // Slope-scaled bias, as in gouraud.fs. An extrusion is only a handful of shadow-map texels\n" " // wide, so grazing faces need the larger bias to keep self-shadow acne off the top surfaces.\n" -" float NdotL = dot(normalize(normal), SHADOW_LIGHT_DIR);\n" +" float NdotL = dot(normalize(normal), light_top_dir);\n" " float bias = mix(0.0004, 0.004, clamp(1.0 - NdotL, 0.0, 1.0));\n" " float sum = 0.0;\n" " for (int x = -2; x <= 2; ++x) {\n" @@ -289,13 +342,22 @@ static const char* Options_Vertex_Shader = "in vec3 in_normal;\n" "out vec3 color;\n" "out float clipping_plane_dot;\n" -"vec3 decode_color(float color) {\n" +// ORCA: as in Segments_Vertex_Shader. +"uniform ivec2 lit_layers;\n" +"uniform int grey_below_layer;\n" +"uniform float dim_brightness;\n" +"uniform int kept_vertex;\n" +"vec3 decode_color(float color, int id, int layer) {\n" " int c = int(round(color));\n" -" int r = (c >> 16) & 0xFF;\n" -" int g = (c >> 8) & 0xFF;\n" -" int b = (c >> 0) & 0xFF;\n" +" vec3 rgb = vec3((c >> 16) & 0xFF, (c >> 8) & 0xFF, c & 0xFF);\n" +" if (id != kept_vertex && layer != lit_layers.x && layer != lit_layers.y) {\n" +" if (dim_brightness >= 0.0)\n" +" rgb = floor(rgb * dim_brightness);\n" +" else if (layer < grey_below_layer)\n" +" rgb = vec3(64.0);\n" +" }\n" " float f = 1.0 / 255.0f;\n" -" return f * vec3(r, g, b);\n" +" return f * rgb;\n" "}\n" "float lighting(vec3 eye_position, vec3 eye_normal) {\n" " float top_diffuse = light_top_diffuse * max(dot(eye_normal, light_top_dir), 0.0);\n" @@ -321,7 +383,7 @@ static const char* Options_Vertex_Shader = " // ORCA: Apply bias to z-position to avoid z-fighting\n" " eye_position.z += bias;\n" " vec3 eye_normal = (view_matrix * vec4(in_normal, 0.0)).xyz;\n" -" vec3 color_base = decode_color(texelFetch(color_tex, id).r);\n" +" vec3 color_base = decode_color(texelFetch(color_tex, id).r, id, int(texelFetch(position_tex, id).w));\n" " color = color_base * lighting(eye_position, eye_normal);\n" " gl_Position = projection_matrix * vec4(eye_position, 1.0);\n" "}\n"; diff --git a/src/libvgcode/src/ShadersES.hpp b/src/libvgcode/src/ShadersES.hpp index 1a88e18f96..12871f6249 100644 --- a/src/libvgcode/src/ShadersES.hpp +++ b/src/libvgcode/src/ShadersES.hpp @@ -15,7 +15,7 @@ static const char* Segments_Vertex_Shader_ES = "precision lowp usampler2D;\n" "#define POINTY_CAPS\n" "#define FIX_TWISTING\n" -"const vec3 light_top_dir = vec3(-0.4574957, 0.4574957, 0.7624929);\n" +"uniform vec3 light_top_dir;\n" "const float light_top_diffuse = 0.6 * 0.8;\n" // ORCA: the specular was 0.6 * 0.125, too faint to give the filament any sheen. "const float light_top_specular = 0.6 * 0.25;\n" @@ -32,6 +32,9 @@ static const char* Segments_Vertex_Shader_ES = "uniform sampler2D height_width_angle_tex;\n" "uniform sampler2D color_tex;\n" "uniform usampler2D segment_index_tex;\n" +// ORCA: draw last segment first, so looking down the top layers hide the rest from the fragment shader. +"uniform bool reverse_order;\n" +"uniform int instances_count;\n" // ORCA: section view "uniform vec4 clipping_plane;\n" "in float vertex_id_float;\n" @@ -47,13 +50,22 @@ static const char* Segments_Vertex_Shader_ES = // ORCA: section view - the segment and the light on its cut face "flat out int segment_id;\n" "out vec3 cut_color_direct;\n" -"vec3 decode_color(float color) {\n" +// ORCA: the layers greyed or dimmed around the one the sliders show, see ViewerImpl::set_layer_colors(). +"uniform ivec2 lit_layers;\n" +"uniform int grey_below_layer;\n" +"uniform float dim_brightness;\n" +"uniform int kept_vertex;\n" +"vec3 decode_color(float color, int id, int layer) {\n" " int c = int(round(color));\n" -" int r = (c >> 16) & 0xFF;\n" -" int g = (c >> 8) & 0xFF;\n" -" int b = (c >> 0) & 0xFF;\n" +" vec3 rgb = vec3((c >> 16) & 0xFF, (c >> 8) & 0xFF, c & 0xFF);\n" +" if (id != kept_vertex && layer != lit_layers.x && layer != lit_layers.y) {\n" +" if (dim_brightness >= 0.0)\n" +" rgb = floor(rgb * dim_brightness);\n" +" else if (layer < grey_below_layer)\n" +" rgb = vec3(64.0);\n" +" }\n" " float f = 1.0 / 255.0f;\n" -" return f * vec3(r, g, b);\n" +" return f * rgb;\n" "}\n" "float direct_lighting(vec3 eye_position, vec3 eye_normal) {\n" " float top_diffuse = light_top_diffuse * max(dot(eye_normal, light_top_dir), 0.0);\n" @@ -71,7 +83,8 @@ static const char* Segments_Vertex_Shader_ES = "}\n" "void main() {\n" " int vertex_id = int(vertex_id_float);\n" -" int id_a = int(texelFetch(segment_index_tex, tex_coord_u(segment_index_tex, gl_InstanceID), 0).r);\n" +" int instance = reverse_order ? instances_count - 1 - gl_InstanceID : gl_InstanceID;\n" +" int id_a = int(texelFetch(segment_index_tex, tex_coord_u(segment_index_tex, instance), 0).r);\n" " int id_b = id_a + 1;\n" " vec3 pos_a = texelFetch(position_tex, tex_coord(position_tex, id_a), 0).xyz;\n" " vec3 pos_b = texelFetch(position_tex, tex_coord(position_tex, id_b), 0).xyz;\n" @@ -156,7 +169,7 @@ static const char* Segments_Vertex_Shader_ES = " }\n" " vec3 eye_position = (view_matrix * vec4(pos, 1.0)).xyz;\n" " vec3 eye_normal = (view_matrix * vec4(normalize(pos - endpoint_pos), 0.0)).xyz;\n" -" vec3 color_base = decode_color(texelFetch(color_tex, tex_coord(color_tex, id), 0).r);\n" +" vec3 color_base = decode_color(texelFetch(color_tex, tex_coord(color_tex, id), 0).r, id, int(texelFetch(position_tex, tex_coord(position_tex, id), 0).w));\n" " color = color_base * (ambient + emission);\n" " color_direct = color_base * direct_lighting(eye_position, eye_normal);\n" " world_position = pos;\n" @@ -167,6 +180,64 @@ static const char* Segments_Vertex_Shader_ES = " gl_Position = projection_matrix * vec4(eye_position, 1.0);\n" "}\n"; +// ORCA: realistic view - shadow caster, one light facing ribbon per segment, not instanced as tiny instances are slow. +static const char* Segments_Shadow_Caster_Vertex_Shader_ES = +"#version 300 es\n" +"precision lowp usampler2D;\n" +"const vec3 UP = vec3(0, 0, 1);\n" +"const int CORNERS[6] = int[](0, 1, 2, 0, 2, 3);\n" +"uniform mat4 view_matrix;\n" +"uniform mat4 projection_matrix;\n" +"uniform sampler2D position_tex;\n" +"uniform sampler2D height_width_angle_tex;\n" +"uniform usampler2D segment_index_tex;\n" +"uniform bool reverse_order;\n" +"uniform int instances_count;\n" +"out vec3 world_position;\n" +"ivec2 tex_coord(sampler2D sampler, int id) {\n" +" ivec2 tex_size = textureSize(sampler, 0);\n" +" return (tex_size.y == 1) ? ivec2(id, 0) : ivec2(id % tex_size.x, id / tex_size.x);\n" +"}\n" +"ivec2 tex_coord_u(usampler2D sampler, int id) {\n" +" ivec2 tex_size = textureSize(sampler, 0);\n" +" return (tex_size.y == 1) ? ivec2(id, 0) : ivec2(id % tex_size.x, id / tex_size.x);\n" +"}\n" +"void main() {\n" +" int segment = gl_VertexID / 6;\n" +" int corner = CORNERS[gl_VertexID - 6 * segment];\n" +" int instance = reverse_order ? instances_count - 1 - segment : segment;\n" +" int id_a = int(texelFetch(segment_index_tex, tex_coord_u(segment_index_tex, instance), 0).r);\n" +" int id = corner < 2 ? id_a : id_a + 1;\n" +" vec3 pos_a = texelFetch(position_tex, tex_coord(position_tex, id_a), 0).xyz;\n" +" vec3 pos_b = texelFetch(position_tex, tex_coord(position_tex, id_a + 1), 0).xyz;\n" +" vec3 line = pos_b - pos_a;\n" +" float line_len = length(line);\n" +" vec3 line_dir = line_len < 1e-4 ? vec3(1.0, 0.0, 0.0) : line / line_len;\n" +" vec3 line_right_dir = abs(dot(line_dir, UP)) > 0.9 ? normalize(cross(vec3(1, 0, 0), line_dir)) : normalize(cross(line_dir, UP));\n" +" vec3 line_up_dir = normalize(cross(line_right_dir, line_dir));\n" +" vec3 to_light = vec3(view_matrix[0][2], view_matrix[1][2], view_matrix[2][2]);\n" +" vec3 side_dir = cross(to_light, line_dir);\n" +" side_dir = dot(side_dir, side_dir) < 1e-8 ? line_right_dir : normalize(side_dir);\n" +" vec2 height_width = texelFetch(height_width_angle_tex, tex_coord(height_width_angle_tex, id), 0).xy;\n" +" float half_extent = 0.5 * (height_width.y * abs(dot(line_right_dir, side_dir)) + height_width.x * abs(dot(line_up_dir, side_dir)));\n" +" float along = (corner < 2 ? -0.5 : 0.5) * height_width.y;\n" +" float side = (corner == 0 || corner == 3) ? -1.0 : 1.0;\n" +" vec3 pos = (corner < 2 ? pos_a : pos_b) + along * line_dir + side * half_extent * side_dir;\n" +" world_position = pos;\n" +" gl_Position = projection_matrix * (view_matrix * vec4(pos, 1.0));\n" +"}\n"; + +// ORCA: realistic view - the shadow caster writes depth only. What a section cuts away casts no shadow. +static const char* Segments_Shadow_Caster_Fragment_Shader_ES = +"#version 300 es\n" +"precision highp float;\n" +"uniform vec4 clipping_plane;\n" +"in vec3 world_position;\n" +"void main() {\n" +" if (dot(vec4(world_position, 1.0), clipping_plane) < 0.0)\n" +" discard;\n" +"}\n"; + static const char* Segments_Fragment_Shader_ES = "#version 300 es\n" "precision highp float;\n" @@ -178,7 +249,7 @@ static const char* Segments_Fragment_Shader_ES = "// ORCA: realistic view - object-on-object and self shadows, read from the same depth map the\n" "// rest of the 3D scene samples. shadow_intensity == 0, the default, short-circuits the lookup,\n" "// so the toolpaths shade exactly as before whenever realistic view is off.\n" -"const vec3 SHADOW_LIGHT_DIR = vec3(-0.4574957, 0.4574957, 0.7624929);\n" +"uniform vec3 light_top_dir;\n" "uniform sampler2D shadow_map;\n" "uniform mat4 shadow_light_vp;\n" "uniform float shadow_intensity;\n" @@ -215,7 +286,7 @@ static const char* Segments_Fragment_Shader_ES = " return 1.0;\n" " // Slope-scaled bias, as in gouraud.fs. An extrusion is only a handful of shadow-map texels\n" " // wide, so grazing faces need the larger bias to keep self-shadow acne off the top surfaces.\n" -" float NdotL = dot(normalize(normal), SHADOW_LIGHT_DIR);\n" +" float NdotL = dot(normalize(normal), light_top_dir);\n" " float bias = mix(0.0004, 0.004, clamp(1.0 - NdotL, 0.0, 1.0));\n" " float sum = 0.0;\n" " for (int x = -2; x <= 2; ++x) {\n" @@ -291,13 +362,22 @@ static const char* Options_Vertex_Shader_ES = "in vec3 in_normal;\n" "out vec3 color;\n" "out float clipping_plane_dot;\n" -"vec3 decode_color(float color) {\n" +// ORCA: the layers greyed or dimmed around the one the sliders show, see ViewerImpl::set_layer_colors(). +"uniform ivec2 lit_layers;\n" +"uniform int grey_below_layer;\n" +"uniform float dim_brightness;\n" +"uniform int kept_vertex;\n" +"vec3 decode_color(float color, int id, int layer) {\n" " int c = int(round(color));\n" -" int r = (c >> 16) & 0xFF;\n" -" int g = (c >> 8) & 0xFF;\n" -" int b = (c >> 0) & 0xFF;\n" +" vec3 rgb = vec3((c >> 16) & 0xFF, (c >> 8) & 0xFF, c & 0xFF);\n" +" if (id != kept_vertex && layer != lit_layers.x && layer != lit_layers.y) {\n" +" if (dim_brightness >= 0.0)\n" +" rgb = floor(rgb * dim_brightness);\n" +" else if (layer < grey_below_layer)\n" +" rgb = vec3(64.0);\n" +" }\n" " float f = 1.0 / 255.0f;\n" -" return f * vec3(r, g, b);\n" +" return f * rgb;\n" "}\n" "float lighting(vec3 eye_position, vec3 eye_normal) {\n" " float top_diffuse = light_top_diffuse * max(dot(eye_normal, light_top_dir), 0.0);\n" @@ -326,7 +406,7 @@ static const char* Options_Vertex_Shader_ES = " clipping_plane_dot = dot(vec4(offset, 1.0), clipping_plane);\n" " vec3 eye_position = (view_matrix * vec4(scale_matrix * in_position + offset, 1.0)).xyz;\n" " vec3 eye_normal = (view_matrix * vec4(in_normal, 0.0)).xyz;\n" -" vec3 color_base = decode_color(texelFetch(color_tex, tex_coord(color_tex, id), 0).r);\n" +" vec3 color_base = decode_color(texelFetch(color_tex, tex_coord(color_tex, id), 0).r, id, int(texelFetch(position_tex, tex_coord(position_tex, id), 0).w));\n" " color = color_base * lighting(eye_position, eye_normal);\n" " gl_Position = projection_matrix * vec4(eye_position, 1.0);\n" "}\n"; diff --git a/src/libvgcode/src/Viewer.cpp b/src/libvgcode/src/Viewer.cpp index 4d5cc29b7c..089f1f7e7b 100644 --- a/src/libvgcode/src/Viewer.cpp +++ b/src/libvgcode/src/Viewer.cpp @@ -52,6 +52,11 @@ void Viewer::set_shadow_map(int texture_unit, const Mat4x4& light_view_projectio m_impl->set_shadow_map(texture_unit, light_view_projection, intensity, texel_size); } +void Viewer::set_light_top_dir(const Vec3& direction) +{ + m_impl->set_light_top_dir(direction); +} + void Viewer::set_tone(float exposure, float saturation) { m_impl->set_tone(exposure, saturation); diff --git a/src/libvgcode/src/ViewerImpl.cpp b/src/libvgcode/src/ViewerImpl.cpp index 47e2d43f01..ee38594817 100644 --- a/src/libvgcode/src/ViewerImpl.cpp +++ b/src/libvgcode/src/ViewerImpl.cpp @@ -349,7 +349,7 @@ void ViewerImpl::TextureData::init(size_t vertices_count) m_tex_ids = std::vector(m_count); } -void ViewerImpl::TextureData::set_positions(const std::vector& positions) +void ViewerImpl::TextureData::set_positions(const std::vector& positions) { if (m_count == 0) return; @@ -383,17 +383,17 @@ void ViewerImpl::TextureData::set_positions(const std::vector& positions) glsafe(glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST)); glsafe(glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_LEVEL, 0)); if (remaining >= tex_capacity) { - glsafe(glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB32F, static_cast(w), static_cast(h), 0, GL_RGB, GL_FLOAT, &positions[offset])); + glsafe(glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA32F, static_cast(w), static_cast(h), 0, GL_RGBA, GL_FLOAT, &positions[offset])); m_tex_ids[i].positions.second = w * h; } else { // the last row is only partially fitted with data, send it separately - glsafe(glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB32F, static_cast(w), static_cast(h), 0, GL_RGB, GL_FLOAT, nullptr)); - glsafe(glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, static_cast(w), static_cast(h - 1), GL_RGB, GL_FLOAT, &positions[offset])); - glsafe(glTexSubImage2D(GL_TEXTURE_2D, 0, 0, static_cast(h - 1), static_cast(remaining % w), 1, GL_RGB, GL_FLOAT, &positions[offset + w * (h - 1)])); + glsafe(glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA32F, static_cast(w), static_cast(h), 0, GL_RGBA, GL_FLOAT, nullptr)); + glsafe(glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, static_cast(w), static_cast(h - 1), GL_RGBA, GL_FLOAT, &positions[offset])); + glsafe(glTexSubImage2D(GL_TEXTURE_2D, 0, 0, static_cast(h - 1), static_cast(remaining % w), 1, GL_RGBA, GL_FLOAT, &positions[offset + w * (h - 1)])); m_tex_ids[i].positions.second = w * (h - 1) + remaining % w; } - m_positions_size += m_tex_ids[i].positions.second * sizeof(Vec3); + m_positions_size += m_tex_ids[i].positions.second * sizeof(Vec4); remaining = (remaining > tex_capacity) ? remaining - tex_capacity: 0; } @@ -402,7 +402,7 @@ void ViewerImpl::TextureData::set_positions(const std::vector& positions) glsafe(glPixelStorei(GL_UNPACK_ALIGNMENT, curr_unpack_alignment)); } -void ViewerImpl::TextureData::set_heights_widths_angles(const std::vector& heights_widths_angles) +void ViewerImpl::TextureData::set_heights_widths_angles(const std::vector& heights_widths_angles) { if (m_count == 0) return; @@ -436,17 +436,17 @@ void ViewerImpl::TextureData::set_heights_widths_angles(const std::vector& glsafe(glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST)); glsafe(glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_LEVEL, 0)); if (remaining >= tex_capacity) { - glsafe(glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB32F, static_cast(w), static_cast(h), 0, GL_RGB, GL_FLOAT, &heights_widths_angles[offset])); + glsafe(glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA32F, static_cast(w), static_cast(h), 0, GL_RGBA, GL_FLOAT, &heights_widths_angles[offset])); m_tex_ids[i].heights_widths_angles.second = w * h; } else { // the last row is only partially fitted with data, send it separately - glsafe(glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB32F, static_cast(w), static_cast(h), 0, GL_RGB, GL_FLOAT, nullptr)); - glsafe(glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, static_cast(w), static_cast(h - 1), GL_RGB, GL_FLOAT, &heights_widths_angles[offset])); - glsafe(glTexSubImage2D(GL_TEXTURE_2D, 0, 0, static_cast(h - 1), static_cast(remaining % w), 1, GL_RGB, GL_FLOAT, &heights_widths_angles[offset + w * (h - 1)])); + glsafe(glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA32F, static_cast(w), static_cast(h), 0, GL_RGBA, GL_FLOAT, nullptr)); + glsafe(glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, static_cast(w), static_cast(h - 1), GL_RGBA, GL_FLOAT, &heights_widths_angles[offset])); + glsafe(glTexSubImage2D(GL_TEXTURE_2D, 0, 0, static_cast(h - 1), static_cast(remaining % w), 1, GL_RGBA, GL_FLOAT, &heights_widths_angles[offset + w * (h - 1)])); m_tex_ids[i].heights_widths_angles.second = w * (h - 1) + remaining % w; } - m_height_width_angle_size += m_tex_ids[i].heights_widths_angles.second * sizeof(Vec3); + m_height_width_angle_size += m_tex_ids[i].heights_widths_angles.second * sizeof(Vec4); remaining = (remaining > tex_capacity) ? remaining - tex_capacity : 0; } @@ -732,6 +732,21 @@ ViewerImpl::ViewerImpl() reset_default_options_colors(); } +void ViewerImpl::SegmentsUniforms::init(unsigned int shader_id) +{ + view_matrix = glGetUniformLocation(shader_id, "view_matrix"); + projection_matrix = glGetUniformLocation(shader_id, "projection_matrix"); + camera_position = glGetUniformLocation(shader_id, "camera_position"); + positions_tex = glGetUniformLocation(shader_id, "position_tex"); + height_width_angle_tex = glGetUniformLocation(shader_id, "height_width_angle_tex"); + colors_tex = glGetUniformLocation(shader_id, "color_tex"); + segment_index_tex = glGetUniformLocation(shader_id, "segment_index_tex"); + reverse_order = glGetUniformLocation(shader_id, "reverse_order"); + instances_count = glGetUniformLocation(shader_id, "instances_count"); + // ORCA: section view + clipping_plane = glGetUniformLocation(shader_id, "clipping_plane"); +} + void ViewerImpl::init(const std::string& opengl_context_version) { if (m_initialized) @@ -756,15 +771,7 @@ void ViewerImpl::init(const std::string& opengl_context_version) m_segments_shader_id = init_shader("segments", Segments_Vertex_Shader, Segments_Fragment_Shader); #endif // ENABLE_OPENGL_ES - m_uni_segments_view_matrix_id = glGetUniformLocation(m_segments_shader_id, "view_matrix"); - m_uni_segments_projection_matrix_id = glGetUniformLocation(m_segments_shader_id, "projection_matrix"); - m_uni_segments_camera_position_id = glGetUniformLocation(m_segments_shader_id, "camera_position"); - m_uni_segments_positions_tex_id = glGetUniformLocation(m_segments_shader_id, "position_tex"); - m_uni_segments_height_width_angle_tex_id = glGetUniformLocation(m_segments_shader_id, "height_width_angle_tex"); - m_uni_segments_colors_tex_id = glGetUniformLocation(m_segments_shader_id, "color_tex"); - m_uni_segments_segment_index_tex_id = glGetUniformLocation(m_segments_shader_id, "segment_index_tex"); - m_uni_segments_reverse_order_id = glGetUniformLocation(m_segments_shader_id, "reverse_order"); - m_uni_segments_instance_count_id = glGetUniformLocation(m_segments_shader_id, "instance_count"); + m_uni_segments.init(m_segments_shader_id); // ORCA: realistic view m_uni_segments_shadow_map_id = glGetUniformLocation(m_segments_shader_id, "shadow_map"); m_uni_segments_shadow_light_vp_id = glGetUniformLocation(m_segments_shader_id, "shadow_light_vp"); @@ -772,17 +779,29 @@ void ViewerImpl::init(const std::string& opengl_context_version) m_uni_segments_shadow_map_texel_id = glGetUniformLocation(m_segments_shader_id, "shadow_map_texel"); m_uni_segments_exposure_id = glGetUniformLocation(m_segments_shader_id, "exposure"); m_uni_segments_saturation_id = glGetUniformLocation(m_segments_shader_id, "saturation"); - m_uni_segments_bias_scale_id = glGetUniformLocation(m_segments_shader_id, "bias_scale"); - // ORCA: section view - m_uni_segments_clipping_plane_id = glGetUniformLocation(m_segments_shader_id, "clipping_plane"); + m_uni_segments_light_top_dir_id = glGetUniformLocation(m_segments_shader_id, "light_top_dir"); + m_uni_segments_layer_colors.init(m_segments_shader_id); + glcheck(); + assert(m_uni_segments.view_matrix != -1 && + m_uni_segments.projection_matrix != -1 && + m_uni_segments.camera_position != -1 && + m_uni_segments.positions_tex != -1 && + m_uni_segments.height_width_angle_tex != -1 && + m_uni_segments.colors_tex != -1 && + m_uni_segments.segment_index_tex != -1 && + m_uni_segments.reverse_order != -1 && + m_uni_segments.instances_count != -1); + + // ORCA: realistic view +#ifdef ENABLE_OPENGL_ES + m_segments_caster_shader_id = init_shader("segments_shadow_caster", Segments_Shadow_Caster_Vertex_Shader_ES, Segments_Shadow_Caster_Fragment_Shader_ES); +#else + m_segments_caster_shader_id = init_shader("segments_shadow_caster", Segments_Shadow_Caster_Vertex_Shader, Segments_Shadow_Caster_Fragment_Shader); +#endif // ENABLE_OPENGL_ES + m_uni_segments_caster.init(m_segments_caster_shader_id); + // The caster pulls its vertices from gl_VertexID, but a core profile needs a vertex array bound to draw. + glsafe(glGenVertexArrays(1, &m_segments_caster_vao_id)); glcheck(); - assert(m_uni_segments_view_matrix_id != -1 && - m_uni_segments_projection_matrix_id != -1 && - m_uni_segments_camera_position_id != -1 && - m_uni_segments_positions_tex_id != -1 && - m_uni_segments_height_width_angle_tex_id != -1 && - m_uni_segments_colors_tex_id != -1 && - m_uni_segments_segment_index_tex_id != -1); m_segment_template.init(); @@ -801,6 +820,7 @@ void ViewerImpl::init(const std::string& opengl_context_version) m_uni_options_segment_index_tex_id = glGetUniformLocation(m_options_shader_id, "segment_index_tex"); // ORCA: section view m_uni_options_clipping_plane_id = glGetUniformLocation(m_options_shader_id, "clipping_plane"); + m_uni_options_layer_colors.init(m_options_shader_id); glcheck(); assert(m_uni_options_view_matrix_id != -1 && m_uni_options_projection_matrix_id != -1 && @@ -874,6 +894,14 @@ void ViewerImpl::shutdown() glsafe(glDeleteProgram(m_segments_shader_id)); m_segments_shader_id = 0; } + if (m_segments_caster_shader_id != 0) { + glsafe(glDeleteProgram(m_segments_caster_shader_id)); + m_segments_caster_shader_id = 0; + } + if (m_segments_caster_vao_id != 0) { + glsafe(glDeleteVertexArrays(1, &m_segments_caster_vao_id)); + m_segments_caster_vao_id = 0; + } m_initialized = false; OpenGLWrapper::unload_opengl(); } @@ -894,7 +922,7 @@ void ViewerImpl::reset() for (std::vector& times : m_layer_start_times) std::vector().swap(times); std::vector().swap(m_layer_first_vertex); - std::vector().swap(m_colors_scratch); + m_layers_in_vertex_order = false; m_valid_lines_bitset.clear(); #if VGCODE_ENABLE_COG_AND_TOOL_MARKERS m_cog_marker.reset(); @@ -924,9 +952,8 @@ void ViewerImpl::reset() // On some graphic cards texture buffers using GL_RGB32F format do not work, see: // https://dev.prusa3d.com/browse/SPE-2411 // https://github.com/prusa3d/PrusaSlicer/issues/12908 -// To let all drivers be happy, we use GL_RGBA32F format, so we need to add an extra (currently unused) float +// To let all drivers be happy, we use GL_RGBA32F format, so we need to add an extra float // to position and heights_widths_angles vectors -using Vec4 = std::array; static void extract_pos_and_or_hwa(const std::vector& vertices, float travels_radius, float wipes_radius, BitSet<>& valid_lines_bitset, std::vector* positions = nullptr, std::vector* heights_widths_angles = nullptr, bool update_bitset = false) { @@ -963,8 +990,8 @@ static void extract_pos_and_or_hwa(const std::vector& vertices, floa } if (positions != nullptr) { - // the last component is a dummy float to comply with GL_RGBA32F format - Vec4 position = { v.position[0], v.position[1], v.position[2], 0.0f }; + // ORCA: the last component, there to comply with GL_RGBA32F format, carries the layer the shaders grey and dim by + Vec4 position = { v.position[0], v.position[1], v.position[2], static_cast(v.layer_id) }; if (move_type == EMoveType::Extrude) // push down extrusion vertices by half height to render them at the right z position[2] -= 0.5f * v.height; @@ -1080,6 +1107,8 @@ void ViewerImpl::load(GCodeInputData&& gcode_data) } for (size_t i = m_layer_first_vertex.size() - 1; i > 0; --i) m_layer_first_vertex[i - 1] = std::min(m_layer_first_vertex[i - 1], m_layer_first_vertex[i]); + m_layers_in_vertex_order = std::is_sorted(m_vertices.begin(), m_vertices.end(), + [](const PathVertex& a, const PathVertex& b) { return a.layer_id < b.layer_id; }); // the running time at each layer's first vertex, summed in vertex order so that // get_estimated_time_at() matches a full accumulation exactly @@ -1274,17 +1303,6 @@ static float encode_color(const Color& color) { return static_cast(i_color); } -// ORCA: returns the encoded color scaled towards black by 'brightness', preserving its hue. -// 1.0 = no change, 0.0 = black. -static float encode_color_dimmed(const Color& color, float brightness) { - const int r = static_cast(color[0] * brightness); - const int g = static_cast(color[1] * brightness); - const int b = static_cast(color[2] * brightness); - const int i_color = r << 16 | g << 8 | b; - return static_cast(i_color); -} - - void ViewerImpl::update_colors_texture() { #if !defined(ENABLE_OPENGL_ES) @@ -1292,6 +1310,32 @@ void ViewerImpl::update_colors_texture() return; #endif // ENABLE_OPENGL_ES + // ORCA: the colors of the vertices alone; the layers greyed or dimmed around the one the sliders + // show are left to the shaders, see set_layer_colors(), so that a slider step uploads nothing. + #ifdef ENABLE_OPENGL_ES + if (!m_vertices_colors.empty()) + // update gpu buffer for colors + m_texture_data.set_colors(m_vertices_colors); + #else + m_colors_tex_size = m_vertices_colors.size() * sizeof(float); + + // update gpu buffer for colors + glsafe(glBindBuffer(GL_TEXTURE_BUFFER, m_colors_buf_id)); + glsafe(glBufferData(GL_TEXTURE_BUFFER, m_vertices_colors.size() * sizeof(float), m_vertices_colors.data(), GL_STATIC_DRAW)); + glsafe(glBindBuffer(GL_TEXTURE_BUFFER, 0)); + #endif // ENABLE_OPENGL_ES +} + +void ViewerImpl::LayerColorsUniforms::init(unsigned int shader_id) +{ + lit_layers = glGetUniformLocation(shader_id, "lit_layers"); + grey_below_layer = glGetUniformLocation(shader_id, "grey_below_layer"); + dim_brightness = glGetUniformLocation(shader_id, "dim_brightness"); + kept_vertex = glGetUniformLocation(shader_id, "kept_vertex"); +} + +void ViewerImpl::set_layer_colors(const LayerColorsUniforms& uni) const +{ const size_t top_layer_id = m_settings.top_layer_only_view_range ? m_layers.get_view_range()[1] : 0; const bool color_top_layer_only = m_view_range.get_full()[1] != m_view_range.get_visible()[1]; @@ -1310,43 +1354,15 @@ void ViewerImpl::update_colors_texture() const bool dim_previous_layers = m_settings.dim_previous_layers && m_settings.top_layer_only_view_range && !m_layers.empty() && (inspecting_top_layer || inspecting_bottom_layer); - // Based on current settings and slider position, we might want to render some - // vertices as dark grey (or darkened, see above). Use either that or the normal color (from the cache). - // Reused across calls: this runs on every slider tick, and the allocation alone is - // 4 bytes per vertex of the whole print each time. - std::vector& colors = m_colors_scratch; - colors.resize(m_vertices_colors.size()); - assert(colors.size() == m_vertices.size() && m_vertices_colors.size() == m_vertices.size()); - for (size_t i=0; i(layers_range[0]) : -1, + dim_previous_layers && inspecting_top_layer ? static_cast(layers_range[1]) : -1)); + glsafe(glUniform1i(uni.grey_below_layer, color_top_layer_only ? static_cast(top_layer_id) : 0)); + glsafe(glUniform1f(uni.dim_brightness, dim_previous_layers ? m_settings.dim_previous_layers_brightness : -1.0f)); + glsafe(glUniform1i(uni.kept_vertex, m_settings.spiral_vase_mode ? static_cast(m_view_range.get_enabled()[0]) : -1)); } - void ViewerImpl::update_colors() { @@ -1366,7 +1382,7 @@ void ViewerImpl::update_colors() // Recalculate "normal" colors of all the vertices for current view settings. // If some part of the preview should be rendered in dark grey, it is taken - // care of in update_colors_texture. That is to avoid the need to recalculate + // care of in the shaders, see set_layer_colors(). That is to avoid the need to recalculate // the "normal" color on every slider move. for (size_t i = 0; i < m_vertices.size(); ++i) m_vertices_colors[i] = encode_color(get_vertex_color(m_vertices[i])); @@ -1453,8 +1469,6 @@ void ViewerImpl::set_layers_view_range(Interval::value_type min, Interval::value update_view_full_range(); m_view_range.set_visible(m_view_range.get_enabled()); m_settings.update_enabled_entities = true; - //m_settings.update_colors = true; - update_colors_texture(); } void ViewerImpl::toggle_top_layer_only_view_range() @@ -1463,8 +1477,6 @@ void ViewerImpl::toggle_top_layer_only_view_range() update_view_full_range(); m_view_range.set_visible(m_view_range.get_enabled()); m_settings.update_enabled_entities = true; - //m_settings.update_colors = true; - update_colors_texture(); } // ORCA: enable/disable darkening of the layers the layer slider is not scrubbed to @@ -1473,9 +1485,6 @@ void ViewerImpl::set_dim_previous_layers(bool value) if (m_settings.dim_previous_layers == value) return; m_settings.dim_previous_layers = value; - // defer the actual color/texture rebuild to the next render(), when the GL context is current - // (this may be toggled from the Preferences dialog, outside the canvas context) - m_settings.update_colors = true; } // ORCA: set how bright the darkened layers are rendered, 1.0 = unchanged, 0.0 = black @@ -1485,7 +1494,6 @@ void ViewerImpl::set_dim_previous_layers_brightness(float value) if (m_settings.dim_previous_layers_brightness == value) return; m_settings.dim_previous_layers_brightness = value; - m_settings.update_colors = true; } std::vector ViewerImpl::get_time_modes() const @@ -1593,8 +1601,6 @@ void ViewerImpl::set_view_visible_range(Interval::value_type min, Interval::valu update_view_full_range(); m_view_range.set_visible(min, max); update_enabled_entities(); - //m_settings.update_colors = true; - update_colors_texture(); } float ViewerImpl::get_estimated_time_at(size_t id) const @@ -1819,7 +1825,6 @@ size_t ViewerImpl::get_used_cpu_memory() const for (const std::vector& times : m_layer_start_times) ret += STDVEC_MEMSIZE(times, float); ret += STDVEC_MEMSIZE(m_layer_first_vertex, uint32_t); - ret += STDVEC_MEMSIZE(m_colors_scratch, float); ret += m_valid_lines_bitset.size_in_bytes_cpu(); ret += m_height_range.size_in_bytes_cpu(); ret += m_width_range.size_in_bytes_cpu(); @@ -1912,6 +1917,11 @@ void ViewerImpl::update_view_full_range() } auto last_it = first_it; + // ORCA: skip straight to the layer above the range rather than walking there + if (m_layers_in_vertex_order && layers_range[1] + 1 < m_layer_first_vertex.size()) + last_it = std::max(first_it, m_vertices.begin() + m_layer_first_vertex[layers_range[1] + 1]); + else if (m_layers_in_vertex_order) + last_it = m_vertices.end(); while (last_it != m_vertices.end() && last_it->layer_id <= layers_range[1]) { ++last_it; } @@ -1951,12 +1961,14 @@ void ViewerImpl::update_view_full_range() if (m_settings.top_layer_only_view_range) { const Interval& full_range = m_view_range.get_full(); - auto top_first_it = m_vertices.begin() + full_range[0]; - bool shortened = false; - while (top_first_it != m_vertices.end() && (top_first_it->layer_id < layers_range[1] || !is_visible(*top_first_it, m_settings))) { + const auto range_first_it = m_vertices.begin() + full_range[0]; + auto top_first_it = range_first_it; + // ORCA: skip straight to the top layer rather than walking there + if (m_layers_in_vertex_order) + top_first_it = std::max(top_first_it, m_vertices.begin() + m_layer_first_vertex[layers_range[1]]); + while (top_first_it != m_vertices.end() && (top_first_it->layer_id < layers_range[1] || !is_visible(*top_first_it, m_settings))) ++top_first_it; - shortened = true; - } + const bool shortened = top_first_it != range_first_it; if (shortened) --top_first_it; @@ -2038,7 +2050,7 @@ void ViewerImpl::update_color_ranges() void ViewerImpl::update_heights_widths() { #ifdef ENABLE_OPENGL_ES - std::vector heights_widths_angles; + std::vector heights_widths_angles; heights_widths_angles.reserve(m_vertices.size()); extract_pos_and_or_hwa(m_vertices, m_travels_radius, m_wipes_radius, m_valid_lines_bitset, nullptr, &heights_widths_angles); m_texture_data.set_heights_widths_angles(heights_widths_angles); @@ -2070,7 +2082,10 @@ void ViewerImpl::update_heights_widths() void ViewerImpl::render_segments(const Mat4x4& view_matrix, const Mat4x4& projection_matrix, const Vec3& camera_position) { - if (m_segments_shader_id == 0) + // ORCA: realistic view. The shadow caster pass draws depth only. + const unsigned int shader_id = m_rendering_shadow_casters ? m_segments_caster_shader_id : m_segments_shader_id; + const SegmentsUniforms& uni = m_rendering_shadow_casters ? m_uni_segments_caster : m_uni_segments; + if (shader_id == 0) return; #ifdef ENABLE_OPENGL_ES @@ -2087,42 +2102,54 @@ void ViewerImpl::render_segments(const Mat4x4& view_matrix, const Mat4x4& projec const bool curr_cull_face = glIsEnabled(GL_CULL_FACE); glcheck(); - glsafe(glUseProgram(m_segments_shader_id)); + glsafe(glUseProgram(shader_id)); - glsafe(glUniform1i(m_uni_segments_positions_tex_id, 0)); - glsafe(glUniform1i(m_uni_segments_height_width_angle_tex_id, 1)); - glsafe(glUniform1i(m_uni_segments_colors_tex_id, 2)); - glsafe(glUniform1i(m_uni_segments_segment_index_tex_id, 3)); - glsafe(glUniformMatrix4fv(m_uni_segments_view_matrix_id, 1, GL_FALSE, view_matrix.data())); - glsafe(glUniformMatrix4fv(m_uni_segments_projection_matrix_id, 1, GL_FALSE, projection_matrix.data())); - glsafe(glUniform3fv(m_uni_segments_camera_position_id, 1, camera_position.data())); - // The segments come in print order, bottom layer first. Seen from above, that is back to front, - // and every hidden fragment is shaded before the one that covers it. Drawing them last to first - // lets the depth test reject the hidden ones instead. The camera looks down when the world's - // up axis points towards it, which is the view matrix's (2, 2) entry being positive. - const bool top_down = !m_rendering_shadow_casters && view_matrix[10] > 0.0f; - glsafe(glUniform1i(m_uni_segments_reverse_order_id, top_down ? 1 : 0)); -#ifndef ENABLE_OPENGL_ES - glsafe(glUniform1i(m_uni_segments_instance_count_id, static_cast(m_enabled_segments_count))); -#endif // ENABLE_OPENGL_ES - // ORCA: realistic view. The depth pass writes the map it would otherwise read, so it shades - // with the lookup off. - glsafe(glUniform1i(m_uni_segments_shadow_map_id, m_shadow_map_texture_unit)); - glsafe(glUniformMatrix4fv(m_uni_segments_shadow_light_vp_id, 1, GL_FALSE, m_shadow_light_vp.data())); - glsafe(glUniform1f(m_uni_segments_shadow_intensity_id, m_rendering_shadow_casters ? 0.0f : m_shadow_intensity)); - glsafe(glUniform1f(m_uni_segments_shadow_map_texel_id, m_shadow_map_texel)); - glsafe(glUniform1f(m_uni_segments_exposure_id, m_exposure)); - glsafe(glUniform1f(m_uni_segments_saturation_id, m_saturation)); - glsafe(glUniform1f(m_uni_segments_bias_scale_id, m_rendering_shadow_casters ? 0.0f : 1.0f)); - glsafe(glUniform4fv(m_uni_segments_clipping_plane_id, 1, m_clipping_plane.data())); + glsafe(glUniform1i(uni.positions_tex, 0)); + glsafe(glUniform1i(uni.height_width_angle_tex, 1)); + glsafe(glUniform1i(uni.colors_tex, 2)); + glsafe(glUniform1i(uni.segment_index_tex, 3)); + glsafe(glUniformMatrix4fv(uni.view_matrix, 1, GL_FALSE, view_matrix.data())); + glsafe(glUniformMatrix4fv(uni.projection_matrix, 1, GL_FALSE, projection_matrix.data())); + glsafe(glUniform3fv(uni.camera_position, 1, camera_position.data())); + // ORCA: view_matrix(2,2) > 0 is looking down, where the last segments printed are the nearest. + const bool reverse_order = view_matrix[10] > 0.0f; + glsafe(glUniform1i(uni.reverse_order, reverse_order ? 1 : 0)); + glsafe(glUniform4fv(uni.clipping_plane, 1, m_clipping_plane.data())); + if (!m_rendering_shadow_casters) { + // ORCA: realistic view + glsafe(glUniform1i(m_uni_segments_shadow_map_id, m_shadow_map_texture_unit)); + glsafe(glUniformMatrix4fv(m_uni_segments_shadow_light_vp_id, 1, GL_FALSE, m_shadow_light_vp.data())); + glsafe(glUniform1f(m_uni_segments_shadow_intensity_id, m_shadow_intensity)); + glsafe(glUniform1f(m_uni_segments_shadow_map_texel_id, m_shadow_map_texel)); + glsafe(glUniform1f(m_uni_segments_exposure_id, m_exposure)); + glsafe(glUniform1f(m_uni_segments_saturation_id, m_saturation)); + glsafe(glUniform3fv(m_uni_segments_light_top_dir_id, 1, m_light_top_dir.data())); + set_layer_colors(m_uni_segments_layer_colors); + } glsafe(glDisable(GL_CULL_FACE)); + auto draw = [this, &uni](size_t count) { + glsafe(glUniform1i(uni.instances_count, static_cast(count))); + if (!m_rendering_shadow_casters) { + m_segment_template.render(count); + return; + } + // ORCA: realistic view. 6 vertices per caster ribbon. + int curr_vertex_array = 0; + glsafe(glGetIntegerv(GL_VERTEX_ARRAY_BINDING, &curr_vertex_array)); + glsafe(glBindVertexArray(m_segments_caster_vao_id)); + glsafe(glDrawArrays(GL_TRIANGLES, 0, static_cast(6 * count))); + glsafe(glBindVertexArray(curr_vertex_array)); + }; + #ifdef ENABLE_OPENGL_ES int curr_bound_texture = 0; glsafe(glGetIntegerv(GL_TEXTURE_BINDING_2D, &curr_bound_texture)); - for (size_t i = 0; i < m_texture_data.get_count(); ++i) { + const size_t tex_count = m_texture_data.get_count(); + for (size_t n = 0; n < tex_count; ++n) { + const size_t i = reverse_order ? tex_count - 1 - n : n; const auto [id, count] = m_texture_data.get_enabled_segments_tex_id(i); if (count == 0) continue; @@ -2134,7 +2161,7 @@ void ViewerImpl::render_segments(const Mat4x4& view_matrix, const Mat4x4& projec glsafe(glBindTexture(GL_TEXTURE_2D, m_texture_data.get_colors_tex_id(i).first)); glsafe(glActiveTexture(GL_TEXTURE3)); glsafe(glBindTexture(GL_TEXTURE_2D, id)); - m_segment_template.render(count); + draw(count); } #else std::array curr_bound_texture = { 0, 0, 0, 0 }; @@ -2157,7 +2184,7 @@ void ViewerImpl::render_segments(const Mat4x4& view_matrix, const Mat4x4& projec glsafe(glBindTexture(GL_TEXTURE_BUFFER, m_enabled_segments_tex_id)); glsafe(glTexBuffer(GL_TEXTURE_BUFFER, GL_R32UI, m_enabled_segments_buf_id)); - m_segment_template.render(m_enabled_segments_count); + draw(m_enabled_segments_count); #endif // ENABLE_OPENGL_ES if (curr_cull_face) @@ -2203,6 +2230,7 @@ void ViewerImpl::render_options(const Mat4x4& view_matrix, const Mat4x4& project glsafe(glUniformMatrix4fv(m_uni_options_view_matrix_id, 1, GL_FALSE, view_matrix.data())); glsafe(glUniformMatrix4fv(m_uni_options_projection_matrix_id, 1, GL_FALSE, projection_matrix.data())); glsafe(glUniform4fv(m_uni_options_clipping_plane_id, 1, m_clipping_plane.data())); + set_layer_colors(m_uni_options_layer_colors); glsafe(glEnable(GL_CULL_FACE)); diff --git a/src/libvgcode/src/ViewerImpl.hpp b/src/libvgcode/src/ViewerImpl.hpp index bf8f8558d9..335fb5ef52 100644 --- a/src/libvgcode/src/ViewerImpl.hpp +++ b/src/libvgcode/src/ViewerImpl.hpp @@ -91,6 +91,7 @@ public: void set_tone(float exposure, float saturation); // ORCA: section view, see Viewer::set_clipping_plane() void set_clipping_plane(const std::array& plane) { m_clipping_plane = plane; } + void set_light_top_dir(const Vec3& direction) { m_light_top_dir = direction; } EViewType get_view_type() const { return m_settings.view_type; } void set_view_type(EViewType type); @@ -264,9 +265,10 @@ private: // std::vector m_layer_first_vertex; // - // Scratch buffer for update_colors_texture(), kept alive across slider steps + // ORCA: whether the layer ids never decrease along the vertices, so that each layer's vertices + // follow m_layer_first_vertex. Not so for a print by object, whose layers start over per object. // - std::vector m_colors_scratch; + bool m_layers_in_vertex_order{ false }; // // Detected travel moves times // @@ -349,6 +351,9 @@ private: // OpenGL shaders ids // unsigned int m_segments_shader_id{ 0 }; + // ORCA: realistic view. Depth-only ribbons for the shadow caster pass, and the empty vertex array they draw with. + unsigned int m_segments_caster_shader_id{ 0 }; + unsigned int m_segments_caster_vao_id{ 0 }; unsigned int m_options_shader_id{ 0 }; #if VGCODE_ENABLE_COG_AND_TOOL_MARKERS unsigned int m_cog_marker_shader_id{ 0 }; @@ -357,25 +362,44 @@ private: // // Caches for OpenGL uniforms id for segments shader // - int m_uni_segments_view_matrix_id{ -1 }; - int m_uni_segments_projection_matrix_id{ -1 }; - int m_uni_segments_camera_position_id{ -1 }; - int m_uni_segments_positions_tex_id{ -1 }; - int m_uni_segments_height_width_angle_tex_id{ -1 }; - int m_uni_segments_colors_tex_id{ -1 }; - int m_uni_segments_segment_index_tex_id{ -1 }; - int m_uni_segments_reverse_order_id{ -1 }; - int m_uni_segments_instance_count_id{ -1 }; + // ORCA: the ones the shaded and the shadow caster programs share. + struct SegmentsUniforms + { + int view_matrix{ -1 }; + int projection_matrix{ -1 }; + int camera_position{ -1 }; + int positions_tex{ -1 }; + int height_width_angle_tex{ -1 }; + int colors_tex{ -1 }; + int segment_index_tex{ -1 }; + int reverse_order{ -1 }; + int instances_count{ -1 }; + int clipping_plane{ -1 }; + + void init(unsigned int shader_id); + }; + SegmentsUniforms m_uni_segments; + SegmentsUniforms m_uni_segments_caster; int m_uni_segments_shadow_map_id{ -1 }; int m_uni_segments_shadow_light_vp_id{ -1 }; int m_uni_segments_shadow_intensity_id{ -1 }; int m_uni_segments_shadow_map_texel_id{ -1 }; int m_uni_segments_exposure_id{ -1 }; int m_uni_segments_saturation_id{ -1 }; - int m_uni_segments_bias_scale_id{ -1 }; - int m_uni_segments_clipping_plane_id{ -1 }; + int m_uni_segments_light_top_dir_id{ -1 }; + // ORCA: the layers greyed or dimmed around the one the sliders show, in the segments and options shaders. + struct LayerColorsUniforms + { + int lit_layers{ -1 }; + int grey_below_layer{ -1 }; + int dim_brightness{ -1 }; + int kept_vertex{ -1 }; + + void init(unsigned int shader_id); + }; + LayerColorsUniforms m_uni_segments_layer_colors; // - // Caches for OpenGL uniforms id for options shader + // Caches for OpenGL uniforms id for options shader // int m_uni_options_view_matrix_id{ -1 }; int m_uni_options_projection_matrix_id{ -1 }; @@ -384,6 +408,7 @@ private: int m_uni_options_colors_tex_id{ -1 }; int m_uni_options_segment_index_tex_id{ -1 }; int m_uni_options_clipping_plane_id{ -1 }; + LayerColorsUniforms m_uni_options_layer_colors; #if VGCODE_ENABLE_COG_AND_TOOL_MARKERS // // Caches for OpenGL uniforms id for cog marker shader @@ -407,8 +432,8 @@ private: { public: void init(size_t vertices_count); - void set_positions(const std::vector& positions); - void set_heights_widths_angles(const std::vector& heights_widths_angles); + void set_positions(const std::vector& positions); + void set_heights_widths_angles(const std::vector& heights_widths_angles); void set_colors(const std::vector& colors); void set_enabled_segments(const std::vector& enabled_segments); void set_enabled_options(const std::vector& enabled_options); @@ -516,8 +541,7 @@ private: // // ORCA: realistic view. Shadow map state set by set_shadow_map(), consumed by the segments - // shader. m_rendering_shadow_casters forces the intensity to 0 for the depth pass, which - // must not sample the very map it is writing. + // shader. m_rendering_shadow_casters switches render_segments() to the depth-only program. // // Defaults past the four texture units render_segments() binds itself, so the sampler never // aliases one of the buffer textures before the owner of the map has said where it lives. @@ -533,6 +557,8 @@ private: // float m_exposure{ 1.0f }; float m_saturation{ 1.0f }; + // ORCA: the light the segments shader shades with, in eye space. + Vec3 m_light_top_dir{ -0.4574957f, 0.4574957f, 0.7624929f }; // ORCA: section view std::array m_clipping_plane{ 0.0f, 0.0f, 0.0f, 1.0f }; @@ -543,6 +569,8 @@ private: void update_heights_widths(); void render_segments(const Mat4x4& view_matrix, const Mat4x4& projection_matrix, const Vec3& camera_position); void render_options(const Mat4x4& view_matrix, const Mat4x4& projection_matrix); + // ORCA: the program using uni must be current. + void set_layer_colors(const LayerColorsUniforms& uni) const; #if VGCODE_ENABLE_COG_AND_TOOL_MARKERS void render_cog_marker(const Mat4x4& view_matrix, const Mat4x4& projection_matrix); void render_tool_marker(const Mat4x4& view_matrix, const Mat4x4& projection_matrix); diff --git a/src/slic3r/CMakeLists.txt b/src/slic3r/CMakeLists.txt index 46c32ff238..b74247e338 100644 --- a/src/slic3r/CMakeLists.txt +++ b/src/slic3r/CMakeLists.txt @@ -503,6 +503,10 @@ set(SLIC3R_GUI_SOURCES GUI/SceneRaycaster.hpp GUI/SceneCache.cpp GUI/SceneCache.hpp + GUI/FrameProfiler.cpp + GUI/FrameProfiler.hpp + GUI/SceneBenchmark.cpp + GUI/SceneBenchmark.hpp GUI/PartSkipCommon.hpp GUI/PartSkipDialog.cpp GUI/PartSkipDialog.hpp diff --git a/src/slic3r/GUI/3DScene.cpp b/src/slic3r/GUI/3DScene.cpp index 0ba85775bd..a46707ff98 100644 --- a/src/slic3r/GUI/3DScene.cpp +++ b/src/slic3r/GUI/3DScene.cpp @@ -1127,8 +1127,17 @@ GLVolumeWithIdAndZList volumes_to_render(const GLVolumePtrs& volumes, GLVolumeCo ); } else if (type == GLVolumeCollection::ERenderType::Opaque && list.size() > 1) { + // Orca: nearest first after the selected ones, so the depth test skips shading hidden surfaces. + for (GLVolumeWithIdAndZ& volume : list) { + volume.second.second = volume.first->transformed_bounding_box().transformed(view_matrix).max(2); + } + std::sort(list.begin(), list.end(), - [](const GLVolumeWithIdAndZ& v1, const GLVolumeWithIdAndZ& v2) -> bool { return v1.first->selected && !v2.first->selected; } + [](const GLVolumeWithIdAndZ& v1, const GLVolumeWithIdAndZ& v2) -> bool { + if (v1.first->selected != v2.first->selected) + return v1.first->selected; + return v1.second.second > v2.second.second; + } ); } diff --git a/src/slic3r/GUI/FrameProfiler.cpp b/src/slic3r/GUI/FrameProfiler.cpp new file mode 100644 index 0000000000..d79a0eef99 --- /dev/null +++ b/src/slic3r/GUI/FrameProfiler.cpp @@ -0,0 +1,153 @@ +#include "libslic3r/libslic3r.h" +#include "FrameProfiler.hpp" + +#include "3DScene.hpp" + +#include + +#include +#include + +namespace Slic3r { +namespace GUI { + +void FrameProfiler::begin_frame() +{ + m_recording = nullptr; + // GL 3.3 or ARB_timer_query + if (glQueryCounter == nullptr) + return; + + collect(false); + Frame& frame = m_frames[m_next]; + // Every frame in flight still waits for the GPU: skip this one rather than stall. + if (frame.pending) + return; + + if (frame.queries[0] == 0) + glsafe(::glGenQueries(GLsizei(frame.queries.size()), frame.queries.data())); + glsafe(::glQueryCounter(frame.queries[0], GL_TIMESTAMP)); + frame.count = 0; + frame.averaged = m_averaging; + m_recording = &frame; + m_last_mark = std::chrono::steady_clock::now(); +} + +void FrameProfiler::mark(const char* name) +{ + if (m_recording == nullptr || m_recording->count == MAX_SECTIONS) + return; + + Frame& frame = *m_recording; + const std::chrono::steady_clock::time_point now = std::chrono::steady_clock::now(); + glsafe(::glQueryCounter(frame.queries[2 * frame.count + 1], GL_TIMESTAMP)); + // Else the GPU time would include the CPU time spent until the driver flushes on its own. + glsafe(::glFlush()); + // Sent with the next section's commands, so the time the GPU waits for them is not billed to it. + if (frame.count + 1 < MAX_SECTIONS) + glsafe(::glQueryCounter(frame.queries[2 * frame.count + 2], GL_TIMESTAMP)); + frame.names[frame.count] = name; + frame.cpu_ms[frame.count] = std::chrono::duration(now - m_last_mark).count(); + ++frame.count; + m_last_mark = now; +} + +void FrameProfiler::end_frame() +{ + if (m_recording == nullptr) + return; + + m_recording->pending = m_recording->count > 0; + m_recording = nullptr; + m_next = (m_next + 1) % FRAMES_IN_FLIGHT; +} + +void FrameProfiler::collect(bool wait) +{ + constexpr double SMOOTHING = 0.1; + + // Oldest first: a frame whose results are not in yet holds back the newer ones too. + for (size_t i = 0; i < FRAMES_IN_FLIGHT; ++i) { + Frame& frame = m_frames[(m_next + i) % FRAMES_IN_FLIGHT]; + if (!frame.pending) + continue; + + if (!wait) { + GLint available = 0; + glsafe(::glGetQueryObjectiv(frame.queries[2 * frame.count - 1], GL_QUERY_RESULT_AVAILABLE, &available)); + if (available == 0) + break; + } + + std::array stamps{}; + for (size_t j = 0; j < 2 * frame.count; ++j) + glsafe(::glGetQueryObjectui64v(frame.queries[j], GL_QUERY_RESULT, &stamps[j])); + frame.pending = false; + + const bool averaged = frame.averaged && m_averaging; + if (averaged) + ++m_averaged_frames; + std::vector
sections; + sections.reserve(frame.count); + for (size_t j = 0; j < frame.count; ++j) { + Section section{ frame.names[j], frame.cpu_ms[j], stamps[2 * j + 1] > stamps[2 * j] ? double(stamps[2 * j + 1] - stamps[2 * j]) * 1e-6 : 0.0 }; + if (averaged) { + auto sum = std::find_if(m_sums.begin(), m_sums.end(), [§ion](const Section& s) { return std::strcmp(s.name, section.name) == 0; }); + if (sum == m_sums.end()) + m_sums.push_back(section); + else { + sum->cpu_ms += section.cpu_ms; + sum->gpu_ms += section.gpu_ms; + } + } + for (const Section& prev : m_sections) { + if (std::strcmp(prev.name, section.name) == 0) { + section.cpu_ms = prev.cpu_ms + SMOOTHING * (section.cpu_ms - prev.cpu_ms); + section.gpu_ms = prev.gpu_ms + SMOOTHING * (section.gpu_ms - prev.gpu_ms); + break; + } + } + sections.push_back(section); + } + m_sections = std::move(sections); + } +} + +void FrameProfiler::start_averaging() +{ + m_averaging = true; + m_sums.clear(); + m_averaged_frames = 0; +} + +std::vector FrameProfiler::finish_averaging(bool wait) +{ + if (wait && glQueryCounter != nullptr) + collect(true); + std::vector
averages = std::move(m_sums); + for (Section& section : averages) { + section.cpu_ms /= double(m_averaged_frames); + section.gpu_ms /= double(m_averaged_frames); + } + m_averaging = false; + m_sums.clear(); + m_averaged_frames = 0; + return averages; +} + +void FrameProfiler::reset() +{ + for (Frame& frame : m_frames) { + if (frame.queries[0] != 0) + glsafe(::glDeleteQueries(GLsizei(frame.queries.size()), frame.queries.data())); + frame = Frame(); + } + m_recording = nullptr; + m_sections.clear(); + m_averaging = false; + m_sums.clear(); + m_averaged_frames = 0; +} + +} // namespace GUI +} // namespace Slic3r diff --git a/src/slic3r/GUI/FrameProfiler.hpp b/src/slic3r/GUI/FrameProfiler.hpp new file mode 100644 index 0000000000..cfda8a4d8a --- /dev/null +++ b/src/slic3r/GUI/FrameProfiler.hpp @@ -0,0 +1,66 @@ +#pragma once + +#include +#include +#include +#include + +namespace Slic3r { +namespace GUI { + +// CPU and GPU time spent in the named sections of a frame, shown under the FPS overlay and averaged +// by the scene benchmark. A section runs from the previous mark to the one naming it; its GPU time +// leaves out any wait for the CPU to send its commands. GPU times are read back a few frames late, +// so profiling never waits on the GPU. +class FrameProfiler +{ +public: + struct Section + { + const char* name{ nullptr }; + double cpu_ms{ 0.0 }; + double gpu_ms{ 0.0 }; + }; + + void begin_frame(); + void mark(const char* name); + void end_frame(); + // The sections of the last frame read back, smoothed over the previous ones. + const std::vector
& sections() const { return m_sections; } + // Averages the sections of the frames begun from now on, until finish_averaging(). + void start_averaging(); + bool is_averaging() const { return m_averaging; } + // With wait, first reads back the averaged frames still on the GPU, with the context current. + std::vector
finish_averaging(bool wait); + // Frees the queries, with the context current. + void reset(); + +private: + static constexpr size_t FRAMES_IN_FLIGHT = 4; + static constexpr size_t MAX_SECTIONS = 32; + + struct Frame + { + // A begin and an end timestamp per section. + std::array queries{}; + std::array names{}; + std::array cpu_ms{}; + size_t count{ 0 }; + bool pending{ false }; + bool averaged{ false }; + }; + + void collect(bool wait); + + std::array m_frames; + Frame* m_recording{ nullptr }; + size_t m_next{ 0 }; + std::chrono::steady_clock::time_point m_last_mark; + std::vector
m_sections; + bool m_averaging{ false }; + std::vector
m_sums; + size_t m_averaged_frames{ 0 }; +}; + +} // namespace GUI +} // namespace Slic3r diff --git a/src/slic3r/GUI/GCodeViewer.cpp b/src/slic3r/GUI/GCodeViewer.cpp index dab9cf145b..c31be41b25 100644 --- a/src/slic3r/GUI/GCodeViewer.cpp +++ b/src/slic3r/GUI/GCodeViewer.cpp @@ -80,6 +80,7 @@ #include #include +#include #include #include #include @@ -1759,6 +1760,26 @@ void GCodeViewer::render_scene(int canvas_width, int canvas_height) m_sequential_view.render_marker(!m_no_render_path, canvas_width, sequential_view_height(canvas_height), m_viewer.get_view_type()); } +size_t GCodeViewer::shadow_casters_signature() const +{ + size_t hash = m_viewer.get_vertices_count(); + const libvgcode::Interval& visible = m_viewer.get_view_visible_range(); + const libvgcode::Interval& layers = m_viewer.get_layers_view_range(); + for (size_t value : { size_t(visible[0]), size_t(visible[1]), size_t(layers[0]), size_t(layers[1]) }) + boost::hash_combine(hash, value); + for (double value : { m_paths_bounding_box.min.x(), m_paths_bounding_box.min.y(), m_paths_bounding_box.min.z(), + m_paths_bounding_box.max.x(), m_paths_bounding_box.max.y(), m_paths_bounding_box.max.z() }) + boost::hash_combine(hash, value); + boost::hash_combine(hash, m_viewer.is_top_layer_only_view_range()); + for (size_t i = 0; i < libvgcode::GCODE_EXTRUSION_ROLES_COUNT; ++i) + boost::hash_combine(hash, m_viewer.is_extrusion_role_visible(libvgcode::EGCodeExtrusionRole(i))); + boost::hash_combine(hash, m_viewer.is_option_visible(libvgcode::EOptionType::Travels)); + boost::hash_combine(hash, m_viewer.is_option_visible(libvgcode::EOptionType::Wipes)); + for (float value : m_clipping_plane) + boost::hash_combine(hash, value); + return hash; +} + void GCodeViewer::render_shadow_casters(const Transform3d& light_view_matrix, const Transform3d& light_projection_matrix, const Vec3d& light_position) { if (!has_data()) @@ -1777,6 +1798,11 @@ void GCodeViewer::set_shadow_map(int texture_unit, const Transform3d& light_view intensity, texel_size); } +void GCodeViewer::set_light_top_dir(const Vec3d& direction) +{ + m_viewer.set_light_top_dir(libvgcode::convert(static_cast(direction.cast()))); +} + void GCodeViewer::set_tone(float exposure, float saturation) { m_viewer.set_tone(exposure, saturation); @@ -1786,9 +1812,10 @@ void GCodeViewer::set_clipping_plane(const ClippingPlane& plane) { // Flipped to match ClippingPlane::distance(). const Vec3f normal = -plane.get_normal().cast(); - m_viewer.set_clipping_plane(plane.is_active() ? + m_clipping_plane = plane.is_active() ? std::array{ normal.x(), normal.y(), normal.z(), float(plane.get_offset()) } : - std::array{ 0.0f, 0.0f, 0.0f, 1.0f }); + std::array{ 0.0f, 0.0f, 0.0f, 1.0f }; + m_viewer.set_clipping_plane(m_clipping_plane); } void GCodeViewer::render_overlay(int canvas_width, int canvas_height, int right_margin) diff --git a/src/slic3r/GUI/GCodeViewer.hpp b/src/slic3r/GUI/GCodeViewer.hpp index 291891ccc2..50bfdee38d 100644 --- a/src/slic3r/GUI/GCodeViewer.hpp +++ b/src/slic3r/GUI/GCodeViewer.hpp @@ -260,6 +260,8 @@ mutable bool m_no_render_path { false }; bool m_is_dark = false; libvgcode::Viewer m_viewer; + // ORCA: section view, as the viewer has it. What it cuts away casts no shadow. + std::array m_clipping_plane{ 0.0f, 0.0f, 0.0f, 1.0f }; bool m_loaded_as_preview{ false }; public: @@ -302,6 +304,7 @@ public: void set_tone(float exposure, float saturation); // ORCA: section view void set_clipping_plane(const ClippingPlane& plane); + void set_light_top_dir(const Vec3d& direction); //BBS // void _render_calibration_thumbnail_internal(ThumbnailData& thumbnail_data, const ThumbnailsParams& thumbnail_params, PartPlateList& partplate_list, OpenGLManager& opengl_manager); // void _render_calibration_thumbnail_framebuffer(ThumbnailData& thumbnail_data, unsigned int w, unsigned int h, const ThumbnailsParams& thumbnail_params, PartPlateList& partplate_list, OpenGLManager& opengl_manager); @@ -324,6 +327,10 @@ public: std::vector get_layers_times() const { return m_viewer.get_layers_estimated_times(); } const std::array &get_layers_z_range() const { return m_viewer.get_layers_view_range(); } + size_t get_vertices_count() const { return m_viewer.get_vertices_count(); } + size_t get_layers_count() const { return m_viewer.get_layers_count(); } + // ORCA: realistic view. Changes whenever the toolpaths casting shadows do. + size_t shadow_casters_signature() const; const SequentialView& get_sequential_view() const { return m_sequential_view; } void update_sequential_view_current(unsigned int first, unsigned int last); diff --git a/src/slic3r/GUI/GLCanvas3D.cpp b/src/slic3r/GUI/GLCanvas3D.cpp index 9462f2e2ed..622568e291 100644 --- a/src/slic3r/GUI/GLCanvas3D.cpp +++ b/src/slic3r/GUI/GLCanvas3D.cpp @@ -1303,6 +1303,7 @@ GLCanvas3D::~GLCanvas3D() { if (_set_current()) { m_scene_cache.reset(); + m_frame_profiler.reset(); if (m_fxaa_texture_id != 0) { glsafe(::glDeleteTextures(1, &m_fxaa_texture_id)); m_fxaa_texture_id = 0; @@ -2166,7 +2167,7 @@ void GLCanvas3D::_render_frame(bool scene_dirty, bool only_init) wxGetApp().imgui()->new_frame(); - if (m_picking_enabled) { + if (m_picking_enabled && !m_benchmarking) { if (m_rectangle_selection.is_dragging()) // picking pass using rectangle selection _rectangular_selection_picking_pass(); @@ -2191,22 +2192,30 @@ void GLCanvas3D::_render_frame(bool scene_dirty, bool only_init) // An overlay-only frame reuses the last scene pass. The overlay is rebuilt either way, and drawn // below once it is known whether the frame differs from the one on screen. const bool reuse_scene = !scene_dirty && _can_reuse_cached_scene(camera); + // Only frames that redraw the scene are profiled. + if (!reuse_scene && (_is_render_timings_enabled() || m_frame_profiler.is_averaging())) + m_frame_profiler.begin_frame(); + Slic3r::ScopeGuard profiler_guard([this]() { m_frame_profiler.end_frame(); }); if (!reuse_scene) { _render_scene(camera, cnv_size); if (!overlay_tick) m_render_stats.increment_scene_fps_counter(); } - if (m_canvas_type == ECanvasType::CanvasPreview && m_render_preview) + if (m_canvas_type == ECanvasType::CanvasPreview && m_render_preview) { // BBS: GUI refactor: add canvas size as parameters _render_gcode_overlay(cnv_size.get_width(), cnv_size.get_height()); + m_frame_profiler.mark("legend"); + } // draw overlays _render_overlays(); + m_frame_profiler.mark("ui"); const int current_fps = m_render_stats.get_fps_and_reset_if_needed(); - if (_is_fps_overlay_enabled()) { + if (_is_fps_overlay_enabled() || _is_render_timings_enabled()) _render_fps_overlay(current_fps); + if (_is_fps_overlay_enabled()) { // The timer requests an overlay-only frame a second from now. A frame it requested // re-arms it only while a count is above zero. if (!overlay_tick || current_fps > 0 || m_render_stats.get_scene_fps() > 0) @@ -2296,12 +2305,14 @@ void GLCanvas3D::_render_frame(bool scene_dirty, bool only_init) wxGetApp().plater()->get_notification_manager()->render_notifications(*this, get_overlay_window_width(), bottom_margin, right_margin); wxGetApp().plater()->get_dailytips()->render(); } + m_frame_profiler.mark("notifications"); ImDrawData* draw_data = wxGetApp().imgui()->end_frame(); std::optional signature; if (_is_frame_skipping_enabled()) signature = _overlay_signature(draw_data); + m_frame_profiler.mark("imgui end"); if (reuse_scene) { // A reused scene under an unchanged overlay is the frame already on screen. @@ -2311,13 +2322,16 @@ void GLCanvas3D::_render_frame(bool scene_dirty, bool only_init) } _render_overlay_toolbars(); + m_frame_profiler.mark("toolbars"); wxGetApp().imgui()->render(draw_data); + m_frame_profiler.mark("imgui draw"); // On Wayland, eglSwapBuffers blocks when the canvas is hidden or // occluded. Skip the swap to avoid stalling the render loop. if (m_canvas->IsShownOnScreen()) { m_canvas->SwapBuffers(); + m_frame_profiler.mark("swap"); if (!overlay_tick) m_render_stats.increment_fps_counter(); m_presented_signature = signature; @@ -2362,20 +2376,25 @@ void GLCanvas3D::_render_scene(const Camera& camera, const Size& cnv_size) // floating would read as a rendering fault rather than a deliberate view option. if (show_bed) _render_bed(camera.get_view_matrix(), camera.get_projection_matrix(), !camera.is_looking_downward(), m_show_world_axes); + m_frame_profiler.mark("bed"); if (show_bed) //BBS: add outline logic _render_platelist(camera.get_view_matrix(), camera.get_projection_matrix(), !camera.is_looking_downward(), only_current, only_body, hover_id, true, show_grid); if (m_axes_at_bed_center && show_bed) // Design tab: replace the plate's corner-origin grid with the origin-centred CAD grid. _render_cad_grid(camera.get_view_matrix(), camera.get_projection_matrix()); + m_frame_profiler.mark("plates"); //BBS: add outline logic // Depth pass for object-on-object and self shadows; consumed by the gouraud shader below. _render_shadows(camera.get_view_matrix(), camera.get_projection_matrix()); + m_frame_profiler.mark("shadows"); _render_objects(GLVolumeCollection::ERenderType::Opaque, !m_gizmos.is_running()); + m_frame_profiler.mark("objects"); _render_sla_slices(); _render_selection(); _render_objects(GLVolumeCollection::ERenderType::Transparent, !m_gizmos.is_running()); _render_wireframe_overlay(); + m_frame_profiler.mark("transparent"); } /* preview render */ else if (m_canvas_type == ECanvasType::CanvasPreview && m_render_preview) { @@ -2384,12 +2403,17 @@ void GLCanvas3D::_render_scene(const Camera& camera, const Size& cnv_size) _render_objects(GLVolumeCollection::ERenderType::Opaque, !m_gizmos.is_running()); _render_sla_slices(); _render_selection(); + m_frame_profiler.mark("objects"); _render_bed(camera.get_view_matrix(), camera.get_projection_matrix(), !camera.is_looking_downward(), m_show_world_axes); + m_frame_profiler.mark("bed"); _render_platelist(camera.get_view_matrix(), camera.get_projection_matrix(), !camera.is_looking_downward(), only_current, true, hover_id); + m_frame_profiler.mark("plates"); // Realistic view: the print casts a shadow onto the plate here as it does in View3D. _render_shadows(camera.get_view_matrix(), camera.get_projection_matrix()); + m_frame_profiler.mark("shadows"); // BBS: GUI refactor: add canvas size as parameters _render_gcode(cnv_size.get_width(), cnv_size.get_height()); + m_frame_profiler.mark("gcode"); } /* assemble render*/ else if (m_canvas_type == ECanvasType::CanvasAssembleView) { @@ -2406,6 +2430,7 @@ void GLCanvas3D::_render_scene(const Camera& camera, const Size& cnv_size) // BBS: add outline logic _render_objects(GLVolumeCollection::ERenderType::Transparent, !m_gizmos.is_running()); _render_wireframe_overlay(); + m_frame_profiler.mark("objects"); } _render_sequential_clearance(); @@ -2428,12 +2453,17 @@ void GLCanvas3D::_render_scene(const Camera& camera, const Size& cnv_size) if (m_picking_enabled && m_rectangle_selection.is_dragging()) m_rectangle_selection.render(*this); + m_frame_profiler.mark("gizmos"); - if (_is_ssao_enabled()) + if (_is_ssao_enabled()) { _render_ssao_pass(static_cast(cnv_size.get_width()), static_cast(cnv_size.get_height())); + m_frame_profiler.mark("ssao"); + } - if (_is_fxaa_enabled()) + if (_is_fxaa_enabled()) { _render_fxaa_pass(static_cast(cnv_size.get_width()), static_cast(cnv_size.get_height())); + m_frame_profiler.mark("fxaa"); + } // Design tab: interactive 2D sketch overlay, drawn over the scene but // beneath the UI overlays (toolbars, labels). @@ -2443,6 +2473,7 @@ void GLCanvas3D::_render_scene(const Camera& camera, const Size& cnv_size) #endif _capture_scene_cache(camera); + m_frame_profiler.mark("cache"); } void GLCanvas3D::render_thumbnail(ThumbnailData & thumbnail_data, @@ -3435,7 +3466,7 @@ void GLCanvas3D::unbind_event_handlers() void GLCanvas3D::on_idle(wxIdleEvent& evt) { - if (!m_initialized) + if (!m_initialized || m_benchmarking) return; // Toolbar states, notifications and ImGui's own layout settling only touch the overlay. @@ -7873,7 +7904,51 @@ int GLCanvas3D::_get_effective_fps_cap() const bool GLCanvas3D::_is_fps_overlay_enabled() const { - return wxGetApp().app_config != nullptr && wxGetApp().app_config->get_bool(SETTING_OPENGL_SHOW_FPS_OVERLAY); + return !m_benchmarking && wxGetApp().app_config != nullptr && wxGetApp().app_config->get_bool(SETTING_OPENGL_SHOW_FPS_OVERLAY); +} + +// Above the front left of the plate. +static const Vec3d STATIC_LIGHT_DIR = Vec3d(-0.4, -0.6, 1.0).normalized(); + +std::optional GLCanvas3D::_static_light_dir_eye() const +{ + if (!_is_realistic_view_enabled() || _shadow_mode() != EShadowMode::Static) + return std::nullopt; + const Matrix3d view_rot = wxGetApp().plater()->get_camera().get_view_matrix().matrix().block<3, 3>(0, 0); + return Vec3d((view_rot * STATIC_LIGHT_DIR).normalized()); +} + +GLCanvas3D::EShadowMode GLCanvas3D::_shadow_mode() const +{ + const std::string mode = wxGetApp().app_config != nullptr ? wxGetApp().app_config->get(SETTING_OPENGL_REALISTIC_SHADOWS) : std::string(); + return mode == "static" ? EShadowMode::Static : mode == "orbit" ? EShadowMode::Orbit : EShadowMode::Off; +} + +size_t GLCanvas3D::_shadow_casters_signature(bool toolpath_casters) const +{ + if (toolpath_casters) + return m_gcode_viewer.shadow_casters_signature(); + + size_t hash = 1; + for (const GLVolume* volume : m_volumes.volumes) { + if (volume == nullptr || !volume->is_active || !volume->printable || volume->is_modifier || volume->is_wipe_tower) + continue; + boost::hash_combine(hash, volume); + boost::hash_combine(hash, volume->model.vertices_count()); + boost::hash_combine(hash, volume->model.indices_count()); + const BoundingBoxf3& bb = volume->model.get_bounding_box(); + for (double value : { bb.min.x(), bb.min.y(), bb.min.z(), bb.max.x(), bb.max.y(), bb.max.z() }) + boost::hash_combine(hash, value); + const Transform3d world = volume->world_matrix(); + for (int i = 0; i < 16; ++i) + boost::hash_combine(hash, world.matrix().data()[i]); + } + return hash; +} + +bool GLCanvas3D::_is_render_timings_enabled() const +{ + return !m_benchmarking && wxGetApp().app_config != nullptr && wxGetApp().app_config->get_bool(SETTING_OPENGL_SHOW_RENDER_TIMINGS); } bool GLCanvas3D::_is_scene_cache_enabled() const @@ -7908,14 +7983,22 @@ bool GLCanvas3D::_is_frame_skipping_enabled() const void GLCanvas3D::_render_fps_overlay(int fps) const { - if (fps < 0) + const bool show_fps = _is_fps_overlay_enabled() && fps >= 0; + const std::vector& sections = m_frame_profiler.sections(); + const bool show_timings = _is_render_timings_enabled() && !sections.empty(); + if (!show_fps && !show_timings) return; ImGuiWrapper& imgui = *wxGetApp().imgui(); const float margin = 10.0f * get_scale(); const ImVec2 display_size = ImGui::GetIO().DisplaySize; - ImGui::SetNextWindowPos(ImVec2(display_size.x - margin, margin), ImGuiCond_Always, ImVec2(1.0f, 0.0f)); + ImVec2 pos(display_size.x - margin, margin); + // The Preview legend takes the top-right corner. + if (const ImGuiWindow* legend = ImGui::FindWindowByName("Legend"); m_canvas_type == ECanvasType::CanvasPreview && legend != nullptr && legend->Active) + pos = ImVec2(legend->Pos.x - margin, legend->Pos.y); + ImGui::SetNextWindowPos(pos, ImGuiCond_Always, ImVec2(1.0f, 0.0f)); ImGui::SetNextWindowBgAlpha(0.35f); + ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 8.0f * get_scale()); imgui.begin( std::string("###fps_overlay"), ImGuiWindowFlags_AlwaysAutoResize | @@ -7925,10 +8008,38 @@ void GLCanvas3D::_render_fps_overlay(int fps) const ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoSavedSettings | ImGuiWindowFlags_NoInputs); - imgui.text(std::string("FPS: ") + std::to_string(fps)); - // The subset of those frames that redrew the scene rather than reusing the cached one. - imgui.text(std::string("3D: ") + std::to_string(m_render_stats.get_scene_fps())); + if (show_fps) { + imgui.text(std::string("FPS: ") + std::to_string(fps)); + ImGui::SameLine(); + // The subset of those frames that redrew the scene rather than reusing the cached one. + imgui.text(std::string("3D: ") + std::to_string(m_render_stats.get_scene_fps())); + } + if (show_timings && ImGui::BeginTable("frame_sections", 3, ImGuiTableFlags_SizingFixedFit)) { + ImGui::TableSetupColumn("ms"); + ImGui::TableSetupColumn("CPU"); + ImGui::TableSetupColumn("GPU"); + ImGui::TableHeadersRow(); + double cpu_ms = 0.0; + double gpu_ms = 0.0; + auto row = [](const char* name, double cpu, double gpu) { + ImGui::TableNextRow(); + ImGui::TableSetColumnIndex(0); + ImGui::TextUnformatted(name); + ImGui::TableSetColumnIndex(1); + ImGui::Text("%.2f", cpu); + ImGui::TableSetColumnIndex(2); + ImGui::Text("%.2f", gpu); + }; + for (const FrameProfiler::Section& section : sections) { + row(section.name, section.cpu_ms, section.gpu_ms); + cpu_ms += section.cpu_ms; + gpu_ms += section.gpu_ms; + } + row("total", cpu_ms, gpu_ms); + ImGui::EndTable(); + } imgui.end(); + ImGui::PopStyleVar(); } void GLCanvas3D::_render_fxaa_pass(unsigned int width, unsigned int height) @@ -8278,7 +8389,8 @@ void GLCanvas3D::_render_shadows(const Transform3d& view_matrix, const Transform return; if (!_is_realistic_view_enabled()) return; - if (!wxGetApp().app_config->get_bool(SETTING_OPENGL_PHONG_BASIC_PLATE_SHADOWS)) + const EShadowMode mode = _shadow_mode(); + if (mode == EShadowMode::Off) return; // The preview canvas holds no volumes of its own for FFF. Once slicing has run its printed @@ -8293,10 +8405,11 @@ void GLCanvas3D::_render_shadows(const Transform3d& view_matrix, const Transform if (OpenGLManager::get_framebuffers_type() == OpenGLManager::EFramebufferType::Arb) { - // Light direction (same as used in shading and plate shading) + // Orbit: the light used for shading, fixed to the camera. Static: a world light, also used for shading. const Vec3d light_dir_eye = Vec3d(-0.4574957, 0.4574957, 0.7624929).normalized(); const Matrix3d view_rot = view_matrix.matrix().block<3, 3>(0, 0); - const Vec3d dir_to_light = (view_rot.transpose() * light_dir_eye).normalized(); + const Vec3d dir_to_light = mode == EShadowMode::Static ? STATIC_LIGHT_DIR : + Vec3d((view_rot.transpose() * light_dir_eye).normalized()); // Bounding box of the printable objects (the shadow casters). BoundingBoxf3 obj_bb; @@ -8348,11 +8461,19 @@ void GLCanvas3D::_render_shadows(const Transform3d& view_matrix, const Transform lmin = lmin.cwiseMin(lp); lmax = lmax.cwiseMax(lp); }; + // The plate area a shadow can reach: the casters' footprint and where their corners land on z = 0. + Vec2d reach_min(DBL_MAX, DBL_MAX); + Vec2d reach_max(-DBL_MAX, -DBL_MAX); + auto reach = [&](const Vec3d& p) { + reach_min = reach_min.cwiseMin(Vec2d(p.x(), p.y())); + reach_max = reach_max.cwiseMax(Vec2d(p.x(), p.y())); + }; for (int i = 0; i < 8; ++i) { const Vec3d corner((i & 1) ? obj_bb.max.x() : obj_bb.min.x(), (i & 2) ? obj_bb.max.y() : obj_bb.min.y(), (i & 4) ? obj_bb.max.z() : obj_bb.min.z()); enclose(corner); + reach(corner); // Where this corner's shadow lands on z = 0, clamped to the plate so a grazing angle // (t -> infinity) stays bounded. if (ray_dir.z() < -1e-6) { @@ -8362,6 +8483,7 @@ void GLCanvas3D::_render_shadows(const Transform3d& view_matrix, const Transform s.y() = std::min(std::max(s.y(), plate_bb.min.y()), plate_bb.max.y()); s.z() = 0.0; enclose(s); + reach(s); } } @@ -8395,6 +8517,11 @@ void GLCanvas3D::_render_shadows(const Transform3d& view_matrix, const Transform // Create / resize the depth texture and FBO const unsigned int size = 2048; + + // Padded by the filter's 2 texel reach, stretched where the light grazes the plate. + const double reach_margin = 3.0 * 2.0 * std::max(halfx, halfy) / size / std::max(0.1, std::abs(dir_to_light.z())); + m_shadow_plate_bounds = { float(reach_min.x() - reach_margin), float(reach_min.y() - reach_margin), + float(reach_max.x() + reach_margin), float(reach_max.y() + reach_margin) }; if (m_shadow_map_texture_id == 0) { glsafe(::glGenTextures(1, &m_shadow_map_texture_id)); glsafe(::glBindTexture(GL_TEXTURE_2D, m_shadow_map_texture_id)); @@ -8414,66 +8541,80 @@ void GLCanvas3D::_render_shadows(const Transform3d& view_matrix, const Transform if (m_shadow_map_fbo == 0) glsafe(::glGenFramebuffers(1, &m_shadow_map_fbo)); - // Save OpenGL state that we will modify - GLint prev_viewport[4] = { 0, 0, 0, 0 }; - glsafe(::glGetIntegerv(GL_VIEWPORT, prev_viewport)); - GLint prev_fbo = 0; - glsafe(::glGetIntegerv(GL_FRAMEBUFFER_BINDING, &prev_fbo)); - GLboolean prev_color_mask[4] = { GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE }; - glsafe(::glGetBooleanv(GL_COLOR_WRITEMASK, prev_color_mask)); - const GLboolean prev_cull = ::glIsEnabled(GL_CULL_FACE); - GLint prev_depth_func = GL_LESS; - glsafe(::glGetIntegerv(GL_DEPTH_FUNC, &prev_depth_func)); - GLboolean prev_depth_mask = GL_TRUE; - glsafe(::glGetBooleanv(GL_DEPTH_WRITEMASK, &prev_depth_mask)); - glsafe(::glBindFramebuffer(GL_FRAMEBUFFER, m_shadow_map_fbo)); - glsafe(::glFramebufferTexture2D(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_TEXTURE_2D, m_shadow_map_texture_id, 0)); - glsafe(::glDrawBuffer(GL_NONE)); - glsafe(::glReadBuffer(GL_NONE)); - - if (::glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) { - glsafe(::glBindFramebuffer(GL_FRAMEBUFFER, static_cast(prev_fbo))); - m_shadow_map_valid = false; - } else { - glsafe(::glViewport(0, 0, size, size)); - glsafe(::glColorMask(GL_FALSE, GL_FALSE, GL_FALSE, GL_FALSE)); - glsafe(::glEnable(GL_DEPTH_TEST)); - glsafe(::glDepthMask(GL_TRUE)); - glsafe(::glDepthFunc(GL_LESS)); - glsafe(::glClear(GL_DEPTH_BUFFER_BIT)); - glsafe(::glEnable(GL_POLYGON_OFFSET_FILL)); - glsafe(::glPolygonOffset(4.0f, 4.0f)); - glsafe(::glDisable(GL_CULL_FACE)); - - if (toolpath_casters) - m_gcode_viewer.render_shadow_casters(Transform3d(light_view), Transform3d(light_proj), eye); - // Only this branch draws through "flat"; the toolpaths bring their own program. - else if (GLShaderProgram* shader = wxGetApp().get_shader("flat"); shader != nullptr) { - shader->start_using(); - shader->set_uniform("projection_matrix", Transform3d(light_proj)); - for (GLVolume* volume : m_volumes.volumes) { - if (volume == nullptr || !volume->is_active || !volume->printable || volume->is_modifier || volume->is_wipe_tower) - continue; - const Transform3d view_model = Transform3d(light_view) * volume->world_matrix(); - shader->set_uniform("view_model_matrix", view_model); - volume->model.render(shader); - } - shader->stop_using(); - } - - // Restore state - glsafe(::glDisable(GL_POLYGON_OFFSET_FILL)); - glsafe(::glColorMask(prev_color_mask[0], prev_color_mask[1], prev_color_mask[2], prev_color_mask[3])); - if (prev_cull) - glsafe(::glEnable(GL_CULL_FACE)); - else - glsafe(::glDisable(GL_CULL_FACE)); - glsafe(::glDepthFunc(prev_depth_func)); - glsafe(::glDepthMask(prev_depth_mask)); - glsafe(::glBindFramebuffer(GL_FRAMEBUFFER, static_cast(prev_fbo))); - glsafe(::glViewport(prev_viewport[0], prev_viewport[1], prev_viewport[2], prev_viewport[3])); - + // A static light keeps the map until its frustum or the casters change. + size_t cache_key = 0; + if (mode == EShadowMode::Static) { + cache_key = _shadow_casters_signature(toolpath_casters); + for (int i = 0; i < 16; ++i) + boost::hash_combine(cache_key, m_shadow_light_vp.matrix().data()[i]); + } + if (cache_key != 0 && cache_key == m_shadow_map_key) m_shadow_map_valid = true; + else { + m_shadow_map_key = 0; + // Save OpenGL state that we will modify + GLint prev_viewport[4] = { 0, 0, 0, 0 }; + glsafe(::glGetIntegerv(GL_VIEWPORT, prev_viewport)); + GLint prev_fbo = 0; + glsafe(::glGetIntegerv(GL_FRAMEBUFFER_BINDING, &prev_fbo)); + GLboolean prev_color_mask[4] = { GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE }; + glsafe(::glGetBooleanv(GL_COLOR_WRITEMASK, prev_color_mask)); + const GLboolean prev_cull = ::glIsEnabled(GL_CULL_FACE); + GLint prev_depth_func = GL_LESS; + glsafe(::glGetIntegerv(GL_DEPTH_FUNC, &prev_depth_func)); + GLboolean prev_depth_mask = GL_TRUE; + glsafe(::glGetBooleanv(GL_DEPTH_WRITEMASK, &prev_depth_mask)); + glsafe(::glBindFramebuffer(GL_FRAMEBUFFER, m_shadow_map_fbo)); + glsafe(::glFramebufferTexture2D(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_TEXTURE_2D, m_shadow_map_texture_id, 0)); + glsafe(::glDrawBuffer(GL_NONE)); + glsafe(::glReadBuffer(GL_NONE)); + + if (::glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) { + glsafe(::glBindFramebuffer(GL_FRAMEBUFFER, static_cast(prev_fbo))); + m_shadow_map_valid = false; + } else { + glsafe(::glViewport(0, 0, size, size)); + glsafe(::glColorMask(GL_FALSE, GL_FALSE, GL_FALSE, GL_FALSE)); + glsafe(::glEnable(GL_DEPTH_TEST)); + glsafe(::glDepthMask(GL_TRUE)); + glsafe(::glDepthFunc(GL_LESS)); + glsafe(::glClear(GL_DEPTH_BUFFER_BIT)); + glsafe(::glEnable(GL_POLYGON_OFFSET_FILL)); + glsafe(::glPolygonOffset(4.0f, 4.0f)); + glsafe(::glDisable(GL_CULL_FACE)); + + if (toolpath_casters) + m_gcode_viewer.render_shadow_casters(Transform3d(light_view), Transform3d(light_proj), eye); + // Only this branch draws through "flat"; the toolpaths bring their own program. + else if (GLShaderProgram* shader = wxGetApp().get_shader("flat"); shader != nullptr) { + shader->start_using(); + shader->set_uniform("projection_matrix", Transform3d(light_proj)); + for (GLVolume* volume : m_volumes.volumes) { + if (volume == nullptr || !volume->is_active || !volume->printable || volume->is_modifier || volume->is_wipe_tower) + continue; + const Transform3d view_model = Transform3d(light_view) * volume->world_matrix(); + shader->set_uniform("view_model_matrix", view_model); + volume->model.render(shader); + } + shader->stop_using(); + } + + // Restore state + glsafe(::glDisable(GL_POLYGON_OFFSET_FILL)); + glsafe(::glColorMask(prev_color_mask[0], prev_color_mask[1], prev_color_mask[2], prev_color_mask[3])); + if (prev_cull) + glsafe(::glEnable(GL_CULL_FACE)); + else + glsafe(::glDisable(GL_CULL_FACE)); + glsafe(::glDepthFunc(prev_depth_func)); + glsafe(::glDepthMask(prev_depth_mask)); + glsafe(::glBindFramebuffer(GL_FRAMEBUFFER, static_cast(prev_fbo))); + glsafe(::glViewport(prev_viewport[0], prev_viewport[1], prev_viewport[2], prev_viewport[3])); + + m_shadow_map_valid = true; + } + if (m_shadow_map_valid) + m_shadow_map_key = cache_key; } } else { m_shadow_map_valid = false; @@ -8573,6 +8714,7 @@ void GLCanvas3D::_render_shadows(const Transform3d& view_matrix, const Transform plate_shader->set_uniform("shadow_light_vp", m_shadow_light_vp); plate_shader->set_uniform("shadow_intensity", 0.35f); plate_shader->set_uniform("shadow_map_texel", 1.0f / static_cast(m_shadow_map_size)); + plate_shader->set_uniform("shadow_bounds", m_shadow_plate_bounds); m_plate_shadow_mask.render(plate_shader); plate_shader->stop_using(); @@ -8697,6 +8839,10 @@ void GLCanvas3D::_render_objects(GLVolumeCollection::ERenderType type, bool with } else shader->set_uniform("shadow_intensity", 0.0f); + const std::optional static_light = _static_light_dir_eye(); + shader->set_uniform("use_static_light", static_light.has_value()); + if (static_light.has_value()) + shader->set_uniform("static_light_dir", *static_light); const Size& cvn_size = get_canvas_size(); { @@ -8789,6 +8935,7 @@ void GLCanvas3D::_render_objects(GLVolumeCollection::ERenderType type, bool with glsafe(::glBindTexture(GL_TEXTURE_2D, 0)); glsafe(::glActiveTexture(GL_TEXTURE0)); } + shader->set_uniform("use_static_light", false); shader->stop_using(); } @@ -8938,8 +9085,9 @@ void GLCanvas3D::_render_gcode(int canvas_width, int canvas_height) // again - realistic view with at least one lossy pass on - else the lift would just clip. const AppConfig* cfg = wxGetApp().app_config; const bool lossy_passes = cfg != nullptr && _is_realistic_view_enabled() && - (cfg->get_bool(SETTING_OPENGL_PHONG_BASIC_PLATE_SHADOWS) || cfg->get_bool(SETTING_OPENGL_PHONG_SSAO)); + (_shadow_mode() != EShadowMode::Off || cfg->get_bool(SETTING_OPENGL_PHONG_SSAO)); m_gcode_viewer.set_tone(lossy_passes ? 1.1f : 1.0f, 1.15f); + m_gcode_viewer.set_light_top_dir(_static_light_dir_eye().value_or(Vec3d(-0.4574957, 0.4574957, 0.7624929))); m_gcode_viewer.render_scene(canvas_width, canvas_height); diff --git a/src/slic3r/GUI/GLCanvas3D.hpp b/src/slic3r/GUI/GLCanvas3D.hpp index 9dffe195c4..9bb2d68b42 100644 --- a/src/slic3r/GUI/GLCanvas3D.hpp +++ b/src/slic3r/GUI/GLCanvas3D.hpp @@ -37,6 +37,7 @@ #include "Camera.hpp" #include "SceneRaycaster.hpp" #include "SceneCache.hpp" +#include "FrameProfiler.hpp" #include "IMToolbar.hpp" #include "slic3r/GUI/3DBed.hpp" #include "libslic3r/Slicing.hpp" @@ -714,6 +715,8 @@ private: bool m_reload_delayed; RenderStats m_render_stats; + FrameProfiler m_frame_profiler; + bool m_benchmarking{ false }; std::chrono::time_point m_last_frame_start_time{ std::chrono::steady_clock::now() }; int m_imgui_undo_redo_hovered_pos{ -1 }; @@ -846,6 +849,10 @@ public: unsigned int m_shadow_map_size{ 0 }; Transform3d m_shadow_light_vp{ Transform3d::Identity() }; bool m_shadow_map_valid{ false }; + // Casters and light frustum the map was last rendered for, under a static light. 0 when none. + size_t m_shadow_map_key{ 0 }; + // Plate rectangle a shadow can reach, min xy then max xy. + std::array m_shadow_plate_bounds{ { 0.0f, 0.0f, 0.0f, 0.0f } }; public: explicit GLCanvas3D(wxGLCanvas* canvas, Bed3D &bed); ~GLCanvas3D(); @@ -1272,6 +1279,10 @@ public: void schedule_extra_frame(int milliseconds); + // The scene benchmark draws every frame itself, without picking or the FPS and timings overlays. + void set_benchmarking(bool benchmarking) { m_benchmarking = benchmarking; } + FrameProfiler& get_frame_profiler() { return m_frame_profiler; } + int get_main_toolbar_item_id(const std::string& name) const { return m_main_toolbar.get_item_id(name); } void force_main_toolbar_left_action(int item_id) { m_main_toolbar.force_left_action(item_id, *this); } void force_main_toolbar_right_action(int item_id) { m_main_toolbar.force_right_action(item_id, *this); } @@ -1385,6 +1396,12 @@ private: bool _is_ssao_enabled() const; int _get_effective_fps_cap() const; bool _is_fps_overlay_enabled() const; + bool _is_render_timings_enabled() const; + enum class EShadowMode { Off, Static, Orbit }; + EShadowMode _shadow_mode() const; + // Direction to the static shadow light in eye space, when it lights the scene. + std::optional _static_light_dir_eye() const; + size_t _shadow_casters_signature(bool toolpath_casters) const; bool _is_scene_cache_enabled() const; bool _is_scene_cacheable() const; bool _is_frame_skipping_enabled() const; diff --git a/src/slic3r/GUI/GUI_App.cpp b/src/slic3r/GUI/GUI_App.cpp index 7eaa0aad85..6bd0797ac9 100644 --- a/src/slic3r/GUI/GUI_App.cpp +++ b/src/slic3r/GUI/GUI_App.cpp @@ -8631,8 +8631,8 @@ void GUI_App::open_preferences(PreferencesTab tab, const std::string& highlight_ { // Render settings the canvas reads every frame; a change needs one redraw to show. static constexpr const char* opengl_render_setting_keys[] = { - SETTING_OPENGL_FXAA_ENABLED, SETTING_OPENGL_FPS_CAP, SETTING_OPENGL_SHOW_FPS_OVERLAY, SETTING_OPENGL_SCENE_CACHE, - SETTING_OPENGL_SKIP_IDENTICAL_FRAMES + SETTING_OPENGL_FXAA_ENABLED, SETTING_OPENGL_FPS_CAP, SETTING_OPENGL_SHOW_FPS_OVERLAY, SETTING_OPENGL_SHOW_RENDER_TIMINGS, + SETTING_OPENGL_SCENE_CACHE, SETTING_OPENGL_SKIP_IDENTICAL_FRAMES, SETTING_OPENGL_REALISTIC_SHADOWS }; std::vector previous_opengl_render_settings; for (const char* key : opengl_render_setting_keys) diff --git a/src/slic3r/GUI/MainFrame.cpp b/src/slic3r/GUI/MainFrame.cpp index f8f1fcc199..53507edfb3 100644 --- a/src/slic3r/GUI/MainFrame.cpp +++ b/src/slic3r/GUI/MainFrame.cpp @@ -117,6 +117,7 @@ #include "NotificationManager.hpp" #include "MarkdownTip.hpp" #include "NetworkTestDialog.hpp" +#include "SceneBenchmark.hpp" #include "ConfigWizard.hpp" #include "Widgets/WebView.hpp" #include "DailyTips.hpp" @@ -2786,6 +2787,10 @@ wxMenu* MainFrame::generate_help_menu() dlg.ShowModal(); }); + if (wxGetApp().is_editor()) + append_menu_item(helpMenu, wxID_ANY, _L("Benchmark 3D Scene"), _L("Measure how fast the 3D scene renders in Prepare and Preview"), + [](wxCommandEvent&) { run_scene_benchmark(); }); + helpMenu->AppendSeparator(); append_menu_item(helpMenu, wxID_ANY, _L("Show Tip of the Day"), _L("Show Tip of the Day"), [](wxCommandEvent&) { diff --git a/src/slic3r/GUI/NativeCommands.cpp b/src/slic3r/GUI/NativeCommands.cpp index 1725c94fcc..cd983d7e47 100644 --- a/src/slic3r/GUI/NativeCommands.cpp +++ b/src/slic3r/GUI/NativeCommands.cpp @@ -17,6 +17,7 @@ #include "Plater.hpp" #include "PluginsDialog.hpp" #include "PlateSettingsDialog.hpp" +#include "SceneBenchmark.hpp" #include "DeviceCore/DevManager.h" #include "libslic3r/Config.hpp" @@ -591,6 +592,10 @@ std::vector build_command_catalog() dlg.ShowModal(); return AppActionRunResult{AppActionRunResult::Level::Success}; }); + add("help_benchmark_3d_scene", _u8L("Benchmark 3D Scene"), _u8L("Help"), [](const std::string&) { + run_scene_benchmark(); + return AppActionRunResult{AppActionRunResult::Level::Success}; + }); add_with_icon("help_tip_of_the_day", _u8L("Show Tip of the Day"), _u8L("Help"), "info", [](const std::string&) { if (Plater* plater = wxGetApp().plater()) { plater->get_dailytips()->open(); diff --git a/src/slic3r/GUI/Preferences.cpp b/src/slic3r/GUI/Preferences.cpp index 61ce51112e..6575465198 100644 --- a/src/slic3r/GUI/Preferences.cpp +++ b/src/slic3r/GUI/Preferences.cpp @@ -65,6 +65,7 @@ #include #include #include "NetworkTestDialog.hpp" +#include "SceneBenchmark.hpp" #include "Widgets/StaticLine.hpp" #include "Widgets/RadioGroup.hpp" #include "Shortcuts.hpp" @@ -1999,11 +2000,14 @@ void PreferencesDialog::create_items() ); g_sizer->Add(item_realistic_ssao); - auto item_realistic_shadows = create_item_checkbox( + std::vector ShadowsLabels = { _L("Off"), _L("Static"), _L("Orbit") }; + std::vector ShadowsValues = { "off", "static", "orbit" }; + auto item_realistic_shadows = create_item_combobox( _L("Shadows"), - _L("Renders cast shadows on the plate, other objects, and each object onto itself in realistic view."), - SETTING_OPENGL_PHONG_BASIC_PLATE_SHADOWS - ); + _L("Renders cast shadows on the plate, other objects, and each object onto itself in realistic view.\n" + "Static: the light stays fixed in the world, so the shadows are only recomputed when the scene changes.\n" + "Orbit: the light turns with the camera, recomputing the shadows every frame the camera moves."), + SETTING_OPENGL_REALISTIC_SHADOWS, ShadowsLabels, ShadowsValues); g_sizer->Add(item_realistic_shadows); //// GRAPHICS > Anti-aliasing @@ -2076,6 +2080,26 @@ void PreferencesDialog::create_items() ); g_sizer->Add(item_fps_overlay); + auto item_render_timings = create_item_checkbox( + _L("Show render timings"), + _L("Displays how many milliseconds each part of a frame that redraws the 3D scene takes, in the top-right corner of the viewport.") + "\n" + + _L("CPU: time spent issuing the drawing commands.") + "\n" + + _L("GPU: time the graphics card spent running them.") + "\n" + + _L("Adds a small overhead to each frame while enabled."), + SETTING_OPENGL_SHOW_RENDER_TIMINGS + ); + g_sizer->Add(item_render_timings); + + if (wxGetApp().is_editor()) { + auto item_benchmark = create_item_button(_L("3D scene benchmark"), _L("Run") + " " + dots, "", + _L("Replaces the current project with the OrcaSliced Combo, then measures the frame rate and render timings while the camera turns around it in Prepare and Preview, and while the layer slider moves through the sliced layers."), + [this]() { + EndModal(wxID_OK); + wxGetApp().CallAfter([] { run_scene_benchmark(); }); + }); + g_sizer->Add(item_benchmark); + } + //// GRAPHICS > G-code Preview g_sizer->Add(create_item_title(_L("G-code Preview")), 1, wxEXPAND); diff --git a/src/slic3r/GUI/SceneBenchmark.cpp b/src/slic3r/GUI/SceneBenchmark.cpp new file mode 100644 index 0000000000..d4609d1f1f --- /dev/null +++ b/src/slic3r/GUI/SceneBenchmark.cpp @@ -0,0 +1,555 @@ +#include "SceneBenchmark.hpp" + +#include "3DScene.hpp" +#include "BuildCommit.hpp" +#include "Camera.hpp" +#include "GLCanvas3D.hpp" +#include "GUI.hpp" +#include "GUI_App.hpp" +#include "GUI_Factories.hpp" +#include "I18N.hpp" +#include "IMSlider.hpp" +#include "MainFrame.hpp" +#include "MsgDialog.hpp" +#include "NotificationManager.hpp" +#include "OpenGLManager.hpp" +#include "Plater.hpp" +#include "Widgets/DialogButtons.hpp" +#include "Widgets/Label.hpp" +#include "Widgets/ProgressBar.hpp" +#include "Widgets/StateColor.hpp" + +#include "libslic3r/AppConfig.hpp" +#include "libslic3r/libslic3r.h" + +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace Slic3r { +namespace GUI { + +FrameTimeStats frame_time_stats(std::vector frame_ms) +{ + FrameTimeStats stats; + if (frame_ms.empty()) + return stats; + + std::sort(frame_ms.begin(), frame_ms.end()); + const double total_ms = std::accumulate(frame_ms.begin(), frame_ms.end(), 0.0); + auto percentile = [&frame_ms](double p) { + const size_t rank = size_t(std::ceil(p * double(frame_ms.size()) / 100.0)); + return frame_ms[std::clamp(rank, 1, frame_ms.size()) - 1]; + }; + stats.fps = total_ms > 0.0 ? 1000.0 * double(frame_ms.size()) / total_ms : 0.0; + stats.average_ms = total_ms / double(frame_ms.size()); + stats.median_ms = percentile(50.0); + stats.p95_ms = percentile(95.0); + stats.p99_ms = percentile(99.0); + stats.max_ms = frame_ms.back(); + return stats; +} + +namespace { + +constexpr size_t WARMUP_FRAMES = 30; +// A scene runs the camera path twice: timing the frames, then averaging the render timings. +constexpr size_t PASS_FRAMES = 360; +constexpr size_t SCENE_FRAMES = WARMUP_FRAMES + 2 * PASS_FRAMES; + +struct SceneResult +{ + std::string name; + std::vector frame_ms; + std::vector sections; +}; + +class SceneBenchmarkDialog : public DPIDialog +{ +public: + SceneBenchmarkDialog(); + ~SceneBenchmarkDialog() override; + + bool is_running() const { return m_stage != Stage::Done; } + +protected: + void on_dpi_changed(const wxRect& suggested_rect) override; + +private: + enum class Stage { Loading, Prepare, Slicing, Preview, Layers, Done }; + + void on_timer(wxTimerEvent& evt); + void on_idle(wxIdleEvent& evt); + void on_first_frame(); + void begin_scene(GLCanvas3D* canvas, Stage stage); + void end_scene(); + void stop_scene(); + void start_slicing(); + void set_camera(size_t pass_frame); + void set_top_layer(size_t pass_frame); + void set_status(const wxString& status); + void finish(const wxString& error = wxString()); + void show_report(const wxString& error); + std::string report() const; + + Stage m_stage{ Stage::Loading }; + std::unique_ptr m_disabler; + wxTimer m_timer; + int m_polls{ 0 }; + Label* m_status{ nullptr }; + ProgressBar* m_progress{ nullptr }; + + GLCanvas3D* m_canvas{ nullptr }; + Camera m_saved_camera; + Vec3d m_target{ Vec3d::Zero() }; + double m_zoom{ 1.0 }; + size_t m_frame{ 0 }; + std::chrono::steady_clock::time_point m_last_frame; + // The layer slider's top before the Layers scene moved it, -1 outside that scene. + int m_saved_top_layer{ -1 }; + int m_swap_interval{ wxGLCanvas::DefaultSwapInterval }; + bool m_restore_swap_interval{ false }; + std::vector m_results; + + // Read on the first frame, with the context current. + std::string m_vsync; + int m_msaa_samples{ 0 }; + int m_viewport_width{ 0 }; + int m_viewport_height{ 0 }; + std::string m_camera_type; +}; + +SceneBenchmarkDialog* s_benchmark = nullptr; + +SceneBenchmarkDialog::SceneBenchmarkDialog() + : DPIDialog(static_cast(wxGetApp().mainframe), wxID_ANY, _L("Benchmark 3D Scene"), wxDefaultPosition, wxDefaultSize, + wxCAPTION | wxCLOSE_BOX) + , m_timer(this) +{ + s_benchmark = this; + SetBackgroundColour(*wxWHITE); + SetFont(Label::Body_14); + + auto* sizer = new wxBoxSizer(wxVERTICAL); + sizer->SetMinSize(wxSize(FromDIP(360), -1)); + m_status = new Label(this, _L("Loading the benchmark model") + dots); + m_progress = new ProgressBar(this, wxID_ANY, 100); + m_progress->SetHeight(FromDIP(8)); + m_progress->SetMaxSize(wxSize(-1, FromDIP(8))); + m_progress->SetProgressForedColour(StateColor::darkModeColorFor(wxColour("#DFDFDF"))); + sizer->Add(m_status, 0, wxEXPAND | wxLEFT | wxRIGHT | wxTOP, FromDIP(15)); + sizer->Add(m_progress, 0, wxEXPAND | wxALL, FromDIP(15)); + auto* buttons = new DialogButtons(this, {"Cancel"}); + buttons->GetCANCEL()->Bind(wxEVT_BUTTON, [this](wxCommandEvent&) { Close(); }); + sizer->Add(buttons, 0, wxEXPAND); + SetSizerAndFit(sizer); + wxGetApp().UpdateDlgDarkUI(this); + + Bind(wxEVT_TIMER, &SceneBenchmarkDialog::on_timer, this); + Bind(wxEVT_IDLE, &SceneBenchmarkDialog::on_idle, this); + Bind(wxEVT_CHAR_HOOK, [this](wxKeyEvent& evt) { + if (evt.GetKeyCode() == WXK_ESCAPE) + Close(); + else + evt.Skip(); + }); + Bind(wxEVT_CLOSE_WINDOW, [this](wxCloseEvent&) { + m_timer.Stop(); + stop_scene(); + m_disabler.reset(); + Destroy(); + }); + + // A corner of the 3D view, away from the model. + const wxRect view = wxGetApp().plater()->get_current_canvas3D()->get_wxglcanvas()->GetScreenRect(); + SetPosition(wxPoint(view.GetLeft() + FromDIP(20), view.GetBottom() - GetSize().GetHeight() - FromDIP(20))); + + m_disabler = std::make_unique(this); + m_timer.Start(100); +} + +SceneBenchmarkDialog::~SceneBenchmarkDialog() +{ + // At shutdown the windows it disabled may be gone already. + if (wxGetApp().is_closing()) + (void) m_disabler.release(); + if (s_benchmark == this) + s_benchmark = nullptr; +} + +void SceneBenchmarkDialog::on_dpi_changed(const wxRect&) +{ + Fit(); + Refresh(); +} + +void SceneBenchmarkDialog::on_timer(wxTimerEvent&) +{ + Plater* plater = wxGetApp().plater(); + if (wxGetApp().is_closing() || plater == nullptr) { + m_timer.Stop(); + return; + } + + if (m_stage == Stage::Loading) { + if (!plater->get_ui_job_worker().is_idle()) + return; + m_timer.Stop(); + GLCanvas3D* canvas = plater->get_view3D_canvas3D(); + const BoundingBoxf3 box = canvas->volumes_bounding_box(true); + if (!box.defined) { + finish(_L("The benchmark model could not be loaded.")); + return; + } + m_target = box.center(); + const Size size = canvas->get_canvas_size(); + m_zoom = double(std::min(size.get_width(), size.get_height())) / (1.1 * box.size().norm()); + plater->deselect_all(); + begin_scene(canvas, Stage::Prepare); + } else if (m_stage == Stage::Slicing) { + if (plater->is_background_process_slicing()) + return; + GLCanvas3D* canvas = plater->get_preview_canvas3D(); + if (canvas->get_gcode_viewer().has_data() && canvas->get_wxglcanvas()->IsShownOnScreen()) { + m_timer.Stop(); + plater->get_notification_manager()->set_slicing_progress_hidden(); + begin_scene(canvas, Stage::Preview); + } else if (++m_polls > 30) + finish(_L("Slicing failed, so only Prepare was measured.")); + } +} + +void SceneBenchmarkDialog::on_idle(wxIdleEvent& evt) +{ + evt.Skip(); + if (m_canvas == nullptr || wxGetApp().is_closing()) + return; + if (!m_canvas->get_wxglcanvas()->IsShownOnScreen()) { + finish(_L("The benchmark stopped because the 3D view was hidden.")); + return; + } + + const std::chrono::steady_clock::time_point now = std::chrono::steady_clock::now(); + if (m_frame > WARMUP_FRAMES && m_frame <= WARMUP_FRAMES + PASS_FRAMES) + m_results.back().frame_ms.push_back(std::chrono::duration(now - m_last_frame).count()); + m_last_frame = now; + + FrameProfiler& profiler = m_canvas->get_frame_profiler(); + if (m_frame == WARMUP_FRAMES + PASS_FRAMES) + profiler.start_averaging(); + if (m_stage == Stage::Layers) + // The warm-up ends where the path starts, so the slider moves in every frame. + set_top_layer(m_frame < WARMUP_FRAMES ? PASS_FRAMES - WARMUP_FRAMES + m_frame : (m_frame - WARMUP_FRAMES) % PASS_FRAMES); + else + set_camera(m_frame < WARMUP_FRAMES ? 0 : (m_frame - WARMUP_FRAMES) % PASS_FRAMES); + m_canvas->render(); + if (m_frame == 0) + on_first_frame(); + + if (++m_frame == SCENE_FRAMES) { + m_results.back().sections = profiler.finish_averaging(true); + end_scene(); + // The next scene starts right away, with no timer to bring the next idle event. + if (m_canvas != nullptr) + evt.RequestMore(); + return; + } + const int scene = m_stage == Stage::Prepare ? 0 : m_stage == Stage::Preview ? 1 : 2; + if (m_frame % 30 == 0) + m_progress->SetValue(int(100 * (scene * SCENE_FRAMES + m_frame) / (3 * SCENE_FRAMES))); + evt.RequestMore(); +} + +void SceneBenchmarkDialog::on_first_frame() +{ + // A benchmark measures the frames the GPU can draw, not the display refresh rate. + wxGLCanvas* glcanvas = m_canvas->get_wxglcanvas(); + m_swap_interval = glcanvas->GetSwapInterval(); + const bool known = m_swap_interval != wxGLCanvas::DefaultSwapInterval; + if (known && m_swap_interval != 0) + m_restore_swap_interval = glcanvas->SetSwapInterval(0) != wxGLCanvas::SwapInterval::NotSet; + if (m_results.size() > 1) + return; + + m_vsync = !known ? "unknown" : m_swap_interval == 0 || m_restore_swap_interval ? "off" : "on"; + GLint samples = 0; + glsafe(::glGetIntegerv(GL_SAMPLES, &samples)); + m_msaa_samples = samples; + const Camera& camera = wxGetApp().plater()->get_camera(); + const std::array& viewport = camera.get_viewport(); + m_viewport_width = viewport[2]; + m_viewport_height = viewport[3]; + m_camera_type = camera.get_type_as_string(); +} + +void SceneBenchmarkDialog::begin_scene(GLCanvas3D* canvas, Stage stage) +{ + m_stage = stage; + m_canvas = canvas; + m_saved_camera = wxGetApp().plater()->get_camera(); + m_frame = 0; + m_restore_swap_interval = false; + m_results.push_back({stage == Stage::Prepare ? "Prepare" : stage == Stage::Preview ? "Preview" : "Layers", {}, {}}); + m_results.back().frame_ms.reserve(PASS_FRAMES); + set_status(stage == Stage::Prepare ? _L("Turning the camera in Prepare") : + stage == Stage::Preview ? _L("Turning the camera in Preview") : + _L("Moving through the layers in Preview")); + if (stage == Stage::Layers) { + m_saved_top_layer = canvas->get_gcode_viewer().get_layers_slider()->GetHigherValue(); + set_camera(0); + } + canvas->set_benchmarking(true); +} + +void SceneBenchmarkDialog::end_scene() +{ + const Stage stage = m_stage; + GLCanvas3D* canvas = m_canvas; + stop_scene(); + if (stage == Stage::Prepare) + start_slicing(); + else if (stage == Stage::Preview) + begin_scene(canvas, Stage::Layers); + else + finish(); +} + +void SceneBenchmarkDialog::stop_scene() +{ + if (m_canvas == nullptr) + return; + if (m_frame < SCENE_FRAMES) + m_results.pop_back(); + FrameProfiler& profiler = m_canvas->get_frame_profiler(); + if (profiler.is_averaging()) + profiler.finish_averaging(false); + if (m_restore_swap_interval && m_canvas->make_current_for_postinit()) + m_canvas->get_wxglcanvas()->SetSwapInterval(m_swap_interval); + if (m_saved_top_layer >= 0) { + m_canvas->get_gcode_viewer().get_layers_slider()->SetHigherValue(m_saved_top_layer); + m_saved_top_layer = -1; + } + m_canvas->set_benchmarking(false); + m_canvas->set_as_dirty(); + wxGetApp().plater()->get_camera() = m_saved_camera; + m_canvas = nullptr; +} + +void SceneBenchmarkDialog::start_slicing() +{ + m_stage = Stage::Slicing; + set_status(_L("Slicing")); + m_progress->SetValue(33); + Plater* plater = wxGetApp().plater(); + plater->reslice(); + plater->select_view_3D("Preview", false); + wxGetApp().mainframe->select_tab(TAB_ID_PREVIEW); + m_polls = 0; + m_timer.Start(100); +} + +void SceneBenchmarkDialog::set_camera(size_t pass_frame) +{ + // Two turns around the model while the view goes three times from below the plate to nearly + // straight above it, zooming in and out twice. + const double t = double(pass_frame) / double(PASS_FRAMES); + const double azimuth = (-0.75 + 4.0 * t) * PI; + const double elevation = (30.0 + 55.0 * std::sin(6.0 * PI * t)) * PI / 180.0; + const Vec3d dir(std::cos(elevation) * std::cos(azimuth), std::cos(elevation) * std::sin(azimuth), std::sin(elevation)); + Camera& camera = wxGetApp().plater()->get_camera(); + camera.look_at(m_target + Camera::DefaultDistance * dir, m_target, Vec3d::UnitZ()); + camera.set_zoom(m_zoom * (1.0 + 0.4 * std::sin(4.0 * PI * t))); +} + +void SceneBenchmarkDialog::set_top_layer(size_t pass_frame) +{ + // Down to the first layer and back up, as dragging the top of the layer slider does. + IMSlider* slider = m_canvas->get_gcode_viewer().get_layers_slider(); + const double t = double(pass_frame) / double(PASS_FRAMES); + const int span = slider->GetMaxValue() - slider->GetMinValue(); + slider->SetHigherValue(slider->GetMinValue() + int(std::lround(span * std::abs(1.0 - 2.0 * t)))); +} + +void SceneBenchmarkDialog::set_status(const wxString& status) +{ + m_status->SetLabel(status + dots); + Fit(); +} + +void SceneBenchmarkDialog::finish(const wxString& error) +{ + m_timer.Stop(); + stop_scene(); + m_disabler.reset(); + m_stage = Stage::Done; + show_report(error); +} + +void SceneBenchmarkDialog::show_report(const wxString& error) +{ + DestroyChildren(); + m_status = nullptr; + m_progress = nullptr; + + auto* sizer = new wxBoxSizer(wxVERTICAL); + if (!error.IsEmpty()) { + auto* message = new Label(this, error); + message->Wrap(FromDIP(500)); + sizer->Add(message, 0, wxEXPAND | wxLEFT | wxRIGHT | wxTOP, FromDIP(15)); + } + + std::string text; + if (!m_results.empty()) { + text = report(); + auto* content = new wxTextCtrl(this, wxID_ANY, from_u8(text), wxDefaultPosition, wxDefaultSize, + wxTE_MULTILINE | wxTE_READONLY | wxTE_DONTWRAP | wxBORDER_SIMPLE); + content->SetFont(wxGetApp().code_font()); + wxClientDC dc(content); + dc.SetFont(content->GetFont()); + const wxSize extent = dc.GetMultiLineTextExtent(content->GetValue()) + FromDIP(wxSize(40, 20)); + content->SetMinSize(wxSize(extent.GetWidth(), std::min(extent.GetHeight(), FromDIP(560)))); + sizer->Add(content, 1, wxEXPAND | wxLEFT | wxRIGHT | wxTOP, FromDIP(15)); + } + + auto* buttons = new DialogButtons(this, text.empty() ? std::vector{"OK"} : std::vector{"Copy", "OK"}); + if (Button* copy = buttons->GetButtonFromID(wxID_COPY)) { + copy->SetToolTip(_L("Copy the results to the clipboard")); + copy->Bind(wxEVT_BUTTON, [text](wxCommandEvent&) { + wxClipboardLocker lock; + if (!lock) + return; + wxTheClipboard->SetData(new wxTextDataObject(from_u8(text))); + }); + } + buttons->GetOK()->Bind(wxEVT_BUTTON, [this](wxCommandEvent&) { Close(); }); + sizer->Add(buttons, 0, wxEXPAND); + + SetSizerAndFit(sizer); + CenterOnParent(); + wxGetApp().UpdateDlgDarkUI(this); + Raise(); +} + +std::string SceneBenchmarkDialog::report() const +{ + const AppConfig& config = *wxGetApp().app_config; + const OpenGLManager::GLInfo& gl = OpenGLManager::get_gl_info(); + auto on_off = [](bool on) { return on ? "on" : "off"; }; + + std::ostringstream out; + out << SLIC3R_APP_NAME << " 3D scene benchmark\n" + << "Version: " << SoftFever_VERSION << " (" << build_commit_label << ")\n" + << "GPU: " << gl.get_renderer() << '\n' + << "OpenGL: " << gl.get_version() << '\n' + << "Viewport: " << m_viewport_width << " x " << m_viewport_height << " px, " << m_camera_type << " camera\n" + << "Settings: MSAA " << (m_msaa_samples > 1 ? std::to_string(m_msaa_samples) + "x" : "off") + << ", FXAA " << on_off(config.get_bool(SETTING_OPENGL_FXAA_ENABLED)) + << ", scene cache " << on_off(config.get_bool(SETTING_OPENGL_SCENE_CACHE)) << ", VSync " << m_vsync << '\n' + << "Realistic: "; + if (config.get_bool(SETTING_OPENGL_REALISTIC_MODE)) + out << "on, Phong " << on_off(config.get_bool(SETTING_OPENGL_REALISTIC_PHONG)) << ", SSAO " + << on_off(config.get_bool(SETTING_OPENGL_PHONG_SSAO)) << ", shadows " << config.get(SETTING_OPENGL_REALISTIC_SHADOWS) + << ", in Preview " << on_off(config.get_bool(SETTING_OPENGL_REALISTIC_PREVIEW)) << '\n'; + else + out << "off\n"; + // The Preview scenes depend on the toolpaths, which depend on the presets it was sliced with. + const PresetBundle& presets = *wxGetApp().preset_bundle; + auto preset_name = [](const PresetCollection& collection) { + return collection.get_selected_preset_name() + (collection.get_edited_preset().is_dirty ? " (modified)" : ""); + }; + out << "Presets: " << preset_name(presets.printers) << ", " << preset_name(presets.prints) << '\n' + << "Scene: OrcaSliced Combo, " << PASS_FRAMES << " frames per pass\n"; + const GCodeViewer& gcode_viewer = wxGetApp().plater()->get_preview_canvas3D()->get_gcode_viewer(); + if (gcode_viewer.has_data()) + out << "Toolpaths: " << gcode_viewer.get_vertices_count() << " vertices, " << gcode_viewer.get_layers_count() << " layers\n"; + out << '\n'; + + std::vector stats; + for (const SceneResult& result : m_results) + stats.push_back(frame_time_stats(result.frame_ms)); + auto row = [&out, &stats](const char* label, double FrameTimeStats::*value) { + out << std::left << std::setw(22) << label << std::right; + for (const FrameTimeStats& s : stats) + out << std::setw(10) << s.*value; + out << '\n'; + }; + + out << std::fixed << std::setprecision(1) << std::setw(22) << ""; + for (const SceneResult& result : m_results) + out << std::right << std::setw(10) << result.name; + out << '\n'; + row("Average FPS", &FrameTimeStats::fps); + out << std::setprecision(2) << "Frame time (ms)\n"; + row(" average", &FrameTimeStats::average_ms); + row(" median", &FrameTimeStats::median_ms); + row(" 95th percentile", &FrameTimeStats::p95_ms); + row(" 99th percentile", &FrameTimeStats::p99_ms); + row(" maximum", &FrameTimeStats::max_ms); + + for (const SceneResult& result : m_results) { + out << "\nRender timings, " << result.name << " (ms per frame)\n"; + if (result.sections.empty()) { + out << " not supported by the graphics driver\n"; + continue; + } + out << std::setw(22) << "" << std::setw(10) << "CPU" << std::setw(10) << "GPU" << '\n'; + double cpu_ms = 0.0; + double gpu_ms = 0.0; + for (const FrameProfiler::Section& section : result.sections) { + out << " " << std::left << std::setw(20) << section.name << std::right << std::setw(10) << section.cpu_ms << std::setw(10) + << section.gpu_ms << '\n'; + cpu_ms += section.cpu_ms; + gpu_ms += section.gpu_ms; + } + out << " " << std::left << std::setw(20) << "total" << std::right << std::setw(10) << cpu_ms << std::setw(10) << gpu_ms << '\n'; + } + return out.str(); +} + +} // namespace + +void run_scene_benchmark() +{ + Plater* plater = wxGetApp().plater(); + if (plater == nullptr || !wxGetApp().is_editor()) + return; + if (s_benchmark != nullptr) { + if (s_benchmark->is_running()) { + s_benchmark->Raise(); + return; + } + s_benchmark->Close(); + } + + MessageDialog confirm(wxGetApp().mainframe, + _L("The benchmark replaces the current project with the OrcaSliced Combo and slices it. It turns the camera " + "around the model in Prepare and in Preview, then moves the layer slider through the sliced layers, timing " + "every frame, and shows the results.") + "\n\n" + + _L("It uses the current graphics settings and usually takes less than a minute. The main window stays " + "locked until it finishes."), + _L("Benchmark 3D Scene"), wxICON_INFORMATION | wxOK | wxCANCEL); + confirm.SetButtonLabel(wxID_OK, _L("Start"), true); + if (confirm.ShowModal() != wxID_OK || plater->new_project() == wxID_CANCEL) + return; + + const std::vector& models = MenuFactory::handy_models(); + const auto combo = std::find_if(models.begin(), models.end(), + [](const MenuFactory::HandyModel& model) { return std::strcmp(model.key, "orcasliced_combo") == 0; }); + MenuFactory::load_handy_model(size_t(combo - models.begin())); + (new SceneBenchmarkDialog())->Show(); +} + +} // namespace GUI +} // namespace Slic3r diff --git a/src/slic3r/GUI/SceneBenchmark.hpp b/src/slic3r/GUI/SceneBenchmark.hpp new file mode 100644 index 0000000000..96c2df5b60 --- /dev/null +++ b/src/slic3r/GUI/SceneBenchmark.hpp @@ -0,0 +1,27 @@ +#pragma once + +#include + +namespace Slic3r { +namespace GUI { + +struct FrameTimeStats +{ + double fps{ 0.0 }; + double average_ms{ 0.0 }; + double median_ms{ 0.0 }; + double p95_ms{ 0.0 }; + double p99_ms{ 0.0 }; + double max_ms{ 0.0 }; +}; + +// Nearest-rank percentiles, so every value is a measured frame time. +FrameTimeStats frame_time_stats(std::vector frame_ms); + +// Loads the OrcaSliced Combo into a new project and turns the camera around it in Prepare, then in +// Preview once sliced, then moves the layer slider through the layers, and reports the frame times and +// render timings of the three scenes. +void run_scene_benchmark(); + +} // namespace GUI +} // namespace Slic3r diff --git a/tests/slic3rutils/CMakeLists.txt b/tests/slic3rutils/CMakeLists.txt index 4d419a4b57..566835f495 100644 --- a/tests/slic3rutils/CMakeLists.txt +++ b/tests/slic3rutils/CMakeLists.txt @@ -9,6 +9,7 @@ add_executable(${_TEST_NAME}_tests test_device_manager_integration.cpp test_web_media_controller.cpp test_scene_raycaster.cpp + test_scene_benchmark.cpp test_lazy.cpp test_prebuild_queue.cpp test_staged_build.cpp diff --git a/tests/slic3rutils/test_action_source.cpp b/tests/slic3rutils/test_action_source.cpp index eec38d7291..8dc4404419 100644 --- a/tests/slic3rutils/test_action_source.cpp +++ b/tests/slic3rutils/test_action_source.cpp @@ -238,7 +238,8 @@ TEST_CASE("Native command catalog includes the Help and developer-mode commands" const Slic3r::GUI::NativeCommand* first = find("help_keyboard_shortcuts"); REQUIRE(first != nullptr); for (const char* key : {"help_setup_wizard", "help_open_config_folder", "help_troubleshoot", "help_network_test", - "help_tip_of_the_day", "help_check_updates", "help_about", "open_wiki", "open_youtube"}) { + "help_benchmark_3d_scene", "help_tip_of_the_day", "help_check_updates", "help_about", "open_wiki", + "open_youtube"}) { const Slic3r::GUI::NativeCommand* c = find(key); REQUIRE(c != nullptr); CHECK(c->group == first->group); diff --git a/tests/slic3rutils/test_scene_benchmark.cpp b/tests/slic3rutils/test_scene_benchmark.cpp new file mode 100644 index 0000000000..c0e04fd690 --- /dev/null +++ b/tests/slic3rutils/test_scene_benchmark.cpp @@ -0,0 +1,57 @@ +#include + +#include "slic3r/GUI/SceneBenchmark.hpp" + +#include +#include +#include +#include + +using Catch::Matchers::WithinAbs; +using Slic3r::GUI::frame_time_stats; +using Slic3r::GUI::FrameTimeStats; + +TEST_CASE("Frame time statistics take percentiles by nearest rank", "[SceneBenchmark]") +{ + // 1 to 100 ms in shuffled order: the p-th percentile is the p-th frame time. + std::vector frame_ms(100); + std::iota(frame_ms.begin(), frame_ms.end(), 1.0); + std::shuffle(frame_ms.begin(), frame_ms.end(), std::mt19937(42)); + + const FrameTimeStats stats = frame_time_stats(frame_ms); + CHECK_THAT(stats.average_ms, WithinAbs(50.5, 1e-9)); + CHECK_THAT(stats.fps, WithinAbs(1000.0 / 50.5, 1e-9)); + CHECK_THAT(stats.median_ms, WithinAbs(50.0, 1e-9)); + CHECK_THAT(stats.p95_ms, WithinAbs(95.0, 1e-9)); + CHECK_THAT(stats.p99_ms, WithinAbs(99.0, 1e-9)); + CHECK_THAT(stats.max_ms, WithinAbs(100.0, 1e-9)); +} + +TEST_CASE("Frame time percentiles are measured frame times", "[SceneBenchmark]") +{ + // 360 frames, as in a benchmark pass: rank 342 for p95 and rank 357 for p99. + std::vector frame_ms(360); + std::iota(frame_ms.begin(), frame_ms.end(), 1.0); + + const FrameTimeStats stats = frame_time_stats(frame_ms); + CHECK_THAT(stats.median_ms, WithinAbs(180.0, 1e-9)); + CHECK_THAT(stats.p95_ms, WithinAbs(342.0, 1e-9)); + CHECK_THAT(stats.p99_ms, WithinAbs(357.0, 1e-9)); +} + +TEST_CASE("Frame time statistics of a single frame are that frame", "[SceneBenchmark]") +{ + const FrameTimeStats stats = frame_time_stats({ 4.0 }); + CHECK_THAT(stats.fps, WithinAbs(250.0, 1e-9)); + CHECK_THAT(stats.median_ms, WithinAbs(4.0, 1e-9)); + CHECK_THAT(stats.p99_ms, WithinAbs(4.0, 1e-9)); + CHECK_THAT(stats.max_ms, WithinAbs(4.0, 1e-9)); +} + +TEST_CASE("Frame time statistics of no frames are zero", "[SceneBenchmark]") +{ + const FrameTimeStats stats = frame_time_stats({}); + CHECK_THAT(stats.fps, WithinAbs(0.0, 1e-9)); + CHECK_THAT(stats.average_ms, WithinAbs(0.0, 1e-9)); + CHECK_THAT(stats.max_ms, WithinAbs(0.0, 1e-9)); +} From 222c6a2df50e4dee7139ff7bf27b7fac56a36c5d Mon Sep 17 00:00:00 2001 From: Ian Bassi Date: Fri, 2 Oct 2026 17:33:41 -0300 Subject: [PATCH 6/6] Improve performance by migrating to Clipper2 2.0.1 (#15969) Co-authored-by: Rodrigo Faselli <162915171+RF47@users.noreply.github.com> --- deps_src/CMakeLists.txt | 1 - deps_src/clipper/CMakeLists.txt | 20 - deps_src/clipper/clipper.cpp | 4213 ----------------- deps_src/clipper/clipper.hpp | 606 --- deps_src/clipper/clipper_z.cpp | 7 - deps_src/clipper/clipper_z.hpp | 18 - deps_src/clipper2/CMakeLists.txt | 7 +- .../include/clipper2/clipper.core.h | 154 +- .../include/clipper2/clipper.engine.h | 32 +- .../include/clipper2/clipper.export.h | 73 +- .../Clipper2Lib/include/clipper2/clipper.h | 102 +- .../include/clipper2/clipper.offset.h | 6 +- .../include/clipper2/clipper.triangulation.h | 30 + .../include/clipper2/clipper.version.h | 2 +- .../Clipper2Lib/src/clipper.engine.cpp | 202 +- .../Clipper2Lib/src/clipper.offset.cpp | 66 +- .../Clipper2Lib/src/clipper.rectclip.cpp | 16 +- .../Clipper2Lib/src/clipper.triangulation.cpp | 1222 +++++ .../clipper2/Clipper2Lib/src/clipper2_z.cpp | 1 + .../backends/libslic3r/geometries.hpp | 4 +- docs/HLSD/polygon-clipping.md | 131 + src/libslic3r/Algorithm/LineSplit.cpp | 11 +- src/libslic3r/Algorithm/LineSplit.hpp | 2 +- src/libslic3r/Algorithm/RegionExpansion.cpp | 149 +- src/libslic3r/Algorithm/RegionExpansion.hpp | 1 - src/libslic3r/Arachne/WallToolPaths.cpp | 19 +- src/libslic3r/Arachne/utils/ExtrusionLine.cpp | 4 +- src/libslic3r/Arachne/utils/ExtrusionLine.hpp | 4 +- src/libslic3r/Arrange.cpp | 17 +- src/libslic3r/CMakeLists.txt | 6 +- src/libslic3r/Clipper2Utils.cpp | 228 - src/libslic3r/Clipper2Utils.hpp | 22 - src/libslic3r/Clipper2ZUtils.hpp | 167 - src/libslic3r/ClipperUtils.cpp | 1439 +++--- src/libslic3r/ClipperUtils.hpp | 226 +- src/libslic3r/ClipperZUtils.cpp | 77 + src/libslic3r/ClipperZUtils.hpp | 24 +- src/libslic3r/CutSurface.cpp | 2 +- src/libslic3r/ElephantFootCompensation.cpp | 2 - src/libslic3r/Emboss.cpp | 83 +- src/libslic3r/Emboss.hpp | 2 +- src/libslic3r/Fill/FillBase.cpp | 31 +- src/libslic3r/Fill/FillRectilinear.cpp | 4 +- src/libslic3r/Fill/Lightning/TreeNode.cpp | 9 +- src/libslic3r/Format/svg.cpp | 26 +- src/libslic3r/Geometry.cpp | 1 - src/libslic3r/LayerRegion.cpp | 6 +- src/libslic3r/NSVGUtils.cpp | 18 +- src/libslic3r/PerimeterGenerator.cpp | 66 +- src/libslic3r/Polygon.cpp | 53 +- src/libslic3r/Polygon.hpp | 2 +- src/libslic3r/Print.cpp | 6 +- src/libslic3r/PrintConfig.hpp | 4 +- src/libslic3r/PrintObject.cpp | 15 +- src/libslic3r/SLA/ConcaveHull.cpp | 2 +- src/libslic3r/SLA/Pad.cpp | 20 +- src/libslic3r/SLA/SupportPointGenerator.cpp | 4 +- src/libslic3r/SVG.cpp | 10 +- src/libslic3r/SVG.hpp | 9 +- src/libslic3r/ShortestPath.cpp | 17 - src/libslic3r/ShortestPath.hpp | 6 - src/libslic3r/Support/SupportCommon.cpp | 110 +- src/libslic3r/Support/SupportMaterial.cpp | 8 +- src/libslic3r/Support/TreeModelVolumes.cpp | 24 +- src/libslic3r/Support/TreeSupport.cpp | 6 +- src/libslic3r/Support/TreeSupport3D.cpp | 16 +- src/libslic3r/Support/TreeSupportCommon.hpp | 2 - src/libslic3r/TriangleMeshSlicer.cpp | 6 +- src/libslic3r/TriangleMeshSlicer.hpp | 6 +- src/libslic3r/clipper.cpp | 13 - src/libslic3r/clipper.hpp | 26 - src/libslic3r/pchheader.hpp | 2 - tests/fff_print/test_fill.cpp | 30 +- tests/fff_print/test_gcodewriter.cpp | 5 +- tests/libslic3r/test_clipper_offset.cpp | 10 +- tests/libslic3r/test_clipper_utils.cpp | 90 +- .../test_elephant_foot_compensation.cpp | 2 +- tests/sla_print/sla_test_utils.cpp | 3 +- 78 files changed, 3028 insertions(+), 7038 deletions(-) delete mode 100644 deps_src/clipper/CMakeLists.txt delete mode 100644 deps_src/clipper/clipper.cpp delete mode 100644 deps_src/clipper/clipper.hpp delete mode 100644 deps_src/clipper/clipper_z.cpp delete mode 100644 deps_src/clipper/clipper_z.hpp create mode 100644 deps_src/clipper2/Clipper2Lib/include/clipper2/clipper.triangulation.h create mode 100644 deps_src/clipper2/Clipper2Lib/src/clipper.triangulation.cpp create mode 100644 docs/HLSD/polygon-clipping.md delete mode 100644 src/libslic3r/Clipper2Utils.cpp delete mode 100644 src/libslic3r/Clipper2Utils.hpp delete mode 100644 src/libslic3r/Clipper2ZUtils.hpp create mode 100644 src/libslic3r/ClipperZUtils.cpp delete mode 100644 src/libslic3r/clipper.cpp delete mode 100644 src/libslic3r/clipper.hpp diff --git a/deps_src/CMakeLists.txt b/deps_src/CMakeLists.txt index a4d71c6f27..d044cedfe5 100644 --- a/deps_src/CMakeLists.txt +++ b/deps_src/CMakeLists.txt @@ -15,7 +15,6 @@ add_subdirectory(stb_dxt) # Header-only STB DXT compression library # Static libraries add_subdirectory(Shiny) add_subdirectory(admesh) -add_subdirectory(clipper) add_subdirectory(clipper2) add_subdirectory(expat) add_subdirectory(glu-libtess) diff --git a/deps_src/clipper/CMakeLists.txt b/deps_src/clipper/CMakeLists.txt deleted file mode 100644 index 074cbdf4ff..0000000000 --- a/deps_src/clipper/CMakeLists.txt +++ /dev/null @@ -1,20 +0,0 @@ -cmake_minimum_required(VERSION 3.13) -project(clipper) - -add_library(clipper STATIC -# We are using ClipperLib compiled as part of the libslic3r project using Slic3r::Point as its base type. -# clipper.cpp -# clipper.hpp - clipper_z.cpp - clipper_z.hpp -) - -target_include_directories(clipper SYSTEM - PUBLIC - ${CMAKE_CURRENT_SOURCE_DIR} -) - -target_link_libraries(clipper - PUBLIC Eigen3::Eigen - PRIVATE TBB::tbb TBB::tbbmalloc -) diff --git a/deps_src/clipper/clipper.cpp b/deps_src/clipper/clipper.cpp deleted file mode 100644 index 1f16446ac8..0000000000 --- a/deps_src/clipper/clipper.cpp +++ /dev/null @@ -1,4213 +0,0 @@ -/******************************************************************************* -* * -* Author : Angus Johnson * -* Version : 6.4.2 * -* Date : 27 February 2017 * -* Website : http://www.angusj.com * -* Copyright : Angus Johnson 2010-2017 * -* * -* License: * -* Use, modification & distribution is subject to Boost Software License Ver 1. * -* http://www.boost.org/LICENSE_1_0.txt * -* * -* Attributions: * -* The code in this library is an extension of Bala Vatti's clipping algorithm: * -* "A generic solution to polygon clipping" * -* Communications of the ACM, Vol 35, Issue 7 (July 1992) pp 56-63. * -* http://portal.acm.org/citation.cfm?id=129906 * -* * -* Computer graphics and geometric modeling: implementation and algorithms * -* By Max K. Agoston * -* Springer; 1 edition (January 4, 2005) * -* http://books.google.com/books?q=vatti+clipping+agoston * -* * -* See also: * -* "Polygon Offsetting by Computing Winding Numbers" * -* Paper no. DETC2005-85513 pp. 565-575 * -* ASME 2005 International Design Engineering Technical Conferences * -* and Computers and Information in Engineering Conference (IDETC/CIE2005) * -* September 24-28, 2005 , Long Beach, California, USA * -* http://www.me.berkeley.edu/~mcmains/pubs/DAC05OffsetPolygon.pdf * -* * -*******************************************************************************/ - -/******************************************************************************* -* * -* This is a translation of the Delphi Clipper library and the naming style * -* used has retained a Delphi flavour. * -* * -*******************************************************************************/ - -#include "clipper.hpp" -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#ifdef CLIPPERLIB_NAMESPACE_PREFIX -namespace CLIPPERLIB_NAMESPACE_PREFIX { -#endif // CLIPPERLIB_NAMESPACE_PREFIX - -#ifdef CLIPPERLIB_USE_XYZ -namespace ClipperLib_Z { -#else /* CLIPPERLIB_USE_XYZ */ -namespace ClipperLib { -#endif /* CLIPPERLIB_USE_XYZ */ - -static double const pi = 3.141592653589793238; -static double const two_pi = pi *2; -static double const def_arc_tolerance = 0.25; - -enum Direction { dRightToLeft, dLeftToRight }; - -static int const Unassigned = -1; //edge not currently 'owning' a solution -static int const Skip = -2; //edge that would otherwise close a path - -#define HORIZONTAL (-1.0E+40) -#define TOLERANCE (1.0e-20) -#define NEAR_ZERO(val) (((val) > -TOLERANCE) && ((val) < TOLERANCE)) - -//------------------------------------------------------------------------------ - -inline IntPoint IntPoint2d(cInt x, cInt y) -{ - return IntPoint(x, y -#ifdef CLIPPERLIB_USE_XYZ - , 0 -#endif // CLIPPERLIB_USE_XYZ - ); -} - -// Fast rounding upwards. -inline double FRound(double a) -{ - // Why does Java Math.round(0.49999999999999994) return 1? - // https://stackoverflow.com/questions/9902968/why-does-math-round0-49999999999999994-return-1 - return a == 0.49999999999999994 ? 0 : floor(a + 0.5); -} - -template -inline IType Round(double val) -{ - double v = FRound(val); -#if defined(CLIPPERLIB_INT32) && ! defined(NDEBUG) - static_assert(sizeof(IType) == 4 || sizeof(IType) == 8, "IType must be int32 or int64"); - static constexpr const double hi = 65536. * 16383. * (sizeof(IType) == 4 ? 1 : 65536. * 65536.); - if (v > hi || -v > hi) - throw clipperException("Coordinate outside allowed range"); -#endif - return static_cast(v); -} - -// Overriding the Eigen operators because we don't want to compare Z coordinate if IntPoint is 3 dimensional. -inline bool operator==(const IntPoint &l, const IntPoint &r) { return l.x() == r.x() && l.y() == r.y(); } -inline bool operator!=(const IntPoint &l, const IntPoint &r) { return l.x() != r.x() || l.y() != r.y(); } - -//------------------------------------------------------------------------------ -// PolyTree methods ... -//------------------------------------------------------------------------------ - -int PolyTree::Total() const -{ - int result = (int)AllNodes.size(); - //with negative offsets, ignore the hidden outer polygon ... - if (result > 0 && Childs.front() != &AllNodes.front()) result--; - return result; -} - -//------------------------------------------------------------------------------ -// PolyNode methods ... -//------------------------------------------------------------------------------ - -void PolyNode::AddChild(PolyNode& child) -{ - unsigned cnt = (unsigned)Childs.size(); - Childs.emplace_back(&child); - child.Parent = this; - child.Index = cnt; -} -//------------------------------------------------------------------------------ - -// Edge delimits a hole if it has an odd number of parent loops. -bool PolyNode::IsHole() const -{ - bool result = true; - PolyNode* node = Parent; - while (node) - { - result = !result; - node = node->Parent; - } - return result; -} - -void PolyTree::RemoveOutermostPolygon() -{ - if (this->ChildCount() == 1 && this->Childs[0]->ChildCount() > 0) { - PolyNode *outerNode = this->Childs[0]; - this->Childs.reserve(outerNode->ChildCount()); - this->Childs[0] = outerNode->Childs[0]; - this->Childs[0]->Parent = outerNode->Parent; - for (int i = 1; i < outerNode->ChildCount(); ++i) - this->AddChild(*outerNode->Childs[i]); - } else - this->Clear(); -} - -//------------------------------------------------------------------------------ -// Miscellaneous global functions -//------------------------------------------------------------------------------ - -double Area(const Path &poly) -{ - int size = (int)poly.size(); - if (size < 3) return 0; - - double a = 0; - for (int i = 0, j = size -1; i < size; ++i) - { - a += ((double)poly[j].x() + poly[i].x()) * ((double)poly[j].y() - poly[i].y()); - j = i; - } - return -a * 0.5; -} -//------------------------------------------------------------------------------ - -double Area(const OutPt *op) -{ - const OutPt *startOp = op; - if (!op) return 0; - double a = 0; - do { - a += (double)(op->Prev->Pt.x() + op->Pt.x()) * (double)(op->Prev->Pt.y() - op->Pt.y()); - op = op->Next; - } while (op != startOp); - return a * 0.5; -} -//------------------------------------------------------------------------------ - -double Area(const OutRec &outRec) -{ - return Area(outRec.Pts); -} -//------------------------------------------------------------------------------ - -bool PointIsVertex(const IntPoint &Pt, OutPt *pp) -{ - OutPt *pp2 = pp; - do - { - if (pp2->Pt == Pt) return true; - pp2 = pp2->Next; - } - while (pp2 != pp); - return false; -} -//------------------------------------------------------------------------------ - -//See "The Point in Polygon Problem for Arbitrary Polygons" by Hormann & Agathos -//http://citeseerx.ist.psu.edu/viewdoc/download?doi=10.1.1.88.5498&rep=rep1&type=pdf -int PointInPolygon(const IntPoint &pt, const Path &path) -{ - //returns 0 if false, +1 if true, -1 if pt ON polygon boundary - int result = 0; - size_t cnt = path.size(); - if (cnt < 3) return 0; - IntPoint ip = path[0]; - for(size_t i = 1; i <= cnt; ++i) - { - IntPoint ipNext = (i == cnt ? path[0] : path[i]); - if (ipNext.y() == pt.y() && ((ipNext.x() == pt.x()) || (ip.y() == pt.y() && ((ipNext.x() > pt.x()) == (ip.x() < pt.x()))))) - return -1; - if ((ip.y() < pt.y()) != (ipNext.y() < pt.y())) - { - if (ip.x() >= pt.x()) - { - if (ipNext.x() > pt.x()) result = 1 - result; - else - { - auto d = CrossProductType(ip.x() - pt.x()) * CrossProductType(ipNext.y() - pt.y()) - CrossProductType(ipNext.x() - pt.x()) * CrossProductType(ip.y() - pt.y()); - if (!d) return -1; - if ((d > 0) == (ipNext.y() > ip.y())) result = 1 - result; - } - } else - { - if (ipNext.x() > pt.x()) - { - auto d = CrossProductType(ip.x() - pt.x()) * CrossProductType(ipNext.y() - pt.y()) - CrossProductType(ipNext.x() - pt.x()) * CrossProductType(ip.y() - pt.y()); - if (!d) return -1; - if ((d > 0) == (ipNext.y() > ip.y())) result = 1 - result; - } - } - } - ip = ipNext; - } - return result; -} -//------------------------------------------------------------------------------ - -// Called by Poly2ContainsPoly1() -int PointInPolygon(const IntPoint &pt, OutPt *op) -{ - //returns 0 if false, +1 if true, -1 if pt ON polygon boundary - int result = 0; - OutPt* startOp = op; - do - { - if (op->Next->Pt.y() == pt.y()) - { - if ((op->Next->Pt.x() == pt.x()) || (op->Pt.y() == pt.y() && - ((op->Next->Pt.x() > pt.x()) == (op->Pt.x() < pt.x())))) return -1; - } - if ((op->Pt.y() < pt.y()) != (op->Next->Pt.y() < pt.y())) - { - if (op->Pt.x() >= pt.x()) - { - if (op->Next->Pt.x() > pt.x()) result = 1 - result; - else - { - auto d = CrossProductType(op->Pt.x() - pt.x()) * CrossProductType(op->Next->Pt.y() - pt.y()) - CrossProductType(op->Next->Pt.x() - pt.x()) * CrossProductType(op->Pt.y() - pt.y()); - if (!d) return -1; - if ((d > 0) == (op->Next->Pt.y() > op->Pt.y())) result = 1 - result; - } - } else - { - if (op->Next->Pt.x() > pt.x()) - { - auto d = CrossProductType(op->Pt.x() - pt.x()) * CrossProductType(op->Next->Pt.y() - pt.y()) - CrossProductType(op->Next->Pt.x() - pt.x()) * CrossProductType(op->Pt.y() - pt.y()); - if (!d) return -1; - if ((d > 0) == (op->Next->Pt.y() > op->Pt.y())) result = 1 - result; - } - } - } - op = op->Next; - } while (startOp != op); - return result; -} -//------------------------------------------------------------------------------ - -// This is potentially very expensive! O(n^2)! -bool Poly2ContainsPoly1(OutPt *OutPt1, OutPt *OutPt2) -{ - OutPt* op = OutPt1; - do - { - //nb: PointInPolygon returns 0 if false, +1 if true, -1 if pt on polygon - int res = PointInPolygon(op->Pt, OutPt2); - if (res >= 0) return res > 0; - op = op->Next; - } - while (op != OutPt1); - return true; -} -//---------------------------------------------------------------------- - -#ifdef CLIPPERLIB_INT32 -inline bool SlopesEqual(const cInt dx1, const cInt dy1, const cInt dx2, const cInt dy2, bool /* UseFullInt64Range */) { - return int64_t(dy1) * int64_t(dx2) == int64_t(dx1) * int64_t(dy2); -} -#else -inline bool SlopesEqual(const cInt dx1, const cInt dy1, const cInt dx2, const cInt dy2, bool UseFullInt64Range) { - return (UseFullInt64Range) ? - // |dx1| < 2^63, |dx2| < 2^63 etc, - Int128::sign_determinant_2x2_filtered(dx1, dy1, dx2, dy2) == 0 : -// Int128::sign_determinant_2x2(dx1, dy1, dx2, dy2) == 0 : - // |dx1| < 2^31, |dx2| < 2^31 etc, - // therefore the following computation could be done with 64bit arithmetics. - dy1 * dx2 == dx1 * dy2; -} -#endif - -inline bool SlopesEqual(const TEdge &e1, const TEdge &e2, bool UseFullInt64Range) - { return SlopesEqual(e1.Top.x() - e1.Bot.x(), e1.Top.y() - e1.Bot.y(), e2.Top.x() - e2.Bot.x(), e2.Top.y() - e2.Bot.y(), UseFullInt64Range); } -inline bool SlopesEqual(const IntPoint &pt1, const IntPoint &pt2, const IntPoint &pt3, bool UseFullInt64Range) - { return SlopesEqual(pt1.x()-pt2.x(), pt1.y()-pt2.y(), pt2.x()-pt3.x(), pt2.y()-pt3.y(), UseFullInt64Range); } -inline bool SlopesEqual(const IntPoint &pt1, const IntPoint &pt2, const IntPoint &pt3, const IntPoint &pt4, bool UseFullInt64Range) - { return SlopesEqual(pt1.x()-pt2.x(), pt1.y()-pt2.y(), pt3.x()-pt4.x(), pt3.y()-pt4.y(), UseFullInt64Range); } - -//------------------------------------------------------------------------------ - -inline bool IsHorizontal(TEdge &e) -{ - return e.Dx == HORIZONTAL; -} -//------------------------------------------------------------------------------ - -inline double GetDx(const IntPoint &pt1, const IntPoint &pt2) -{ - return (pt1.y() == pt2.y()) ? - HORIZONTAL : (double)(pt2.x() - pt1.x()) / (pt2.y() - pt1.y()); -} -//--------------------------------------------------------------------------- - -inline cInt TopX(TEdge &edge, const cInt currentY) -{ - return (currentY == edge.Top.y()) ? - edge.Top.x() : - edge.Bot.x() + Round(edge.Dx *(currentY - edge.Bot.y())); -} -//------------------------------------------------------------------------------ - -void IntersectPoint(TEdge &Edge1, TEdge &Edge2, IntPoint &ip) -{ -#ifdef CLIPPERLIB_USE_XYZ - ip.z() = 0; -#endif - - if (Edge1.Dx == Edge2.Dx) - { - ip.y() = Edge1.Curr.y(); - ip.x() = TopX(Edge1, ip.y()); - return; - } - - int64_t y; - if (Edge1.Dx == 0) - { - ip.x() = Edge1.Bot.x(); - y = IsHorizontal(Edge2) ? - Edge2.Bot.y() : - Round(ip.x() / Edge2.Dx + Edge2.Bot.y() - (Edge2.Bot.x() / Edge2.Dx)); - - } - else if (Edge2.Dx == 0) - { - ip.x() = Edge2.Bot.x(); - y = IsHorizontal(Edge1) ? - Edge1.Bot.y() : - Round(ip.x() / Edge1.Dx + Edge1.Bot.y() - (Edge1.Bot.x() / Edge1.Dx)); - } - else - { - double b1 = double(Edge1.Bot.x()) - double(Edge1.Bot.y()) * Edge1.Dx; - double b2 = double(Edge2.Bot.x()) - double(Edge2.Bot.y()) * Edge2.Dx; - double q = (b2-b1) / (Edge1.Dx - Edge2.Dx); - y = Round(q); - ip.x() = (std::fabs(Edge1.Dx) < std::fabs(Edge2.Dx)) ? - Round(Edge1.Dx * q + b1) : - Round(Edge2.Dx * q + b2); - } - - ip.y() = cInt(y); - if (y < Edge1.Top.y() || y < Edge2.Top.y()) - { - ip.y() = (Edge1.Top.y() > Edge2.Top.y() ? Edge1 : Edge2).Top.y(); - y = ip.y(); - ip.x() = TopX(std::fabs(Edge1.Dx) < std::fabs(Edge2.Dx) ? Edge1 : Edge2, ip.y()); - } - //finally, don't allow 'ip' to be BELOW curr.y() (ie bottom of scanbeam) ... - if (y > Edge1.Curr.y()) - { - ip.y() = Edge1.Curr.y(); - //use the more vertical edge to derive X ... - ip.x() = TopX(std::fabs(Edge1.Dx) > std::fabs(Edge2.Dx) ? Edge2 : Edge1, ip.y()); - } -} -//------------------------------------------------------------------------------ - -// Reverse a linked loop of points representing a closed polygon. -// This has a time complexity of O(n) -void ReversePolyPtLinks(OutPt *pp) -{ - if (!pp) return; - OutPt *pp1 = pp; - do { - OutPt *pp2 = pp1->Next; - pp1->Next = pp1->Prev; - pp1->Prev = pp2; - pp1 = pp2; - } while( pp1 != pp ); -} -//------------------------------------------------------------------------------ - -inline void InitEdge(TEdge* e, TEdge* eNext, TEdge* ePrev, const IntPoint& Pt) -{ - std::memset(e, 0, sizeof(TEdge)); - e->Next = eNext; - e->Prev = ePrev; - e->Curr = Pt; - e->OutIdx = Unassigned; -} -//------------------------------------------------------------------------------ - -void InitEdge2(TEdge& e, PolyType Pt) -{ - if (e.Curr.y() >= e.Next->Curr.y()) - { - e.Bot = e.Curr; - e.Top = e.Next->Curr; - } else - { - e.Top = e.Curr; - e.Bot = e.Next->Curr; - } - - cInt dy = (e.Top.y() - e.Bot.y()); - if (dy == 0) e.Dx = HORIZONTAL; - else e.Dx = (double)(e.Top.x() - e.Bot.x()) / dy; - - e.PolyTyp = Pt; -} -//------------------------------------------------------------------------------ - -// Called from ClipperBase::AddPathInternal() to remove collinear and duplicate points. -inline TEdge* RemoveEdge(TEdge* e) -{ - //removes e from double_linked_list (but without removing from memory) - e->Prev->Next = e->Next; - e->Next->Prev = e->Prev; - TEdge* result = e->Next; - e->Prev = 0; //flag as removed (see ClipperBase.Clear) - return result; -} -//------------------------------------------------------------------------------ - -inline void ReverseHorizontal(TEdge &e) -{ - //swap horizontal edges' Top and Bottom x's so they follow the natural - //progression of the bounds - ie so their xbots will align with the - //adjoining lower edge. [Helpful in the ProcessHorizontal() method.] - std::swap(e.Top.x(), e.Bot.x()); -#ifdef CLIPPERLIB_USE_XYZ - std::swap(e.Top.z(), e.Bot.z()); -#endif -} -//------------------------------------------------------------------------------ - -bool GetOverlapSegment(IntPoint pt1a, IntPoint pt1b, IntPoint pt2a, - IntPoint pt2b, IntPoint &pt1, IntPoint &pt2) -{ - //precondition: segments are Collinear. - if (std::abs(pt1a.x() - pt1b.x()) > std::abs(pt1a.y() - pt1b.y())) - { - if (pt1a.x() > pt1b.x()) std::swap(pt1a, pt1b); - if (pt2a.x() > pt2b.x()) std::swap(pt2a, pt2b); - if (pt1a.x() > pt2a.x()) pt1 = pt1a; else pt1 = pt2a; - if (pt1b.x() < pt2b.x()) pt2 = pt1b; else pt2 = pt2b; - return pt1.x() < pt2.x(); - } else - { - if (pt1a.y() < pt1b.y()) std::swap(pt1a, pt1b); - if (pt2a.y() < pt2b.y()) std::swap(pt2a, pt2b); - if (pt1a.y() < pt2a.y()) pt1 = pt1a; else pt1 = pt2a; - if (pt1b.y() > pt2b.y()) pt2 = pt1b; else pt2 = pt2b; - return pt1.y() > pt2.y(); - } -} -//------------------------------------------------------------------------------ - -bool FirstIsBottomPt(const OutPt* btmPt1, const OutPt* btmPt2) -{ - OutPt *p = btmPt1->Prev; - while ((p->Pt == btmPt1->Pt) && (p != btmPt1)) p = p->Prev; - double dx1p = std::fabs(GetDx(btmPt1->Pt, p->Pt)); - p = btmPt1->Next; - while ((p->Pt == btmPt1->Pt) && (p != btmPt1)) p = p->Next; - double dx1n = std::fabs(GetDx(btmPt1->Pt, p->Pt)); - - p = btmPt2->Prev; - while ((p->Pt == btmPt2->Pt) && (p != btmPt2)) p = p->Prev; - double dx2p = std::fabs(GetDx(btmPt2->Pt, p->Pt)); - p = btmPt2->Next; - while ((p->Pt == btmPt2->Pt) && (p != btmPt2)) p = p->Next; - double dx2n = std::fabs(GetDx(btmPt2->Pt, p->Pt)); - - if (std::max(dx1p, dx1n) == std::max(dx2p, dx2n) && - std::min(dx1p, dx1n) == std::min(dx2p, dx2n)) - return Area(btmPt1) > 0; //if otherwise identical use orientation - else - return (dx1p >= dx2p && dx1p >= dx2n) || (dx1n >= dx2p && dx1n >= dx2n); -} -//------------------------------------------------------------------------------ - -// Called by GetLowermostRec() -OutPt* GetBottomPt(OutPt *pp) -{ - OutPt* dups = nullptr; - OutPt* p = pp->Next; - while (p != pp) - { - if (p->Pt.y() > pp->Pt.y()) - { - pp = p; - dups = nullptr; - } - else if (p->Pt.y() == pp->Pt.y() && p->Pt.x() <= pp->Pt.x()) - { - if (p->Pt.x() < pp->Pt.x()) - { - dups = nullptr; - pp = p; - } else - { - if (p->Next != pp && p->Prev != pp) dups = p; - } - } - p = p->Next; - } - if (dups) - { - //there appears to be at least 2 vertices at BottomPt so ... - while (dups != p) - { - if (!FirstIsBottomPt(p, dups)) pp = dups; - dups = dups->Next; - while (dups->Pt != pp->Pt) dups = dups->Next; - } - } - return pp; -} - -//------------------------------------------------------------------------------ - -bool Pt2IsBetweenPt1AndPt3(const IntPoint &pt1, - const IntPoint &pt2, const IntPoint &pt3) -{ - if ((pt1 == pt3) || (pt1 == pt2) || (pt3 == pt2)) - return false; - else if (pt1.x() != pt3.x()) - return (pt2.x() > pt1.x()) == (pt2.x() < pt3.x()); - else - return (pt2.y() > pt1.y()) == (pt2.y() < pt3.y()); -} -//------------------------------------------------------------------------------ - -bool HorzSegmentsOverlap(cInt seg1a, cInt seg1b, cInt seg2a, cInt seg2b) -{ - if (seg1a > seg1b) std::swap(seg1a, seg1b); - if (seg2a > seg2b) std::swap(seg2a, seg2b); - return (seg1a < seg2b) && (seg2a < seg1b); -} - -//------------------------------------------------------------------------------ -// ClipperBase class methods ... -//------------------------------------------------------------------------------ - -#ifdef CLIPPERLIB_INT32 -static inline void RangeTest(const IntPoint &pt) -{ -#ifndef NDEBUG - static constexpr const int32_t hi = 65536 * 16383; - if (pt.x() > hi || pt.y() > hi || -pt.x() > hi || -pt.y() > hi) - throw clipperException("Coordinate outside allowed range"); -#endif // NDEBUG -} -#else // CLIPPERLIB_INT32 -// Called from ClipperBase::AddPath() to verify the scale of the input polygon coordinates. -static inline void RangeTest(const IntPoint& Pt, bool& useFullRange) -{ - if (useFullRange) - { - if (Pt.x() > hiRange || Pt.y() > hiRange || -Pt.x() > hiRange || -Pt.y() > hiRange) - throw clipperException("Coordinate outside allowed range"); - } - else if (Pt.x() > loRange|| Pt.y() > loRange || -Pt.x() > loRange || -Pt.y() > loRange) - { - useFullRange = true; - RangeTest(Pt, useFullRange); - } -} -#endif // CLIPPERLIB_INT32 -//------------------------------------------------------------------------------ - -// Called from ClipperBase::AddPath() to construct the Local Minima List. -// Find a local minimum edge on the path starting with E. -inline TEdge* FindNextLocMin(TEdge* E) -{ - for (;;) - { - while (E->Bot != E->Prev->Bot || E->Curr == E->Top) E = E->Next; - if (!IsHorizontal(*E) && !IsHorizontal(*E->Prev)) break; - while (IsHorizontal(*E->Prev)) E = E->Prev; - TEdge* E2 = E; - while (IsHorizontal(*E)) E = E->Next; - if (E->Top.y() == E->Prev->Bot.y()) continue; //ie just an intermediate horz. - if (E2->Prev->Bot.x() < E->Bot.x()) E = E2; - break; - } - return E; -} -//------------------------------------------------------------------------------ - -// Called from ClipperBase::AddPath(). -TEdge* ClipperBase::ProcessBound(TEdge* E, bool NextIsForward) -{ - TEdge *Result = E; - TEdge *Horz = 0; - - if (E->OutIdx == Skip) - { - //if edges still remain in the current bound beyond the skip edge then - //create another LocMin and call ProcessBound once more - if (NextIsForward) - { - while (E->Top.y() == E->Next->Bot.y()) E = E->Next; - //don't include top horizontals when parsing a bound a second time, - //they will be contained in the opposite bound ... - while (E != Result && IsHorizontal(*E)) E = E->Prev; - } - else - { - while (E->Top.y() == E->Prev->Bot.y()) E = E->Prev; - while (E != Result && IsHorizontal(*E)) E = E->Next; - } - - if (E == Result) - { - if (NextIsForward) Result = E->Next; - else Result = E->Prev; - } - else - { - //there are more edges in the bound beyond result starting with E - if (NextIsForward) - E = Result->Next; - else - E = Result->Prev; - LocalMinimum locMin; - locMin.Y = E->Bot.y(); - locMin.LeftBound = 0; - locMin.RightBound = E; - E->WindDelta = 0; - Result = ProcessBound(E, NextIsForward); - m_MinimaList.emplace_back(locMin); - } - return Result; - } - - TEdge *EStart; - - if (IsHorizontal(*E)) - { - //We need to be careful with open paths because this may not be a - //true local minima (ie E may be following a skip edge). - //Also, consecutive horz. edges may start heading left before going right. - if (NextIsForward) - EStart = E->Prev; - else - EStart = E->Next; - if (IsHorizontal(*EStart)) //ie an adjoining horizontal skip edge - { - if (EStart->Bot.x() != E->Bot.x() && EStart->Top.x() != E->Bot.x()) - ReverseHorizontal(*E); - } - else if (EStart->Bot.x() != E->Bot.x()) - ReverseHorizontal(*E); - } - - EStart = E; - if (NextIsForward) - { - while (Result->Top.y() == Result->Next->Bot.y() && Result->Next->OutIdx != Skip) - Result = Result->Next; - if (IsHorizontal(*Result) && Result->Next->OutIdx != Skip) - { - //nb: at the top of a bound, horizontals are added to the bound - //only when the preceding edge attaches to the horizontal's left vertex - //unless a Skip edge is encountered when that becomes the top divide - Horz = Result; - while (IsHorizontal(*Horz->Prev)) Horz = Horz->Prev; - if (Horz->Prev->Top.x() > Result->Next->Top.x()) Result = Horz->Prev; - } - while (E != Result) - { - E->NextInLML = E->Next; - if (IsHorizontal(*E) && E != EStart && - E->Bot.x() != E->Prev->Top.x()) ReverseHorizontal(*E); - E = E->Next; - } - if (IsHorizontal(*E) && E != EStart && E->Bot.x() != E->Prev->Top.x()) - ReverseHorizontal(*E); - Result = Result->Next; //move to the edge just beyond current bound - } else - { - while (Result->Top.y() == Result->Prev->Bot.y() && Result->Prev->OutIdx != Skip) - Result = Result->Prev; - if (IsHorizontal(*Result) && Result->Prev->OutIdx != Skip) - { - Horz = Result; - while (IsHorizontal(*Horz->Next)) Horz = Horz->Next; - if (Horz->Next->Top.x() == Result->Prev->Top.x() || - Horz->Next->Top.x() > Result->Prev->Top.x()) Result = Horz->Next; - } - - while (E != Result) - { - E->NextInLML = E->Prev; - if (IsHorizontal(*E) && E != EStart && E->Bot.x() != E->Next->Top.x()) - ReverseHorizontal(*E); - E = E->Prev; - } - if (IsHorizontal(*E) && E != EStart && E->Bot.x() != E->Next->Top.x()) - ReverseHorizontal(*E); - Result = Result->Prev; //move to the edge just beyond current bound - } - - return Result; -} -//------------------------------------------------------------------------------ - -bool ClipperBase::AddPath(const Path &pg, PolyType PolyTyp, bool Closed) -{ - // Remove duplicate end point from a closed input path. - // Remove duplicate points from the end of the input path. - int highI = (int)pg.size() -1; - if (Closed) - while (highI > 0 && (pg[highI] == pg[0])) - --highI; - while (highI > 0 && (pg[highI] == pg[highI -1])) - --highI; - if ((Closed && highI < 2) || (!Closed && highI < 1)) - return false; - - // Allocate a new edge array. - Edges edges(highI + 1); - // Fill in the edge array. - bool result = AddPathInternal(pg, highI, PolyTyp, Closed, edges.data()); - if (result) - // Success, remember the edge array. - m_edges.emplace_back(std::move(edges)); - return result; -} - -bool ClipperBase::AddPathInternal(const Path &pg, int highI, PolyType PolyTyp, bool Closed, TEdge* edges) -{ -#ifdef use_lines - if (!Closed && PolyTyp == ptClip) - throw clipperException("AddPath: Open paths must be subject."); -#else - if (!Closed) - throw clipperException("AddPath: Open paths have been disabled."); -#endif - - assert(highI >= 0 && highI < pg.size()); - - //1. Basic (first) edge initialization ... - try - { - edges[1].Curr = pg[1]; -#ifdef CLIPPERLIB_INT32 - RangeTest(pg[0]); - RangeTest(pg[highI]); -#else - RangeTest(pg[0], m_UseFullRange); - RangeTest(pg[highI], m_UseFullRange); -#endif // CLIPPERLIB_INT32 - InitEdge(&edges[0], &edges[1], &edges[highI], pg[0]); - InitEdge(&edges[highI], &edges[0], &edges[highI-1], pg[highI]); - for (int i = highI - 1; i >= 1; --i) - { -#ifdef CLIPPERLIB_INT32 - RangeTest(pg[i]); -#else - RangeTest(pg[i], m_UseFullRange); -#endif // CLIPPERLIB_INT32 - InitEdge(&edges[i], &edges[i+1], &edges[i-1], pg[i]); - } - } - catch(...) - { - throw; //range test fails - } - TEdge *eStart = &edges[0]; - - //2. Remove duplicate vertices, and (when closed) collinear edges ... - TEdge *E = eStart, *eLoopStop = eStart; - for (;;) - { - //nb: allows matching start and end points when not Closed ... - if (E->Curr == E->Next->Curr && (Closed || E->Next != eStart)) - { - if (E == E->Next) break; - if (E == eStart) eStart = E->Next; - E = RemoveEdge(E); - eLoopStop = E; - continue; - } - if (E->Prev == E->Next) - break; //only two vertices - else if (Closed && - SlopesEqual(E->Prev->Curr, E->Curr, E->Next->Curr, m_UseFullRange) && - (!m_PreserveCollinear || - !Pt2IsBetweenPt1AndPt3(E->Prev->Curr, E->Curr, E->Next->Curr))) - { - //Collinear edges are allowed for open paths but in closed paths - //the default is to merge adjacent collinear edges into a single edge. - //However, if the PreserveCollinear property is enabled, only overlapping - //collinear edges (ie spikes) will be removed from closed paths. - if (E == eStart) eStart = E->Next; - E = RemoveEdge(E); - E = E->Prev; - eLoopStop = E; - continue; - } - E = E->Next; - if ((E == eLoopStop) || (!Closed && E->Next == eStart)) break; - } - - if ((!Closed && (E == E->Next)) || (Closed && (E->Prev == E->Next))) - { - return false; - } - - if (!Closed) - { - m_HasOpenPaths = true; - eStart->Prev->OutIdx = Skip; - } - - //3. Do second stage of edge initialization ... - // IsFlat means all vertices have the same Y coordinate. - bool IsFlat = true; - E = eStart; - do - { - InitEdge2(*E, PolyTyp); - E = E->Next; - if (IsFlat && E->Curr.y() != eStart->Curr.y()) IsFlat = false; - } - while (E != eStart); - - //4. Finally, add edge bounds to LocalMinima list ... - - //Totally flat paths must be handled differently when adding them - //to LocalMinima list to avoid endless loops etc ... - if (IsFlat) - { - if (Closed) - { - return false; - } - E->Prev->OutIdx = Skip; - LocalMinimum locMin; - locMin.Y = E->Bot.y(); - locMin.LeftBound = 0; - locMin.RightBound = E; - locMin.RightBound->Side = esRight; - locMin.RightBound->WindDelta = 0; - for (;;) - { - if (E->Bot.x() != E->Prev->Top.x()) ReverseHorizontal(*E); - if (E->Next->OutIdx == Skip) break; - E->NextInLML = E->Next; - E = E->Next; - } - m_MinimaList.emplace_back(locMin); - return true; - } - - bool leftBoundIsForward; - TEdge* EMin = 0; - - //workaround to avoid an endless loop in the while loop below when - //open paths have matching start and end points ... - if (E->Prev->Bot == E->Prev->Top) E = E->Next; - - // Find local minima and store them into a Local Minima List. - // Multiple Local Minima could be created for a single path. - for (;;) - { - E = FindNextLocMin(E); - if (E == EMin) break; - else if (!EMin) EMin = E; - - //E and E.Prev now share a local minima (left aligned if horizontal). - //Compare their slopes to find which starts which bound ... - LocalMinimum locMin; - locMin.Y = E->Bot.y(); - if (E->Dx < E->Prev->Dx) - { - locMin.LeftBound = E->Prev; - locMin.RightBound = E; - leftBoundIsForward = false; //Q.nextInLML = Q.prev - } else - { - locMin.LeftBound = E; - locMin.RightBound = E->Prev; - leftBoundIsForward = true; //Q.nextInLML = Q.next - } - - if (!Closed) locMin.LeftBound->WindDelta = 0; - else if (locMin.LeftBound->Next == locMin.RightBound) - locMin.LeftBound->WindDelta = -1; - else locMin.LeftBound->WindDelta = 1; - locMin.RightBound->WindDelta = -locMin.LeftBound->WindDelta; - - E = ProcessBound(locMin.LeftBound, leftBoundIsForward); - if (E->OutIdx == Skip) E = ProcessBound(E, leftBoundIsForward); - - TEdge* E2 = ProcessBound(locMin.RightBound, !leftBoundIsForward); - if (E2->OutIdx == Skip) E2 = ProcessBound(E2, !leftBoundIsForward); - - if (locMin.LeftBound->OutIdx == Skip) - locMin.LeftBound = 0; - else if (locMin.RightBound->OutIdx == Skip) - locMin.RightBound = 0; - m_MinimaList.emplace_back(locMin); - if (!leftBoundIsForward) E = E2; - } - return true; -} -//------------------------------------------------------------------------------ - -void ClipperBase::Clear() -{ - m_MinimaList.clear(); - m_edges.clear(); -#ifndef CLIPPERLIB_INT32 - m_UseFullRange = false; -#endif // CLIPPERLIB_INT32 - m_HasOpenPaths = false; -} -//------------------------------------------------------------------------------ - -// Initialize the Local Minima List: -// Sort the LML entries, initialize the left / right bound edges of each Local Minima. -void ClipperBase::Reset() -{ - if (m_MinimaList.empty()) return; //ie nothing to process - std::sort(m_MinimaList.begin(), m_MinimaList.end(), [](const LocalMinimum& lm1, const LocalMinimum& lm2){ return lm1.Y < lm2.Y; }); - - //reset all edges ... - for (LocalMinimum &lm : m_MinimaList) { - TEdge* e = lm.LeftBound; - if (e) - { - e->Curr = e->Bot; - e->Side = esLeft; - e->OutIdx = Unassigned; - } - - e = lm.RightBound; - if (e) - { - e->Curr = e->Bot; - e->Side = esRight; - e->OutIdx = Unassigned; - } - } -} -//------------------------------------------------------------------------------ - -// Get bounds of the edges referenced by the Local Minima List. -// Returns (0,0,0,0) for an empty rectangle. -IntRect ClipperBase::GetBounds() -{ - IntRect result; - auto lm = m_MinimaList.begin(); - if (lm == m_MinimaList.end()) - { - result.left = result.top = result.right = result.bottom = 0; - return result; - } - result.left = lm->LeftBound->Bot.x(); - result.top = lm->LeftBound->Bot.y(); - result.right = lm->LeftBound->Bot.x(); - result.bottom = lm->LeftBound->Bot.y(); - while (lm != m_MinimaList.end()) - { - result.bottom = std::max(result.bottom, lm->LeftBound->Bot.y()); - TEdge* e = lm->LeftBound; - for (;;) { - TEdge* bottomE = e; - while (e->NextInLML) - { - if (e->Bot.x() < result.left) result.left = e->Bot.x(); - if (e->Bot.x() > result.right) result.right = e->Bot.x(); - e = e->NextInLML; - } - result.left = std::min(result.left, e->Bot.x()); - result.right = std::max(result.right, e->Bot.x()); - result.left = std::min(result.left, e->Top.x()); - result.right = std::max(result.right, e->Top.x()); - result.top = std::min(result.top, e->Top.y()); - if (bottomE == lm->LeftBound) e = lm->RightBound; - else break; - } - ++lm; - } - return result; -} - -//------------------------------------------------------------------------------ -// TClipper methods ... -//------------------------------------------------------------------------------ - -Clipper::Clipper(int initOptions) : - ClipperBase(), - m_OutPtsFree(nullptr), - m_OutPtsChunkLast(m_OutPtsChunkSize), - m_ActiveEdges(nullptr), - m_SortedEdges(nullptr) -{ - m_ReverseOutput = ((initOptions & ioReverseSolution) != 0); - m_StrictSimple = ((initOptions & ioStrictlySimple) != 0); - m_PreserveCollinear = ((initOptions & ioPreserveCollinear) != 0); - m_HasOpenPaths = false; -#ifdef CLIPPERLIB_USE_XYZ - m_ZFill = 0; -#endif -} -//------------------------------------------------------------------------------ - -void Clipper::Reset() -{ - ClipperBase::Reset(); - m_Scanbeam = std::priority_queue{}; - m_Maxima.clear(); - m_ActiveEdges = 0; - m_SortedEdges = 0; - for (auto lm = m_MinimaList.rbegin(); lm != m_MinimaList.rend(); ++lm) - m_Scanbeam.push(lm->Y); -} - -//------------------------------------------------------------------------------ - -bool Clipper::Execute(ClipType clipType, Paths &solution, - PolyFillType subjFillType, PolyFillType clipFillType) -{ - if (m_HasOpenPaths) - throw clipperException("Error: PolyTree struct is needed for open path clipping."); - solution.clear(); - m_SubjFillType = subjFillType; - m_ClipFillType = clipFillType; - m_ClipType = clipType; - m_UsingPolyTree = false; - bool succeeded = ExecuteInternal(); - if (succeeded) BuildResult(solution); - DisposeAllOutRecs(); - return succeeded; -} -//------------------------------------------------------------------------------ - -bool Clipper::Execute(ClipType clipType, PolyTree& polytree, - PolyFillType subjFillType, PolyFillType clipFillType) -{ - m_SubjFillType = subjFillType; - m_ClipFillType = clipFillType; - m_ClipType = clipType; - m_UsingPolyTree = true; - bool succeeded = ExecuteInternal(); - if (succeeded) BuildResult2(polytree); - DisposeAllOutRecs(); - return succeeded; -} -//------------------------------------------------------------------------------ - -bool Clipper::ExecuteInternal() -{ - bool succeeded = true; - try { - Reset(); - if (m_MinimaList.empty()) return true; - cInt botY = m_Scanbeam.top(); - do { m_Scanbeam.pop(); } while (! m_Scanbeam.empty() && botY == m_Scanbeam.top()); - do { - InsertLocalMinimaIntoAEL(botY); - ProcessHorizontals(); - m_GhostJoins.clear(); - if (m_Scanbeam.empty()) break; - cInt topY = m_Scanbeam.top(); - do { m_Scanbeam.pop(); } while (! m_Scanbeam.empty() && topY == m_Scanbeam.top()); - succeeded = ProcessIntersections(topY); - if (!succeeded) break; - ProcessEdgesAtTopOfScanbeam(topY); - botY = topY; - } while (!m_Scanbeam.empty() || !m_MinimaList.empty()); - } - catch(...) - { - succeeded = false; - } - - if (succeeded) - { - - //fix orientations ... - //FIXME Vojtech: Does it not invalidate the loop hierarchy maintained as OutRec::FirstLeft pointers? - //FIXME Vojtech: The area is calculated with floats, it may not be numerically stable! - { - for (OutRec &outRec : m_PolyOuts) - if (outRec.Pts && !outRec.IsOpen && (outRec.IsHole ^ m_ReverseOutput) == (Area(outRec) > 0)) - ReversePolyPtLinks(outRec.Pts); - } - - JoinCommonEdges(); - - //unfortunately FixupOutPolygon() must be done after JoinCommonEdges() - { - for (OutRec &outRec : m_PolyOuts) - if (outRec.Pts) { - if (outRec.IsOpen) - // Removes duplicate points. - FixupOutPolyline(outRec); - else - // Removes duplicate points and simplifies consecutive parallel edges by removing the middle vertex. - FixupOutPolygon(outRec); - } - } - // For each polygon, search for exactly duplicate non-successive points. - // If such a point is found, the loop is split into two pieces. - // Search for the duplicate points is O(n^2)! - // http://www.angusj.com/delphi/clipper/documentation/Docs/Units/ClipperLib/Classes/Clipper/Properties/StrictlySimple.htm - if (m_StrictSimple) DoSimplePolygons(); - } - - m_Joins.clear(); - m_GhostJoins.clear(); - return succeeded; -} -//------------------------------------------------------------------------------ - -OutPt* Clipper::AllocateOutPt() -{ - OutPt *pt; - if (m_OutPtsFree) { - // Recycle some of the already released points. - pt = m_OutPtsFree; - m_OutPtsFree = pt->Next; - } else if (m_OutPtsChunkLast < m_OutPtsChunkSize) { - // Get a point from the last chunk. - pt = &m_OutPts.back()[m_OutPtsChunkLast ++]; - } else { - // The last chunk is full. Allocate a new one. - m_OutPts.emplace_back(); - m_OutPtsChunkLast = 1; - pt = &m_OutPts.back().front(); - } - return pt; -} - -void Clipper::DisposeAllOutRecs() -{ - m_OutPts.clear(); - m_OutPtsFree = nullptr; - m_OutPtsChunkLast = m_OutPtsChunkSize; - m_PolyOuts.clear(); -} -//------------------------------------------------------------------------------ - -void Clipper::SetWindingCount(TEdge &edge) const -{ - TEdge *e = edge.PrevInAEL; - //find the edge of the same polytype that immediately preceeds 'edge' in AEL - while (e && ((e->PolyTyp != edge.PolyTyp) || (e->WindDelta == 0))) e = e->PrevInAEL; - if (!e) - { - if (edge.WindDelta == 0) - { - PolyFillType pft = (edge.PolyTyp == ptSubject ? m_SubjFillType : m_ClipFillType); - edge.WindCnt = (pft == pftNegative ? -1 : 1); - } - else - edge.WindCnt = edge.WindDelta; - edge.WindCnt2 = 0; - e = m_ActiveEdges; //ie get ready to calc WindCnt2 - } - else if (edge.WindDelta == 0 && m_ClipType != ctUnion) - { - edge.WindCnt = 1; - edge.WindCnt2 = e->WindCnt2; - e = e->NextInAEL; //ie get ready to calc WindCnt2 - } - else if (IsEvenOddFillType(edge)) - { - //EvenOdd filling ... - if (edge.WindDelta == 0) - { - //are we inside a subj polygon ... - bool Inside = true; - TEdge *e2 = e->PrevInAEL; - while (e2) - { - if (e2->PolyTyp == e->PolyTyp && e2->WindDelta != 0) - Inside = !Inside; - e2 = e2->PrevInAEL; - } - edge.WindCnt = (Inside ? 0 : 1); - } - else - { - edge.WindCnt = edge.WindDelta; - } - edge.WindCnt2 = e->WindCnt2; - e = e->NextInAEL; //ie get ready to calc WindCnt2 - } - else - { - //nonZero, Positive or Negative filling ... - if (e->WindCnt * e->WindDelta < 0) - { - //prev edge is 'decreasing' WindCount (WC) toward zero - //so we're outside the previous polygon ... - if (std::abs(e->WindCnt) > 1) - { - //outside prev poly but still inside another. - //when reversing direction of prev poly use the same WC - if (e->WindDelta * edge.WindDelta < 0) edge.WindCnt = e->WindCnt; - //otherwise continue to 'decrease' WC ... - else edge.WindCnt = e->WindCnt + edge.WindDelta; - } - else - //now outside all polys of same polytype so set own WC ... - edge.WindCnt = (edge.WindDelta == 0 ? 1 : edge.WindDelta); - } else - { - //prev edge is 'increasing' WindCount (WC) away from zero - //so we're inside the previous polygon ... - if (edge.WindDelta == 0) - edge.WindCnt = (e->WindCnt < 0 ? e->WindCnt - 1 : e->WindCnt + 1); - //if wind direction is reversing prev then use same WC - else if (e->WindDelta * edge.WindDelta < 0) edge.WindCnt = e->WindCnt; - //otherwise add to WC ... - else edge.WindCnt = e->WindCnt + edge.WindDelta; - } - edge.WindCnt2 = e->WindCnt2; - e = e->NextInAEL; //ie get ready to calc WindCnt2 - } - - //update WindCnt2 ... - if (IsEvenOddAltFillType(edge)) - { - //EvenOdd filling ... - while (e != &edge) - { - if (e->WindDelta != 0) - edge.WindCnt2 = (edge.WindCnt2 == 0 ? 1 : 0); - e = e->NextInAEL; - } - } else - { - //nonZero, Positive or Negative filling ... - while ( e != &edge ) - { - edge.WindCnt2 += e->WindDelta; - e = e->NextInAEL; - } - } -} -//------------------------------------------------------------------------------ - -bool Clipper::IsContributing(const TEdge& edge) const -{ - PolyFillType pft, pft2; - if (edge.PolyTyp == ptSubject) - { - pft = m_SubjFillType; - pft2 = m_ClipFillType; - } else - { - pft = m_ClipFillType; - pft2 = m_SubjFillType; - } - - switch(pft) - { - case pftEvenOdd: - //return false if a subj line has been flagged as inside a subj polygon - if (edge.WindDelta == 0 && edge.WindCnt != 1) return false; - break; - case pftNonZero: - if (std::abs(edge.WindCnt) != 1) return false; - break; - case pftPositive: - if (edge.WindCnt != 1) return false; - break; - default: //pftNegative - if (edge.WindCnt != -1) return false; - } - - switch(m_ClipType) - { - case ctIntersection: - switch(pft2) - { - case pftEvenOdd: - case pftNonZero: - return (edge.WindCnt2 != 0); - case pftPositive: - return (edge.WindCnt2 > 0); - default: - return (edge.WindCnt2 < 0); - } - break; - case ctUnion: - switch(pft2) - { - case pftEvenOdd: - case pftNonZero: - return (edge.WindCnt2 == 0); - case pftPositive: - return (edge.WindCnt2 <= 0); - default: - return (edge.WindCnt2 >= 0); - } - break; - case ctDifference: - if (edge.PolyTyp == ptSubject) - switch(pft2) - { - case pftEvenOdd: - case pftNonZero: - return (edge.WindCnt2 == 0); - case pftPositive: - return (edge.WindCnt2 <= 0); - default: - return (edge.WindCnt2 >= 0); - } - else - switch(pft2) - { - case pftEvenOdd: - case pftNonZero: - return (edge.WindCnt2 != 0); - case pftPositive: - return (edge.WindCnt2 > 0); - default: - return (edge.WindCnt2 < 0); - } - break; - case ctXor: - if (edge.WindDelta == 0) //XOr always contributing unless open - switch(pft2) - { - case pftEvenOdd: - case pftNonZero: - return (edge.WindCnt2 == 0); - case pftPositive: - return (edge.WindCnt2 <= 0); - default: - return (edge.WindCnt2 >= 0); - } - else - return true; - break; - default: - return true; - } -} -//------------------------------------------------------------------------------ - -// Called from Clipper::InsertLocalMinimaIntoAEL() and Clipper::IntersectEdges(). -OutPt* Clipper::AddLocalMinPoly(TEdge *e1, TEdge *e2, const IntPoint &Pt) -{ - OutPt* result; - TEdge *e, *prevE; - if (IsHorizontal(*e2) || ( e1->Dx > e2->Dx )) - { - result = AddOutPt(e1, Pt); - e2->OutIdx = e1->OutIdx; - e1->Side = esLeft; - e2->Side = esRight; - e = e1; - if (e->PrevInAEL == e2) - prevE = e2->PrevInAEL; - else - prevE = e->PrevInAEL; - } else - { - result = AddOutPt(e2, Pt); - e1->OutIdx = e2->OutIdx; - e1->Side = esRight; - e2->Side = esLeft; - e = e2; - if (e->PrevInAEL == e1) - prevE = e1->PrevInAEL; - else - prevE = e->PrevInAEL; - } - - if (prevE && prevE->OutIdx >= 0 && prevE->Top.y() < Pt.y() && e->Top.y() < Pt.y()) - { - cInt xPrev = TopX(*prevE, Pt.y()); - cInt xE = TopX(*e, Pt.y()); - if (xPrev == xE && (e->WindDelta != 0) && (prevE->WindDelta != 0) && - SlopesEqual(IntPoint2d(xPrev, Pt.y()), prevE->Top, IntPoint2d(xE, Pt.y()), e->Top, m_UseFullRange)) - { - OutPt* outPt = AddOutPt(prevE, Pt); - m_Joins.emplace_back(Join(result, outPt, e->Top)); - } - } - return result; -} -//------------------------------------------------------------------------------ - -void Clipper::AddLocalMaxPoly(TEdge *e1, TEdge *e2, const IntPoint &Pt) -{ - AddOutPt( e1, Pt ); - if (e2->WindDelta == 0) AddOutPt(e2, Pt); - if( e1->OutIdx == e2->OutIdx ) - { - e1->OutIdx = Unassigned; - e2->OutIdx = Unassigned; - } - else if (e1->OutIdx < e2->OutIdx) - AppendPolygon(e1, e2); - else - AppendPolygon(e2, e1); -} -//------------------------------------------------------------------------------ - -void Clipper::AddEdgeToSEL(TEdge *edge) -{ - //SEL pointers in PEdge are reused to build a list of horizontal edges. - //However, we don't need to worry about order with horizontal edge processing. - if( !m_SortedEdges ) - { - m_SortedEdges = edge; - edge->PrevInSEL = 0; - edge->NextInSEL = 0; - } - else - { - edge->NextInSEL = m_SortedEdges; - edge->PrevInSEL = 0; - m_SortedEdges->PrevInSEL = edge; - m_SortedEdges = edge; - } -} -//------------------------------------------------------------------------------ - -void Clipper::CopyAELToSEL() -{ - TEdge* e = m_ActiveEdges; - m_SortedEdges = e; - while ( e ) - { - e->PrevInSEL = e->PrevInAEL; - e->NextInSEL = e->NextInAEL; - e = e->NextInAEL; - } -} - -//------------------------------------------------------------------------------ - -// Called from Clipper::ExecuteInternal() -void Clipper::InsertLocalMinimaIntoAEL(const cInt botY) -{ - while (!m_MinimaList.empty() && m_MinimaList.back().Y == botY) - { - TEdge* lb = m_MinimaList.back().LeftBound; - TEdge* rb = m_MinimaList.back().RightBound; - m_MinimaList.pop_back(); - - OutPt *Op1 = 0; - if (!lb) - { - //nb: don't insert LB into either AEL or SEL - InsertEdgeIntoAEL(rb, 0); - SetWindingCount(*rb); - if (IsContributing(*rb)) - Op1 = AddOutPt(rb, rb->Bot); - } - else if (!rb) - { - InsertEdgeIntoAEL(lb, 0); - SetWindingCount(*lb); - if (IsContributing(*lb)) - Op1 = AddOutPt(lb, lb->Bot); - m_Scanbeam.push(lb->Top.y()); - } - else - { - InsertEdgeIntoAEL(lb, 0); - InsertEdgeIntoAEL(rb, lb); - SetWindingCount( *lb ); - rb->WindCnt = lb->WindCnt; - rb->WindCnt2 = lb->WindCnt2; - if (IsContributing(*lb)) - Op1 = AddLocalMinPoly(lb, rb, lb->Bot); - m_Scanbeam.push(lb->Top.y()); - } - - if (rb) - { - if (IsHorizontal(*rb)) - { - AddEdgeToSEL(rb); - if (rb->NextInLML) - m_Scanbeam.push(rb->NextInLML->Top.y()); - } - else m_Scanbeam.push(rb->Top.y()); - } - - if (!lb || !rb) continue; - - //if any output polygons share an edge, they'll need joining later ... - if (Op1 && IsHorizontal(*rb) && - m_GhostJoins.size() > 0 && (rb->WindDelta != 0)) - { - for (Join &jr : m_GhostJoins) - //if the horizontal Rb and a 'ghost' horizontal overlap, then convert - //the 'ghost' join to a real join ready for later ... - if (HorzSegmentsOverlap(jr.OutPt1->Pt.x(), jr.OffPt.x(), rb->Bot.x(), rb->Top.x())) - m_Joins.emplace_back(Join(jr.OutPt1, Op1, jr.OffPt)); - } - - if (lb->OutIdx >= 0 && lb->PrevInAEL && - lb->PrevInAEL->Curr.x() == lb->Bot.x() && - lb->PrevInAEL->OutIdx >= 0 && - SlopesEqual(lb->PrevInAEL->Bot, lb->PrevInAEL->Top, lb->Curr, lb->Top, m_UseFullRange) && - (lb->WindDelta != 0) && (lb->PrevInAEL->WindDelta != 0)) - { - OutPt *Op2 = AddOutPt(lb->PrevInAEL, lb->Bot); - m_Joins.emplace_back(Join(Op1, Op2, lb->Top)); - } - - if(lb->NextInAEL != rb) - { - - if (rb->OutIdx >= 0 && rb->PrevInAEL->OutIdx >= 0 && - SlopesEqual(rb->PrevInAEL->Curr, rb->PrevInAEL->Top, rb->Curr, rb->Top, m_UseFullRange) && - (rb->WindDelta != 0) && (rb->PrevInAEL->WindDelta != 0)) - { - OutPt *Op2 = AddOutPt(rb->PrevInAEL, rb->Bot); - m_Joins.emplace_back(Join(Op1, Op2, rb->Top)); - } - - TEdge* e = lb->NextInAEL; - if (e) - { - while( e != rb ) - { - //nb: For calculating winding counts etc, IntersectEdges() assumes - //that param1 will be to the Right of param2 ABOVE the intersection ... - IntersectEdges(rb , e , lb->Curr); //order important here - e = e->NextInAEL; - } - } - } - - } -} -//------------------------------------------------------------------------------ - -void Clipper::DeleteFromAEL(TEdge *e) -{ - TEdge* AelPrev = e->PrevInAEL; - TEdge* AelNext = e->NextInAEL; - if( !AelPrev && !AelNext && (e != m_ActiveEdges) ) return; //already deleted - if( AelPrev ) AelPrev->NextInAEL = AelNext; - else m_ActiveEdges = AelNext; - if( AelNext ) AelNext->PrevInAEL = AelPrev; - e->NextInAEL = 0; - e->PrevInAEL = 0; -} -//------------------------------------------------------------------------------ - -void Clipper::DeleteFromSEL(TEdge *e) -{ - TEdge* SelPrev = e->PrevInSEL; - TEdge* SelNext = e->NextInSEL; - if( !SelPrev && !SelNext && (e != m_SortedEdges) ) return; //already deleted - if( SelPrev ) SelPrev->NextInSEL = SelNext; - else m_SortedEdges = SelNext; - if( SelNext ) SelNext->PrevInSEL = SelPrev; - e->NextInSEL = 0; - e->PrevInSEL = 0; -} -//------------------------------------------------------------------------------ - -#ifdef CLIPPERLIB_USE_XYZ -void Clipper::SetZ(IntPoint& pt, TEdge& e1, TEdge& e2) -{ - if (pt.z() != 0 || !m_ZFill) return; - else if (pt == e1.Bot) pt.z() = e1.Bot.z(); - else if (pt == e1.Top) pt.z() = e1.Top.z(); - else if (pt == e2.Bot) pt.z() = e2.Bot.z(); - else if (pt == e2.Top) pt.z() = e2.Top.z(); - else m_ZFill(e1.Bot, e1.Top, e2.Bot, e2.Top, pt); -} -//------------------------------------------------------------------------------ -#endif - -void Clipper::IntersectEdges(TEdge *e1, TEdge *e2, IntPoint &Pt) -{ - bool e1Contributing = ( e1->OutIdx >= 0 ); - bool e2Contributing = ( e2->OutIdx >= 0 ); - -#ifdef CLIPPERLIB_USE_XYZ - SetZ(Pt, *e1, *e2); -#endif - -#ifdef use_lines - //if either edge is on an OPEN path ... - if (e1->WindDelta == 0 || e2->WindDelta == 0) - { - //ignore subject-subject open path intersections UNLESS they - //are both open paths, AND they are both 'contributing maximas' ... - if (e1->WindDelta == 0 && e2->WindDelta == 0) return; - - //if intersecting a subj line with a subj poly ... - else if (e1->PolyTyp == e2->PolyTyp && - e1->WindDelta != e2->WindDelta && m_ClipType == ctUnion) - { - if (e1->WindDelta == 0) - { - if (e2Contributing) - { - AddOutPt(e1, Pt); - if (e1Contributing) e1->OutIdx = Unassigned; - } - } - else - { - if (e1Contributing) - { - AddOutPt(e2, Pt); - if (e2Contributing) e2->OutIdx = Unassigned; - } - } - } - else if (e1->PolyTyp != e2->PolyTyp) - { - //toggle subj open path OutIdx on/off when Abs(clip.WndCnt) == 1 ... - if ((e1->WindDelta == 0) && std::abs(e2->WindCnt) == 1 && - (m_ClipType != ctUnion || e2->WindCnt2 == 0)) - { - AddOutPt(e1, Pt); - if (e1Contributing) e1->OutIdx = Unassigned; - } - else if ((e2->WindDelta == 0) && (std::abs(e1->WindCnt) == 1) && - (m_ClipType != ctUnion || e1->WindCnt2 == 0)) - { - AddOutPt(e2, Pt); - if (e2Contributing) e2->OutIdx = Unassigned; - } - } - return; - } -#endif - - //update winding counts... - //assumes that e1 will be to the Right of e2 ABOVE the intersection - if ( e1->PolyTyp == e2->PolyTyp ) - { - if ( IsEvenOddFillType( *e1) ) - { - int oldE1WindCnt = e1->WindCnt; - e1->WindCnt = e2->WindCnt; - e2->WindCnt = oldE1WindCnt; - } else - { - if (e1->WindCnt + e2->WindDelta == 0 ) e1->WindCnt = -e1->WindCnt; - else e1->WindCnt += e2->WindDelta; - if ( e2->WindCnt - e1->WindDelta == 0 ) e2->WindCnt = -e2->WindCnt; - else e2->WindCnt -= e1->WindDelta; - } - } else - { - if (!IsEvenOddFillType(*e2)) e1->WindCnt2 += e2->WindDelta; - else e1->WindCnt2 = ( e1->WindCnt2 == 0 ) ? 1 : 0; - if (!IsEvenOddFillType(*e1)) e2->WindCnt2 -= e1->WindDelta; - else e2->WindCnt2 = ( e2->WindCnt2 == 0 ) ? 1 : 0; - } - - PolyFillType e1FillType, e2FillType, e1FillType2, e2FillType2; - if (e1->PolyTyp == ptSubject) - { - e1FillType = m_SubjFillType; - e1FillType2 = m_ClipFillType; - } else - { - e1FillType = m_ClipFillType; - e1FillType2 = m_SubjFillType; - } - if (e2->PolyTyp == ptSubject) - { - e2FillType = m_SubjFillType; - e2FillType2 = m_ClipFillType; - } else - { - e2FillType = m_ClipFillType; - e2FillType2 = m_SubjFillType; - } - - cInt e1Wc, e2Wc; - switch (e1FillType) - { - case pftPositive: e1Wc = e1->WindCnt; break; - case pftNegative: e1Wc = -e1->WindCnt; break; - default: e1Wc = std::abs(e1->WindCnt); - } - switch(e2FillType) - { - case pftPositive: e2Wc = e2->WindCnt; break; - case pftNegative: e2Wc = -e2->WindCnt; break; - default: e2Wc = std::abs(e2->WindCnt); - } - - if ( e1Contributing && e2Contributing ) - { - if ((e1Wc != 0 && e1Wc != 1) || (e2Wc != 0 && e2Wc != 1) || - (e1->PolyTyp != e2->PolyTyp && m_ClipType != ctXor) ) - { - AddLocalMaxPoly(e1, e2, Pt); - } - else - { - AddOutPt(e1, Pt); - AddOutPt(e2, Pt); - std::swap(e1->Side, e2->Side); - std::swap(e1->OutIdx, e2->OutIdx); - } - } - else if ( e1Contributing ) - { - if (e2Wc == 0 || e2Wc == 1) - { - AddOutPt(e1, Pt); - std::swap(e1->Side, e2->Side); - std::swap(e1->OutIdx, e2->OutIdx); - } - } - else if ( e2Contributing ) - { - if (e1Wc == 0 || e1Wc == 1) - { - AddOutPt(e2, Pt); - std::swap(e1->Side, e2->Side); - std::swap(e1->OutIdx, e2->OutIdx); - } - } - else if ( (e1Wc == 0 || e1Wc == 1) && (e2Wc == 0 || e2Wc == 1)) - { - //neither edge is currently contributing ... - - cInt e1Wc2, e2Wc2; - switch (e1FillType2) - { - case pftPositive: e1Wc2 = e1->WindCnt2; break; - case pftNegative : e1Wc2 = -e1->WindCnt2; break; - default: e1Wc2 = std::abs(e1->WindCnt2); - } - switch (e2FillType2) - { - case pftPositive: e2Wc2 = e2->WindCnt2; break; - case pftNegative: e2Wc2 = -e2->WindCnt2; break; - default: e2Wc2 = std::abs(e2->WindCnt2); - } - - if (e1->PolyTyp != e2->PolyTyp) - { - AddLocalMinPoly(e1, e2, Pt); - } - else if (e1Wc == 1 && e2Wc == 1) - switch( m_ClipType ) { - case ctIntersection: - if (e1Wc2 > 0 && e2Wc2 > 0) - AddLocalMinPoly(e1, e2, Pt); - break; - case ctUnion: - if ( e1Wc2 <= 0 && e2Wc2 <= 0 ) - AddLocalMinPoly(e1, e2, Pt); - break; - case ctDifference: - if (((e1->PolyTyp == ptClip) && (e1Wc2 > 0) && (e2Wc2 > 0)) || - ((e1->PolyTyp == ptSubject) && (e1Wc2 <= 0) && (e2Wc2 <= 0))) - AddLocalMinPoly(e1, e2, Pt); - break; - case ctXor: - AddLocalMinPoly(e1, e2, Pt); - } - else - std::swap(e1->Side, e2->Side); - } -} -//------------------------------------------------------------------------------ - -void Clipper::SetHoleState(TEdge *e, OutRec *outrec) -{ - TEdge *e2 = e->PrevInAEL; - TEdge *eTmp = 0; - while (e2) - { - if (e2->OutIdx >= 0 && e2->WindDelta != 0) - { - if (!eTmp) eTmp = e2; - else if (eTmp->OutIdx == e2->OutIdx) eTmp = 0; - } - e2 = e2->PrevInAEL; - } - if (!eTmp) - { - outrec->FirstLeft = 0; - outrec->IsHole = false; - } - else - { - outrec->FirstLeft = &m_PolyOuts[eTmp->OutIdx]; - outrec->IsHole = !outrec->FirstLeft->IsHole; - } -} -//------------------------------------------------------------------------------ - -OutRec* GetLowermostRec(OutRec *outRec1, OutRec *outRec2) -{ - //work out which polygon fragment has the correct hole state ... - if (!outRec1->BottomPt) - outRec1->BottomPt = GetBottomPt(outRec1->Pts); - if (!outRec2->BottomPt) - outRec2->BottomPt = GetBottomPt(outRec2->Pts); - OutPt *OutPt1 = outRec1->BottomPt; - OutPt *OutPt2 = outRec2->BottomPt; - if (OutPt1->Pt.y() > OutPt2->Pt.y()) return outRec1; - else if (OutPt1->Pt.y() < OutPt2->Pt.y()) return outRec2; - else if (OutPt1->Pt.x() < OutPt2->Pt.x()) return outRec1; - else if (OutPt1->Pt.x() > OutPt2->Pt.x()) return outRec2; - else if (OutPt1->Next == OutPt1) return outRec2; - else if (OutPt2->Next == OutPt2) return outRec1; - else if (FirstIsBottomPt(OutPt1, OutPt2)) return outRec1; - else return outRec2; -} -//------------------------------------------------------------------------------ - -bool OutRec1RightOfOutRec2(OutRec* outRec1, OutRec* outRec2) -{ - do - { - outRec1 = outRec1->FirstLeft; - if (outRec1 == outRec2) return true; - } while (outRec1); - return false; -} -//------------------------------------------------------------------------------ - -OutRec* Clipper::GetOutRec(int Idx) -{ - OutRec* outrec = &m_PolyOuts[Idx]; - while (outrec != &m_PolyOuts[outrec->Idx]) - outrec = &m_PolyOuts[outrec->Idx]; - return outrec; -} -//------------------------------------------------------------------------------ - -void Clipper::AppendPolygon(TEdge *e1, TEdge *e2) -{ - //get the start and ends of both output polygons ... - OutRec *outRec1 = &m_PolyOuts[e1->OutIdx]; - OutRec *outRec2 = &m_PolyOuts[e2->OutIdx]; - - OutRec *holeStateRec; - if (OutRec1RightOfOutRec2(outRec1, outRec2)) - holeStateRec = outRec2; - else if (OutRec1RightOfOutRec2(outRec2, outRec1)) - holeStateRec = outRec1; - else - holeStateRec = GetLowermostRec(outRec1, outRec2); - - //get the start and ends of both output polygons and - //join e2 poly onto e1 poly and delete pointers to e2 ... - - OutPt* p1_lft = outRec1->Pts; - OutPt* p1_rt = p1_lft->Prev; - OutPt* p2_lft = outRec2->Pts; - OutPt* p2_rt = p2_lft->Prev; - - //join e2 poly onto e1 poly and delete pointers to e2 ... - if( e1->Side == esLeft ) - { - if( e2->Side == esLeft ) - { - //z y x a b c - ReversePolyPtLinks(p2_lft); - p2_lft->Next = p1_lft; - p1_lft->Prev = p2_lft; - p1_rt->Next = p2_rt; - p2_rt->Prev = p1_rt; - outRec1->Pts = p2_rt; - } else - { - //x y z a b c - p2_rt->Next = p1_lft; - p1_lft->Prev = p2_rt; - p2_lft->Prev = p1_rt; - p1_rt->Next = p2_lft; - outRec1->Pts = p2_lft; - } - } else - { - if( e2->Side == esRight ) - { - //a b c z y x - ReversePolyPtLinks(p2_lft); - p1_rt->Next = p2_rt; - p2_rt->Prev = p1_rt; - p2_lft->Next = p1_lft; - p1_lft->Prev = p2_lft; - } else - { - //a b c x y z - p1_rt->Next = p2_lft; - p2_lft->Prev = p1_rt; - p1_lft->Prev = p2_rt; - p2_rt->Next = p1_lft; - } - } - - outRec1->BottomPt = 0; - if (holeStateRec == outRec2) - { - if (outRec2->FirstLeft != outRec1) - outRec1->FirstLeft = outRec2->FirstLeft; - outRec1->IsHole = outRec2->IsHole; - } - outRec2->Pts = 0; - outRec2->BottomPt = 0; - outRec2->FirstLeft = outRec1; - - int OKIdx = e1->OutIdx; - int ObsoleteIdx = e2->OutIdx; - - e1->OutIdx = Unassigned; //nb: safe because we only get here via AddLocalMaxPoly - e2->OutIdx = Unassigned; - - TEdge* e = m_ActiveEdges; - while( e ) - { - if( e->OutIdx == ObsoleteIdx ) - { - e->OutIdx = OKIdx; - e->Side = e1->Side; - break; - } - e = e->NextInAEL; - } - - outRec2->Idx = outRec1->Idx; -} -//------------------------------------------------------------------------------ - -OutRec* Clipper::CreateOutRec() -{ - m_PolyOuts.emplace_back(); - OutRec &result = m_PolyOuts.back(); - result.IsHole = false; - result.IsOpen = false; - result.FirstLeft = 0; - result.Pts = 0; - result.BottomPt = 0; - result.PolyNd = 0; - result.Idx = (int)m_PolyOuts.size()-1; - return &result; -} -//------------------------------------------------------------------------------ - -OutPt* Clipper::AddOutPt(TEdge *e, const IntPoint &pt) -{ - if( e->OutIdx < 0 ) - { - OutRec *outRec = CreateOutRec(); - outRec->IsOpen = (e->WindDelta == 0); - OutPt* newOp = this->AllocateOutPt(); - outRec->Pts = newOp; - newOp->Idx = outRec->Idx; - newOp->Pt = pt; - newOp->Next = newOp; - newOp->Prev = newOp; - if (!outRec->IsOpen) - SetHoleState(e, outRec); - e->OutIdx = outRec->Idx; - return newOp; - } else - { - OutRec *outRec = &m_PolyOuts[e->OutIdx]; - //OutRec.Pts is the 'Left-most' point & OutRec.Pts.Prev is the 'Right-most' - OutPt* op = outRec->Pts; - - bool ToFront = (e->Side == esLeft); - if (ToFront && (pt == op->Pt)) return op; - else if (!ToFront && (pt == op->Prev->Pt)) return op->Prev; - - OutPt* newOp = this->AllocateOutPt(); - newOp->Idx = outRec->Idx; - newOp->Pt = pt; - newOp->Next = op; - newOp->Prev = op->Prev; - newOp->Prev->Next = newOp; - op->Prev = newOp; - if (ToFront) outRec->Pts = newOp; - return newOp; - } -} -//------------------------------------------------------------------------------ - -OutPt* Clipper::GetLastOutPt(TEdge *e) -{ - OutRec *outRec = &m_PolyOuts[e->OutIdx]; - if (e->Side == esLeft) - return outRec->Pts; - else - return outRec->Pts->Prev; -} -//------------------------------------------------------------------------------ - -void Clipper::ProcessHorizontals() -{ - TEdge* horzEdge = m_SortedEdges; - while(horzEdge) - { - DeleteFromSEL(horzEdge); - ProcessHorizontal(horzEdge); - horzEdge = m_SortedEdges; - } -} -//------------------------------------------------------------------------------ - -inline bool IsMaxima(TEdge *e, const cInt Y) -{ - return e && e->Top.y() == Y && !e->NextInLML; -} -//------------------------------------------------------------------------------ - -inline bool IsIntermediate(TEdge *e, const cInt Y) -{ - return e->Top.y() == Y && e->NextInLML; -} -//------------------------------------------------------------------------------ - -inline TEdge *GetMaximaPair(TEdge *e) -{ - if ((e->Next->Top == e->Top) && !e->Next->NextInLML) - return e->Next; - else if ((e->Prev->Top == e->Top) && !e->Prev->NextInLML) - return e->Prev; - else return 0; -} -//------------------------------------------------------------------------------ - -inline TEdge *GetMaximaPairEx(TEdge *e) -{ - //as GetMaximaPair() but returns 0 if MaxPair isn't in AEL (unless it's horizontal) - TEdge* result = GetMaximaPair(e); - if (result && (result->OutIdx == Skip || - (result->NextInAEL == result->PrevInAEL && !IsHorizontal(*result)))) return 0; - return result; -} -//------------------------------------------------------------------------------ - -void Clipper::SwapPositionsInAEL(TEdge *Edge1, TEdge *Edge2) -{ - //check that one or other edge hasn't already been removed from AEL ... - if (Edge1->NextInAEL == Edge1->PrevInAEL || - Edge2->NextInAEL == Edge2->PrevInAEL) return; - - if( Edge1->NextInAEL == Edge2 ) - { - TEdge* Next = Edge2->NextInAEL; - if( Next ) Next->PrevInAEL = Edge1; - TEdge* Prev = Edge1->PrevInAEL; - if( Prev ) Prev->NextInAEL = Edge2; - Edge2->PrevInAEL = Prev; - Edge2->NextInAEL = Edge1; - Edge1->PrevInAEL = Edge2; - Edge1->NextInAEL = Next; - } - else if( Edge2->NextInAEL == Edge1 ) - { - TEdge* Next = Edge1->NextInAEL; - if( Next ) Next->PrevInAEL = Edge2; - TEdge* Prev = Edge2->PrevInAEL; - if( Prev ) Prev->NextInAEL = Edge1; - Edge1->PrevInAEL = Prev; - Edge1->NextInAEL = Edge2; - Edge2->PrevInAEL = Edge1; - Edge2->NextInAEL = Next; - } - else - { - TEdge* Next = Edge1->NextInAEL; - TEdge* Prev = Edge1->PrevInAEL; - Edge1->NextInAEL = Edge2->NextInAEL; - if( Edge1->NextInAEL ) Edge1->NextInAEL->PrevInAEL = Edge1; - Edge1->PrevInAEL = Edge2->PrevInAEL; - if( Edge1->PrevInAEL ) Edge1->PrevInAEL->NextInAEL = Edge1; - Edge2->NextInAEL = Next; - if( Edge2->NextInAEL ) Edge2->NextInAEL->PrevInAEL = Edge2; - Edge2->PrevInAEL = Prev; - if( Edge2->PrevInAEL ) Edge2->PrevInAEL->NextInAEL = Edge2; - } - - if( !Edge1->PrevInAEL ) m_ActiveEdges = Edge1; - else if( !Edge2->PrevInAEL ) m_ActiveEdges = Edge2; -} -//------------------------------------------------------------------------------ - -void Clipper::SwapPositionsInSEL(TEdge *Edge1, TEdge *Edge2) -{ - if( !( Edge1->NextInSEL ) && !( Edge1->PrevInSEL ) ) return; - if( !( Edge2->NextInSEL ) && !( Edge2->PrevInSEL ) ) return; - - if( Edge1->NextInSEL == Edge2 ) - { - TEdge* Next = Edge2->NextInSEL; - if( Next ) Next->PrevInSEL = Edge1; - TEdge* Prev = Edge1->PrevInSEL; - if( Prev ) Prev->NextInSEL = Edge2; - Edge2->PrevInSEL = Prev; - Edge2->NextInSEL = Edge1; - Edge1->PrevInSEL = Edge2; - Edge1->NextInSEL = Next; - } - else if( Edge2->NextInSEL == Edge1 ) - { - TEdge* Next = Edge1->NextInSEL; - if( Next ) Next->PrevInSEL = Edge2; - TEdge* Prev = Edge2->PrevInSEL; - if( Prev ) Prev->NextInSEL = Edge1; - Edge1->PrevInSEL = Prev; - Edge1->NextInSEL = Edge2; - Edge2->PrevInSEL = Edge1; - Edge2->NextInSEL = Next; - } - else - { - TEdge* Next = Edge1->NextInSEL; - TEdge* Prev = Edge1->PrevInSEL; - Edge1->NextInSEL = Edge2->NextInSEL; - if( Edge1->NextInSEL ) Edge1->NextInSEL->PrevInSEL = Edge1; - Edge1->PrevInSEL = Edge2->PrevInSEL; - if( Edge1->PrevInSEL ) Edge1->PrevInSEL->NextInSEL = Edge1; - Edge2->NextInSEL = Next; - if( Edge2->NextInSEL ) Edge2->NextInSEL->PrevInSEL = Edge2; - Edge2->PrevInSEL = Prev; - if( Edge2->PrevInSEL ) Edge2->PrevInSEL->NextInSEL = Edge2; - } - - if( !Edge1->PrevInSEL ) m_SortedEdges = Edge1; - else if( !Edge2->PrevInSEL ) m_SortedEdges = Edge2; -} -//------------------------------------------------------------------------------ - -inline void GetHorzDirection(TEdge& HorzEdge, Direction& Dir, cInt& Left, cInt& Right) -{ - if (HorzEdge.Bot.x() < HorzEdge.Top.x()) - { - Left = HorzEdge.Bot.x(); - Right = HorzEdge.Top.x(); - Dir = dLeftToRight; - } else - { - Left = HorzEdge.Top.x(); - Right = HorzEdge.Bot.x(); - Dir = dRightToLeft; - } -} -//------------------------------------------------------------------------ - -/******************************************************************************* -* Notes: Horizontal edges (HEs) at scanline intersections (ie at the Top or * -* Bottom of a scanbeam) are processed as if layered. The order in which HEs * -* are processed doesn't matter. HEs intersect with other HE Bot.x()s only [#] * -* (or they could intersect with Top.x()s only, ie EITHER Bot.x()s OR Top.x()s), * -* and with other non-horizontal edges [*]. Once these intersections are * -* processed, intermediate HEs then 'promote' the Edge above (NextInLML) into * -* the AEL. These 'promoted' edges may in turn intersect [%] with other HEs. * -*******************************************************************************/ - -void Clipper::ProcessHorizontal(TEdge *horzEdge) -{ - Direction dir; - cInt horzLeft, horzRight; - bool IsOpen = (horzEdge->WindDelta == 0); - - GetHorzDirection(*horzEdge, dir, horzLeft, horzRight); - - TEdge* eLastHorz = horzEdge, *eMaxPair = 0; - while (eLastHorz->NextInLML && IsHorizontal(*eLastHorz->NextInLML)) - eLastHorz = eLastHorz->NextInLML; - if (!eLastHorz->NextInLML) - eMaxPair = GetMaximaPair(eLastHorz); - - cInts::const_iterator maxIt; - cInts::const_reverse_iterator maxRit; - if (!m_Maxima.empty()) - { - //get the first maxima in range (X) ... - if (dir == dLeftToRight) - { - maxIt = m_Maxima.begin(); - while (maxIt != m_Maxima.end() && *maxIt <= horzEdge->Bot.x()) ++maxIt; - if (maxIt != m_Maxima.end() && *maxIt >= eLastHorz->Top.x()) - maxIt = m_Maxima.end(); - } - else - { - maxRit = m_Maxima.rbegin(); - while (maxRit != m_Maxima.rend() && *maxRit > horzEdge->Bot.x()) ++maxRit; - if (maxRit != m_Maxima.rend() && *maxRit <= eLastHorz->Top.x()) - maxRit = m_Maxima.rend(); - } - } - - OutPt* op1 = 0; - - for (;;) //loop through consec. horizontal edges - { - - bool IsLastHorz = (horzEdge == eLastHorz); - TEdge* e = (dir == dLeftToRight) ? horzEdge->NextInAEL : horzEdge->PrevInAEL; - while(e) - { - - //this code block inserts extra coords into horizontal edges (in output - //polygons) whereever maxima touch these horizontal edges. This helps - //'simplifying' polygons (ie if the Simplify property is set). - if (!m_Maxima.empty()) - { - if (dir == dLeftToRight) - { - while (maxIt != m_Maxima.end() && *maxIt < e->Curr.x()) - { - if (horzEdge->OutIdx >= 0 && !IsOpen) - AddOutPt(horzEdge, IntPoint2d(*maxIt, horzEdge->Bot.y())); - ++maxIt; - } - } - else - { - while (maxRit != m_Maxima.rend() && *maxRit > e->Curr.x()) - { - if (horzEdge->OutIdx >= 0 && !IsOpen) - AddOutPt(horzEdge, IntPoint2d(*maxRit, horzEdge->Bot.y())); - ++maxRit; - } - } - }; - - if ((dir == dLeftToRight && e->Curr.x() > horzRight) || - (dir == dRightToLeft && e->Curr.x() < horzLeft)) break; - - //Also break if we've got to the end of an intermediate horizontal edge ... - //nb: Smaller Dx's are to the right of larger Dx's ABOVE the horizontal. - if (e->Curr.x() == horzEdge->Top.x() && horzEdge->NextInLML && - e->Dx < horzEdge->NextInLML->Dx) break; - - if (horzEdge->OutIdx >= 0 && !IsOpen) //note: may be done multiple times - { -#ifdef CLIPPERLIB_USE_XYZ - if (dir == dLeftToRight) - SetZ(e->Curr, *horzEdge, *e); - else - SetZ(e->Curr, *e, *horzEdge); -#endif - op1 = AddOutPt(horzEdge, e->Curr); - TEdge* eNextHorz = m_SortedEdges; - while (eNextHorz) - { - if (eNextHorz->OutIdx >= 0 && - HorzSegmentsOverlap(horzEdge->Bot.x(), - horzEdge->Top.x(), eNextHorz->Bot.x(), eNextHorz->Top.x())) - { - OutPt* op2 = GetLastOutPt(eNextHorz); - m_Joins.emplace_back(Join(op2, op1, eNextHorz->Top)); - } - eNextHorz = eNextHorz->NextInSEL; - } - m_GhostJoins.emplace_back(Join(op1, 0, horzEdge->Bot)); - } - - //OK, so far we're still in range of the horizontal Edge but make sure - //we're at the last of consec. horizontals when matching with eMaxPair - if(e == eMaxPair && IsLastHorz) - { - if (horzEdge->OutIdx >= 0) - AddLocalMaxPoly(horzEdge, eMaxPair, horzEdge->Top); - DeleteFromAEL(horzEdge); - DeleteFromAEL(eMaxPair); - return; - } - - if(dir == dLeftToRight) - { - IntPoint Pt = IntPoint2d(e->Curr.x(), horzEdge->Curr.y()); - IntersectEdges(horzEdge, e, Pt); - } - else - { - IntPoint Pt = IntPoint2d(e->Curr.x(), horzEdge->Curr.y()); - IntersectEdges( e, horzEdge, Pt); - } - TEdge* eNext = (dir == dLeftToRight) ? e->NextInAEL : e->PrevInAEL; - SwapPositionsInAEL( horzEdge, e ); - e = eNext; - } //end while(e) - - //Break out of loop if HorzEdge.NextInLML is not also horizontal ... - if (!horzEdge->NextInLML || !IsHorizontal(*horzEdge->NextInLML)) break; - - UpdateEdgeIntoAEL(horzEdge); - if (horzEdge->OutIdx >= 0) AddOutPt(horzEdge, horzEdge->Bot); - GetHorzDirection(*horzEdge, dir, horzLeft, horzRight); - - } //end for (;;) - - if (horzEdge->OutIdx >= 0 && !op1) - { - op1 = GetLastOutPt(horzEdge); - TEdge* eNextHorz = m_SortedEdges; - while (eNextHorz) - { - if (eNextHorz->OutIdx >= 0 && - HorzSegmentsOverlap(horzEdge->Bot.x(), - horzEdge->Top.x(), eNextHorz->Bot.x(), eNextHorz->Top.x())) - { - OutPt* op2 = GetLastOutPt(eNextHorz); - m_Joins.emplace_back(Join(op2, op1, eNextHorz->Top)); - } - eNextHorz = eNextHorz->NextInSEL; - } - m_GhostJoins.emplace_back(Join(op1, 0, horzEdge->Top)); - } - - if (horzEdge->NextInLML) - { - if(horzEdge->OutIdx >= 0) - { - op1 = AddOutPt( horzEdge, horzEdge->Top); - UpdateEdgeIntoAEL(horzEdge); - if (horzEdge->WindDelta == 0) return; - //nb: HorzEdge is no longer horizontal here - TEdge* ePrev = horzEdge->PrevInAEL; - TEdge* eNext = horzEdge->NextInAEL; - if (ePrev && ePrev->Curr.x() == horzEdge->Bot.x() && - ePrev->Curr.y() == horzEdge->Bot.y() && ePrev->WindDelta != 0 && - (ePrev->OutIdx >= 0 && ePrev->Curr.y() > ePrev->Top.y() && - SlopesEqual(*horzEdge, *ePrev, m_UseFullRange))) - { - OutPt* op2 = AddOutPt(ePrev, horzEdge->Bot); - m_Joins.emplace_back(Join(op1, op2, horzEdge->Top)); - } - else if (eNext && eNext->Curr.x() == horzEdge->Bot.x() && - eNext->Curr.y() == horzEdge->Bot.y() && eNext->WindDelta != 0 && - eNext->OutIdx >= 0 && eNext->Curr.y() > eNext->Top.y() && - SlopesEqual(*horzEdge, *eNext, m_UseFullRange)) - { - OutPt* op2 = AddOutPt(eNext, horzEdge->Bot); - m_Joins.emplace_back(Join(op1, op2, horzEdge->Top)); - } - } - else - UpdateEdgeIntoAEL(horzEdge); - } - else - { - if (horzEdge->OutIdx >= 0) AddOutPt(horzEdge, horzEdge->Top); - DeleteFromAEL(horzEdge); - } -} -//------------------------------------------------------------------------------ - -void Clipper::UpdateEdgeIntoAEL(TEdge *&e) -{ - if( !e->NextInLML ) - throw clipperException("UpdateEdgeIntoAEL: invalid call"); - - e->NextInLML->OutIdx = e->OutIdx; - TEdge* AelPrev = e->PrevInAEL; - TEdge* AelNext = e->NextInAEL; - if (AelPrev) AelPrev->NextInAEL = e->NextInLML; - else m_ActiveEdges = e->NextInLML; - if (AelNext) AelNext->PrevInAEL = e->NextInLML; - e->NextInLML->Side = e->Side; - e->NextInLML->WindDelta = e->WindDelta; - e->NextInLML->WindCnt = e->WindCnt; - e->NextInLML->WindCnt2 = e->WindCnt2; - e = e->NextInLML; - e->Curr = e->Bot; - e->PrevInAEL = AelPrev; - e->NextInAEL = AelNext; - if (!IsHorizontal(*e)) - m_Scanbeam.push(e->Top.y()); -} -//------------------------------------------------------------------------------ - -bool Clipper::ProcessIntersections(const cInt topY) -{ - if( !m_ActiveEdges ) return true; - try { - BuildIntersectList(topY); - size_t IlSize = m_IntersectList.size(); - if (IlSize == 0) return true; - if (IlSize == 1 || FixupIntersectionOrder()) { - for (IntersectNode &iNode : m_IntersectList) { - IntersectEdges( iNode.Edge1, iNode.Edge2, iNode.Pt); - SwapPositionsInAEL( iNode.Edge1 , iNode.Edge2 ); - } - m_IntersectList.clear(); - } - else return false; - } - catch(...) - { - m_SortedEdges = 0; - m_IntersectList.clear(); - throw clipperException("ProcessIntersections error"); - } - m_SortedEdges = 0; - return true; -} -//------------------------------------------------------------------------------ - -void Clipper::BuildIntersectList(const cInt topY) -{ - if ( !m_ActiveEdges ) return; - - //prepare for sorting ... - TEdge* e = m_ActiveEdges; - m_SortedEdges = e; - while( e ) - { - e->PrevInSEL = e->PrevInAEL; - e->NextInSEL = e->NextInAEL; - e->Curr.x() = TopX( *e, topY ); - e = e->NextInAEL; - } - - //bubblesort ... - bool isModified; - do - { - isModified = false; - e = m_SortedEdges; - while( e->NextInSEL ) - { - TEdge *eNext = e->NextInSEL; - IntPoint Pt; - if(e->Curr.x() > eNext->Curr.x()) - { - IntersectPoint(*e, *eNext, Pt); - - if (Pt.y() < topY) Pt = IntPoint2d(TopX(*e, topY), topY); - m_IntersectList.emplace_back(IntersectNode(e, eNext, Pt)); - SwapPositionsInSEL(e, eNext); - isModified = true; - } - else - e = eNext; - } - if( e->PrevInSEL ) e->PrevInSEL->NextInSEL = 0; - else break; - } - while ( isModified ); - m_SortedEdges = 0; //important -} -//------------------------------------------------------------------------------ - - -inline bool EdgesAdjacent(const IntersectNode &inode) -{ - return (inode.Edge1->NextInSEL == inode.Edge2) || - (inode.Edge1->PrevInSEL == inode.Edge2); -} -//------------------------------------------------------------------------------ - -bool Clipper::FixupIntersectionOrder() -{ - //pre-condition: intersections are sorted Bottom-most first. - //Now it's crucial that intersections are made only between adjacent edges, - //so to ensure this the order of intersections may need adjusting ... - CopyAELToSEL(); - std::sort(m_IntersectList.begin(), m_IntersectList.end(), [](const IntersectNode &node1, const IntersectNode &node2) { return node2.Pt.y() < node1.Pt.y(); }); - - size_t cnt = m_IntersectList.size(); - for (size_t i = 0; i < cnt; ++i) - { - if (!EdgesAdjacent(m_IntersectList[i])) - { - size_t j = i + 1; - while (j < cnt && !EdgesAdjacent(m_IntersectList[j])) j++; - if (j == cnt) return false; - std::swap(m_IntersectList[i], m_IntersectList[j]); - } - SwapPositionsInSEL(m_IntersectList[i].Edge1, m_IntersectList[i].Edge2); - } - return true; -} -//------------------------------------------------------------------------------ - -void Clipper::DoMaxima(TEdge *e) -{ - TEdge* eMaxPair = GetMaximaPairEx(e); - if (!eMaxPair) - { - if (e->OutIdx >= 0) - AddOutPt(e, e->Top); - DeleteFromAEL(e); - return; - } - - TEdge* eNext = e->NextInAEL; - while(eNext && eNext != eMaxPair) - { - IntersectEdges(e, eNext, e->Top); - SwapPositionsInAEL(e, eNext); - eNext = e->NextInAEL; - } - - if(e->OutIdx == Unassigned && eMaxPair->OutIdx == Unassigned) - { - DeleteFromAEL(e); - DeleteFromAEL(eMaxPair); - } - else if( e->OutIdx >= 0 && eMaxPair->OutIdx >= 0 ) - { - if (e->OutIdx >= 0) AddLocalMaxPoly(e, eMaxPair, e->Top); - DeleteFromAEL(e); - DeleteFromAEL(eMaxPair); - } -#ifdef use_lines - else if (e->WindDelta == 0) - { - if (e->OutIdx >= 0) - { - AddOutPt(e, e->Top); - e->OutIdx = Unassigned; - } - DeleteFromAEL(e); - - if (eMaxPair->OutIdx >= 0) - { - AddOutPt(eMaxPair, e->Top); - eMaxPair->OutIdx = Unassigned; - } - DeleteFromAEL(eMaxPair); - } -#endif - else throw clipperException("DoMaxima error"); -} -//------------------------------------------------------------------------------ - -void Clipper::ProcessEdgesAtTopOfScanbeam(const cInt topY) -{ - TEdge* e = m_ActiveEdges; - while( e ) - { - //1. process maxima, treating them as if they're 'bent' horizontal edges, - // but exclude maxima with horizontal edges. nb: e can't be a horizontal. - bool IsMaximaEdge = IsMaxima(e, topY); - - if(IsMaximaEdge) - { - TEdge* eMaxPair = GetMaximaPairEx(e); - IsMaximaEdge = (!eMaxPair || !IsHorizontal(*eMaxPair)); - } - - if(IsMaximaEdge) - { - if (m_StrictSimple) m_Maxima.emplace_back(e->Top.x()); - TEdge* ePrev = e->PrevInAEL; - DoMaxima(e); - if( !ePrev ) e = m_ActiveEdges; - else e = ePrev->NextInAEL; - } - else - { - //2. promote horizontal edges, otherwise update Curr.x() and Curr.y() ... - if (IsIntermediate(e, topY) && IsHorizontal(*e->NextInLML)) - { - UpdateEdgeIntoAEL(e); - if (e->OutIdx >= 0) - AddOutPt(e, e->Bot); - AddEdgeToSEL(e); - } - else - { - e->Curr.x() = TopX( *e, topY ); - e->Curr.y() = topY; -#ifdef CLIPPERLIB_USE_XYZ - e->Curr.z() = topY == e->Top.y() ? e->Top.z() : (topY == e->Bot.y() ? e->Bot.z() : 0); -#endif - } - - //When StrictlySimple and 'e' is being touched by another edge, then - //make sure both edges have a vertex here ... - if (m_StrictSimple) - { - TEdge* ePrev = e->PrevInAEL; - if ((e->OutIdx >= 0) && (e->WindDelta != 0) && ePrev && (ePrev->OutIdx >= 0) && - (ePrev->Curr.x() == e->Curr.x()) && (ePrev->WindDelta != 0)) - { - IntPoint pt = e->Curr; -#ifdef CLIPPERLIB_USE_XYZ - SetZ(pt, *ePrev, *e); -#endif - OutPt* op = AddOutPt(ePrev, pt); - OutPt* op2 = AddOutPt(e, pt); - m_Joins.emplace_back(Join(op, op2, pt)); //StrictlySimple (type-3) join - } - } - - e = e->NextInAEL; - } - } - - //3. Process horizontals at the Top of the scanbeam ... - std::sort(m_Maxima.begin(), m_Maxima.end()); - ProcessHorizontals(); - m_Maxima.clear(); - - //4. Promote intermediate vertices ... - e = m_ActiveEdges; - while(e) - { - if(IsIntermediate(e, topY)) - { - OutPt* op = 0; - if( e->OutIdx >= 0 ) - op = AddOutPt(e, e->Top); - UpdateEdgeIntoAEL(e); - - //if output polygons share an edge, they'll need joining later ... - TEdge* ePrev = e->PrevInAEL; - TEdge* eNext = e->NextInAEL; - if (ePrev && ePrev->Curr.x() == e->Bot.x() && - ePrev->Curr.y() == e->Bot.y() && op && - ePrev->OutIdx >= 0 && ePrev->Curr.y() > ePrev->Top.y() && - SlopesEqual(e->Curr, e->Top, ePrev->Curr, ePrev->Top, m_UseFullRange) && - (e->WindDelta != 0) && (ePrev->WindDelta != 0)) - { - OutPt* op2 = AddOutPt(ePrev, e->Bot); - m_Joins.emplace_back(Join(op, op2, e->Top)); - } - else if (eNext && eNext->Curr.x() == e->Bot.x() && - eNext->Curr.y() == e->Bot.y() && op && - eNext->OutIdx >= 0 && eNext->Curr.y() > eNext->Top.y() && - SlopesEqual(e->Curr, e->Top, eNext->Curr, eNext->Top, m_UseFullRange) && - (e->WindDelta != 0) && (eNext->WindDelta != 0)) - { - OutPt* op2 = AddOutPt(eNext, e->Bot); - m_Joins.emplace_back(Join(op, op2, e->Top)); - } - } - e = e->NextInAEL; - } -} -//------------------------------------------------------------------------------ - -void Clipper::FixupOutPolyline(OutRec &outrec) -{ - OutPt *pp = outrec.Pts; - OutPt *lastPP = pp->Prev; - while (pp != lastPP) - { - pp = pp->Next; - if (pp->Pt == pp->Prev->Pt) - { - if (pp == lastPP) lastPP = pp->Prev; - OutPt *tmpPP = pp->Prev; - tmpPP->Next = pp->Next; - pp->Next->Prev = tmpPP; - this->DisposeOutPt(pp); - pp = tmpPP; - } - } - - if (pp == pp->Prev) - { - this->DisposeOutPts(pp); - outrec.Pts = 0; - return; - } -} -//------------------------------------------------------------------------------ - -void Clipper::FixupOutPolygon(OutRec &outrec) -{ - //FixupOutPolygon() - removes duplicate points and simplifies consecutive - //parallel edges by removing the middle vertex. - OutPt *lastOK = nullptr; - outrec.BottomPt = nullptr; - OutPt *pp = outrec.Pts; - bool preserveCol = m_PreserveCollinear || m_StrictSimple; - - for (;;) - { - if (pp->Prev == pp || pp->Prev == pp->Next) - { - // Empty loop or a stick. Release the polygon. - this->DisposeOutPts(pp); - outrec.Pts = nullptr; - return; - } - - //test for duplicate points and collinear edges ... - if ((pp->Pt == pp->Next->Pt) || (pp->Pt == pp->Prev->Pt) || - (SlopesEqual(pp->Prev->Pt, pp->Pt, pp->Next->Pt, m_UseFullRange) && - (!preserveCol || !Pt2IsBetweenPt1AndPt3(pp->Prev->Pt, pp->Pt, pp->Next->Pt)))) - { - lastOK = nullptr; - OutPt *tmp = pp; - pp->Prev->Next = pp->Next; - pp->Next->Prev = pp->Prev; - pp = pp->Prev; - this->DisposeOutPt(tmp); - } - else if (pp == lastOK) break; - else - { - if (!lastOK) lastOK = pp; - pp = pp->Next; - } - } - outrec.Pts = pp; -} -//------------------------------------------------------------------------------ - -// Count the number of points in a closed linked loop starting with Pts. -int PointCount(OutPt *Pts) -{ - if (!Pts) return 0; - int result = 0; - OutPt* p = Pts; - do - { - result++; - p = p->Next; - } - while (p != Pts); - return result; -} -//------------------------------------------------------------------------------ - -void Clipper::BuildResult(Paths &polys) -{ - polys.reserve(m_PolyOuts.size()); - for (OutRec &outRec : m_PolyOuts) - { - assert(! outRec.IsOpen); - if (!outRec.Pts) continue; - Path pg; - OutPt* p = outRec.Pts->Prev; - int cnt = PointCount(p); - if (cnt < 2) continue; - pg.reserve(cnt); - for (int i = 0; i < cnt; ++i) - { - pg.emplace_back(p->Pt); - p = p->Prev; - } - polys.emplace_back(std::move(pg)); - } -} -//------------------------------------------------------------------------------ - -void Clipper::BuildResult2(PolyTree& polytree) -{ - polytree.Clear(); - polytree.AllNodes.reserve(m_PolyOuts.size()); - //add each output polygon/contour to polytree ... - for (OutRec &outRec : m_PolyOuts) - { - int cnt = PointCount(outRec.Pts); - if ((outRec.IsOpen && cnt < 2) || (!outRec.IsOpen && cnt < 3)) - // Ignore an invalid output loop or a polyline. - continue; - - //skip OutRecs that (a) contain outermost polygons or - //(b) already have the correct owner/child linkage ... - if (outRec.FirstLeft && - (outRec.IsHole == outRec.FirstLeft->IsHole || ! outRec.FirstLeft->Pts)) { - OutRec* orfl = outRec.FirstLeft; - while (orfl && ((orfl->IsHole == outRec.IsHole) || !orfl->Pts)) - orfl = orfl->FirstLeft; - outRec.FirstLeft = orfl; - } - - //nb: polytree takes ownership of all the PolyNodes - polytree.AllNodes.emplace_back(PolyNode()); - PolyNode* pn = &polytree.AllNodes.back(); - outRec.PolyNd = pn; - pn->Parent = 0; - pn->Index = 0; - pn->Contour.reserve(cnt); - OutPt *op = outRec.Pts->Prev; - for (int j = 0; j < cnt; j++) - { - pn->Contour.emplace_back(op->Pt); - op = op->Prev; - } - } - - //fixup PolyNode links etc ... - polytree.Childs.reserve(m_PolyOuts.size()); - for (OutRec &outRec : m_PolyOuts) - { - if (!outRec.PolyNd) continue; - if (outRec.IsOpen) - { - outRec.PolyNd->m_IsOpen = true; - polytree.AddChild(*outRec.PolyNd); - } - else if (outRec.FirstLeft && outRec.FirstLeft->PolyNd) - outRec.FirstLeft->PolyNd->AddChild(*outRec.PolyNd); - else - polytree.AddChild(*outRec.PolyNd); - } -} -//------------------------------------------------------------------------------ - -inline bool E2InsertsBeforeE1(TEdge &e1, TEdge &e2) -{ - if (e2.Curr.x() == e1.Curr.x()) - { - if (e2.Top.y() > e1.Top.y()) - return e2.Top.x() < TopX(e1, e2.Top.y()); - else return e1.Top.x() > TopX(e2, e1.Top.y()); - } - else return e2.Curr.x() < e1.Curr.x(); -} -//------------------------------------------------------------------------------ - -bool GetOverlap(const cInt a1, const cInt a2, const cInt b1, const cInt b2, - cInt& Left, cInt& Right) -{ - if (a1 < a2) - { - if (b1 < b2) {Left = std::max(a1,b1); Right = std::min(a2,b2);} - else {Left = std::max(a1,b2); Right = std::min(a2,b1);} - } - else - { - if (b1 < b2) {Left = std::max(a2,b1); Right = std::min(a1,b2);} - else {Left = std::max(a2,b2); Right = std::min(a1,b1);} - } - return Left < Right; -} -//------------------------------------------------------------------------------ - -// Make all points of outrec point to outrec.Idx -inline void UpdateOutPtIdxs(OutRec& outrec) -{ - OutPt* op = outrec.Pts; - do - { - op->Idx = outrec.Idx; - op = op->Prev; - } - while(op != outrec.Pts); -} -//------------------------------------------------------------------------------ - -void Clipper::InsertEdgeIntoAEL(TEdge *edge, TEdge* startEdge) -{ - if(!m_ActiveEdges) - { - edge->PrevInAEL = 0; - edge->NextInAEL = 0; - m_ActiveEdges = edge; - } - else if(!startEdge && E2InsertsBeforeE1(*m_ActiveEdges, *edge)) - { - edge->PrevInAEL = 0; - edge->NextInAEL = m_ActiveEdges; - m_ActiveEdges->PrevInAEL = edge; - m_ActiveEdges = edge; - } - else - { - if(!startEdge) startEdge = m_ActiveEdges; - while(startEdge->NextInAEL && - !E2InsertsBeforeE1(*startEdge->NextInAEL , *edge)) - startEdge = startEdge->NextInAEL; - edge->NextInAEL = startEdge->NextInAEL; - if(startEdge->NextInAEL) startEdge->NextInAEL->PrevInAEL = edge; - edge->PrevInAEL = startEdge; - startEdge->NextInAEL = edge; - } -} -//---------------------------------------------------------------------- - -OutPt* Clipper::DupOutPt(OutPt* outPt, bool InsertAfter) -{ - OutPt* result = this->AllocateOutPt(); - result->Pt = outPt->Pt; - result->Idx = outPt->Idx; - if (InsertAfter) - { - result->Next = outPt->Next; - result->Prev = outPt; - outPt->Next->Prev = result; - outPt->Next = result; - } - else - { - result->Prev = outPt->Prev; - result->Next = outPt; - outPt->Prev->Next = result; - outPt->Prev = result; - } - return result; -} -//------------------------------------------------------------------------------ - -bool Clipper::JoinHorz(OutPt* op1, OutPt* op1b, OutPt* op2, OutPt* op2b, - const IntPoint &Pt, bool DiscardLeft) -{ - Direction Dir1 = (op1->Pt.x() > op1b->Pt.x() ? dRightToLeft : dLeftToRight); - Direction Dir2 = (op2->Pt.x() > op2b->Pt.x() ? dRightToLeft : dLeftToRight); - if (Dir1 == Dir2) return false; - - //When DiscardLeft, we want Op1b to be on the Left of Op1, otherwise we - //want Op1b to be on the Right. (And likewise with Op2 and Op2b.) - //So, to facilitate this while inserting Op1b and Op2b ... - //when DiscardLeft, make sure we're AT or RIGHT of Pt before adding Op1b, - //otherwise make sure we're AT or LEFT of Pt. (Likewise with Op2b.) - if (Dir1 == dLeftToRight) - { - while (op1->Next->Pt.x() <= Pt.x() && - op1->Next->Pt.x() >= op1->Pt.x() && op1->Next->Pt.y() == Pt.y()) - op1 = op1->Next; - if (DiscardLeft && (op1->Pt.x() != Pt.x())) op1 = op1->Next; - op1b = this->DupOutPt(op1, !DiscardLeft); - if (op1b->Pt != Pt) - { - op1 = op1b; - op1->Pt = Pt; - op1b = this->DupOutPt(op1, !DiscardLeft); - } - } - else - { - while (op1->Next->Pt.x() >= Pt.x() && - op1->Next->Pt.x() <= op1->Pt.x() && op1->Next->Pt.y() == Pt.y()) - op1 = op1->Next; - if (!DiscardLeft && (op1->Pt.x() != Pt.x())) op1 = op1->Next; - op1b = this->DupOutPt(op1, DiscardLeft); - if (op1b->Pt != Pt) - { - op1 = op1b; - op1->Pt = Pt; - op1b = this->DupOutPt(op1, DiscardLeft); - } - } - - if (Dir2 == dLeftToRight) - { - while (op2->Next->Pt.x() <= Pt.x() && - op2->Next->Pt.x() >= op2->Pt.x() && op2->Next->Pt.y() == Pt.y()) - op2 = op2->Next; - if (DiscardLeft && (op2->Pt.x() != Pt.x())) op2 = op2->Next; - op2b = this->DupOutPt(op2, !DiscardLeft); - if (op2b->Pt != Pt) - { - op2 = op2b; - op2->Pt = Pt; - op2b = this->DupOutPt(op2, !DiscardLeft); - }; - } else - { - while (op2->Next->Pt.x() >= Pt.x() && - op2->Next->Pt.x() <= op2->Pt.x() && op2->Next->Pt.y() == Pt.y()) - op2 = op2->Next; - if (!DiscardLeft && (op2->Pt.x() != Pt.x())) op2 = op2->Next; - op2b = this->DupOutPt(op2, DiscardLeft); - if (op2b->Pt != Pt) - { - op2 = op2b; - op2->Pt = Pt; - op2b = this->DupOutPt(op2, DiscardLeft); - }; - }; - - if ((Dir1 == dLeftToRight) == DiscardLeft) - { - op1->Prev = op2; - op2->Next = op1; - op1b->Next = op2b; - op2b->Prev = op1b; - } - else - { - op1->Next = op2; - op2->Prev = op1; - op1b->Prev = op2b; - op2b->Next = op1b; - } - return true; -} -//------------------------------------------------------------------------------ - -bool Clipper::JoinPoints(Join *j, OutRec* outRec1, OutRec* outRec2) -{ - OutPt *op1 = j->OutPt1, *op1b; - OutPt *op2 = j->OutPt2, *op2b; - - //There are 3 kinds of joins for output polygons ... - //1. Horizontal joins where Join.OutPt1 & Join.OutPt2 are vertices anywhere - //along (horizontal) collinear edges (& Join.OffPt is on the same horizontal). - //2. Non-horizontal joins where Join.OutPt1 & Join.OutPt2 are at the same - //location at the Bottom of the overlapping segment (& Join.OffPt is above). - //3. StrictSimple joins where edges touch but are not collinear and where - //Join.OutPt1, Join.OutPt2 & Join.OffPt all share the same point. - bool isHorizontal = (j->OutPt1->Pt.y() == j->OffPt.y()); - - if (isHorizontal && (j->OffPt == j->OutPt1->Pt) && - (j->OffPt == j->OutPt2->Pt)) - { - //Strictly Simple join ... - if (outRec1 != outRec2) return false; - op1b = j->OutPt1->Next; - while (op1b != op1 && (op1b->Pt == j->OffPt)) - op1b = op1b->Next; - bool reverse1 = (op1b->Pt.y() > j->OffPt.y()); - op2b = j->OutPt2->Next; - while (op2b != op2 && (op2b->Pt == j->OffPt)) - op2b = op2b->Next; - bool reverse2 = (op2b->Pt.y() > j->OffPt.y()); - if (reverse1 == reverse2) return false; - if (reverse1) - { - op1b = this->DupOutPt(op1, false); - op2b = this->DupOutPt(op2, true); - op1->Prev = op2; - op2->Next = op1; - op1b->Next = op2b; - op2b->Prev = op1b; - j->OutPt1 = op1; - j->OutPt2 = op1b; - return true; - } else - { - op1b = this->DupOutPt(op1, true); - op2b = this->DupOutPt(op2, false); - op1->Next = op2; - op2->Prev = op1; - op1b->Prev = op2b; - op2b->Next = op1b; - j->OutPt1 = op1; - j->OutPt2 = op1b; - return true; - } - } - else if (isHorizontal) - { - //treat horizontal joins differently to non-horizontal joins since with - //them we're not yet sure where the overlapping is. OutPt1.Pt & OutPt2.Pt - //may be anywhere along the horizontal edge. - op1b = op1; - while (op1->Prev->Pt.y() == op1->Pt.y() && op1->Prev != op1b && op1->Prev != op2) - op1 = op1->Prev; - while (op1b->Next->Pt.y() == op1b->Pt.y() && op1b->Next != op1 && op1b->Next != op2) - op1b = op1b->Next; - if (op1b->Next == op1 || op1b->Next == op2) return false; //a flat 'polygon' - - op2b = op2; - while (op2->Prev->Pt.y() == op2->Pt.y() && op2->Prev != op2b && op2->Prev != op1b) - op2 = op2->Prev; - while (op2b->Next->Pt.y() == op2b->Pt.y() && op2b->Next != op2 && op2b->Next != op1) - op2b = op2b->Next; - if (op2b->Next == op2 || op2b->Next == op1) return false; //a flat 'polygon' - - cInt Left, Right; - //Op1 --> Op1b & Op2 --> Op2b are the extremites of the horizontal edges - if (!GetOverlap(op1->Pt.x(), op1b->Pt.x(), op2->Pt.x(), op2b->Pt.x(), Left, Right)) - return false; - - //DiscardLeftSide: when overlapping edges are joined, a spike will created - //which needs to be cleaned up. However, we don't want Op1 or Op2 caught up - //on the discard Side as either may still be needed for other joins ... - IntPoint Pt; - bool DiscardLeftSide; - if (op1->Pt.x() >= Left && op1->Pt.x() <= Right) - { - Pt = op1->Pt; DiscardLeftSide = (op1->Pt.x() > op1b->Pt.x()); - } - else if (op2->Pt.x() >= Left&& op2->Pt.x() <= Right) - { - Pt = op2->Pt; DiscardLeftSide = (op2->Pt.x() > op2b->Pt.x()); - } - else if (op1b->Pt.x() >= Left && op1b->Pt.x() <= Right) - { - Pt = op1b->Pt; DiscardLeftSide = op1b->Pt.x() > op1->Pt.x(); - } - else - { - Pt = op2b->Pt; DiscardLeftSide = (op2b->Pt.x() > op2->Pt.x()); - } - j->OutPt1 = op1; j->OutPt2 = op2; - return JoinHorz(op1, op1b, op2, op2b, Pt, DiscardLeftSide); - } else - { - //nb: For non-horizontal joins ... - // 1. Jr.OutPt1.Pt.y() == Jr.OutPt2.Pt.y() - // 2. Jr.OutPt1.Pt > Jr.OffPt.y() - - //make sure the polygons are correctly oriented ... - op1b = op1->Next; - while ((op1b->Pt == op1->Pt) && (op1b != op1)) op1b = op1b->Next; - bool Reverse1 = ((op1b->Pt.y() > op1->Pt.y()) || - !SlopesEqual(op1->Pt, op1b->Pt, j->OffPt, m_UseFullRange)); - if (Reverse1) - { - op1b = op1->Prev; - while ((op1b->Pt == op1->Pt) && (op1b != op1)) op1b = op1b->Prev; - if ((op1b->Pt.y() > op1->Pt.y()) || - !SlopesEqual(op1->Pt, op1b->Pt, j->OffPt, m_UseFullRange)) return false; - }; - op2b = op2->Next; - while ((op2b->Pt == op2->Pt) && (op2b != op2))op2b = op2b->Next; - bool Reverse2 = ((op2b->Pt.y() > op2->Pt.y()) || - !SlopesEqual(op2->Pt, op2b->Pt, j->OffPt, m_UseFullRange)); - if (Reverse2) - { - op2b = op2->Prev; - while ((op2b->Pt == op2->Pt) && (op2b != op2)) op2b = op2b->Prev; - if ((op2b->Pt.y() > op2->Pt.y()) || - !SlopesEqual(op2->Pt, op2b->Pt, j->OffPt, m_UseFullRange)) return false; - } - - if ((op1b == op1) || (op2b == op2) || (op1b == op2b) || - ((outRec1 == outRec2) && (Reverse1 == Reverse2))) return false; - - if (Reverse1) - { - op1b = this->DupOutPt(op1, false); - op2b = this->DupOutPt(op2, true); - op1->Prev = op2; - op2->Next = op1; - op1b->Next = op2b; - op2b->Prev = op1b; - j->OutPt1 = op1; - j->OutPt2 = op1b; - return true; - } else - { - op1b = this->DupOutPt(op1, true); - op2b = this->DupOutPt(op2, false); - op1->Next = op2; - op2->Prev = op1; - op1b->Prev = op2b; - op2b->Next = op1b; - j->OutPt1 = op1; - j->OutPt2 = op1b; - return true; - } - } -} -//---------------------------------------------------------------------- - -inline OutRec* ParseFirstLeft(OutRec* FirstLeft) -{ - while (FirstLeft && !FirstLeft->Pts) - FirstLeft = FirstLeft->FirstLeft; - return FirstLeft; -} -//------------------------------------------------------------------------------ - -// This is potentially very expensive! O(n^3)! -void Clipper::FixupFirstLefts1(OutRec* OldOutRec, OutRec* NewOutRec) -{ - //tests if NewOutRec contains the polygon before reassigning FirstLeft - for (OutRec &outRec : m_PolyOuts) - { - OutRec* firstLeft = ParseFirstLeft(outRec.FirstLeft); - if (outRec.Pts && firstLeft == OldOutRec && Poly2ContainsPoly1(outRec.Pts, NewOutRec->Pts)) - outRec.FirstLeft = NewOutRec; - } -} -//---------------------------------------------------------------------- - -void Clipper::FixupFirstLefts2(OutRec* InnerOutRec, OutRec* OuterOutRec) -{ - //A polygon has split into two such that one is now the inner of the other. - //It's possible that these polygons now wrap around other polygons, so check - //every polygon that's also contained by OuterOutRec's FirstLeft container - //(including 0) to see if they've become inner to the new inner polygon ... - OutRec* orfl = OuterOutRec->FirstLeft; - for (OutRec &outRec : m_PolyOuts) - { - if (!outRec.Pts || &outRec == OuterOutRec || &outRec == InnerOutRec) - continue; - OutRec* firstLeft = ParseFirstLeft(outRec.FirstLeft); - if (firstLeft != orfl && firstLeft != InnerOutRec && firstLeft != OuterOutRec) - continue; - if (Poly2ContainsPoly1(outRec.Pts, InnerOutRec->Pts)) - outRec.FirstLeft = InnerOutRec; - else if (Poly2ContainsPoly1(outRec.Pts, OuterOutRec->Pts)) - outRec.FirstLeft = OuterOutRec; - else if (outRec.FirstLeft == InnerOutRec || outRec.FirstLeft == OuterOutRec) - outRec.FirstLeft = orfl; - } -} -//---------------------------------------------------------------------- -void Clipper::FixupFirstLefts3(OutRec* OldOutRec, OutRec* NewOutRec) -{ - //reassigns FirstLeft WITHOUT testing if NewOutRec contains the polygon - for (OutRec &outRec : m_PolyOuts) - { - OutRec* firstLeft = ParseFirstLeft(outRec.FirstLeft); - if (outRec.Pts && firstLeft == OldOutRec) - outRec.FirstLeft = NewOutRec; - } -} -//---------------------------------------------------------------------- - -void Clipper::JoinCommonEdges() -{ - for (Join &join : m_Joins) - { - OutRec *outRec1 = GetOutRec(join.OutPt1->Idx); - OutRec *outRec2 = GetOutRec(join.OutPt2->Idx); - - if (!outRec1->Pts || !outRec2->Pts) continue; - if (outRec1->IsOpen || outRec2->IsOpen) continue; - - //get the polygon fragment with the correct hole state (FirstLeft) - //before calling JoinPoints() ... - OutRec *holeStateRec; - if (outRec1 == outRec2) holeStateRec = outRec1; - else if (OutRec1RightOfOutRec2(outRec1, outRec2)) holeStateRec = outRec2; - else if (OutRec1RightOfOutRec2(outRec2, outRec1)) holeStateRec = outRec1; - else holeStateRec = GetLowermostRec(outRec1, outRec2); - - if (!JoinPoints(&join, outRec1, outRec2)) continue; - - if (outRec1 == outRec2) - { - //instead of joining two polygons, we've just created a new one by - //splitting one polygon into two. - outRec1->Pts = join.OutPt1; - outRec1->BottomPt = 0; - outRec2 = CreateOutRec(); - outRec2->Pts = join.OutPt2; - - //update all OutRec2.Pts Idx's ... - UpdateOutPtIdxs(*outRec2); - - if (Poly2ContainsPoly1(outRec2->Pts, outRec1->Pts)) - { - //outRec1 contains outRec2 ... - outRec2->IsHole = !outRec1->IsHole; - outRec2->FirstLeft = outRec1; - - // For each m_PolyOuts, replace FirstLeft from outRec2 to outRec1. - if (m_UsingPolyTree) FixupFirstLefts2(outRec2, outRec1); - - if ((outRec2->IsHole ^ m_ReverseOutput) == (Area(*outRec2) > 0)) - ReversePolyPtLinks(outRec2->Pts); - - } else if (Poly2ContainsPoly1(outRec1->Pts, outRec2->Pts)) - { - //outRec2 contains outRec1 ... - outRec2->IsHole = outRec1->IsHole; - outRec1->IsHole = !outRec2->IsHole; - outRec2->FirstLeft = outRec1->FirstLeft; - outRec1->FirstLeft = outRec2; - - // For each m_PolyOuts, replace FirstLeft from outRec1 to outRec2. - if (m_UsingPolyTree) FixupFirstLefts2(outRec1, outRec2); - - if ((outRec1->IsHole ^ m_ReverseOutput) == (Area(*outRec1) > 0)) - ReversePolyPtLinks(outRec1->Pts); - } - else - { - //the 2 polygons are completely separate ... - outRec2->IsHole = outRec1->IsHole; - outRec2->FirstLeft = outRec1->FirstLeft; - - //fixup FirstLeft pointers that may need reassigning to OutRec2 - // For each polygon of m_PolyOuts, replace FirstLeft from outRec1 to outRec2 if the polygon is inside outRec2. - //FIXME This is potentially very expensive! O(n^3)! - if (m_UsingPolyTree) FixupFirstLefts1(outRec1, outRec2); - } - - } else - { - //joined 2 polygons together ... - - outRec2->Pts = 0; - outRec2->BottomPt = 0; - outRec2->Idx = outRec1->Idx; - - outRec1->IsHole = holeStateRec->IsHole; - if (holeStateRec == outRec2) - outRec1->FirstLeft = outRec2->FirstLeft; - outRec2->FirstLeft = outRec1; - - // For each m_PolyOuts, replace FirstLeft from outRec2 to outRec1. - if (m_UsingPolyTree) FixupFirstLefts3(outRec2, outRec1); - } - } -} - -//------------------------------------------------------------------------------ -// ClipperOffset support functions ... -//------------------------------------------------------------------------------ - -DoublePoint GetUnitNormal(const IntPoint &pt1, const IntPoint &pt2) -{ - if(pt2.x() == pt1.x() && pt2.y() == pt1.y()) - return DoublePoint(0, 0); - - double Dx = double(pt2.x() - pt1.x()); - double dy = double(pt2.y() - pt1.y()); - double f = 1.0 / std::sqrt( Dx*Dx + dy*dy ); - Dx *= f; - dy *= f; - return DoublePoint(dy, -Dx); -} - -//------------------------------------------------------------------------------ -// ClipperOffset class -//------------------------------------------------------------------------------ - -void ClipperOffset::Clear() -{ - for (int i = 0; i < m_polyNodes.ChildCount(); ++i) - delete m_polyNodes.Childs[i]; - m_polyNodes.Childs.clear(); - m_lowest.x() = -1; -} -//------------------------------------------------------------------------------ - -void ClipperOffset::AddPath(const Path& path, JoinType joinType, EndType endType) -{ - int highI = (int)path.size() - 1; - if (highI < 0) return; - PolyNode* newNode = new PolyNode(); - newNode->m_jointype = joinType; - newNode->m_endtype = endType; - - //strip duplicate points from path and also get index to the lowest point ... - bool has_shortest_edge_length = ShortestEdgeLength > 0.; - double shortest_edge_length2 = has_shortest_edge_length ? ShortestEdgeLength * ShortestEdgeLength : 0.; - if (endType == etClosedLine || endType == etClosedPolygon) - for (; highI > 0; -- highI) { - bool same = false; - if (has_shortest_edge_length) { - double dx = double(path[highI].x() - path[0].x()); - double dy = double(path[highI].y() - path[0].y()); - same = dx*dx + dy*dy < shortest_edge_length2; - } else - same = path[0] == path[highI]; - if (! same) - break; - } - newNode->Contour.reserve(highI + 1); - newNode->Contour.emplace_back(path[0]); - int j = 0, k = 0; - for (int i = 1; i <= highI; i++) { - bool same = false; - if (has_shortest_edge_length) { - double dx = double(path[i].x() - newNode->Contour[j].x()); - double dy = double(path[i].y() - newNode->Contour[j].y()); - same = dx*dx + dy*dy < shortest_edge_length2; - } else - same = newNode->Contour[j] == path[i]; - if (same) - continue; - j++; - newNode->Contour.emplace_back(path[i]); - if (path[i].y() > newNode->Contour[k].y() || - (path[i].y() == newNode->Contour[k].y() && - path[i].x() < newNode->Contour[k].x())) k = j; - } - if (endType == etClosedPolygon && j < 2) - { - delete newNode; - return; - } - m_polyNodes.AddChild(*newNode); - - //if this path's lowest pt is lower than all the others then update m_lowest - if (endType != etClosedPolygon) return; - if (m_lowest.x() < 0) - m_lowest = IntPoint2d(m_polyNodes.ChildCount() - 1, k); - else - { - IntPoint ip = m_polyNodes.Childs[(int)m_lowest.x()]->Contour[(int)m_lowest.y()]; - if (newNode->Contour[k].y() > ip.y() || - (newNode->Contour[k].y() == ip.y() && - newNode->Contour[k].x() < ip.x())) - m_lowest = IntPoint2d(m_polyNodes.ChildCount() - 1, k); - } -} -//------------------------------------------------------------------------------ - -void ClipperOffset::FixOrientations() -{ - //fixup orientations of all closed paths if the orientation of the - //closed path with the lowermost vertex is wrong ... - if (m_lowest.x() >= 0 && - !Orientation(m_polyNodes.Childs[(int)m_lowest.x()]->Contour)) - { - for (int i = 0; i < m_polyNodes.ChildCount(); ++i) - { - PolyNode& node = *m_polyNodes.Childs[i]; - if (node.m_endtype == etClosedPolygon || - (node.m_endtype == etClosedLine && Orientation(node.Contour))) - ReversePath(node.Contour); - } - } else - { - for (int i = 0; i < m_polyNodes.ChildCount(); ++i) - { - PolyNode& node = *m_polyNodes.Childs[i]; - if (node.m_endtype == etClosedLine && !Orientation(node.Contour)) - ReversePath(node.Contour); - } - } -} -//------------------------------------------------------------------------------ - -void ClipperOffset::Execute(Paths& solution, double delta) -{ - solution.clear(); - FixOrientations(); - DoOffset(delta); - - //now clean up 'corners' ... - Clipper clpr; - clpr.AddPaths(m_destPolys, ptSubject, true); - if (delta > 0) - { - clpr.Execute(ctUnion, solution, pftPositive, pftPositive); - } - else - { - IntRect r = clpr.GetBounds(); - Path outer(4); - outer[0] = IntPoint2d(r.left - 10, r.bottom + 10); - outer[1] = IntPoint2d(r.right + 10, r.bottom + 10); - outer[2] = IntPoint2d(r.right + 10, r.top - 10); - outer[3] = IntPoint2d(r.left - 10, r.top - 10); - - clpr.AddPath(outer, ptSubject, true); - clpr.ReverseSolution(true); - clpr.Execute(ctUnion, solution, pftNegative, pftNegative); - if (! solution.empty()) - solution.erase(solution.begin()); - } -} -//------------------------------------------------------------------------------ - -void ClipperOffset::Execute(PolyTree& solution, double delta) -{ - solution.Clear(); - FixOrientations(); - DoOffset(delta); - - //now clean up 'corners' ... - Clipper clpr; - clpr.AddPaths(m_destPolys, ptSubject, true); - if (delta > 0) - { - clpr.Execute(ctUnion, solution, pftPositive, pftPositive); - } - else - { - IntRect r = clpr.GetBounds(); - Path outer(4); - outer[0] = IntPoint2d(r.left - 10, r.bottom + 10); - outer[1] = IntPoint2d(r.right + 10, r.bottom + 10); - outer[2] = IntPoint2d(r.right + 10, r.top - 10); - outer[3] = IntPoint2d(r.left - 10, r.top - 10); - - clpr.AddPath(outer, ptSubject, true); - clpr.ReverseSolution(true); - clpr.Execute(ctUnion, solution, pftNegative, pftNegative); - //remove the outer PolyNode rectangle ... - solution.RemoveOutermostPolygon(); - } -} -//------------------------------------------------------------------------------ - -void ClipperOffset::DoOffset(double delta) -{ - m_destPolys.clear(); - m_delta = delta; - - //if Zero offset, just copy any CLOSED polygons to m_p and return ... - if (NEAR_ZERO(delta)) - { - m_destPolys.reserve(m_polyNodes.ChildCount()); - for (int i = 0; i < m_polyNodes.ChildCount(); i++) - { - PolyNode& node = *m_polyNodes.Childs[i]; - if (node.m_endtype == etClosedPolygon) - m_destPolys.emplace_back(node.Contour); - } - return; - } - - //see offset_triginometry3.svg in the documentation folder ... - m_miterLim = (MiterLimit > 2) ? - 2. / (MiterLimit * MiterLimit) : - 0.5; - - double y; - if (ArcTolerance <= 0.0) y = def_arc_tolerance; - else if (ArcTolerance > std::fabs(delta) * def_arc_tolerance) - y = std::fabs(delta) * def_arc_tolerance; - else y = ArcTolerance; - //see offset_triginometry2.svg in the documentation folder ... - double steps = pi / std::acos(1 - y / std::fabs(delta)); - if (steps > std::fabs(delta) * pi) - steps = std::fabs(delta) * pi; //ie excessive precision check - m_sin = std::sin(two_pi / steps); - m_cos = std::cos(two_pi / steps); - m_StepsPerRad = steps / two_pi; - if (delta < 0.0) m_sin = -m_sin; - - m_destPolys.reserve(m_polyNodes.ChildCount() * 2); - for (int i = 0; i < m_polyNodes.ChildCount(); i++) - { - PolyNode& node = *m_polyNodes.Childs[i]; - m_srcPoly = node.Contour; - - int len = (int)m_srcPoly.size(); - if (len == 0 || (delta <= 0 && (len < 3 || node.m_endtype != etClosedPolygon))) - continue; - - m_destPoly.clear(); - if (len == 1) - { - if (node.m_jointype == jtRound) - { - double X = 1.0, Y = 0.0; - for (cInt j = 1; j <= steps; j++) - { - m_destPoly.emplace_back(IntPoint2d( - Round(m_srcPoly[0].x() + X * delta), - Round(m_srcPoly[0].y() + Y * delta))); - double X2 = X; - X = X * m_cos - m_sin * Y; - Y = X2 * m_sin + Y * m_cos; - } - } - else - { - double X = -1.0, Y = -1.0; - for (int j = 0; j < 4; ++j) - { - m_destPoly.emplace_back(IntPoint2d( - Round(m_srcPoly[0].x() + X * delta), - Round(m_srcPoly[0].y() + Y * delta))); - if (X < 0) X = 1; - else if (Y < 0) Y = 1; - else X = -1; - } - } - m_destPolys.emplace_back(m_destPoly); - continue; - } - //build m_normals ... - m_normals.clear(); - m_normals.reserve(len); - for (int j = 0; j < len - 1; ++j) - m_normals.emplace_back(GetUnitNormal(m_srcPoly[j], m_srcPoly[j + 1])); - if (node.m_endtype == etClosedLine || node.m_endtype == etClosedPolygon) - m_normals.emplace_back(GetUnitNormal(m_srcPoly[len - 1], m_srcPoly[0])); - else - m_normals.emplace_back(DoublePoint(m_normals[len - 2])); - - if (node.m_endtype == etClosedPolygon) - { - int k = len - 1; - for (int j = 0; j < len; ++j) - OffsetPoint(j, k, node.m_jointype); - m_destPolys.emplace_back(m_destPoly); - } - else if (node.m_endtype == etClosedLine) - { - int k = len - 1; - for (int j = 0; j < len; ++j) - OffsetPoint(j, k, node.m_jointype); - m_destPolys.emplace_back(m_destPoly); - m_destPoly.clear(); - //re-build m_normals ... - DoublePoint n = m_normals[len -1]; - for (int j = len - 1; j > 0; j--) - m_normals[j] = DoublePoint(-m_normals[j - 1].x(), -m_normals[j - 1].y()); - m_normals[0] = DoublePoint(-n.x(), -n.y()); - k = 0; - for (int j = len - 1; j >= 0; j--) - OffsetPoint(j, k, node.m_jointype); - m_destPolys.emplace_back(m_destPoly); - } - else - { - int k = 0; - for (int j = 1; j < len - 1; ++j) - OffsetPoint(j, k, node.m_jointype); - - IntPoint pt1; - if (node.m_endtype == etOpenButt) - { - int j = len - 1; - pt1 = IntPoint2d(Round(m_srcPoly[j].x() + m_normals[j].x() * delta), Round(m_srcPoly[j].y() + m_normals[j].y() * delta)); - m_destPoly.emplace_back(pt1); - pt1 = IntPoint2d(Round(m_srcPoly[j].x() - m_normals[j].x() * delta), Round(m_srcPoly[j].y() - m_normals[j].y() * delta)); - m_destPoly.emplace_back(pt1); - } - else - { - int j = len - 1; - k = len - 2; - m_sinA = 0; - m_normals[j] = DoublePoint(-m_normals[j].x(), -m_normals[j].y()); - if (node.m_endtype == etOpenSquare) - DoSquare(j, k); - else - DoRound(j, k); - } - - //re-build m_normals ... - for (int j = len - 1; j > 0; j--) - m_normals[j] = DoublePoint(-m_normals[j - 1].x(), -m_normals[j - 1].y()); - m_normals[0] = DoublePoint(-m_normals[1].x(), -m_normals[1].y()); - - k = len - 1; - for (int j = k - 1; j > 0; --j) OffsetPoint(j, k, node.m_jointype); - - if (node.m_endtype == etOpenButt) - { - pt1 = IntPoint2d(Round(m_srcPoly[0].x() - m_normals[0].x() * delta), Round(m_srcPoly[0].y() - m_normals[0].y() * delta)); - m_destPoly.emplace_back(pt1); - pt1 = IntPoint2d(Round(m_srcPoly[0].x() + m_normals[0].x() * delta), Round(m_srcPoly[0].y() + m_normals[0].y() * delta)); - m_destPoly.emplace_back(pt1); - } - else - { - k = 1; - m_sinA = 0; - if (node.m_endtype == etOpenSquare) - DoSquare(0, 1); - else - DoRound(0, 1); - } - m_destPolys.emplace_back(m_destPoly); - } - } -} -//------------------------------------------------------------------------------ - -void ClipperOffset::OffsetPoint(int j, int& k, JoinType jointype) -{ - //cross product ... - m_sinA = (m_normals[k].x() * m_normals[j].y() - m_normals[j].x() * m_normals[k].y()); - if (std::fabs(m_sinA * m_delta) < 1.0) - { - //dot product ... - double cosA = (m_normals[k].x() * m_normals[j].x() + m_normals[j].y() * m_normals[k].y() ); - if (cosA > 0) // angle => 0 degrees - { - m_destPoly.emplace_back(IntPoint2d(Round(m_srcPoly[j].x() + m_normals[k].x() * m_delta), - Round(m_srcPoly[j].y() + m_normals[k].y() * m_delta))); - return; - } - //else angle => 180 degrees - } - else if (m_sinA > 1.0) m_sinA = 1.0; - else if (m_sinA < -1.0) m_sinA = -1.0; - - if (m_sinA * m_delta < 0) - { - m_destPoly.emplace_back(IntPoint2d(Round(m_srcPoly[j].x() + m_normals[k].x() * m_delta), - Round(m_srcPoly[j].y() + m_normals[k].y() * m_delta))); - m_destPoly.emplace_back(m_srcPoly[j]); - m_destPoly.emplace_back(IntPoint2d(Round(m_srcPoly[j].x() + m_normals[j].x() * m_delta), - Round(m_srcPoly[j].y() + m_normals[j].y() * m_delta))); - } - else - switch (jointype) - { - case jtMiter: - { - double r = 1 + (m_normals[j].x() * m_normals[k].x() + - m_normals[j].y() * m_normals[k].y()); - if (r >= m_miterLim) DoMiter(j, k, r); else DoSquare(j, k); - break; - } - case jtSquare: DoSquare(j, k); break; - case jtRound: DoRound(j, k); break; - } - k = j; -} -//------------------------------------------------------------------------------ - -void ClipperOffset::DoSquare(int j, int k) -{ - double dx = std::tan(std::atan2(m_sinA, - m_normals[k].x() * m_normals[j].x() + m_normals[k].y() * m_normals[j].y()) / 4); - m_destPoly.emplace_back(IntPoint2d( - Round(m_srcPoly[j].x() + m_delta * (m_normals[k].x() - m_normals[k].y() * dx)), - Round(m_srcPoly[j].y() + m_delta * (m_normals[k].y() + m_normals[k].x() * dx)))); - m_destPoly.emplace_back(IntPoint2d( - Round(m_srcPoly[j].x() + m_delta * (m_normals[j].x() + m_normals[j].y() * dx)), - Round(m_srcPoly[j].y() + m_delta * (m_normals[j].y() - m_normals[j].x() * dx)))); -} -//------------------------------------------------------------------------------ - -void ClipperOffset::DoMiter(int j, int k, double r) -{ - double q = m_delta / r; - m_destPoly.emplace_back(IntPoint2d(Round(m_srcPoly[j].x() + (m_normals[k].x() + m_normals[j].x()) * q), - Round(m_srcPoly[j].y() + (m_normals[k].y() + m_normals[j].y()) * q))); -} -//------------------------------------------------------------------------------ - -void ClipperOffset::DoRound(int j, int k) -{ - double a = std::atan2(m_sinA, - m_normals[k].x() * m_normals[j].x() + m_normals[k].y() * m_normals[j].y()); - auto steps = std::max(Round(m_StepsPerRad * std::fabs(a)), 1); - - double X = m_normals[k].x(), Y = m_normals[k].y(), X2; - for (int i = 0; i < steps; ++i) - { - m_destPoly.emplace_back(IntPoint2d( - Round(m_srcPoly[j].x() + X * m_delta), - Round(m_srcPoly[j].y() + Y * m_delta))); - X2 = X; - X = X * m_cos - m_sin * Y; - Y = X2 * m_sin + Y * m_cos; - } - m_destPoly.emplace_back(IntPoint2d( - Round(m_srcPoly[j].x() + m_normals[j].x() * m_delta), - Round(m_srcPoly[j].y() + m_normals[j].y() * m_delta))); -} - -//------------------------------------------------------------------------------ -// Miscellaneous public functions -//------------------------------------------------------------------------------ - -// Called by Clipper::ExecuteInternal() -// For each polygon, search for exactly duplicate non-successive points. -// If such a point is found, the loop is split into two pieces. -// Search for the duplicate points is O(n^2)! -// http://www.angusj.com/delphi/clipper/documentation/Docs/Units/ClipperLib/Classes/Clipper/Properties/StrictlySimple.htm -void Clipper::DoSimplePolygons() -{ - size_t i = 0; - while (i < m_PolyOuts.size()) - { - OutRec &outrec = m_PolyOuts[i++]; - OutPt* op = outrec.Pts; - if (!op || outrec.IsOpen) continue; - do //for each Pt in Polygon until duplicate found do ... - { - OutPt* op2 = op->Next; - while (op2 != outrec.Pts) - { - if ((op->Pt == op2->Pt) && op2->Next != op && op2->Prev != op) - { - //split the polygon into two ... - OutPt* op3 = op->Prev; - OutPt* op4 = op2->Prev; - op->Prev = op4; - op4->Next = op; - op2->Prev = op3; - op3->Next = op2; - - outrec.Pts = op; - OutRec* outrec2 = CreateOutRec(); - outrec2->Pts = op2; - UpdateOutPtIdxs(*outrec2); - if (Poly2ContainsPoly1(outrec2->Pts, outrec.Pts)) - { - //OutRec2 is contained by OutRec1 ... - outrec2->IsHole = !outrec.IsHole; - outrec2->FirstLeft = &outrec; - // For each m_PolyOuts, replace FirstLeft from outRec2 to outrec. - if (m_UsingPolyTree) FixupFirstLefts2(outrec2, &outrec); - } - else - if (Poly2ContainsPoly1(outrec.Pts, outrec2->Pts)) - { - //OutRec1 is contained by OutRec2 ... - outrec2->IsHole = outrec.IsHole; - outrec.IsHole = !outrec2->IsHole; - outrec2->FirstLeft = outrec.FirstLeft; - outrec.FirstLeft = outrec2; - // For each m_PolyOuts, replace FirstLeft from outrec to outrec2. - if (m_UsingPolyTree) FixupFirstLefts2(&outrec, outrec2); - } - else - { - //the 2 polygons are separate ... - outrec2->IsHole = outrec.IsHole; - outrec2->FirstLeft = outrec.FirstLeft; - // For each polygon of m_PolyOuts, replace FirstLeft from outrec to outrec2 if the polygon is inside outRec2. - //FIXME This is potentially very expensive! O(n^3)! - if (m_UsingPolyTree) FixupFirstLefts1(&outrec, outrec2); - } - op2 = op; //ie get ready for the Next iteration - } - op2 = op2->Next; - } - op = op->Next; - } - while (op != outrec.Pts); - } -} -//------------------------------------------------------------------------------ - -void ReversePath(Path& p) -{ - std::reverse(p.begin(), p.end()); -} -//------------------------------------------------------------------------------ - -void ReversePaths(Paths& p) -{ - for (Paths::size_type i = 0; i < p.size(); ++i) - ReversePath(p[i]); -} -//------------------------------------------------------------------------------ - -Paths SimplifyPolygon(const Path &in_poly, PolyFillType fillType, bool strictly_simple /* = true */) -{ - Clipper c; - c.StrictlySimple(strictly_simple); - c.AddPath(in_poly, ptSubject, true); - Paths out; - c.Execute(ctUnion, out, fillType, fillType); - return out; -} - -//------------------------------------------------------------------------------ - -inline double DistanceSqrd(const IntPoint& pt1, const IntPoint& pt2) -{ - auto Dx = double(pt1.x() - pt2.x()); - auto dy = double(pt1.y() - pt2.y()); - return (Dx*Dx + dy*dy); -} -//------------------------------------------------------------------------------ - -double DistanceFromLineSqrd( - const IntPoint& pt, const IntPoint& ln1, const IntPoint& ln2) -{ - //The equation of a line in general form (Ax + By + C = 0) - //given 2 points (x¹,y¹) & (x²,y²) is ... - //(y¹ - y²)x + (x² - x¹)y + (y² - y¹)x¹ - (x² - x¹)y¹ = 0 - //A = (y¹ - y²); B = (x² - x¹); C = (y² - y¹)x¹ - (x² - x¹)y¹ - //perpendicular distance of point (x³,y³) = (Ax³ + By³ + C)/Sqrt(A² + B²) - //see http://en.wikipedia.org/wiki/Perpendicular_distance - double A = double(ln1.y() - ln2.y()); - double B = double(ln2.x() - ln1.x()); - double C = A * ln1.x() + B * ln1.y(); - C = A * pt.x() + B * pt.y() - C; - return (C * C) / (A * A + B * B); -} -//--------------------------------------------------------------------------- - -bool SlopesNearCollinear(const IntPoint& pt1, - const IntPoint& pt2, const IntPoint& pt3, double distSqrd) -{ - //this function is more accurate when the point that's geometrically - //between the other 2 points is the one that's tested for distance. - //ie makes it more likely to pick up 'spikes' ... - if (std::abs(pt1.x() - pt2.x()) > std::abs(pt1.y() - pt2.y())) - { - if ((pt1.x() > pt2.x()) == (pt1.x() < pt3.x())) - return DistanceFromLineSqrd(pt1, pt2, pt3) < distSqrd; - else if ((pt2.x() > pt1.x()) == (pt2.x() < pt3.x())) - return DistanceFromLineSqrd(pt2, pt1, pt3) < distSqrd; - else - return DistanceFromLineSqrd(pt3, pt1, pt2) < distSqrd; - } - else - { - if ((pt1.y() > pt2.y()) == (pt1.y() < pt3.y())) - return DistanceFromLineSqrd(pt1, pt2, pt3) < distSqrd; - else if ((pt2.y() > pt1.y()) == (pt2.y() < pt3.y())) - return DistanceFromLineSqrd(pt2, pt1, pt3) < distSqrd; - else - return DistanceFromLineSqrd(pt3, pt1, pt2) < distSqrd; - } -} -//------------------------------------------------------------------------------ - -bool PointsAreClose(IntPoint pt1, IntPoint pt2, double distSqrd) -{ - auto Dx = double(pt1.x() - pt2.x()); - auto dy = double(pt1.y() - pt2.y()); - return ((Dx * Dx) + (dy * dy) <= distSqrd); -} -//------------------------------------------------------------------------------ - -OutPt* ExcludeOp(OutPt* op) -{ - OutPt* result = op->Prev; - result->Next = op->Next; - op->Next->Prev = result; - result->Idx = 0; - return result; -} -//------------------------------------------------------------------------------ - -// Simplify a polygon using a linked list of points. -void CleanPolygon(const Path& in_poly, Path& out_poly, double distance) -{ - //distance = proximity in units/pixels below which vertices - //will be stripped. Default ~= sqrt(2). - - size_t size = in_poly.size(); - - if (size == 0) - { - out_poly.clear(); - return; - } - - OutPts outPts(size); - for (size_t i = 0; i < size; ++i) - { - outPts[i].Pt = in_poly[i]; - outPts[i].Next = &outPts[(i + 1) % size]; - outPts[i].Next->Prev = &outPts[i]; - outPts[i].Idx = 0; - } - - double distSqrd = distance * distance; - OutPt* op = &outPts[0]; - while (op->Idx == 0 && op->Next != op->Prev) - { - if (PointsAreClose(op->Pt, op->Prev->Pt, distSqrd)) - { - op = ExcludeOp(op); - size--; - } - else if (PointsAreClose(op->Prev->Pt, op->Next->Pt, distSqrd)) - { - ExcludeOp(op->Next); - op = ExcludeOp(op); - size -= 2; - } - else if (SlopesNearCollinear(op->Prev->Pt, op->Pt, op->Next->Pt, distSqrd)) - { - op = ExcludeOp(op); - size--; - } - else - { - op->Idx = 1; - op = op->Next; - } - } - - if (size < 3) size = 0; - out_poly.resize(size); - for (size_t i = 0; i < size; ++i) - { - out_poly[i] = op->Pt; - op = op->Next; - } -} -//------------------------------------------------------------------------------ - -void CleanPolygon(Path& poly, double distance) -{ - CleanPolygon(poly, poly, distance); -} -//------------------------------------------------------------------------------ - -void CleanPolygons(const Paths& in_polys, Paths& out_polys, double distance) -{ - out_polys.resize(in_polys.size()); - for (Paths::size_type i = 0; i < in_polys.size(); ++i) - CleanPolygon(in_polys[i], out_polys[i], distance); -} -//------------------------------------------------------------------------------ - -void CleanPolygons(Paths& polys, double distance) -{ - CleanPolygons(polys, polys, distance); -} -//------------------------------------------------------------------------------ - -void Minkowski(const Path& poly, const Path& path, - Paths& solution, bool isSum, bool isClosed) -{ - int delta = (isClosed ? 1 : 0); - size_t polyCnt = poly.size(); - size_t pathCnt = path.size(); - Paths pp; - pp.reserve(pathCnt); - if (isSum) - for (size_t i = 0; i < pathCnt; ++i) - { - Path p; - p.reserve(polyCnt); - for (size_t j = 0; j < poly.size(); ++j) - p.emplace_back(IntPoint2d(path[i].x() + poly[j].x(), path[i].y() + poly[j].y())); - pp.emplace_back(p); - } - else - for (size_t i = 0; i < pathCnt; ++i) - { - Path p; - p.reserve(polyCnt); - for (size_t j = 0; j < poly.size(); ++j) - p.emplace_back(IntPoint2d(path[i].x() - poly[j].x(), path[i].y() - poly[j].y())); - pp.emplace_back(p); - } - - solution.clear(); - solution.reserve((pathCnt + delta) * (polyCnt + 1)); - for (size_t i = 0; i < pathCnt - 1 + delta; ++i) - for (size_t j = 0; j < polyCnt; ++j) - { - Path quad; - quad.reserve(4); - quad.emplace_back(pp[i % pathCnt][j % polyCnt]); - quad.emplace_back(pp[(i + 1) % pathCnt][j % polyCnt]); - quad.emplace_back(pp[(i + 1) % pathCnt][(j + 1) % polyCnt]); - quad.emplace_back(pp[i % pathCnt][(j + 1) % polyCnt]); - if (!Orientation(quad)) ReversePath(quad); - solution.emplace_back(quad); - } -} -//------------------------------------------------------------------------------ - -void MinkowskiSum(const Path& pattern, const Path& path, Paths& solution, bool pathIsClosed) -{ - Minkowski(pattern, path, solution, true, pathIsClosed); - Clipper c; - c.AddPaths(solution, ptSubject, true); - c.Execute(ctUnion, solution, pftNonZero, pftNonZero); -} -//------------------------------------------------------------------------------ - -void TranslatePath(const Path& input, Path& output, const IntPoint& delta) -{ - //precondition: input != output - output.resize(input.size()); - for (size_t i = 0; i < input.size(); ++i) - output[i] = IntPoint2d(input[i].x() + delta.x(), input[i].y() + delta.y()); -} -//------------------------------------------------------------------------------ - -void MinkowskiSum(const Path& pattern, const Paths& paths, Paths& solution, bool pathIsClosed) -{ - Clipper c; - for (size_t i = 0; i < paths.size(); ++i) - { - Paths tmp; - Minkowski(pattern, paths[i], tmp, true, pathIsClosed); - c.AddPaths(tmp, ptSubject, true); - if (pathIsClosed) - { - Path tmp2; - TranslatePath(paths[i], tmp2, pattern[0]); - c.AddPath(tmp2, ptClip, true); - } - } - c.Execute(ctUnion, solution, pftNonZero, pftNonZero); -} -//------------------------------------------------------------------------------ - -void MinkowskiDiff(const Path& poly1, const Path& poly2, Paths& solution) -{ - Minkowski(poly1, poly2, solution, false, true); - Clipper c; - c.AddPaths(solution, ptSubject, true); - c.Execute(ctUnion, solution, pftNonZero, pftNonZero); -} -//------------------------------------------------------------------------------ - -enum NodeType {ntAny, ntOpen, ntClosed}; - -void AddPolyNodeToPaths(const PolyNode& polynode, NodeType nodetype, Paths& paths) -{ - bool match = true; - if (nodetype == ntClosed) match = !polynode.IsOpen(); - else if (nodetype == ntOpen) return; - - if (!polynode.Contour.empty() && match) - paths.emplace_back(polynode.Contour); - for (int i = 0; i < polynode.ChildCount(); ++i) - AddPolyNodeToPaths(*polynode.Childs[i], nodetype, paths); -} - -void AddPolyNodeToPaths(PolyNode&& polynode, NodeType nodetype, Paths& paths) -{ - bool match = true; - if (nodetype == ntClosed) match = !polynode.IsOpen(); - else if (nodetype == ntOpen) return; - - if (!polynode.Contour.empty() && match) - paths.emplace_back(std::move(polynode.Contour)); - for (int i = 0; i < polynode.ChildCount(); ++i) - AddPolyNodeToPaths(std::move(*polynode.Childs[i]), nodetype, paths); -} - -//------------------------------------------------------------------------------ - -void PolyTreeToPaths(const PolyTree& polytree, Paths& paths) -{ - paths.clear(); - paths.reserve(polytree.Total()); - AddPolyNodeToPaths(polytree, ntAny, paths); -} - -void PolyTreeToPaths(PolyTree&& polytree, Paths& paths) -{ - paths.clear(); - paths.reserve(polytree.Total()); - AddPolyNodeToPaths(std::move(polytree), ntAny, paths); -} - -//------------------------------------------------------------------------------ - -void ClosedPathsFromPolyTree(const PolyTree& polytree, Paths& paths) -{ - paths.clear(); - paths.reserve(polytree.Total()); - AddPolyNodeToPaths(polytree, ntClosed, paths); -} -//------------------------------------------------------------------------------ - -void OpenPathsFromPolyTree(PolyTree& polytree, Paths& paths) -{ - paths.clear(); - paths.reserve(polytree.Total()); - //Open paths are top level only, so ... - for (int i = 0; i < polytree.ChildCount(); ++i) - if (polytree.Childs[i]->IsOpen()) - paths.emplace_back(polytree.Childs[i]->Contour); -} -//------------------------------------------------------------------------------ - -std::ostream& operator <<(std::ostream &s, const IntPoint &p) -{ - s << "(" << p.x() << "," << p.y() << ")"; - return s; -} -//------------------------------------------------------------------------------ - -std::ostream& operator <<(std::ostream &s, const Path &p) -{ - if (p.empty()) return s; - Path::size_type last = p.size() -1; - for (Path::size_type i = 0; i < last; i++) - s << "(" << p[i].x() << "," << p[i].y() << "), "; - s << "(" << p[last].x() << "," << p[last].y() << ")\n"; - return s; -} -//------------------------------------------------------------------------------ - -std::ostream& operator <<(std::ostream &s, const Paths &p) -{ - for (Paths::size_type i = 0; i < p.size(); i++) - s << p[i]; - s << "\n"; - return s; -} -//------------------------------------------------------------------------------ - -} //ClipperLib namespace - -#ifdef CLIPPERLIB_NAMESPACE_PREFIX -} // namespace CLIPPERLIB_NAMESPACE_PREFIX -#endif // CLIPPERLIB_NAMESPACE_PREFIX diff --git a/deps_src/clipper/clipper.hpp b/deps_src/clipper/clipper.hpp deleted file mode 100644 index 06637effce..0000000000 --- a/deps_src/clipper/clipper.hpp +++ /dev/null @@ -1,606 +0,0 @@ -/******************************************************************************* -* * -* Author : Angus Johnson * -* Version : 6.4.2 * -* Date : 27 February 2017 * -* Website : http://www.angusj.com * -* Copyright : Angus Johnson 2010-2017 * -* * -* License: * -* Use, modification & distribution is subject to Boost Software License Ver 1. * -* http://www.boost.org/LICENSE_1_0.txt * -* * -* Attributions: * -* The code in this library is an extension of Bala Vatti's clipping algorithm: * -* "A generic solution to polygon clipping" * -* Communications of the ACM, Vol 35, Issue 7 (July 1992) pp 56-63. * -* http://portal.acm.org/citation.cfm?id=129906 * -* * -* Computer graphics and geometric modeling: implementation and algorithms * -* By Max K. Agoston * -* Springer; 1 edition (January 4, 2005) * -* http://books.google.com/books?q=vatti+clipping+agoston * -* * -* See also: * -* "Polygon Offsetting by Computing Winding Numbers" * -* Paper no. DETC2005-85513 pp. 565-575 * -* ASME 2005 International Design Engineering Technical Conferences * -* and Computers and Information in Engineering Conference (IDETC/CIE2005) * -* September 24-28, 2005 , Long Beach, California, USA * -* http://www.me.berkeley.edu/~mcmains/pubs/DAC05OffsetPolygon.pdf * -* * -*******************************************************************************/ - -#ifndef clipper_hpp -#define clipper_hpp - -#include -#include - -#include - -#include - -#define CLIPPER_VERSION "6.2.6" - -//CLIPPERLIB_USE_XYZ: adds a Z member to IntPoint. Adds a minor cost to perfomance. -//#define CLIPPERLIB_USE_XYZ - -//use_lines: Enables line clipping. Adds a very minor cost to performance. -#define use_lines - -//use_deprecated: Enables temporary support for the obsolete functions -//#define use_deprecated - -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#ifdef CLIPPERLIB_NAMESPACE_PREFIX - namespace CLIPPERLIB_NAMESPACE_PREFIX { -#endif // CLIPPERLIB_NAMESPACE_PREFIX - -#ifdef CLIPPERLIB_USE_XYZ - namespace ClipperLib_Z { -#else - namespace ClipperLib { -#endif - -enum ClipType { ctIntersection, ctUnion, ctDifference, ctXor }; -enum PolyType { ptSubject, ptClip }; -//By far the most widely used winding rules for polygon filling are -//EvenOdd & NonZero (GDI, GDI+, XLib, OpenGL, Cairo, AGG, Quartz, SVG, Gr32) -//Others rules include Positive, Negative and ABS_GTR_EQ_TWO (only in OpenGL) -//see http://glprogramming.com/red/chapter11.html -enum PolyFillType { pftEvenOdd, pftNonZero, pftPositive, pftNegative }; - -// If defined, Clipper will work with 32bit signed int coordinates to reduce memory -// consumption and to speed up exact orientation predicate calculation. -// In that case, coordinates and their differences (vectors of the coordinates) have to fit int32_t. -// #define CLIPPERLIB_INT32 - -// Point coordinate type -#ifdef CLIPPERLIB_INT32 - // Coordinates and their differences (vectors of the coordinates) have to fit int32_t. - using cInt = int32_t; - using CrossProductType = int64_t; -#else - using cInt = int64_t; - using CrossProductType = double; - // Maximum cInt value to allow a cross product calculation using 32bit expressions. - static constexpr cInt const loRange = 0x3FFFFFFF; // 0x3FFFFFFF = 1 073 741 823 - // Maximum allowed cInt value. - static constexpr cInt const hiRange = 0x3FFFFFFFFFFFFFFFLL; -#endif // CLIPPERLIB_INT32 - -#ifdef CLIPPERLIB_INTPOINT_TYPE -using IntPoint = CLIPPERLIB_INTPOINT_TYPE; -#else // CLIPPERLIB_INTPOINT_TYPE -using IntPoint = Eigen::Matrix; -#endif // CLIPPERLIB_INTPOINT_TYPE - -using DoublePoint = Eigen::Matrix; - -//------------------------------------------------------------------------------ - -template -using Allocator = tbb::scalable_allocator; -//using Allocator = std::allocator; -using Path = std::vector>; -using Paths = std::vector>; - -inline Path& operator <<(Path& poly, const IntPoint& p) {poly.push_back(p); return poly;} -inline Paths& operator <<(Paths& polys, const Path& p) {polys.push_back(p); return polys;} - -std::ostream& operator <<(std::ostream &s, const IntPoint &p); -std::ostream& operator <<(std::ostream &s, const Path &p); -std::ostream& operator <<(std::ostream &s, const Paths &p); - -//------------------------------------------------------------------------------ - -#ifdef CLIPPERLIB_USE_XYZ -typedef std::function ZFillCallback; -#endif - -enum InitOptions {ioReverseSolution = 1, ioStrictlySimple = 2, ioPreserveCollinear = 4}; -enum JoinType {jtSquare, jtRound, jtMiter}; -enum EndType {etClosedPolygon, etClosedLine, etOpenButt, etOpenSquare, etOpenRound}; - -class PolyNode; -typedef std::vector> PolyNodes; - -class PolyNode -{ -public: - PolyNode() : Parent(0), Index(0), m_IsOpen(false) {} - virtual ~PolyNode(){}; - Path Contour; - PolyNodes Childs; - PolyNode* Parent; - // Traversal of the polygon tree in a depth first fashion. - PolyNode* GetNext() const { return Childs.empty() ? GetNextSiblingUp() : Childs.front(); } - bool IsHole() const; - bool IsOpen() const { return m_IsOpen; } - int ChildCount() const { return (int)Childs.size(); } -private: - unsigned Index; //node index in Parent.Childs - bool m_IsOpen; - JoinType m_jointype; - EndType m_endtype; - PolyNode* GetNextSiblingUp() const { return Parent ? ((Index == Parent->Childs.size() - 1) ? Parent->GetNextSiblingUp() : Parent->Childs[Index + 1]) : nullptr; } - void AddChild(PolyNode& child); - friend class Clipper; //to access Index - friend class ClipperOffset; - friend class PolyTree; //to implement the PolyTree::move operator -}; - -class PolyTree: public PolyNode -{ -public: - PolyTree() {} - PolyTree(PolyTree &&src) { *this = std::move(src); } - virtual ~PolyTree(){Clear();}; - PolyTree& operator=(PolyTree &&src) { - AllNodes = std::move(src.AllNodes); - Contour = std::move(src.Contour); - Childs = std::move(src.Childs); - Parent = nullptr; - Index = src.Index; - m_IsOpen = src.m_IsOpen; - m_jointype = src.m_jointype; - m_endtype = src.m_endtype; - for (size_t i = 0; i < Childs.size(); ++ i) - Childs[i]->Parent = this; - return *this; - } - PolyNode* GetFirst() const { return Childs.empty() ? nullptr : Childs.front(); } - void Clear() { AllNodes.clear(); Childs.clear(); } - int Total() const; - void RemoveOutermostPolygon(); -private: - PolyTree(const PolyTree &src) = delete; - PolyTree& operator=(const PolyTree &src) = delete; - std::vector> AllNodes; - friend class Clipper; //to access AllNodes -}; - -double Area(const Path &poly); -inline bool Orientation(const Path &poly) { return Area(poly) >= 0; } -int PointInPolygon(const IntPoint &pt, const Path &path); - -// Union with "strictly simple" fix enabled. -Paths SimplifyPolygon(const Path &in_poly, PolyFillType fillType = pftNonZero, bool strictly_simple = true); - -void CleanPolygon(const Path& in_poly, Path& out_poly, double distance = 1.415); -void CleanPolygon(Path& poly, double distance = 1.415); -void CleanPolygons(const Paths& in_polys, Paths& out_polys, double distance = 1.415); -void CleanPolygons(Paths& polys, double distance = 1.415); - -void MinkowskiSum(const Path& pattern, const Path& path, Paths& solution, bool pathIsClosed); -void MinkowskiSum(const Path& pattern, const Paths& paths, Paths& solution, bool pathIsClosed); -void MinkowskiDiff(const Path& poly1, const Path& poly2, Paths& solution); - -void PolyTreeToPaths(const PolyTree& polytree, Paths& paths); -void PolyTreeToPaths(PolyTree&& polytree, Paths& paths); -void ClosedPathsFromPolyTree(const PolyTree& polytree, Paths& paths); -void OpenPathsFromPolyTree(PolyTree& polytree, Paths& paths); - -void ReversePath(Path& p); -void ReversePaths(Paths& p); - -struct IntRect { cInt left; cInt top; cInt right; cInt bottom; }; - -//enums that are used internally ... -enum EdgeSide { esLeft = 1, esRight = 2}; - -// namespace Internal { - //forward declarations (for stuff used internally) ... - struct TEdge { - // Bottom point of this edge (with minimum Y). - IntPoint Bot; - // Current position. - IntPoint Curr; - // Top point of this edge (with maximum Y). - IntPoint Top; - // Slope (dx/dy). For horiontal edges, the slope is set to HORIZONTAL (-1.0E+40). - double Dx; - PolyType PolyTyp; - EdgeSide Side; - // Winding number delta. 1 or -1 depending on winding direction, 0 for open paths and flat closed paths. - int WindDelta; - int WindCnt; - int WindCnt2; //winding count of the opposite polytype - int OutIdx; - // Next edge in the input path. - TEdge *Next; - // Previous edge in the input path. - TEdge *Prev; - // Next edge in the Local Minima List chain. - TEdge *NextInLML; - TEdge *NextInAEL; - TEdge *PrevInAEL; - TEdge *NextInSEL; - TEdge *PrevInSEL; - }; - - struct IntersectNode { - IntersectNode(TEdge *Edge1, TEdge *Edge2, IntPoint Pt) : - Edge1(Edge1), Edge2(Edge2), Pt(Pt) {} - TEdge *Edge1; - TEdge *Edge2; - IntPoint Pt; - }; - - struct LocalMinimum { - cInt Y; - TEdge *LeftBound; - TEdge *RightBound; - }; - - // Point of an output polygon. - // 36B on 64bit system without CLIPPERLIB_USE_XYZ. - struct OutPt { - // 4B - int Idx; - // 16B without CLIPPERLIB_USE_XYZ / 24B with CLIPPERLIB_USE_XYZ - IntPoint Pt; - // 4B on 32bit system, 8B on 64bit system - OutPt *Next; - // 4B on 32bit system, 8B on 64bit system - OutPt *Prev; - }; - - using OutPts = std::vector>; - - // Output polygon. - struct OutRec { - int Idx; - bool IsHole; - bool IsOpen; - //The 'FirstLeft' field points to another OutRec that contains or is the - //'parent' of OutRec. It is 'first left' because the ActiveEdgeList (AEL) is - //parsed left from the current edge (owning OutRec) until the owner OutRec - //is found. This field simplifies sorting the polygons into a tree structure - //which reflects the parent/child relationships of all polygons. - //This field should be renamed Parent, and will be later. - OutRec* FirstLeft; - // Used only by void Clipper::BuildResult2(PolyTree& polytree) - PolyNode* PolyNd; - // Linked list of output points, dynamically allocated. - OutPt* Pts; - OutPt* BottomPt; - }; - - struct Join { - Join(OutPt *OutPt1, OutPt *OutPt2, IntPoint OffPt) : - OutPt1(OutPt1), OutPt2(OutPt2), OffPt(OffPt) {} - OutPt *OutPt1; - OutPt *OutPt2; - IntPoint OffPt; - }; -// }; // namespace Internal - -//------------------------------------------------------------------------------ - -//ClipperBase is the ancestor to the Clipper class. It should not be -//instantiated directly. This class simply abstracts the conversion of sets of -//polygon coordinates into edge objects that are stored in a LocalMinima list. -class ClipperBase -{ -public: - ClipperBase() : -#ifndef CLIPPERLIB_INT32 - m_UseFullRange(false), -#endif // CLIPPERLIB_INT32 - m_HasOpenPaths(false) {} - ~ClipperBase() { Clear(); } - bool AddPath(const Path &pg, PolyType PolyTyp, bool Closed); - - template - bool AddPaths(PathsProvider &&paths_provider, PolyType PolyTyp, bool Closed) - { - size_t num_paths = paths_provider.size(); - if (num_paths == 0) - return false; - if (num_paths == 1) - return AddPath(*paths_provider.begin(), PolyTyp, Closed); - - std::vector> num_edges(num_paths, 0); - int num_edges_total = 0; - size_t i = 0; - for (const Path &pg : paths_provider) { - // Remove duplicate end point from a closed input path. - // Remove duplicate points from the end of the input path. - int highI = (int)pg.size() -1; - if (Closed) - while (highI > 0 && (pg[highI] == pg[0])) - --highI; - while (highI > 0 && (pg[highI] == pg[highI -1])) - --highI; - if ((Closed && highI < 2) || (!Closed && highI < 1)) - highI = -1; - num_edges[i ++] = highI + 1; - num_edges_total += highI + 1; - } - if (num_edges_total == 0) - return false; - - // Allocate a new edge array. - std::vector> edges(num_edges_total); - // Fill in the edge array. - bool result = false; - TEdge *p_edge = edges.data(); - i = 0; - for (const Path &pg : paths_provider) { - if (num_edges[i] && !pg.empty()) { - bool res = AddPathInternal(pg, num_edges[i] - 1, PolyTyp, Closed, p_edge); - if (res) { - p_edge += num_edges[i]; - result = true; - } - } - ++ i; - } - if (result) - // At least some edges were generated. Remember the edge array. - m_edges.emplace_back(std::move(edges)); - return result; - } - - void Clear(); - IntRect GetBounds(); - // By default, when three or more vertices are collinear in input polygons (subject or clip), the Clipper object removes the 'inner' vertices before clipping. - // When enabled the PreserveCollinear property prevents this default behavior to allow these inner vertices to appear in the solution. - bool PreserveCollinear() const {return m_PreserveCollinear;}; - void PreserveCollinear(bool value) {m_PreserveCollinear = value;}; -protected: - bool AddPathInternal(const Path &pg, int highI, PolyType PolyTyp, bool Closed, TEdge* edges); - TEdge* AddBoundsToLML(TEdge *e, bool IsClosed); - void Reset(); - TEdge* ProcessBound(TEdge* E, bool IsClockwise); - TEdge* DescendToMin(TEdge *&E); - void AscendToMax(TEdge *&E, bool Appending, bool IsClosed); - - // Local minima (Y, left edge, right edge) sorted by ascending Y. - std::vector> m_MinimaList; - -#ifdef CLIPPERLIB_INT32 - static constexpr const bool m_UseFullRange = false; -#else // CLIPPERLIB_INT32 - // True if the input polygons have abs values higher than loRange, but lower than hiRange. - // False if the input polygons have abs values lower or equal to loRange. - bool m_UseFullRange; -#endif // CLIPPERLIB_INT32 - - // A vector of edges per each input path. - using Edges = std::vector>; - std::vector> m_edges; - // Don't remove intermediate vertices of a collinear sequence of points. - bool m_PreserveCollinear; - // Is any of the paths inserted by AddPath() or AddPaths() open? - bool m_HasOpenPaths; -}; -//------------------------------------------------------------------------------ - -class Clipper : public ClipperBase -{ -public: - Clipper(int initOptions = 0); - ~Clipper() { Clear(); } - void Clear() { ClipperBase::Clear(); DisposeAllOutRecs(); } - bool Execute(ClipType clipType, - Paths &solution, - PolyFillType fillType = pftEvenOdd) - { return Execute(clipType, solution, fillType, fillType); } - bool Execute(ClipType clipType, - Paths &solution, - PolyFillType subjFillType, - PolyFillType clipFillType); - bool Execute(ClipType clipType, - PolyTree &polytree, - PolyFillType fillType = pftEvenOdd) - { return Execute(clipType, polytree, fillType, fillType); } - bool Execute(ClipType clipType, - PolyTree &polytree, - PolyFillType subjFillType, - PolyFillType clipFillType); - bool ReverseSolution() const { return m_ReverseOutput; }; - void ReverseSolution(bool value) {m_ReverseOutput = value;}; - bool StrictlySimple() const {return m_StrictSimple;}; - void StrictlySimple(bool value) {m_StrictSimple = value;}; - //set the callback function for z value filling on intersections (otherwise Z is 0) -#ifdef CLIPPERLIB_USE_XYZ - void ZFillFunction(ZFillCallback zFillFunc) { m_ZFill = zFillFunc; } -#endif -protected: - void Reset(); - virtual bool ExecuteInternal(); -private: - - // Output polygons. - std::deque> m_PolyOuts; - // Output points, allocated by a continuous sets of m_OutPtsChunkSize. - static constexpr const size_t m_OutPtsChunkSize = 32; - std::deque, Allocator>> m_OutPts; - // List of free output points, to be used before taking a point from m_OutPts or allocating a new chunk. - OutPt *m_OutPtsFree; - size_t m_OutPtsChunkLast; - - std::vector> m_Joins; - std::vector> m_GhostJoins; - std::vector> m_IntersectList; - ClipType m_ClipType; - // A priority queue (a binary heap) of Y coordinates. - using cInts = std::vector>; - std::priority_queue m_Scanbeam; - // Maxima are collected by ProcessEdgesAtTopOfScanbeam(), consumed by ProcessHorizontal(). - cInts m_Maxima; - TEdge *m_ActiveEdges; - TEdge *m_SortedEdges; - PolyFillType m_ClipFillType; - PolyFillType m_SubjFillType; - bool m_ReverseOutput; - // Does the result go to a PolyTree or Paths? - bool m_UsingPolyTree; - bool m_StrictSimple; -#ifdef CLIPPERLIB_USE_XYZ - ZFillCallback m_ZFill; //custom callback -#endif - void SetWindingCount(TEdge& edge) const; - bool IsEvenOddFillType(const TEdge& edge) const - { return (edge.PolyTyp == ptSubject) ? m_SubjFillType == pftEvenOdd : m_ClipFillType == pftEvenOdd; } - bool IsEvenOddAltFillType(const TEdge& edge) const - { return (edge.PolyTyp == ptSubject) ? m_ClipFillType == pftEvenOdd : m_SubjFillType == pftEvenOdd; } - void InsertLocalMinimaIntoAEL(const cInt botY); - void InsertEdgeIntoAEL(TEdge *edge, TEdge* startEdge); - void AddEdgeToSEL(TEdge *edge); - void CopyAELToSEL(); - void DeleteFromSEL(TEdge *e); - void DeleteFromAEL(TEdge *e); - void UpdateEdgeIntoAEL(TEdge *&e); - void SwapPositionsInSEL(TEdge *edge1, TEdge *edge2); - bool IsContributing(const TEdge& edge) const; - bool IsTopHorz(const cInt XPos); - void SwapPositionsInAEL(TEdge *edge1, TEdge *edge2); - void DoMaxima(TEdge *e); - void ProcessHorizontals(); - void ProcessHorizontal(TEdge *horzEdge); - void AddLocalMaxPoly(TEdge *e1, TEdge *e2, const IntPoint &pt); - OutPt* AddLocalMinPoly(TEdge *e1, TEdge *e2, const IntPoint &pt); - OutRec* GetOutRec(int idx); - void AppendPolygon(TEdge *e1, TEdge *e2); - void IntersectEdges(TEdge *e1, TEdge *e2, IntPoint &pt); - OutRec* CreateOutRec(); - OutPt* AddOutPt(TEdge *e, const IntPoint &pt); - OutPt* GetLastOutPt(TEdge *e); - OutPt* AllocateOutPt(); - OutPt* DupOutPt(OutPt* outPt, bool InsertAfter); - // Add the point to a list of free points. - void DisposeOutPt(OutPt *pt) { pt->Next = m_OutPtsFree; m_OutPtsFree = pt; } - void DisposeOutPts(OutPt*& pp) { if (pp != nullptr) { pp->Prev->Next = m_OutPtsFree; m_OutPtsFree = pp; } } - void DisposeAllOutRecs(); - bool ProcessIntersections(const cInt topY); - void BuildIntersectList(const cInt topY); - void ProcessEdgesAtTopOfScanbeam(const cInt topY); - void BuildResult(Paths& polys); - void BuildResult2(PolyTree& polytree); - void SetHoleState(TEdge *e, OutRec *outrec); - bool FixupIntersectionOrder(); - void FixupOutPolygon(OutRec &outrec); - void FixupOutPolyline(OutRec &outrec); - bool FindOwnerFromSplitRecs(OutRec &outRec, OutRec *&currOrfl); - void FixHoleLinkage(OutRec &outrec); - bool JoinPoints(Join *j, OutRec* outRec1, OutRec* outRec2); - bool JoinHorz(OutPt* op1, OutPt* op1b, OutPt* op2, OutPt* op2b, const IntPoint &Pt, bool DiscardLeft); - void JoinCommonEdges(); - void DoSimplePolygons(); - void FixupFirstLefts1(OutRec* OldOutRec, OutRec* NewOutRec); - void FixupFirstLefts2(OutRec* InnerOutRec, OutRec* OuterOutRec); - void FixupFirstLefts3(OutRec* OldOutRec, OutRec* NewOutRec); -#ifdef CLIPPERLIB_USE_XYZ - void SetZ(IntPoint& pt, TEdge& e1, TEdge& e2); -#endif -}; -//------------------------------------------------------------------------------ - -class ClipperOffset -{ -public: - ClipperOffset(double miterLimit = 2.0, double roundPrecision = 0.25, double shortestEdgeLength = 0.) : - MiterLimit(miterLimit), ArcTolerance(roundPrecision), ShortestEdgeLength(shortestEdgeLength), m_lowest(-1, 0) {} - ~ClipperOffset() { Clear(); } - void AddPath(const Path& path, JoinType joinType, EndType endType); - template - void AddPaths(PathsProvider &&paths, JoinType joinType, EndType endType) { - for (const Path &path : paths) - AddPath(path, joinType, endType); - } - void Execute(Paths& solution, double delta); - void Execute(PolyTree& solution, double delta); - void Clear(); - double MiterLimit; - double ArcTolerance; - double ShortestEdgeLength; - -private: - Paths m_destPolys; - Path m_srcPoly; - Path m_destPoly; - std::vector> m_normals; - double m_delta, m_sinA, m_sin, m_cos; - double m_miterLim, m_StepsPerRad; - // x: index of the lowest contour in m_polyNodes - // y: index of the lowest point in the lowest contour - IntPoint m_lowest; - PolyNode m_polyNodes; - - void FixOrientations(); - void DoOffset(double delta); - void OffsetPoint(int j, int& k, JoinType jointype); - void DoSquare(int j, int k); - void DoMiter(int j, int k, double r); - void DoRound(int j, int k); -}; -//------------------------------------------------------------------------------ - -class clipperException : public std::exception -{ - public: - clipperException(const char* description): m_descr(description) {} - virtual ~clipperException() throw() {} - virtual const char* what() const throw() {return m_descr.c_str();} - private: - std::string m_descr; -}; -//------------------------------------------------------------------------------ - -// Union with "strictly simple" fix enabled. -template -inline Paths SimplifyPolygons(PathsProvider &&in_polys, PolyFillType fillType = pftNonZero, bool strictly_simple = true) { - Clipper c; - c.StrictlySimple(strictly_simple); - c.AddPaths(std::forward(in_polys), ptSubject, true); - Paths out; - c.Execute(ctUnion, out, fillType, fillType); - return out; -} - -} //ClipperLib namespace - -#ifdef CLIPPERLIB_NAMESPACE_PREFIX -} // namespace CLIPPERLIB_NAMESPACE_PREFIX -#endif // CLIPPERLIB_NAMESPACE_PREFIX - -#endif //clipper_hpp diff --git a/deps_src/clipper/clipper_z.cpp b/deps_src/clipper/clipper_z.cpp deleted file mode 100644 index f26be7089a..0000000000 --- a/deps_src/clipper/clipper_z.cpp +++ /dev/null @@ -1,7 +0,0 @@ -// Hackish wrapper around the ClipperLib library to compile the Clipper library with the Z support. - -// Enable the Z coordinate support. -#define CLIPPERLIB_USE_XYZ - -// and let it compile -#include "clipper.cpp" diff --git a/deps_src/clipper/clipper_z.hpp b/deps_src/clipper/clipper_z.hpp deleted file mode 100644 index 20596d8e19..0000000000 --- a/deps_src/clipper/clipper_z.hpp +++ /dev/null @@ -1,18 +0,0 @@ -// Hackish wrapper around the ClipperLib library to compile the Clipper library with the Z support. - -#ifndef clipper_z_hpp -#ifdef clipper_hpp -#error "You should include clipper_z.hpp before clipper.hpp" -#endif - -#define clipper_z_hpp - -// Enable the Z coordinate support. -#define CLIPPERLIB_USE_XYZ - -#include "clipper.hpp" - -#undef clipper_hpp -#undef CLIPPERLIB_USE_XYZ - -#endif // clipper_z_hpp diff --git a/deps_src/clipper2/CMakeLists.txt b/deps_src/clipper2/CMakeLists.txt index 86c9a9efab..6343a6ceb5 100644 --- a/deps_src/clipper2/CMakeLists.txt +++ b/deps_src/clipper2/CMakeLists.txt @@ -1,5 +1,5 @@ cmake_minimum_required(VERSION 3.10) -project(Clipper2 VERSION 1.5.2 LANGUAGES C CXX) +project(Clipper2 VERSION 2.0.1 LANGUAGES C CXX) set(CMAKE_POSITION_INDEPENDENT_CODE ON) set(CMAKE_CXX_STANDARD 17) @@ -19,6 +19,7 @@ set(CLIPPER2_INC Clipper2Lib/include/clipper2/clipper.minkowski.h Clipper2Lib/include/clipper2/clipper.offset.h Clipper2Lib/include/clipper2/clipper.rectclip.h + Clipper2Lib/include/clipper2/clipper.triangulation.h Clipper2Lib/include/clipper2/clipper2_z.hpp ) @@ -26,6 +27,7 @@ set(CLIPPER2_SRC Clipper2Lib/src/clipper.engine.cpp Clipper2Lib/src/clipper.offset.cpp Clipper2Lib/src/clipper.rectclip.cpp + Clipper2Lib/src/clipper.triangulation.cpp Clipper2Lib/src/clipper2_z.cpp ) @@ -36,6 +38,9 @@ target_include_directories(Clipper2 PUBLIC Clipper2Lib/include ) +# Engine nodes are allocated through tbbmalloc (see clipper.engine.cpp). +target_link_libraries(Clipper2 PRIVATE TBB::tbbmalloc) + if (WIN32) if (MSVC AND NOT CMAKE_CXX_COMPILER_ID STREQUAL "Clang") target_compile_options(Clipper2 PRIVATE /W4 /WX) diff --git a/deps_src/clipper2/Clipper2Lib/include/clipper2/clipper.core.h b/deps_src/clipper2/Clipper2Lib/include/clipper2/clipper.core.h index 6d00f8fa07..25a5c02cc2 100644 --- a/deps_src/clipper2/Clipper2Lib/include/clipper2/clipper.core.h +++ b/deps_src/clipper2/Clipper2Lib/include/clipper2/clipper.core.h @@ -1,8 +1,8 @@ /******************************************************************************* * Author : Angus Johnson * -* Date : 12 May 2024 * +* Date : 12 October 2025 * * Website : https://www.angusj.com * -* Copyright : Angus Johnson 2010-2024 * +* Copyright : Angus Johnson 2010-2025 * * Purpose : Core Clipper Library structures and functions * * License : https://www.boost.org/LICENSE_1_0.txt * *******************************************************************************/ @@ -251,6 +251,20 @@ namespace Clipper2Lib { template using Paths = std::vector>; + template + Path& operator<<(Path& poly, const Point& p) + { + poly.emplace_back(p); + return poly; + } + + template + Paths& operator<<(Paths& polys, const Path& p) + { + polys.emplace_back(p); + return polys; + } + using Path64 = Path; using PathD = Path; using Paths64 = std::vector< Path64>; @@ -685,32 +699,31 @@ namespace Clipper2Lib { inline int TriSign(int64_t x) // returns 0, 1 or -1 { - return (x > 0) - (x < 0); + return (x > 0) - (x < 0); } - struct MultiplyUInt64Result + struct UInt128Struct { - const uint64_t result = 0; - const uint64_t carry = 0; + const uint64_t lo = 0; + const uint64_t hi = 0; - bool operator==(const MultiplyUInt64Result& other) const + bool operator==(const UInt128Struct& other) const { - return result == other.result && carry == other.carry; + return lo == other.lo && hi == other.hi; }; + }; - inline MultiplyUInt64Result Multiply(uint64_t a, uint64_t b) // #834, #835 + inline UInt128Struct MultiplyUInt64(uint64_t a, uint64_t b) // #834, #835 { + // note to self - lamba expressions follow const auto lo = [](uint64_t x) { return x & 0xFFFFFFFF; }; const auto hi = [](uint64_t x) { return x >> 32; }; const uint64_t x1 = lo(a) * lo(b); const uint64_t x2 = hi(a) * lo(b) + hi(x1); const uint64_t x3 = lo(a) * hi(b) + lo(x2); - const uint64_t result = lo(x3) << 32 | lo(x1); - const uint64_t carry = hi(a) * hi(b) + hi(x2) + hi(x3); - - return { result, carry }; + return { uint64_t(lo(x3) << 32 | lo(x1)), uint64_t(hi(a) * hi(b) + hi(x2) + hi(x3)) }; } // returns true if (and only if) a * b == c * d @@ -727,14 +740,50 @@ namespace Clipper2Lib { const auto abs_c = static_cast(std::abs(c)); const auto abs_d = static_cast(std::abs(d)); - const auto abs_ab = Multiply(abs_a, abs_b); - const auto abs_cd = Multiply(abs_c, abs_d); + const auto ab = MultiplyUInt64(abs_a, abs_b); + const auto cd = MultiplyUInt64(abs_c, abs_d); // nb: it's important to differentiate 0 values here from other values const auto sign_ab = TriSign(a) * TriSign(b); const auto sign_cd = TriSign(c) * TriSign(d); - return abs_ab == abs_cd && sign_ab == sign_cd; + return ab == cd && sign_ab == sign_cd; +#endif + } + + template + inline int CrossProductSign(const Point& pt1, const Point& pt2, const Point& pt3) + { + const auto a = pt2.x - pt1.x; + const auto b = pt3.y - pt2.y; + const auto c = pt2.y - pt1.y; + const auto d = pt3.x - pt2.x; + +#if (defined(__clang__) || defined(__GNUC__)) && UINTPTR_MAX >= UINT64_MAX + const auto ab = static_cast<__int128_t>(a) * static_cast<__int128_t>(b); + const auto cd = static_cast<__int128_t>(c) * static_cast<__int128_t>(d); + if (ab > cd) return 1; + else if (ab < cd) return -1; + else return 0; +#else + const auto ab = MultiplyUInt64(std::abs(a), std::abs(b)); + const auto cd = MultiplyUInt64(std::abs(c), std::abs(d)); + + const auto sign_ab = TriSign(a) * TriSign(b); + const auto sign_cd = TriSign(c) * TriSign(d); + + if (sign_ab == sign_cd) + { + int result; + if (ab.hi == cd.hi) + { + if (ab.lo == cd.lo) return 0; + result = (ab.lo > cd.lo) ? 1 : -1; + } + else result = (ab.hi > cd.hi) ? 1 : -1; + return (sign_ab > 0) ? result : -result; + } + return (sign_ab > sign_cd) ? 1 : -1; #endif } @@ -838,6 +887,10 @@ namespace Clipper2Lib { return Area(poly) >= 0; } + // GetLineIntersectPt - a 'true' result is non-parallel. The 'ip' will also + // be constrained to seg1. However, it's possible that 'ip' won't be inside + // seg2, even when 'ip' hasn't been constrained (ie 'ip' is inside seg1). + #if CLIPPER2_HI_PRECISION // caution: this will compromise performance // https://github.com/AngusJohnson/Clipper2/issues/317#issuecomment-1314023253 @@ -845,7 +898,7 @@ namespace Clipper2Lib { #define CC_MIN(x,y) ((x)>(y)?(y):(x)) #define CC_MAX(x,y) ((x)<(y)?(y):(x)) template - inline bool GetSegmentIntersectPt(const Point& ln1a, const Point& ln1b, + inline bool GetLineIntersectPt(const Point& ln1a, const Point& ln1b, const Point& ln2a, const Point& ln2b, Point& ip) { double ln1dy = static_cast(ln1b.y - ln1a.y); @@ -891,11 +944,14 @@ namespace Clipper2Lib { ip.x = originx + static_cast(hitx); ip.y = originy + static_cast(hity); } +#ifdef USINGZ + ip.z = 0; +#endif return true; } #else template - inline bool GetSegmentIntersectPt(const Point& ln1a, const Point& ln1b, + inline bool GetLineIntersectPt(const Point& ln1a, const Point& ln1b, const Point& ln2a, const Point& ln2b, Point& ip) { // https://en.wikipedia.org/wiki/Line%E2%80%93line_intersection @@ -913,7 +969,10 @@ namespace Clipper2Lib { { ip.x = static_cast(ln1a.x + t * dx1); ip.y = static_cast(ln1a.y + t * dy1); - } +#ifdef USINGZ + ip.z = 0; +#endif + } return true; } #endif @@ -940,30 +999,53 @@ namespace Clipper2Lib { } template - inline int GetSign(const T& val) - { - if (!val) return 0; + inline int GetSign(const T& val) + { + if (!val) return 0; return (val > 0) ? 1 : -1; } inline bool SegmentsIntersect(const Point64& seg1a, const Point64& seg1b, const Point64& seg2a, const Point64& seg2b, bool inclusive = false) { + double dy1 = static_cast(seg1b.y - seg1a.y); + double dx1 = static_cast(seg1b.x - seg1a.x); + double dy2 = static_cast(seg2b.y - seg2a.y); + double dx2 = static_cast(seg2b.x - seg2a.x); + double cp = dy1 * dx2 - dy2 * dx1; + if (cp == 0) return false; // ie parallel segments + if (inclusive) { - double res1 = CrossProduct(seg1a, seg2a, seg2b); - double res2 = CrossProduct(seg1b, seg2a, seg2b); - if (res1 * res2 > 0) return false; - double res3 = CrossProduct(seg2a, seg1a, seg1b); - double res4 = CrossProduct(seg2b, seg1a, seg1b); - if (res3 * res4 > 0) return false; - return (res1 || res2 || res3 || res4); // ensures not collinear + //result **includes** segments that touch at an end point + double t = ((seg1a.x - seg2a.x) * dy2 - (seg1a.y - seg2a.y) * dx2); + if (t == 0) return true; + if (t > 0) + { + if (cp < 0 || t > cp) return false; + } + else if (cp > 0 || t < cp) return false; // false when t more neg. than cp + + t = ((seg1a.x - seg2a.x) * dy1 - (seg1a.y - seg2a.y) * dx1); + if (t == 0) return true; + if (t > 0) return (cp > 0 && t <= cp); + else return (cp < 0 && t >= cp); // true when t less neg. than cp } - else { - return (GetSign(CrossProduct(seg1a, seg2a, seg2b)) * - GetSign(CrossProduct(seg1b, seg2a, seg2b)) < 0) && - (GetSign(CrossProduct(seg2a, seg1a, seg1b)) * - GetSign(CrossProduct(seg2b, seg1a, seg1b)) < 0); + else + { + //result **excludes** segments that touch at an end point + double t = ((seg1a.x - seg2a.x) * dy2 - (seg1a.y - seg2a.y) * dx2); + if (t == 0) return false; + if (t > 0) + { + if (cp < 0 || t >= cp) return false; + } + else if (cp > 0 || t <= cp ) return false; // false when t more neg. than cp + + t = ((seg1a.x - seg2a.x) * dy1 - (seg1a.y - seg2a.y) * dx1); + if (t == 0) return false; + if (t > 0) return (cp > 0 && t < cp); + else return (cp < 0 && t > cp); // true when t less neg. than cp } } @@ -1051,7 +1133,7 @@ namespace Clipper2Lib { val = 1 - val; // toggle val else { - double d = CrossProduct(*prev, *curr, pt); + int d = CrossProductSign(*prev, *curr, pt); if (d == 0) return PointInPolygonResult::IsOn; if ((d < 0) == is_above) val = 1 - val; } @@ -1065,7 +1147,7 @@ namespace Clipper2Lib { if (curr == cend) curr = cbegin; if (curr == cbegin) prev = cend - 1; else prev = curr - 1; - double d = CrossProduct(*prev, *curr, pt); + int d = CrossProductSign(*prev, *curr, pt); if (d == 0) return PointInPolygonResult::IsOn; if ((d < 0) == is_above) val = 1 - val; } diff --git a/deps_src/clipper2/Clipper2Lib/include/clipper2/clipper.engine.h b/deps_src/clipper2/Clipper2Lib/include/clipper2/clipper.engine.h index 8387d2337d..5151e53c7b 100644 --- a/deps_src/clipper2/Clipper2Lib/include/clipper2/clipper.engine.h +++ b/deps_src/clipper2/Clipper2Lib/include/clipper2/clipper.engine.h @@ -15,6 +15,13 @@ #include #include +// Orca: engine nodes are allocated through tbbmalloc, see clipper.engine.cpp. +#define CLIPPER2_NODE_ALLOCATOR \ + static void* operator new(size_t size); \ + static void operator delete(void* ptr) noexcept; \ + static void* operator new[](size_t size); \ + static void operator delete[](void* ptr) noexcept; + #ifdef USINGZ namespace Clipper2Lib_Z { #else @@ -50,6 +57,7 @@ namespace Clipper2Lib { } struct Vertex { + CLIPPER2_NODE_ALLOCATOR Point64 pt; Vertex* next = nullptr; Vertex* prev = nullptr; @@ -57,6 +65,7 @@ namespace Clipper2Lib { }; struct OutPt { + CLIPPER2_NODE_ALLOCATOR Point64 pt; OutPt* next = nullptr; OutPt* prev = nullptr; @@ -81,6 +90,7 @@ namespace Clipper2Lib { //OutRec: contains a path in the clipping solution. Edges in the AEL will //have OutRec pointers assigned when they form part of the clipping solution. struct OutRec { + CLIPPER2_NODE_ALLOCATOR size_t idx = 0; OutRec* owner = nullptr; Active* front_edge = nullptr; @@ -106,6 +116,7 @@ namespace Clipper2Lib { /////////////////////////////////////////////////////////////////// struct Active { + CLIPPER2_NODE_ALLOCATOR Point64 bot; Point64 top; int64_t curr_x = 0; //current (updated at every new scanline) @@ -133,6 +144,7 @@ namespace Clipper2Lib { }; struct LocalMinima { + CLIPPER2_NODE_ALLOCATOR Vertex* vertex; PathType polytype; bool is_open; @@ -303,6 +315,7 @@ namespace Clipper2Lib { protected: PolyPath* parent_; public: + CLIPPER2_NODE_ALLOCATOR PolyPath(PolyPath* parent = nullptr): parent_(parent){} virtual ~PolyPath() {}; //https://en.cppreference.com/w/cpp/language/rule_of_three @@ -330,15 +343,16 @@ namespace Clipper2Lib { //Even levels except level 0 return lvl && !(lvl & 1); } - template - static double Clipper2LibArea(const Path &poly) - { + // Area() of the namespace this header is compiled into (Clipper2Lib or Clipper2Lib_Z). + template + static double Clipper2LibArea(const Path &poly) + { #ifdef USINGZ - return Clipper2Lib_Z::Area(poly); + return Clipper2Lib_Z::Area(poly); #else - return Clipper2Lib::Area(poly); + return Clipper2Lib::Area(poly); #endif - } + } }; typedef typename std::vector> PolyPath64List; @@ -388,7 +402,8 @@ namespace Clipper2Lib { double Area() const { - return std::accumulate(childs_.cbegin(), childs_.cend(), Clipper2LibArea(polygon_), + return std::accumulate(childs_.cbegin(), childs_.cend(), + Clipper2LibArea(polygon_), [](double a, const auto& child) {return a + child->Area(); }); } @@ -462,7 +477,8 @@ namespace Clipper2Lib { double Area() const { - return std::accumulate(childs_.begin(), childs_.end(), Clipper2LibArea(polygon_), + return std::accumulate(childs_.begin(), childs_.end(), + Clipper2LibArea(polygon_), [](double a, const auto& child) {return a + child->Area(); }); } }; diff --git a/deps_src/clipper2/Clipper2Lib/include/clipper2/clipper.export.h b/deps_src/clipper2/Clipper2Lib/include/clipper2/clipper.export.h index a0d33d51df..2694f285fe 100644 --- a/deps_src/clipper2/Clipper2Lib/include/clipper2/clipper.export.h +++ b/deps_src/clipper2/Clipper2Lib/include/clipper2/clipper.export.h @@ -19,17 +19,17 @@ 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 are exported here as very simple array structures -(either of int64_t or double) that can be parsed by just about any +languages, these paths are exported here as very simple array structures +(either of int64_t or double) that can be parsed by just about any programming language. These 2D paths are defined by series of x and y coordinates together with an optional user-defined 'z' value (see Z-values below). Hence, a vertex refers -to a single x and y coordinate (+/- a user-defined value). Data structures -have names with suffixes that indicate the array type (either int64_t or -double). For example, the data structure CPath64 contains an array of int64_t -values, whereas the data structure CPathD contains an array of double. -Where documentation omits the type suffix (eg CPath), it is referring to an +to a single x and y coordinate (+/- a user-defined value). Data structures +have names with suffixes that indicate the array type (either int64_t or +double). For example, the data structure CPath64 contains an array of int64_t +values, whereas the data structure CPathD contains an array of double. +Where documentation omits the type suffix (eg CPath), it is referring to an array whose data type could be either int64_t or double. For conciseness, the following letters are used in the diagrams below: @@ -39,10 +39,10 @@ A: Number of elements in an array CPath64 and CPathD: -These are arrays of either int64_t or double values. Apart from -the first two elements, these arrays are a series of vertices -that together define a path. The very first element contains the -number of vertices (N) in the path, while second element should +These are arrays of either int64_t or double values. Apart from +the first two elements, these arrays are a series of vertices +that together define a path. The very first element contains the +number of vertices (N) in the path, while second element should contain a 0 value. _______________________________________________________________ | counters | vertex1 | vertex2 | ... | vertexN | @@ -52,9 +52,9 @@ _______________________________________________________________ CPaths64 and CPathsD: These are also arrays of either int64_t or double values that -contain any number of consecutive CPath structures. However, +contain any number of consecutive CPath structures. However, preceding the first path is a pair of values. The first value -contains the length of the entire array structure (A), and the +contains the length of the entire array structure (A), and the second contains the number (ie count) of contained paths (C). Memory allocation for CPaths64 = A * sizeof(int64_t) Memory allocation for CPathsD = A * sizeof(double) @@ -65,12 +65,12 @@ __________________________________________ CPolytree64 and CPolytreeD: -The entire polytree structure is an array of int64_t or double. The -first element in the array indicates the array's total length (A). -The second element indicates the number (C) of CPolyPath structures +The entire polytree structure is an array of int64_t or double. The +first element in the array indicates the array's total length (A). +The second element indicates the number (C) of CPolyPath structures that are the TOP LEVEL CPolyPath in the polytree, and these top -level CPolyPath immediately follow these first two array elements. -These top level CPolyPath structures may, in turn, contain nested +level CPolyPath immediately follow these first two array elements. +These top level CPolyPath structures may, in turn, contain nested CPolyPath children, and these collectively make a tree structure. _________________________________________________________ | counters | CPolyPath1 | CPolyPath2 | ... | CPolyPathC | @@ -116,13 +116,10 @@ the four vertices that define the two segments that are intersecting. #include "clipper2/clipper.engine.h" #include "clipper2/clipper.offset.h" #include "clipper2/clipper.rectclip.h" +#include "clipper2/clipper.triangulation.h" #include -#ifdef USINGZ -namespace Clipper2Lib_Z { -#else namespace Clipper2Lib { -#endif typedef int64_t* CPath64; typedef int64_t* CPaths64; @@ -254,9 +251,9 @@ ZCallback64 dllCallback64 = nullptr; ZCallbackD dllCallbackD = nullptr; constexpr int EXPORT_VERTEX_DIMENSIONALITY = 3; -#else +#else constexpr int EXPORT_VERTEX_DIMENSIONALITY = 2; -#endif +#endif template static void GetPathCountAndCPathsArrayLen(const Paths& paths, @@ -396,7 +393,7 @@ static Path ConvertCPathToPathT(T* path) #ifdef USINGZ z_type z = Reinterpret(*v++); result.emplace_back(x, y, z); -#else +#else result.emplace_back(x, y); #endif } @@ -414,7 +411,7 @@ static Paths ConvertCPathsToPathsT(T* paths) for (size_t i = 0; i < cnt; ++i) { size_t cnt2 = static_cast(*v); - v += 2; + v += 2; Path path; path.reserve(cnt2); for (size_t j = 0; j < cnt2; ++j) @@ -447,7 +444,7 @@ static Path64 ConvertCPathDToPath64WithScale(const CPathD path, double scale) #ifdef USINGZ z_type z = Reinterpret(*v++); result.emplace_back(x, y, z); -#else +#else result.emplace_back(x, y); #endif } @@ -492,7 +489,7 @@ static void CreateCPolyPath64(const PolyPath64* pp, int64_t*& v) { *v++ = pt.x; *v++ = pt.y; -#ifdef USINGZ +#ifdef USINGZ * v++ = Reinterpret(pt.z); // raw memory copy #endif } @@ -508,7 +505,7 @@ static void CreateCPolyPathD(const PolyPathD* pp, double*& v) { *v++ = pt.x; *v++ = pt.y; -#ifdef USINGZ +#ifdef USINGZ * v++ = Reinterpret(pt.z); // raw memory copy #endif } @@ -816,6 +813,24 @@ EXTERN_DLL_EXPORT CPaths64 MinkowskiDiff64(const CPath64& cpattern, const CPath6 return CreateCPathsFromPathsT(solution); } +EXTERN_DLL_EXPORT CPaths64 Triangulate64(const CPaths64 paths, bool use_delaunay) +{ + Paths64 pp = ConvertCPathsToPathsT(paths); + Paths64 sol; + if (Triangulate(pp, sol, use_delaunay) != TriangulateResult::success) return nullptr; + return CreateCPathsFromPathsT(sol); +} + +EXTERN_DLL_EXPORT CPathsD TriangulateD(const CPathsD paths, int decimal_precison, bool use_delaunay) +{ + if (decimal_precison < -8 || decimal_precison > 8) return nullptr; + const double scale = std::pow(10, decimal_precison); + Paths64 pp = ConvertCPathsDToPaths64(paths, scale); + Paths64 sol; + if (Triangulate(pp, sol, use_delaunay) != TriangulateResult::success) return nullptr; + return CreateCPathsDFromPaths64(sol, 1 / scale); +} + #ifdef USINGZ typedef void (*DLLZCallback64)(const Point64& e1bot, const Point64& e1top, const Point64& e2bot, const Point64& e2top, Point64& pt); typedef void (*DLLZCallbackD)(const PointD& e1bot, const PointD& e1top, const PointD& e2bot, const PointD& e2top, PointD& pt); diff --git a/deps_src/clipper2/Clipper2Lib/include/clipper2/clipper.h b/deps_src/clipper2/Clipper2Lib/include/clipper2/clipper.h index f6301fc52e..139dd0f9bd 100644 --- a/deps_src/clipper2/Clipper2Lib/include/clipper2/clipper.h +++ b/deps_src/clipper2/Clipper2Lib/include/clipper2/clipper.h @@ -1,8 +1,8 @@ /******************************************************************************* * Author : Angus Johnson * -* Date : 27 April 2024 * +* Date : 5 March 2025 * * Website : https://www.angusj.com * -* Copyright : Angus Johnson 2010-2024 * +* Copyright : Angus Johnson 2010-2025 * * Purpose : This module provides a simple interface to the Clipper Library * * License : https://www.boost.org/LICENSE_1_0.txt * *******************************************************************************/ @@ -13,14 +13,15 @@ #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" +#include "clipper2/clipper.minkowski.h" +#include "clipper2/clipper.triangulation.h" #include #ifdef USINGZ - namespace Clipper2Lib_Z { +namespace Clipper2Lib_Z { #else - namespace Clipper2Lib { +namespace Clipper2Lib { #endif inline Paths64 BooleanOp(ClipType cliptype, FillRule fillrule, @@ -154,14 +155,14 @@ if (!delta) return paths; if (error_code) return PathsD(); const double scale = std::pow(10, precision); - ClipperOffset clip_offset(miter_limit, arc_tolerance); + ClipperOffset clip_offset(miter_limit, arc_tolerance * scale); clip_offset.AddPaths(ScalePaths(paths, scale, error_code), jt, et); if (error_code) return PathsD(); Paths64 solution; clip_offset.Execute(delta * scale, solution); return ScalePaths(solution, 1 / scale, error_code); } - + template inline Path TranslatePath(const Path& path, T dx, T dy) { @@ -355,6 +356,29 @@ #endif } + inline size_t GetNext(size_t current, size_t high, + const std::vector& flags) + { + ++current; + while (current <= high && flags[current]) ++current; + if (current <= high) return current; + current = 0; + while (flags[current]) ++current; + return current; + } + + inline size_t GetPrior(size_t current, size_t high, + const std::vector& flags) + { + if (current == 0) current = high; + else --current; + while (current > 0 && flags[current]) --current; + if (!flags[current]) return current; + current = high; + while (flags[current]) --current; + return current; + } + } // end details namespace inline std::ostream& operator<< (std::ostream& os, const PolyTree64& pp) @@ -615,29 +639,6 @@ return result; } - inline size_t GetNext(size_t current, size_t high, - const std::vector& flags) - { - ++current; - while (current <= high && flags[current]) ++current; - if (current <= high) return current; - current = 0; - while (flags[current]) ++current; - return current; - } - - inline size_t GetPrior(size_t current, size_t high, - const std::vector& flags) - { - if (current == 0) current = high; - else --current; - while (current > 0 && flags[current]) --current; - if (!flags[current]) return current; - current = high; - while (flags[current]) --current; - return current; - } - template inline Path SimplifyPath(const Path &path, double epsilon, bool isClosedPath = true) @@ -669,13 +670,13 @@ start = curr; do { - curr = GetNext(curr, high, flags); + curr = details::GetNext(curr, high, flags); } while (curr != start && distSqr[curr] > epsSqr); if (curr == start) break; } - prior = GetPrior(curr, high, flags); - next = GetNext(curr, high, flags); + prior = details::GetPrior(curr, high, flags); + next = details::GetNext(curr, high, flags); if (next == prior) break; // flag for removal the smaller of adjacent 'distances' @@ -684,14 +685,14 @@ prior2 = prior; prior = curr; curr = next; - next = GetNext(next, high, flags); + next = details::GetNext(next, high, flags); } else - prior2 = GetPrior(prior, high, flags); + prior2 = details::GetPrior(prior, high, flags); flags[curr] = true; curr = next; - next = GetNext(next, high, flags); + next = details::GetNext(next, high, flags); if (isClosedPath || ((curr != high) && (curr != 0))) distSqr[curr] = PerpendicDistFromLineSqrd(path[curr], path[prior], path[next]); @@ -716,6 +717,35 @@ return result; } + + template + inline bool Path2ContainsPath1(const Path& path1, const Path& path2) + { + // precondition: paths must not intersect, except for + // transient (and presumed 'micro') path intersections + PointInPolygonResult pip = PointInPolygonResult::IsOn; + for (const Point& pt : path1) + { + switch (PointInPolygon(pt, path2)) + { + case PointInPolygonResult::IsOutside: + if (pip == PointInPolygonResult::IsOutside) return false; + pip = PointInPolygonResult::IsOutside; + break; + case PointInPolygonResult::IsInside: + if (pip == PointInPolygonResult::IsInside) return true; + pip = PointInPolygonResult::IsInside; + break; + default: + break; + } + } + if (pip != PointInPolygonResult::IsInside) return false; + // result is likely true but check midpoint + Point mp1 = GetBounds(path1).MidPoint(); + return PointInPolygon(mp1, path2) == PointInPolygonResult::IsInside; + } + template inline void RDP(const Path path, std::size_t begin, std::size_t end, double epsSqrd, std::vector& flags) diff --git a/deps_src/clipper2/Clipper2Lib/include/clipper2/clipper.offset.h b/deps_src/clipper2/Clipper2Lib/include/clipper2/clipper.offset.h index a7d53df36f..fd553609c4 100644 --- a/deps_src/clipper2/Clipper2Lib/include/clipper2/clipper.offset.h +++ b/deps_src/clipper2/Clipper2Lib/include/clipper2/clipper.offset.h @@ -39,7 +39,7 @@ private: class Group { public: Paths64 paths_in; - std::optional lowest_path_idx{}; + std::optional lowest_path_idx{}; bool is_reversed = false; JoinType join_type; EndType end_type; @@ -100,7 +100,7 @@ public: void AddPath(const Path64& path, JoinType jt_, EndType et_); void AddPaths(const Paths64& paths, JoinType jt_, EndType et_); void Clear() { groups_.clear(); norms.clear(); }; - + void Execute(double delta, Paths64& sols_64); void Execute(double delta, PolyTree64& polytree); void Execute(DeltaCallback64 delta_cb, Paths64& paths); @@ -114,7 +114,7 @@ public: bool PreserveCollinear() const { return preserve_collinear_; } void PreserveCollinear(bool preserve_collinear){preserve_collinear_ = preserve_collinear;} - + bool ReverseSolution() const { return reverse_solution_; } void ReverseSolution(bool reverse_solution) {reverse_solution_ = reverse_solution;} diff --git a/deps_src/clipper2/Clipper2Lib/include/clipper2/clipper.triangulation.h b/deps_src/clipper2/Clipper2Lib/include/clipper2/clipper.triangulation.h new file mode 100644 index 0000000000..a33cb9fe6a --- /dev/null +++ b/deps_src/clipper2/Clipper2Lib/include/clipper2/clipper.triangulation.h @@ -0,0 +1,30 @@ +/******************************************************************************* +* Author : Angus Johnson * +* Date : 6 December 2025 * +* Release : BETA RELEASE * +* Website : https://www.angusj.com * +* Copyright : Angus Johnson 2010-2025 * +* Purpose : Delaunay Triangulation * +* License : https://www.boost.org/LICENSE_1_0.txt * +*******************************************************************************/ + +#ifndef CLIPPER_TRIANGULATION_H +#define CLIPPER_TRIANGULATION_H + +#include +#include "clipper2/clipper.core.h" + +#ifdef USINGZ +namespace Clipper2Lib_Z { +#else +namespace Clipper2Lib { +#endif + + enum class TriangulateResult { success, fail, no_polygons, paths_intersect }; + + // Triangulate - this function will not accept intesecting paths + TriangulateResult Triangulate(const Paths64& pp, Paths64& solution, bool useDelaunay = true); + TriangulateResult Triangulate(const PathsD& pp, int decPlaces, PathsD& solution, bool useDelaunay = true); + +} // Clipper2Lib namespace +#endif // CLIPPER_TRIANGULATION_H diff --git a/deps_src/clipper2/Clipper2Lib/include/clipper2/clipper.version.h b/deps_src/clipper2/Clipper2Lib/include/clipper2/clipper.version.h index 661b0f1c58..9aec09a2a8 100644 --- a/deps_src/clipper2/Clipper2Lib/include/clipper2/clipper.version.h +++ b/deps_src/clipper2/Clipper2Lib/include/clipper2/clipper.version.h @@ -1,6 +1,6 @@ #ifndef CLIPPER_VERSION_H #define CLIPPER_VERSION_H -constexpr auto CLIPPER2_VERSION = "1.5.2"; +constexpr auto CLIPPER2_VERSION = "2.0.1"; #endif // CLIPPER_VERSION_H diff --git a/deps_src/clipper2/Clipper2Lib/src/clipper.engine.cpp b/deps_src/clipper2/Clipper2Lib/src/clipper.engine.cpp index fd77bda7bb..2948565ff4 100644 --- a/deps_src/clipper2/Clipper2Lib/src/clipper.engine.cpp +++ b/deps_src/clipper2/Clipper2Lib/src/clipper.engine.cpp @@ -1,8 +1,8 @@ /******************************************************************************* * Author : Angus Johnson * -* Date : 17 September 2024 * +* Date : 5 November 2025 * * Website : https://www.angusj.com * -* Copyright : Angus Johnson 2010-2024 * +* Copyright : Angus Johnson 2010-2025 * * Purpose : This is the main polygon clipping module * * License : https://www.boost.org/LICENSE_1_0.txt * *******************************************************************************/ @@ -10,6 +10,8 @@ #include "clipper2/clipper.engine.h" #include "clipper2/clipper.h" #include +#include +#include // https://github.com/AngusJohnson/Clipper2/discussions/334 // #discussioncomment-4248602 @@ -27,10 +29,30 @@ namespace Clipper2Lib_Z { namespace Clipper2Lib { #endif + // Orca: tbbmalloc scales far better than the default heap when all slicing threads clip at once. + static void* NodeAlloc(size_t size) + { + if (void* p = scalable_malloc(size)) return p; + throw std::bad_alloc(); + } + +#define CLIPPER2_DEFINE_NODE_ALLOCATOR(T) \ + void* T::operator new(size_t size) { return NodeAlloc(size); } \ + void T::operator delete(void* ptr) noexcept { scalable_free(ptr); } \ + void* T::operator new[](size_t size) { return NodeAlloc(size); } \ + void T::operator delete[](void* ptr) noexcept { scalable_free(ptr); } + CLIPPER2_DEFINE_NODE_ALLOCATOR(Vertex) + CLIPPER2_DEFINE_NODE_ALLOCATOR(OutPt) + CLIPPER2_DEFINE_NODE_ALLOCATOR(OutRec) + CLIPPER2_DEFINE_NODE_ALLOCATOR(Active) + CLIPPER2_DEFINE_NODE_ALLOCATOR(LocalMinima) + CLIPPER2_DEFINE_NODE_ALLOCATOR(PolyPath) +#undef CLIPPER2_DEFINE_NODE_ALLOCATOR + static const Rect64 invalid_rect = Rect64(false); - // Every closed path (ie polygon) is made up of a series of vertices forming edge - // 'bounds' that alternate between ascending bounds (containing edges going up + // Every closed path (ie polygon) is made up of a series of vertices forming edge + // 'bounds' that alternate between ascending bounds (containing edges going up // relative to the Y-axis) and descending bounds. 'Local Minima' refers to // vertices where ascending and descending bounds join at the bottom, and // 'Local Maxima' are where ascending and descending bounds join at the top. @@ -482,8 +504,7 @@ namespace Clipper2Lib { inline void SetOwner(OutRec* outrec, OutRec* new_owner) { //precondition1: new_owner is never null - while (new_owner->owner && !new_owner->owner->pts) - new_owner->owner = new_owner->owner->owner; + new_owner->owner = GetRealOutRec(new_owner->owner); OutRec* tmp = new_owner; while (tmp && tmp != outrec) tmp = tmp->owner; if (tmp) new_owner->owner = outrec->owner; @@ -536,9 +557,9 @@ namespace Clipper2Lib { 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; + int i = CrossProductSign(op2->prev->pt, op2->pt, pt); + if (i == 0) return PointInPolygonResult::IsOn; + if ((i < 0) == is_above) val = 1 - val; } is_above = !is_above; op2 = op2->next; @@ -546,9 +567,9 @@ namespace Clipper2Lib { 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; + int i = CrossProductSign(op2->prev->pt, op2->pt, pt); + if (i == 0) return PointInPolygonResult::IsOn; + if ((i < 0) == is_above) val = 1 - val; } if (val == 0) return PointInPolygonResult::IsOutside; @@ -578,30 +599,31 @@ namespace Clipper2Lib { return result; } - inline bool Path1InsidePath2(OutPt* op1, OutPt* op2) + inline bool Path2ContainsPath1(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; + // this function accommodates rounding errors that + // can cause path micro intersections + PointInPolygonResult pip = PointInPolygonResult::IsOn; OutPt* op = op1; - do - { - result = PointInOpPolygon(op->pt, op2); - if (result == PointInPolygonResult::IsOutside) ++outside_cnt; - else if (result == PointInPolygonResult::IsInside) --outside_cnt; + do { + switch (PointInOpPolygon(op->pt, op2)) + { + case PointInPolygonResult::IsOutside: + if (pip == PointInPolygonResult::IsOutside) return false; + pip = PointInPolygonResult::IsOutside; + break; + case PointInPolygonResult::IsInside: + if (pip == PointInPolygonResult::IsInside) return true; + pip = PointInPolygonResult::IsInside; + break; + default: break; + } 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; + } while (op != op1); + // result unclear, so try again using cleaned paths + return Path2ContainsPath1(GetCleanPath(op1), GetCleanPath(op2)); // (#973) } - //------------------------------------------------------------------------------ - //------------------------------------------------------------------------------ - void AddLocMin(LocalMinimaList& list, Vertex& vert, PathType polytype, bool is_open) { @@ -1126,21 +1148,19 @@ namespace Clipper2Lib { return newcomer.curr_x > resident.curr_x; //get the turning direction a1.top, a2.bot, a2.top - double d = CrossProduct(resident.top, newcomer.bot, newcomer.top); - if (d != 0) return d < 0; + int i = CrossProductSign(resident.top, newcomer.bot, newcomer.top); + if (i != 0) return i < 0; //edges must be collinear to get here //for starting open paths, place them according to //the direction they're about to turn if (!IsMaxima(resident) && (resident.top.y > newcomer.top.y)) { - return CrossProduct(newcomer.bot, - resident.top, NextVertex(resident)->pt) <= 0; + return (CrossProductSign(newcomer.bot, resident.top, NextVertex(resident)->pt) <= 0); } else if (!IsMaxima(newcomer) && (newcomer.top.y > resident.top.y)) { - return CrossProduct(newcomer.bot, - newcomer.top, NextVertex(newcomer)->pt) >= 0; + return (CrossProductSign(newcomer.bot, newcomer.top, NextVertex(newcomer)->pt) >= 0); } int64_t y = newcomer.bot.y; @@ -1155,7 +1175,7 @@ namespace Clipper2Lib { resident.bot, resident.top)) return true; else //compare turning direction of the alternate bound - return (CrossProduct(PrevPrevVertex(resident)->pt, + return (CrossProductSign(PrevPrevVertex(resident)->pt, newcomer.bot, PrevPrevVertex(newcomer)->pt) > 0) == newcomerIsLeft; } @@ -1565,7 +1585,7 @@ namespace Clipper2Lib { FixSelfIntersects(outrec); } - void ClipperBase::DoSplitOp(OutRec* outrec, OutPt* splitOp) + void ClipperBase::DoSplitOp (OutRec* outrec, OutPt* splitOp) { // splitOp.prev -> splitOp && // splitOp.next -> splitOp.next.next are intersecting @@ -1574,7 +1594,7 @@ namespace Clipper2Lib { outrec->pts = prevOp; Point64 ip; - GetSegmentIntersectPt(prevOp->pt, splitOp->pt, + GetLineIntersectPt(prevOp->pt, splitOp->pt, splitOp->next->pt, nextNextOp->pt, ip); #ifdef USINGZ @@ -1630,7 +1650,7 @@ namespace Clipper2Lib { if (using_polytree_) { - if (Path1InsidePath2(prevOp, newOp)) + if (Path2ContainsPath1(prevOp, newOp)) { newOr->splits = new OutRecList(); newOr->splits->emplace_back(outrec); @@ -1652,19 +1672,32 @@ namespace Clipper2Lib { void ClipperBase::FixSelfIntersects(OutRec* outrec) { OutPt* op2 = outrec->pts; + if (op2->prev == op2->next->next) + return; // because triangles can't self-intersect for (; ; ) { - // triangles can't self-intersect - if (op2->prev == op2->next->next) break; if (SegmentsIntersect(op2->prev->pt, op2->pt, op2->next->pt, op2->next->next->pt)) { - if (op2 == outrec->pts || op2->next == outrec->pts) - outrec->pts = outrec->pts->prev; - DoSplitOp(outrec, op2); - if (!outrec->pts) break; - op2 = outrec->pts; - continue; + if (SegmentsIntersect(op2->prev->pt, + op2->pt, op2->next->next->pt, op2->next->next->next->pt)) + { + // adjacent intersections (ie a micro self-intersections) + op2 = DuplicateOp(op2, false); + op2->pt = op2->next->next->next->pt; + op2 = op2->next; + } + else + { + if (op2 == outrec->pts || op2->next == outrec->pts) + outrec->pts = outrec->pts->prev; + DoSplitOp(outrec, op2); + if (!outrec->pts) break; + op2 = outrec->pts; + if (op2->prev == op2->next->next) + break; // again, because triangles can't self-intersect + continue; + } } else op2 = op2->next; @@ -1805,14 +1838,14 @@ namespace Clipper2Lib { switch (fillrule_) { - case FillRule::Positive: - if (edge_c->wind_cnt != 1) return; + case FillRule::Positive: + if (edge_c->wind_cnt != 1) return; break; - case FillRule::Negative: - if (edge_c->wind_cnt != -1) return; + case FillRule::Negative: + if (edge_c->wind_cnt != -1) return; break; - default: - if (std::abs(edge_c->wind_cnt) != 1) return; + default: + if (std::abs(edge_c->wind_cnt) != 1) return; } #ifdef USINGZ @@ -1933,7 +1966,7 @@ namespace Clipper2Lib { const bool e1_windcnt_in_01 = old_e1_windcnt == 0 || old_e1_windcnt == 1; const bool e2_windcnt_in_01 = old_e2_windcnt == 0 || old_e2_windcnt == 1; - if ((!IsHotEdge(e1) && !e1_windcnt_in_01) || + if ((!IsHotEdge(e1) && !e1_windcnt_in_01) || (!IsHotEdge(e2) && !e2_windcnt_in_01)) return; @@ -2112,10 +2145,9 @@ namespace Clipper2Lib { e->prev_in_sel = e->prev_in_ael; e->next_in_sel = e->next_in_ael; e->jump = e->next_in_sel; - if (e->join_with == JoinWith::Left) - e->curr_x = e->prev_in_ael->curr_x; // also avoids complications - else - e->curr_x = TopX(*e, top_y); + // it is safe to ignore 'joined' edges here because + // if necessary they will be split in IntersectEdges() + e->curr_x = TopX(*e, top_y); e = e->next_in_ael; } } @@ -2262,15 +2294,14 @@ 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->emplace_back(*orIter); + if (toOr != *orIter) // #987 + toOr->splits->emplace_back(*orIter); fromOr->splits->clear(); } - void ClipperBase::ProcessHorzJoins() { for (const HorzJoin& j : horz_join_list_) @@ -2299,8 +2330,8 @@ namespace Clipper2Lib { } if (using_polytree_) //#498, #520, #584, D#576, #618 - { - if (Path1InsidePath2(or1->pts, or2->pts)) + { + if (Path2ContainsPath1(or1->pts, or2->pts)) { //swap or1's & or2's pts OutPt* tmp = or1->pts; @@ -2311,7 +2342,7 @@ namespace Clipper2Lib { //or2 is now inside or1 or2->owner = or1; } - else if (Path1InsidePath2(or2->pts, or1->pts)) + else if (Path2ContainsPath1(or2->pts, or1->pts)) { or2->owner = or1; } @@ -2324,13 +2355,14 @@ namespace Clipper2Lib { else or2->owner = or1; } - else + else // joining, not splitting { or2->pts = nullptr; if (using_polytree_) { SetOwner(or2, or1); - MoveSplits(or2, or1); //#618 + if (or2->splits) + MoveSplits(or2, or1); //#618 } else or2->owner = or1; @@ -2350,7 +2382,7 @@ namespace Clipper2Lib { void ClipperBase::AddNewIntersectNode(Active& e1, Active& e2, int64_t top_y) { Point64 ip; - if (!GetSegmentIntersectPt(e1.bot, e1.top, e2.bot, e2.top, ip)) + if (!GetLineIntersectPt(e1.bot, e1.top, e2.bot, e2.top, ip)) ip = Point64(e1.curr_x, top_y); //parallel edges //rounding errors can occasionally place the calculated intersection @@ -2934,22 +2966,28 @@ namespace Clipper2Lib { bool ClipperBase::CheckSplitOwner(OutRec* outrec, OutRecList* splits) { - for (auto split : *splits) + // nb: use indexing (not an iterator) in case 'splits' is modified inside this loop (#1029) + for (size_t idx = 0; idx < splits->size(); ++idx) { + OutRec* split = (*splits)[idx]; + if (!split->pts && split->splits && + CheckSplitOwner(outrec, split->splits)) return true; //#942 split = GetRealOutRec(split); - if(!split || split == outrec || split->recursive_split == outrec) continue; + 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 true; + + if (!CheckBounds(split) || !split->bounds.Contains(outrec->bounds) || + !Path2ContainsPath1(outrec->pts, split->pts)) continue; + + if (!IsValidOwner(outrec, split)) // split is owned by outrec! (#957) + split->owner = outrec->owner; + + outrec->owner = split; + return true; + } return false; } @@ -2960,13 +2998,12 @@ namespace Clipper2Lib { // post-condition: if a valid path, outrec will have a polypath if (outrec->polypath || outrec->bounds.IsEmpty()) return; - while (outrec->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; + Path2ContainsPath1(outrec->pts, outrec->owner->pts)) break; outrec->owner = outrec->owner->owner; } @@ -3029,6 +3066,7 @@ namespace Clipper2Lib { { OutRec* outrec = outrec_list_[i]; if (!outrec || !outrec->pts) continue; + if (outrec->is_open) { Path64 path; diff --git a/deps_src/clipper2/Clipper2Lib/src/clipper.offset.cpp b/deps_src/clipper2/Clipper2Lib/src/clipper.offset.cpp index c00a345c01..60b514356a 100644 --- a/deps_src/clipper2/Clipper2Lib/src/clipper.offset.cpp +++ b/deps_src/clipper2/Clipper2Lib/src/clipper.offset.cpp @@ -1,6 +1,6 @@ /******************************************************************************* * Author : Angus Johnson * -* Date : 22 January 2025 * +* Date : 11 October 2025 * * Website : https://www.angusj.com * * Copyright : Angus Johnson 2010-2025 * * Purpose : Path Offset (Inflate/Shrink) * @@ -37,29 +37,35 @@ const double arc_const = 0.002; // <-- 1/500 // Miscellaneous methods //------------------------------------------------------------------------------ -std::optional GetLowestClosedPathIdx(const Paths64& paths) +void GetLowestClosedPathInfo(const Paths64& paths, std::optional& idx, bool& is_neg_area) { - std::optional result; + idx.reset(); Point64 botPt = Point64(INT64_MAX, INT64_MIN); for (size_t i = 0; i < paths.size(); ++i) { + double a = MAX_DBL; for (const Point64& pt : paths[i]) { if ((pt.y < botPt.y) || ((pt.y == botPt.y) && (pt.x >= botPt.x))) continue; - result = i; + if (a == MAX_DBL) + { + a = Area(paths[i]); + if (a == 0) break; // invalid closed path, so break from inner loop + is_neg_area = a < 0; + } + idx = i; botPt.x = pt.x; botPt.y = pt.y; } } - return result; } inline double Hypot(double x, double y) { // given that this is an internal function, and given the x and y parameters // will always be coordinate values (or the difference between coordinate values), - // x and y should always be within INT64_MIN to INT64_MAX. Consequently, + // x and y should always be within INT64_MIN to INT64_MAX. Consequently, // there should be no risk that the following computation will overflow // see https://stackoverflow.com/a/32436148/359538 return std::sqrt(x * x + y * y); @@ -145,15 +151,16 @@ ClipperOffset::Group::Group(const Paths64& _paths, JoinType _join_type, EndType if (end_type == EndType::Polygon) { - lowest_path_idx = GetLowestClosedPathIdx(paths_in); + bool is_neg_area; + GetLowestClosedPathInfo(paths_in, lowest_path_idx, is_neg_area); // 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.has_value()) && Area(paths_in[lowest_path_idx.value()]) < 0; + is_reversed = lowest_path_idx.has_value() && is_neg_area; } else { - lowest_path_idx = std::nullopt; + lowest_path_idx.reset(); is_reversed = false; } } @@ -236,7 +243,7 @@ void ClipperOffset::DoSquare(const Path64& path, size_t j, size_t k) { PointD pt4 = PointD(pt3.x + vec.x * group_delta_, pt3.y + vec.y * group_delta_); PointD pt = ptQ; - GetSegmentIntersectPt(pt1, pt2, pt3, pt4, pt); + GetLineIntersectPt(pt1, pt2, pt3, pt4, pt); //get the second intersect point through reflecion path_out.emplace_back(ReflectPoint(pt, ptQ)); path_out.emplace_back(pt); @@ -245,7 +252,7 @@ void ClipperOffset::DoSquare(const Path64& path, size_t j, size_t k) { PointD pt4 = GetPerpendicD(path[j], norms[k], group_delta_); PointD pt = ptQ; - GetSegmentIntersectPt(pt1, pt2, pt3, pt4, pt); + GetLineIntersectPt(pt1, pt2, pt3, pt4, pt); path_out.emplace_back(pt); //get the second intersect point through reflecion path_out.emplace_back(ReflectPoint(pt, ptQ)); @@ -291,7 +298,8 @@ void ClipperOffset::DoRound(const Path64& path, size_t j, size_t k, double angle #else path_out.emplace_back(pt.x + offsetVec.x, pt.y + offsetVec.y); #endif - int steps = static_cast(std::ceil(steps_per_rad_ * std::abs(angle))); // #448, #456 + // Orca: round the step count like Clipper1 did, so round offsets keep their vertices. + int steps = std::max(static_cast(std::round(steps_per_rad_ * std::abs(angle))), 1); for (int i = 1; i < steps; ++i) // ie 1 less than steps { offsetVec = PointD(offsetVec.x * step_cos_ - step_sin_ * offsetVec.y, @@ -333,9 +341,9 @@ void ClipperOffset::OffsetPoint(Group& group, const Path64& path, size_t j, size if (cos_a > -0.999 && (sin_a * group_delta_ < 0)) // test for concavity first (#593) { // is concave - // by far the simplest way to construct concave joins, especially those joining very - // short segments, is to insert 3 points that produce negative regions. These regions - // will be removed later by the finishing union operation. This is also the best way + // by far the simplest way to construct concave joins, especially those joining very + // short segments, is to insert 3 points that produce negative regions. These regions + // will be removed later by the finishing union operation. This is also the best way // to ensure that path reversals (ie over-shrunk paths) are removed. #ifdef USINGZ path_out.emplace_back(GetPerpendic(path[j], norms[k], group_delta_), path[j].z); @@ -366,11 +374,31 @@ void ClipperOffset::OffsetPoint(Group& group, const Path64& path, size_t j, size DoSquare(path, j, k); } +// Orca: join concave corners at the crossing of both edge offsets where safe, 3-point loops make dense inward offsets slow. +static bool OffsetConcaveCrossing(const Path64& path, const PathD& norms, size_t j, size_t k, size_t next, + double delta, Path64& path_out) +{ + const double sin_a = CrossProduct(norms[j], norms[k]); + const double cos_a = DotProduct(norms[j], norms[k]); + if (cos_a <= -0.999 || sin_a * delta >= 0) return false; + const double x = std::fabs(delta * sin_a) / (1 + cos_a); + if (4 * x * x > DistanceSqr(path[k], path[j]) || 4 * x * x > DistanceSqr(path[j], path[next])) return false; + const double q = delta / (1 + cos_a); +#ifdef USINGZ + path_out.emplace_back(path[j].x + (norms[k].x + norms[j].x) * q, path[j].y + (norms[k].y + norms[j].y) * q, path[j].z); +#else + path_out.emplace_back(path[j].x + (norms[k].x + norms[j].x) * q, path[j].y + (norms[k].y + norms[j].y) * q); +#endif + return true; +} + void ClipperOffset::OffsetPolygon(Group& group, const Path64& path) { path_out.clear(); for (Path64::size_type j = 0, k = path.size() - 1; j < path.size(); k = j, ++j) - OffsetPoint(group, path, j, k); + if (deltaCallback64_ || path[j] == path[k] || + !OffsetConcaveCrossing(path, norms, j, k, j + 1 == path.size() ? 0 : j + 1, group_delta_, path_out)) + OffsetPoint(group, path, j, k); solution->emplace_back(path_out); } @@ -380,7 +408,7 @@ void ClipperOffset::OffsetOpenJoined(Group& group, const Path64& path) Path64 reverse_path(path); std::reverse(reverse_path.begin(), reverse_path.end()); - //rebuild normals + //rebuild normals std::reverse(norms.begin(), norms.end()); norms.emplace_back(norms[0]); norms.erase(norms.begin()); @@ -601,10 +629,10 @@ void ClipperOffset::ExecuteInternal(double delta) if (!solution->size()) return; - bool paths_reversed = CheckReverseOrientation(); + bool paths_reversed = CheckReverseOrientation(); //clean up self-intersections ... Clipper64 c; - c.PreserveCollinear(false); + c.PreserveCollinear(preserve_collinear_); //the solution should retain the orientation of the input c.ReverseSolution(reverse_solution_ != paths_reversed); #ifdef USINGZ diff --git a/deps_src/clipper2/Clipper2Lib/src/clipper.rectclip.cpp b/deps_src/clipper2/Clipper2Lib/src/clipper.rectclip.cpp index a4d83ad137..216ccbab19 100644 --- a/deps_src/clipper2/Clipper2Lib/src/clipper.rectclip.cpp +++ b/deps_src/clipper2/Clipper2Lib/src/clipper.rectclip.cpp @@ -1,8 +1,8 @@ /******************************************************************************* * Author : Angus Johnson * -* Date : 5 July 2024 * +* Date : 11 October 2025 * * Website : https://www.angusj.com * -* Copyright : Angus Johnson 2010-2024 * +* Copyright : Angus Johnson 2010-2025 * * Purpose : FAST rectangular clipping * * License : https://www.boost.org/LICENSE_1_0.txt * *******************************************************************************/ @@ -77,8 +77,8 @@ namespace Clipper2Lib { 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); + int res1 = CrossProductSign(p1, p3, p4); + int res2 = CrossProductSign(p2, p3, p4); if (res1 == 0) { ip = p1; @@ -97,8 +97,8 @@ namespace Clipper2Lib { } if ((res1 > 0) == (res2 > 0)) return false; - double res3 = CrossProduct(p3, p1, p2); - double res4 = CrossProduct(p4, p1, p2); + int res3 = CrossProductSign(p3, p1, p2); + int res4 = CrossProductSign(p4, p1, p2); if (res3 == 0) { ip = p3; @@ -116,7 +116,7 @@ namespace Clipper2Lib { if ((res3 > 0) == (res4 > 0)) return false; // segments must intersect to get here - return GetSegmentIntersectPt(p1, p2, p3, p4, ip); + return GetLineIntersectPt(p1, p2, p3, p4, ip); } inline bool GetIntersection(const Path64& rectPath, @@ -227,7 +227,7 @@ namespace Clipper2Lib { const Point64& prev_pt, const Point64& curr_pt, const Point64& rect_mp) { if (AreOpposites(prev, curr)) - return CrossProduct(prev_pt, rect_mp, curr_pt) < 0; + return CrossProductSign(prev_pt, rect_mp, curr_pt) < 0; else return HeadingClockwise(prev, curr); } diff --git a/deps_src/clipper2/Clipper2Lib/src/clipper.triangulation.cpp b/deps_src/clipper2/Clipper2Lib/src/clipper.triangulation.cpp new file mode 100644 index 0000000000..25e3acd158 --- /dev/null +++ b/deps_src/clipper2/Clipper2Lib/src/clipper.triangulation.cpp @@ -0,0 +1,1222 @@ +/******************************************************************************* +* Author : Angus Johnson * +* Date : 13 December 2025 * +* Release : BETA RELEASE * +* Website : https://www.angusj.com * +* Copyright : Angus Johnson 2010-2025 * +* Purpose : Constrained Delaunay Triangulation * +* License : https://www.boost.org/LICENSE_1_0.txt * +*******************************************************************************/ + +#include "clipper2/clipper.h" +#include "clipper2/clipper.triangulation.h" + +#ifdef USINGZ +namespace Clipper2Lib_Z { +#else +namespace Clipper2Lib { +#endif + + enum class EdgeKind { loose, ascend, descend }; // ascend & descend are 'fixed' edges + enum class IntersectKind { none, collinear, intersect }; + enum class EdgeContainsResult { neither, left, right }; + + //forward definitions + class Vertex2; + class Edge; + class Triangle; + + typedef std::vector VertexList; + typedef std::vector EdgeList; + + class Vertex2 { + public: + Point64 pt; + EdgeList edges; + bool innerLM = false; + Vertex2(const Point64& p64) : pt(p64) { edges.reserve(2); }; + }; + + class Edge { + public: + Vertex2* vL = nullptr; + Vertex2* vR = nullptr; + Vertex2* vB = nullptr; + Vertex2* vT = nullptr; + EdgeKind kind = EdgeKind::loose; + Triangle* triA = nullptr; + Triangle* triB = nullptr; + bool isActive = false; + Edge* nextE = nullptr; + Edge* prevE = nullptr; + }; + + class Triangle { + public: + Edge* edges[3]; + Triangle(Edge* e1, Edge* e2, Edge* e3) + { + edges[0] = e1; + edges[1] = e2; + edges[2] = e3; + } + }; + + ///////////////////////////////////////////////////////////////////////////// + // Delaunay class declaration + ///////////////////////////////////////////////////////////////////////////// + + class Delaunay { + private: + VertexList allVertices; + EdgeList allEdges; + std::vector allTriangles; + std::stack pendingDelaunayStack; + std::stack horzEdgeStack; + std::stack locMinStack; + bool useDelaunay = true; + Vertex2* lowermostVertex = nullptr; + Edge* firstActive = nullptr; + void AddPath(const Path64& path); + bool AddPaths(const Paths64& paths); + void CleanUp(); + bool FixupEdgeIntersects(); + void MergeDupOrCollinearVertices(); + void SplitEdge(Edge* longE, Edge* shortE); + bool RemoveIntersection(Edge* e1, Edge* e2); + Edge* CreateEdge(Vertex2* v1, Vertex2* v2, EdgeKind k); + Triangle* CreateTriangle(Edge* e1, Edge* e2, Edge* e3); + Edge* CreateInnerLocMinLooseEdge(Vertex2* vAbove); + Edge* HorizontalBetween(Vertex2* v1, Vertex2* v2); + void DoTriangulateLeft(Edge* edge, Vertex2* pivot, int64_t minY); + void DoTriangulateRight(Edge* edge, Vertex2* pivot, int64_t minY); + void AddEdgeToActives(Edge* edge); + void RemoveEdgeFromActives(Edge* edge); + void ForceLegal(Edge* edge); + public: + explicit Delaunay(bool delaunay = true) : useDelaunay(delaunay) {}; + ~Delaunay() { CleanUp(); }; + Paths64 Execute(const Paths64& paths, TriangulateResult& triResult); + }; + + ///////////////////////////////////////////////////////////////////////////// + // Miscellaneous functions + ///////////////////////////////////////////////////////////////////////////// + + static bool VertexListSort(const Vertex2* a, const Vertex2* b) + { + return (a->pt.y == b->pt.y) ? (a->pt.x < b->pt.x) : (a->pt.y > b->pt.y); + } + + static bool EdgeListSort(const Edge* a, const Edge* b) + { + return (a->vL->pt.x < b->vL->pt.x); + } + + static bool IsLooseEdge(const Edge& e) + { + return e.kind == EdgeKind::loose; + } + + static bool IsLeftEdge(const Edge& e) + { + // left edge (bound) of a fill region + // ie fills on the **right** side of the edge + // precondition - e is never a 'loose' edge + return e.kind == EdgeKind::ascend; + } + + static bool IsRightEdge(const Edge& e) + { + // right edge (bound) of a fill region + // but still fills on the **right** side of the edge + // precondition - e is never a 'loose' edge + return e.kind == EdgeKind::descend; + } + + static bool IsHorizontal(const Edge& e) + { + return e.vB->pt.y == e.vT->pt.y; + } + + static bool LeftTurning(const Point64& p1, const Point64& p2, const Point64& p3) + { + return CrossProductSign(p1, p2, p3) < 0; + } + + static bool RightTurning(const Point64& p1, const Point64& p2, const Point64& p3) + { + return CrossProductSign(p1, p2, p3) > 0; + } + + static bool EdgeCompleted(Edge* edge) + { + if (!edge->triA) return false; + if (edge->triB) return true; + return edge->kind != EdgeKind::loose; + } + + static EdgeContainsResult EdgeContains(const Edge* edge, const Vertex2* v) + { + if (edge->vL == v) return EdgeContainsResult::left; + else if (edge->vR == v) return EdgeContainsResult::right; + else return EdgeContainsResult::neither; + } + + static double GetAngle(const Point64& a, const Point64& b, const Point64& c) + { + //https://stackoverflow.com/a/3487062/359538 + double abx = static_cast(b.x - a.x); + double aby = static_cast(b.y - a.y); + double bcx = static_cast(b.x - c.x); + double bcy = static_cast(b.y - c.y); + double dp = (abx * bcx + aby * bcy); + double cp = (abx * bcy - aby * bcx); + return std::atan2(cp, dp); //range between -Pi and Pi + } + + static double GetLocMinAngle(Vertex2* v) + { + // todo - recheck the result's sign compared to left vs right turning + // (currently assumes left turning => positive values) + // precondition - this function is called before processing locMin. + //Assert(v->edges.size() == 2); + int asc, des; + if (v->edges[0]->kind == EdgeKind::ascend) + { + asc = 0; + des = 1; + } + else + { + des = 0; + asc = 1; + } + // winding direction - descending to ascending + return GetAngle(v->edges[des]->vT->pt, v->pt, v->edges[asc]->vT->pt); + } + + static void RemoveEdgeFromVertex(Vertex2* vert, Edge* edge) + { + auto it = std::find(vert->edges.begin(), vert->edges.end(), edge); + if (it == vert->edges.end()) throw "oops!"; + vert->edges.erase(it); + } + + static bool FindLocMinIdx(const Path64& path, size_t len, size_t& idx) + { + if (len < 3) return false; + size_t i0 = idx, n = (idx + 1) % len; + while (path[n].y <= path[idx].y) + { + idx = n; n = (n + 1) % len; + if (idx == i0) return false; // fails if the path is completely horizontal + } + while (path[n].y >= path[idx].y) + { + idx = n; n = (n + 1) % len; + } + return true; + } + + static size_t Prev(size_t& idx, size_t len) + { + if (idx == 0) return len - 1; else return idx - 1; + } + + static size_t Next(size_t& idx, size_t len) + { + return (idx + 1) % len; + } + + static Edge* FindLinkingEdge(const Vertex2* vert1, const Vertex2* vert2, bool preferAscending) + { + Edge* res = nullptr; + for (auto e : vert1->edges) + { + if (e->vL == vert2 || e->vR == vert2) + { + if (e->kind == EdgeKind::loose || + ((e->kind == EdgeKind::ascend) == preferAscending)) return e; + res = e; + } + } + return res; + } + + static Path64 PathFromTriangle(Triangle tri) + { + Path64 res; + res.reserve(3); + res.push_back(tri.edges[0]->vL->pt); + res.push_back(tri.edges[0]->vR->pt); + const Edge& e = *tri.edges[1]; + if (e.vL->pt == res[0] || e.vL->pt == res[1]) + res.push_back(e.vR->pt); + else + res.push_back(e.vL->pt); + return res; + } + + static double InCircleTest(const Point64& ptA, const Point64& ptB, + const Point64& ptC, const Point64& ptD) + { + // Return the determinant value of 3 x 3 matrix ... + // | ax-dx ay-dy Sqr(ax-dx)+Sqr(ay-dy) | + // | bx-dx by-dy Sqr(bx-dx)+Sqr(by-dy) | + // | cx-dx cy-dy Sqr(cx-dx)+Sqr(cy-dy) | + + // The *sign* of the return value is determined by + // the orientation (CW vs CCW) of ptA, ptB & ptC. + + // precondition - ptA, ptB & ptC make a *non-empty* triangle + double m00 = static_cast(ptA.x - ptD.x); + double m01 = static_cast(ptA.y - ptD.y); + double m02 = (Sqr(m00) + Sqr(m01)); + double m10 = static_cast(ptB.x - ptD.x); + double m11 = static_cast(ptB.y - ptD.y); + double m12 = (Sqr(m10) + Sqr(m11)); + double m20 = static_cast(ptC.x - ptD.x); + double m21 = static_cast(ptC.y - ptD.y); + double m22 = (Sqr(m20) + Sqr(m21)); + return m00 * (m11 * m22 - m21 * m12) - + m10 * (m01 * m22 - m21 * m02) + + m20 * (m01 * m12 - m11 * m02); + } + + static double ShortestDistFromSegment(const Point64& pt, const Point64& segPt1, const Point64& segPt2) + { + // precondition: segPt1 <> segPt2 + double dx = static_cast(segPt2.x - segPt1.x); + double dy = static_cast(segPt2.y - segPt1.y); + //Assert((dx < > 0) or (dy < > 0)); // ie segPt1 <> segPt2 + + double ax = static_cast(pt.x - segPt1.x); + double ay = static_cast(pt.y - segPt1.y); + //q = (ax * dx + ay * dy) / (dx * dx + dy * dy); + double qNum = ax * dx + ay * dy; + if (qNum < 0) // pt is closest to seq1 + return DistanceSqr(pt, segPt1); + else if (qNum > (Sqr(dx) + Sqr(dy))) // pt is closest to seq2 + return DistanceSqr(pt, segPt2); + else + return Sqr(ax * dy - dx * ay) / (dx * dx + dy * dy); + } + + static IntersectKind SegsIntersect(const Point64 s1a, const Point64 s1b, + const Point64 s2a, const Point64 s2b) + { + double dy1 = static_cast(s1b.y - s1a.y); + double dx1 = static_cast(s1b.x - s1a.x); + double dy2 = static_cast(s2b.y - s2a.y); + double dx2 = static_cast(s2b.x - s2a.x); + double cp = dy1 * dx2 - dy2 * dx1; + if (cp == 0) return IntersectKind::collinear; + + double t = (static_cast(s1a.x - s2a.x) * dy2 - + static_cast(s1a.y - s2a.y) * dx2); + //ignore segments that 'intersect' at an end-point + if (t == 0) return IntersectKind::none; + if (t > 0) + { + if (cp < 0 || t >= cp) return IntersectKind::none; + } + else + { + if (cp > 0 || t <= cp) return IntersectKind::none; + } + + // so far, the *segment* 's1' intersects the *line* through 's2', + // but now make sure it also intersects the *segment* 's2' + t = ((s1a.x - s2a.x) * dy1 - (s1a.y - s2a.y) * dx1); + if (t == 0) return IntersectKind::none; + if (t > 0) + { + if (cp > 0 && t < cp) return IntersectKind::intersect; + } + else + { + if (cp < 0 && t > cp) return IntersectKind::intersect; + } + return IntersectKind::none; + } + + ///////////////////////////////////////////////////////////////////////////// + // Delaunay class definitions + ///////////////////////////////////////////////////////////////////////////// + + void Delaunay::CleanUp() + { + for (auto v : allVertices) delete v; + allVertices.resize(0); + for (auto e : allEdges) delete e; + allEdges.resize(0); + for (auto t : allTriangles) delete t; + allTriangles.resize(0); + + firstActive = nullptr; + lowermostVertex = nullptr; + } + + void Delaunay::ForceLegal(Edge* edge) + { + // don't try to make empty triangles legal + if (!edge->triA || !edge->triB) return; + + // vertA will be assigned the vertex in edge's triangleA + // that is NOT an end vertex of edge + // Likewise, vertB will be assigned the vertex in edge's + // triangleB that is NOT an end vertex of edge + // If edge is rotated, vertA & vertB will become its end vertices. + Vertex2* vertA = nullptr; + Vertex2* vertB = nullptr; + + // Excluding 'edge', edgesA will contain two edges (one from + // triangleA and one from triangleB) that touch edge.vL. + // And edgesB will contain the two edges that touch edge.vR. + + Edge* edgesA[3] = { nullptr, nullptr, nullptr }; + Edge* edgesB[3] = { nullptr, nullptr, nullptr }; + for (int i = 0; i < 3; ++i) + { + if (edge->triA->edges[i] == edge) continue; + switch (EdgeContains(edge->triA->edges[i], edge->vL)) + { + case EdgeContainsResult::left: + edgesA[1] = edge->triA->edges[i]; + vertA = edge->triA->edges[i]->vR; + break; + case EdgeContainsResult::right: + edgesA[1] = edge->triA->edges[i]; + vertA = edge->triA->edges[i]->vL; + break; + default: + edgesB[1] = edge->triA->edges[i]; + } + } + + for (int i = 0; i < 3; ++i) + { + if (edge->triB->edges[i] == edge) continue; + switch (EdgeContains(edge->triB->edges[i], edge->vL)) + { + case EdgeContainsResult::left: + edgesA[2] = edge->triB->edges[i]; + vertB = edge->triB->edges[i]->vR; + break; + case EdgeContainsResult::right: + edgesA[2] = edge->triB->edges[i]; + vertB = edge->triB->edges[i]->vL; + break; + default: + edgesB[2] = edge->triB->edges[i]; + } + } + + // InCircleTest reqires edge.triangleA to be a valid triangle + // if IsEmptyTriangle(edge.triangleA) then Exit; // slower + if (CrossProductSign(vertA->pt, edge->vL->pt, edge->vR->pt) == 0) return; + + // ictResult - result sign is dependant on triangleA's orientation + double ictResult = InCircleTest(vertA->pt, edge->vL->pt, edge->vR->pt, vertB->pt); + if (ictResult == 0 || // if on or out of circle then exit + (RightTurning(vertA->pt, edge->vL->pt, edge->vR->pt) == (ictResult < 0))) return; + + // TRIANGLES HERE ARE **NOT** DELAUNAY COMPLIANT, SO MAKE THEM SO. + + // NOTE: ONCE WE BEGIN DELAUNAY COMPLIANCE, vL & vR WILL + // NO LONGER REPRESENT LEFT AND RIGHT VERTEX ORIENTATION. + // THIS IS MINOR PERFORMANCE EFFICIENCY IS SAFE AS LONG AS + // THE TRIANGULATE() METHOD IS CALLED ONCE ONLY ON A GIVEN + // SET OF PATHS + + edge->vL = vertA; + edge->vR = vertB; + + edge->triA->edges[0] = edge; + for (int i = 1; i < 3; ++i) + { + edge->triA->edges[i] = edgesA[i]; + if (!edgesA[i]) throw "oops"; // stops compiler warnings + if (IsLooseEdge(*edgesA[i])) + pendingDelaunayStack.push(edgesA[i]); + // since each edge has its own triangleA and triangleB, we have to be careful + // to update the correct one ... + if (edgesA[i]->triA == edge->triA || edgesA[i]->triB == edge->triA) continue; + + if (edgesA[i]->triA == edge->triB) + edgesA[i]->triA = edge->triA; + else if (edgesA[i]->triB == edge->triB) + edgesA[i]->triB = edge->triA; + else throw "oops"; + } + + edge->triB->edges[0] = edge; + for (int i = 1; i < 3; ++i) + { + edge->triB->edges[i] = edgesB[i]; + if (!edgesB[i]) throw "oops"; // stops compiler warnings + if (IsLooseEdge(*edgesB[i])) + pendingDelaunayStack.push(edgesB[i]); + // since each edge has its own triangleA and triangleB, we have to be careful + // to update the correct one ... + if (edgesB[i]->triA == edge->triB || edgesB[i]->triB == edge->triB) continue; + + if (edgesB[i]->triA == edge->triA) + edgesB[i]->triA = edge->triB; + else if (edgesB[i]->triB == edge->triA) + edgesB[i]->triB = edge->triB; + else throw "oops"; + } + + } + + Edge* Delaunay::CreateEdge(Vertex2* v1, Vertex2* v2, EdgeKind k) + { + Edge* res = allEdges.emplace_back(new Edge()); + if (v1->pt.y == v2->pt.y) + { + res->vB = v1; res->vT = v2; + } + else if (v1->pt.y < v2->pt.y) + { + res->vB = v2; res->vT = v1; + } + else + { + res->vB = v1; res->vT = v2; + } + + if (v1->pt.x <= v2->pt.x) + { + res->vL = v1; res->vR = v2; + } + else + { + res->vL = v2; res->vR = v1; + } + res->kind = k; + v1->edges.push_back(res); + v2->edges.push_back(res); + + if (k == EdgeKind::loose) + { + pendingDelaunayStack.push(res); + AddEdgeToActives(res); + } + return res; + } + + Triangle* Delaunay::CreateTriangle(Edge* e1, Edge* e2, Edge* e3) + { + Triangle* res = allTriangles.emplace_back(new Triangle(e1, e2, e3)); + // nb: only expire loose edges when both sides of these edges have triangles. + for (int i = 0; i < 3; ++i) + if (res->edges[i]->triA) + { + res->edges[i]->triB = res; + // this is the edge's second triangle hence no longer active + RemoveEdgeFromActives(res->edges[i]); + } + else + { + res->edges[i]->triA = res; + // this is the edge's first triangle, so only remove + // this edge from actives if it's a fixed edge. + if (!IsLooseEdge(*res->edges[i])) + RemoveEdgeFromActives(res->edges[i]); + } + return res; + } + + bool Delaunay::RemoveIntersection(Edge* e1, Edge* e2) + { + // find which vertex is closest to the other segment + // (ie not the vertex closest to the intersection point) ... + + Vertex2* v = e1->vL; + Edge* tmpE = e2; + double d = ShortestDistFromSegment(e1->vL->pt, e2->vL->pt, e2->vR->pt); + double d2 = ShortestDistFromSegment(e1->vR->pt, e2->vL->pt, e2->vR->pt); + if (d2 < d) { d = d2; v = e1->vR; } + d2 = ShortestDistFromSegment(e2->vL->pt, e1->vL->pt, e1->vR->pt); + if (d2 < d) { d = d2; tmpE = e1; v = e2->vL; } + d2 = ShortestDistFromSegment(e2->vR->pt, e1->vL->pt, e1->vR->pt); + if (d2 < d) { d = d2; tmpE = e1; v = e2->vR; } + if (d > 1.000) + return false; // Oops - this is not just a simple 'rounding' intersection + + // split 'tmpE' into 2 edges at 'v' + Vertex2* v2 = tmpE->vT; + RemoveEdgeFromVertex(v2, tmpE); + // replace v2 in tmpE with v + if (tmpE->vL == v2) tmpE->vL = v; + else tmpE->vR = v; + tmpE->vT = v; + v->edges.push_back(tmpE); + v->innerLM = false; // #47 + // left turning is angle positive + if (tmpE->vB->innerLM && GetLocMinAngle(tmpE->vB) <= 0) + tmpE->vB->innerLM = false; // #44, 52 + // finally create a new edge between v and v2 ... + CreateEdge(v, v2, tmpE->kind); + return true; + } + + bool Delaunay::FixupEdgeIntersects() + { + // precondition - edgeList must be sorted - ascending on edge.vL.pt.x + + for (size_t i1 = 0; i1 < allEdges.size(); ++i1) + { + Edge* e1 = allEdges[i1]; + // nb: we can safely ignore edges newly created inside this for loop + for (size_t i2 = i1 + 1; i2 < allEdges.size(); ++i2) + { + Edge* e2 = allEdges[i2]; + if (e2->vL->pt.x >= e1->vR->pt.x) + break; // all 'e' from now on are too far right + + // 'e2' is inside e1's horizontal region. If 'e2' is also inside + // e1's vertical region, only then check for an intersection ... + if (e2->vT->pt.y < e1->vB->pt.y && e2->vB->pt.y > e1->vT->pt.y && + (SegsIntersect(e2->vL->pt, e2->vR->pt, + e1->vL->pt, e1->vR->pt) == IntersectKind::intersect)) + { + if (!RemoveIntersection(e2, e1)) + return false; // oops!! + } + // nb: collinear edges are managed in MergeDupOrCollinearVertices below + } + } + return true; + } + + void Delaunay::SplitEdge(Edge* longE, Edge* shortE) + { + auto oldT = longE->vT, newT = shortE->vT; + // remove longEdge from longEdge.vT.edges + RemoveEdgeFromVertex(oldT, longE); + // shorten longEdge + longE->vT = newT; + if (longE->vL == oldT) longE->vL = newT; + else longE->vR = newT; + // add shortened longEdge to newT.edges + newT->edges.push_back(longE); + // and create a new edge betweem newV, oldT + CreateEdge(newT, oldT, longE->kind); + } + + void Delaunay::MergeDupOrCollinearVertices() + { + // note: this procedure may add new edges and change the + // number of edges connected with a given vertex, but it + // won't add or delete vertices (so it's safe to use iterators) + auto vIter1 = allVertices.begin(); + for (auto vIter2 = allVertices.begin() + 1; vIter2 != allVertices.end(); ++vIter2) + { + if ((*vIter1)->pt != (*vIter2)->pt) + { + vIter1 = vIter2; + continue; + } + + // merge v1 & v2 + Vertex2* v1 = *vIter1, * v2 = *vIter2; + if (!v1->innerLM || !v2->innerLM) + v1->innerLM = false; + + // in all of v2's edges, replace links to v2 with links to v1 + for (auto e : v2->edges) + { + if (e->vB == v2) e->vB = v1; + else e->vT = v1; + if (e->vL == v2) e->vL = v1; + else e->vR = v1; + } + // move all of v2's edges to v1 + std::copy(v2->edges.begin(), v2->edges.end(), back_inserter(v1->edges)); + v2->edges.resize(0); + + // excluding horizontals, if pv.edges contains two edges + // that are *collinear* and share the same bottom coords + // but have different lengths, split the longer edge at + // the top of the shorter edge ... + for (auto itE = v1->edges.begin(); itE != v1->edges.end(); ++itE) + { + if (IsHorizontal(*(*itE)) || (*itE)->vB != v1) continue; + for (auto itE2 = itE + 1; itE2 != v1->edges.end(); ++itE2) + { + auto e1 = *itE, e2 = *itE2; + if (e2->vB != v1 || e1->vT->pt.y == e2->vT->pt.y || + (CrossProductSign(e1->vT->pt, v1->pt, e2->vT->pt) != 0)) continue; + // we have parallel edges, both heading up from v1.pt. + // split the longer edge at the top of the shorter edge. + if (e1->vT->pt.y < e2->vT->pt.y) SplitEdge(e1, e2); + else SplitEdge(e2, e1); + break; // because only two edges can be collinear + } + } + } + } + + Edge* Delaunay::CreateInnerLocMinLooseEdge(Vertex2* vAbove) + { + if (!firstActive) return nullptr; // oops!! + + int64_t xAbove = vAbove->pt.x; + int64_t yAbove = vAbove->pt.y; + + // find the closest 'active' edge with a vertex that's not above vAbove + Edge* e = firstActive, *eBelow = nullptr; + double bestD = -1.0; + while (e) + { + if (e->vL->pt.x <= xAbove && e->vR->pt.x >= xAbove && + e->vB->pt.y >= yAbove && e->vB != vAbove && e->vT != vAbove && + !LeftTurning(e->vL->pt, vAbove->pt, e->vR->pt)) + { + double d = ShortestDistFromSegment(vAbove->pt, e->vL->pt, e->vR->pt); + if (!eBelow || d < bestD) // compare e with eBelow + { + eBelow = e; + bestD = d; + } + } + e = e->nextE; + } + if (!eBelow) return nullptr; // oops!! + + // get the best vertex from 'eBelow' + Vertex2* vBest = (eBelow->vT->pt.y <= yAbove) ? eBelow->vB : eBelow->vT; + int64_t xBest = vBest->pt.x; + int64_t yBest = vBest->pt.y; + + // make sure no edges intersect 'vAbove' and 'vBest' ... + // todo: fActives is currently *unsorted* but consider making it + // a tree structure based on each edge's left and right bounds + e = firstActive; + if (xBest < xAbove) + { + while (e) + { + if (e->vR->pt.x > xBest && e->vL->pt.x < xAbove && + e->vB->pt.y > yAbove && e->vT->pt.y < yBest && + (SegsIntersect(e->vB->pt, e->vT->pt, + vBest->pt, vAbove->pt) == IntersectKind::intersect)) + { + vBest = (e->vT->pt.y > yAbove) ? e->vT : e->vB; + xBest = vBest->pt.x; + yBest = vBest->pt.y; + } + e = e->nextE; + } + } + else + { + while (e) + { + if (e->vR->pt.x < xBest && e->vL->pt.x > xAbove && + e->vB->pt.y > yAbove && e->vT->pt.y < yBest && + (SegsIntersect(e->vB->pt, e->vT->pt, + vBest->pt, vAbove->pt) == IntersectKind::intersect)) + { + vBest = e->vT->pt.y > yAbove ? e->vT : e->vB; + xBest = vBest->pt.x; + yBest = vBest->pt.y; + } + e = e->nextE; + } + } + return CreateEdge(vBest, vAbove, EdgeKind::loose); + } + + Edge* Delaunay::HorizontalBetween(Vertex2* v1, Vertex2* v2) + { + int64_t y = v1->pt.y, l, r; + if (v1->pt.x > v2->pt.x) + { + l = v2->pt.x; + r = v1->pt.x; + } + else + { + l = v1->pt.x; + r = v2->pt.x; + } + + Edge* res = firstActive; + while (res) + { + if (res->vL->pt.y == y && res->vR->pt.y == y && + res->vL->pt.x >= l && res->vR->pt.x <= r && + (res->vL->pt.x != l || res->vL->pt.x != r)) break; + res = res->nextE; + } + return res; + } + + void Delaunay::DoTriangulateLeft(Edge* edge, Vertex2* pivot, int64_t minY) + { + // precondition - pivot must be one end of edge (Usually .vB) + //Assert(!EdgeCompleted(edge)); + Vertex2* vAlt = nullptr; + Edge* eAlt = nullptr; + Vertex2* v = (edge->vB == pivot) ? edge->vT : edge->vB; + + for (auto e : pivot->edges) + { + if (e == edge || !e->isActive) continue; + Vertex2* vX = e->vT == pivot ? e->vB : e->vT; + if (vX == v) continue; + + int cps = CrossProductSign(v->pt, pivot->pt, vX->pt); + if (cps == 0) //collinear paths + { + // if pivot is between v and vX then continue; + // nb: this is important for both horiz and non-horiz collinear + if ((v->pt.x > pivot->pt.x) == (pivot->pt.x > vX->pt.x)) continue; + } + // else if right-turning or not the best edge, then continue + else if (cps > 0 || (vAlt && !LeftTurning(vX->pt, pivot->pt, vAlt->pt))) + continue; + vAlt = vX; + eAlt = e; + } + + if (!vAlt || vAlt->pt.y < minY) return; + + // Don't triangulate **across** fixed edges + if (vAlt->pt.y < pivot->pt.y) + { + if (IsLeftEdge(*eAlt)) return; + } + else if (vAlt->pt.y > pivot->pt.y) + { + if (IsRightEdge(*eAlt)) return; + } + + Edge* eX = FindLinkingEdge(vAlt, v, (vAlt->pt.y < v->pt.y)); + if (!eX) + { + // be very careful creating loose horizontals at minY + if (vAlt->pt.y == v->pt.y && v->pt.y == minY && + HorizontalBetween(vAlt, v)) return; + eX = CreateEdge(vAlt, v, EdgeKind::loose); + } + + CreateTriangle(edge, eAlt, eX); + if (!EdgeCompleted(eX)) + DoTriangulateLeft(eX, vAlt, minY); + + } + + void Delaunay::DoTriangulateRight(Edge* edge, Vertex2* pivot, int64_t minY) + { + // precondition - pivot must be one end of edge (Usually .vB) + //Assert(!EdgeCompleted(edge)); + Vertex2* vAlt = nullptr; + Edge* eAlt = nullptr; + Vertex2* v = (edge->vB == pivot) ? edge->vT : edge->vB; + + for (auto e : pivot->edges) + { + if (e == edge || !e->isActive) continue; + Vertex2* vX = e->vT == pivot ? e->vB : e->vT; + if (vX == v) continue; + + int cps = CrossProductSign(v->pt, pivot->pt, vX->pt); + if (cps == 0) //collinear paths + { + // if pivot is between v and vX then continue; + // nb: this is important for both horiz and non-horiz collinear + if ((v->pt.x > pivot->pt.x) == (pivot->pt.x > vX->pt.x)) continue; + } + // else if right-turning or not the best edge, then continue + else if (cps < 0 || (vAlt && !RightTurning(vX->pt, pivot->pt, vAlt->pt))) + continue; + vAlt = vX; + eAlt = e; + } + + if (!vAlt || vAlt->pt.y < minY) return; + + // Don't triangulate **across** fixed edges + if (vAlt->pt.y < pivot->pt.y) + { + if (IsRightEdge(*eAlt)) return; + } + else if (vAlt->pt.y > pivot->pt.y) + { + if (IsLeftEdge(*eAlt)) return; + } + + Edge* eX = FindLinkingEdge(vAlt, v, (vAlt->pt.y > v->pt.y)); + if (!eX) + { + // be very careful creating loose horizontals at minY + if (vAlt->pt.y == v->pt.y && v->pt.y == minY && + HorizontalBetween(vAlt, v)) return; + eX = CreateEdge(vAlt, v, EdgeKind::loose); + } + + CreateTriangle(edge, eX, eAlt); + if (!EdgeCompleted(eX)) + DoTriangulateRight(eX, vAlt, minY); + + + } + + void Delaunay::AddEdgeToActives(Edge* edge) + { + // nb: on occassions this method can get called twice for a given edge + // This is because, in the Triangulate() method where vertex 'edges' + // arrays are being parsed, edges can can be removed from the array + // which changes the index of following edges. + if (edge->isActive) return; + + edge->prevE = nullptr; + edge->nextE = firstActive; + edge->isActive = true; + if (firstActive) + firstActive->prevE = edge; + firstActive = edge; + } + + + void Delaunay::RemoveEdgeFromActives(Edge* edge) + { + // first, remove the edge from its vertices + RemoveEdgeFromVertex(edge->vB, edge); + RemoveEdgeFromVertex(edge->vT, edge); + + // now remove the edge from double linked list (AEL) + Edge* prev = edge->prevE; + Edge* next = edge->nextE; + if (next) next->prevE = prev; + if (prev) prev->nextE = next; + edge->isActive = false; + if (firstActive == edge) firstActive = next; + } + + Paths64 Delaunay::Execute(const Paths64& paths, TriangulateResult& triResult) + { + if (!AddPaths(paths)) + { + triResult = TriangulateResult::no_polygons; + return Paths64(); // oops! + } + + // if necessary fix path orientation because the algorithm + // expects clockwise outer paths and counter-clockwise inner paths + if (lowermostVertex->innerLM) + { + // the orientation of added paths must be wrong, so + // 1. reverse innerLM flags ... + Vertex2* lm; + while (!locMinStack.empty()) + { + lm = locMinStack.top(); + lm->innerLM = !lm->innerLM; + locMinStack.pop(); + } + // 2. swap edge kinds + for (Edge* e : allEdges) + if (e->kind == EdgeKind::ascend) + e->kind = EdgeKind::descend; + else + e->kind = EdgeKind::ascend; + } + else + { + // path orientation is fine so ... + while (!locMinStack.empty()) + locMinStack.pop(); + } + + std::sort(allEdges.begin(), allEdges.end(), EdgeListSort); + if (!FixupEdgeIntersects()) + { + CleanUp(); + triResult = TriangulateResult::paths_intersect; + return Paths64(); // oops! + } + + std::sort(allVertices.begin(), allVertices.end(), VertexListSort); + MergeDupOrCollinearVertices(); + + int64_t currY = allVertices[0]->pt.y; + for (auto vIt = allVertices.begin(); vIt != allVertices.end(); ++vIt) + { + Vertex2* v = *vIt; + if (v->edges.empty()) continue; + if (v->pt.y != currY) + { + // JOIN AN INNER LOCMIN WITH A SUITABLE EDGE BELOW + while (!locMinStack.empty()) + { + Vertex2* lm = locMinStack.top(); + locMinStack.pop(); + Edge* e = CreateInnerLocMinLooseEdge(lm); + if (!e) + { + CleanUp(); + triResult = TriangulateResult::fail; + return Paths64(); // oops! + } + + if (IsHorizontal(*e)) + { + if (e->vL == e->vB) + DoTriangulateLeft(e, e->vB, currY); else + DoTriangulateRight(e, e->vB, currY); + } + else + { + DoTriangulateLeft(e, e->vB, currY); + if (!EdgeCompleted(e)) + DoTriangulateRight(e, e->vB, currY); + } + + // and because adding locMin edges to Actives was delayed .. + AddEdgeToActives(lm->edges[0]); + AddEdgeToActives(lm->edges[1]); + } + + while (!horzEdgeStack.empty()) + { + Edge* e = horzEdgeStack.top(); + horzEdgeStack.pop(); + if (EdgeCompleted(e)) continue; + if (e->vB == e->vL) // #45 + { + if (IsLeftEdge(*e)) + DoTriangulateLeft(e, e->vB, currY); + } + else + if (IsRightEdge(*e)) + DoTriangulateRight(e, e->vB, currY); + } + currY = v->pt.y; + } + + for (int i = static_cast(v->edges.size()) - 1; i >= 0; --i) + { + // the following line may look superfluous, but within this loop + // v->edges may be altered with additions and or deletions. + // So this line *is* necessary (and why we can't use an iterator). + // Also, we need to use a *descending* index which is safe because + // any additions will be loose edges which are ignored here. + if (i >= static_cast(v->edges.size())) continue; + + Edge* e = v->edges[i]; + if (EdgeCompleted(e) || IsLooseEdge(*e)) continue; + + if (v == e->vB) + { + if (IsHorizontal(*e)) + horzEdgeStack.push(e); + // delay adding locMin edges to actives + if (!v->innerLM) + AddEdgeToActives(e); + } + else + { + if (IsHorizontal(*e)) + horzEdgeStack.push(e); + else if (IsLeftEdge(*e)) + DoTriangulateLeft(e, e->vB, v->pt.y); + else + DoTriangulateRight(e, e->vB, v->pt.y); + } + } // for v->edges loop + + if (v->innerLM) locMinStack.push(v); + + } // for allVertices loop + + + while (!horzEdgeStack.empty()) + { + Edge* e = horzEdgeStack.top(); + horzEdgeStack.pop(); + if (!EdgeCompleted(e) && e->vB == e->vL) + DoTriangulateLeft(e, e->vB, currY); + } + + if (useDelaunay) + { + // Convert triangles to Delaunay conforming + while (!pendingDelaunayStack.empty()) + { + Edge* e = pendingDelaunayStack.top(); + pendingDelaunayStack.pop(); + ForceLegal(e); + } + } + + Paths64 res; + res.reserve(allTriangles.size()); + for (auto tri : allTriangles) + { + Path64 p = PathFromTriangle(*tri); + int cps = CrossProductSign(p[0], p[1], p[2]); + if (cps == 0) continue; // skip any empty triangles + if (cps < 0) // ccw + std::reverse(p.begin(), p.end()); + res.push_back(p); + } + + CleanUp(); + triResult = TriangulateResult::success; + return res; + } + + static double DistSqr(const Point64& pt1, const Point64& pt2) + { + return Sqr(pt1.x - pt2.x) + Sqr(pt1.y - pt2.y); + } + + void Delaunay::AddPath(const Path64& path) + { + size_t len = path.size(), i0 = 0, iPrev, iNext; + // find the first locMin for the current path + if (!FindLocMinIdx(path, len, i0)) return; + iPrev = Prev(i0, len); + while (path[iPrev] == path[i0]) iPrev = Prev(iPrev, len); + iNext = Next(i0, len); + + // it is possible for a locMin here to simply be a + // collinear spike that should be ignored, so ... + size_t i = i0; + while (CrossProductSign(path[iPrev], path[i], path[iNext]) == 0) + { + FindLocMinIdx(path, len, i); + if (i == i0) return; // this is an entirely collinear path + iPrev = Prev(i, len); + while (path[iPrev] == path[i]) iPrev = Prev(iPrev, len); + iNext = Next(i, len); + } + + size_t vert_cnt = allVertices.size(); + + // we are now at the first legitimate locMin + Vertex2* v0 = allVertices.emplace_back(new Vertex2(path[i])); + + if (LeftTurning(path[iPrev], path[i], path[iNext])) + v0->innerLM = true; + Vertex2* vPrev = v0; + i = iNext; + + for (;;) + { + // vPrev is a locMin here + locMinStack.push(vPrev); + // ? update lowermostVertex ... + if (!lowermostVertex || + vPrev->pt.y > lowermostVertex->pt.y || + (vPrev->pt.y == lowermostVertex->pt.y && + vPrev->pt.x < lowermostVertex->pt.x)) + lowermostVertex = vPrev; + + iNext = Next(i, len); + if (CrossProductSign(vPrev->pt, path[i], path[iNext]) == 0) + { + i = iNext; + continue; + } + + // ascend up next bound to LocMax + while (path[i].y <= vPrev->pt.y) + { + Vertex2* v = allVertices.emplace_back(new Vertex2(path[i])); + CreateEdge(vPrev, v, EdgeKind::ascend); + vPrev = v; + i = iNext; + iNext = Next(i, len); + + while (CrossProductSign(vPrev->pt, path[i], path[iNext]) == 0) + { + i = iNext; + iNext = Next(i, len); + } + } + + // Now at a locMax, so descend to next locMin + Vertex2* vPrevPrev = vPrev; + while (i != i0 && path[i].y >= vPrev->pt.y) + { + Vertex2* v = allVertices.emplace_back(new Vertex2(path[i])); + CreateEdge(v, vPrev, EdgeKind::descend); + vPrevPrev = vPrev; + vPrev = v; + i = iNext; + iNext = Next(i, len); + + while (CrossProductSign(vPrev->pt, path[i], path[iNext]) == 0) + { + i = iNext; + iNext = Next(i, len); + } + } + + // now at the next locMin + if (i == i0) break; // break for(;;) loop + if (LeftTurning(vPrevPrev->pt, vPrev->pt, path[i])) + vPrev->innerLM = true; + } + CreateEdge(v0, vPrev, EdgeKind::descend); + + // finally, ignore this path if is not a polygon or too small + len = allVertices.size() - vert_cnt; + i = vert_cnt; + if (len < 3 || (len == 3 && // or just a very tiny triangle + ((DistSqr(allVertices[i]->pt, allVertices[i + 1]->pt) <= 1) || + (DistSqr(allVertices[i + 1]->pt, allVertices[i + 2]->pt) <= 1) || + (DistSqr(allVertices[i + 2]->pt, allVertices[i]->pt) <= 1)))) + { + for (size_t j = vert_cnt; j < allVertices.size(); ++j) + allVertices[j]->edges.clear(); // flag to ignore + } + } + + bool Delaunay::AddPaths(const Paths64& paths) + { + const auto total_vertex_count = + std::accumulate(paths.begin(), paths.end(), size_t(0), + [](const auto& a, const Path64& path) + {return a + path.size(); }); + if (total_vertex_count == 0) return false; + allVertices.reserve(allVertices.capacity() + total_vertex_count); + allEdges.reserve(allEdges.capacity() + total_vertex_count); + + for (const Path64& path : paths) + AddPath(path); + return (allVertices.size() > 2); + } + + TriangulateResult Triangulate(const Paths64& pp, Paths64& solution, bool useDelaunay) + { + TriangulateResult result; + Delaunay d(useDelaunay); + solution = d.Execute(pp, result); + return result; + } + + TriangulateResult Triangulate(const PathsD& pp, int decPlaces, PathsD& solution, bool useDelaunay) + { + int ec; + double scale; + TriangulateResult result; + if (decPlaces <= 0) scale = 1; + else if (decPlaces > 8) scale = std::pow(10, 8); + else scale = std::pow(10, decPlaces); + Paths64 pp64 = ScalePaths(pp, scale, ec); + + Delaunay d(useDelaunay); + Paths64 sol64 = d.Execute(pp64, result); + solution = ScalePaths(sol64, 1 / scale, ec); + return result; + } + +} // Clipper2Lib namespace diff --git a/deps_src/clipper2/Clipper2Lib/src/clipper2_z.cpp b/deps_src/clipper2/Clipper2Lib/src/clipper2_z.cpp index f5c7c6f4fd..385bd37463 100644 --- a/deps_src/clipper2/Clipper2Lib/src/clipper2_z.cpp +++ b/deps_src/clipper2/Clipper2Lib/src/clipper2_z.cpp @@ -6,3 +6,4 @@ #include "clipper.engine.cpp" #include "clipper.offset.cpp" #include "clipper.rectclip.cpp" +#include "clipper.triangulation.cpp" diff --git a/deps_src/libnest2d/include/libnest2d/backends/libslic3r/geometries.hpp b/deps_src/libnest2d/include/libnest2d/backends/libslic3r/geometries.hpp index 63ed1c2779..381e4a7e90 100644 --- a/deps_src/libnest2d/include/libnest2d/backends/libslic3r/geometries.hpp +++ b/deps_src/libnest2d/include/libnest2d/backends/libslic3r/geometries.hpp @@ -132,14 +132,14 @@ template<> inline void offset(Slic3r::ExPolygon& sh, coord_t distance, const PolygonTag&) { #define DISABLE_BOOST_OFFSET - auto res = Slic3r::offset_ex(sh, distance, Slic3r::ClipperLib::jtSquare); + auto res = Slic3r::offset_ex(sh, distance, Slic3r::jtSquare); if (!res.empty()) sh = res.front(); } template<> inline void offset(Slic3r::Polygon& sh, coord_t distance, const PathTag&) { - auto res = Slic3r::offset(sh, distance, Slic3r::ClipperLib::jtSquare); + auto res = Slic3r::offset(sh, distance, Slic3r::jtSquare); if (!res.empty()) sh = res.front(); } diff --git a/docs/HLSD/polygon-clipping.md b/docs/HLSD/polygon-clipping.md new file mode 100644 index 0000000000..60bf97d515 --- /dev/null +++ b/docs/HLSD/polygon-clipping.md @@ -0,0 +1,131 @@ +# Polygon Clipping — High Level Design + +## Purpose and scope + +Almost every stage of slicing works on 2D regions: slices, perimeters, infill +areas, bridges, supports and brims are all produced by boolean operations and +offsets on polygons. libslic3r does this through two interfaces, both built on +the Clipper2 library vendored in `deps_src/clipper2`: + +- `ClipperUtils` (`src/libslic3r/ClipperUtils.hpp`) takes and returns Slic3r + geometry: `Polygon(s)`, `ExPolygon(s)`, `Polyline(s)`, `Lines` and + `Surfaces`. It provides unions, intersections, differences and xor, closed + and open offsets, morphological opening and closing, variable width offsets + and polyline clipping. +- `ClipperZUtils` (`src/libslic3r/ClipperZUtils.hpp`) clips paths whose + vertices carry a Z value, which callers use to tag vertices with a source + index or an extrusion width. + +No other code calls Clipper2. + +`ClipperUtils` declares its own `JoinType`, `EndType`, `PolyFillType` and +`ClipType` enums and maps them to Clipper2's. Every call builds its own +Clipper2 objects and shares no state, so slicing threads can clip +concurrently. + +Clipping is one of the largest costs of slicing, and nearly all of it goes +through `ClipperUtils`. The layer is therefore designed for throughput as much +as for predictable geometry. + +## Vendored Clipper2 + +`deps_src/clipper2` builds the static target `Clipper2`. It carries four +changes to the upstream sources that must be carried over when Clipper2 is +updated. The namespace switch sits at the top of every header and source, the +other three are marked with `Orca:` comments. + +| Change | Files | Why | +| --- | --- | --- | +| Z build in its own namespace | all headers and sources, `clipper2_z.cpp`, `clipper2_z.hpp` | The library is compiled a second time with `USINGZ` in namespace `Clipper2Lib_Z`, so the 2D and the Z variants link into one binary. | +| Engine nodes from tbbmalloc | `clipper.engine.h`, `clipper.engine.cpp` | Vertices, active edges, output points and records, local minima and `PolyTree` nodes are allocated one by one. `CLIPPER2_NODE_ALLOCATOR` routes them through `scalable_malloc`, because the default heap does not scale when all slicing threads clip at once. | +| Concave joins at the edge crossing | `clipper.offset.cpp` | For closed paths, a concave corner is joined at the crossing of the two offset edges when that point lies within half of both adjacent edges. The upstream 3-point loop makes inward offsets of dense curves very slow to union. | +| Rounded arc steps | `clipper.offset.cpp` | Round joins use the rounded number of steps, not the ceiling, which keeps the vertex count of round offsets that the rest of the code is tuned for. | + +## ClipperUtils semantics + +The callers of `ClipperUtils` rely on a fixed set of behaviours. Where +Clipper2 behaves differently by default, the wrapper adjusts it. + +### Booleans + +- The fill rule is non-zero unless the function takes a `PolyFillType`. One + rule applies to both subject and clip; Clipper2 has no per-operand rule. +- Collinear vertices are removed from the result. Clipper2 keeps them by + default, so every boolean sets `PreserveCollinear(false)`. +- Outer contours are CCW and holes are CW. No output contour touches + itself: where one would pass twice through a vertex, it is split there into + two contours. +- `ExPolygons` results are built from one `PolyTree64` pass. An island inside + a hole becomes an `ExPolygon` of its own. +- `ApplySafetyOffset::Yes` grows the clip polygons by `ClipperSafetyOffset` + before an intersection or a difference, so that edges shared by subject and + clip do not leave slivers. +- Open polylines are clipped with the non-zero rule and keep their direction. + +### Offsets + +- Before offsetting, input vertices closer than + `ClipperOffsetShortestEdgeFactor` × |delta| to the previously kept vertex + are dropped. This bounds the work on dense contours, and the error it + introduces is far below the offset distance. +- The miter limit is at least 2. For `jtRound`, a positive `miterLimit` + argument is the arc tolerance, capped at |delta| / 4, and 0.25 is used + otherwise. Other joins use the smaller of 0.25 and |delta| / 4 for round end + caps. +- A single `Polygon` keeps its orientation: a CCW polygon grows with a + positive delta, a CW polygon is a hole and shrinks. +- `Polygons` follow the same rule per path. When every CW path lies strictly + inside the bounding box of a CCW path, which is the usual case of contours + with their holes, all paths are offset in one Clipper2 group. Otherwise + each path is offset on its own and the results are united, with the + non-zero rule when growing and the positive rule when shrinking. +- `ExPolygons` and `Surfaces` are offset as one group after the contours are + oriented CCW and the holes CW, whatever their input orientation. +- Zero-area paths vanish under a negative offset instead of growing. +- Polyline offsets use the requested end type. Clipper2 already unites the + result, so no further union is done. + +### Coordinate range + +Clipper2 computes intersections and slopes in doubles, which hold integers +exactly only up to 2^53 (about 9e15 units, 9,000 km). Geometry passed to +`ClipperUtils` must stay well inside that range; near the int64 limit the +results shift by hundreds of units. This is why the arrange `InfiniteBed` is a +box of ±2^50 units around its centre rather than libnest2d's infinite box, +which reaches ±2.3e18. + +## ClipperZUtils + +`ZPoint` is a `Vec3crd`, and a `ZPath` is a vector of them. +`clip_zpaths()` runs one boolean with the non-zero rule on the Clipper2 Z +build. The subject may be open, the clip is closed, and the result lists the +closed paths before the open ones. + +The Z of each output vertex follows these rules: + +- An input vertex keeps its Z. +- An intersection that lies on an end point of one of the two crossing edges + takes that end point's Z, preferring the subject edge. +- Any other intersection gets its Z from the callback, which receives both + crossing edges, the subject edge first. + +Clipper2 calls the callback only when it creates an output vertex at an +intersection, not for every crossing it processes. A callback that records +intersections, like `ClipperZIntersectionVisitor`, therefore sees only those. + +The users are: + +| User | Z carries | +| --- | --- | +| `Algorithm::wave_seeds()` (region expansion) | source and boundary index; intersections get a negative index into the visitor's list of crossing pairs | +| `Algorithm::split_line()` | index of the source vertex; an intersection gets the negated index of its source edge, so the pieces can be put back in path order | +| `PerimeterGenerator` overhang and top-surface clipping of Arachne walls | extrusion width, interpolated along the edge at intersections | +| Tree support anchors in `SupportCommon` | index of the source contour, -1 at intersections | +| `extrusion_paths_append()` | extrusion width, turned into extrusion paths | + +## Testing + +`tests/libslic3r/test_clipper_utils.cpp` and `test_clipper_offset.cpp` cover +the wrapper's booleans, orientation and offset rules. The perimeter, support +and region expansion users are exercised by the slicing tests in +`tests/fff_print`. diff --git a/src/libslic3r/Algorithm/LineSplit.cpp b/src/libslic3r/Algorithm/LineSplit.cpp index 9e359b76ea..d91514dbf9 100644 --- a/src/libslic3r/Algorithm/LineSplit.cpp +++ b/src/libslic3r/Algorithm/LineSplit.cpp @@ -14,7 +14,7 @@ static std::atomic g_dbg_id = 0; #endif // Z for points from clip polygon -static constexpr auto CLIP_IDX = std::numeric_limits::max(); +static constexpr auto CLIP_IDX = std::numeric_limits::max(); static void cb_split_line(const ClipperZUtils::ZPoint& e1bot, const ClipperZUtils::ZPoint& e1top, @@ -91,14 +91,7 @@ SplittedLine do_split_line(const ClipperZUtils::ZPath& path, const ExPolygons& c clip_path.emplace_back(ClipperZUtils::to_zpath(hole.points, CLIP_IDX)); } - ClipperLib_Z::Clipper zclipper; - zclipper.PreserveCollinear(true); - zclipper.ZFillFunction(cb_split_line); - zclipper.AddPaths(clip_path, ClipperLib_Z::ptClip, true); - zclipper.AddPath(path, ClipperLib_Z::ptSubject, false); - ClipperLib_Z::PolyTree polytree; - zclipper.Execute(ClipperLib_Z::ctIntersection, polytree, ClipperLib_Z::pftNonZero, ClipperLib_Z::pftNonZero); - ClipperLib_Z::PolyTreeToPaths(std::move(polytree), intersections); + intersections = ClipperZUtils::clip_zpaths(ctIntersection, ClipperZUtils::ZPaths{ path }, true, clip_path, cb_split_line, true); } if (intersections.empty()) { return {}; diff --git a/src/libslic3r/Algorithm/LineSplit.hpp b/src/libslic3r/Algorithm/LineSplit.hpp index e12113fa6c..80f64e684c 100644 --- a/src/libslic3r/Algorithm/LineSplit.hpp +++ b/src/libslic3r/Algorithm/LineSplit.hpp @@ -50,7 +50,7 @@ SplittedLine split_line(const PathType& path, const ExPolygons& clip, bool close // Convert the input path into an open ZPath ClipperZUtils::ZPath p; p.reserve(path.size() + (closed ? 1 : 0)); - ClipperLib_Z::cInt z = 0; + coord_t z = 0; for (const auto& point : path) { p.emplace_back(point.x(), point.y(), z); z++; diff --git a/src/libslic3r/Algorithm/RegionExpansion.cpp b/src/libslic3r/Algorithm/RegionExpansion.cpp index 6a77c470e7..2cfb15ce6d 100644 --- a/src/libslic3r/Algorithm/RegionExpansion.cpp +++ b/src/libslic3r/Algorithm/RegionExpansion.cpp @@ -10,7 +10,7 @@ namespace Slic3r { namespace Algorithm { -// Calculating radius discretization according to ClipperLib offsetter code, see void ClipperOffset::DoOffset(double delta) +// Calculating radius discretization according to the Clipper offsetter code, see ClipperOffset::DoGroupOffset() inline double clipper_round_offset_error(double offset, double arc_tolerance) { static constexpr const double def_arc_tolerance = 0.25; @@ -61,7 +61,6 @@ RegionExpansionParameters RegionExpansionParameters::build( // Accuracy of the offsetter for wave propagation. out.arc_tolerance = scaled(0.1); - out.shortest_edge_length = out.initial_step * ClipperOffsetShortestEdgeFactor; // Maximum inflation of seed contours over the boundary. Used to trim boundary to speed up // clipping during wave propagation. Needs to be in sync with the offsetter accuracy. @@ -75,25 +74,20 @@ RegionExpansionParameters RegionExpansionParameters::build( // similar to expolygons_to_zpaths(), but each contour is expanded before converted to zpath. // The expanded contours are then opened (the first point is repeated at the end). -static ClipperLib_Z::Paths expolygons_to_zpaths_expanded_opened( +static ClipperZUtils::ZPaths expolygons_to_zpaths_expanded_opened( const ExPolygons &src, const float expansion, coord_t &base_idx) { - ClipperLib_Z::Paths out; + ClipperZUtils::ZPaths out; out.reserve(2 * std::accumulate(src.begin(), src.end(), size_t(0), [](const size_t acc, const ExPolygon &expoly) { return acc + expoly.num_contours(); })); - ClipperLib::ClipperOffset offsetter; - offsetter.ShortestEdgeLength = expansion * ClipperOffsetShortestEdgeFactor; - ClipperLib::Paths expansion_cache; for (const ExPolygon &expoly : src) { for (size_t icontour = 0; icontour < expoly.num_contours(); ++ icontour) { - // Execute reorients the contours so that the outer most contour has a positive area. Thus the output - // contours will be CCW oriented even though the input paths are CW oriented. - // Offset is applied after contour reorientation, thus the signum of the offset value is reversed. - offsetter.Clear(); - offsetter.AddPath(expoly.contour_or_hole(icontour).points, ClipperLib::jtSquare, ClipperLib::etClosedPolygon); - expansion_cache.clear(); - offsetter.Execute(expansion_cache, icontour == 0 ? expansion : -expansion); - append(out, ClipperZUtils::to_zpaths(expansion_cache, base_idx)); + // Orient CCW, then grow the contour and shrink the holes. The output contours are CCW. + Polygon contour = expoly.contour_or_hole(icontour); + if (! contour.is_counter_clockwise()) + contour.reverse(); + for (const Polygon &expanded : offset(contour, icontour == 0 ? expansion : -expansion, jtSquare)) + out.emplace_back(ClipperZUtils::to_zpath(expanded.points, base_idx)); } ++ base_idx; } @@ -104,21 +98,21 @@ static ClipperLib_Z::Paths expolygons_to_zpaths_expanded_opened( // Thus some pieces of the clipped polygons may now become split at the ends of the source polygons. // Those ends are sorted lexicographically in "splits". // Reconnect those split pieces. -static inline void merge_splits(ClipperLib_Z::Paths &paths, std::vector> &splits) +static inline void merge_splits(ClipperZUtils::ZPaths &paths, std::vector> &splits) { for (auto it_path = paths.begin(); it_path != paths.end(); ) { - ClipperLib_Z::Path &path = *it_path; + ClipperZUtils::ZPath &path = *it_path; assert(path.size() >= 2); bool merged = false; if (path.size() >= 2) { - const ClipperLib_Z::IntPoint &front = path.front(); - const ClipperLib_Z::IntPoint &back = path.back(); + const ClipperZUtils::ZPoint &front = path.front(); + const ClipperZUtils::ZPoint &back = path.back(); // The path before clipping was supposed to cross the clipping boundary or be fully out of it. // Thus the clipped contour is supposed to become open, with one exception: The anchor expands into a closed hole. if (front.x() != back.x() || front.y() != back.y()) { // Look up the ends in "splits", possibly join the contours. // "splits" maps into the other piece connected to the same end point. - auto find_end = [&splits](const ClipperLib_Z::IntPoint &pt) -> std::pair* { + auto find_end = [&splits](const ClipperZUtils::ZPoint &pt) -> std::pair* { auto it = std::lower_bound(splits.begin(), splits.end(), pt, [](const auto &l, const auto &r){ return ClipperZUtils::zpoint_lower(l.first, r); }); return it != splits.end() && it->first == pt ? &(*it) : nullptr; @@ -136,7 +130,7 @@ static inline void merge_splits(ClipperLib_Z::Paths &paths, std::vectorsecond = int(it_path - paths.begin()); } else { // Open end was found and matched with end->second - ClipperLib_Z::Path &other_path = paths[end->second]; + ClipperZUtils::ZPath &other_path = paths[end->second]; polylines_merge(other_path, other_path.front() == end->first, std::move(path), end_front); if (std::next(it_path) == paths.end()) { paths.pop_back(); @@ -212,36 +206,29 @@ std::vector wave_seeds( using Intersection = ClipperZUtils::ClipperZIntersectionVisitor::Intersection; using Intersections = ClipperZUtils::ClipperZIntersectionVisitor::Intersections; - ClipperLib_Z::Paths segments; - Intersections intersections; + ClipperZUtils::ZPaths segments; + Intersections intersections; - coord_t idx_boundary_begin = 1; - coord_t idx_boundary_end = idx_boundary_begin; - coord_t idx_src_end; + coord_t idx_boundary_begin = 1; + coord_t idx_boundary_end = idx_boundary_begin; + coord_t idx_src_end; { - ClipperLib_Z::Clipper zclipper; ClipperZUtils::ClipperZIntersectionVisitor visitor(intersections); - zclipper.ZFillFunction(visitor.clipper_callback()); // as closed contours - zclipper.AddPaths(ClipperZUtils::expolygons_to_zpaths(boundary, idx_boundary_end), ClipperLib_Z::ptClip, true); + ClipperZUtils::ZPaths zboundary = ClipperZUtils::expolygons_to_zpaths(boundary, idx_boundary_end); // as open contours - std::vector> zsrc_splits; - { - idx_src_end = idx_boundary_end; - ClipperLib_Z::Paths zsrc = expolygons_to_zpaths_expanded_opened(src, tiny_expansion, idx_src_end); - zclipper.AddPaths(zsrc, ClipperLib_Z::ptSubject, false); - zsrc_splits.reserve(zsrc.size()); - for (const ClipperLib_Z::Path &path : zsrc) { - assert(path.size() >= 2); - assert(path.front() == path.back()); - zsrc_splits.emplace_back(path.front(), -1); - } - std::sort(zsrc_splits.begin(), zsrc_splits.end(), [](const auto &l, const auto &r){ return ClipperZUtils::zpoint_lower(l.first, r.first); }); + std::vector> zsrc_splits; + idx_src_end = idx_boundary_end; + ClipperZUtils::ZPaths zsrc = expolygons_to_zpaths_expanded_opened(src, tiny_expansion, idx_src_end); + zsrc_splits.reserve(zsrc.size()); + for (const ClipperZUtils::ZPath &path : zsrc) { + assert(path.size() >= 2); + assert(path.front() == path.back()); + zsrc_splits.emplace_back(path.front(), -1); } - ClipperLib_Z::PolyTree polytree; - zclipper.Execute(ClipperLib_Z::ctIntersection, polytree, ClipperLib_Z::pftNonZero, ClipperLib_Z::pftNonZero); - ClipperLib_Z::PolyTreeToPaths(std::move(polytree), segments); + std::sort(zsrc_splits.begin(), zsrc_splits.end(), [](const auto &l, const auto &r){ return ClipperZUtils::zpoint_lower(l.first, r.first); }); + segments = ClipperZUtils::clip_zpaths(ctIntersection, zsrc, true, zboundary, visitor.clipper_callback()); merge_splits(segments, zsrc_splits); } @@ -256,10 +243,10 @@ std::vector wave_seeds( WaveSeeds out; out.reserve(segments.size()); int iseed = 0; - for (const ClipperLib_Z::Path &path : segments) { + for (const ClipperZUtils::ZPath &path : segments) { assert(path.size() >= 2); - ClipperLib_Z::IntPoint front = path.front(); - ClipperLib_Z::IntPoint back = path.back(); + ClipperZUtils::ZPoint front = path.front(); + ClipperZUtils::ZPoint back = path.back(); // Both ends of a seed segment are supposed to be inside a single boundary expolygon. // Thus as long as the seed contour is not closed, it should be open at a boundary point. assert((front == back && front.z() >= idx_boundary_end && front.z() < idx_src_end) || @@ -280,7 +267,7 @@ std::vector wave_seeds( // boundary ID is not directly available). coord_t src_z = -1, boundary_z = -1; // Scan all path points for the information we need. - for (const ClipperLib_Z::IntPoint &point : path) { + for (const ClipperZUtils::ZPoint &point : path) { if (point.z() >= idx_boundary_end && point.z() < idx_src_end && src_z < 0) src_z = point.z(); else if (point.z() >= idx_boundary_begin && point.z() < idx_boundary_end && boundary_z < 0) @@ -311,7 +298,7 @@ std::vector wave_seeds( // See https://github.com/prusa3d/PrusaSlicer/issues/12469. // Segement is open, yet its first point seems to be part of boundary polygon. // Take the first point with src polygon index. - for (const ClipperLib_Z::IntPoint &point : path) { + for (const ClipperZUtils::ZPoint &point : path) { if (point.z() >= idx_boundary_end) { front = point; back = point; @@ -360,17 +347,13 @@ std::vector wave_seeds( return out; } -static ClipperLib::Paths wavefront_initial(ClipperLib::ClipperOffset &co, const ClipperLib::Paths &polylines, float offset) +static Polygons wavefront_initial(const VecOfPoints &polylines, float offset, double arc_tolerance) { - ClipperLib::Paths out; + Polygons out; out.reserve(polylines.size()); - ClipperLib::Paths out_this; - for (const ClipperLib::Path &path : polylines) { + for (const Points &path : polylines) { assert(path.size() >= 2); - co.Clear(); - co.AddPath(path, jtRound, path.front() == path.back() ? ClipperLib::etClosedLine : ClipperLib::etOpenRound); - co.Execute(out_this, offset); - append(out, std::move(out_this)); + append(out, Slic3r::offset(Polyline(path), offset, jtRound, arc_tolerance, path.front() == path.back() ? etClosedLine : etOpenRound)); } return out; } @@ -378,43 +361,24 @@ static ClipperLib::Paths wavefront_initial(ClipperLib::ClipperOffset &co, const // Input polygons may consist of multiple expolygons, even nested expolygons. // After inflation some polygons may thus overlap, however the overlap is being resolved during the successive // clipping operation, thus it is not being done here. -static ClipperLib::Paths wavefront_step(ClipperLib::ClipperOffset &co, const ClipperLib::Paths &polygons, float offset) +static Polygons wavefront_step(const Polygons &polygons, float offset, double arc_tolerance) { - ClipperLib::Paths out; + Polygons out; out.reserve(polygons.size()); - ClipperLib::Paths out_this; - for (const ClipperLib::Path &polygon : polygons) { - co.Clear(); - // Execute reorients the contours so that the outer most contour has a positive area. Thus the output - // contours will be CCW oriented even though the input paths are CW oriented. - // Offset is applied after contour reorientation, thus the signum of the offset value is reversed. - co.AddPath(polygon, jtRound, ClipperLib::etClosedPolygon); - bool ccw = ClipperLib::Orientation(polygon); - co.Execute(out_this, ccw ? offset : - offset); - if (! ccw) { - // Reverse the resulting contours. - for (ClipperLib::Path &path : out_this) - std::reverse(path.begin(), path.end()); - } - append(out, std::move(out_this)); - } + for (const Polygon &polygon : polygons) + // CCW contours grow, CW holes shrink. + append(out, Slic3r::offset(polygon, offset, jtRound, arc_tolerance)); return out; } -static ClipperLib::Paths wavefront_clip(const ClipperLib::Paths &wavefront, const Polygons &clipping) +static Polygons wavefront_clip(const Polygons &wavefront, const Polygons &clipping) { - ClipperLib::Clipper clipper; - clipper.AddPaths(wavefront, ClipperLib::ptSubject, true); - clipper.AddPaths(ClipperUtils::PolygonsProvider(clipping), ClipperLib::ptClip, true); - ClipperLib::Paths out; - clipper.Execute(ClipperLib::ctIntersection, out, ClipperLib::pftPositive, ClipperLib::pftPositive); - return out; + return intersection(wavefront, clipping, pftPositive); } static Polygons propagate_wave_from_boundary( - ClipperLib::ClipperOffset &co, // Seed of the wave: Open polylines very close to the boundary. - const ClipperLib::Paths &seed, + const VecOfPoints &seed, // Boundary inside which the waveform will propagate. const ExPolygon &boundary, // How much to inflate the seed lines to produce the first wave area. @@ -425,25 +389,24 @@ static Polygons propagate_wave_from_boundary( const size_t num_other_steps, // Maximum inflation of seed contours over the boundary. Used to trim boundary to speed up // clipping during wave propagation. - const float max_inflation) + const float max_inflation, + // Accuracy of the round offsets. + const double arc_tolerance) { assert(! seed.empty() && seed.front().size() >= 2); Polygons clipping = ClipperUtils::clip_clipper_polygons_with_subject_bbox(boundary, get_extents(seed).inflated(max_inflation)); - ClipperLib::Paths polygons = wavefront_clip(wavefront_initial(co, seed, initial_step), clipping); + Polygons polygons = wavefront_clip(wavefront_initial(seed, initial_step, arc_tolerance), clipping); // Now offset the remaining for (size_t ioffset = 0; ioffset < num_other_steps; ++ ioffset) - polygons = wavefront_clip(wavefront_step(co, polygons, other_step), clipping); - return to_polygons(polygons); + polygons = wavefront_clip(wavefront_step(polygons, other_step, arc_tolerance), clipping); + return polygons; } // Resulting regions are sorted by boundary id and source id. std::vector propagate_waves(const WaveSeeds &seeds, const ExPolygons &boundary, const RegionExpansionParameters ¶ms) { std::vector out; - ClipperLib::Paths paths; - ClipperLib::ClipperOffset co; - co.ArcTolerance = params.arc_tolerance; - co.ShortestEdgeLength = params.shortest_edge_length; + VecOfPoints paths; for (auto it_seed = seeds.begin(); it_seed != seeds.end();) { auto it = it_seed; paths.clear(); @@ -452,7 +415,7 @@ std::vector propagate_waves(const WaveSeeds &seeds, const ExPol // Propagate the wavefront while clipping it with the trimmed boundary. // Collect the expanded polygons, merge them with the source polygons. RegionExpansion re; - for (Polygon &polygon : propagate_wave_from_boundary(co, paths, boundary[it_seed->boundary], params.initial_step, params.other_step, params.num_other_steps, params.max_inflation)) + for (Polygon &polygon : propagate_wave_from_boundary(paths, boundary[it_seed->boundary], params.initial_step, params.other_step, params.num_other_steps, params.max_inflation, params.arc_tolerance)) out.push_back({ std::move(polygon), it_seed->src, it_seed->boundary }); it_seed = it; } diff --git a/src/libslic3r/Algorithm/RegionExpansion.hpp b/src/libslic3r/Algorithm/RegionExpansion.hpp index eb99674902..a3e5ddaa38 100644 --- a/src/libslic3r/Algorithm/RegionExpansion.hpp +++ b/src/libslic3r/Algorithm/RegionExpansion.hpp @@ -25,7 +25,6 @@ struct RegionExpansionParameters // Accuracy of the offsetter for wave propagation. double arc_tolerance; - double shortest_edge_length; static RegionExpansionParameters build( // Scaled expansion value diff --git a/src/libslic3r/Arachne/WallToolPaths.cpp b/src/libslic3r/Arachne/WallToolPaths.cpp index 724016bcb1..d1f0341bda 100644 --- a/src/libslic3r/Arachne/WallToolPaths.cpp +++ b/src/libslic3r/Arachne/WallToolPaths.cpp @@ -263,10 +263,8 @@ std::unique_ptr cre */ void fixSelfIntersections(const coord_t epsilon, Polygons &thiss) { - if (epsilon < 1) { - ClipperLib::SimplifyPolygons(ClipperUtils::PolygonsProvider(thiss), ClipperLib::pftEvenOdd); + if (epsilon < 1) return; - } const int64_t half_epsilon = (epsilon + 1) / 2; @@ -304,8 +302,6 @@ void fixSelfIntersections(const coord_t epsilon, Polygons &thiss) } } } - - ClipperLib::SimplifyPolygons(ClipperUtils::PolygonsProvider(thiss), ClipperLib::pftEvenOdd); } /*! @@ -352,18 +348,11 @@ void removeDegenerateVerts(Polygons &thiss) void removeSmallAreas(Polygons &thiss, const double min_area_size, const bool remove_holes) { - auto to_path = [](const Polygon &poly) -> ClipperLib::Path { - ClipperLib::Path out; - for (const Point &pt : poly.points) - out.emplace_back(ClipperLib::cInt(pt.x()), ClipperLib::cInt(pt.y())); - return out; - }; - auto new_end = thiss.end(); if (remove_holes) { for (auto it = thiss.begin(); it < new_end;) { // All polygons smaller than target are removed by replacing them with a polygon from the back of the vector. - if (fabs(ClipperLib::Area(to_path(*it))) < min_area_size) { + if (fabs(it->area()) < min_area_size) { --new_end; *it = std::move(*new_end); continue; // Don't increment the iterator such that the polygon just swapped in is checked next. @@ -374,7 +363,7 @@ void removeSmallAreas(Polygons &thiss, const double min_area_size, const bool re // For each polygon, computes the signed area, move small outlines at the end of the vector and keep pointer on small holes Polygons small_holes; for (auto it = thiss.begin(); it < new_end;) { - if (double area = ClipperLib::Area(to_path(*it)); fabs(area) < min_area_size) { + if (double area = it->area(); fabs(area) < min_area_size) { if (area >= 0) { --new_end; if (it < new_end) { @@ -773,7 +762,7 @@ void WallToolPaths::separateOutInnerContour() //To get a correct shape, we need to make the outside contour positive and any holes inside negative. //This can be done by applying the even-odd rule to the shape. This rule is not sensitive to the winding order of the polygon. //The even-odd rule would be incorrect if the polygon self-intersects, but that should never be generated by the skeletal trapezoidation. - inner_contour = union_(inner_contour, ClipperLib::PolyFillType::pftEvenOdd); + inner_contour = union_(inner_contour, pftEvenOdd); } const Polygons& WallToolPaths::getInnerContour() diff --git a/src/libslic3r/Arachne/utils/ExtrusionLine.cpp b/src/libslic3r/Arachne/utils/ExtrusionLine.cpp index 66bb707ebe..2beaa4f92f 100644 --- a/src/libslic3r/Arachne/utils/ExtrusionLine.cpp +++ b/src/libslic3r/Arachne/utils/ExtrusionLine.cpp @@ -287,9 +287,9 @@ double ExtrusionLine::area() const } // namespace Slic3r::Arachne namespace Slic3r { -void extrusion_paths_append(ExtrusionPaths &dst, const ClipperLib_Z::Paths &extrusion_paths, const ExtrusionRole role, const Flow &flow) +void extrusion_paths_append(ExtrusionPaths &dst, const ClipperZUtils::ZPaths &extrusion_paths, const ExtrusionRole role, const Flow &flow) { - for (const ClipperLib_Z::Path &extrusion_path : extrusion_paths) { + for (const ClipperZUtils::ZPath &extrusion_path : extrusion_paths) { ThickPolyline thick_polyline = Arachne::to_thick_polyline(extrusion_path); Slic3r::append(dst, thick_polyline_to_multi_path(thick_polyline, role, flow, scaled(0.05), float(SCALED_EPSILON)).paths); } diff --git a/src/libslic3r/Arachne/utils/ExtrusionLine.hpp b/src/libslic3r/Arachne/utils/ExtrusionLine.hpp index 72e008cef1..8b4a753f5e 100644 --- a/src/libslic3r/Arachne/utils/ExtrusionLine.hpp +++ b/src/libslic3r/Arachne/utils/ExtrusionLine.hpp @@ -5,7 +5,7 @@ #ifndef UTILS_EXTRUSION_LINE_H #define UTILS_EXTRUSION_LINE_H -#include +#include "../../ClipperZUtils.hpp" #include #include #include @@ -294,7 +294,7 @@ using VariableWidthLines = std::vector; //, int...EigenArgs> inline constexpr Eigen::Matrix unscaled( - const Slic3r::ClipperLib::IntPoint &v) noexcept + const Slic3r::Point &v) noexcept { return Eigen::Matrix{unscaled(v.x()), unscaled(v.y())}; @@ -405,7 +405,7 @@ protected: // 2) X distance of item corner to bed corner (low weight) // 3) item row occupancy (useful when rotation is enabled) // 4)需要允许往屏蔽区域的左边或下边去一点,不然很多物体可能认为摆不进去,实际上我们最后是可以做平移的 - double dist_for_BOTTOM_LEFT(Box ibb, const ClipperLib::IntPoint& origin_pack) + double dist_for_BOTTOM_LEFT(Box ibb, const Slic3r::Point& origin_pack) { double dist_corner_y = ibb.minCorner().y() - origin_pack.y(); double dist_corner_x = ibb.minCorner().x() - origin_pack.x(); @@ -421,7 +421,7 @@ protected: return bindist; } - double dist_to_bin(const Box& ibb, const ClipperLib::IntPoint& origin_pack, typename Packer::PlacementConfig::Alignment starting_point_alignment) + double dist_to_bin(const Box& ibb, const Slic3r::Point& origin_pack, typename Packer::PlacementConfig::Alignment starting_point_alignment) { double bindist = 0; if (starting_point_alignment == PConfig::Alignment::BOTTOM_LEFT) @@ -439,7 +439,7 @@ protected: // as it possibly can be but at the same time, it has to provide // reasonable results. std::tuple - objfunc(const Item &item, const ClipperLib::IntPoint &origin_pack) + objfunc(const Item &item, const Slic3r::Point &origin_pack) { const double bin_area = m_bin_area; const SpatIndex& spatindex = m_rtree; @@ -1014,7 +1014,12 @@ void _arrange( inline Box to_nestbin(const BoundingBox &bb) { return Box{{bb.min(X), bb.min(Y)}, {bb.max(X), bb.max(Y)}};} inline Circle to_nestbin(const CircleBed &c) { return Circle({c.center()(0), c.center()(1)}, c.radius()); } inline ExPolygon to_nestbin(const Polygon &p) { return ExPolygon{p}; } -inline Box to_nestbin(const InfiniteBed &bed) { return Box::infinite({bed.center.x(), bed.center.y()}); } +// libnest2d's infinite box reaches the int64 limit, where Clipper2's double math is no longer exact. +inline Box to_nestbin(const InfiniteBed &bed) +{ + const coord_t r = coord_t(1) << 50; + return Box{{bed.center.x() - r, bed.center.y() - r}, {bed.center.x() + r, bed.center.y() + r}}; +} inline coord_t width(const BoundingBox& box) { return box.max.x() - box.min.x(); } inline coord_t height(const BoundingBox& box) { return box.max.y() - box.min.y(); } @@ -1122,8 +1127,6 @@ void arrange(ArrangePolygons & arrangables, const BedT & bed, const ArrangeParams & params) { - namespace clppr = Slic3r::ClipperLib; - std::vector items, fixeditems; items.reserve(arrangables.size()); diff --git a/src/libslic3r/CMakeLists.txt b/src/libslic3r/CMakeLists.txt index cb296e43c0..ef15304c8b 100644 --- a/src/libslic3r/CMakeLists.txt +++ b/src/libslic3r/CMakeLists.txt @@ -94,12 +94,9 @@ set(lisbslic3r_sources calib.hpp Circle.cpp Circle.hpp - clipper.cpp - clipper.hpp ClipperUtils.cpp ClipperUtils.hpp - Clipper2Utils.cpp - Clipper2Utils.hpp + ClipperZUtils.cpp ClipperZUtils.hpp Color.cpp Color.hpp @@ -716,7 +713,6 @@ target_link_libraries(libslic3r ${OCCT_LIBS} boost_libs cereal::cereal - clipper Clipper2 draco::draco glu-libtess diff --git a/src/libslic3r/Clipper2Utils.cpp b/src/libslic3r/Clipper2Utils.cpp deleted file mode 100644 index f119fc3924..0000000000 --- a/src/libslic3r/Clipper2Utils.cpp +++ /dev/null @@ -1,228 +0,0 @@ -#include "Clipper2Utils.hpp" -#include "libslic3r.h" -#include "clipper2/clipper.h" - -namespace Slic3r { - -//BBS: FIXME -Slic3r::Polylines Paths64_to_polylines(const Clipper2Lib::Paths64& in) -{ - Slic3r::Polylines out; - out.reserve(in.size()); - for (const Clipper2Lib::Path64& path64 : in) { - Slic3r::Points points; - points.reserve(path64.size()); - for (const Clipper2Lib::Point64& point64 : path64) - points.emplace_back(Slic3r::Point(point64.x, point64.y)); - out.emplace_back(Slic3r::Polyline(points)); - } - return out; -} - -//BBS: FIXME -template -Clipper2Lib::Paths64 Slic3rPoints_to_Paths64(const Container& in) -{ - Clipper2Lib::Paths64 out; - out.reserve(in.size()); - for (const auto& item : in) { - Clipper2Lib::Path64 path; - path.reserve(item.size()); - for (const Slic3r::Point& point : item.points) - path.emplace_back(Clipper2Lib::Point64(point.x(), point.y())); - out.emplace_back(std::move(path)); - } - return out; -} - -Clipper2Lib::Paths64 Slic3rPolylines_to_Paths64(const Polylines& in) -{ - return Slic3rPoints_to_Paths64(in); -} - -Points Path64ToPoints(const Clipper2Lib::Path64& path64) -{ - Points points; - points.reserve(path64.size()); - for (const Clipper2Lib::Point64 &point64 : path64) points.emplace_back(Slic3r::Point(point64.x, point64.y)); - return points; -} - -static ExPolygons PolyTreeToExPolygons(Clipper2Lib::PolyTree64 &&polytree) -{ - struct Inner - { - static void PolyTreeToExPolygonsRecursive(Clipper2Lib::PolyTree64 &&polynode, ExPolygons *expolygons) - { - size_t cnt = expolygons->size(); - expolygons->resize(cnt + 1); - (*expolygons)[cnt].contour.points = Path64ToPoints(polynode.Polygon()); - - (*expolygons)[cnt].holes.resize(polynode.Count()); - for (int i = 0; i < polynode.Count(); ++i) { - (*expolygons)[cnt].holes[i].points = Path64ToPoints(polynode[i]->Polygon()); - // Add outer polygons contained by (nested within) holes. - for (int j = 0; j < polynode[i]->Count(); ++j) PolyTreeToExPolygonsRecursive(std::move(*polynode[i]->Child(j)), expolygons); - } - } - - static size_t PolyTreeCountExPolygons(const Clipper2Lib::PolyPath64& polynode) - { - size_t cnt = 1; - for (size_t i = 0; i < polynode.Count(); ++i) { - for (size_t j = 0; j < polynode.Child(i)->Count(); ++j) cnt += PolyTreeCountExPolygons(*polynode.Child(i)->Child(j)); - } - return cnt; - } - }; - - ExPolygons retval; - size_t cnt = 0; - for (int i = 0; i < polytree.Count(); ++i) cnt += Inner::PolyTreeCountExPolygons(*polytree[i]); - retval.reserve(cnt); - for (int i = 0; i < polytree.Count(); ++i) Inner::PolyTreeToExPolygonsRecursive(std::move(*polytree[i]), &retval); - return retval; -} - -void SimplifyPolyTree(const Clipper2Lib::PolyPath64 &polytree, double epsilon, Clipper2Lib::PolyPath64 &result) -{ - for (const auto &child : polytree) { - Clipper2Lib::PolyPath64 *newchild = result.AddChild(Clipper2Lib::SimplifyPath(child->Polygon(), epsilon)); - SimplifyPolyTree(*child, epsilon, *newchild); - } -} - -Clipper2Lib::Paths64 Slic3rPolygons_to_Paths64(const Polygons &in) -{ - Clipper2Lib::Paths64 out; - out.reserve(in.size()); - for (const Polygon &poly : in) { - Clipper2Lib::Path64 path; - path.reserve(poly.points.size()); - for (const Slic3r::Point &point : poly.points) path.emplace_back(Clipper2Lib::Point64(point.x(), point.y())); - out.emplace_back(std::move(path)); - } - return out; -} - -Clipper2Lib::Paths64 Slic3rExPolygons_to_Paths64(const ExPolygons& in) -{ - Clipper2Lib::Paths64 out; - out.reserve(in.size()); - for (const ExPolygon& expolygon : in) { - for (size_t i = 0; i < expolygon.num_contours(); i++) { - const auto &poly = expolygon.contour_or_hole(i); - Clipper2Lib::Path64 path; - path.reserve(poly.points.size()); - for (const Slic3r::Point &point : poly.points) path.emplace_back(Clipper2Lib::Point64(point.x(), point.y())); - out.emplace_back(std::move(path)); - } - } - return out; -} - -Polylines _clipper2_pl_open(Clipper2Lib::ClipType clipType, const Slic3r::Polylines& subject, const Slic3r::Polygons& clip) -{ - Clipper2Lib::Clipper64 c; - c.AddOpenSubject(Slic3rPoints_to_Paths64(subject)); - c.AddClip(Slic3rPoints_to_Paths64(clip)); - - Clipper2Lib::ClipType ct = clipType; - Clipper2Lib::FillRule fr = Clipper2Lib::FillRule::NonZero; - Clipper2Lib::Paths64 solution, solution_open; - c.Execute(ct, fr, solution, solution_open); - - Slic3r::Polylines out; - out.reserve(solution.size() + solution_open.size()); - polylines_append(out, Paths64_to_polylines(solution)); - polylines_append(out, Paths64_to_polylines(solution_open)); - - return out; -} - -Slic3r::Polylines intersection_pl_2(const Slic3r::Polylines& subject, const Slic3r::Polygons& clip) - { return _clipper2_pl_open(Clipper2Lib::ClipType::Intersection, subject, clip); } -Slic3r::Polylines diff_pl_2(const Slic3r::Polylines& subject, const Slic3r::Polygons& clip) - { return _clipper2_pl_open(Clipper2Lib::ClipType::Difference, subject, clip); } - -ExPolygons union_ex_2(const Polygons& polygons) -{ - Clipper2Lib::Clipper64 c; - c.AddSubject(Slic3rPolygons_to_Paths64(polygons)); - - Clipper2Lib::ClipType ct = Clipper2Lib::ClipType::Union; - Clipper2Lib::FillRule fr = Clipper2Lib::FillRule::NonZero; - Clipper2Lib::PolyTree64 solution; - c.Execute(ct, fr, solution); - - ExPolygons results = PolyTreeToExPolygons(std::move(solution)); - - return results; -} - -ExPolygons union_ex_2(const ExPolygons &expolygons) -{ - Clipper2Lib::Clipper64 c; - c.AddSubject(Slic3rExPolygons_to_Paths64(expolygons)); - - Clipper2Lib::ClipType ct = Clipper2Lib::ClipType::Union; - Clipper2Lib::FillRule fr = Clipper2Lib::FillRule::NonZero; - Clipper2Lib::PolyTree64 solution; - c.Execute(ct, fr, solution); - - ExPolygons results = PolyTreeToExPolygons(std::move(solution)); - - return results; -} - -// 对 ExPolygons 进行偏移 -ExPolygons offset_ex_2(const ExPolygons &expolygons, double delta) -{ - Clipper2Lib::Paths64 subject = Slic3rExPolygons_to_Paths64(expolygons); - Clipper2Lib::ClipperOffset offsetter; - offsetter.AddPaths(subject, Clipper2Lib::JoinType::Round, Clipper2Lib::EndType::Polygon); - Clipper2Lib::PolyPath64 polytree; - offsetter.Execute(delta, polytree); - ExPolygons results = PolyTreeToExPolygons(std::move(polytree)); - - return results; -} - -ExPolygons offset_ex_2(const ExPolygons &expolygons, double delta, Clipper2Lib::JoinType joinType) -{ - Clipper2Lib::Paths64 subject = Slic3rExPolygons_to_Paths64(expolygons); - Clipper2Lib::ClipperOffset offsetter; - offsetter.AddPaths(subject, joinType, Clipper2Lib::EndType::Polygon); - Clipper2Lib::PolyPath64 polytree; - offsetter.Execute(delta, polytree); - ExPolygons results = PolyTreeToExPolygons(std::move(polytree)); - - return results; -} - -ExPolygons offset2_ex_2(const ExPolygons& expolygons, double delta1, double delta2) -{ - // 1st offset - Clipper2Lib::Paths64 subject = Slic3rExPolygons_to_Paths64(expolygons); - Clipper2Lib::ClipperOffset offsetter; - offsetter.AddPaths(subject, Clipper2Lib::JoinType::Round, Clipper2Lib::EndType::Polygon); - Clipper2Lib::PolyPath64 polytree; - offsetter.Execute(delta1, polytree); - - // simplify the result - Clipper2Lib::PolyPath64 polytree2; - SimplifyPolyTree(polytree, SCALED_EPSILON, polytree2); - - // 2nd offset - offsetter.Clear(); - offsetter.AddPaths(Clipper2Lib::PolyTreeToPaths64(polytree2), Clipper2Lib::JoinType::Round, Clipper2Lib::EndType::Polygon); - polytree.Clear(); - offsetter.Execute(delta2, polytree); - - // convert back to expolygons - ExPolygons results = PolyTreeToExPolygons(std::move(polytree)); - - return results; -} - -} \ No newline at end of file diff --git a/src/libslic3r/Clipper2Utils.hpp b/src/libslic3r/Clipper2Utils.hpp deleted file mode 100644 index 2b4b0fb2ef..0000000000 --- a/src/libslic3r/Clipper2Utils.hpp +++ /dev/null @@ -1,22 +0,0 @@ -#ifndef slic3r_Clipper2Utils_hpp_ -#define slic3r_Clipper2Utils_hpp_ - -#include "ExPolygon.hpp" -#include "Polygon.hpp" -#include "Polyline.hpp" -#include "clipper2/clipper.h" - -namespace Slic3r { - -Clipper2Lib::Paths64 Slic3rPolylines_to_Paths64(const Slic3r::Polylines& in); -Slic3r::Polylines Paths64_to_polylines(const Clipper2Lib::Paths64& in); -Slic3r::Polylines intersection_pl_2(const Slic3r::Polylines& subject, const Slic3r::Polygons& clip); -Slic3r::Polylines diff_pl_2(const Slic3r::Polylines& subject, const Slic3r::Polygons& clip); -ExPolygons union_ex_2(const Polygons &expolygons); -ExPolygons union_ex_2(const ExPolygons &expolygons); -ExPolygons offset_ex_2(const ExPolygons &expolygons, double delta); -ExPolygons offset_ex_2(const ExPolygons &expolygons, double delta, Clipper2Lib::JoinType joinType); -ExPolygons offset2_ex_2(const ExPolygons &expolygons, double delta1, double delta2); -} - -#endif \ No newline at end of file diff --git a/src/libslic3r/Clipper2ZUtils.hpp b/src/libslic3r/Clipper2ZUtils.hpp deleted file mode 100644 index b0891d1c63..0000000000 --- a/src/libslic3r/Clipper2ZUtils.hpp +++ /dev/null @@ -1,167 +0,0 @@ -#ifndef slic3r_Clipper2ZUtils_hpp_ -#define slic3r_Clipper2ZUtils_hpp_ -#include -#include -#include - -#include -#include -namespace Slic3r { namespace Clipper2ZUtils { - -using ZPoint64 = Clipper2Lib_Z::Point64; -using ZPoints64 = Clipper2Lib_Z::Path64; -using ZPath64 = Clipper2Lib_Z::Path64; -using ZPaths64 = Clipper2Lib_Z::Paths64; - -inline bool zpoint64_lower(const ZPoint64 &l, const ZPoint64 &r) { - return l.x < r.x || (l.x == r.x && (l.y < r.y || (l.y == r.y && l.z < r.z))); -} - -// Convert a single path to zpath with a given Z coordinate. -// If Open, then duplicate the first point at the end. -template -inline ZPath64 to_zpath64(const Points &path, int64_t z) -{ - ZPath64 out; - if (!path.empty()) { - out.reserve(path.size() + (Open ? 1 : 0)); - for (const Point &p : path) out.emplace_back(p.x(), p.y(), z); - if (Open) out.emplace_back(out.front()); - } - return out; -} - -template -inline ZPath64 to_zpath64(const Clipper2Lib_Z::Path64 &path, int64_t z) -{ - ZPath64 out; - if (!path.empty()) { - out.reserve(path.size() + (Open ? 1 : 0)); - for (const Clipper2Lib_Z::Point64 &p : path) out.emplace_back(p.x, p.y, z); - if (Open) out.emplace_back(out.front()); - } - return out; -} - -// Convert multiple paths to zpaths with a given Z coordinate. -template -inline ZPaths64 to_zpaths64(const VecOfPoints &paths, int64_t z) -{ - ZPaths64 out; - out.reserve(paths.size()); - for (const Points &path : paths) out.emplace_back(to_zpath64(path, z)); - return out; -} - -template -inline ZPaths64 to_zpaths64(const Clipper2Lib_Z::Paths64 &paths, int64_t z) -{ - ZPaths64 out; - out.reserve(paths.size()); - for (const Clipper2Lib_Z::Path64 &path : paths) out.emplace_back(to_zpath64(path, z)); - return out; -} - -// Convert multiple expolygons into zpaths with Z specified by index -// offset by base_idx. -template -inline ZPaths64 expolygons_to_zpaths64(const ExPolygons &src, int64_t &base_idx) -{ - ZPaths64 out; - out.reserve(std::accumulate(src.begin(), src.end(), size_t(0), - [](const size_t acc, const ExPolygon &expoly) { return acc + expoly.num_contours(); })); - for (const ExPolygon &expoly : src) { - out.emplace_back(to_zpath64(expoly.contour.points, base_idx)); - for (const Polygon &hole : expoly.holes) - out.emplace_back(to_zpath64(hole.points, base_idx)); - ++base_idx; - } - return out; -} - -// Convert multiple expolygons into zpaths with the same Z. -template -inline ZPaths64 expolygons_to_zpaths64_with_same_z(const ExPolygons &src, int64_t z) -{ - ZPaths64 out; - out.reserve(std::accumulate(src.begin(), src.end(), size_t(0), - [](const size_t acc, const ExPolygon &expoly) { return acc + expoly.num_contours(); })); - for (const ExPolygon &expoly : src) { - out.emplace_back(to_zpath64(expoly.contour.points, z)); - for (const Polygon &hole : expoly.holes) - out.emplace_back(to_zpath64(hole.points, z)); - } - return out; -} - -// Convert a zpath back to 2D Points. -// If Open, then duplicate the first point at the end. -template -inline Points from_zpath64(const ZPath64 &path) -{ - Points out; - if (!path.empty()) { - out.reserve(path.size() + (Open ? 1 : 0)); - for (const ZPoint64 &p : path) out.emplace_back(p.x, p.y); - if (Open) out.emplace_back(out.front()); - } - return out; -} - -// Convert multiple zpaths back to 2D paths. -template -inline void from_zpaths64(const ZPaths64 &paths, VecOfPoints &out) -{ - out.reserve(out.size() + paths.size()); - for (const ZPath64 &path : paths) out.emplace_back(from_zpath64(path)); -} -template -inline VecOfPoints from_zpaths64(const ZPaths64 &paths) -{ - VecOfPoints out; - from_zpaths64(paths, out); - return out; -} - -// Intersection visitor for Clipper2 (zCallback_). -class Clipper2ZIntersectionVisitor -{ -public: - using Intersection = std::pair; - using Intersections = std::vector; - - Clipper2ZIntersectionVisitor(Intersections &intersections) : m_intersections(intersections) {} - - void reset() { m_intersections.clear(); } - - void operator()(const ZPoint64 &e1bot, const ZPoint64 &e1top, const ZPoint64 &e2bot, const ZPoint64 &e2top, ZPoint64 &pt) - { - std::array srcs{e1bot.z, e1top.z, e2bot.z, e2top.z}; - std::sort(srcs.begin(), srcs.end()); - auto it = std::unique(srcs.begin(), srcs.end()); - int new_size = std::distance(srcs.begin(), it); - assert(new_size == 1 || new_size == 2); - if (new_size == 1) { - pt.z = srcs[0]; - } - else if(new_size == 2){ - m_intersections.emplace_back(srcs[0], srcs[1]); - pt.z = -int64_t(m_intersections.size()); - } - } - - auto clipper_callback() - { - return [this](const ZPoint64 &e1bot, const ZPoint64 &e1top, - const ZPoint64 &e2bot, const ZPoint64 &e2top, ZPoint64 &pt) { - return (*this)(e1bot, e1top, e2bot, e2top, pt); }; - } - - const Intersections &intersections() const { return m_intersections; } - -private: - Intersections &m_intersections; -}; - -}} // namespace Slic3r::Clipper2ZUtils -#endif // slic3r_Clipper2ZUtils_hpp_ \ No newline at end of file diff --git a/src/libslic3r/ClipperUtils.cpp b/src/libslic3r/ClipperUtils.cpp index ea19eeb559..88be185f8c 100644 --- a/src/libslic3r/ClipperUtils.cpp +++ b/src/libslic3r/ClipperUtils.cpp @@ -1,3 +1,5 @@ +#include +#include #include #include #include @@ -6,6 +8,8 @@ #include "Geometry.hpp" #include "ShortestPath.hpp" +#include + // #define CLIPPER_UTILS_DEBUG #ifdef CLIPPER_UTILS_DEBUG @@ -25,45 +29,12 @@ namespace Slic3r { -#ifdef CLIPPER_UTILS_DEBUG -// For debugging the Clipper library, for providing bug reports to the Clipper author. -bool export_clipper_input_polygons_bin(const char *path, const ClipperLib::Paths &input_subject, const ClipperLib::Paths &input_clip) -{ - FILE *pfile = fopen(path, "wb"); - if (pfile == NULL) - return false; - - uint32_t sz = uint32_t(input_subject.size()); - fwrite(&sz, 1, sizeof(sz), pfile); - for (size_t i = 0; i < input_subject.size(); ++i) { - const ClipperLib::Path &path = input_subject[i]; - sz = uint32_t(path.size()); - ::fwrite(&sz, 1, sizeof(sz), pfile); - ::fwrite(path.data(), sizeof(ClipperLib::IntPoint), sz, pfile); - } - sz = uint32_t(input_clip.size()); - ::fwrite(&sz, 1, sizeof(sz), pfile); - for (size_t i = 0; i < input_clip.size(); ++i) { - const ClipperLib::Path &path = input_clip[i]; - sz = uint32_t(path.size()); - ::fwrite(&sz, 1, sizeof(sz), pfile); - ::fwrite(path.data(), sizeof(ClipperLib::IntPoint), sz, pfile); - } - ::fclose(pfile); - return true; - -err: - ::fclose(pfile); - return false; -} -#endif /* CLIPPER_UTILS_DEBUG */ - namespace ClipperUtils { Points EmptyPathsProvider::s_empty_points; Points SinglePathProvider::s_end; // Clip source polygon to be used as a clipping polygon with a bouding box around the source (to be clipped) polygon. -// Useful as an optimization for expensive ClipperLib operations, for example when clipping source polygons one by one +// Useful as an optimization for expensive Clipper operations, for example when clipping source polygons one by one // with a set of polygons covering the whole layer below. template inline void clip_clipper_polygon_with_subject_bbox_templ(const PointsType &src, const BoundingBox &bbox, PointsType &out, const bool get_entire_polygons=false) { @@ -170,517 +141,501 @@ void clip_clipper_polygon_with_subject_bbox(const Polygon &src, const BoundingBo } } -static ExPolygons PolyTreeToExPolygons(ClipperLib::PolyTree &&polytree) +namespace C2 = Clipper2Lib; + +namespace { + +C2::ClipType to_c2(ClipType type) { - struct Inner { - static void PolyTreeToExPolygonsRecursive(ClipperLib::PolyNode &&polynode, ExPolygons *expolygons) - { - size_t cnt = expolygons->size(); - expolygons->resize(cnt + 1); - (*expolygons)[cnt].contour.points = std::move(polynode.Contour); - (*expolygons)[cnt].holes.resize(polynode.ChildCount()); - for (int i = 0; i < polynode.ChildCount(); ++ i) { - (*expolygons)[cnt].holes[i].points = std::move(polynode.Childs[i]->Contour); - // Add outer polygons contained by (nested within) holes. - for (int j = 0; j < polynode.Childs[i]->ChildCount(); ++ j) - PolyTreeToExPolygonsRecursive(std::move(*polynode.Childs[i]->Childs[j]), expolygons); - } - } - - static size_t PolyTreeCountExPolygons(const ClipperLib::PolyNode &polynode) - { - size_t cnt = 1; - for (int i = 0; i < polynode.ChildCount(); ++ i) { - for (int j = 0; j < polynode.Childs[i]->ChildCount(); ++ j) - cnt += PolyTreeCountExPolygons(*polynode.Childs[i]->Childs[j]); - } - return cnt; - } - }; - - ExPolygons retval; - size_t cnt = 0; - for (int i = 0; i < polytree.ChildCount(); ++ i) - cnt += Inner::PolyTreeCountExPolygons(*polytree.Childs[i]); - retval.reserve(cnt); - for (int i = 0; i < polytree.ChildCount(); ++ i) - Inner::PolyTreeToExPolygonsRecursive(std::move(*polytree.Childs[i]), &retval); - return retval; + switch (type) { + case ctIntersection: return C2::ClipType::Intersection; + case ctUnion: return C2::ClipType::Union; + case ctDifference: return C2::ClipType::Difference; + default: return C2::ClipType::Xor; + } } -Polylines PolyTreeToPolylines(ClipperLib::PolyTree &&polytree) +C2::FillRule to_c2(PolyFillType type) { - struct Inner { - static void AddPolyNodeToPaths(ClipperLib::PolyNode &polynode, Polylines &out) - { - if (! polynode.Contour.empty()) - out.emplace_back(std::move(polynode.Contour)); - for (ClipperLib::PolyNode *child : polynode.Childs) - AddPolyNodeToPaths(*child, out); - } - }; - - Polylines out; - out.reserve(polytree.Total()); - Inner::AddPolyNodeToPaths(polytree, out); - return out; + switch (type) { + case pftEvenOdd: return C2::FillRule::EvenOdd; + case pftNonZero: return C2::FillRule::NonZero; + case pftPositive: return C2::FillRule::Positive; + default: return C2::FillRule::Negative; + } } -#if 0 -// Global test. -bool has_duplicate_points(const ClipperLib::PolyTree &polytree) +C2::JoinType to_c2(JoinType type) { - struct Helper { - static void collect_points_recursive(const ClipperLib::PolyNode &polynode, ClipperLib::Path &out) { - // For each hole of the current expolygon: - out.insert(out.end(), polynode.Contour.begin(), polynode.Contour.end()); - for (int i = 0; i < polynode.ChildCount(); ++ i) - collect_points_recursive(*polynode.Childs[i], out); - } - }; - ClipperLib::Path pts; - for (int i = 0; i < polytree.ChildCount(); ++ i) - Helper::collect_points_recursive(*polytree.Childs[i], pts); - return has_duplicate_points(std::move(pts)); + switch (type) { + case jtSquare: return C2::JoinType::Square; + case jtRound: return C2::JoinType::Round; + default: return C2::JoinType::Miter; + } } -#else -// Local test inside each of the contours. -bool has_duplicate_points(const ClipperLib::PolyTree &polytree) -{ - struct Helper { - static bool has_duplicate_points_recursive(const ClipperLib::PolyNode &polynode) { - if (has_duplicate_points(polynode.Contour)) - return true; - for (int i = 0; i < polynode.ChildCount(); ++ i) - if (has_duplicate_points_recursive(*polynode.Childs[i])) - return true; - return false; - } - }; - ClipperLib::Path pts; - for (int i = 0; i < polytree.ChildCount(); ++ i) - if (Helper::has_duplicate_points_recursive(*polytree.Childs[i])) - return true; - return false; -} -#endif -// Offset CCW contours outside, CW contours (holes) inside. -// Don't calculate union of the output paths. +C2::EndType to_c2(EndType type) +{ + switch (type) { + case etClosedPolygon: return C2::EndType::Polygon; + case etClosedLine: return C2::EndType::Joined; + case etOpenSquare: return C2::EndType::Square; + case etOpenRound: return C2::EndType::Round; + default: return C2::EndType::Butt; + } +} + +inline int64_t px(const Point &pt) { return pt.x(); } +inline int64_t py(const Point &pt) { return pt.y(); } +inline int64_t px(const C2::Point64 &pt) { return pt.x; } +inline int64_t py(const C2::Point64 &pt) { return pt.y; } + template -static ClipperLib::Paths raw_offset(PathsProvider &&paths, float offset, ClipperLib::JoinType joinType, double miterLimit, ClipperLib::EndType endType = ClipperLib::etClosedPolygon) +C2::Paths64 to_paths64(PathsProvider &&paths) { - ClipperLib::ClipperOffset co; - ClipperLib::Paths out; + C2::Paths64 out; out.reserve(paths.size()); - ClipperLib::Paths out_this; - if (joinType == jtRound) - co.ArcTolerance = miterLimit; - else - co.MiterLimit = miterLimit; - co.ShortestEdgeLength = std::abs(offset * ClipperOffsetShortestEdgeFactor); - for (const ClipperLib::Path &path : paths) { - co.Clear(); - // Execute reorients the contours so that the outer most contour has a positive area. Thus the output - // contours will be CCW oriented even though the input paths are CW oriented. - // Offset is applied after contour reorientation, thus the signum of the offset value is reversed. - co.AddPath(path, joinType, endType); - bool ccw = endType == ClipperLib::etClosedPolygon ? ClipperLib::Orientation(path) : true; - co.Execute(out_this, ccw ? offset : - offset); - if (! ccw) { - // Reverse the resulting contours. - for (ClipperLib::Path &path : out_this) - std::reverse(path.begin(), path.end()); + for (const Points &path : paths) { + C2::Path64 &dst = out.emplace_back(); + dst.reserve(path.size()); + for (const Point &pt : path) + dst.emplace_back(pt.x(), pt.y()); + } + return out; +} + +// Drops vertices closer than `shortest` to the last kept one, like Clipper1's ShortestEdgeLength. +template +bool append_offset_path(C2::Paths64 &out, const PathT &path, double shortest, C2::EndType end) +{ + int high = int(path.size()) - 1; + if (high < 0) + return false; + const double shortest2 = shortest * shortest; + auto same = [shortest2](const auto &a, const auto &b) { + const double dx = double(px(a) - px(b)); + const double dy = double(py(a) - py(b)); + return shortest2 > 0. ? dx * dx + dy * dy < shortest2 : dx == 0. && dy == 0.; + }; + if (end == C2::EndType::Polygon || end == C2::EndType::Joined) + while (high > 0 && same(path[high], path[0])) + -- high; + C2::Path64 dst; + dst.reserve(high + 1); + dst.emplace_back(px(path[0]), py(path[0])); + for (int i = 1, last = 0; i <= high; ++ i) + if (! same(path[i], path[last])) { + dst.emplace_back(px(path[i]), py(path[i])); + last = i; } - append(out, std::move(out_this)); - } + if (end == C2::EndType::Polygon && dst.size() < 3) + return false; + out.emplace_back(std::move(dst)); + return true; +} + +inline Points c2_to_points(const C2::Path64 &path) +{ + Points out; + out.reserve(path.size()); + for (const C2::Point64 &pt : path) + out.emplace_back(pt.x, pt.y); return out; } -// Offset outside by 10um, one by one. -template -static ClipperLib::Paths safety_offset(PathsProvider &&paths) +Polygons c2_to_polygons(const C2::Paths64 &paths) { - return raw_offset(std::forward(paths), ClipperSafetyOffset, DefaultJoinType, DefaultMiterLimit); -} - -template -TResult clipper_do( - const ClipperLib::ClipType clipType, - TSubj && subject, - TClip && clip, - const ClipperLib::PolyFillType fillType) -{ - ClipperLib::Clipper clipper; - clipper.AddPaths(std::forward(subject), ClipperLib::ptSubject, true); - clipper.AddPaths(std::forward(clip), ClipperLib::ptClip, true); - TResult retval; - clipper.Execute(clipType, retval, fillType, fillType); - return retval; -} - -template -TResult clipper_do( - const ClipperLib::ClipType clipType, - TSubj && subject, - TClip && clip, - const ClipperLib::PolyFillType fillType, - const ApplySafetyOffset do_safety_offset) -{ - // Safety offset only allowed on intersection and difference. - assert(do_safety_offset == ApplySafetyOffset::No || clipType != ClipperLib::ctUnion); - return do_safety_offset == ApplySafetyOffset::Yes ? - clipper_do(clipType, std::forward(subject), safety_offset(std::forward(clip)), fillType) : - clipper_do(clipType, std::forward(subject), std::forward(clip), fillType); -} - -template -TResult clipper_union( - TSubj && subject, - // fillType pftNonZero and pftPositive "should" produce the same result for "normalized with implicit union" set of polygons - const ClipperLib::PolyFillType fillType = ClipperLib::pftNonZero) -{ - ClipperLib::Clipper clipper; - clipper.AddPaths(std::forward(subject), ClipperLib::ptSubject, true); - TResult retval; - clipper.Execute(ClipperLib::ctUnion, retval, fillType, fillType); - return retval; -} - -// Perform union of input polygons using the positive rule, convert to ExPolygons. -//FIXME is there any benefit of not doing the boolean / using pftEvenOdd? -inline ExPolygons ClipperPaths_to_Slic3rExPolygons(const ClipperLib::Paths &input, bool do_union) -{ - return PolyTreeToExPolygons(clipper_union(input, do_union ? ClipperLib::pftNonZero : ClipperLib::pftEvenOdd)); -} - -template -static ClipperLib::Paths raw_offset_polyline(PathsProvider &&paths, float offset, ClipperLib::JoinType joinType, double miterLimit, ClipperLib::EndType end_type = ClipperLib::etOpenButt) -{ - assert(offset > 0); - return raw_offset(std::forward(paths), offset, joinType, miterLimit, end_type); -} - -template -static TResult expand_paths(PathsProvider &&paths, float offset, ClipperLib::JoinType joinType, double miterLimit) -{ - // BBS - //assert(offset > 0); - return clipper_union(raw_offset(std::forward(paths), offset, joinType, miterLimit)); -} - -// used by shrink_paths() -template static void remove_outermost_polygon(Container & solution); -template<> void remove_outermost_polygon(ClipperLib::Paths &solution) - { if (! solution.empty()) solution.erase(solution.begin()); } -template<> void remove_outermost_polygon(ClipperLib::PolyTree &solution) - { solution.RemoveOutermostPolygon(); } - -template -static TResult shrink_paths(PathsProvider &&paths, float offset, ClipperLib::JoinType joinType, double miterLimit) -{ - // BBS - //assert(offset > 0); - TResult out; - if (auto raw = raw_offset(std::forward(paths), - offset, joinType, miterLimit); ! raw.empty()) { - ClipperLib::Clipper clipper; - clipper.AddPaths(raw, ClipperLib::ptSubject, true); - ClipperLib::IntRect r = clipper.GetBounds(); - clipper.AddPath({ { r.left - 10, r.bottom + 10 }, { r.right + 10, r.bottom + 10 }, { r.right + 10, r.top - 10 }, { r.left - 10, r.top - 10 } }, ClipperLib::ptSubject, true); - clipper.ReverseSolution(true); - clipper.Execute(ClipperLib::ctUnion, out, ClipperLib::pftNegative, ClipperLib::pftNegative); - remove_outermost_polygon(out); - } + Polygons out; + out.reserve(paths.size()); + for (const C2::Path64 &path : paths) + out.emplace_back().points = c2_to_points(path); return out; } -template -static TResult offset_paths(PathsProvider &&paths, float offset, ClipperLib::JoinType joinType, double miterLimit) +Polylines c2_to_polylines(const C2::Paths64 &paths) { - // BBS - //assert(offset != 0); - - return offset > 0 ? - expand_paths(std::forward(paths), offset, joinType, miterLimit) : - shrink_paths(std::forward(paths), - offset, joinType, miterLimit); + Polylines out; + out.reserve(paths.size()); + for (const C2::Path64 &path : paths) + out.emplace_back(c2_to_points(path)); + return out; } -Slic3r::Polygons offset(const Slic3r::Polygon &polygon, const float delta, ClipperLib::JoinType joinType, double miterLimit) - { return to_polygons(raw_offset(ClipperUtils::SinglePathProvider(polygon.points), delta, joinType, miterLimit)); } +size_t c2_count_expolygons(const C2::PolyPath64 &outer) +{ + size_t cnt = 1; + for (const auto &hole : outer) + for (const auto &island : *hole) + cnt += c2_count_expolygons(*island); + return cnt; +} -Slic3r::Polygons offset(const Slic3r::Polygons &polygons, const float delta, ClipperLib::JoinType joinType, double miterLimit) - { return to_polygons(offset_paths(ClipperUtils::PolygonsProvider(polygons), delta, joinType, miterLimit)); } -Slic3r::ExPolygons offset_ex(const Slic3r::Polygons &polygons, const float delta, ClipperLib::JoinType joinType, double miterLimit) - { return PolyTreeToExPolygons(offset_paths(ClipperUtils::PolygonsProvider(polygons), delta, joinType, miterLimit)); } +void c2_append_expolygons(const C2::PolyPath64 &outer, ExPolygons &out) +{ + ExPolygon &expoly = out.emplace_back(); + expoly.contour.points = c2_to_points(outer.Polygon()); + expoly.holes.reserve(outer.Count()); + for (const auto &hole : outer) + expoly.holes.emplace_back().points = c2_to_points(hole->Polygon()); + // Islands inside the holes. + for (const auto &hole : outer) + for (const auto &island : *hole) + c2_append_expolygons(*island, out); +} -Slic3r::Polygons offset(const Slic3r::Polyline &polyline, const float delta, ClipperLib::JoinType joinType, double miterLimit, ClipperLib::EndType end_type) - { assert(delta > 0); return to_polygons(clipper_union(raw_offset_polyline(ClipperUtils::SinglePathProvider(polyline.points), delta, joinType, miterLimit, end_type))); } +ExPolygons c2_to_expolygons(const C2::PolyTree64 &tree) +{ + size_t cnt = 0; + for (const auto &outer : tree) + cnt += c2_count_expolygons(*outer); + ExPolygons out; + out.reserve(cnt); + for (const auto &outer : tree) + c2_append_expolygons(*outer, out); + return out; +} -Slic3r::Polygons offset(const Slic3r::Polyline3 &polyline, const float delta, ClipperLib::JoinType joinType, double miterLimit, ClipperLib::EndType end_type) +template +void c2_clip(C2::ClipType type, const C2::Paths64 &subject, const C2::Paths64 &clip, C2::FillRule fill, TOut &out) +{ + C2::Clipper64 clipper; + // Clipper2 keeps collinear vertices by default, Clipper1 removed them. + clipper.PreserveCollinear(false); + clipper.AddSubject(subject); + if (! clip.empty()) + clipper.AddClip(clip); + clipper.Execute(type, fill, out); +} + +Polylines c2_clip_open(C2::ClipType type, const C2::Paths64 &subject, const C2::Paths64 &clip) +{ + C2::Clipper64 clipper; + clipper.PreserveCollinear(false); + clipper.AddOpenSubject(subject); + if (! clip.empty()) + clipper.AddClip(clip); + C2::Paths64 closed, open; + clipper.Execute(type, C2::FillRule::NonZero, closed, open); + return c2_to_polylines(open); +} + +// Clipper1 semantics: miter limit at least 2, arc tolerance (also for round caps) at most a quarter of the offset. +void c2_offset_setup(C2::ClipperOffset &co, float delta, JoinType joinType, double miterLimit) +{ + const double max_arc_tolerance = std::abs(delta) * 0.25; + if (joinType == jtRound) + co.ArcTolerance(miterLimit > 0. ? std::min(miterLimit, max_arc_tolerance) : 0.25); + else { + co.MiterLimit(std::max(miterLimit, 2.)); + co.ArcTolerance(std::min(0.25, max_arc_tolerance)); + } +} + +// A single group offsets like Clipper1's path by path offset only if every CW path is a hole of a CCW path. +bool c2_offset_as_group(const C2::Paths64 &paths, const std::vector &areas) +{ + std::vector contours; + for (size_t i = 0; i < paths.size(); ++ i) + if (areas[i] > 0.) + contours.emplace_back(C2::GetBounds(paths[i])); + for (size_t i = 0; i < paths.size(); ++ i) + if (areas[i] < 0.) { + const C2::Rect64 hole = C2::GetBounds(paths[i]); + if (std::none_of(contours.begin(), contours.end(), [&hole](const C2::Rect64 &r) { + return r.left < hole.left && r.right > hole.right && r.top < hole.top && r.bottom > hole.bottom; + })) + return false; + } + return true; +} + +template +void c2_offset_closed_paths(C2::Paths64 &&paths, float delta, JoinType joinType, double miterLimit, TOut &out) +{ + std::vector areas; + areas.reserve(paths.size()); + for (const C2::Path64 &path : paths) + areas.emplace_back(C2::Area(path)); + C2::ClipperOffset co; + c2_offset_setup(co, delta, joinType, miterLimit); + if (c2_offset_as_group(paths, areas)) { + // Clipper2 would grow degenerate paths even for a negative offset. + if (delta < 0. && std::all_of(areas.begin(), areas.end(), [](double a) { return a == 0.; })) + return; + co.AddPaths(paths, to_c2(joinType), C2::EndType::Polygon); + co.Execute(delta, out); + return; + } + C2::Paths64 offsetted, single; + for (size_t i = 0; i < paths.size(); ++ i) { + if (areas[i] == 0. && delta < 0.) + continue; + co.Clear(); + co.AddPath(paths[i], to_c2(joinType), C2::EndType::Polygon); + // Clipper2 reverses a lone CW path, so the flipped sign shrinks it as a hole. + co.Execute(areas[i] < 0. ? - delta : delta, single); + append(offsetted, std::move(single)); + } + c2_clip(C2::ClipType::Union, offsetted, {}, delta > 0. ? C2::FillRule::NonZero : C2::FillRule::Positive, out); +} + +template +void c2_offset_closed(PathsT &&src, float delta, JoinType joinType, double miterLimit, TOut &out) +{ + const double shortest = std::abs(delta * ClipperOffsetShortestEdgeFactor); + C2::Paths64 paths; + paths.reserve(src.size()); + for (const auto &path : src) + append_offset_path(paths, path, shortest, C2::EndType::Polygon); + c2_offset_closed_paths(std::move(paths), delta, joinType, miterLimit, out); +} + +template +void c2_offset2_closed(PathsProvider &&src, float delta1, float delta2, JoinType joinType, double miterLimit, TOut &out) +{ + C2::Paths64 first; + c2_offset_closed(std::forward(src), delta1, joinType, miterLimit, first); + c2_offset_closed(first, delta2, joinType, miterLimit, out); +} + +inline const ExPolygon& expolygon_of(const ExPolygon &expoly) { return expoly; } +inline const ExPolygon& expolygon_of(const ExPolygon *expoly) { return *expoly; } +inline const ExPolygon& expolygon_of(const Surface &surface) { return surface.expolygon; } +inline const ExPolygon& expolygon_of(const Surface *surface) { return surface->expolygon; } + +// Clipper1 ignored the orientation of expolygons, a single group needs CCW contours and CW holes. +template +void c2_offset_expolygons(const ExPolygonRange &src, float delta, JoinType joinType, double miterLimit, TOut &out) +{ + const double shortest = std::abs(delta * ClipperOffsetShortestEdgeFactor); + C2::Paths64 paths; + for (const auto &item : src) { + const ExPolygon &expoly = expolygon_of(item); + if (! append_offset_path(paths, expoly.contour.points, shortest, C2::EndType::Polygon)) + continue; + if (const double area = C2::Area(paths.back()); area == 0.) { + // A degenerate contour vanishes when shrunk and encloses no hole when grown. + if (delta < 0.) + paths.pop_back(); + continue; + } else if (area < 0.) + std::reverse(paths.back().begin(), paths.back().end()); + for (const Polygon &hole : expoly.holes) + if (append_offset_path(paths, hole.points, shortest, C2::EndType::Polygon)) { + if (const double area = C2::Area(paths.back()); area == 0.) + paths.pop_back(); + else if (area > 0.) + std::reverse(paths.back().begin(), paths.back().end()); + } + } + C2::ClipperOffset co; + c2_offset_setup(co, delta, joinType, miterLimit); + co.AddPaths(paths, to_c2(joinType), C2::EndType::Polygon); + co.Execute(delta, out); +} + +template +Polygons c2_expolygons_offset(const ExPolygonRange &src, float delta, JoinType joinType, double miterLimit) +{ + C2::Paths64 out; + c2_offset_expolygons(src, delta, joinType, miterLimit, out); + return c2_to_polygons(out); +} + +template +ExPolygons c2_expolygons_offset_ex(const ExPolygonRange &src, float delta, JoinType joinType, double miterLimit) +{ + C2::PolyTree64 out; + c2_offset_expolygons(src, delta, joinType, miterLimit, out); + return c2_to_expolygons(out); +} + +template +Polygons c2_offset_lines(PathsProvider &&src, float delta, JoinType joinType, double miterLimit, EndType endType) +{ + C2::Paths64 out; + if (endType == etClosedPolygon) { + c2_offset_closed(std::forward(src), delta, joinType, miterLimit, out); + return c2_to_polygons(out); + } + const C2::EndType end = to_c2(endType); + const double shortest = std::abs(delta * ClipperOffsetShortestEdgeFactor); + C2::Paths64 paths; + paths.reserve(src.size()); + for (const Points &path : src) + append_offset_path(paths, path, shortest, end); + C2::ClipperOffset co; + c2_offset_setup(co, delta, joinType, miterLimit); + co.AddPaths(paths, to_c2(joinType), end); + co.Execute(delta, out); + return c2_to_polygons(out); +} + +template +C2::Paths64 c2_clip_paths(PathsProvider &&clip, ApplySafetyOffset do_safety_offset) +{ + if (do_safety_offset == ApplySafetyOffset::No) + return to_paths64(std::forward(clip)); + C2::Paths64 out; + c2_offset_closed(std::forward(clip), ClipperSafetyOffset, DefaultJoinType, DefaultMiterLimit, out); + return out; +} + +} // namespace + +Slic3r::Polygons offset(const Slic3r::Polygon &polygon, const float delta, JoinType joinType, double miterLimit) +{ + C2::Paths64 paths; + if (! append_offset_path(paths, polygon.points, std::abs(delta * ClipperOffsetShortestEdgeFactor), C2::EndType::Polygon)) + return {}; + const double area = C2::Area(paths.front()); + if (area == 0. && delta < 0.) + return {}; + C2::ClipperOffset co; + c2_offset_setup(co, delta, joinType, miterLimit); + co.AddPaths(paths, to_c2(joinType), C2::EndType::Polygon); + C2::Paths64 out; + // A CW polygon shrinks as a hole and stays CW. + co.Execute(area < 0. ? - delta : delta, out); + return c2_to_polygons(out); +} + +Slic3r::Polygons offset(const Slic3r::Polygons &polygons, const float delta, JoinType joinType, double miterLimit) +{ + C2::Paths64 out; + c2_offset_closed(ClipperUtils::PolygonsProvider(polygons), delta, joinType, miterLimit, out); + return c2_to_polygons(out); +} +Slic3r::ExPolygons offset_ex(const Slic3r::Polygons &polygons, const float delta, JoinType joinType, double miterLimit) +{ + C2::PolyTree64 out; + c2_offset_closed(ClipperUtils::PolygonsProvider(polygons), delta, joinType, miterLimit, out); + return c2_to_expolygons(out); +} + +Slic3r::Polygons offset(const Slic3r::Polyline &polyline, const float delta, JoinType joinType, double miterLimit, EndType end_type) + { assert(delta > 0); return c2_offset_lines(ClipperUtils::SinglePathProvider(polyline.points), delta, joinType, miterLimit, end_type); } + +Slic3r::Polygons offset(const Slic3r::Polyline3 &polyline, const float delta, JoinType joinType, double miterLimit, EndType end_type) { assert(delta > 0); - return to_polygons( - clipper_union( - raw_offset_polyline( - ClipperUtils::SinglePathProvider(polyline.to_polyline().points), - delta, - joinType, - miterLimit, - end_type))); + return c2_offset_lines(ClipperUtils::SinglePathProvider(polyline.to_polyline().points), delta, joinType, miterLimit, end_type); } -Slic3r::Polygons offset(const Slic3r::Polylines &polylines, const float delta, ClipperLib::JoinType joinType, double miterLimit, ClipperLib::EndType end_type) - { assert(delta > 0); return to_polygons(clipper_union(raw_offset_polyline(ClipperUtils::PolylinesProvider(polylines), delta, joinType, miterLimit, end_type))); } +Slic3r::Polygons offset(const Slic3r::Polylines &polylines, const float delta, JoinType joinType, double miterLimit, EndType end_type) + { assert(delta > 0); return c2_offset_lines(ClipperUtils::PolylinesProvider(polylines), delta, joinType, miterLimit, end_type); } -Polygons contour_to_polygons(const Polygon &polygon, const float line_width, ClipperLib::JoinType join_type, double miter_limit){ - assert(line_width > 1.f); return to_polygons(clipper_union( - raw_offset(ClipperUtils::SinglePathProvider(polygon.points), line_width/2, join_type, miter_limit, ClipperLib::etClosedLine)));} -Polygons contour_to_polygons(const Polygons &polygons, const float line_width, ClipperLib::JoinType join_type, double miter_limit){ - assert(line_width > 1.f); return to_polygons(clipper_union( - raw_offset(ClipperUtils::PolygonsProvider(polygons), line_width/2, join_type, miter_limit, ClipperLib::etClosedLine)));} +Polygons contour_to_polygons(const Polygon &polygon, const float line_width, JoinType join_type, double miter_limit) + { assert(line_width > 1.f); return c2_offset_lines(ClipperUtils::SinglePathProvider(polygon.points), line_width / 2, join_type, miter_limit, etClosedLine); } +Polygons contour_to_polygons(const Polygons &polygons, const float line_width, JoinType join_type, double miter_limit) + { assert(line_width > 1.f); return c2_offset_lines(ClipperUtils::PolygonsProvider(polygons), line_width / 2, join_type, miter_limit, etClosedLine); } -// returns number of expolygons collected (0 or 1). -static int offset_expolygon_inner(const Slic3r::ExPolygon &expoly, const float delta, ClipperLib::JoinType joinType, double miterLimit, ClipperLib::Paths &out) +Slic3r::Polygons offset(const Slic3r::ExPolygon &expolygon, const float delta, JoinType joinType, double miterLimit) + { return c2_expolygons_offset(std::array{ &expolygon }, delta, joinType, miterLimit); } +Slic3r::Polygons offset(const Slic3r::ExPolygons &expolygons, const float delta, JoinType joinType, double miterLimit) + { return c2_expolygons_offset(expolygons, delta, joinType, miterLimit); } +Slic3r::Polygons offset(const Slic3r::Surfaces &surfaces, const float delta, JoinType joinType, double miterLimit) + { return c2_expolygons_offset(surfaces, delta, joinType, miterLimit); } +Slic3r::Polygons offset(const Slic3r::SurfacesPtr &surfaces, const float delta, JoinType joinType, double miterLimit) + { return c2_expolygons_offset(surfaces, delta, joinType, miterLimit); } +Slic3r::ExPolygons offset_ex(const Slic3r::ExPolygon &expolygon, const float delta, JoinType joinType, double miterLimit) + { return c2_expolygons_offset_ex(std::array{ &expolygon }, delta, joinType, miterLimit); } +Slic3r::ExPolygons offset_ex(const Slic3r::ExPolygons &expolygons, const float delta, JoinType joinType, double miterLimit) + { return c2_expolygons_offset_ex(expolygons, delta, joinType, miterLimit); } +Slic3r::ExPolygons offset_ex(const Slic3r::Surfaces &surfaces, const float delta, JoinType joinType, double miterLimit) + { return c2_expolygons_offset_ex(surfaces, delta, joinType, miterLimit); } +Slic3r::ExPolygons offset_ex(const Slic3r::SurfacesPtr &surfaces, const float delta, JoinType joinType, double miterLimit) + { return c2_expolygons_offset_ex(surfaces, delta, joinType, miterLimit); } + +Polygons offset2(const ExPolygons &expolygons, const float delta1, const float delta2, JoinType joinType, double miterLimit) { - // 1) Offset the outer contour. - ClipperLib::Paths contours; - { - ClipperLib::ClipperOffset co; - if (joinType == jtRound) - co.ArcTolerance = miterLimit; - else - co.MiterLimit = miterLimit; - co.ShortestEdgeLength = std::abs(delta * ClipperOffsetShortestEdgeFactor); - co.AddPath(expoly.contour.points, joinType, ClipperLib::etClosedPolygon); - co.Execute(contours, delta); - } - if (contours.empty()) - // No need to try to offset the holes. - return 0; - - if (expoly.holes.empty()) { - // No need to subtract holes from the offsetted expolygon, we are done. - append(out, std::move(contours)); - } else { - // 2) Offset the holes one by one, collect the offsetted holes. - ClipperLib::Paths holes; - { - for (const Polygon &hole : expoly.holes) { - ClipperLib::ClipperOffset co; - if (joinType == jtRound) - co.ArcTolerance = miterLimit; - else - co.MiterLimit = miterLimit; - co.ShortestEdgeLength = std::abs(delta * ClipperOffsetShortestEdgeFactor); - co.AddPath(hole.points, joinType, ClipperLib::etClosedPolygon); - ClipperLib::Paths out2; - // Execute reorients the contours so that the outer most contour has a positive area. Thus the output - // contours will be CCW oriented even though the input paths are CW oriented. - // Offset is applied after contour reorientation, thus the signum of the offset value is reversed. - co.Execute(out2, - delta); - append(holes, std::move(out2)); - } - } - - // 3) Subtract holes from the contours. - if (holes.empty()) { - // No hole remaining after an offset. Just copy the outer contour. - append(out, std::move(contours)); - } else if (delta < 0) { - // Negative offset. There is a chance, that the offsetted hole intersects the outer contour. - // Subtract the offsetted holes from the offsetted contours. - if (auto output = clipper_do(ClipperLib::ctDifference, contours, holes, ClipperLib::pftNonZero); ! output.empty()) { - append(out, std::move(output)); - } else { - // The offsetted holes have eaten up the offsetted outer contour. - return 0; - } - } else { - // Positive offset. As long as the Clipper offset does what one expects it to do, the offsetted hole will have a smaller - // area than the original hole or even disappear, therefore there will be no new intersections. - // Just collect the reversed holes. - out.reserve(contours.size() + holes.size()); - append(out, std::move(contours)); - // Reverse the holes in place. - for (size_t i = 0; i < holes.size(); ++ i) - std::reverse(holes[i].begin(), holes[i].end()); - append(out, std::move(holes)); - } - } - - return 1; + C2::Paths64 first, out; + c2_offset_expolygons(expolygons, delta1, joinType, miterLimit, first); + c2_offset_closed(first, delta2, joinType, miterLimit, out); + return c2_to_polygons(out); } - -static int offset_expolygon_inner(const Slic3r::Surface &surface, const float delta, ClipperLib::JoinType joinType, double miterLimit, ClipperLib::Paths &out) - { return offset_expolygon_inner(surface.expolygon, delta, joinType, miterLimit, out); } -static int offset_expolygon_inner(const Slic3r::Surface *surface, const float delta, ClipperLib::JoinType joinType, double miterLimit, ClipperLib::Paths &out) - { return offset_expolygon_inner(surface->expolygon, delta, joinType, miterLimit, out); } - -ClipperLib::Paths expolygon_offset(const Slic3r::ExPolygon &expolygon, const float delta, ClipperLib::JoinType joinType, double miterLimit) +ExPolygons offset2_ex(const ExPolygons &expolygons, const float delta1, const float delta2, JoinType joinType, double miterLimit) { - ClipperLib::Paths out; - offset_expolygon_inner(expolygon, delta, joinType, miterLimit, out); - return out; + C2::Paths64 first; + C2::PolyTree64 out; + c2_offset_expolygons(expolygons, delta1, joinType, miterLimit, first); + c2_offset_closed(first, delta2, joinType, miterLimit, out); + return c2_to_expolygons(out); } - -// This is a safe variant of the polygons offset, tailored for multiple ExPolygons. -// It is required, that the input expolygons do not overlap and that the holes of each ExPolygon don't intersect with their respective outer contours. -// Each ExPolygon is offsetted separately. For outer offset, the offsetted ExPolygons shall be united outside of this function. -template -static std::pair expolygons_offset_raw(const ExPolygonVector &expolygons, const float delta, ClipperLib::JoinType joinType, double miterLimit) +ExPolygons offset2_ex(const Surfaces &surfaces, const float delta1, const float delta2, JoinType joinType, double miterLimit) { - // Offsetted ExPolygons before they are united. - ClipperLib::Paths output; - output.reserve(expolygons.size()); - // How many non-empty offsetted expolygons were actually collected into output? - // If only one, then there is no need to do a final union. - size_t expolygons_collected = 0; - for (const auto &expoly : expolygons) - expolygons_collected += offset_expolygon_inner(expoly, delta, joinType, miterLimit, output); - return std::make_pair(std::move(output), expolygons_collected); -} - -// See comment on expolygon_offsets_raw. In addition, for positive offset the contours are united. -template -static ClipperLib::Paths expolygons_offset(const ExPolygonVector &expolygons, const float delta, ClipperLib::JoinType joinType, double miterLimit) -{ - auto [output, expolygons_collected] = expolygons_offset_raw(expolygons, delta, joinType, miterLimit); - // Unite the offsetted expolygons. - return expolygons_collected > 1 && delta > 0 ? - // There is a chance that the outwards offsetted expolygons may intersect. Perform a union. - clipper_union(output) : - // Negative offset. The shrunk expolygons shall not mutually intersect. Just copy the output. - output; -} - -// See comment on expolygons_offset_raw. In addition, the polygons are always united to conver to polytree. -template -static ClipperLib::PolyTree expolygons_offset_pt(const ExPolygonVector &expolygons, const float delta, ClipperLib::JoinType joinType, double miterLimit) -{ - auto [output, expolygons_collected] = expolygons_offset_raw(expolygons, delta, joinType, miterLimit); - // Unite the offsetted expolygons for both the - return clipper_union(output); -} - -Slic3r::Polygons offset(const Slic3r::ExPolygon &expolygon, const float delta, ClipperLib::JoinType joinType, double miterLimit) - { return to_polygons(expolygon_offset(expolygon, delta, joinType, miterLimit)); } -Slic3r::Polygons offset(const Slic3r::ExPolygons &expolygons, const float delta, ClipperLib::JoinType joinType, double miterLimit) - { return to_polygons(expolygons_offset(expolygons, delta, joinType, miterLimit)); } -Slic3r::Polygons offset(const Slic3r::Surfaces &surfaces, const float delta, ClipperLib::JoinType joinType, double miterLimit) - { return to_polygons(expolygons_offset(surfaces, delta, joinType, miterLimit)); } -Slic3r::Polygons offset(const Slic3r::SurfacesPtr &surfaces, const float delta, ClipperLib::JoinType joinType, double miterLimit) - { return to_polygons(expolygons_offset(surfaces, delta, joinType, miterLimit)); } -Slic3r::ExPolygons offset_ex(const Slic3r::ExPolygon &expolygon, const float delta, ClipperLib::JoinType joinType, double miterLimit) - //FIXME one may spare one Clipper Union call. - { return ClipperPaths_to_Slic3rExPolygons(expolygon_offset(expolygon, delta, joinType, miterLimit)); } -Slic3r::ExPolygons offset_ex(const Slic3r::ExPolygons &expolygons, const float delta, ClipperLib::JoinType joinType, double miterLimit) - { return PolyTreeToExPolygons(expolygons_offset_pt(expolygons, delta, joinType, miterLimit)); } -Slic3r::ExPolygons offset_ex(const Slic3r::Surfaces &surfaces, const float delta, ClipperLib::JoinType joinType, double miterLimit) - { return PolyTreeToExPolygons(expolygons_offset_pt(surfaces, delta, joinType, miterLimit)); } -Slic3r::ExPolygons offset_ex(const Slic3r::SurfacesPtr &surfaces, const float delta, ClipperLib::JoinType joinType, double miterLimit) - { return PolyTreeToExPolygons(expolygons_offset_pt(surfaces, delta, joinType, miterLimit)); } - -Polygons offset2(const ExPolygons &expolygons, const float delta1, const float delta2, ClipperLib::JoinType joinType, double miterLimit) -{ - return to_polygons(offset_paths(expolygons_offset(expolygons, delta1, joinType, miterLimit), delta2, joinType, miterLimit)); -} -ExPolygons offset2_ex(const ExPolygons &expolygons, const float delta1, const float delta2, ClipperLib::JoinType joinType, double miterLimit) -{ - return PolyTreeToExPolygons(offset_paths(expolygons_offset(expolygons, delta1, joinType, miterLimit), delta2, joinType, miterLimit)); -} -ExPolygons offset2_ex(const Surfaces &surfaces, const float delta1, const float delta2, ClipperLib::JoinType joinType, double miterLimit) -{ - //FIXME it may be more efficient to offset to_expolygons(surfaces) instead of to_polygons(surfaces). - return PolyTreeToExPolygons(offset_paths(expolygons_offset(surfaces, delta1, joinType, miterLimit), delta2, joinType, miterLimit)); + C2::Paths64 first; + C2::PolyTree64 out; + c2_offset_expolygons(surfaces, delta1, joinType, miterLimit, first); + c2_offset_closed(first, delta2, joinType, miterLimit, out); + return c2_to_expolygons(out); } // Offset outside, then inside produces morphological closing. All deltas should be positive. -Slic3r::Polygons closing(const Slic3r::Polygons &polygons, const float delta1, const float delta2, ClipperLib::JoinType joinType, double miterLimit) +Slic3r::Polygons closing(const Slic3r::Polygons &polygons, const float delta1, const float delta2, JoinType joinType, double miterLimit) { assert(delta1 > 0); assert(delta2 > 0); - return to_polygons(shrink_paths(expand_paths(ClipperUtils::PolygonsProvider(polygons), delta1, joinType, miterLimit), delta2, joinType, miterLimit)); + C2::Paths64 out; + c2_offset2_closed(ClipperUtils::PolygonsProvider(polygons), delta1, - delta2, joinType, miterLimit, out); + return c2_to_polygons(out); } -Slic3r::ExPolygons closing_ex(const Slic3r::Polygons &polygons, const float delta1, const float delta2, ClipperLib::JoinType joinType, double miterLimit) +Slic3r::ExPolygons closing_ex(const Slic3r::Polygons &polygons, const float delta1, const float delta2, JoinType joinType, double miterLimit) { assert(delta1 > 0); assert(delta2 > 0); - return PolyTreeToExPolygons(shrink_paths(expand_paths(ClipperUtils::PolygonsProvider(polygons), delta1, joinType, miterLimit), delta2, joinType, miterLimit)); + C2::PolyTree64 out; + c2_offset2_closed(ClipperUtils::PolygonsProvider(polygons), delta1, - delta2, joinType, miterLimit, out); + return c2_to_expolygons(out); } -Slic3r::ExPolygons closing_ex(const Slic3r::Surfaces &surfaces, const float delta1, const float delta2, ClipperLib::JoinType joinType, double miterLimit) +Slic3r::ExPolygons closing_ex(const Slic3r::Surfaces &surfaces, const float delta1, const float delta2, JoinType joinType, double miterLimit) { assert(delta1 > 0); assert(delta2 > 0); - //FIXME it may be more efficient to offset to_expolygons(surfaces) instead of to_polygons(surfaces). - return PolyTreeToExPolygons(shrink_paths(expand_paths(ClipperUtils::SurfacesProvider(surfaces), delta1, joinType, miterLimit), delta2, joinType, miterLimit)); + C2::PolyTree64 out; + c2_offset2_closed(ClipperUtils::SurfacesProvider(surfaces), delta1, - delta2, joinType, miterLimit, out); + return c2_to_expolygons(out); } // Offset inside, then outside produces morphological opening. All deltas should be positive. -Slic3r::Polygons opening(const Slic3r::Polygons &polygons, const float delta1, const float delta2, ClipperLib::JoinType joinType, double miterLimit) +Slic3r::Polygons opening(const Slic3r::Polygons &polygons, const float delta1, const float delta2, JoinType joinType, double miterLimit) { assert(delta1 > 0); assert(delta2 > 0); - return to_polygons(expand_paths(shrink_paths(ClipperUtils::PolygonsProvider(polygons), delta1, joinType, miterLimit), delta2, joinType, miterLimit)); + C2::Paths64 out; + c2_offset2_closed(ClipperUtils::PolygonsProvider(polygons), - delta1, delta2, joinType, miterLimit, out); + return c2_to_polygons(out); } -Slic3r::Polygons opening(const Slic3r::ExPolygons &expolygons, const float delta1, const float delta2, ClipperLib::JoinType joinType, double miterLimit) +Slic3r::Polygons opening(const Slic3r::ExPolygons &expolygons, const float delta1, const float delta2, JoinType joinType, double miterLimit) { assert(delta1 > 0); assert(delta2 > 0); - return to_polygons(expand_paths(shrink_paths(ClipperUtils::ExPolygonsProvider(expolygons), delta1, joinType, miterLimit), delta2, joinType, miterLimit)); + C2::Paths64 out; + c2_offset2_closed(ClipperUtils::ExPolygonsProvider(expolygons), - delta1, delta2, joinType, miterLimit, out); + return c2_to_polygons(out); } -Slic3r::Polygons opening(const Slic3r::Surfaces &surfaces, const float delta1, const float delta2, ClipperLib::JoinType joinType, double miterLimit) +Slic3r::Polygons opening(const Slic3r::Surfaces &surfaces, const float delta1, const float delta2, JoinType joinType, double miterLimit) { assert(delta1 > 0); assert(delta2 > 0); - //FIXME it may be more efficient to offset to_expolygons(surfaces) instead of to_polygons(surfaces). - return to_polygons(expand_paths(shrink_paths(ClipperUtils::SurfacesProvider(surfaces), delta1, joinType, miterLimit), delta2, joinType, miterLimit)); -} - -// Fix of #117: A large fractal pyramid takes ages to slice -// The Clipper library has difficulties processing overlapping polygons. -// Namely, the function ClipperLib::JoinCommonEdges() has potentially a terrible time complexity if the output -// of the operation is of the PolyTree type. -// This function implemenets a following workaround: -// 1) Peform the Clipper operation with the output to Paths. This method handles overlaps in a reasonable time. -// 2) Run Clipper Union once again to extract the PolyTree from the result of 1). -template -inline ClipperLib::PolyTree clipper_do_polytree( - const ClipperLib::ClipType clipType, - PathProvider1 &&subject, - PathProvider2 &&clip, - const ClipperLib::PolyFillType fillType) -{ - // Perform the operation with the output to input_subject. - // This pass does not generate a PolyTree, which is a very expensive operation with the current Clipper library - // if there are overapping edges. - if (auto output = clipper_do(clipType, subject, clip, fillType); ! output.empty()) - // Perform an additional Union operation to generate the PolyTree ordering. - return clipper_union(output, fillType); - return ClipperLib::PolyTree(); -} -template -inline ClipperLib::PolyTree clipper_do_polytree( - const ClipperLib::ClipType clipType, - PathProvider1 &&subject, - PathProvider2 &&clip, - const ClipperLib::PolyFillType fillType, - const ApplySafetyOffset do_safety_offset) -{ - assert(do_safety_offset == ApplySafetyOffset::No || clipType != ClipperLib::ctUnion); - return do_safety_offset == ApplySafetyOffset::Yes ? - clipper_do_polytree(clipType, std::forward(subject), safety_offset(std::forward(clip)), fillType) : - clipper_do_polytree(clipType, std::forward(subject), std::forward(clip), fillType); + C2::Paths64 out; + c2_offset2_closed(ClipperUtils::SurfacesProvider(surfaces), - delta1, delta2, joinType, miterLimit, out); + return c2_to_polygons(out); } template -static inline Polygons _clipper(ClipperLib::ClipType clipType, TSubj &&subject, TClip &&clip, ApplySafetyOffset do_safety_offset) +static inline Polygons _clipper(ClipType clipType, TSubj &&subject, TClip &&clip, ApplySafetyOffset do_safety_offset) { - return to_polygons(clipper_do(clipType, std::forward(subject), std::forward(clip), ClipperLib::pftNonZero, do_safety_offset)); + // Safety offset only allowed on intersection and difference. + assert(do_safety_offset == ApplySafetyOffset::No || clipType != ctUnion); + C2::Paths64 out; + c2_clip(to_c2(clipType), to_paths64(std::forward(subject)), c2_clip_paths(std::forward(clip), do_safety_offset), C2::FillRule::NonZero, out); + return c2_to_polygons(out); } Slic3r::Polygons diff(const Slic3r::Polygon &subject, const Slic3r::Polygon &clip, ApplySafetyOffset do_safety_offset) - { return _clipper(ClipperLib::ctDifference, ClipperUtils::SinglePathProvider(subject.points), ClipperUtils::SinglePathProvider(clip.points), do_safety_offset); } + { return _clipper(ctDifference, ClipperUtils::SinglePathProvider(subject.points), ClipperUtils::SinglePathProvider(clip.points), do_safety_offset); } Slic3r::Polygons diff(const Slic3r::Polygons &subject, const Slic3r::Polygons &clip, ApplySafetyOffset do_safety_offset) - { return _clipper(ClipperLib::ctDifference, ClipperUtils::PolygonsProvider(subject), ClipperUtils::PolygonsProvider(clip), do_safety_offset); } + { return _clipper(ctDifference, ClipperUtils::PolygonsProvider(subject), ClipperUtils::PolygonsProvider(clip), do_safety_offset); } Slic3r::Polygons diff_clipped(const Slic3r::Polygons &subject, const Slic3r::Polygons &clip, ApplySafetyOffset do_safety_offset) { return diff(subject, ClipperUtils::clip_clipper_polygons_with_subject_bbox(clip, get_extents(subject).inflated(SCALED_EPSILON)), do_safety_offset); } Slic3r::ExPolygons diff_clipped(const Slic3r::ExPolygons &subject, const Slic3r::Polygons &clip, ApplySafetyOffset do_safety_offset) @@ -690,31 +645,37 @@ Slic3r::ExPolygons diff_clipped(const Slic3r::ExPolygons & subject, const Slic3r return diff_ex(subject, ClipperUtils::clip_clipper_polygons_with_subject_bbox(clip, get_extents(subject).inflated(SCALED_EPSILON)), do_safety_offset); } Slic3r::Polygons diff(const Slic3r::Polygons &subject, const Slic3r::ExPolygons &clip, ApplySafetyOffset do_safety_offset) - { return _clipper(ClipperLib::ctDifference, ClipperUtils::PolygonsProvider(subject), ClipperUtils::ExPolygonsProvider(clip), do_safety_offset); } + { return _clipper(ctDifference, ClipperUtils::PolygonsProvider(subject), ClipperUtils::ExPolygonsProvider(clip), do_safety_offset); } Slic3r::Polygons diff(const Slic3r::ExPolygons &subject, const Slic3r::Polygons &clip, ApplySafetyOffset do_safety_offset) - { return _clipper(ClipperLib::ctDifference, ClipperUtils::ExPolygonsProvider(subject), ClipperUtils::PolygonsProvider(clip), do_safety_offset); } + { return _clipper(ctDifference, ClipperUtils::ExPolygonsProvider(subject), ClipperUtils::PolygonsProvider(clip), do_safety_offset); } Slic3r::Polygons diff(const Slic3r::ExPolygons &subject, const Slic3r::ExPolygons &clip, ApplySafetyOffset do_safety_offset) - { return _clipper(ClipperLib::ctDifference, ClipperUtils::ExPolygonsProvider(subject), ClipperUtils::ExPolygonsProvider(clip), do_safety_offset); } + { return _clipper(ctDifference, ClipperUtils::ExPolygonsProvider(subject), ClipperUtils::ExPolygonsProvider(clip), do_safety_offset); } Slic3r::Polygons diff(const Slic3r::Surfaces &subject, const Slic3r::Polygons &clip, ApplySafetyOffset do_safety_offset) - { return _clipper(ClipperLib::ctDifference, ClipperUtils::SurfacesProvider(subject), ClipperUtils::PolygonsProvider(clip), do_safety_offset); } + { return _clipper(ctDifference, ClipperUtils::SurfacesProvider(subject), ClipperUtils::PolygonsProvider(clip), do_safety_offset); } Slic3r::Polygons intersection(const Slic3r::Polygon &subject, const Slic3r::Polygon &clip, ApplySafetyOffset do_safety_offset) - { return _clipper(ClipperLib::ctIntersection, ClipperUtils::SinglePathProvider(subject.points), ClipperUtils::SinglePathProvider(clip.points), do_safety_offset); } + { return _clipper(ctIntersection, ClipperUtils::SinglePathProvider(subject.points), ClipperUtils::SinglePathProvider(clip.points), do_safety_offset); } Slic3r::Polygons intersection_clipped(const Slic3r::Polygons &subject, const Slic3r::Polygons &clip, ApplySafetyOffset do_safety_offset) { return intersection(subject, ClipperUtils::clip_clipper_polygons_with_subject_bbox(clip, get_extents(subject).inflated(SCALED_EPSILON)), do_safety_offset); } Slic3r::Polygons intersection(const Slic3r::Polygons &subject, const Slic3r::ExPolygon &clip, ApplySafetyOffset do_safety_offset) - { return _clipper(ClipperLib::ctIntersection, ClipperUtils::PolygonsProvider(subject), ClipperUtils::ExPolygonProvider(clip), do_safety_offset); } + { return _clipper(ctIntersection, ClipperUtils::PolygonsProvider(subject), ClipperUtils::ExPolygonProvider(clip), do_safety_offset); } Slic3r::Polygons intersection(const Slic3r::Polygons &subject, const Slic3r::Polygons &clip, ApplySafetyOffset do_safety_offset) - { return _clipper(ClipperLib::ctIntersection, ClipperUtils::PolygonsProvider(subject), ClipperUtils::PolygonsProvider(clip), do_safety_offset); } + { return _clipper(ctIntersection, ClipperUtils::PolygonsProvider(subject), ClipperUtils::PolygonsProvider(clip), do_safety_offset); } +Slic3r::Polygons intersection(const Slic3r::Polygons &subject, const Slic3r::Polygons &clip, PolyFillType fill_type) +{ + C2::Paths64 out; + c2_clip(C2::ClipType::Intersection, to_paths64(ClipperUtils::PolygonsProvider(subject)), to_paths64(ClipperUtils::PolygonsProvider(clip)), to_c2(fill_type), out); + return c2_to_polygons(out); +} Slic3r::Polygons intersection(const Slic3r::ExPolygon &subject, const Slic3r::ExPolygon &clip, ApplySafetyOffset do_safety_offset) - { return _clipper(ClipperLib::ctIntersection, ClipperUtils::ExPolygonProvider(subject), ClipperUtils::ExPolygonProvider(clip), do_safety_offset); } + { return _clipper(ctIntersection, ClipperUtils::ExPolygonProvider(subject), ClipperUtils::ExPolygonProvider(clip), do_safety_offset); } Slic3r::Polygons intersection(const Slic3r::ExPolygons &subject, const Slic3r::Polygons &clip, ApplySafetyOffset do_safety_offset) - { return _clipper(ClipperLib::ctIntersection, ClipperUtils::ExPolygonsProvider(subject), ClipperUtils::PolygonsProvider(clip), do_safety_offset); } + { return _clipper(ctIntersection, ClipperUtils::ExPolygonsProvider(subject), ClipperUtils::PolygonsProvider(clip), do_safety_offset); } Slic3r::Polygons intersection(const Slic3r::ExPolygons &subject, const Slic3r::ExPolygons &clip, ApplySafetyOffset do_safety_offset) - { return _clipper(ClipperLib::ctIntersection, ClipperUtils::ExPolygonsProvider(subject), ClipperUtils::ExPolygonsProvider(clip), do_safety_offset); } + { return _clipper(ctIntersection, ClipperUtils::ExPolygonsProvider(subject), ClipperUtils::ExPolygonsProvider(clip), do_safety_offset); } Slic3r::Polygons intersection(const Slic3r::Surfaces &subject, const Slic3r::Polygons &clip, ApplySafetyOffset do_safety_offset) - { return _clipper(ClipperLib::ctIntersection, ClipperUtils::SurfacesProvider(subject), ClipperUtils::PolygonsProvider(clip), do_safety_offset); } + { return _clipper(ctIntersection, ClipperUtils::SurfacesProvider(subject), ClipperUtils::PolygonsProvider(clip), do_safety_offset); } Slic3r::Polygons intersection(const Slic3r::Surfaces &subject, const Slic3r::ExPolygons &clip, ApplySafetyOffset do_safety_offset) - { return _clipper(ClipperLib::ctIntersection, ClipperUtils::SurfacesProvider(subject), ClipperUtils::ExPolygonsProvider(clip), do_safety_offset); } + { return _clipper(ctIntersection, ClipperUtils::SurfacesProvider(subject), ClipperUtils::ExPolygonsProvider(clip), do_safety_offset); } // BBS Slic3r::Polygons intersection(const Slic3r::Polygons& subject, const Slic3r::Polygon& clip, ApplySafetyOffset do_safety_offset) { @@ -724,11 +685,16 @@ Slic3r::Polygons intersection(const Slic3r::Polygons& subject, const Slic3r::Pol } Slic3r::Polygons union_(const Slic3r::Polygons &subject) - { return _clipper(ClipperLib::ctUnion, ClipperUtils::PolygonsProvider(subject), ClipperUtils::EmptyPathsProvider(), ApplySafetyOffset::No); } + { return _clipper(ctUnion, ClipperUtils::PolygonsProvider(subject), ClipperUtils::EmptyPathsProvider(), ApplySafetyOffset::No); } Slic3r::Polygons union_(const Slic3r::ExPolygons &subject) - { return _clipper(ClipperLib::ctUnion, ClipperUtils::ExPolygonsProvider(subject), ClipperUtils::EmptyPathsProvider(), ApplySafetyOffset::No); } -Slic3r::Polygons union_(const Slic3r::Polygons &subject, const ClipperLib::PolyFillType fillType) - { return to_polygons(clipper_do(ClipperLib::ctUnion, ClipperUtils::PolygonsProvider(subject), ClipperUtils::EmptyPathsProvider(), fillType, ApplySafetyOffset::No)); } + { return _clipper(ctUnion, ClipperUtils::ExPolygonsProvider(subject), ClipperUtils::EmptyPathsProvider(), ApplySafetyOffset::No); } +Slic3r::Polygons union_(const Slic3r::Polygons &subject, const PolyFillType fillType) +{ + C2::Paths64 out; + c2_clip(C2::ClipType::Union, to_paths64(ClipperUtils::PolygonsProvider(subject)), {}, to_c2(fillType), out); + return c2_to_polygons(out); +} + Slic3r::Polygons union_(const Slic3r::Polygons &subject, const Slic3r::Polygons &subject2) { // BBS @@ -738,38 +704,44 @@ Slic3r::Polygons union_(const Slic3r::Polygons &subject, const Slic3r::Polygons return union_(polys); } +// Clipper2 builds the PolyTree in one pass without Clipper1's slowdown on shared edges (#117). template -static ExPolygons _clipper_ex(ClipperLib::ClipType clipType, TSubject &&subject, TClip &&clip, ApplySafetyOffset do_safety_offset, ClipperLib::PolyFillType fill_type = ClipperLib::pftNonZero) - { return PolyTreeToExPolygons(clipper_do_polytree(clipType, std::forward(subject), std::forward(clip), fill_type, do_safety_offset)); } +static ExPolygons _clipper_ex(ClipType clipType, TSubject &&subject, TClip &&clip, ApplySafetyOffset do_safety_offset, PolyFillType fill_type = pftNonZero) +{ + assert(do_safety_offset == ApplySafetyOffset::No || clipType != ctUnion); + C2::PolyTree64 out; + c2_clip(to_c2(clipType), to_paths64(std::forward(subject)), c2_clip_paths(std::forward(clip), do_safety_offset), to_c2(fill_type), out); + return c2_to_expolygons(out); +} Slic3r::ExPolygons diff_ex(const Slic3r::Polygons &subject, const Slic3r::Polygons &clip, ApplySafetyOffset do_safety_offset) - { return _clipper_ex(ClipperLib::ctDifference, ClipperUtils::PolygonsProvider(subject), ClipperUtils::PolygonsProvider(clip), do_safety_offset); } + { return _clipper_ex(ctDifference, ClipperUtils::PolygonsProvider(subject), ClipperUtils::PolygonsProvider(clip), do_safety_offset); } Slic3r::ExPolygons diff_ex(const Slic3r::Polygons &subject, const Slic3r::Surfaces &clip, ApplySafetyOffset do_safety_offset) - { return _clipper_ex(ClipperLib::ctDifference, ClipperUtils::PolygonsProvider(subject), ClipperUtils::SurfacesProvider(clip), do_safety_offset); } + { return _clipper_ex(ctDifference, ClipperUtils::PolygonsProvider(subject), ClipperUtils::SurfacesProvider(clip), do_safety_offset); } Slic3r::ExPolygons diff_ex(const Slic3r::Polygon &subject, const Slic3r::ExPolygons &clip, ApplySafetyOffset do_safety_offset) - { return _clipper_ex(ClipperLib::ctDifference, ClipperUtils::SinglePathProvider(subject.points), ClipperUtils::ExPolygonsProvider(clip), do_safety_offset); } + { return _clipper_ex(ctDifference, ClipperUtils::SinglePathProvider(subject.points), ClipperUtils::ExPolygonsProvider(clip), do_safety_offset); } Slic3r::ExPolygons diff_ex(const Slic3r::Polygons &subject, const Slic3r::ExPolygons &clip, ApplySafetyOffset do_safety_offset) - { return _clipper_ex(ClipperLib::ctDifference, ClipperUtils::PolygonsProvider(subject), ClipperUtils::ExPolygonsProvider(clip), do_safety_offset); } + { return _clipper_ex(ctDifference, ClipperUtils::PolygonsProvider(subject), ClipperUtils::ExPolygonsProvider(clip), do_safety_offset); } Slic3r::ExPolygons diff_ex(const Slic3r::ExPolygon &subject, const Slic3r::Polygon &clip, ApplySafetyOffset do_safety_offset) - { return _clipper_ex(ClipperLib::ctDifference, ClipperUtils::ExPolygonProvider(subject), ClipperUtils::SinglePathProvider(clip.points), do_safety_offset); } + { return _clipper_ex(ctDifference, ClipperUtils::ExPolygonProvider(subject), ClipperUtils::SinglePathProvider(clip.points), do_safety_offset); } Slic3r::ExPolygons diff_ex(const Slic3r::ExPolygon &subject, const Slic3r::Polygons &clip, ApplySafetyOffset do_safety_offset) - { return _clipper_ex(ClipperLib::ctDifference, ClipperUtils::ExPolygonProvider(subject), ClipperUtils::PolygonsProvider(clip), do_safety_offset); } + { return _clipper_ex(ctDifference, ClipperUtils::ExPolygonProvider(subject), ClipperUtils::PolygonsProvider(clip), do_safety_offset); } Slic3r::ExPolygons diff_ex(const Slic3r::ExPolygons &subject, const Slic3r::Polygons &clip, ApplySafetyOffset do_safety_offset) - { return _clipper_ex(ClipperLib::ctDifference, ClipperUtils::ExPolygonsProvider(subject), ClipperUtils::PolygonsProvider(clip), do_safety_offset); } + { return _clipper_ex(ctDifference, ClipperUtils::ExPolygonsProvider(subject), ClipperUtils::PolygonsProvider(clip), do_safety_offset); } Slic3r::ExPolygons diff_ex(const Slic3r::ExPolygons &subject, const Slic3r::ExPolygons &clip, ApplySafetyOffset do_safety_offset) - { return _clipper_ex(ClipperLib::ctDifference, ClipperUtils::ExPolygonsProvider(subject), ClipperUtils::ExPolygonsProvider(clip), do_safety_offset); } + { return _clipper_ex(ctDifference, ClipperUtils::ExPolygonsProvider(subject), ClipperUtils::ExPolygonsProvider(clip), do_safety_offset); } Slic3r::ExPolygons diff_ex(const Slic3r::Surfaces &subject, const Slic3r::Polygons &clip, ApplySafetyOffset do_safety_offset) - { return _clipper_ex(ClipperLib::ctDifference, ClipperUtils::SurfacesProvider(subject), ClipperUtils::PolygonsProvider(clip), do_safety_offset); } + { return _clipper_ex(ctDifference, ClipperUtils::SurfacesProvider(subject), ClipperUtils::PolygonsProvider(clip), do_safety_offset); } Slic3r::ExPolygons diff_ex(const Slic3r::Surfaces &subject, const Slic3r::ExPolygons &clip, ApplySafetyOffset do_safety_offset) - { return _clipper_ex(ClipperLib::ctDifference, ClipperUtils::SurfacesProvider(subject), ClipperUtils::ExPolygonsProvider(clip), do_safety_offset); } + { return _clipper_ex(ctDifference, ClipperUtils::SurfacesProvider(subject), ClipperUtils::ExPolygonsProvider(clip), do_safety_offset); } Slic3r::ExPolygons diff_ex(const Slic3r::ExPolygons &subject, const Slic3r::Surfaces &clip, ApplySafetyOffset do_safety_offset) - { return _clipper_ex(ClipperLib::ctDifference, ClipperUtils::ExPolygonsProvider(subject), ClipperUtils::SurfacesProvider(clip), do_safety_offset); } + { return _clipper_ex(ctDifference, ClipperUtils::ExPolygonsProvider(subject), ClipperUtils::SurfacesProvider(clip), do_safety_offset); } Slic3r::ExPolygons diff_ex(const Slic3r::Surfaces &subject, const Slic3r::Surfaces &clip, ApplySafetyOffset do_safety_offset) - { return _clipper_ex(ClipperLib::ctDifference, ClipperUtils::SurfacesProvider(subject), ClipperUtils::SurfacesProvider(clip), do_safety_offset); } + { return _clipper_ex(ctDifference, ClipperUtils::SurfacesProvider(subject), ClipperUtils::SurfacesProvider(clip), do_safety_offset); } Slic3r::ExPolygons diff_ex(const Slic3r::SurfacesPtr &subject, const Slic3r::Polygons &clip, ApplySafetyOffset do_safety_offset) - { return _clipper_ex(ClipperLib::ctDifference, ClipperUtils::SurfacesPtrProvider(subject), ClipperUtils::PolygonsProvider(clip), do_safety_offset); } + { return _clipper_ex(ctDifference, ClipperUtils::SurfacesPtrProvider(subject), ClipperUtils::PolygonsProvider(clip), do_safety_offset); } Slic3r::ExPolygons diff_ex(const Slic3r::SurfacesPtr &subject, const Slic3r::ExPolygons &clip, ApplySafetyOffset do_safety_offset) - { return _clipper_ex(ClipperLib::ctDifference, ClipperUtils::SurfacesPtrProvider(subject), ClipperUtils::ExPolygonsProvider(clip), do_safety_offset); } + { return _clipper_ex(ctDifference, ClipperUtils::SurfacesPtrProvider(subject), ClipperUtils::ExPolygonsProvider(clip), do_safety_offset); } // BBS inline Slic3r::ExPolygons diff_ex(const Slic3r::Polygon& subject, const Slic3r::Polygons& clip, ApplySafetyOffset do_safety_offset) { @@ -790,38 +762,39 @@ inline Slic3r::ExPolygons diff_ex(const Slic3r::Polygon& subject, const Slic3r:: } Slic3r::ExPolygons intersection_ex(const Slic3r::Polygons &subject, const Slic3r::Polygons &clip, ApplySafetyOffset do_safety_offset) - { return _clipper_ex(ClipperLib::ctIntersection, ClipperUtils::PolygonsProvider(subject), ClipperUtils::PolygonsProvider(clip), do_safety_offset); } + { return _clipper_ex(ctIntersection, ClipperUtils::PolygonsProvider(subject), ClipperUtils::PolygonsProvider(clip), do_safety_offset); } Slic3r::ExPolygons intersection_ex(const Slic3r::ExPolygon &subject, const Slic3r::Polygons &clip, ApplySafetyOffset do_safety_offset) - { return _clipper_ex(ClipperLib::ctIntersection, ClipperUtils::ExPolygonProvider(subject), ClipperUtils::PolygonsProvider(clip), do_safety_offset); } + { return _clipper_ex(ctIntersection, ClipperUtils::ExPolygonProvider(subject), ClipperUtils::PolygonsProvider(clip), do_safety_offset); } Slic3r::ExPolygons intersection_ex(const Slic3r::ExPolygon& subject, const Slic3r::ExPolygon& clip, ApplySafetyOffset do_safety_offset) - { return _clipper_ex(ClipperLib::ctIntersection, ClipperUtils::ExPolygonProvider(subject), ClipperUtils::ExPolygonProvider(clip), do_safety_offset); } + { return _clipper_ex(ctIntersection, ClipperUtils::ExPolygonProvider(subject), ClipperUtils::ExPolygonProvider(clip), do_safety_offset); } Slic3r::ExPolygons intersection_ex(const Slic3r::Polygons &subject, const Slic3r::ExPolygons &clip, ApplySafetyOffset do_safety_offset) - { return _clipper_ex(ClipperLib::ctIntersection, ClipperUtils::PolygonsProvider(subject), ClipperUtils::ExPolygonsProvider(clip), do_safety_offset); } + { return _clipper_ex(ctIntersection, ClipperUtils::PolygonsProvider(subject), ClipperUtils::ExPolygonsProvider(clip), do_safety_offset); } Slic3r::ExPolygons intersection_ex(const Slic3r::ExPolygons &subject, const Slic3r::Polygons &clip, ApplySafetyOffset do_safety_offset) - { return _clipper_ex(ClipperLib::ctIntersection, ClipperUtils::ExPolygonsProvider(subject), ClipperUtils::PolygonsProvider(clip), do_safety_offset); } + { return _clipper_ex(ctIntersection, ClipperUtils::ExPolygonsProvider(subject), ClipperUtils::PolygonsProvider(clip), do_safety_offset); } Slic3r::ExPolygons intersection_ex(const Slic3r::ExPolygons& subject, const Slic3r::ExPolygon& clip, ApplySafetyOffset do_safety_offset) - { return _clipper_ex(ClipperLib::ctIntersection, ClipperUtils::ExPolygonsProvider(subject), ClipperUtils::ExPolygonProvider(clip), do_safety_offset);} + { return _clipper_ex(ctIntersection, ClipperUtils::ExPolygonsProvider(subject), ClipperUtils::ExPolygonProvider(clip), do_safety_offset);} Slic3r::ExPolygons intersection_ex(const Slic3r::ExPolygon& subject, const Slic3r::ExPolygons& clip, ApplySafetyOffset do_safety_offset) - { return _clipper_ex(ClipperLib::ctIntersection, ClipperUtils::ExPolygonProvider(subject), ClipperUtils::ExPolygonsProvider(clip), do_safety_offset);} + { return _clipper_ex(ctIntersection, ClipperUtils::ExPolygonProvider(subject), ClipperUtils::ExPolygonsProvider(clip), do_safety_offset);} Slic3r::ExPolygons intersection_ex(const Slic3r::ExPolygons &subject, const Slic3r::ExPolygons &clip, ApplySafetyOffset do_safety_offset) - { return _clipper_ex(ClipperLib::ctIntersection, ClipperUtils::ExPolygonsProvider(subject), ClipperUtils::ExPolygonsProvider(clip), do_safety_offset); } + { return _clipper_ex(ctIntersection, ClipperUtils::ExPolygonsProvider(subject), ClipperUtils::ExPolygonsProvider(clip), do_safety_offset); } Slic3r::ExPolygons intersection_ex(const Slic3r::Surfaces &subject, const Slic3r::Polygons &clip, ApplySafetyOffset do_safety_offset) - { return _clipper_ex(ClipperLib::ctIntersection, ClipperUtils::SurfacesProvider(subject), ClipperUtils::PolygonsProvider(clip), do_safety_offset); } + { return _clipper_ex(ctIntersection, ClipperUtils::SurfacesProvider(subject), ClipperUtils::PolygonsProvider(clip), do_safety_offset); } Slic3r::ExPolygons intersection_ex(const Slic3r::Surfaces &subject, const Slic3r::ExPolygons &clip, ApplySafetyOffset do_safety_offset) - { return _clipper_ex(ClipperLib::ctIntersection, ClipperUtils::SurfacesProvider(subject), ClipperUtils::ExPolygonsProvider(clip), do_safety_offset); } + { return _clipper_ex(ctIntersection, ClipperUtils::SurfacesProvider(subject), ClipperUtils::ExPolygonsProvider(clip), do_safety_offset); } Slic3r::ExPolygons intersection_ex(const Slic3r::Surfaces &subject, const Slic3r::Surfaces &clip, ApplySafetyOffset do_safety_offset) - { return _clipper_ex(ClipperLib::ctIntersection, ClipperUtils::SurfacesProvider(subject), ClipperUtils::SurfacesProvider(clip), do_safety_offset); } + { return _clipper_ex(ctIntersection, ClipperUtils::SurfacesProvider(subject), ClipperUtils::SurfacesProvider(clip), do_safety_offset); } Slic3r::ExPolygons intersection_ex(const Slic3r::SurfacesPtr &subject, const Slic3r::ExPolygons &clip, ApplySafetyOffset do_safety_offset) - { return _clipper_ex(ClipperLib::ctIntersection, ClipperUtils::SurfacesPtrProvider(subject), ClipperUtils::ExPolygonsProvider(clip), do_safety_offset); } + { return _clipper_ex(ctIntersection, ClipperUtils::SurfacesPtrProvider(subject), ClipperUtils::ExPolygonsProvider(clip), do_safety_offset); } // May be used to "heal" unusual models (3DLabPrints etc.) by providing fill_type (pftEvenOdd, pftNonZero, pftPositive, pftNegative). -Slic3r::ExPolygons union_ex(const Slic3r::Polygons &subject, ClipperLib::PolyFillType fill_type) - { return _clipper_ex(ClipperLib::ctUnion, ClipperUtils::PolygonsProvider(subject), ClipperUtils::EmptyPathsProvider(), ApplySafetyOffset::No, fill_type); } +Slic3r::ExPolygons union_ex(const Slic3r::Polygons &subject, PolyFillType fill_type) + { return _clipper_ex(ctUnion, ClipperUtils::PolygonsProvider(subject), ClipperUtils::EmptyPathsProvider(), ApplySafetyOffset::No, fill_type); } Slic3r::ExPolygons union_ex(const Slic3r::ExPolygons &subject) - { return PolyTreeToExPolygons(clipper_do_polytree(ClipperLib::ctUnion, ClipperUtils::ExPolygonsProvider(subject), ClipperUtils::EmptyPathsProvider(), ClipperLib::pftNonZero)); } + { return _clipper_ex(ctUnion, ClipperUtils::ExPolygonsProvider(subject), ClipperUtils::EmptyPathsProvider(), ApplySafetyOffset::No); } Slic3r::ExPolygons union_ex(const Slic3r::ExPolygons &subject, const Slic3r::Polygons &subject2) - { return PolyTreeToExPolygons(clipper_do_polytree(ClipperLib::ctUnion, ClipperUtils::ExPolygonsProvider(subject), ClipperUtils::PolygonsProvider(subject2), ClipperLib::pftNonZero)); } + { return _clipper_ex(ctUnion, ClipperUtils::ExPolygonsProvider(subject), ClipperUtils::PolygonsProvider(subject2), ApplySafetyOffset::No); } Slic3r::ExPolygons union_ex(const Slic3r::Surfaces &subject) - { return PolyTreeToExPolygons(clipper_do_polytree(ClipperLib::ctUnion, ClipperUtils::SurfacesProvider(subject), ClipperUtils::EmptyPathsProvider(), ClipperLib::pftNonZero)); } + { return _clipper_ex(ctUnion, ClipperUtils::SurfacesProvider(subject), ClipperUtils::EmptyPathsProvider(), ApplySafetyOffset::No); } + // BBS Slic3r::ExPolygons union_ex(const Slic3r::ExPolygons& poly1, const Slic3r::ExPolygons& poly2, bool safety_offset_) { @@ -832,22 +805,15 @@ Slic3r::ExPolygons union_ex(const Slic3r::ExPolygons& poly1, const Slic3r::ExPol } Slic3r::ExPolygons xor_ex(const Slic3r::ExPolygons &subject, const Slic3r::ExPolygon &clip, ApplySafetyOffset do_safety_offset) { - return _clipper_ex(ClipperLib::ctXor, ClipperUtils::ExPolygonsProvider(subject), ClipperUtils::ExPolygonProvider(clip), do_safety_offset); + return _clipper_ex(ctXor, ClipperUtils::ExPolygonsProvider(subject), ClipperUtils::ExPolygonProvider(clip), do_safety_offset); } Slic3r::ExPolygons xor_ex(const Slic3r::ExPolygons &subject, const Slic3r::ExPolygons &clip, ApplySafetyOffset do_safety_offset) { - return _clipper_ex(ClipperLib::ctXor, ClipperUtils::ExPolygonsProvider(subject), ClipperUtils::ExPolygonsProvider(clip), do_safety_offset); + return _clipper_ex(ctXor, ClipperUtils::ExPolygonsProvider(subject), ClipperUtils::ExPolygonsProvider(clip), do_safety_offset); } template -Polylines _clipper_pl_open(ClipperLib::ClipType clipType, PathsProvider1 &&subject, PathsProvider2 &&clip) -{ - ClipperLib::Clipper clipper; - clipper.AddPaths(std::forward(subject), ClipperLib::ptSubject, false); - clipper.AddPaths(std::forward(clip), ClipperLib::ptClip, true); - ClipperLib::PolyTree retval; - clipper.Execute(clipType, retval, ClipperLib::pftNonZero, ClipperLib::pftNonZero); - return PolyTreeToPolylines(std::move(retval)); -} +Polylines _clipper_pl_open(ClipType clipType, PathsProvider1 &&subject, PathsProvider2 &&clip) + { return c2_clip_open(to_c2(clipType), to_paths64(std::forward(subject)), to_paths64(std::forward(clip))); } // If the split_at_first_point() call above happens to split the polygon inside the clipping area // we would get two consecutive polylines instead of a single one, so we go through them in order @@ -888,51 +854,52 @@ static void _clipper_pl_recombine(Polylines &polylines) } template -Polylines _clipper_pl_closed(ClipperLib::ClipType clipType, PathProvider1 &&subject, PathProvider2 &&clip) +Polylines _clipper_pl_closed(ClipType clipType, PathProvider1 &&subject, PathProvider2 &&clip) { // Transform input polygons into open paths. - ClipperLib::Paths paths; + C2::Paths64 paths; paths.reserve(subject.size()); for (const Points &poly : subject) { // Emplace polygon, duplicate the 1st point. - paths.push_back({}); - ClipperLib::Path &path = paths.back(); + C2::Path64 &path = paths.emplace_back(); path.reserve(poly.size() + 1); - path = poly; - path.emplace_back(poly.front()); + for (const Point &pt : poly) + path.emplace_back(pt.x(), pt.y()); + if (! poly.empty()) + path.emplace_back(poly.front().x(), poly.front().y()); } // perform clipping - Polylines retval = _clipper_pl_open(clipType, paths, std::forward(clip)); + Polylines retval = c2_clip_open(to_c2(clipType), paths, to_paths64(std::forward(clip))); _clipper_pl_recombine(retval); return retval; } Slic3r::Polylines diff_pl(const Slic3r::Polyline& subject, const Slic3r::Polygons& clip) - { return _clipper_pl_open(ClipperLib::ctDifference, ClipperUtils::SinglePathProvider(subject.points), ClipperUtils::PolygonsProvider(clip)); } + { return _clipper_pl_open(ctDifference, ClipperUtils::SinglePathProvider(subject.points), ClipperUtils::PolygonsProvider(clip)); } Slic3r::Polylines diff_pl(const Slic3r::Polylines &subject, const Slic3r::Polygons &clip) - { return _clipper_pl_open(ClipperLib::ctDifference, ClipperUtils::PolylinesProvider(subject), ClipperUtils::PolygonsProvider(clip)); } + { return _clipper_pl_open(ctDifference, ClipperUtils::PolylinesProvider(subject), ClipperUtils::PolygonsProvider(clip)); } Slic3r::Polylines diff_pl(const Slic3r::Polyline &subject, const Slic3r::ExPolygon &clip) - { return _clipper_pl_open(ClipperLib::ctDifference, ClipperUtils::SinglePathProvider(subject.points), ClipperUtils::ExPolygonProvider(clip)); } + { return _clipper_pl_open(ctDifference, ClipperUtils::SinglePathProvider(subject.points), ClipperUtils::ExPolygonProvider(clip)); } Slic3r::Polylines diff_pl(const Slic3r::Polylines &subject, const Slic3r::ExPolygon &clip) - { return _clipper_pl_open(ClipperLib::ctDifference, ClipperUtils::PolylinesProvider(subject), ClipperUtils::ExPolygonProvider(clip)); } + { return _clipper_pl_open(ctDifference, ClipperUtils::PolylinesProvider(subject), ClipperUtils::ExPolygonProvider(clip)); } Slic3r::Polylines diff_pl(const Slic3r::Polylines &subject, const Slic3r::ExPolygons &clip) - { return _clipper_pl_open(ClipperLib::ctDifference, ClipperUtils::PolylinesProvider(subject), ClipperUtils::ExPolygonsProvider(clip)); } + { return _clipper_pl_open(ctDifference, ClipperUtils::PolylinesProvider(subject), ClipperUtils::ExPolygonsProvider(clip)); } Slic3r::Polylines diff_pl(const Slic3r::Polygons &subject, const Slic3r::Polygons &clip) - { return _clipper_pl_closed(ClipperLib::ctDifference, ClipperUtils::PolygonsProvider(subject), ClipperUtils::PolygonsProvider(clip)); } + { return _clipper_pl_closed(ctDifference, ClipperUtils::PolygonsProvider(subject), ClipperUtils::PolygonsProvider(clip)); } Slic3r::Polylines intersection_pl(const Slic3r::Polylines &subject, const Slic3r::Polygon &clip) - { return _clipper_pl_open(ClipperLib::ctIntersection, ClipperUtils::PolylinesProvider(subject), ClipperUtils::SinglePathProvider(clip.points)); } + { return _clipper_pl_open(ctIntersection, ClipperUtils::PolylinesProvider(subject), ClipperUtils::SinglePathProvider(clip.points)); } Slic3r::Polylines intersection_pl(const Slic3r::Polyline &subject, const Slic3r::ExPolygon &clip) - { return _clipper_pl_open(ClipperLib::ctIntersection, ClipperUtils::SinglePathProvider(subject.points), ClipperUtils::ExPolygonProvider(clip)); } + { return _clipper_pl_open(ctIntersection, ClipperUtils::SinglePathProvider(subject.points), ClipperUtils::ExPolygonProvider(clip)); } Slic3r::Polylines intersection_pl(const Slic3r::Polylines &subject, const Slic3r::ExPolygon &clip) - { return _clipper_pl_open(ClipperLib::ctIntersection, ClipperUtils::PolylinesProvider(subject), ClipperUtils::ExPolygonProvider(clip)); } + { return _clipper_pl_open(ctIntersection, ClipperUtils::PolylinesProvider(subject), ClipperUtils::ExPolygonProvider(clip)); } Slic3r::Polylines intersection_pl(const Slic3r::Polyline &subject, const Slic3r::Polygons &clip) - { return _clipper_pl_open(ClipperLib::ctIntersection, ClipperUtils::SinglePathProvider(subject.points), ClipperUtils::PolygonsProvider(clip)); } + { return _clipper_pl_open(ctIntersection, ClipperUtils::SinglePathProvider(subject.points), ClipperUtils::PolygonsProvider(clip)); } Slic3r::Polylines intersection_pl(const Slic3r::Polylines &subject, const Slic3r::Polygons &clip) - { return _clipper_pl_open(ClipperLib::ctIntersection, ClipperUtils::PolylinesProvider(subject), ClipperUtils::PolygonsProvider(clip)); } + { return _clipper_pl_open(ctIntersection, ClipperUtils::PolylinesProvider(subject), ClipperUtils::PolygonsProvider(clip)); } Slic3r::Polylines intersection_pl(const Slic3r::Polylines &subject, const Slic3r::ExPolygons &clip) - { return _clipper_pl_open(ClipperLib::ctIntersection, ClipperUtils::PolylinesProvider(subject), ClipperUtils::ExPolygonsProvider(clip)); } + { return _clipper_pl_open(ctIntersection, ClipperUtils::PolylinesProvider(subject), ClipperUtils::ExPolygonsProvider(clip)); } Slic3r::Polylines intersection_pl(const Slic3r::Polygons &subject, const Slic3r::Polygons &clip) - { return _clipper_pl_closed(ClipperLib::ctIntersection, ClipperUtils::PolygonsProvider(subject), ClipperUtils::PolygonsProvider(clip)); } + { return _clipper_pl_closed(ctIntersection, ClipperUtils::PolygonsProvider(subject), ClipperUtils::PolygonsProvider(clip)); } // Orca: Sort and orient open polyline fragments produced by clipping `source` with // intersection_pl(), so that they run in the same order and direction as the source @@ -1005,7 +972,7 @@ void restore_source_path_order(const Slic3r::Polyline &source, Slic3r::Polylines fragments = std::move(sorted); } -Lines _clipper_ln(ClipperLib::ClipType clipType, const Lines &subject, const Polygons &clip) +Lines _clipper_ln(ClipType clipType, const Lines &subject, const Polygons &clip) { // convert Lines to Polylines Polylines polylines; @@ -1025,162 +992,83 @@ Lines _clipper_ln(ClipperLib::ClipType clipType, const Lines &subject, const Pol return retval; } -// Convert polygons / expolygons into ClipperLib::PolyTree using ClipperLib::pftEvenOdd, thus union will NOT be performed. -// If the contours are not intersecting, their orientation shall not be modified by union_pt(). -ClipperLib::PolyTree union_pt(const Polygons &subject) -{ - return clipper_do(ClipperLib::ctUnion, ClipperUtils::PolygonsProvider(subject), ClipperUtils::EmptyPathsProvider(), ClipperLib::pftEvenOdd); -} -ClipperLib::PolyTree union_pt(const ExPolygons &subject) -{ - return clipper_do(ClipperLib::ctUnion, ClipperUtils::ExPolygonsProvider(subject), ClipperUtils::EmptyPathsProvider(), ClipperLib::pftEvenOdd); -} - -// Simple spatial ordering of Polynodes -ClipperLib::PolyNodes order_nodes(const ClipperLib::PolyNodes &nodes) +static void traverse_pt_outside_in(const C2::PolyPath64 &parent, Polygons *retval) { // collect ordering points Points ordering_points; - ordering_points.reserve(nodes.size()); - - for (const ClipperLib::PolyNode *node : nodes) - ordering_points.emplace_back( - Point(node->Contour.front().x(), node->Contour.front().y())); - - // perform the ordering - ClipperLib::PolyNodes ordered_nodes = - chain_clipper_polynodes(ordering_points, nodes); - - return ordered_nodes; -} - -static void traverse_pt_noholes(const ClipperLib::PolyNodes &nodes, Polygons *out) -{ - foreach_node(nodes, [&out](const ClipperLib::PolyNode *node) - { - traverse_pt_noholes(node->Childs, out); - out->emplace_back(node->Contour); - if (node->IsHole()) out->back().reverse(); // ccw - }); -} - -static void traverse_pt_outside_in(ClipperLib::PolyNodes &&nodes, Polygons *retval) -{ - // collect ordering points - Points ordering_points; - ordering_points.reserve(nodes.size()); - for (const ClipperLib::PolyNode *node : nodes) - ordering_points.emplace_back(node->Contour.front().x(), node->Contour.front().y()); + ordering_points.reserve(parent.Count()); + for (const auto &node : parent) + ordering_points.emplace_back(node->Polygon().front().x, node->Polygon().front().y); // Perform the ordering, push results recursively. - //FIXME pass the last point to chain_clipper_polynodes? - for (ClipperLib::PolyNode *node : chain_clipper_polynodes(ordering_points, nodes)) { - retval->emplace_back(std::move(node->Contour)); - if (node->IsHole()) + //FIXME pass the last point to chain_points? + for (size_t idx : chain_points(ordering_points)) { + const C2::PolyPath64 &node = *parent.Child(idx); + retval->emplace_back().points = c2_to_points(node.Polygon()); + if (node.IsHole()) // Orient a hole, which is clockwise oriented, to CCW. retval->back().reverse(); // traverse the next depth - traverse_pt_outside_in(std::move(node->Childs), retval); + traverse_pt_outside_in(node, retval); } } Polygons union_pt_chained_outside_in(const Polygons &subject) { + C2::PolyTree64 tree; + c2_clip(C2::ClipType::Union, to_paths64(ClipperUtils::PolygonsProvider(subject)), {}, C2::FillRule::EvenOdd, tree); Polygons retval; - traverse_pt_outside_in(union_pt(subject).Childs, &retval); + traverse_pt_outside_in(tree, &retval); return retval; } +// Clipper2 unions are strictly simple, like Clipper1's with StrictlySimple(true). Polygons simplify_polygons(const Polygons &subject) { - ClipperLib::Paths output; - ClipperLib::Clipper c; -// c.PreserveCollinear(true); - //FIXME StrictlySimple is very expensive! Is it needed? - c.StrictlySimple(true); - c.AddPaths(ClipperUtils::PolygonsProvider(subject), ClipperLib::ptSubject, true); - c.Execute(ClipperLib::ctUnion, output, ClipperLib::pftNonZero, ClipperLib::pftNonZero); - - // convert into Slic3r polygons - return to_polygons(std::move(output)); + C2::Paths64 out; + c2_clip(C2::ClipType::Union, to_paths64(ClipperUtils::PolygonsProvider(subject)), {}, C2::FillRule::NonZero, out); + return c2_to_polygons(out); } ExPolygons simplify_polygons_ex(const Polygons &subject) { - ClipperLib::PolyTree polytree; - ClipperLib::Clipper c; -// c.PreserveCollinear(true); - //FIXME StrictlySimple is very expensive! Is it needed? - c.StrictlySimple(true); - c.AddPaths(ClipperUtils::PolygonsProvider(subject), ClipperLib::ptSubject, true); - c.Execute(ClipperLib::ctUnion, polytree, ClipperLib::pftNonZero, ClipperLib::pftNonZero); - - // convert into ExPolygons - return PolyTreeToExPolygons(std::move(polytree)); + C2::PolyTree64 out; + c2_clip(C2::ClipType::Union, to_paths64(ClipperUtils::PolygonsProvider(subject)), {}, C2::FillRule::NonZero, out); + return c2_to_expolygons(out); } Polygons top_level_islands(const Slic3r::Polygons &polygons) { - // init Clipper - ClipperLib::Clipper clipper; - clipper.Clear(); - // perform union - clipper.AddPaths(ClipperUtils::PolygonsProvider(polygons), ClipperLib::ptSubject, true); - ClipperLib::PolyTree polytree; - clipper.Execute(ClipperLib::ctUnion, polytree, ClipperLib::pftEvenOdd, ClipperLib::pftEvenOdd); + C2::PolyTree64 tree; + c2_clip(C2::ClipType::Union, to_paths64(ClipperUtils::PolygonsProvider(polygons)), {}, C2::FillRule::EvenOdd, tree); // Convert only the top level islands to the output. Polygons out; - out.reserve(polytree.ChildCount()); - for (int i = 0; i < polytree.ChildCount(); ++i) - out.emplace_back(std::move(polytree.Childs[i]->Contour)); + out.reserve(tree.Count()); + for (const auto &island : tree) + out.emplace_back().points = c2_to_points(island->Polygon()); return out; } -// Outer offset shall not split the input contour into multiples. It is expected, that the solution will be non empty and it will contain just a single polygon. -ClipperLib::Paths fix_after_outer_offset( - const ClipperLib::Path &input, - // combination of default prameters to correspond to void ClipperOffset::Execute(Paths& solution, double delta) - // to produce a CCW output contour from CCW input contour for a positive offset. - ClipperLib::PolyFillType filltype, // = ClipperLib::pftPositive - bool reverse_result) // = false +ExPolygons top_level_expolygons(const ExPolygons &expolygons, ExPolygons *nested) { - ClipperLib::Paths solution; - if (! input.empty()) { - ClipperLib::Clipper clipper; - clipper.AddPath(input, ClipperLib::ptSubject, true); - clipper.ReverseSolution(reverse_result); - clipper.Execute(ClipperLib::ctUnion, solution, filltype, filltype); - } - return solution; + C2::PolyTree64 tree; + c2_clip(C2::ClipType::Union, to_paths64(ClipperUtils::ExPolygonsProvider(expolygons)), {}, C2::FillRule::EvenOdd, tree); + ExPolygons out; + out.reserve(tree.Count()); + for (const auto &outer : tree) { + ExPolygon &expoly = out.emplace_back(); + expoly.contour.points = c2_to_points(outer->Polygon()); + for (const auto &hole : *outer) { + expoly.holes.emplace_back().points = c2_to_points(hole->Polygon()); + if (nested) + for (const auto &island : *hole) + c2_append_expolygons(*island, *nested); + } + } + return out; } -// Inner offset may split the source contour into multiple contours, but one resulting contour shall not lie inside the other. -ClipperLib::Paths fix_after_inner_offset( - const ClipperLib::Path &input, - // combination of default prameters to correspond to void ClipperOffset::Execute(Paths& solution, double delta) - // to produce a CCW output contour from CCW input contour for a negative offset. - ClipperLib::PolyFillType filltype, // = ClipperLib::pftNegative - bool reverse_result) // = true -{ - ClipperLib::Paths solution; - if (! input.empty()) { - ClipperLib::Clipper clipper; - clipper.AddPath(input, ClipperLib::ptSubject, true); - ClipperLib::IntRect r = clipper.GetBounds(); - r.left -= 10; r.top -= 10; r.right += 10; r.bottom += 10; - if (filltype == ClipperLib::pftPositive) - clipper.AddPath({ ClipperLib::IntPoint(r.left, r.bottom), ClipperLib::IntPoint(r.left, r.top), ClipperLib::IntPoint(r.right, r.top), ClipperLib::IntPoint(r.right, r.bottom) }, ClipperLib::ptSubject, true); - else - clipper.AddPath({ ClipperLib::IntPoint(r.left, r.bottom), ClipperLib::IntPoint(r.right, r.bottom), ClipperLib::IntPoint(r.right, r.top), ClipperLib::IntPoint(r.left, r.top) }, ClipperLib::ptSubject, true); - clipper.ReverseSolution(reverse_result); - clipper.Execute(ClipperLib::ctUnion, solution, filltype, filltype); - if (! solution.empty()) - solution.erase(solution.begin()); - } - return solution; -} - -ClipperLib::Path mittered_offset_path_scaled(const Points &contour, const std::vector &deltas, double miter_limit) +Points mittered_offset_path_scaled(const Points &contour, const std::vector &deltas, double miter_limit) { assert(contour.size() == deltas.size()); @@ -1196,7 +1084,7 @@ ClipperLib::Path mittered_offset_path_scaled(const Points &contour, const std::v assert(! (negative && positive)); #endif /* NDEBUG */ - ClipperLib::Path out; + Points out; if (deltas.size() > 2) { @@ -1208,10 +1096,10 @@ ClipperLib::Path mittered_offset_path_scaled(const Points &contour, const std::v // perpenduclar vector auto perp = [](const Vec2d &v) -> Vec2d { return Vec2d(v.y(), - v.x()); }; - // Add a new point to the output, scale by CLIPPER_OFFSET_SCALE and round to ClipperLib::cInt. + // Add a new point to the output, rounded to coord_t. auto add_offset_point = [&out](Vec2d pt) { pt += Vec2d(0.5 - (pt.x() < 0), 0.5 - (pt.y() < 0)); - out.emplace_back(ClipperLib::cInt(pt.x()), ClipperLib::cInt(pt.y())); + out.emplace_back(coord_t(pt.x()), coord_t(pt.y())); }; // Minimum edge length, squared. @@ -1301,25 +1189,41 @@ ClipperLib::Path mittered_offset_path_scaled(const Points &contour, const std::v } } -#if 0 - { - ClipperLib::Path polytmp(out); - unscaleClipperPolygon(polytmp); - Slic3r::Polygon offsetted(std::move(polytmp)); - BoundingBox bbox = get_extents(contour); - bbox.merge(get_extents(offsetted)); - static int iRun = 0; - SVG svg(debug_out_path("mittered_offset_path_scaled-%d.svg", iRun ++).c_str(), bbox); - svg.draw_outline(Polygon(contour), "blue", scale_(0.01)); - svg.draw_outline(offsetted, "red", scale_(0.01)); - svg.draw(contour, "blue", scale_(0.03)); - svg.draw((Points)offsetted, "blue", scale_(0.03)); - } -#endif - return out; } +// Clipper1 united the raw contour offsets with the positive rule and the raw hole offsets with the negative rule. +template +static void variable_offset(const ExPolygon &expoly, const std::vector> &deltas, double miter_limit, TOut &out) +{ + auto unite = [](const Points &raw, C2::FillRule fill) { + C2::Paths64 united; + if (! raw.empty()) + c2_clip(C2::ClipType::Union, to_paths64(ClipperUtils::SinglePathProvider(raw)), {}, fill, united); + return united; + }; + + // 1) Offset the outer contour. + C2::Paths64 contours = unite(mittered_offset_path_scaled(expoly.contour.points, deltas.front(), miter_limit), C2::FillRule::Positive); +#ifndef NDEBUG + for (auto &c : contours) + assert(C2::Area(c) > 0.); +#endif /* NDEBUG */ + + // 2) Offset the holes one by one, collect the results. + C2::Paths64 holes; + holes.reserve(expoly.holes.size()); + for (const Polygon& hole : expoly.holes) + append(holes, unite(mittered_offset_path_scaled(hole.points, deltas[1 + &hole - expoly.holes.data()], miter_limit), C2::FillRule::Negative)); +#ifndef NDEBUG + for (auto &c : holes) + assert(C2::Area(c) > 0.); +#endif /* NDEBUG */ + + // 3) Subtract holes from the contours. + c2_clip(C2::ClipType::Difference, contours, holes, C2::FillRule::NonZero, out); +} + Polygons variable_offset_inner(const ExPolygon &expoly, const std::vector> &deltas, double miter_limit) { #ifndef NDEBUG @@ -1330,126 +1234,41 @@ Polygons variable_offset_inner(const ExPolygon &expoly, const std::vector 0.); -#endif /* NDEBUG */ - - // 2) Offset the holes one by one, collect the results. - ClipperLib::Paths holes; - holes.reserve(expoly.holes.size()); - for (const Polygon& hole : expoly.holes) - append(holes, fix_after_outer_offset(mittered_offset_path_scaled(hole.points, deltas[1 + &hole - expoly.holes.data()], miter_limit), ClipperLib::pftNegative, false)); -#ifndef NDEBUG - for (auto &c : holes) - assert(ClipperLib::Area(c) > 0.); -#endif /* NDEBUG */ - - // 3) Subtract holes from the contours. - ClipperLib::Paths output; - if (holes.empty()) - output = std::move(contours); - else { - ClipperLib::Clipper clipper; - clipper.Clear(); - clipper.AddPaths(contours, ClipperLib::ptSubject, true); - clipper.AddPaths(holes, ClipperLib::ptClip, true); - clipper.Execute(ClipperLib::ctDifference, output, ClipperLib::pftNonZero, ClipperLib::pftNonZero); - } - - return to_polygons(std::move(output)); + C2::Paths64 out; + variable_offset(expoly, deltas, miter_limit, out); + return c2_to_polygons(out); } Polygons variable_offset_outer(const ExPolygon &expoly, const std::vector> &deltas, double miter_limit) { #ifndef NDEBUG - // Verify that the deltas are all non positive. -for (const std::vector& ds : deltas) + // Verify that the deltas are all non negative. + for (const std::vector& ds : deltas) for (float delta : ds) assert(delta >= 0.); assert(expoly.holes.size() + 1 == deltas.size()); #endif /* NDEBUG */ - // 1) Offset the outer contour. - ClipperLib::Paths contours = fix_after_outer_offset(mittered_offset_path_scaled(expoly.contour.points, deltas.front(), miter_limit), ClipperLib::pftPositive, false); -#ifndef NDEBUG - for (auto &c : contours) - assert(ClipperLib::Area(c) > 0.); -#endif /* NDEBUG */ - - // 2) Offset the holes one by one, collect the results. - ClipperLib::Paths holes; - holes.reserve(expoly.holes.size()); - for (const Polygon& hole : expoly.holes) - append(holes, fix_after_inner_offset(mittered_offset_path_scaled(hole.points, deltas[1 + &hole - expoly.holes.data()], miter_limit), ClipperLib::pftPositive, true)); -#ifndef NDEBUG - for (auto &c : holes) - assert(ClipperLib::Area(c) > 0.); -#endif /* NDEBUG */ - - // 3) Subtract holes from the contours. - ClipperLib::Paths output; - if (holes.empty()) - output = std::move(contours); - else { - ClipperLib::Clipper clipper; - clipper.Clear(); - clipper.AddPaths(contours, ClipperLib::ptSubject, true); - clipper.AddPaths(holes, ClipperLib::ptClip, true); - clipper.Execute(ClipperLib::ctDifference, output, ClipperLib::pftNonZero, ClipperLib::pftNonZero); - } - - return to_polygons(std::move(output)); + C2::Paths64 out; + variable_offset(expoly, deltas, miter_limit, out); + return c2_to_polygons(out); } ExPolygons variable_offset_outer_ex(const ExPolygon &expoly, const std::vector> &deltas, double miter_limit) { #ifndef NDEBUG - // Verify that the deltas are all non positive. -for (const std::vector& ds : deltas) + // Verify that the deltas are all non negative. + for (const std::vector& ds : deltas) for (float delta : ds) assert(delta >= 0.); assert(expoly.holes.size() + 1 == deltas.size()); #endif /* NDEBUG */ - // 1) Offset the outer contour. - ClipperLib::Paths contours = fix_after_outer_offset(mittered_offset_path_scaled(expoly.contour.points, deltas.front(), miter_limit), ClipperLib::pftPositive, false); -#ifndef NDEBUG - for (auto &c : contours) - assert(ClipperLib::Area(c) > 0.); -#endif /* NDEBUG */ - - // 2) Offset the holes one by one, collect the results. - ClipperLib::Paths holes; - holes.reserve(expoly.holes.size()); - for (const Polygon& hole : expoly.holes) - append(holes, fix_after_inner_offset(mittered_offset_path_scaled(hole.points, deltas[1 + &hole - expoly.holes.data()], miter_limit), ClipperLib::pftPositive, true)); -#ifndef NDEBUG - for (auto &c : holes) - assert(ClipperLib::Area(c) > 0.); -#endif /* NDEBUG */ - - // 3) Subtract holes from the contours. - ExPolygons output; - if (holes.empty()) { - output.reserve(contours.size()); - for (ClipperLib::Path &path : contours) - output.emplace_back(std::move(path)); - } else { - ClipperLib::Clipper clipper; - clipper.AddPaths(contours, ClipperLib::ptSubject, true); - clipper.AddPaths(holes, ClipperLib::ptClip, true); - ClipperLib::PolyTree polytree; - clipper.Execute(ClipperLib::ctDifference, polytree, ClipperLib::pftNonZero, ClipperLib::pftNonZero); - output = PolyTreeToExPolygons(std::move(polytree)); - } - - return output; + C2::PolyTree64 out; + variable_offset(expoly, deltas, miter_limit, out); + return c2_to_expolygons(out); } - ExPolygons variable_offset_inner_ex(const ExPolygon &expoly, const std::vector> &deltas, double miter_limit) { #ifndef NDEBUG @@ -1460,39 +1279,9 @@ ExPolygons variable_offset_inner_ex(const ExPolygon &expoly, const std::vector 0.); -#endif /* NDEBUG */ - - // 2) Offset the holes one by one, collect the results. - ClipperLib::Paths holes; - holes.reserve(expoly.holes.size()); - for (const Polygon& hole : expoly.holes) - append(holes, fix_after_outer_offset(mittered_offset_path_scaled(hole.points, deltas[1 + &hole - expoly.holes.data()], miter_limit), ClipperLib::pftNegative, false)); -#ifndef NDEBUG - for (auto &c : holes) - assert(ClipperLib::Area(c) > 0.); -#endif /* NDEBUG */ - - // 3) Subtract holes from the contours. - ExPolygons output; - if (holes.empty()) { - output.reserve(contours.size()); - for (ClipperLib::Path &path : contours) - output.emplace_back(std::move(path)); - } else { - ClipperLib::Clipper clipper; - clipper.AddPaths(contours, ClipperLib::ptSubject, true); - clipper.AddPaths(holes, ClipperLib::ptClip, true); - ClipperLib::PolyTree polytree; - clipper.Execute(ClipperLib::ctDifference, polytree, ClipperLib::pftNonZero, ClipperLib::pftNonZero); - output = PolyTreeToExPolygons(std::move(polytree)); - } - - return output; + C2::PolyTree64 out; + variable_offset(expoly, deltas, miter_limit, out); + return c2_to_expolygons(out); } Pointfs make_counter_clockwise(const Pointfs& pointfs) diff --git a/src/libslic3r/ClipperUtils.hpp b/src/libslic3r/ClipperUtils.hpp index 5ed161c27a..5f05c8e3c4 100644 --- a/src/libslic3r/ClipperUtils.hpp +++ b/src/libslic3r/ClipperUtils.hpp @@ -2,23 +2,32 @@ #define slic3r_ClipperUtils_hpp_ #include "libslic3r.h" -#include "clipper.hpp" #include "ExPolygon.hpp" #include "Polygon.hpp" #include "Surface.hpp" +namespace Slic3r { + +// Offset joins and ends, fill rules and boolean operations, named as in Clipper1. +enum JoinType { jtSquare, jtRound, jtMiter }; +enum EndType { etClosedPolygon, etClosedLine, etOpenButt, etOpenSquare, etOpenRound }; +enum PolyFillType { pftEvenOdd, pftNonZero, pftPositive, pftNegative }; +enum ClipType { ctIntersection, ctUnion, ctDifference, ctXor }; + +} // namespace Slic3r + // import these wherever we're included -using Slic3r::ClipperLib::jtMiter; -using Slic3r::ClipperLib::jtRound; -using Slic3r::ClipperLib::jtSquare; +using Slic3r::jtMiter; +using Slic3r::jtRound; +using Slic3r::jtSquare; namespace Slic3r { static constexpr const float ClipperSafetyOffset = 10.f; -static constexpr const Slic3r::ClipperLib::JoinType DefaultJoinType = Slic3r::ClipperLib::jtMiter; +static constexpr const JoinType DefaultJoinType = jtMiter; -static constexpr const Slic3r::ClipperLib::EndType DefaultEndType = Slic3r::ClipperLib::etOpenButt; +static constexpr const EndType DefaultEndType = etOpenButt; //FIXME evaluate the default miter limit. 3 seems to be extreme, Cura uses 1.2. // Mitter Limit 3 is useful for perimeter generator, where sharp corners are extruded without needing a gap fill. @@ -26,7 +35,7 @@ static constexpr const Slic3r::ClipperLib::EndType DefaultEndType = Sl // is extended excessively. static constexpr const double DefaultMiterLimit = 3.; -static constexpr const Slic3r::ClipperLib::JoinType DefaultLineJoinType = Slic3r::ClipperLib::jtSquare; +static constexpr const JoinType DefaultLineJoinType = jtSquare; // Miter limit is ignored for jtSquare. static constexpr const double DefaultLineMiterLimit = 0.; @@ -304,12 +313,12 @@ namespace ClipperUtils { }; - // For ClipperLib with Z coordinates. + // Points with a Z coordinate. using ZPoint = Vec3i32; using ZPoints = std::vector; // Clip source polygon to be used as a clipping polygon with a bouding box around the source (to be clipped) polygon. - // Useful as an optimization for expensive ClipperLib operations, for example when clipping source polygons one by one + // Useful as an optimization for expensive Clipper operations, for example when clipping source polygons one by one // with a set of polygons covering the whole layer below. void clip_clipper_polygon_with_subject_bbox(const Points &src, const BoundingBox &bbox, Points &out, const bool get_entire_polygons = false); void clip_clipper_polygon_with_subject_bbox(const ZPoints &src, const BoundingBox &bbox, ZPoints &out); @@ -323,31 +332,28 @@ namespace ClipperUtils { } -// Perform union of input polygons using the non-zero rule, convert to ExPolygons. -ExPolygons ClipperPaths_to_Slic3rExPolygons(const ClipperLib::Paths &input, bool do_union = false); - // offset Polygons // Wherever applicable, please use the expand() / shrink() variants instead, they convey their purpose better. -Slic3r::Polygons offset(const Slic3r::Polygon &polygon, const float delta, ClipperLib::JoinType joinType = DefaultJoinType, double miterLimit = DefaultMiterLimit); +Slic3r::Polygons offset(const Slic3r::Polygon &polygon, const float delta, JoinType joinType = DefaultJoinType, double miterLimit = DefaultMiterLimit); // offset Polylines // Wherever applicable, please use the expand() / shrink() variants instead, they convey their purpose better. // Input polygons for negative offset shall be "normalized": There must be no overlap / intersections between the input polygons. -Slic3r::Polygons offset(const Slic3r::Polyline &polyline, const float delta, ClipperLib::JoinType joinType = DefaultLineJoinType, double miterLimit = DefaultLineMiterLimit, ClipperLib::EndType end_type = DefaultEndType); -Slic3r::Polygons offset(const Slic3r::Polyline3 &polyline, const float delta, ClipperLib::JoinType joinType = DefaultLineJoinType, double miterLimit = DefaultLineMiterLimit, ClipperLib::EndType end_type = DefaultEndType); -Slic3r::Polygons offset(const Slic3r::Polylines &polylines, const float delta, ClipperLib::JoinType joinType = DefaultLineJoinType, double miterLimit = DefaultLineMiterLimit, ClipperLib::EndType end_type = DefaultEndType); -Slic3r::Polygons offset(const Slic3r::Polygons &polygons, const float delta, ClipperLib::JoinType joinType = DefaultJoinType, double miterLimit = DefaultMiterLimit); -Slic3r::Polygons offset(const Slic3r::ExPolygon &expolygon, const float delta, ClipperLib::JoinType joinType = DefaultJoinType, double miterLimit = DefaultMiterLimit); -Slic3r::Polygons offset(const Slic3r::ExPolygons &expolygons, const float delta, ClipperLib::JoinType joinType = DefaultJoinType, double miterLimit = DefaultMiterLimit); -Slic3r::Polygons offset(const Slic3r::Surfaces &surfaces, const float delta, ClipperLib::JoinType joinType = DefaultJoinType, double miterLimit = DefaultMiterLimit); -Slic3r::Polygons offset(const Slic3r::SurfacesPtr &surfaces, const float delta, ClipperLib::JoinType joinType = DefaultJoinType, double miterLimit = DefaultMiterLimit); -Slic3r::ExPolygons offset_ex(const Slic3r::Polygons &polygons, const float delta, ClipperLib::JoinType joinType = DefaultJoinType, double miterLimit = DefaultMiterLimit); -Slic3r::ExPolygons offset_ex(const Slic3r::ExPolygon &expolygon, const float delta, ClipperLib::JoinType joinType = DefaultJoinType, double miterLimit = DefaultMiterLimit); -Slic3r::ExPolygons offset_ex(const Slic3r::ExPolygons &expolygons, const float delta, ClipperLib::JoinType joinType = DefaultJoinType, double miterLimit = DefaultMiterLimit); -Slic3r::ExPolygons offset_ex(const Slic3r::Surfaces &surfaces, const float delta, ClipperLib::JoinType joinType = DefaultJoinType, double miterLimit = DefaultMiterLimit); -Slic3r::ExPolygons offset_ex(const Slic3r::SurfacesPtr &surfaces, const float delta, ClipperLib::JoinType joinType = DefaultJoinType, double miterLimit = DefaultMiterLimit); +Slic3r::Polygons offset(const Slic3r::Polyline &polyline, const float delta, JoinType joinType = DefaultLineJoinType, double miterLimit = DefaultLineMiterLimit, EndType end_type = DefaultEndType); +Slic3r::Polygons offset(const Slic3r::Polyline3 &polyline, const float delta, JoinType joinType = DefaultLineJoinType, double miterLimit = DefaultLineMiterLimit, EndType end_type = DefaultEndType); +Slic3r::Polygons offset(const Slic3r::Polylines &polylines, const float delta, JoinType joinType = DefaultLineJoinType, double miterLimit = DefaultLineMiterLimit, EndType end_type = DefaultEndType); +Slic3r::Polygons offset(const Slic3r::Polygons &polygons, const float delta, JoinType joinType = DefaultJoinType, double miterLimit = DefaultMiterLimit); +Slic3r::Polygons offset(const Slic3r::ExPolygon &expolygon, const float delta, JoinType joinType = DefaultJoinType, double miterLimit = DefaultMiterLimit); +Slic3r::Polygons offset(const Slic3r::ExPolygons &expolygons, const float delta, JoinType joinType = DefaultJoinType, double miterLimit = DefaultMiterLimit); +Slic3r::Polygons offset(const Slic3r::Surfaces &surfaces, const float delta, JoinType joinType = DefaultJoinType, double miterLimit = DefaultMiterLimit); +Slic3r::Polygons offset(const Slic3r::SurfacesPtr &surfaces, const float delta, JoinType joinType = DefaultJoinType, double miterLimit = DefaultMiterLimit); +Slic3r::ExPolygons offset_ex(const Slic3r::Polygons &polygons, const float delta, JoinType joinType = DefaultJoinType, double miterLimit = DefaultMiterLimit); +Slic3r::ExPolygons offset_ex(const Slic3r::ExPolygon &expolygon, const float delta, JoinType joinType = DefaultJoinType, double miterLimit = DefaultMiterLimit); +Slic3r::ExPolygons offset_ex(const Slic3r::ExPolygons &expolygons, const float delta, JoinType joinType = DefaultJoinType, double miterLimit = DefaultMiterLimit); +Slic3r::ExPolygons offset_ex(const Slic3r::Surfaces &surfaces, const float delta, JoinType joinType = DefaultJoinType, double miterLimit = DefaultMiterLimit); +Slic3r::ExPolygons offset_ex(const Slic3r::SurfacesPtr &surfaces, const float delta, JoinType joinType = DefaultJoinType, double miterLimit = DefaultMiterLimit); // BBS -inline Slic3r::ExPolygons offset_ex(const Slic3r::Polygon &polygon, const float delta, ClipperLib::JoinType joinType = DefaultJoinType, double miterLimit = DefaultMiterLimit) +inline Slic3r::ExPolygons offset_ex(const Slic3r::Polygon &polygon, const float delta, JoinType joinType = DefaultJoinType, double miterLimit = DefaultMiterLimit) { Slic3r::Polygons temp; temp.push_back(polygon); @@ -356,8 +362,8 @@ inline Slic3r::ExPolygons offset_ex(const Slic3r::Polygon &polygon, const float } // convert stroke to path by offsetting of contour -Polygons contour_to_polygons(const Polygon &polygon, const float line_width, ClipperLib::JoinType join_type = DefaultJoinType, double miter_limit = DefaultMiterLimit); -Polygons contour_to_polygons(const Polygons &polygon, const float line_width, ClipperLib::JoinType join_type = DefaultJoinType, double miter_limit = DefaultMiterLimit); +Polygons contour_to_polygons(const Polygon &polygon, const float line_width, JoinType join_type = DefaultJoinType, double miter_limit = DefaultMiterLimit); +Polygons contour_to_polygons(const Polygons &polygon, const float line_width, JoinType join_type = DefaultJoinType, double miter_limit = DefaultMiterLimit); inline Slic3r::Polygons union_safety_offset (const Slic3r::Polygons &polygons) { return offset (polygons, ClipperSafetyOffset); } inline Slic3r::Polygons union_safety_offset (const Slic3r::ExPolygons &expolygons) { return offset (expolygons, ClipperSafetyOffset); } @@ -370,62 +376,62 @@ Slic3r::ExPolygons union_safety_offset_ex(const Slic3r::Polygons &polygons); Slic3r::ExPolygons union_safety_offset_ex(const Slic3r::ExPolygons &expolygons); // Aliases for the various offset(...) functions, conveying the purpose of the offset. -inline Slic3r::Polygons expand(const Slic3r::Polygon &polygon, const float delta, ClipperLib::JoinType joinType = DefaultJoinType, double miterLimit = DefaultMiterLimit) +inline Slic3r::Polygons expand(const Slic3r::Polygon &polygon, const float delta, JoinType joinType = DefaultJoinType, double miterLimit = DefaultMiterLimit) { assert(delta > 0); return offset(polygon, delta, joinType, miterLimit); } -inline Slic3r::Polygons expand(const Slic3r::Polygons &polygons, const float delta, ClipperLib::JoinType joinType = DefaultJoinType, double miterLimit = DefaultMiterLimit) +inline Slic3r::Polygons expand(const Slic3r::Polygons &polygons, const float delta, JoinType joinType = DefaultJoinType, double miterLimit = DefaultMiterLimit) { assert(delta > 0); return offset(polygons, delta, joinType, miterLimit); } -inline Slic3r::Polygons expand(const Slic3r::ExPolygons &polygons, const float delta, ClipperLib::JoinType joinType = DefaultJoinType, double miterLimit = DefaultMiterLimit) +inline Slic3r::Polygons expand(const Slic3r::ExPolygons &polygons, const float delta, JoinType joinType = DefaultJoinType, double miterLimit = DefaultMiterLimit) { assert(delta > 0); return offset(polygons, delta, joinType, miterLimit); } -inline Slic3r::ExPolygons expand_ex(const Slic3r::Polygons &polygons, const float delta, ClipperLib::JoinType joinType = DefaultJoinType, double miterLimit = DefaultMiterLimit) +inline Slic3r::ExPolygons expand_ex(const Slic3r::Polygons &polygons, const float delta, JoinType joinType = DefaultJoinType, double miterLimit = DefaultMiterLimit) { assert(delta > 0); return offset_ex(polygons, delta, joinType, miterLimit); } // Input polygons for shrinking shall be "normalized": There must be no overlap / intersections between the input polygons. -inline Slic3r::Polygons shrink(const Slic3r::Polygons &polygons, const float delta, ClipperLib::JoinType joinType = DefaultJoinType, double miterLimit = DefaultMiterLimit) +inline Slic3r::Polygons shrink(const Slic3r::Polygons &polygons, const float delta, JoinType joinType = DefaultJoinType, double miterLimit = DefaultMiterLimit) { assert(delta > 0); return offset(polygons, -delta, joinType, miterLimit); } -inline Slic3r::ExPolygons shrink_ex(const Slic3r::Polygons &polygons, const float delta, ClipperLib::JoinType joinType = DefaultJoinType, double miterLimit = DefaultMiterLimit) +inline Slic3r::ExPolygons shrink_ex(const Slic3r::Polygons &polygons, const float delta, JoinType joinType = DefaultJoinType, double miterLimit = DefaultMiterLimit) { assert(delta > 0); return offset_ex(polygons, -delta, joinType, miterLimit); } -inline Slic3r::ExPolygons shrink_ex(const Slic3r::ExPolygons &polygons, const float delta, ClipperLib::JoinType joinType = DefaultJoinType, double miterLimit = DefaultMiterLimit) +inline Slic3r::ExPolygons shrink_ex(const Slic3r::ExPolygons &polygons, const float delta, JoinType joinType = DefaultJoinType, double miterLimit = DefaultMiterLimit) { assert(delta > 0); return offset_ex(polygons, -delta, joinType, miterLimit); } // Wherever applicable, please use the opening() / closing() variants instead, they convey their purpose better. // Input polygons for negative offset shall be "normalized": There must be no overlap / intersections between the input polygons. -Slic3r::Polygons offset2(const Slic3r::ExPolygons &expolygons, const float delta1, const float delta2, ClipperLib::JoinType joinType = DefaultJoinType, double miterLimit = DefaultMiterLimit); -Slic3r::ExPolygons offset2_ex(const Slic3r::ExPolygons &expolygons, const float delta1, const float delta2, ClipperLib::JoinType joinType = DefaultJoinType, double miterLimit = DefaultMiterLimit); -Slic3r::ExPolygons offset2_ex(const Slic3r::Surfaces &surfaces, const float delta1, const float delta2, ClipperLib::JoinType joinType = DefaultJoinType, double miterLimit = DefaultMiterLimit); +Slic3r::Polygons offset2(const Slic3r::ExPolygons &expolygons, const float delta1, const float delta2, JoinType joinType = DefaultJoinType, double miterLimit = DefaultMiterLimit); +Slic3r::ExPolygons offset2_ex(const Slic3r::ExPolygons &expolygons, const float delta1, const float delta2, JoinType joinType = DefaultJoinType, double miterLimit = DefaultMiterLimit); +Slic3r::ExPolygons offset2_ex(const Slic3r::Surfaces &surfaces, const float delta1, const float delta2, JoinType joinType = DefaultJoinType, double miterLimit = DefaultMiterLimit); // BBS -Slic3r::ExPolygons _clipper_ex(ClipperLib::ClipType clipType, +Slic3r::ExPolygons _clipper_ex(ClipType clipType, const Slic3r::Polygons &subject, const Slic3r::Polygons &clip, bool safety_offset_ = false); // Offset outside, then inside produces morphological closing. All deltas should be positive. -Slic3r::Polygons closing(const Slic3r::Polygons &polygons, const float delta1, const float delta2, ClipperLib::JoinType joinType = DefaultJoinType, double miterLimit = DefaultMiterLimit); -inline Slic3r::Polygons closing(const Slic3r::Polygons &polygons, const float delta, ClipperLib::JoinType joinType = DefaultJoinType, double miterLimit = DefaultMiterLimit) +Slic3r::Polygons closing(const Slic3r::Polygons &polygons, const float delta1, const float delta2, JoinType joinType = DefaultJoinType, double miterLimit = DefaultMiterLimit); +inline Slic3r::Polygons closing(const Slic3r::Polygons &polygons, const float delta, JoinType joinType = DefaultJoinType, double miterLimit = DefaultMiterLimit) { return closing(polygons, delta, delta, joinType, miterLimit); } -Slic3r::ExPolygons closing_ex(const Slic3r::Polygons &polygons, const float delta1, const float delta2, ClipperLib::JoinType joinType = DefaultJoinType, double miterLimit = DefaultMiterLimit); -inline Slic3r::ExPolygons closing_ex(const Slic3r::Polygons &polygons, const float delta, ClipperLib::JoinType joinType = DefaultJoinType, double miterLimit = DefaultMiterLimit) +Slic3r::ExPolygons closing_ex(const Slic3r::Polygons &polygons, const float delta1, const float delta2, JoinType joinType = DefaultJoinType, double miterLimit = DefaultMiterLimit); +inline Slic3r::ExPolygons closing_ex(const Slic3r::Polygons &polygons, const float delta, JoinType joinType = DefaultJoinType, double miterLimit = DefaultMiterLimit) { return closing_ex(polygons, delta, delta, joinType, miterLimit); } -inline Slic3r::ExPolygons closing_ex(const Slic3r::ExPolygons &polygons, const float delta, ClipperLib::JoinType joinType = DefaultJoinType, double miterLimit = DefaultMiterLimit) +inline Slic3r::ExPolygons closing_ex(const Slic3r::ExPolygons &polygons, const float delta, JoinType joinType = DefaultJoinType, double miterLimit = DefaultMiterLimit) { assert(delta > 0); return offset2_ex(polygons, delta, - delta, joinType, miterLimit); } -inline Slic3r::ExPolygons closing_ex(const Slic3r::Surfaces &surfaces, const float delta, ClipperLib::JoinType joinType = DefaultJoinType, double miterLimit = DefaultMiterLimit) +inline Slic3r::ExPolygons closing_ex(const Slic3r::Surfaces &surfaces, const float delta, JoinType joinType = DefaultJoinType, double miterLimit = DefaultMiterLimit) { assert(delta > 0); return offset2_ex(surfaces, delta, - delta, joinType, miterLimit); } // Offset inside, then outside produces morphological opening. All deltas should be positive. // Input polygons for opening shall be "normalized": There must be no overlap / intersections between the input polygons. -Slic3r::Polygons opening(const Slic3r::Polygons &polygons, const float delta1, const float delta2, ClipperLib::JoinType joinType = DefaultJoinType, double miterLimit = DefaultMiterLimit); -Slic3r::Polygons opening(const Slic3r::ExPolygons &expolygons, const float delta1, const float delta2, ClipperLib::JoinType joinType = DefaultJoinType, double miterLimit = DefaultMiterLimit); -Slic3r::Polygons opening(const Slic3r::Surfaces &surfaces, const float delta1, const float delta2, ClipperLib::JoinType joinType = DefaultJoinType, double miterLimit = DefaultMiterLimit); -inline Slic3r::Polygons opening(const Slic3r::Polygons &polygons, const float delta, ClipperLib::JoinType joinType = DefaultJoinType, double miterLimit = DefaultMiterLimit) +Slic3r::Polygons opening(const Slic3r::Polygons &polygons, const float delta1, const float delta2, JoinType joinType = DefaultJoinType, double miterLimit = DefaultMiterLimit); +Slic3r::Polygons opening(const Slic3r::ExPolygons &expolygons, const float delta1, const float delta2, JoinType joinType = DefaultJoinType, double miterLimit = DefaultMiterLimit); +Slic3r::Polygons opening(const Slic3r::Surfaces &surfaces, const float delta1, const float delta2, JoinType joinType = DefaultJoinType, double miterLimit = DefaultMiterLimit); +inline Slic3r::Polygons opening(const Slic3r::Polygons &polygons, const float delta, JoinType joinType = DefaultJoinType, double miterLimit = DefaultMiterLimit) { return opening(polygons, delta, delta, joinType, miterLimit); } -inline Slic3r::Polygons opening(const Slic3r::ExPolygons &expolygons, const float delta, ClipperLib::JoinType joinType = DefaultJoinType, double miterLimit = DefaultMiterLimit) +inline Slic3r::Polygons opening(const Slic3r::ExPolygons &expolygons, const float delta, JoinType joinType = DefaultJoinType, double miterLimit = DefaultMiterLimit) { return opening(expolygons, delta, delta, joinType, miterLimit); } -inline Slic3r::Polygons opening(const Slic3r::Surfaces &surfaces, const float delta, ClipperLib::JoinType joinType = DefaultJoinType, double miterLimit = DefaultMiterLimit) +inline Slic3r::Polygons opening(const Slic3r::Surfaces &surfaces, const float delta, JoinType joinType = DefaultJoinType, double miterLimit = DefaultMiterLimit) { return opening(surfaces, delta, delta, joinType, miterLimit); } -inline Slic3r::ExPolygons opening_ex(const Slic3r::ExPolygons &polygons, const float delta, ClipperLib::JoinType joinType = DefaultJoinType, double miterLimit = DefaultMiterLimit) +inline Slic3r::ExPolygons opening_ex(const Slic3r::ExPolygons &polygons, const float delta, JoinType joinType = DefaultJoinType, double miterLimit = DefaultMiterLimit) { assert(delta > 0); return offset2_ex(polygons, - delta, delta, joinType, miterLimit); } -inline Slic3r::ExPolygons opening_ex(const Slic3r::Surfaces &surfaces, const float delta, ClipperLib::JoinType joinType = DefaultJoinType, double miterLimit = DefaultMiterLimit) +inline Slic3r::ExPolygons opening_ex(const Slic3r::Surfaces &surfaces, const float delta, JoinType joinType = DefaultJoinType, double miterLimit = DefaultMiterLimit) { assert(delta > 0); return offset2_ex(surfaces, - delta, delta, joinType, miterLimit); } -Slic3r::Lines _clipper_ln(ClipperLib::ClipType clipType, const Slic3r::Lines &subject, const Slic3r::Polygons &clip); +Slic3r::Lines _clipper_ln(ClipType clipType, const Slic3r::Lines &subject, const Slic3r::Polygons &clip); // Safety offset is applied to the clipping polygons only. Slic3r::Polygons diff(const Slic3r::Polygon &subject, const Slic3r::Polygon &clip, ApplySafetyOffset do_safety_offset = ApplySafetyOffset::No); @@ -489,13 +495,14 @@ inline Slic3r::ExPolygons diff_ex(const Slic3r::ExPolygons& subject, const Slic3 inline Slic3r::Lines diff_ln(const Slic3r::Lines &subject, const Slic3r::Polygons &clip) { - return _clipper_ln(ClipperLib::ctDifference, subject, clip); + return _clipper_ln(ctDifference, subject, clip); } // Safety offset is applied to the clipping polygons only. Slic3r::Polygons intersection(const Slic3r::Polygon &subject, const Slic3r::Polygon &clip, ApplySafetyOffset do_safety_offset = ApplySafetyOffset::No); Slic3r::Polygons intersection(const Slic3r::Polygons &subject, const Slic3r::ExPolygon &clip, ApplySafetyOffset do_safety_offset = ApplySafetyOffset::No); Slic3r::Polygons intersection(const Slic3r::Polygons &subject, const Slic3r::Polygons &clip, ApplySafetyOffset do_safety_offset = ApplySafetyOffset::No); +Slic3r::Polygons intersection(const Slic3r::Polygons &subject, const Slic3r::Polygons &clip, PolyFillType fill_type); Slic3r::Polygons intersection(const Slic3r::ExPolygon &subject, const Slic3r::ExPolygon &clip, ApplySafetyOffset do_safety_offset = ApplySafetyOffset::No); // Optimized version clipping the "clipping" polygon using clip_clipper_polygon_with_subject_bbox(). // To be used with complex clipping polygons, where majority of the clipping polygons are outside of the source polygon. @@ -534,135 +541,42 @@ void restore_source_path_order(const Slic3r::Polyline &source, Slic3r::Polylines inline Slic3r::Lines intersection_ln(const Slic3r::Lines &subject, const Slic3r::Polygons &clip) { - return _clipper_ln(ClipperLib::ctIntersection, subject, clip); + return _clipper_ln(ctIntersection, subject, clip); } inline Slic3r::Lines intersection_ln(const Slic3r::Line &subject, const Slic3r::Polygons &clip) { Slic3r::Lines lines; lines.emplace_back(subject); - return _clipper_ln(ClipperLib::ctIntersection, lines, clip); + return _clipper_ln(ctIntersection, lines, clip); } Slic3r::Polygons union_(const Slic3r::Polygons &subject); Slic3r::Polygons union_(const Slic3r::ExPolygons &subject); -Slic3r::Polygons union_(const Slic3r::Polygons &subject, const ClipperLib::PolyFillType fillType); +Slic3r::Polygons union_(const Slic3r::Polygons &subject, const PolyFillType fillType); Slic3r::Polygons union_(const Slic3r::Polygons &subject, const Slic3r::Polygons &subject2); // May be used to "heal" unusual models (3DLabPrints etc.) by providing fill_type (pftEvenOdd, pftNonZero, pftPositive, pftNegative). -Slic3r::ExPolygons union_ex(const Slic3r::Polygons &subject, ClipperLib::PolyFillType fill_type = ClipperLib::pftNonZero); +Slic3r::ExPolygons union_ex(const Slic3r::Polygons &subject, PolyFillType fill_type = pftNonZero); Slic3r::ExPolygons union_ex(const Slic3r::ExPolygons &subject); Slic3r::ExPolygons union_ex(const Slic3r::ExPolygons &subject, const Slic3r::Polygons &subject2); Slic3r::ExPolygons union_ex(const Slic3r::Surfaces &subject); Slic3r::ExPolygons union_ex(const Slic3r::ExPolygons& poly1, const Slic3r::ExPolygons& poly2, bool safety_offset_ = false); -// Convert polygons / expolygons into ClipperLib::PolyTree using ClipperLib::pftEvenOdd, thus union will NOT be performed. -// If the contours are not intersecting, their orientation shall not be modified by union_pt(). -ClipperLib::PolyTree union_pt(const Slic3r::Polygons &subject); -ClipperLib::PolyTree union_pt(const Slic3r::ExPolygons &subject); - Slic3r::ExPolygons xor_ex(const Slic3r::ExPolygons &subject, const Slic3r::ExPolygon &clip, ApplySafetyOffset do_safety_offset = ApplySafetyOffset::No); Slic3r::ExPolygons xor_ex(const Slic3r::ExPolygons &subject, const Slic3r::ExPolygons &clip, ApplySafetyOffset do_safety_offset = ApplySafetyOffset::No); Slic3r::Polygons union_pt_chained_outside_in(const Slic3r::Polygons &subject); -ClipperLib::PolyNodes order_nodes(const ClipperLib::PolyNodes &nodes); - -// Implementing generalized loop (foreach) over a list of nodes which can be -// ordered or unordered (performance gain) based on template parameter -enum class e_ordering { - ON, - OFF -}; - -// Create a template struct, template functions can not be partially specialized -template struct _foreach_node { - void operator()(const ClipperLib::PolyNodes &nodes, Fn &&fn); -}; - -// Specialization with NO ordering -template struct _foreach_node { - void operator()(const ClipperLib::PolyNodes &nodes, Fn &&fn) - { - for (auto &n : nodes) fn(n); - } -}; - -// Specialization with ordering -template struct _foreach_node { - void operator()(const ClipperLib::PolyNodes &nodes, Fn &&fn) - { - auto ordered_nodes = order_nodes(nodes); - for (auto &n : nodes) fn(n); - } -}; - -// Wrapper function for the foreach_node which can deduce arguments automatically -template -void foreach_node(const ClipperLib::PolyNodes &nodes, Fn &&fn) -{ - _foreach_node()(nodes, std::forward(fn)); -} - -// Collecting polygons of the tree into a list of Polygons, holes have clockwise -// orientation. -template -void traverse_pt(const ClipperLib::PolyNode *tree, Polygons *out) -{ - if (!tree) return; // terminates recursion - - // Push the contour of the current level - out->emplace_back(tree->Contour); - - // Do the recursion for all the children. - traverse_pt(tree->Childs, out); -} - -// Collecting polygons of the tree into a list of ExPolygons. -template -void traverse_pt(const ClipperLib::PolyNode *tree, ExPolygons *out) -{ - if (!tree) return; - else if(tree->IsHole()) { - // Levels of holes are skipped and handled together with the - // contour levels. - traverse_pt(tree->Childs, out); - return; - } - - ExPolygon level; - level.contour.points = tree->Contour; - - foreach_node(tree->Childs, - [out, &level] (const ClipperLib::PolyNode *node) { - - // Holes are collected here. - level.holes.emplace_back(node->Contour); - - // By doing a recursion, a new level expoly is created with the contour - // and holes of the lower level. Doing this for all the childs. - traverse_pt(node->Childs, out); - }); - - out->emplace_back(level); -} - -template -void traverse_pt(const ClipperLib::PolyNodes &nodes, ExOrJustPolygons *retval) -{ - foreach_node(nodes, [&retval](const ClipperLib::PolyNode *node) { - traverse_pt(node, retval); - }); -} - - /* OTHER */ Slic3r::Polygons simplify_polygons(const Slic3r::Polygons &subject); Slic3r::ExPolygons simplify_polygons_ex(const Slic3r::Polygons &subject); Polygons top_level_islands(const Slic3r::Polygons &polygons); +// Top level expolygons of the even-odd union, the ones nested in their holes go to `nested`. +ExPolygons top_level_expolygons(const ExPolygons &expolygons, ExPolygons *nested = nullptr); -ClipperLib::Path mittered_offset_path_scaled(const Points &contour, const std::vector &deltas, double miter_limit); +Points mittered_offset_path_scaled(const Points &contour, const std::vector &deltas, double miter_limit); Polygons variable_offset_inner(const ExPolygon &expoly, const std::vector> &deltas, double miter_limit = 2.); Polygons variable_offset_outer(const ExPolygon &expoly, const std::vector> &deltas, double miter_limit = 2.); ExPolygons variable_offset_outer_ex(const ExPolygon &expoly, const std::vector> &deltas, double miter_limit = 2.); diff --git a/src/libslic3r/ClipperZUtils.cpp b/src/libslic3r/ClipperZUtils.cpp new file mode 100644 index 0000000000..f7a4b79b0d --- /dev/null +++ b/src/libslic3r/ClipperZUtils.cpp @@ -0,0 +1,77 @@ +#include "ClipperZUtils.hpp" + +#include + +namespace Slic3r { +namespace ClipperZUtils { + +namespace C2 = Clipper2Lib_Z; + +static C2::Paths64 to_paths64(const ZPaths &paths) +{ + C2::Paths64 out; + out.reserve(paths.size()); + for (const ZPath &path : paths) { + C2::Path64 &dst = out.emplace_back(); + dst.reserve(path.size()); + for (const ZPoint &pt : path) + dst.emplace_back(pt.x(), pt.y(), pt.z()); + } + return out; +} + +static void append_zpaths(const C2::Paths64 &paths, ZPaths &out) +{ + out.reserve(out.size() + paths.size()); + for (const C2::Path64 &path : paths) { + ZPath &dst = out.emplace_back(); + dst.reserve(path.size()); + for (const C2::Point64 &pt : path) + dst.emplace_back(pt.x, pt.y, pt.z); + } +} + +double area(const ZPath &path) +{ + const size_t size = path.size(); + if (size < 3) + return 0.; + double a = 0.; + for (size_t i = 0, j = size - 1; i < size; j = i ++) + a += (double(path[j].x()) + path[i].x()) * (double(path[j].y()) - path[i].y()); + return -a * 0.5; +} + +ZPaths clip_zpaths(ClipType clip_type, const ZPaths &subject, bool subject_open, const ZPaths &clip, const ZFillCallback &zfill, bool preserve_collinear) +{ + C2::Clipper64 clipper; + clipper.PreserveCollinear(preserve_collinear); + if (zfill) + clipper.SetZCallback([&zfill](const C2::Point64 &e1bot, const C2::Point64 &e1top, const C2::Point64 &e2bot, const C2::Point64 &e2top, C2::Point64 &pt) { + // Clipper2 already copied Z from a coincident input vertex. + auto coincident = [&pt](const C2::Point64 &p) { return p.x == pt.x && p.y == pt.y; }; + if (coincident(e1bot) || coincident(e1top) || coincident(e2bot) || coincident(e2top)) + return; + ZPoint zpt(pt.x, pt.y, pt.z); + zfill(ZPoint(e1bot.x, e1bot.y, e1bot.z), ZPoint(e1top.x, e1top.y, e1top.z), + ZPoint(e2bot.x, e2bot.y, e2bot.z), ZPoint(e2top.x, e2top.y, e2top.z), zpt); + pt.z = zpt.z(); + }); + if (subject_open) + clipper.AddOpenSubject(to_paths64(subject)); + else + clipper.AddSubject(to_paths64(subject)); + clipper.AddClip(to_paths64(clip)); + const C2::ClipType type = clip_type == ctIntersection ? C2::ClipType::Intersection : + clip_type == ctUnion ? C2::ClipType::Union : + clip_type == ctDifference ? C2::ClipType::Difference : C2::ClipType::Xor; + C2::Paths64 closed, open; + clipper.Execute(type, C2::FillRule::NonZero, closed, open); + ZPaths out; + append_zpaths(closed, out); + append_zpaths(open, out); + return out; +} + +} // namespace ClipperZUtils +} // namespace Slic3r diff --git a/src/libslic3r/ClipperZUtils.hpp b/src/libslic3r/ClipperZUtils.hpp index 7465bd31a4..159a4e81b3 100644 --- a/src/libslic3r/ClipperZUtils.hpp +++ b/src/libslic3r/ClipperZUtils.hpp @@ -1,10 +1,11 @@ #ifndef slic3r_ClipperZUtils_hpp_ #define slic3r_ClipperZUtils_hpp_ +#include #include #include -#include +#include #include #include @@ -12,10 +13,13 @@ namespace Slic3r { namespace ClipperZUtils { -using ZPoint = ClipperLib_Z::IntPoint; -using ZPoints = ClipperLib_Z::Path; -using ZPath = ClipperLib_Z::Path; -using ZPaths = ClipperLib_Z::Paths; +using ZPoint = Vec3crd; +using ZPoints = std::vector>; +using ZPath = ZPoints; +using ZPaths = std::vector>; + +// Sets Z of an intersection point from the edges crossing there. +using ZFillCallback = std::function; inline bool zpoint_lower(const ZPoint &l, const ZPoint &r) { @@ -101,6 +105,12 @@ inline VecOfPoints from_zpaths(const ZPaths &paths) return out; } +// Signed area, positive for a CCW path. +double area(const ZPath &path); + +// Non-zero rule boolean, the subject may be open. zfill only sees intersections off the input vertices. +ZPaths clip_zpaths(ClipType clip_type, const ZPaths &subject, bool subject_open, const ZPaths &clip, const ZFillCallback &zfill, bool preserve_collinear = false); + class ClipperZIntersectionVisitor { public: using Intersection = std::pair; @@ -126,8 +136,8 @@ public: } } } - ClipperLib_Z::ZFillCallback clipper_callback() { - return [this](const ZPoint &e1bot, const ZPoint &e1top, + ZFillCallback clipper_callback() { + return [this](const ZPoint &e1bot, const ZPoint &e1top, const ZPoint &e2bot, const ZPoint &e2top, ZPoint &pt) { return (*this)(e1bot, e1top, e2bot, e2top, pt); }; } diff --git a/src/libslic3r/CutSurface.cpp b/src/libslic3r/CutSurface.cpp index 606a5b8aaa..dbf737e3ed 100644 --- a/src/libslic3r/CutSurface.cpp +++ b/src/libslic3r/CutSurface.cpp @@ -3519,7 +3519,7 @@ ExPolygon priv::to_expoly(const SurfacePatch &patch, const Project &projection, { Polygons polys = unproject_loops(patch, projection, depth_range); // should not be used when no opposit triangle are counted so should not create overlaps - ClipperLib::PolyFillType fill_type = ClipperLib::PolyFillType::pftEvenOdd; + PolyFillType fill_type = pftEvenOdd; ExPolygons expolys = Slic3r::union_ex(polys, fill_type); if (expolys.size() == 1) return expolys.front(); diff --git a/src/libslic3r/ElephantFootCompensation.cpp b/src/libslic3r/ElephantFootCompensation.cpp index 0adff1ba4e..aaf18608ee 100644 --- a/src/libslic3r/ElephantFootCompensation.cpp +++ b/src/libslic3r/ElephantFootCompensation.cpp @@ -1,5 +1,3 @@ -#include "clipper/clipper_z.hpp" - #include "libslic3r.h" #include "ClipperUtils.hpp" #include "EdgeGrid.hpp" diff --git a/src/libslic3r/Emboss.cpp b/src/libslic3r/Emboss.cpp index 38c035806c..80a9274fd0 100644 --- a/src/libslic3r/Emboss.cpp +++ b/src/libslic3r/Emboss.cpp @@ -404,18 +404,89 @@ bool Emboss::divide_segments_for_close_point(ExPolygons &expolygons, double dist return true; } +// Clipper1's CleanPolygon(): removes vertices closer than `distance` to a neighbour or to the line through their neighbours. +static void clean_polygon(Points &points, double distance) +{ + auto points_are_close = [](const Point &a, const Point &b, double dist2) { + const double dx = double(a.x() - b.x()), dy = double(a.y() - b.y()); + return dx * dx + dy * dy <= dist2; + }; + auto distance_from_line2 = [](const Point &pt, const Point &ln1, const Point &ln2) { + const double A = double(ln1.y() - ln2.y()); + const double B = double(ln2.x() - ln1.x()); + const double C = A * pt.x() + B * pt.y() - (A * ln1.x() + B * ln1.y()); + return (C * C) / (A * A + B * B); + }; + auto slopes_near_collinear = [&distance_from_line2](const Point &pt1, const Point &pt2, const Point &pt3, double dist2) { + if (std::abs(pt1.x() - pt2.x()) > std::abs(pt1.y() - pt2.y())) { + if ((pt1.x() > pt2.x()) == (pt1.x() < pt3.x())) + return distance_from_line2(pt1, pt2, pt3) < dist2; + if ((pt2.x() > pt1.x()) == (pt2.x() < pt3.x())) + return distance_from_line2(pt2, pt1, pt3) < dist2; + return distance_from_line2(pt3, pt1, pt2) < dist2; + } + if ((pt1.y() > pt2.y()) == (pt1.y() < pt3.y())) + return distance_from_line2(pt1, pt2, pt3) < dist2; + if ((pt2.y() > pt1.y()) == (pt2.y() < pt3.y())) + return distance_from_line2(pt2, pt1, pt3) < dist2; + return distance_from_line2(pt3, pt1, pt2) < dist2; + }; + + size_t size = points.size(); + if (size == 0) + return; + std::vector next(size), prev(size); + std::vector done(size, 0); + for (size_t i = 0; i < size; ++i) { + next[i] = (i + 1) % size; + prev[next[i]] = i; + } + auto exclude = [&next, &prev, &done](size_t op) { + const size_t result = prev[op]; + next[result] = next[op]; + prev[next[op]] = result; + done[result] = 0; + return result; + }; + const double dist2 = distance * distance; + size_t op = 0; + while (! done[op] && next[op] != prev[op]) { + if (points_are_close(points[op], points[prev[op]], dist2)) { + op = exclude(op); + -- size; + } else if (points_are_close(points[prev[op]], points[next[op]], dist2)) { + exclude(next[op]); + op = exclude(op); + size -= 2; + } else if (slopes_near_collinear(points[prev[op]], points[op], points[next[op]], dist2)) { + op = exclude(op); + -- size; + } else { + done[op] = 1; + op = next[op]; + } + } + if (size < 3) + size = 0; + Points out; + out.reserve(size); + for (size_t i = 0; i < size; ++i) { + out.emplace_back(points[op]); + op = next[op]; + } + points = std::move(out); +} + HealedExPolygons Emboss::heal_polygons(const Polygons &shape, bool is_non_zero, unsigned int max_iteration) { const double clean_distance = 1.415; // little grater than sqrt(2) - ClipperLib::PolyFillType fill_type = is_non_zero ? - ClipperLib::pftNonZero : ClipperLib::pftEvenOdd; + PolyFillType fill_type = is_non_zero ? pftNonZero : pftEvenOdd; // When edit this code check that font 'ALIENATE.TTF' and glyph 'i' still work // fix of self intersections - // http://www.angusj.com/delphi/clipper/documentation/Docs/Units/ClipperLib/Functions/SimplifyPolygon.htm - ClipperLib::Paths paths = ClipperLib::SimplifyPolygons(ClipperUtils::PolygonsProvider(shape), fill_type); - ClipperLib::CleanPolygons(paths, clean_distance); - Polygons polygons = to_polygons(paths); + Polygons polygons = union_(shape, fill_type); + for (Polygon &polygon : polygons) + clean_polygon(polygon.points, clean_distance); polygons.erase(std::remove_if(polygons.begin(), polygons.end(), [](const Polygon &p) { return p.size() < 3; }), polygons.end()); diff --git a/src/libslic3r/Emboss.hpp b/src/libslic3r/Emboss.hpp index 506e703b2a..2d445fcc6c 100644 --- a/src/libslic3r/Emboss.hpp +++ b/src/libslic3r/Emboss.hpp @@ -165,7 +165,7 @@ namespace Emboss /// Fix duplicit points and self intersections in polygons. /// Also try to reduce amount of points and remove useless polygon parts /// - /// Fill type ClipperLib::pftNonZero for overlapping otherwise + /// Fill type pftNonZero for overlapping otherwise /// Look at heal_expolygon()::max_iteration /// Healed shapes with flag is fully healed HealedExPolygons heal_polygons(const Polygons &shape, bool is_non_zero = true, unsigned max_iteration = 10); diff --git a/src/libslic3r/Fill/FillBase.cpp b/src/libslic3r/Fill/FillBase.cpp index f4615dc640..d0126e9cb8 100644 --- a/src/libslic3r/Fill/FillBase.cpp +++ b/src/libslic3r/Fill/FillBase.cpp @@ -3,7 +3,6 @@ #include #include "../ClipperUtils.hpp" -#include "../Clipper2Utils.hpp" #include "../EdgeGrid.hpp" #include "../Geometry.hpp" #include "../Geometry/Circle.hpp" @@ -2734,11 +2733,8 @@ void multiline_fill(Polylines& polylines, const FillParams& params, float spacin if (polylines.empty()) return; - // Convert source polylines to Clipper2 paths - Clipper2Lib::Paths64 subject_paths = Slic3rPolylines_to_Paths64(polylines); - const double miter_limit = 2.0; - const int rings = n_lines / 2; + const int rings = n_lines / 2; // Compute offsets (in units of spacing) std::vector offsets; @@ -2757,10 +2753,6 @@ void multiline_fill(Polylines& polylines, const FillParams& params, float spacin offsets.push_back(start + i * spacing); } - // Process each offset - Clipper2Lib::ClipperOffset offsetter(miter_limit); - offsetter.AddPaths(subject_paths, Clipper2Lib::JoinType::Round, Clipper2Lib::EndType::Round); - for (double t : offsets) { if (t == 0.0) { // Center line (only applies when n_lines is odd) @@ -2768,22 +2760,11 @@ void multiline_fill(Polylines& polylines, const FillParams& params, float spacin continue; } - // ClipperOffset with current offset distance (union is not needed here) - Clipper2Lib::Paths64 offset_paths; - offsetter.Execute(scale_(t), offset_paths); - if (offset_paths.empty()) - continue; - - // Convert back to polylines - Polylines new_polylines = Paths64_to_polylines(offset_paths); - - for (Polyline& pl : new_polylines) { - if (pl.points.size() < 3) - continue; - if (pl.points.front() != pl.points.back()) - pl.points.push_back(pl.points.front()); - all_polylines.emplace_back(std::move(pl)); - } + const float delta = float(scale_(t)); + // Arc tolerance of 1/500 of the offset, Clipper2's default. + for (const Polygon &ring : offset(polylines, delta, jtRound, 0.002 * delta, etOpenRound)) + if (ring.size() >= 3) + all_polylines.emplace_back(ring.split_at_first_point()); } polylines = std::move(all_polylines); diff --git a/src/libslic3r/Fill/FillRectilinear.cpp b/src/libslic3r/Fill/FillRectilinear.cpp index 998cfd9a7e..b4373d3ea6 100644 --- a/src/libslic3r/Fill/FillRectilinear.cpp +++ b/src/libslic3r/Fill/FillRectilinear.cpp @@ -417,9 +417,9 @@ public: // bool sticks_removed = remove_sticks(polygons_src); // if (sticks_removed) BOOST_LOG_TRIVIAL(error) << "Sticks removed!"; - polygons_outer = aoffset1 == 0 ? to_polygons(polygons_src) : offset(polygons_src, float(aoffset1), ClipperLib::jtMiter, miterLimit); + polygons_outer = aoffset1 == 0 ? to_polygons(polygons_src) : offset(polygons_src, float(aoffset1), jtMiter, miterLimit); if (aoffset2 < 0) - polygons_inner = shrink(polygons_outer, float(aoffset1 - aoffset2), ClipperLib::jtMiter, miterLimit); + polygons_inner = shrink(polygons_outer, float(aoffset1 - aoffset2), jtMiter, miterLimit); // Filter out contours with zero area or small area, contours with 2 points only. const double min_area_threshold = 0.01 * aoffset2 * aoffset2; remove_small(polygons_outer, min_area_threshold); diff --git a/src/libslic3r/Fill/Lightning/TreeNode.cpp b/src/libslic3r/Fill/Lightning/TreeNode.cpp index 3d57ebae4a..c57180e530 100644 --- a/src/libslic3r/Fill/Lightning/TreeNode.cpp +++ b/src/libslic3r/Fill/Lightning/TreeNode.cpp @@ -143,14 +143,7 @@ NodeSPtr Node::closestNode(const Point& loc) bool inside(const Polygons &polygons, const Point &p) { - int poly_count_inside = 0; - for (const Polygon &poly : polygons) { - const int is_inside_this_poly = ClipperLib::PointInPolygon(p, poly.points); - if (is_inside_this_poly == -1) - return true; - poly_count_inside += is_inside_this_poly; - } - return (poly_count_inside % 2) == 1; + return contains(polygons, p, true); } bool lineSegmentPolygonsIntersection(const Point& a, const Point& b, const EdgeGrid::Grid& outline_locator, Point& result, const coord_t within_max_dist) diff --git a/src/libslic3r/Format/svg.cpp b/src/libslic3r/Format/svg.cpp index 7b720e62ef..c9738c8058 100644 --- a/src/libslic3r/Format/svg.cpp +++ b/src/libslic3r/Format/svg.cpp @@ -20,7 +20,6 @@ #include "TopExp_Explorer.hxx" #include "TopoDS.hxx" #include "BRepExtrema_SelfIntersection.hxx" -#include "libslic3r/clipper.hpp" #include "libslic3r/Polygon.hpp" namespace Slic3r { @@ -211,26 +210,15 @@ bool get_svg_profile(const char *path, std::vector &element_infos, Polygons polygons; bool close_polygon = false; for (int i = 0; i < path_line_points.size(); ++i) { - ClipperLib::Path pt_path; - for (auto line_point : path_line_points[i]) { - pt_path.push_back(ClipperLib::IntPoint(line_point.first.X() * scale_size, line_point.first.Y() * scale_size)); + Polyline pt_path; + for (auto line_point : path_line_points[i]) { + pt_path.points.push_back(Point(line_point.first.X() * scale_size, line_point.first.Y() * scale_size)); } - pt_path.push_back(ClipperLib::IntPoint(path_line_points[i].back().second.X() * scale_size, path_line_points[i].back().second.Y() * scale_size)); + pt_path.points.push_back(Point(path_line_points[i].back().second.X() * scale_size, path_line_points[i].back().second.Y() * scale_size)); - ClipperLib::Paths out_paths; - ClipperLib::ClipperOffset co; - if (pt_path.front() == pt_path.back()) { - co.AddPath(pt_path, ClipperLib::jtMiter, ClipperLib::etClosedLine); - close_polygon = true; - } else { - co.AddPath(pt_path, ClipperLib::jtMiter, ClipperLib::etOpenSquare); - close_polygon = false; - } - co.Execute(out_paths, stroke_width / 2); - - for (auto out_path : out_paths) { - polygons.emplace_back(Polygon(out_path)); - } + close_polygon = pt_path.points.front() == pt_path.points.back(); + if (stroke_width > 0) + append(polygons, offset(pt_path, stroke_width / 2, jtMiter, 2., close_polygon ? etClosedLine : etOpenSquare)); } if (!close_polygon) diff --git a/src/libslic3r/Geometry.cpp b/src/libslic3r/Geometry.cpp index 69aff57c9a..1c775be3a8 100644 --- a/src/libslic3r/Geometry.cpp +++ b/src/libslic3r/Geometry.cpp @@ -4,7 +4,6 @@ #include "ClipperUtils.hpp" #include "ExPolygon.hpp" #include "Line.hpp" -#include "clipper.hpp" #include #include #include diff --git a/src/libslic3r/LayerRegion.cpp b/src/libslic3r/LayerRegion.cpp index 6f868ba095..ab2096184b 100644 --- a/src/libslic3r/LayerRegion.cpp +++ b/src/libslic3r/LayerRegion.cpp @@ -627,9 +627,9 @@ void LayerRegion::process_external_surfaces(const Layer *lower_layer, const Poly } #else -//#define EXTERNAL_SURFACES_OFFSET_PARAMETERS ClipperLib::jtMiter, 3. -//#define EXTERNAL_SURFACES_OFFSET_PARAMETERS ClipperLib::jtMiter, 1.5 -#define EXTERNAL_SURFACES_OFFSET_PARAMETERS ClipperLib::jtSquare, 0. +//#define EXTERNAL_SURFACES_OFFSET_PARAMETERS jtMiter, 3. +//#define EXTERNAL_SURFACES_OFFSET_PARAMETERS jtMiter, 1.5 +#define EXTERNAL_SURFACES_OFFSET_PARAMETERS jtSquare, 0. void LayerRegion::process_external_surfaces(const Layer *lower_layer, const Polygons *lower_layer_covered) { diff --git a/src/libslic3r/NSVGUtils.cpp b/src/libslic3r/NSVGUtils.cpp index e27a6738e2..d12a866b3d 100644 --- a/src/libslic3r/NSVGUtils.cpp +++ b/src/libslic3r/NSVGUtils.cpp @@ -500,26 +500,26 @@ Polylines to_dashes(const Polyline &polyline, const DashesParam& param) HealedExPolygons stroke_to_expolygons(const LinesPath &lines_path, const NSVGshape &shape, const NSVGLineParams ¶m) { // convert stroke to polygon - ClipperLib::JoinType join_type = ClipperLib::JoinType::jtSquare; + JoinType join_type = jtSquare; switch (static_cast(shape.strokeLineJoin)) { - case NSVGlineJoin::NSVG_JOIN_BEVEL: join_type = ClipperLib::JoinType::jtSquare; break; - case NSVGlineJoin::NSVG_JOIN_MITER: join_type = ClipperLib::JoinType::jtMiter; break; - case NSVGlineJoin::NSVG_JOIN_ROUND: join_type = ClipperLib::JoinType::jtRound; break; + case NSVGlineJoin::NSVG_JOIN_BEVEL: join_type = jtSquare; break; + case NSVGlineJoin::NSVG_JOIN_MITER: join_type = jtMiter; break; + case NSVGlineJoin::NSVG_JOIN_ROUND: join_type = jtRound; break; } double mitter = shape.miterLimit * param.scale; - if (join_type == ClipperLib::JoinType::jtRound) { + if (join_type == jtRound) { // mitter is used as ArcTolerance // http://www.angusj.com/delphi/clipper/documentation/Docs/Units/ClipperLib/Classes/ClipperOffset/Properties/ArcTolerance.htm mitter = std::pow(param.tesselation_tolerance, 1/3.); } float stroke_width = static_cast(shape.strokeWidth * param.scale); - ClipperLib::EndType end_type = ClipperLib::EndType::etOpenButt; + EndType end_type = etOpenButt; switch (static_cast(shape.strokeLineCap)) { - case NSVGlineCap::NSVG_CAP_BUTT: end_type = ClipperLib::EndType::etOpenButt; break; - case NSVGlineCap::NSVG_CAP_ROUND: end_type = ClipperLib::EndType::etOpenRound; break; - case NSVGlineCap::NSVG_CAP_SQUARE: end_type = ClipperLib::EndType::etOpenSquare; break; + case NSVGlineCap::NSVG_CAP_BUTT: end_type = etOpenButt; break; + case NSVGlineCap::NSVG_CAP_ROUND: end_type = etOpenRound; break; + case NSVGlineCap::NSVG_CAP_SQUARE: end_type = etOpenSquare; break; } Polygons result; diff --git a/src/libslic3r/PerimeterGenerator.cpp b/src/libslic3r/PerimeterGenerator.cpp index 1f1f1289e5..2f8e29c237 100644 --- a/src/libslic3r/PerimeterGenerator.cpp +++ b/src/libslic3r/PerimeterGenerator.cpp @@ -2,6 +2,7 @@ #include "AABBTreeLines.hpp" #include "BridgeDetector.hpp" #include "ClipperUtils.hpp" +#include "ClipperZUtils.hpp" #include "ExtrusionEntity.hpp" #include "ExtrusionEntityCollection.hpp" #include "Feature/FuzzySkin/FuzzySkin.hpp" @@ -296,13 +297,12 @@ static ExtrusionEntityCollection traverse_loops(const PerimeterGenerator &perime return out; } -static ClipperLib_Z::Paths clip_extrusion(const ClipperLib_Z::Path& subject, const ClipperLib_Z::Paths& clip, ClipperLib_Z::ClipType clipType) +static ClipperZUtils::ZPaths clip_extrusion(const ClipperZUtils::ZPath& subject, const ClipperZUtils::ZPaths& clip, ClipType clipType) { - ClipperLib_Z::Clipper clipper; - clipper.ZFillFunction([](const ClipperLib_Z::IntPoint& e1bot, const ClipperLib_Z::IntPoint& e1top, const ClipperLib_Z::IntPoint& e2bot, - const ClipperLib_Z::IntPoint& e2top, ClipperLib_Z::IntPoint& pt) { - ClipperLib_Z::IntPoint start = e1bot; - ClipperLib_Z::IntPoint end = e1top; + auto zfill = [](const ClipperZUtils::ZPoint& e1bot, const ClipperZUtils::ZPoint& e1top, const ClipperZUtils::ZPoint& e2bot, + const ClipperZUtils::ZPoint& e2top, ClipperZUtils::ZPoint& pt) { + ClipperZUtils::ZPoint start = e1bot; + ClipperZUtils::ZPoint end = e1top; if (start.z() <= 0 && end.z() <= 0) { start = e2bot; @@ -317,23 +317,15 @@ static ClipperLib_Z::Paths clip_extrusion(const ClipperLib_Z::Path& subject, con double t = std::sqrt(dist_sqr / length_sqr); pt.z() = start.z() + coord_t((end.z() - start.z()) * t); - }); + }; - clipper.AddPath(subject, ClipperLib_Z::ptSubject, false); - clipper.AddPaths(clip, ClipperLib_Z::ptClip, true); - - ClipperLib_Z::Paths clipped_paths; - { - ClipperLib_Z::PolyTree clipped_polytree; - clipper.Execute(clipType, clipped_polytree, ClipperLib_Z::pftNonZero, ClipperLib_Z::pftNonZero); - ClipperLib_Z::PolyTreeToPaths(std::move(clipped_polytree), clipped_paths); - } + ClipperZUtils::ZPaths clipped_paths = ClipperZUtils::clip_zpaths(clipType, ClipperZUtils::ZPaths{ subject }, true, clip, zfill); // Clipped path could contain vertices from the clip with a Z coordinate equal to zero. // For those vertices, we must assign value based on the subject. // This happens only in sporadic cases. - for (ClipperLib_Z::Path& path : clipped_paths) - for (ClipperLib_Z::IntPoint& c_pt : path) + for (ClipperZUtils::ZPath& path : clipped_paths) + for (ClipperZUtils::ZPoint& c_pt : path) if (c_pt.z() == 0) { // Now we must find the corresponding line on with this point is located and compute line width (Z coordinate). if (subject.size() <= 2) @@ -366,8 +358,8 @@ static ClipperLib_Z::Paths clip_extrusion(const ClipperLib_Z::Path& subject, con } assert([&clipped_paths = std::as_const(clipped_paths)]() -> bool { - for (const ClipperLib_Z::Path& path : clipped_paths) - for (const ClipperLib_Z::IntPoint& pt : path) + for (const ClipperZUtils::ZPath& path : clipped_paths) + for (const ClipperZUtils::ZPoint& pt : path) if (pt.z() <= 0) return false; return true; @@ -376,7 +368,7 @@ static ClipperLib_Z::Paths clip_extrusion(const ClipperLib_Z::Path& subject, con return clipped_paths; } -static double clipper_z_path_length(const ClipperLib_Z::Path &path) +static double clipper_z_path_length(const ClipperZUtils::ZPath &path) { double len = 0.; for (size_t i = 1; i < path.size(); ++ i) @@ -413,7 +405,7 @@ static ExtrusionEntityCollection traverse_extrusions(const PerimeterGenerator& p ExtrusionPaths paths; // detect overhanging/bridging perimeters if (perimeter_generator.config->detect_overhang_wall && perimeter_generator.layer_id > perimeter_generator.object_config->raft_layers) { - ClipperLib_Z::Path extrusion_path; + ClipperZUtils::ZPath extrusion_path; extrusion_path.reserve(extrusion->size()); BoundingBox extrusion_path_bbox; for (const Arachne::ExtrusionJunction &ej : extrusion->junctions) { @@ -421,7 +413,7 @@ static ExtrusionEntityCollection traverse_extrusions(const PerimeterGenerator& p extrusion_path_bbox.merge(Point(ej.p.x(), ej.p.y())); } - ClipperLib_Z::Paths lower_slices_paths; + ClipperZUtils::ZPaths lower_slices_paths; { lower_slices_paths.reserve(perimeter_generator.lower_slices_polygons().size()); Points clipped; @@ -431,7 +423,7 @@ static ExtrusionEntityCollection traverse_extrusions(const PerimeterGenerator& p ClipperUtils::clip_clipper_polygon_with_subject_bbox(poly.points, extrusion_path_bbox, clipped); if (!clipped.empty()) { lower_slices_paths.emplace_back(); - ClipperLib_Z::Path &out = lower_slices_paths.back(); + ClipperZUtils::ZPath &out = lower_slices_paths.back(); out.reserve(clipped.size()); for (const Point &pt : clipped) out.emplace_back(pt.x(), pt.y(), 0); @@ -440,7 +432,7 @@ static ExtrusionEntityCollection traverse_extrusions(const PerimeterGenerator& p } // get non-overhang paths by intersecting this loop with the grown lower slices - extrusion_paths_append(paths, clip_extrusion(extrusion_path, lower_slices_paths, ClipperLib_Z::ctIntersection), role, + extrusion_paths_append(paths, clip_extrusion(extrusion_path, lower_slices_paths, ctIntersection), role, is_external ? perimeter_generator.ext_perimeter_flow : perimeter_generator.perimeter_flow); // Always reverse extrusion if use fuzzy skin: https://github.com/OrcaSlicer/OrcaSlicer/pull/2413#issuecomment-1769735357 @@ -481,7 +473,7 @@ static ExtrusionEntityCollection traverse_extrusions(const PerimeterGenerator& p // get overhang paths by checking what parts of this loop fall // outside the grown lower slices (thus where the distance between // the loop centerline and original lower slices is >= half nozzle diameter - extrusion_paths_append(paths, clip_extrusion(extrusion_path, lower_slices_paths, ClipperLib_Z::ctDifference), erOverhangPerimeter, + extrusion_paths_append(paths, clip_extrusion(extrusion_path, lower_slices_paths, ctDifference), erOverhangPerimeter, perimeter_generator.overhang_flow); // Reapply the nearest point search for starting point. @@ -674,10 +666,10 @@ static void clip_inner_walls_over_top(std::vector & }; // Pull the cut back by half a wall width: the clip severs the centerline, but the bead's rounded end // extends half a width past its endpoint and would otherwise overlap the top fill. - ClipperLib_Z::Paths top_paths_z; + ClipperZUtils::ZPaths top_paths_z; for (const Polygon &poly : to_polygons(offset_ex(top_region, float(perimeter_width) / 2.f))) { top_paths_z.emplace_back(); - ClipperLib_Z::Path &out = top_paths_z.back(); + ClipperZUtils::ZPath &out = top_paths_z.back(); out.reserve(poly.points.size()); for (const Point &pt : poly.points) out.emplace_back(pt.x(), pt.y(), 0); @@ -695,21 +687,21 @@ static void clip_inner_walls_over_top(std::vector & } if (overlap == TopOverlap::Full) continue; // the clip below would return nothing anyway - ClipperLib_Z::Path subject; + ClipperZUtils::ZPath subject; subject.reserve(el.size()); for (const Arachne::ExtrusionJunction &j : el.junctions) subject.emplace_back(j.p.x(), j.p.y(), j.w); - ClipperLib_Z::Paths pieces = clip_extrusion(subject, top_paths_z, ClipperLib_Z::ctDifference); + ClipperZUtils::ZPaths pieces = clip_extrusion(subject, top_paths_z, ctDifference); // Clipper treats the subject as an open polyline, so it also cuts a closed loop at its (arbitrary) // start vertex and may reverse pieces. Stitch pieces sharing an endpoint back together. - auto same_pt = [](const ClipperLib_Z::IntPoint &p, const ClipperLib_Z::IntPoint &q) { + auto same_pt = [](const ClipperZUtils::ZPoint &p, const ClipperZUtils::ZPoint &q) { return std::abs(p.x() - q.x()) <= SCALED_EPSILON && std::abs(p.y() - q.y()) <= SCALED_EPSILON; }; for (size_t i = 0; i < pieces.size(); ++ i) { for (size_t j = i + 1; j < pieces.size();) { - ClipperLib_Z::Path &a = pieces[i]; - ClipperLib_Z::Path &b = pieces[j]; + ClipperZUtils::ZPath &a = pieces[i]; + ClipperZUtils::ZPath &b = pieces[j]; if (same_pt(a.front(), b.front()) || same_pt(a.front(), b.back())) std::reverse(a.begin(), a.end()); if (same_pt(a.back(), b.back())) @@ -726,7 +718,7 @@ static void clip_inner_walls_over_top(std::vector & // If the clip removed next to nothing, keep the loop untouched instead of slitting it open. The // half-width pull-back above already costs about one width per crossing, hence two widths. double kept_length = 0.; - for (const ClipperLib_Z::Path &path : pieces) + for (const ClipperZUtils::ZPath &path : pieces) kept_length += clipper_z_path_length(path); if (clipper_z_path_length(subject) - kept_length < 2. * double(perimeter_width)) { append(kept_over_top, covered_by(el)); @@ -734,10 +726,10 @@ static void clip_inner_walls_over_top(std::vector & continue; } - for (const ClipperLib_Z::Path &path : pieces) { + for (const ClipperZUtils::ZPath &path : pieces) { Arachne::ExtrusionLine clipped(el.inset_idx, el.is_odd); clipped.junctions.reserve(path.size()); - for (const ClipperLib_Z::IntPoint &pt : path) + for (const ClipperZUtils::ZPoint &pt : path) clipped.junctions.emplace_back(Point(pt.x(), pt.y()), coord_t(pt.z()), el.inset_idx); // Discard tiny leftovers that would print as zits. if (clipped.size() >= 2 && clipped.getLength() >= perimeter_width) @@ -1056,7 +1048,7 @@ ExtrusionPaths sort_extra_perimeters(const ExtrusionPaths& extra_perims, int ind return filtered; } -#define EXTRA_PERIMETER_OFFSET_PARAMETERS ClipperLib::jtSquare, 0. +#define EXTRA_PERIMETER_OFFSET_PARAMETERS jtSquare, 0. // #define EXTRA_PERIM_DEBUG_FILES // Function will generate extra perimeters clipped over nonbridgeable areas of the provided surface and returns both the new perimeters and // Polygons filled by those clipped perimeters diff --git a/src/libslic3r/Polygon.cpp b/src/libslic3r/Polygon.cpp index 77ff30fb40..b2e2888111 100644 --- a/src/libslic3r/Polygon.cpp +++ b/src/libslic3r/Polygon.cpp @@ -70,7 +70,7 @@ double Polygon::area() const bool Polygon::is_counter_clockwise() const { - return ClipperLib::Orientation(this->points); + return this->area() >= 0.; } bool Polygon::is_clockwise() const @@ -678,7 +678,7 @@ void remove_collinear(Polygons &polys) remove_collinear(poly); } -Polygons polygons_simplify(const Polygons &source_polygons, double tolerance, bool strictly_simple /* = true */) +Polygons polygons_simplify(const Polygons &source_polygons, double tolerance) { Polygons out; out.reserve(source_polygons.size()); @@ -687,13 +687,15 @@ Polygons polygons_simplify(const Polygons &source_polygons, double tolerance, bo Points simplified = MultiPoint::_douglas_peucker(to_polyline(source_polygon).points, tolerance); // then remove the last (repeated) point. simplified.pop_back(); - // Simplify the decimated contour by ClipperLib. - bool ccw = ClipperLib::Area(simplified) > 0.; - for (Points &path : ClipperLib::SimplifyPolygons(ClipperUtils::SinglePathProvider(simplified), ClipperLib::pftNonZero, strictly_simple)) { + // Simplify the decimated contour by a union. + bool ccw = Polygon::area(simplified) > 0.; + Polygons decimated(1); + decimated.front().points = std::move(simplified); + for (Polygon &polygon : union_(decimated)) { if (! ccw) - // ClipperLib likely reoriented negative area contours to become positive. Reverse holes back to CW. - std::reverse(path.begin(), path.end()); - out.emplace_back(std::move(path)); + // The union reorients negative area contours to become positive. Reverse holes back to CW. + polygon.reverse(); + out.emplace_back(std::move(polygon)); } } return out; @@ -728,9 +730,40 @@ bool overlaps(const Polygons& polys1, const Polygons& polys2) return false; } +// Clipper1's PointInPolygon(): 1 inside, 0 outside, -1 on the boundary. +static int point_in_polygon(const Point &pt, const Points &path) +{ + int result = 0; + size_t cnt = path.size(); + if (cnt < 3) return 0; + Point ip = path[0]; + for (size_t i = 1; i <= cnt; ++i) { + Point ipNext = (i == cnt ? path[0] : path[i]); + if (ipNext.y() == pt.y() && ((ipNext.x() == pt.x()) || (ip.y() == pt.y() && ((ipNext.x() > pt.x()) == (ip.x() < pt.x()))))) + return -1; + if ((ip.y() < pt.y()) != (ipNext.y() < pt.y())) { + if (ip.x() >= pt.x()) { + if (ipNext.x() > pt.x()) + result = 1 - result; + else { + int64_t d = int64_t(ip.x() - pt.x()) * int64_t(ipNext.y() - pt.y()) - int64_t(ipNext.x() - pt.x()) * int64_t(ip.y() - pt.y()); + if (! d) return -1; + if ((d > 0) == (ipNext.y() > ip.y())) result = 1 - result; + } + } else if (ipNext.x() > pt.x()) { + int64_t d = int64_t(ip.x() - pt.x()) * int64_t(ipNext.y() - pt.y()) - int64_t(ipNext.x() - pt.x()) * int64_t(ip.y() - pt.y()); + if (! d) return -1; + if ((d > 0) == (ipNext.y() > ip.y())) result = 1 - result; + } + } + ip = ipNext; + } + return result; +} + bool contains(const Polygon &polygon, const Point &p, bool border_result) { - if (const int poly_count_inside = ClipperLib::PointInPolygon(p, polygon.points); + if (const int poly_count_inside = point_in_polygon(p, polygon.points); poly_count_inside == -1) return border_result; else @@ -741,7 +774,7 @@ bool contains(const Polygons &polygons, const Point &p, bool border_result) { int poly_count_inside = 0; for (const Polygon &poly : polygons) { - const int is_inside_this_poly = ClipperLib::PointInPolygon(p, poly.points); + const int is_inside_this_poly = point_in_polygon(p, poly.points); if (is_inside_this_poly == -1) return border_result; poly_count_inside += is_inside_this_poly; diff --git a/src/libslic3r/Polygon.hpp b/src/libslic3r/Polygon.hpp index 34ca214cf3..146491e281 100644 --- a/src/libslic3r/Polygon.hpp +++ b/src/libslic3r/Polygon.hpp @@ -159,7 +159,7 @@ inline void polygons_append(Polygons &dst, Polygons &&src) } } -Polygons polygons_simplify(const Polygons &polys, double tolerance, bool strictly_simple = true); +Polygons polygons_simplify(const Polygons &polys, double tolerance); inline void polygons_rotate(Polygons &polys, double angle) { diff --git a/src/libslic3r/Print.cpp b/src/libslic3r/Print.cpp index 5f5592cfce..6d60874c18 100644 --- a/src/libslic3r/Print.cpp +++ b/src/libslic3r/Print.cpp @@ -3489,7 +3489,7 @@ void Print::_make_skirt() Polygon loop; { // Orca: the hull already represents the occupied outline used for this skirt. - Polygons loops = offset(hull, distance, ClipperLib::jtRound, float(scale_(0.1))); + Polygons loops = offset(hull, distance, jtRound, float(scale_(0.1))); Geometry::simplify_polygons(loops, scale_(0.05), &loops); if (loops.empty()) break; @@ -3526,7 +3526,7 @@ void Print::_make_skirt() } if (collect_skirt_hull) - for (Polygon &poly : offset(hull, distance + 0.5f * float(scale_(spacing)), ClipperLib::jtRound, float(scale_(0.1)))) + for (Polygon &poly : offset(hull, distance + 0.5f * float(scale_(spacing)), jtRound, float(scale_(0.1)))) append(m_skirt_convex_hull, std::move(poly.points)); }; @@ -3632,7 +3632,7 @@ void Print::_make_skirt() if (group.emits_skirt) { // Orca: If the expanded skirt outline touches another group // or obstacle, merge them and run the pass again. - Polygons envelopes = offset(envelope, grouping_offset, ClipperLib::jtRound, float(scale_(0.1))); + Polygons envelopes = offset(envelope, grouping_offset, jtRound, float(scale_(0.1))); if (envelopes.empty()) continue; envelope = std::move(envelopes.front()); diff --git a/src/libslic3r/PrintConfig.hpp b/src/libslic3r/PrintConfig.hpp index b1c407e3b6..f2c7b2810a 100644 --- a/src/libslic3r/PrintConfig.hpp +++ b/src/libslic3r/PrintConfig.hpp @@ -251,9 +251,9 @@ enum class PrintOrder enum class SlicingMode { - // Regular, applying ClipperLib::pftNonZero rule when creating ExPolygons. + // Regular, applying pftNonZero rule when creating ExPolygons. Regular, - // Compatible with 3DLabPrint models, applying ClipperLib::pftEvenOdd rule when creating ExPolygons. + // Compatible with 3DLabPrint models, applying pftEvenOdd rule when creating ExPolygons. EvenOdd, // Orienting all contours CCW, thus closing all holes. CloseHoles, diff --git a/src/libslic3r/PrintObject.cpp b/src/libslic3r/PrintObject.cpp index 3b7e889472..699bbb99b2 100644 --- a/src/libslic3r/PrintObject.cpp +++ b/src/libslic3r/PrintObject.cpp @@ -4,7 +4,6 @@ #include "Print.hpp" #include "BoundingBox.hpp" #include "ClipperUtils.hpp" -#include "Clipper2Utils.hpp" #include "ElephantFootCompensation.hpp" #include "Geometry.hpp" #include "I18N.hpp" @@ -56,7 +55,7 @@ using namespace std::literals; // #define PRINT_OBJECT_TIMING #ifdef PRINT_OBJECT_TIMING - // time limit for one ClipperLib operation (union / diff / offset), in ms + // time limit for one Clipper operation (union / diff / offset), in ms #define PRINT_OBJECT_TIME_LIMIT_DEFAULT 50 #include #include "Timer.hpp" @@ -1851,7 +1850,7 @@ void PrintObject::detect_surfaces_type() // Grow, then keep only what the configured direction allows, using the top's own filled // outline (same outer edge, holes closed) to tell the two apart. - ExPolygons expanded = offset_ex_2(island_top, d, Clipper2Lib::JoinType::Miter); + ExPolygons expanded = offset_ex(island_top, float(d), jtMiter, 2.); if (direction != TopSurfaceExpansionDirection::InwardAndOutward) { ExPolygons outline; outline.reserve(island_top.size()); @@ -2558,9 +2557,9 @@ void PrintObject::discover_vertical_shells() // Open to remove (filter out) regions narrower than an infill extrusion line width. -narrow_ensure_vertical_wall_thickness_region_radius, // Then close gaps narrower than 1.2 * line width, such gaps are difficult to fill in with sparse infill. - narrow_ensure_vertical_wall_thickness_region_radius + narrow_sparse_infill_region_radius, ClipperLib::jtSquare), + narrow_ensure_vertical_wall_thickness_region_radius + narrow_sparse_infill_region_radius, jtSquare), // Finally expand the infill a bit to remove tiny gaps between solid infill and the other regions. - narrow_sparse_infill_region_radius - tiny_overlap_radius, ClipperLib::jtSquare); + narrow_sparse_infill_region_radius - tiny_overlap_radius, jtSquare); Polygons object_volume; Polygons internal_volume; @@ -4170,7 +4169,7 @@ void PrintObject::clip_fill_surfaces() upper_internal = intersection( // Regularize the overhang regions, so that the infill areas will not become excessively jagged. smooth_outward( - closing(upper_internal, closing_radius, ClipperLib::jtSquare, 0.), + closing(upper_internal, closing_radius, jtSquare, 0.), scaled(0.1)), lower_layer_internal_surfaces); // Apply new internal infill to regions. @@ -4341,7 +4340,7 @@ void PrintObject::discover_horizontal_shells() // have the same angle, so the next shell would be grown even more and so on. Polygons too_narrow = diff( new_internal_solid, - opening(new_internal_solid, margin, margin + ClipperSafetyOffset, ClipperLib::jtMiter, 5)); + opening(new_internal_solid, margin, margin + ClipperSafetyOffset, jtMiter, 5)); if (! too_narrow.empty()) { // grow the collapsing parts and add the extra area to the neighbor layer // as well as to our original surfaces so that we support this @@ -4543,7 +4542,7 @@ void PrintObject::_generate_support_material() } // BBS -#define SUPPORT_SURFACES_OFFSET_PARAMETERS ClipperLib::jtSquare, 0. +#define SUPPORT_SURFACES_OFFSET_PARAMETERS jtSquare, 0. #define SUPPORT_MATERIAL_MARGIN 1.2 template void PrintObject::remove_bridges_from_contacts( diff --git a/src/libslic3r/SLA/ConcaveHull.cpp b/src/libslic3r/SLA/ConcaveHull.cpp index f657fce746..2484fda2ff 100644 --- a/src/libslic3r/SLA/ConcaveHull.cpp +++ b/src/libslic3r/SLA/ConcaveHull.cpp @@ -135,7 +135,7 @@ ExPolygons offset_waffle_style_ex(const ConcaveHull &hull, coord_t delta) Polygons offset_waffle_style(const ConcaveHull &hull, coord_t delta) { auto arc_tolerance = scaled(0.01); - Polygons res = closing(hull.polygons(), 2 * delta, delta, ClipperLib::jtRound, arc_tolerance); + Polygons res = closing(hull.polygons(), 2 * delta, delta, jtRound, arc_tolerance); auto it = std::remove_if(res.begin(), res.end(), [](Polygon &p) { return p.is_clockwise(); }); res.erase(it, res.end()); diff --git a/src/libslic3r/SLA/Pad.cpp b/src/libslic3r/SLA/Pad.cpp index 447a416341..8726dfa55b 100644 --- a/src/libslic3r/SLA/Pad.cpp +++ b/src/libslic3r/SLA/Pad.cpp @@ -171,24 +171,8 @@ struct PadSkeleton { ExPolygons inner, outer; }; PadSkeleton divide_blueprint(const ExPolygons &bp) { - ClipperLib::PolyTree ptree = union_pt(bp); - PadSkeleton ret; - ret.inner.reserve(size_t(ptree.Total())); - ret.outer.reserve(size_t(ptree.Total())); - - for (ClipperLib::PolyTree::PolyNode *node : ptree.Childs) { - ExPolygon poly; - poly.contour.points = std::move(node->Contour); - for (ClipperLib::PolyTree::PolyNode *child : node->Childs) { - poly.holes.emplace_back(std::move(child->Contour)); - - traverse_pt(child->Childs, &ret.inner); - } - - ret.outer.emplace_back(poly); - } - + ret.outer = top_level_expolygons(bp, &ret.inner); return ret; } @@ -257,7 +241,7 @@ public: auto model_bp_offs = offset_ex(model_blueprint, scaled(cfg.embed_object.object_gap_mm), - ClipperLib::jtMiter, 1); + jtMiter, 1); ExPolygons fullcvh = wafflized_concave_hull(support_blueprint, model_bp_offs, cfg, thr); diff --git a/src/libslic3r/SLA/SupportPointGenerator.cpp b/src/libslic3r/SLA/SupportPointGenerator.cpp index 7c6c8d1f6b..e3cca1324f 100644 --- a/src/libslic3r/SLA/SupportPointGenerator.cpp +++ b/src/libslic3r/SLA/SupportPointGenerator.cpp @@ -186,8 +186,8 @@ static std::vector make_layers( // Produce 2 bands around the island, a safe band for dangling overhangs // and an unsafe band for sloped overhangs. // These masks include the original island - auto dangl_mask = expand(bottom_polygons, between_layers_offset, ClipperLib::jtSquare); - auto overh_mask = expand(bottom_polygons, slope_offset, ClipperLib::jtSquare); + auto dangl_mask = expand(bottom_polygons, between_layers_offset, jtSquare); + auto overh_mask = expand(bottom_polygons, slope_offset, jtSquare); // Absolutely hopeless overhangs are those outside the unsafe band top.overhangs = diff_ex(*top.polygon, overh_mask); diff --git a/src/libslic3r/SVG.cpp b/src/libslic3r/SVG.cpp index 9ccddcd4f2..206158edd7 100644 --- a/src/libslic3r/SVG.cpp +++ b/src/libslic3r/SVG.cpp @@ -230,15 +230,15 @@ void SVG::draw(const Points &points, std::string fill, coord_t radius) this->draw(*it, fill, radius); } -void SVG::draw(const ClipperLib::Path &polygon, double scale, std::string stroke, coordf_t stroke_width) +void SVG::draw(const Points &polygon, double scale, std::string stroke, coordf_t stroke_width) { this->stroke = stroke; this->path(this->get_path_d(polygon, scale, true), false, stroke_width, 1.f); } -void SVG::draw(const ClipperLib::Paths &polygons, double scale, std::string stroke, coordf_t stroke_width) +void SVG::draw(const VecOfPoints &polygons, double scale, std::string stroke, coordf_t stroke_width) { - for (ClipperLib::Paths::const_iterator it = polygons.begin(); it != polygons.end(); ++ it) + for (VecOfPoints::const_iterator it = polygons.begin(); it != polygons.end(); ++ it) draw(*it, scale, stroke, stroke_width); } @@ -284,11 +284,11 @@ std::string SVG::get_path_d(const MultiPoint &mp, bool closed) const return d.str(); } -std::string SVG::get_path_d(const ClipperLib::Path &path, double scale, bool closed) const +std::string SVG::get_path_d(const Points &path, double scale, bool closed) const { std::ostringstream d; d << "M "; - for (ClipperLib::Path::const_iterator p = path.begin(); p != path.end(); ++p) { + for (Points::const_iterator p = path.begin(); p != path.end(); ++p) { d << to_svg_x(scale * p->x() - origin(0)) << " "; d << to_svg_y(scale * p->y() - origin(1)) << " "; } diff --git a/src/libslic3r/SVG.hpp b/src/libslic3r/SVG.hpp index 5f57961053..d677f48375 100644 --- a/src/libslic3r/SVG.hpp +++ b/src/libslic3r/SVG.hpp @@ -2,7 +2,6 @@ #define slic3r_SVG_hpp_ #include "libslic3r.h" -#include "clipper.hpp" #include "ExPolygon.hpp" #include "Line.hpp" #include "TriangleMesh.hpp" @@ -70,9 +69,9 @@ public: void draw(const Point &point, std::string fill = "black", coord_t radius = 0); void draw(const Points &points, std::string fill = "black", coord_t radius = 0); - // Support for rendering the ClipperLib paths - void draw(const ClipperLib::Path &polygon, double scale, std::string fill = "grey", coordf_t stroke_width = 0); - void draw(const ClipperLib::Paths &polygons, double scale, std::string fill = "grey", coordf_t stroke_width = 0); + // Paths drawn with their coordinates multiplied by scale. + void draw(const Points &polygon, double scale, std::string fill = "grey", coordf_t stroke_width = 0); + void draw(const VecOfPoints &polygons, double scale, std::string fill = "grey", coordf_t stroke_width = 0); void draw_text(const Point &pt, const char *text, const char *color, int font_size = 20); void draw_legend(const Point &pt, const char *text, const char *color); @@ -88,7 +87,7 @@ public: void path(const std::string &d, bool fill, coordf_t stroke_width, const float fill_opacity); std::string get_path_d(const MultiPoint &mp, bool closed = false) const; - std::string get_path_d(const ClipperLib::Path &mp, double scale, bool closed = false) const; + std::string get_path_d(const Points &mp, double scale, bool closed = false) const; public: static void export_expolygons(const char *path, const BoundingBox &bbox, const Slic3r::ExPolygons &expolygons, std::string stroke_outer = "black", std::string stroke_holes = "blue", coordf_t stroke_width = 0); diff --git a/src/libslic3r/ShortestPath.cpp b/src/libslic3r/ShortestPath.cpp index 08f973e20b..301a804753 100644 --- a/src/libslic3r/ShortestPath.cpp +++ b/src/libslic3r/ShortestPath.cpp @@ -4,7 +4,6 @@ #undef assert #endif -#include "clipper.hpp" #include "ShortestPath.hpp" #include "ExtrusionEntityCollection.hpp" #include "KDTreeIndirect.hpp" @@ -2041,22 +2040,6 @@ Polylines chain_polylines(Polylines &&polylines, const Point *start_near) return out; } -template static inline T chain_path_items(const Points &points, const T &items) -{ - auto segment_end_point = [&points](size_t idx, bool /* first_point */) -> const Point& { return points[idx]; }; - std::vector> ordered = chain_segments_greedy(segment_end_point, points.size(), nullptr); - T out; - out.reserve(items.size()); - for (auto &segment_and_reversal : ordered) - out.emplace_back(items[segment_and_reversal.first]); - return out; -} - -ClipperLib::PolyNodes chain_clipper_polynodes(const Points &points, const ClipperLib::PolyNodes &items) -{ - return chain_path_items(points, items); -} - // BBS std::vector chain_print_object_instances(const std::vector& print_objects, const Point* start_near) { diff --git a/src/libslic3r/ShortestPath.hpp b/src/libslic3r/ShortestPath.hpp index c7a61d99ee..f4ce89a16b 100644 --- a/src/libslic3r/ShortestPath.hpp +++ b/src/libslic3r/ShortestPath.hpp @@ -11,11 +11,6 @@ namespace Slic3r { - namespace ClipperLib { - class PolyNode; - using PolyNodes = std::vector>; - } - std::vector chain_points(const Points &points, const Point *start_near = nullptr); // Variant with post-processing (crossing removal + 2-opt) for object ordering. std::vector chain_points_with_postprocessing(const Points &points, const Point *start_near = nullptr); @@ -49,7 +44,6 @@ template inline void reorder_by_shortest_traverse(std::vector &po for (size_t i:order) polylines_out.emplace_back(std::move(Temp[i])); } -ClipperLib::PolyNodes chain_clipper_polynodes(const Points &points, const ClipperLib::PolyNodes &items); // Chain instances of print objects by an approximate shortest path. // Returns pairs of PrintObject idx and instance of that PrintObject. diff --git a/src/libslic3r/Support/SupportCommon.cpp b/src/libslic3r/Support/SupportCommon.cpp index e734608803..d4495cf4a2 100644 --- a/src/libslic3r/Support/SupportCommon.cpp +++ b/src/libslic3r/Support/SupportCommon.cpp @@ -1,5 +1,5 @@ #include "../ClipperUtils.hpp" -// #include "../ClipperZUtils.hpp" +#include "../ClipperZUtils.hpp" #include "../ExtrusionEntityCollection.hpp" #include "../Layer.hpp" #include "../Print.hpp" @@ -7,7 +7,6 @@ #include "../MutablePolygon.hpp" #include "../Geometry.hpp" #include "../Point.hpp" -#include "clipper/clipper_z.hpp" #include #include @@ -39,9 +38,9 @@ namespace Slic3r { //FIXME this should be dependent on the nozzle diameter! #define SUPPORT_MATERIAL_MARGIN 1.5 -//#define SUPPORT_SURFACES_OFFSET_PARAMETERS ClipperLib::jtMiter, 3. -//#define SUPPORT_SURFACES_OFFSET_PARAMETERS ClipperLib::jtMiter, 1.5 -#define SUPPORT_SURFACES_OFFSET_PARAMETERS ClipperLib::jtSquare, 0. +//#define SUPPORT_SURFACES_OFFSET_PARAMETERS jtMiter, 3. +//#define SUPPORT_SURFACES_OFFSET_PARAMETERS jtMiter, 1.5 +#define SUPPORT_SURFACES_OFFSET_PARAMETERS jtSquare, 0. // Convert some of the intermediate layers into top/bottom interface layers as well as base interface layers. std::pair generate_interface_layers( @@ -280,13 +279,13 @@ SupportGeneratorLayersPtr generate_raft_base( polygons_append(brim, offset(ex, brim_object_gap)); else { if (brim_outer) - polygons_append(brim, offset(ex.contour, brim_object_gap, ClipperLib::jtRound, float(scale_(0.1)))); + polygons_append(brim, offset(ex.contour, brim_object_gap, jtRound, float(scale_(0.1)))); else brim.emplace_back(ex.contour); if (brim_inner) { Polygons holes = ex.holes; polygons_reverse(holes); - holes = shrink(holes, brim_object_gap, ClipperLib::jtRound, float(scale_(0.1))); + holes = shrink(holes, brim_object_gap, jtRound, float(scale_(0.1))); polygons_reverse(holes); polygons_append(brim, std::move(holes)); } else @@ -493,32 +492,25 @@ void tree_supports_generate_paths( // Offset expolygon inside, returns number of expolygons collected (0 or 1). // Vertices of output paths are marked with Z = source contour index of the expoly. // Vertices at the intersection of source contours are marked with Z = -1. - auto shrink_expolygon_with_contour_idx = [](const Slic3r::ExPolygon &expoly, const float delta, ClipperLib::JoinType joinType, double miterLimit, ClipperLib_Z::Paths &out) -> int + auto shrink_expolygon_with_contour_idx = [](const Slic3r::ExPolygon &expoly, const float delta, JoinType joinType, double miterLimit, ClipperZUtils::ZPaths &out) -> int { assert(delta > 0); - auto append_paths_with_z = [](ClipperLib::Paths &src, coord_t contour_idx, ClipperLib_Z::Paths &dst) { + auto append_paths_with_z = [](const Polygons &src, coord_t contour_idx, ClipperZUtils::ZPaths &dst) { dst.reserve(next_highest_power_of_2(dst.size() + src.size())); - for (const ClipperLib::Path &contour : src) { - ClipperLib_Z::Path tmp; - tmp.reserve(contour.size()); - for (const Point &p : contour) - tmp.emplace_back(p.x(), p.y(), contour_idx); - dst.emplace_back(std::move(tmp)); - } + for (const Polygon &contour : src) + dst.emplace_back(ClipperZUtils::to_zpath(contour.points, contour_idx)); + }; + // Oriented CCW, the sign of d alone decides between growing and shrinking. + auto offset_ccw = [joinType, miterLimit](Polygon polygon, float d) { + if (! polygon.is_counter_clockwise()) + polygon.reverse(); + return offset(polygon, d, joinType, miterLimit); }; // 1) Offset the outer contour. - ClipperLib_Z::Paths contours; + ClipperZUtils::ZPaths contours; { - ClipperLib::ClipperOffset co; - if (joinType == jtRound) - co.ArcTolerance = miterLimit; - else - co.MiterLimit = miterLimit; - co.ShortestEdgeLength = double(delta * 0.005); - co.AddPath(expoly.contour.points, joinType, ClipperLib::etClosedPolygon); - ClipperLib::Paths contours_raw; - co.Execute(contours_raw, - delta); + Polygons contours_raw = offset_ccw(expoly.contour, - delta); if (contours_raw.empty()) // No need to try to offset the holes. return 0; @@ -530,24 +522,9 @@ void tree_supports_generate_paths( append(out, std::move(contours)); } else { // 2) Offset the holes one by one, collect the offsetted holes. - ClipperLib_Z::Paths holes; - { - for (const Polygon &hole : expoly.holes) { - ClipperLib::ClipperOffset co; - if (joinType == jtRound) - co.ArcTolerance = miterLimit; - else - co.MiterLimit = miterLimit; - co.ShortestEdgeLength = double(delta * 0.005); - co.AddPath(hole.points, joinType, ClipperLib::etClosedPolygon); - ClipperLib::Paths out2; - // Execute reorients the contours so that the outer most contour has a positive area. Thus the output - // contours will be CCW oriented even though the input paths are CW oriented. - // Offset is applied after contour reorientation, thus the signum of the offset value is reversed. - co.Execute(out2, delta); - append_paths_with_z(out2, 1 + (&hole - expoly.holes.data()), holes); - } - } + ClipperZUtils::ZPaths holes; + for (const Polygon &hole : expoly.holes) + append_paths_with_z(offset_ccw(hole, delta), 1 + (&hole - expoly.holes.data()), holes); // 3) Subtract holes from the contours. if (holes.empty()) { @@ -556,16 +533,11 @@ void tree_supports_generate_paths( } else { // Negative offset. There is a chance, that the offsetted hole intersects the outer contour. // Subtract the offsetted holes from the offsetted contours. - ClipperLib_Z::Clipper clipper; - clipper.ZFillFunction([](const ClipperLib_Z::IntPoint &e1bot, const ClipperLib_Z::IntPoint &e1top, const ClipperLib_Z::IntPoint &e2bot, const ClipperLib_Z::IntPoint &e2top, ClipperLib_Z::IntPoint &pt) { - //pt.z() = std::max(std::max(e1bot.z(), e1top.z()), std::max(e2bot.z(), e2top.z())); + ClipperZUtils::ZPaths output = ClipperZUtils::clip_zpaths(ctDifference, contours, false, holes, + [](const ClipperZUtils::ZPoint &, const ClipperZUtils::ZPoint &, const ClipperZUtils::ZPoint &, const ClipperZUtils::ZPoint &, ClipperZUtils::ZPoint &pt) { // Just mark the intersection. pt.z() = -1; }); - clipper.AddPaths(contours, ClipperLib_Z::ptSubject, true); - clipper.AddPaths(holes, ClipperLib_Z::ptClip, true); - ClipperLib_Z::Paths output; - clipper.Execute(ClipperLib_Z::ctDifference, output, ClipperLib_Z::pftNonZero, ClipperLib_Z::pftNonZero); if (! output.empty()) { append(out, std::move(output)); } else { @@ -582,7 +554,7 @@ void tree_supports_generate_paths( // Clip the sheath path to avoid the extruder to get exactly on the first point of the loop. const double clip_length = spacing * 0.15; const double anchor_length = spacing * 6.; - ClipperLib_Z::Paths anchor_candidates; + ClipperZUtils::ZPaths anchor_candidates; for (ExPolygon& expoly : closing_ex(polygons, float(SCALED_EPSILON), float(SCALED_EPSILON + 0.5 * flow.scaled_width()))) { std::unique_ptr eec; ExPolygons regions_to_draw_inner_wall{expoly}; @@ -608,10 +580,10 @@ void tree_supports_generate_paths( // First genrate a 2nd perimeter loop as a source for anchor candidates. // The anchor candidate points are annotated with an index of the source contour or with -1 if on intersection. anchor_candidates.clear(); - shrink_expolygon_with_contour_idx(expoly, flow.scaled_width(), DefaultJoinType, 1.2, anchor_candidates); + shrink_expolygon_with_contour_idx(expoly, flow.scaled_width(), jtMiter, 1.2, anchor_candidates); // Orient all contours CW. for (auto &path : anchor_candidates) - if (ClipperLib_Z::Area(path) > 0) std::reverse(path.begin(), path.end()); + if (ClipperZUtils::area(path) > 0) std::reverse(path.begin(), path.end()); // Draw the perimeters. Polylines polylines; @@ -628,13 +600,13 @@ void tree_supports_generate_paths( pl.clip_end(clip_length); if (pl.size() < 2) continue; // Find the foot of the seam point on anchor_candidates. Only pick an anchor point that was created by offsetting the source contour. - ClipperLib_Z::Path *closest_contour = nullptr; - Vec2d closest_point; - int closest_point_idx = -1; - double closest_point_t = 0.; - double d2min = std::numeric_limits::max(); - Vec2d seam_pt = pl.back().cast(); - for (ClipperLib_Z::Path &path : anchor_candidates) + ClipperZUtils::ZPath *closest_contour = nullptr; + Vec2d closest_point; + int closest_point_idx = -1; + double closest_point_t = 0.; + double d2min = std::numeric_limits::max(); + Vec2d seam_pt = pl.back().cast(); + for (ClipperZUtils::ZPath &path : anchor_candidates) for (int i = 0; i < int(path.size()); ++i) { int j = next_idx_modulo(i, path); if (path[i].z() == idx_loop || path[j].z() == idx_loop) { @@ -662,14 +634,14 @@ void tree_supports_generate_paths( // Try to cut an anchor from the closest_contour. // Both closest_contour and pl are CW oriented. pl.points.emplace_back(closest_point.cast()); - const ClipperLib_Z::Path &path = *closest_contour; - double remaining_length = anchor_length - (seam_pt - closest_point).norm(); - int i = closest_point_idx; - int j = next_idx_modulo(i, *closest_contour); - Vec2d pi(path[i].x(), path[i].y()); - Vec2d pj(path[j].x(), path[j].y()); - Vec2d v = pj - pi; - double l = v.norm(); + const ClipperZUtils::ZPath &path = *closest_contour; + double remaining_length = anchor_length - (seam_pt - closest_point).norm(); + int i = closest_point_idx; + int j = next_idx_modulo(i, *closest_contour); + Vec2d pi(path[i].x(), path[i].y()); + Vec2d pj(path[j].x(), path[j].y()); + Vec2d v = pj - pi; + double l = v.norm(); if (remaining_length < (1. - closest_point_t) * l) { // Just trim the current line. pl.points.emplace_back((closest_point + v * (remaining_length / l)).cast()); diff --git a/src/libslic3r/Support/SupportMaterial.cpp b/src/libslic3r/Support/SupportMaterial.cpp index a955eb5ca7..2850160d91 100644 --- a/src/libslic3r/Support/SupportMaterial.cpp +++ b/src/libslic3r/Support/SupportMaterial.cpp @@ -63,9 +63,9 @@ namespace Slic3r { #define PILLAR_SIZE (2.5) #define PILLAR_SPACING 10 -//#define SUPPORT_SURFACES_OFFSET_PARAMETERS ClipperLib::jtMiter, 3. -//#define SUPPORT_SURFACES_OFFSET_PARAMETERS ClipperLib::jtMiter, 1.5 -#define SUPPORT_SURFACES_OFFSET_PARAMETERS ClipperLib::jtSquare, 0. +//#define SUPPORT_SURFACES_OFFSET_PARAMETERS jtMiter, 3. +//#define SUPPORT_SURFACES_OFFSET_PARAMETERS jtMiter, 1.5 +#define SUPPORT_SURFACES_OFFSET_PARAMETERS jtSquare, 0. static constexpr bool support_with_sheath = false; @@ -1634,7 +1634,7 @@ static inline std::tuple detect_contacts( offset( diff_polygons, scaled(SUPPORT_MATERIAL_MARGIN / NUM_MARGIN_STEPS), - ClipperLib::jtRound, + jtRound, // round mitter limit scale_(0.05)), slices_margin.polygons); diff --git a/src/libslic3r/Support/TreeModelVolumes.cpp b/src/libslic3r/Support/TreeModelVolumes.cpp index 70c33aab0d..e933ac3c1b 100644 --- a/src/libslic3r/Support/TreeModelVolumes.cpp +++ b/src/libslic3r/Support/TreeModelVolumes.cpp @@ -112,7 +112,7 @@ TreeModelVolumes::TreeModelVolumes( tbb::parallel_for(tbb::blocked_range(num_raft_layers, num_layers, std::min(1, std::max(16, num_layers / (8 * tbb::this_task_arena::max_concurrency())))), [&](const tbb::blocked_range &range) { for (size_t layer_idx = range.begin(); layer_idx < range.end(); ++ layer_idx) - outlines[layer_idx] = polygons_simplify(to_polygons(print_object.get_layer(layer_idx - num_raft_layers)->lslices), mesh_settings.resolution, polygons_strictly_simple); + outlines[layer_idx] = polygons_simplify(to_polygons(print_object.get_layer(layer_idx - num_raft_layers)->lslices), mesh_settings.resolution); }); } #endif @@ -462,7 +462,7 @@ void TreeModelVolumes::calculateCollision(const coord_t radius, const LayerIndex // if a key does not exist when it is accessed it is added! collision_areas_offsetted[layer_idx] = offset_value == 0 ? union_(collision_areas) : - offset(union_ex(collision_areas), offset_value, ClipperLib::jtMiter, 1.2); + offset(union_ex(collision_areas), offset_value, jtMiter, 1.2); if(throw_on_cancel) throw_on_cancel(); } @@ -515,16 +515,16 @@ void TreeModelVolumes::calculateCollision(const coord_t radius, const LayerIndex // the conditional -0.5 ensures that plastic can never touch on the diagonal // downward when the z_distance_top_layers = 1. It is assumed to be better to // not support an overhang<90 degree than to risk fusing to it. - append(collisions, offset(union_ex(collision_areas_original), radius + required_range_x, ClipperLib::jtMiter, 1.2)); + append(collisions, offset(union_ex(collision_areas_original), radius + required_range_x, jtMiter, 1.2)); } collisions = processing_last_mesh && layer_idx < int(anti_overhang.size()) ? - union_(collisions, offset(union_ex(anti_overhang[layer_idx]), radius, ClipperLib::jtMiter, 1.2)) : + union_(collisions, offset(union_ex(anti_overhang[layer_idx]), radius, jtMiter, 1.2)) : union_(collisions); auto &dst = data[layer_idx]; if (processing_last_mesh) { if (! dst.empty()) collisions = union_(collisions, dst); - dst = polygons_simplify(collisions, min_resolution, polygons_strictly_simple); + dst = polygons_simplify(collisions, min_resolution); } else append(dst, std::move(collisions)); if (throw_on_cancel) @@ -552,7 +552,7 @@ void TreeModelVolumes::calculateCollision(const coord_t radius, const LayerIndex if (processing_last_mesh) { if (! dst.empty()) placable = union_(placable, dst); - dst = polygons_simplify(placable, min_resolution, polygons_strictly_simple); + dst = polygons_simplify(placable, min_resolution); } else append(dst, placable); if (throw_on_cancel) @@ -601,8 +601,8 @@ void TreeModelVolumes::calculateCollisionHolefree(const std::vectorgetCollision(m_increase_until_radius, layer_idx, false)), - 5 - increase_radius_ceil, ClipperLib::jtRound, m_min_resolution), - m_min_resolution, polygons_strictly_simple)); + 5 - increase_radius_ceil, jtRound, m_min_resolution), + m_min_resolution)); if (throw_on_cancel) throw_on_cancel(); } @@ -689,10 +689,10 @@ void TreeModelVolumes::calculateAvoidance(const std::vector &ke latest_avoidance = union_(current_layer_collisions, offset(latest_avoidance, istep + 1 == move_steps ? - last_move_step : - move_step, - ClipperLib::jtRound, m_min_resolution)); + jtRound, m_min_resolution)); if (task.to_model) latest_avoidance = diff(latest_avoidance, getPlaceableAreas(task.radius, layer_idx, throw_on_cancel)); - latest_avoidance = polygons_simplify(latest_avoidance, m_min_resolution, polygons_strictly_simple); + latest_avoidance = polygons_simplify(latest_avoidance, m_min_resolution); data.emplace_back(RadiusLayerPair{task.radius, layer_idx}, latest_avoidance); if (throw_on_cancel) throw_on_cancel(); @@ -815,12 +815,12 @@ void TreeModelVolumes::calculateWallRestrictions(const std::vectorenable_support.value && is_tree(m_object_config->support_type.value); @@ -2571,7 +2571,7 @@ void TreeSupport::draw_circles() Point direction = normal(pt_on_expoly - pt_on_poly, line_width_scaled / 2); hole_lower.translate(direction); // note to expand a hole, we need to do negative offset - auto hole_expanded = offset(hole_lower, -line_width_scaled / 4, ClipperLib::JoinType::jtSquare); + auto hole_expanded = offset(hole_lower, -line_width_scaled / 4, jtSquare); if (!hole_expanded.empty()) { base_area_lower.holes.push_back(std::move(hole_expanded[0])); holePropagationInfos.insert({ &base_area_lower.holes.back(), {25, direction, pt_far_on_poly} }); @@ -2584,7 +2584,7 @@ void TreeSupport::draw_circles() auto&& direction = std::get<1>(holePropagationInfos[&hole]); hole_lower.translate(direction); // note to shrink a hole, we need to do positive offset - auto hole_expanded = offset(hole_lower, line_width_scaled / 2, ClipperLib::JoinType::jtSquare); + auto hole_expanded = offset(hole_lower, line_width_scaled / 2, jtSquare); Point farPoint = std::get<2>(holePropagationInfos[&hole]) + direction * 2; if (!hole_expanded.empty()) { base_area_lower.holes.push_back(std::move(hole_expanded[0])); diff --git a/src/libslic3r/Support/TreeSupport3D.cpp b/src/libslic3r/Support/TreeSupport3D.cpp index 3cd3f4110d..c9fceabaea 100644 --- a/src/libslic3r/Support/TreeSupport3D.cpp +++ b/src/libslic3r/Support/TreeSupport3D.cpp @@ -68,7 +68,7 @@ static inline void validate_range(const Point &pt) { static constexpr const int32_t hi = 65536 * 16384; if (pt.x() > hi || pt.y() > hi || -pt.x() > hi || -pt.y() > hi) - throw ClipperLib::clipperException("Coordinate outside allowed range"); + throw RuntimeError("Coordinate outside allowed range"); } static inline void validate_range(const Points &points) @@ -832,16 +832,16 @@ static std::optional> polyline_sample_next_point_at_dis } // offset in steps for (int i = 0; i < steps; ++ i) { - ret = diff(offset(ret, step_size, ClipperLib::jtRound, scaled(0.01)), collision_trimmed()); + ret = diff(offset(ret, step_size, jtRound, scaled(0.01)), collision_trimmed()); // ensure that if many offsets are done the performance does not suffer extremely by the new vertices of jtRound. if (i % 10 == 7) - ret = polygons_simplify(ret, scaled(0.015), polygons_strictly_simple); + ret = polygons_simplify(ret, scaled(0.015)); } // offset the remainder float last_offset = distance - steps * step_size; if (last_offset > SCALED_EPSILON) - ret = offset(ret, distance - steps * step_size, ClipperLib::jtRound, scaled(0.01)); - ret = polygons_simplify(ret, scaled(0.015), polygons_strictly_simple); + ret = offset(ret, distance - steps * step_size, jtRound, scaled(0.01)); + ret = polygons_simplify(ret, scaled(0.015)); if (do_final_difference) ret = diff(ret, collision_trimmed()); @@ -1622,8 +1622,8 @@ static Point move_inside_if_outside(const Polygons &polygons, Point from, int di safe_movement_distance, safe_movement_distance + radius, 1); } if (settings.no_error && settings.move) - // as ClipperLib::jtRound has to be used for offsets this simplify is VERY important for performance. - polygons_simplify(increased, scaled(0.025), polygons_strictly_simple); + // as jtRound has to be used for offsets this simplify is VERY important for performance. + polygons_simplify(increased, scaled(0.025)); } else // if no movement is done the areas keep parent area as no move == offset(0) increased = parent.influence_area; @@ -4145,7 +4145,7 @@ void organic_draw_branches( base_layer_polygons = smooth_outward(union_(base_layer_polygons), config.support_line_width); //FIXME was .smooth(50); //smooth_outward(closing(std::move(bottom), closing_distance + minimum_island_radius, closing_distance, SUPPORT_SURFACES_OFFSET_PARAMETERS), smoothing_distance) : // simplify a bit, to ensure the output does not contain outrageous amounts of vertices. Should not be necessary, just a precaution. - base_layer_polygons = polygons_simplify(base_layer_polygons, std::min(scaled(0.03), double(config.resolution)), polygons_strictly_simple); + base_layer_polygons = polygons_simplify(base_layer_polygons, std::min(scaled(0.03), double(config.resolution))); } // Subtract top contact layer polygons from support base. diff --git a/src/libslic3r/Support/TreeSupportCommon.hpp b/src/libslic3r/Support/TreeSupportCommon.hpp index 07dcb324b8..fb08a1a928 100644 --- a/src/libslic3r/Support/TreeSupportCommon.hpp +++ b/src/libslic3r/Support/TreeSupportCommon.hpp @@ -588,8 +588,6 @@ private: // std::vector known_z; }; -static constexpr const bool polygons_strictly_simple = false; - inline double tiny_area_threshold() { return sqr(scaled(0.001)); } inline void tree_supports_show_error(std::string_view message, bool critical) diff --git a/src/libslic3r/TriangleMeshSlicer.cpp b/src/libslic3r/TriangleMeshSlicer.cpp index a08ea6b55d..444f33c296 100644 --- a/src/libslic3r/TriangleMeshSlicer.cpp +++ b/src/libslic3r/TriangleMeshSlicer.cpp @@ -1856,7 +1856,7 @@ static ExPolygons make_expolygons_simple(std::vector &lines) return slices; } -static void make_expolygons(const Polygons &loops, const float closing_radius, const float extra_offset, ClipperLib::PolyFillType fill_type, ExPolygons* slices) +static void make_expolygons(const Polygons &loops, const float closing_radius, const float extra_offset, PolyFillType fill_type, ExPolygons* slices) { /* Input loops are not suitable for evenodd nor nonzero fill types, as we might get @@ -2156,8 +2156,8 @@ std::vector slice_mesh_ex( const auto this_mode = layer_id < params.slicing_mode_normal_below_layer ? params.mode_below : params.mode; Slic3r::make_expolygons( layers_p[layer_id], params.closing_radius, params.extra_offset, - this_mode == MeshSlicingParams::SlicingMode::EvenOdd ? ClipperLib::pftEvenOdd : - this_mode == MeshSlicingParams::SlicingMode::PositiveLargestContour ? ClipperLib::pftPositive : ClipperLib::pftNonZero, + this_mode == MeshSlicingParams::SlicingMode::EvenOdd ? pftEvenOdd : + this_mode == MeshSlicingParams::SlicingMode::PositiveLargestContour ? pftPositive : pftNonZero, &expolygons); //FIXME simplify if (this_mode == MeshSlicingParams::SlicingMode::PositiveLargestContour) diff --git a/src/libslic3r/TriangleMeshSlicer.hpp b/src/libslic3r/TriangleMeshSlicer.hpp index 1f7bba9d27..1a78e5b7d0 100644 --- a/src/libslic3r/TriangleMeshSlicer.hpp +++ b/src/libslic3r/TriangleMeshSlicer.hpp @@ -12,13 +12,13 @@ struct MeshSlicingParams { enum class SlicingMode : uint32_t { // Regular slicing, maintain all contours and their orientation. - // slice_mesh_ex() applies ClipperLib::pftNonZero rule to the result of slice_mesh(). + // slice_mesh_ex() applies pftNonZero rule to the result of slice_mesh(). Regular, // For slicing 3DLabPrints plane models (aka to be compatible with S3D default strategy). - // slice_mesh_ex() applies ClipperLib::pftEvenOdd rule. slice_mesh() slices EvenOdd as Regular. + // slice_mesh_ex() applies pftEvenOdd rule. slice_mesh() slices EvenOdd as Regular. EvenOdd, // Maintain all contours, orient all contours CCW. - // slice_mesh_ex() applies ClipperLib::pftNonZero rule, thus holes will be closed. + // slice_mesh_ex() applies pftNonZero rule, thus holes will be closed. Positive, // Orient all contours CCW and keep only the contour with the largest area. // This mode is useful for slicing complex objects in vase mode. diff --git a/src/libslic3r/clipper.cpp b/src/libslic3r/clipper.cpp deleted file mode 100644 index 8f38ba6215..0000000000 --- a/src/libslic3r/clipper.cpp +++ /dev/null @@ -1,13 +0,0 @@ -// Hackish wrapper around the ClipperLib library to compile the Clipper library using Slic3r::Point. - -#include "clipper.hpp" - -// Don't include for the second time. -#define clipper_hpp - -// Override ClipperLib namespace to Slic3r::ClipperLib -#define CLIPPERLIB_NAMESPACE_PREFIX Slic3r -// Override Slic3r::ClipperLib::IntPoint to Slic3r::Point -#define CLIPPERLIB_INTPOINT_TYPE Slic3r::Point - -#include diff --git a/src/libslic3r/clipper.hpp b/src/libslic3r/clipper.hpp deleted file mode 100644 index b0dd51a4f7..0000000000 --- a/src/libslic3r/clipper.hpp +++ /dev/null @@ -1,26 +0,0 @@ -// Hackish wrapper around the ClipperLib library to compile the Clipper library using Slic3r's own Point type. - -#ifndef slic3r_clipper_hpp - -#ifdef clipper_hpp -#error "You should include the libslic3r/clipper.hpp before clipper/clipper.hpp" -#endif - -#ifdef CLIPPERLIB_USE_XYZ -#error "Something went wrong. Using clipper.hpp with Slic3r Point type, but CLIPPERLIB_USE_XYZ is defined." -#endif - -#define slic3r_clipper_hpp - -#include "Point.hpp" - -#define CLIPPERLIB_NAMESPACE_PREFIX Slic3r -#define CLIPPERLIB_INTPOINT_TYPE Slic3r::Point - -#include - -#undef clipper_hpp -#undef CLIPPERLIB_NAMESPACE_PREFIX -#undef CLIPPERLIB_INTPOINT_TYPE - -#endif // slic3r_clipper_hpp diff --git a/src/libslic3r/pchheader.hpp b/src/libslic3r/pchheader.hpp index 1bedfb482a..401e9b1145 100644 --- a/src/libslic3r/pchheader.hpp +++ b/src/libslic3r/pchheader.hpp @@ -110,8 +110,6 @@ #include #include -#include -#include "clipper.hpp" #include "BoundingBox.hpp" #include "ClipperUtils.hpp" #include "Config.hpp" diff --git a/tests/fff_print/test_fill.cpp b/tests/fff_print/test_fill.cpp index 1d5a3bd345..0efaba023d 100644 --- a/tests/fff_print/test_fill.cpp +++ b/tests/fff_print/test_fill.cpp @@ -1671,24 +1671,32 @@ TEST_CASE("Smoothing multiline lightning infill keeps its outlines connected", " // and the outlines of branches that run close to each other merge into one. Rounding the branches // before those outlines are built moves them apart, which breaks the merged outlines up into // separate loops - many more of them, each needing its own travel move. + // A micron change of the cube moves the loop count of a single slice by several percent, so the + // shapes of a few nearly equal cubes are added up. auto shape_for = [](const std::string &smooth_factor) { - Print print; - Slic3r::Test::init_and_process_print({Slic3r::Test::cube(20)}, print, - {{"sparse_infill_pattern", "lightning"}, - {"sparse_infill_density", "50%"}, - {"fill_multiline", 2}, - {"sparse_infill_smooth_factor", smooth_factor}, - {"layer_height", 0.2}}); - return sparse_infill_shape(print); + SparseInfillShape sum; + for (const double size : {20., 20.001, 20.002, 20.003}) { + Print print; + Slic3r::Test::init_and_process_print({Slic3r::Test::cube(size)}, print, + {{"sparse_infill_pattern", "lightning"}, + {"sparse_infill_density", "50%"}, + {"fill_multiline", 2}, + {"sparse_infill_smooth_factor", smooth_factor}, + {"layer_height", 0.2}}); + const SparseInfillShape shape = sparse_infill_shape(print); + sum.path_count += shape.path_count; + sum.point_count += shape.point_count; + sum.sharp_turns += shape.sharp_turns; + } + return sum; }; const SparseInfillShape sharp = shape_for("0%"); const SparseInfillShape smooth = shape_for("100%"); REQUIRE(sharp.path_count > 0); - // The loop count varies by a loop or two between platforms and between runs, so this is not an - // exact comparison. Smoothing should leave it about where it was; uncapping the smoothing - // reach, the regression this guards against, adds about 10%. + // Smoothing should leave the loop count about where it was; uncapping the smoothing reach, the + // regression this guards against, adds about 10%. const size_t allowed_extra = sharp.path_count / 50; // 2% REQUIRE(smooth.path_count <= sharp.path_count + allowed_extra); // The outlines are still rounded. diff --git a/tests/fff_print/test_gcodewriter.cpp b/tests/fff_print/test_gcodewriter.cpp index dc46df965d..f371fcb7f4 100644 --- a/tests/fff_print/test_gcodewriter.cpp +++ b/tests/fff_print/test_gcodewriter.cpp @@ -23,10 +23,7 @@ using namespace Slic3r; using namespace Slic3r::Test; -// Arrange on a finite bed, not an unbounded InfiniteBed: the latter places items -// near INT64_MIN/4 (~2.3e18), which reaches ClipperLib's coordinate limit and throws -// "Coordinate outside allowed range" on Windows/arm64. A 500x500 bed keeps coordinates -// small while still covering large printers. +// Arrange on a 500x500 bed, which keeps coordinates small while still covering large printers. static void arrange_objects_on_test_bed(Model &model, const DynamicPrintConfig &config) { const BoundingBox bed{Point::new_scale(0., 0.), Point::new_scale(500., 500.)}; diff --git a/tests/libslic3r/test_clipper_offset.cpp b/tests/libslic3r/test_clipper_offset.cpp index 3b855130be..ecf14a60c8 100644 --- a/tests/libslic3r/test_clipper_offset.cpp +++ b/tests/libslic3r/test_clipper_offset.cpp @@ -22,7 +22,7 @@ SCENARIO("Constant offset", "[ClipperUtils]") { WHEN("Slic3r::offset()") { for (double miter : { 2.0, 1.5, 1.2 }) { DYNAMIC_SECTION("plus 1mm, miter " << miter << "x") { - output = Slic3r::offset(box20mm, 1. * s, ClipperLib::jtMiter, miter); + output = Slic3r::offset(box20mm, 1. * s, jtMiter, miter); #ifdef TESTS_EXPORT_SVGS { SVG svg(debug_out_path("constant_offset_box20mm_plus1mm_miter%lf.svg", miter).c_str(), get_extents(output)); @@ -36,7 +36,7 @@ SCENARIO("Constant offset", "[ClipperUtils]") { } } DYNAMIC_SECTION("minus 1mm, miter " << miter << "x") { - output = Slic3r::offset(box20mm, - 1. * s, ClipperLib::jtMiter, miter); + output = Slic3r::offset(box20mm, - 1. * s, jtMiter, miter); #ifdef TESTS_EXPORT_SVGS { SVG svg(debug_out_path("constant_offset_box20mm_minus1mm_miter%lf.svg", miter).c_str(), get_extents(output)); @@ -96,7 +96,7 @@ SCENARIO("Constant offset", "[ClipperUtils]") { for (double miter : { 2.0, 1.5, 1.2 }) { DYNAMIC_SECTION("miter " << miter << "x") { WHEN("plus 1mm") { - output = Slic3r::offset_ex(box20mm, 1. * s, ClipperLib::jtMiter, miter); + output = Slic3r::offset_ex(box20mm, 1. * s, jtMiter, miter); #ifdef TESTS_EXPORT_SVGS { SVG svg(debug_out_path("constant_offset_box20mm_10mm_hole_plus1mm_miter%lf.svg", miter).c_str(), get_extents(output)); @@ -110,7 +110,7 @@ SCENARIO("Constant offset", "[ClipperUtils]") { } } WHEN("minus 1mm") { - output = Slic3r::offset_ex(box20mm, - 1. * s, ClipperLib::jtMiter, miter); + output = Slic3r::offset_ex(box20mm, - 1. * s, jtMiter, miter); #ifdef TESTS_EXPORT_SVGS { SVG svg(debug_out_path("constant_offset_box20mm_10mm_hole_minus1mm_miter%lf.svg", miter).c_str(), get_extents(output)); @@ -176,7 +176,7 @@ SCENARIO("Constant offset", "[ClipperUtils]") { SECTION("Slic3r::offset()") { for (double miter : { 2.0, 1.5, 1.2 }) { DYNAMIC_SECTION("Outer offset 1mm, miter " << miter << "x") { - output = Slic3r::offset(triangle20mm, offset * s, ClipperLib::jtMiter, 2.0); + output = Slic3r::offset(triangle20mm, offset * s, jtMiter, 2.0); #ifdef TESTS_EXPORT_SVGS { SVG svg(debug_out_path("constant_offset_triangle20mm_plus1mm_miter%lf.svg", miter).c_str(), get_extents(output)); diff --git a/tests/libslic3r/test_clipper_utils.cpp b/tests/libslic3r/test_clipper_utils.cpp index fe9961a771..ddec07089b 100644 --- a/tests/libslic3r/test_clipper_utils.cpp +++ b/tests/libslic3r/test_clipper_utils.cpp @@ -10,6 +10,27 @@ using namespace Slic3r; +// Clipper may start an output polygon at any of its vertices. +static Polygon start_at_min(Polygon polygon) +{ + std::rotate(polygon.points.begin(), std::min_element(polygon.points.begin(), polygon.points.end()), polygon.points.end()); + return polygon; +} +static Polygons start_at_min(Polygons polygons) +{ + for (Polygon &polygon : polygons) + polygon = start_at_min(std::move(polygon)); + return polygons; +} +static ExPolygons start_at_min(ExPolygons expolygons) +{ + for (ExPolygon &expolygon : expolygons) { + expolygon.contour = start_at_min(std::move(expolygon.contour)); + expolygon.holes = start_at_min(std::move(expolygon.holes)); + } + return expolygons; +} + SCENARIO("Various Clipper operations - xs/t/11_clipper.t", "[ClipperUtils]") { // CCW oriented contour Slic3r::Polygon square{ { 200, 100 }, {200, 200}, {100, 200}, {100, 100} }; @@ -20,25 +41,25 @@ SCENARIO("Various Clipper operations - xs/t/11_clipper.t", "[ClipperUtils]") { WHEN("offset") { Polygons result = Slic3r::offset(square_with_hole, 5.f); THEN("offset matches") { - REQUIRE(result == Polygons { + REQUIRE(start_at_min(result) == start_at_min(Polygons { { { 205, 205 }, { 95, 205 }, { 95, 95 }, { 205, 95 }, }, - { { 155, 145 }, { 145, 145 }, { 145, 155 }, { 155, 155 } } }); + { { 155, 145 }, { 145, 145 }, { 145, 155 }, { 155, 155 } } })); } } WHEN("offset_ex") { ExPolygons result = Slic3r::offset_ex(square_with_hole, 5.f); THEN("offset matches") { - REQUIRE(result == ExPolygons { { + REQUIRE(start_at_min(result) == start_at_min(ExPolygons { { { { 205, 205 }, { 95, 205 }, { 95, 95 }, { 205, 95 }, }, - { { 145, 145 }, { 145, 155 }, { 155, 155 }, { 155, 145 } } } } ); + { { 145, 145 }, { 145, 155 }, { 155, 155 }, { 155, 145 } } } })); } } WHEN("offset2_ex") { ExPolygons result = Slic3r::offset2_ex({ square_with_hole }, 5.f, -2.f); THEN("offset matches") { - REQUIRE(result == ExPolygons { { + REQUIRE(start_at_min(result) == start_at_min(ExPolygons { { { { 203, 203 }, { 97, 203 }, { 97, 97 }, { 203, 97 } }, - { { 143, 143 }, { 143, 157 }, { 157, 157 }, { 157, 143 } } } } ); + { { 143, 143 }, { 143, 157 }, { 157, 157 }, { 157, 143 } } } })); } } } @@ -225,24 +246,7 @@ SCENARIO("Various Clipper operations - t/clipper.t", "[ClipperUtils]") { } } -template -double polytree_area(const Tree &tree, std::vector *out) -{ - traverse_pt(tree, out); - - return std::accumulate(out->begin(), out->end(), 0.0, - [](double a, const P &p) { return a + p.area(); }); -} - -size_t count_polys(const ExPolygons& expolys) -{ - size_t c = 0; - for (auto &ep : expolys) c += ep.holes.size() + 1; - - return c; -} - -TEST_CASE("Traversing Clipper PolyTree", "[ClipperUtils]") { +TEST_CASE("Top level expolygons of an even-odd union", "[ClipperUtils]") { // Create a polygon representing unit box Polygon unitbox; const auto UNIT = coord_t(1. / SCALING_FACTOR); @@ -266,36 +270,24 @@ TEST_CASE("Traversing Clipper PolyTree", "[ClipperUtils]") { Polygon inner_right = inner_left; inner_right.translate(UNIT * 10, 0); - Polygons reference = union_({box_frame, hole_left, hole_right, inner_left, inner_right}); + ExPolygons reference; + for (const Polygon &polygon : union_({box_frame, hole_left, hole_right, inner_left, inner_right})) + reference.emplace_back(polygon); - ClipperLib::PolyTree tree = union_pt(reference); double area_sum = box_frame.area() + hole_left.area() + hole_right.area() + inner_left.area() + inner_right.area(); REQUIRE(area_sum > 0); - SECTION("Traverse into Polygons WITHOUT spatial ordering") { - Polygons output; - REQUIRE(area_sum == Catch::Approx(polytree_area(tree.GetFirst(), &output))); - REQUIRE(output.size() == reference.size()); - } - - SECTION("Traverse into ExPolygons WITHOUT spatial ordering") { - ExPolygons output; - REQUIRE(area_sum == Catch::Approx(polytree_area(tree.GetFirst(), &output))); - REQUIRE(count_polys(output) == reference.size()); - } - - SECTION("Traverse into Polygons WITH spatial ordering") { - Polygons output; - REQUIRE(area_sum == Catch::Approx(polytree_area(tree.GetFirst(), &output))); - REQUIRE(output.size() == reference.size()); - } - - SECTION("Traverse into ExPolygons WITH spatial ordering") { - ExPolygons output; - REQUIRE(area_sum == Catch::Approx(polytree_area(tree.GetFirst(), &output))); - REQUIRE(count_polys(output) == reference.size()); - } + ExPolygons nested; + ExPolygons top_level = top_level_expolygons(reference, &nested); + auto area = [](const ExPolygons &expolys) { + return std::accumulate(expolys.begin(), expolys.end(), 0.0, [](double a, const ExPolygon &e) { return a + e.area(); }); + }; + REQUIRE(top_level.size() == 1); + REQUIRE(top_level.front().holes.size() == 2); + REQUIRE(nested.size() == 2); + REQUIRE(area_sum == Catch::Approx(area(top_level) + area(nested))); + REQUIRE(top_level_expolygons(reference).size() == 1); } diff --git a/tests/libslic3r/test_elephant_foot_compensation.cpp b/tests/libslic3r/test_elephant_foot_compensation.cpp index f509c6c0dd..ab9700a305 100644 --- a/tests/libslic3r/test_elephant_foot_compensation.cpp +++ b/tests/libslic3r/test_elephant_foot_compensation.cpp @@ -14,7 +14,7 @@ using namespace Slic3r; // #define TESTS_EXPORT_SVGS namespace Slic3r { - ClipperLib::Path mittered_offset_path_scaled(const Points& contour, const std::vector& deltas, double miter_limit); + Points mittered_offset_path_scaled(const Points& contour, const std::vector& deltas, double miter_limit); } static ExPolygon spirograph_gear_1mm() diff --git a/tests/sla_print/sla_test_utils.cpp b/tests/sla_print/sla_test_utils.cpp index a74bad09e4..8951bb3853 100644 --- a/tests/sla_print/sla_test_utils.cpp +++ b/tests/sla_print/sla_test_utils.cpp @@ -261,7 +261,8 @@ static void _test_concave_hull(const Polygons &hull, const ExPolygons &polys) REQUIRE(cchull_holes == 0); - Polygons intr = diff(to_polygons(polys), hull); + // Hull vertices where the connectors cross the input are rounded, so the hull may miss the input by a unit or two. + Polygons intr = diff(to_polygons(polys), offset(hull, float(SCALED_EPSILON))); REQUIRE(intr.empty()); }