From a394ba696c1d300c89c4d1e421bfd191b0d0f060 Mon Sep 17 00:00:00 2001 From: Argo <52103738+Argolein@users.noreply.github.com> Date: Fri, 17 Apr 2026 06:27:42 +0200 Subject: [PATCH] Tree support "on build plate only" no interface layers fix (#13192) --- src/CMakeLists.txt | 12 ++++ src/libslic3r/Support/TreeSupport.cpp | 18 +++++- src/libslic3r/Support/TreeSupport3D.cpp | 79 +++++++++++++++---------- src/libslic3r/Support/TreeSupport3D.hpp | 20 ++++++- 4 files changed, 94 insertions(+), 35 deletions(-) diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index fffc126505..f7de86410d 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -44,6 +44,18 @@ if (SLIC3R_GUI) include(${wxWidgets_USE_FILE}) endif() + if(APPLE AND TARGET wx::wxgl) + # Newer wxWidgets CMake exports may inject the macOS OpenGL framework + # as a quoted file path inside wx::wxgl, which Ninja then treats as a + # missing input file. Orca links OpenGL explicitly elsewhere, so strip + # these broken pseudo-paths from the imported target. + get_target_property(_wx_gl_libs wx::wxgl INTERFACE_LINK_LIBRARIES) + if(_wx_gl_libs) + list(FILTER _wx_gl_libs EXCLUDE REGEX "framework OpenGL") + set_target_properties(wx::wxgl PROPERTIES INTERFACE_LINK_LIBRARIES "${_wx_gl_libs}") + endif() + endif() + find_package(JPEG QUIET) string(REGEX MATCH "wxpng" WX_PNG_BUILTIN ${wxWidgets_LIBRARIES}) diff --git a/src/libslic3r/Support/TreeSupport.cpp b/src/libslic3r/Support/TreeSupport.cpp index 50272009cd..fcd29e4dcd 100644 --- a/src/libslic3r/Support/TreeSupport.cpp +++ b/src/libslic3r/Support/TreeSupport.cpp @@ -2076,9 +2076,10 @@ void TreeSupport::draw_circles() if (!area.empty()) has_circle_node = true; if (node.need_extra_wall) need_extra_wall = true; - // merge overhang to get a smoother interface surface - // Do not merge when buildplate_only is on, because some underneath nodes may have been deleted. - if (top_interface_layers > 0 && node.support_roof_layers_below > 0 && !on_buildplate_only && !node.is_sharp_tail) { + // Merge the overhang into the roof area so tree tips can still produce + // a continuous support interface. Suppressing this for build-plate-only + // support drops the roof polygons entirely in valid tree branches. + if (top_interface_layers > 0 && node.support_roof_layers_below > 0 && !node.is_sharp_tail) { ExPolygons overhang_expanded; if (node.overhang.contour.size() > 100 || node.overhang.holes.size()>1) overhang_expanded.emplace_back(node.overhang); @@ -2121,6 +2122,17 @@ void TreeSupport::draw_circles() roof_1st_layer = diff_ex(roof_1st_layer, ClipperUtils::clip_clipper_polygons_with_subject_bbox(roof_areas,get_extents(roof_1st_layer))); roof_1st_layer = intersection_ex(roof_1st_layer, m_machine_border); + // Build-plate-only pruning can collapse the roof stack down to a single + // printable layer. In that case we still need to emit an interface layer + // instead of downgrading the last roof-adjacent layer to base support. + if (on_buildplate_only && top_interface_layers > 0 && roof_areas.empty() && !roof_1st_layer.empty()) { + append(roof_areas, roof_1st_layer); + roof_1st_layer.clear(); + max_layers_above_roof = std::max(max_layers_above_roof, max_layers_above_roof1); + max_layers_above_roof1 = 0; + interface_id = obj_layer_nr % top_interface_layers; + } + ExPolygons roofs; append(roofs, roof_1st_layer); append(roofs, roof_areas);append(roofs, roof_gap_areas); base_areas = diff_ex(base_areas, ClipperUtils::clip_clipper_polygons_with_subject_bbox(roofs, get_extents(base_areas))); base_areas = intersection_ex(base_areas, m_machine_border); diff --git a/src/libslic3r/Support/TreeSupport3D.cpp b/src/libslic3r/Support/TreeSupport3D.cpp index 307e4d314d..ba3008cd83 100644 --- a/src/libslic3r/Support/TreeSupport3D.cpp +++ b/src/libslic3r/Support/TreeSupport3D.cpp @@ -898,6 +898,7 @@ public: size_t dtt_roof_tip; for (dtt_roof_tip = 0; dtt_roof_tip < roof_tip_layers && insert_layer_idx - dtt_roof_tip >= 1; ++ dtt_roof_tip) { size_t this_layer_idx = insert_layer_idx - dtt_roof_tip; + const size_t roof_recovery_depth = dtt_roof_tip + supports_roof_layers; auto evaluateRoofWillGenerate = [&](const std::pair &p) { //FIXME Vojtech: The circle is just shifted, it has a known size, the infill should fit all the time! #if 0 @@ -927,7 +928,9 @@ public: // don't move until roof_tip_layers - dtt_roof_tip, // supports roof - dtt_roof_tip + supports_roof_layers > 0, + roof_recovery_depth > 0, + // recovered roof/contact depth for this slice + roof_recovery_depth, // disable ovalization false); } @@ -942,9 +945,10 @@ public: roof_circle.translate(p.first); new_roofs.emplace_back(std::move(roof_circle)); } - this->add_roof(std::move(new_roofs), this_layer_idx, dtt_roof_tip + supports_roof_layers); + this->add_roof(std::move(new_roofs), this_layer_idx, roof_recovery_depth); } + const size_t roof_recovery_depth = dtt_roof_tip + supports_roof_layers; for (const LineInformation &line : lines) { // If a line consists of enough tips, the assumption is that it is not a single tip, but part of a simulated support pattern. // Ovalisation should be disabled for these to improve the quality of the lines when tip_diameter=line_width @@ -954,14 +958,16 @@ public: // don't move until dont_move_until > dtt_roof_tip ? dont_move_until - dtt_roof_tip : 0, // supports roof - dtt_roof_tip + supports_roof_layers > 0, + roof_recovery_depth > 0, + // recovered roof/contact depth for this slice + roof_recovery_depth, disable_ovalistation); } } private: // called by this->add_points_along_lines() - void add_point_as_influence_area(std::pair p, LayerIndex insert_layer, size_t dont_move_until, bool roof, bool skip_ovalisation) + void add_point_as_influence_area(std::pair p, LayerIndex insert_layer, size_t dont_move_until, bool roof, size_t roof_recovery_dtt, bool skip_ovalisation) { bool to_bp = p.second == LineStatus::TO_BP || p.second == LineStatus::TO_BP_SAFE; bool gracious = to_bp || p.second == LineStatus::TO_MODEL_GRACIOUS || p.second == LineStatus::TO_MODEL_GRACIOUS_SAFE; @@ -997,7 +1003,7 @@ private: state.supports_roof = roof; state.dont_move_until = dont_move_until; state.can_use_safe_radius = safe_radius; - state.missing_roof_layers = force_tip_to_roof ? dont_move_until : 0; + state.set_pending_roof_recovery(force_tip_to_roof ? dont_move_until : 0, roof_recovery_dtt); state.skip_ovalisation = skip_ovalisation; move_bounds[insert_layer].emplace_back(state, std::move(circle)); } @@ -1095,10 +1101,9 @@ void finalize_raft_contact( // 1) Maximum num_support_roof_layers roof (top interface & contact) layers. // 2) Tree tips supporting either the roof layers or the object itself. // num_support_roof_layers should always be respected: -// If num_support_roof_layers contact layers could not be produced, then the tree tip -// is augmented with SupportElementState::missing_roof_layers -// and the top "missing_roof_layers" of such particular tree tips are supposed to be coverted to -// roofs aka interface layers by the tool path generator. +// If the requested roof/contact stack cannot be generated directly, the affected tree tips +// carry explicit pending roof recovery metadata so the sliced branch geometry can later be +// promoted back to top contacts / interfaces at the correct contact depth. void sample_overhang_area( // Area to support Polygons &&overhang_area, @@ -1606,7 +1611,6 @@ static Point move_inside_if_outside(const Polygons &polygons, Point from, int di if (settings.increase_radius) current_elem.effective_radius_height += 1; coord_t radius = support_element_collision_radius(config, current_elem); - const auto _tiny_area_threshold = tiny_area_threshold(); if (settings.move) { increased = relevant_offset; @@ -2059,7 +2063,10 @@ static void increase_areas_one_layer( out.supports_roof = first.supports_roof || second.supports_roof; out.dont_move_until = std::max(first.dont_move_until, second.dont_move_until); out.can_use_safe_radius = first.can_use_safe_radius || second.can_use_safe_radius; - out.missing_roof_layers = std::min(first.missing_roof_layers, second.missing_roof_layers); + // Preserve the deepest outstanding roof recovery request across merged sub-branches. + out.set_pending_roof_recovery( + std::max(first.missing_roof_layers, second.missing_roof_layers), + std::max(first.roof_recovery_dtt, second.roof_recovery_dtt)); out.skip_ovalisation = false; if (first.target_height > second.target_height) { out.target_height = first.target_height; @@ -3473,7 +3480,6 @@ static void generate_support_areas(Print &print, TreeSupport* tree_support, cons // value is the area where support may be placed. As this is calculated in CreateLayerPathing it is saved and reused in draw_areas std::vector move_bounds(num_support_layers); - // ### Place tips of the support tree for (size_t mesh_idx : processing.second) generate_initial_areas(*print.get_object(mesh_idx), volumes, config, overhangs, @@ -3591,7 +3597,33 @@ static void generate_support_areas(Print &print, TreeSupport* tree_support, cons // storage.support.generated = true; } -// Organic specific: Smooth branches and produce one cummulative mesh to be sliced. +static void recover_pending_branch_roofs( + InterfacePlacer &interface_placer, + const std::vector &branch_path, + const LayerIndex layer_begin, + std::vector &slices) +{ + if (! interface_placer.support_parameters.has_top_contacts) + return; + + for (auto it = branch_path.rbegin(); it != branch_path.rend(); ++ it) { + const SupportElement &el = **it; + if (! el.state.has_pending_roof_recovery()) + break; + + const LayerIndex slice_idx = el.state.layer_idx - layer_begin; + if (slice_idx < 0 || slice_idx >= LayerIndex(slices.size())) + continue; + if (slices[size_t(slice_idx)].empty()) + continue; + if (el.state.roof_recovery_dtt > interface_placer.support_parameters.num_top_interface_layers) + continue; + + interface_placer.add_roof(std::move(slices[size_t(slice_idx)]), el.state.layer_idx, el.state.roof_recovery_dtt); + } +} + +// Organic specific: Smooth branches and produce one cumulative mesh to be sliced. void organic_draw_branches( PrintObject &print_object, TreeModelVolumes &volumes, @@ -3770,13 +3802,12 @@ void organic_draw_branches( // ++ ielement; } } - const SlicingParameters &slicing_params = print_object.slicing_parameters(); MeshSlicingParams mesh_slicing_params; mesh_slicing_params.mode = MeshSlicingParams::SlicingMode::Positive; tbb::parallel_for(tbb::blocked_range(0, trees.size(), 1), - [&trees, &volumes, &config, &slicing_params, &move_bounds, &mesh_slicing_params, &throw_on_cancel](const tbb::blocked_range &range) { + [&trees, &volumes, &config, &slicing_params, &move_bounds, &mesh_slicing_params, &interface_placer, &throw_on_cancel](const tbb::blocked_range &range) { indexed_triangle_set partial_mesh; std::vector slice_z; std::vector bottom_contacts; @@ -3811,7 +3842,7 @@ void organic_draw_branches( num_empty = std::find_if(slices.begin(), slices.end(), [](auto &s) { return !s.empty(); }) - slices.begin(); } else { if (branch.has_root) { - if (branch.path.front()->state.to_model_gracious) { + if (config.support_rests_on_model && branch.path.front()->state.to_model_gracious) { if (config.settings.support_floor_layers > 0) //FIXME one may just take the whole tree slice as bottom interface. bottom_contacts.emplace_back(intersection_clipped(slices.front(), volumes.getPlaceableAreas(0, layer_begin, [] {}))); @@ -3860,7 +3891,7 @@ void organic_draw_branches( } } #endif - if (config.settings.support_floor_layers > 0) + if (config.support_rests_on_model && config.settings.support_floor_layers > 0) for (int i = int(bottom_extra_slices.size()) - 2; i >= 0; -- i) bottom_contacts.emplace_back( intersection_clipped(bottom_extra_slices[i].polygons, volumes.getPlaceableAreas(0, layer_begin - i - 1, [] {}))); @@ -3872,19 +3903,7 @@ void organic_draw_branches( } } -#if 0 - //FIXME branch.has_tip seems to not be reliable. - if (branch.has_tip && interface_placer.support_parameters.has_top_contacts) - // Add top slices to top contacts / interfaces / base interfaces. - for (int i = int(branch.path.size()) - 1; i >= 0; -- i) { - const SupportElement &el = *branch.path[i]; - if (el.state.missing_roof_layers == 0) - break; - //FIXME Move or not? - interface_placer.add_roof(std::move(slices[int(slices.size()) - i - 1]), el.state.layer_idx, - interface_placer.support_parameters.num_top_interface_layers + 1 - el.state.missing_roof_layers); - } -#endif + recover_pending_branch_roofs(interface_placer, branch.path, layer_begin, slices); } layer_begin += LayerIndex(num_empty); diff --git a/src/libslic3r/Support/TreeSupport3D.hpp b/src/libslic3r/Support/TreeSupport3D.hpp index 2d50f73ac5..a333edb962 100644 --- a/src/libslic3r/Support/TreeSupport3D.hpp +++ b/src/libslic3r/Support/TreeSupport3D.hpp @@ -199,9 +199,20 @@ struct SupportElementState : public SupportElementStateBits AreaIncreaseSettings last_area_increase; /*! - * \brief Amount of roof layers that were not yet added, because the branch needed to move. + * \brief Number of pending roof/contact recovery slices from this node downward, including this node. */ - uint32_t missing_roof_layers; + uint32_t missing_roof_layers = 0; + + /*! + * \brief Contact/interface depth that this node should recover when missing_roof_layers > 0. + */ + uint32_t roof_recovery_dtt = 0; + + void set_pending_roof_recovery(uint32_t pending_layers, uint32_t recovery_depth) + { + this->missing_roof_layers = pending_layers; + this->roof_recovery_dtt = pending_layers > 0 ? recovery_depth : 0; + } // called by increase_single_area() and increaseAreas() [[nodiscard]] static SupportElementState propagate_down(const SupportElementState &src) @@ -209,6 +220,10 @@ struct SupportElementState : public SupportElementStateBits SupportElementState dst{ src }; ++ dst.distance_to_top; -- dst.layer_idx; + if (dst.has_pending_roof_recovery()) { + -- dst.missing_roof_layers; + ++ dst.roof_recovery_dtt; + } // set to invalid as we are a new node on a new layer dst.result_on_layer_reset(); dst.skip_ovalisation = false; @@ -216,6 +231,7 @@ struct SupportElementState : public SupportElementStateBits } [[nodiscard]] bool locked() const { return this->distance_to_top < this->dont_move_until; } + [[nodiscard]] bool has_pending_roof_recovery() const { return this->missing_roof_layers > 0; } }; /*!