From da149ee75accd3e6d5efa8fcdeafc4c70860267b Mon Sep 17 00:00:00 2001 From: Ian Bassi Date: Wed, 8 Jul 2026 14:18:41 -0300 Subject: [PATCH 1/8] Add toolchange ordering option (Standard/Cyclic). (#13582) --- src/libslic3r/GCode/ToolOrdering.cpp | 77 ++++++++++++++++++++++------ src/libslic3r/Preset.cpp | 1 + src/libslic3r/Print.cpp | 1 + src/libslic3r/PrintConfig.cpp | 22 ++++++++ src/libslic3r/PrintConfig.hpp | 8 +++ src/slic3r/GUI/Tab.cpp | 1 + 6 files changed, 93 insertions(+), 17 deletions(-) diff --git a/src/libslic3r/GCode/ToolOrdering.cpp b/src/libslic3r/GCode/ToolOrdering.cpp index 5cb66ba5e5..ee03d6e959 100644 --- a/src/libslic3r/GCode/ToolOrdering.cpp +++ b/src/libslic3r/GCode/ToolOrdering.cpp @@ -187,6 +187,10 @@ static void apply_first_layer_order(const DynamicPrintConfig* config, std::vecto void ToolOrdering::handle_dontcare_extruder(const std::vector& tool_order_layer0) { + const PrintConfig* print_config = m_print_config_ptr; + if (!print_config && m_print_object_ptr) + print_config = &m_print_object_ptr->print()->config(); + if(m_layer_tools.empty() || tool_order_layer0.empty()) return; @@ -221,6 +225,8 @@ void ToolOrdering::handle_dontcare_extruder(const std::vector& too for (int i = 1; i < m_layer_tools.size(); i++) { LayerTools& lt = m_layer_tools[i]; + // Extruders in lt.extruders are already sorted. + if (lt.extruders.empty()) continue; if (lt.extruders.size() == 1 && lt.extruders.front() == 0) @@ -229,14 +235,23 @@ void ToolOrdering::handle_dontcare_extruder(const std::vector& too if (lt.extruders.front() == 0) // Pop the "don't care" extruder, the "don't care" region will be merged with the next one. lt.extruders.erase(lt.extruders.begin()); - // Reorder the extruders to start with the last one. - for (size_t i = 1; i < lt.extruders.size(); ++i) - if (lt.extruders[i] == last_extruder_id) { - // Move the last extruder to the front. - memmove(lt.extruders.data() + 1, lt.extruders.data(), i * sizeof(unsigned int)); - lt.extruders.front() = last_extruder_id; - break; + + if (print_config == nullptr + || print_config->toolchange_ordering == ToolChangeOrderingType::Default) + { + // Reorder the extruders to start with the last one. + for (size_t i = 1; i < lt.extruders.size(); ++i) { + if (lt.extruders[i] == last_extruder_id) { + // Move the last extruder to the front. + std::rotate( + lt.extruders.begin(), + lt.extruders.begin() + i, + lt.extruders.begin() + i + 1 + ); + break; + } } + } } last_extruder_id = lt.extruders.back(); } @@ -252,6 +267,10 @@ void ToolOrdering::handle_dontcare_extruder(const std::vector& too void ToolOrdering::handle_dontcare_extruder(unsigned int last_extruder_id) { + const PrintConfig* print_config = m_print_config_ptr; + if (!print_config && m_print_object_ptr) + print_config = &m_print_object_ptr->print()->config(); + if(m_layer_tools.empty()) return; if(last_extruder_id == (unsigned int)-1){ @@ -275,6 +294,8 @@ void ToolOrdering::handle_dontcare_extruder(unsigned int last_extruder_id) } for (LayerTools < : m_layer_tools) { + // Extruders in lt.extruders are already sorted. + if (lt.extruders.empty()) continue; if (lt.extruders.size() == 1 && lt.extruders.front() == 0) @@ -283,21 +304,30 @@ void ToolOrdering::handle_dontcare_extruder(unsigned int last_extruder_id) if (lt.extruders.front() == 0) // Pop the "don't care" extruder, the "don't care" region will be merged with the next one. lt.extruders.erase(lt.extruders.begin()); - // Reorder the extruders to start with the last one. - for (size_t i = 1; i < lt.extruders.size(); ++ i) - if (lt.extruders[i] == last_extruder_id) { - // Move the last extruder to the front. - memmove(lt.extruders.data() + 1, lt.extruders.data(), i * sizeof(unsigned int)); - lt.extruders.front() = last_extruder_id; - break; + + if (print_config == nullptr + || print_config->toolchange_ordering == ToolChangeOrderingType::Default) + { + // Reorder the extruders to start with the last one. + for (size_t i = 1; i < lt.extruders.size(); ++i) { + if (lt.extruders[i] == last_extruder_id) { + // Move the last extruder to the front. + std::rotate( + lt.extruders.begin(), + lt.extruders.begin() + i, + lt.extruders.begin() + i + 1 + ); + break; + } } + } if (lt == m_layer_tools[0]) { // On first layer with wipe tower, prefer a soluble extruder // at the beginning, so it is not wiped on the first layer. - if (m_print_config_ptr && m_print_config_ptr->enable_prime_tower) { + if (print_config && print_config->enable_prime_tower) { for (size_t i = 0; ifilament_soluble.get_at(lt.extruders[i]-1)) { // 1-based... + if (print_config->filament_soluble.get_at(lt.extruders[i]-1)) { // 1-based... std::swap(lt.extruders[i], lt.extruders.front()); break; } @@ -396,6 +426,7 @@ void ToolOrdering::sort_and_build_data(const PrintObject& object , unsigned int ToolOrdering::ToolOrdering(const PrintObject &object, unsigned int first_extruder, bool prime_multi_material) { m_print_full_config = &object.print()->full_print_config(); + m_print_config_ptr = &object.print()->config(); m_print_object_ptr = &object; m_print = const_cast(object.print()); if (object.layers().empty()) @@ -1329,8 +1360,11 @@ void ToolOrdering::reorder_extruders_for_minimum_flush_volume(bool reorder_first if (!m_layer_tools.empty()) first_layer_filaments = m_layer_tools[0].extruders; + const bool use_cyclic_ordering = + (print_config->toolchange_ordering == ToolChangeOrderingType::Cyclic); + // other_layers_seq: the layer_idx and extruder_idx are base on 1 - auto get_custom_seq = [&other_layers_seqs, &reorder_first_layer, &first_layer_filaments](int layer_idx, std::vector& out_seq) -> bool { + auto get_custom_seq = [&other_layers_seqs, &reorder_first_layer, &first_layer_filaments, &layer_filaments, use_cyclic_ordering](int layer_idx, std::vector& out_seq) -> bool { if (!reorder_first_layer && layer_idx == 0) { out_seq.resize(first_layer_filaments.size()); std::transform(first_layer_filaments.begin(), first_layer_filaments.end(), out_seq.begin(), [](auto item) {return item + 1; }); @@ -1343,6 +1377,15 @@ void ToolOrdering::reorder_extruders_for_minimum_flush_volume(bool reorder_first return true; } } + + if (use_cyclic_ordering && layer_idx >= 0 && size_t(layer_idx) < layer_filaments.size()) { + std::vector ordered = layer_filaments[size_t(layer_idx)]; + std::sort(ordered.begin(), ordered.end()); + out_seq.resize(ordered.size()); + std::transform(ordered.begin(), ordered.end(), out_seq.begin(), [](auto item) { return int(item) + 1; }); + return true; + } + return false; }; diff --git a/src/libslic3r/Preset.cpp b/src/libslic3r/Preset.cpp index 98a5a4fcb6..cf8469567f 100644 --- a/src/libslic3r/Preset.cpp +++ b/src/libslic3r/Preset.cpp @@ -1272,6 +1272,7 @@ static std::vector s_Preset_print_options{ "wipe_tower_bridging", "wipe_tower_extra_flow", "single_extruder_multi_material_priming", + "toolchange_ordering", "wipe_tower_rotation_angle", "tree_support_branch_distance_organic", "tree_support_branch_diameter_organic", diff --git a/src/libslic3r/Print.cpp b/src/libslic3r/Print.cpp index a676d24cc0..257bbd9c31 100644 --- a/src/libslic3r/Print.cpp +++ b/src/libslic3r/Print.cpp @@ -330,6 +330,7 @@ bool Print::invalidate_state_by_config_options(const ConfigOptionResolver & /* n || opt_key == "first_layer_print_sequence" || opt_key == "other_layers_print_sequence" || opt_key == "other_layers_print_sequence_nums" + || opt_key == "toolchange_ordering" || opt_key == "extruder_ams_count" || opt_key == "filament_map_mode" || opt_key == "filament_map" diff --git a/src/libslic3r/PrintConfig.cpp b/src/libslic3r/PrintConfig.cpp index dda24e4e5b..d8887b19e2 100644 --- a/src/libslic3r/PrintConfig.cpp +++ b/src/libslic3r/PrintConfig.cpp @@ -522,6 +522,12 @@ static t_config_enum_values s_keys_map_PerimeterGeneratorType{ }; CONFIG_OPTION_ENUM_DEFINE_STATIC_MAPS(PerimeterGeneratorType) +static t_config_enum_values s_keys_map_ToolChangeOrderingType { + { "default", int(ToolChangeOrderingType::Default) }, + { "cyclic", int(ToolChangeOrderingType::Cyclic) } +}; +CONFIG_OPTION_ENUM_DEFINE_STATIC_MAPS(ToolChangeOrderingType) + static const t_config_enum_values s_keys_map_ZHopType = { { "Auto Lift", zhtAuto }, { "Normal Lift", zhtNormal }, @@ -6076,6 +6082,22 @@ void PrintConfigDef::init_fff_params() def->mode = comAdvanced; def->set_default_value(new ConfigOptionBool(false)); + def = this->add("toolchange_ordering", coEnum); + def->label = L("Toolchange ordering"); + def->category = L("Advanced"); + def->tooltip = L( + "Determines the order of tool changes on each layer.\n" + "- Default: Starts with the last used extruder to minimize tool changes.\n" + "- Cyclic: Uses a fixed tool sequence each layer. This sacrifices speed for better surface quality, as the extra toolchanges allow layers more time to cool." + ); + def->mode = comAdvanced; + def->enum_keys_map = &ConfigOptionEnum::get_enum_values(); + def->enum_values.emplace_back("default"); + def->enum_values.emplace_back("cyclic"); + def->enum_labels.emplace_back(L("Default")); + def->enum_labels.emplace_back(L("Cyclic")); + def->set_default_value(new ConfigOptionEnum(ToolChangeOrderingType::Default)); + def = this->add("slice_closing_radius", coFloat); def->label = L("Slice gap closing radius"); def->category = L("Quality"); diff --git a/src/libslic3r/PrintConfig.hpp b/src/libslic3r/PrintConfig.hpp index 3b8b809c78..fbeb9f0574 100644 --- a/src/libslic3r/PrintConfig.hpp +++ b/src/libslic3r/PrintConfig.hpp @@ -300,6 +300,12 @@ enum class PerimeterGeneratorType Arachne }; +enum class ToolChangeOrderingType +{ + Default, + Cyclic, +}; + // BBS enum OverhangFanThreshold { Overhang_threshold_none = 0, @@ -561,6 +567,7 @@ CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(PrintHostType) CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(AuthorizationType) CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(WipeTowerWallType) CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(PerimeterGeneratorType) +CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(ToolChangeOrderingType) CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(PowerLossRecoveryMode) #undef CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS @@ -1413,6 +1420,7 @@ PRINT_CONFIG_CLASS_DEFINE( ((ConfigOptionBool, single_extruder_multi_material)) ((ConfigOptionBool, manual_filament_change)) ((ConfigOptionBool, single_extruder_multi_material_priming)) + ((ConfigOptionEnum, toolchange_ordering)) ((ConfigOptionBool, wipe_tower_no_sparse_layers)) ((ConfigOptionString, change_filament_gcode)) ((ConfigOptionString, change_extrusion_role_gcode)) diff --git a/src/slic3r/GUI/Tab.cpp b/src/slic3r/GUI/Tab.cpp index f1eb41b0b4..b3fc51c245 100644 --- a/src/slic3r/GUI/Tab.cpp +++ b/src/slic3r/GUI/Tab.cpp @@ -2976,6 +2976,7 @@ void TabPrint::build() optgroup->append_single_option_line("flush_into_support", "multimaterial_settings_flush_options#flush-into-objects-support"); optgroup = page->new_optgroup(L("Advanced"), L"advanced"); optgroup->append_single_option_line("interlocking_beam", "multimaterial_settings_advanced#interlocking-beam"); + optgroup->append_single_option_line("toolchange_ordering", "multimaterial_settings_advanced#toolchange-ordering"); optgroup->append_single_option_line("interface_shells", "multimaterial_settings_advanced#interface-shells"); optgroup->append_single_option_line("mmu_segmented_region_max_width", "multimaterial_settings_advanced#maximum-width-of-segmented-region"); optgroup->append_single_option_line("mmu_segmented_region_interlocking_depth", "multimaterial_settings_advanced#interlocking-depth-of-segmented-region"); From 20a66fa99bb77136a4d0930708a94dba3d81e412 Mon Sep 17 00:00:00 2001 From: Ian Bassi Date: Wed, 8 Jul 2026 14:20:40 -0300 Subject: [PATCH 2/8] Top Surface Expansion (#14296) Co-authored-by: Rodrigo Faselli <162915171+RF47@users.noreply.github.com> --- src/libslic3r/Clipper2Utils.cpp | 12 +++++ src/libslic3r/Clipper2Utils.hpp | 1 + src/libslic3r/PerimeterGenerator.cpp | 16 +++++++ src/libslic3r/Preset.cpp | 3 ++ src/libslic3r/PrintConfig.cpp | 48 +++++++++++++++++++ src/libslic3r/PrintConfig.hpp | 11 +++++ src/libslic3r/PrintObject.cpp | 67 +++++++++++++++++++++++++++ src/slic3r/GUI/ConfigManipulation.cpp | 7 +++ src/slic3r/GUI/GUI_Factories.cpp | 5 +- src/slic3r/GUI/Tab.cpp | 4 ++ 10 files changed, 173 insertions(+), 1 deletion(-) diff --git a/src/libslic3r/Clipper2Utils.cpp b/src/libslic3r/Clipper2Utils.cpp index 12fd867500..d389d25209 100644 --- a/src/libslic3r/Clipper2Utils.cpp +++ b/src/libslic3r/Clipper2Utils.cpp @@ -188,6 +188,18 @@ ExPolygons offset_ex_2(const ExPolygons &expolygons, double delta) 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 diff --git a/src/libslic3r/Clipper2Utils.hpp b/src/libslic3r/Clipper2Utils.hpp index 54b48d6bd7..2b4b0fb2ef 100644 --- a/src/libslic3r/Clipper2Utils.hpp +++ b/src/libslic3r/Clipper2Utils.hpp @@ -15,6 +15,7 @@ Slic3r::Polylines diff_pl_2(const Slic3r::Polylines& subject, const Slic3r::Pol 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); } diff --git a/src/libslic3r/PerimeterGenerator.cpp b/src/libslic3r/PerimeterGenerator.cpp index 6c29367c01..1c785c3184 100644 --- a/src/libslic3r/PerimeterGenerator.cpp +++ b/src/libslic3r/PerimeterGenerator.cpp @@ -571,6 +571,19 @@ static ExtrusionEntityCollection traverse_extrusions(const PerimeterGenerator& p return extrusion_coll; } +// ORCA: only_one_wall_top detects the top as "slice − upper", so a feature rising from the middle of a +// top surface becomes an enclosed hole that gets ringed with extra inner walls. Fill those holes back +// into the top. Only holes that are both covered by the upper layer (excludes bridges) and backed by +// solid material (excludes voids) are filled. +static ExPolygons fill_enclosed_top_feature_holes(const ExPolygons &top, const Polygons &covered_by_upper, const ExPolygons &solid) +{ + ExPolygons filled = top; + for (ExPolygon &ex : filled) + ex.holes.clear(); + const ExPolygons feature_holes = intersection_ex(intersection_ex(diff_ex(filled, top), covered_by_upper), solid); + return feature_holes.empty() ? top : union_ex(top, feature_holes); +} + void PerimeterGenerator::split_top_surfaces(const ExPolygons &orig_polygons, ExPolygons &top_fills, ExPolygons &non_top_polygons, ExPolygons &fill_clip) const { // other perimeters @@ -636,6 +649,8 @@ void PerimeterGenerator::split_top_surfaces(const ExPolygons &orig_polygons, ExP ExPolygons delete_bridge = diff_ex(orig_polygons, bridge_checker, ApplySafetyOffset::Yes); ExPolygons top_polygons = diff_ex(delete_bridge, upper_polygons_series_clipped, ApplySafetyOffset::Yes); + top_polygons = fill_enclosed_top_feature_holes(top_polygons, upper_polygons_series_clipped, orig_polygons); + // get the not-top surface, from the "real top" but enlarged by external_infill_margin (and the // min_width_top_surface we removed a bit before) ExPolygons temp_gap = diff_ex(top_polygons, fill_clip); @@ -2194,6 +2209,7 @@ void PerimeterGenerator::process_arachne() upper_slices_clipped = ClipperUtils::clip_clipper_polygons_with_subject_bbox(*upper_slices, infill_contour_bbox); top_expolygons = diff_ex(infill_contour, upper_slices_clipped); + top_expolygons = fill_enclosed_top_feature_holes(top_expolygons, upper_slices_clipped, infill_contour); if (!top_expolygons.empty()) { if (lower_slices != nullptr) { diff --git a/src/libslic3r/Preset.cpp b/src/libslic3r/Preset.cpp index cf8469567f..e045b8c3c3 100644 --- a/src/libslic3r/Preset.cpp +++ b/src/libslic3r/Preset.cpp @@ -1044,6 +1044,9 @@ static std::vector s_Preset_print_options{ "lightning_prune_angle", "lightning_straightening_angle", "top_surface_pattern", + "top_surface_expansion", + "top_surface_expansion_margin", + "top_surface_expansion_direction", "bottom_surface_pattern", "infill_direction", "solid_infill_direction", diff --git a/src/libslic3r/PrintConfig.cpp b/src/libslic3r/PrintConfig.cpp index d8887b19e2..e626d3e37f 100644 --- a/src/libslic3r/PrintConfig.cpp +++ b/src/libslic3r/PrintConfig.cpp @@ -221,6 +221,13 @@ static t_config_enum_values s_keys_map_FuzzySkinMode { }; CONFIG_OPTION_ENUM_DEFINE_STATIC_MAPS(FuzzySkinMode) +static t_config_enum_values s_keys_map_TopSurfaceExpansionDirection { + { "inward_and_outward", int(TopSurfaceExpansionDirection::InwardAndOutward) }, + { "inward", int(TopSurfaceExpansionDirection::Inward) }, + { "outward", int(TopSurfaceExpansionDirection::Outward) } +}; +CONFIG_OPTION_ENUM_DEFINE_STATIC_MAPS(TopSurfaceExpansionDirection) + static t_config_enum_values s_keys_map_InfillPattern { { "monotonic", ipMonotonic }, { "monotonicline", ipMonotonicLine }, @@ -2123,6 +2130,47 @@ void PrintConfigDef::init_fff_params() def->max = 100; def->set_default_value(new ConfigOptionPercent(100)); + def = this->add("top_surface_expansion", coFloat); + def->label = L("Top surface expansion"); + def->category = L("Strength"); + def->tooltip = L("Expands the top surfaces by this distance to connect distinct top surfaces and fill gaps.\n" + "Useful for cases where the top surface is interrupted by a raised feature, such as text on a plane." + "Expanding it removes the holes beneath these features and creates a continuous path with a better finish for printing on top." + "The expansion is applied to the original top surface, before any other processing such as bridging or overhang detection."); + def->sidetext = L("mm"); + def->min = 0; + def->mode = comAdvanced; + def->set_default_value(new ConfigOptionFloat(0)); + + def = this->add("top_surface_expansion_margin", coFloat); + def->label = L("Top expansion wall margin"); + def->category = L("Strength"); + def->tooltip = L("Using “Top surface expansion” may cause a surface that did not previously touch the model's outer walls to now do so.\n" + "This can cause contraction marks (such as the hull line) on the outer walls.\n" + "By adding a small margin, this contraction will not occur directly on the walls, thereby preventing a visible mark."); + def->sidetext = L("mm"); + def->min = 0; + def->max = 10; + def->mode = comAdvanced; + def->set_default_value(new ConfigOptionFloat(0)); + + def = this->add("top_surface_expansion_direction", coEnum); + def->label = L("Top expansion direction"); + def->category = L("Strength"); + def->tooltip = L("Direction in which the top surface expansion grows.\n" + " - Inward grows into the holes and gaps left by features rising from the middle of a top surface.\n" + " - Outward grows the outer edge of the surface, connecting surfaces separated by features that can divide a surface, such as a lattice pattern.\n" + " - Inward and Outward does both."); + def->enum_keys_map = &ConfigOptionEnum::get_enum_values(); + def->enum_values.push_back("inward_and_outward"); + def->enum_values.push_back("inward"); + def->enum_values.push_back("outward"); + def->enum_labels.push_back(L("Inward and Outward")); + def->enum_labels.push_back(L("Inward")); + def->enum_labels.push_back(L("Outward")); + def->mode = comAdvanced; + def->set_default_value(new ConfigOptionEnum(TopSurfaceExpansionDirection::InwardAndOutward)); + def = this->add("bottom_surface_pattern", coEnum); def->label = L("Bottom surface pattern"); def->category = L("Strength"); diff --git a/src/libslic3r/PrintConfig.hpp b/src/libslic3r/PrintConfig.hpp index fbeb9f0574..31e3c2cc45 100644 --- a/src/libslic3r/PrintConfig.hpp +++ b/src/libslic3r/PrintConfig.hpp @@ -62,6 +62,13 @@ enum class FuzzySkinMode { Combined, }; +// ORCA: direction in which top_surface_expansion grows the top surfaces. +enum class TopSurfaceExpansionDirection { + InwardAndOutward, + Inward, + Outward, +}; + enum class NoiseType { Classic, Perlin, @@ -540,6 +547,7 @@ CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(PrinterTechnology) CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(GCodeFlavor) CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(FuzzySkinType) CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(FuzzySkinMode) +CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(TopSurfaceExpansionDirection) CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(WipeTowerType) CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(NoiseType) CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(InfillPattern) @@ -1194,6 +1202,9 @@ PRINT_CONFIG_CLASS_DEFINE( ((ConfigOptionFloatOrPercent, top_surface_line_width)) ((ConfigOptionInt, top_shell_layers)) ((ConfigOptionFloat, top_shell_thickness)) + ((ConfigOptionFloat, top_surface_expansion)) + ((ConfigOptionFloat, top_surface_expansion_margin)) + ((ConfigOptionEnum, top_surface_expansion_direction)) ((ConfigOptionFloatsNullable, top_surface_speed)) //BBS ((ConfigOptionBoolsNullable, enable_overhang_speed)) diff --git a/src/libslic3r/PrintObject.cpp b/src/libslic3r/PrintObject.cpp index 13d7297f93..10cc92013b 100644 --- a/src/libslic3r/PrintObject.cpp +++ b/src/libslic3r/PrintObject.cpp @@ -4,6 +4,7 @@ #include "Print.hpp" #include "BoundingBox.hpp" #include "ClipperUtils.hpp" +#include "Clipper2Utils.hpp" #include "ElephantFootCompensation.hpp" #include "Geometry.hpp" #include "I18N.hpp" @@ -1297,6 +1298,9 @@ bool PrintObject::invalidate_state_by_config_options( || opt_key == "infill_combination_max_layer_height" || opt_key == "bottom_shell_thickness" || opt_key == "top_shell_thickness" + || opt_key == "top_surface_expansion" + || opt_key == "top_surface_expansion_margin" + || opt_key == "top_surface_expansion_direction" || opt_key == "minimum_sparse_infill_area" || opt_key == "sparse_infill_filament_id" || opt_key == "internal_solid_filament_id" @@ -1686,6 +1690,69 @@ void PrintObject::detect_surfaces_type() } } + // ORCA: Expand the top surfaces outward by top_surface_expansion in every direction. This + // enlarges the top solid infill and, in particular, grows it over the covered material left + // by features rising from the middle of a top surface (filling holes and joining tops so the + // features rest on it). The expansion stays inside the section it belongs to: each connected + // solid island has its own outer wall, so the top is grown within each island separately and + // clipped to it - growing one island's top across the gap into another island (which may have + // no top surface, leaving a partially filled layer) is never allowed. The top infill sits + // inside the perimeters, so the margin is measured from the walls: the island is inset by the + // band the walls consume (outer wall + inner walls) plus the configured margin, making that + // value the real clearance between the expanded top and the walls (avoiding a hull line). The + // original top is unioned back in, so where it already sits within that band it is kept as-is. + // Never claims a bottom surface. + const double top_expansion = layerm->region().config().top_surface_expansion.value; + if (top_expansion > 0. && ! top.empty()) { + const double d = scale_(top_expansion); + const auto jt = Clipper2Lib::JoinType::Miter; + const ExPolygons T = union_ex(to_expolygons(top)); + const int wall_loops = layerm->region().config().wall_loops.value; + const double wall_band = wall_loops <= 0 ? 0. : + double(layerm->flow(frExternalPerimeter).scaled_width()) + + double(layerm->flow(frPerimeter).scaled_width()) * double(wall_loops - 1); + const double margin = scale_(layerm->region().config().top_surface_expansion_margin.value); + // minimum real top to act on: ignore anything thinner than ~2 top-infill lines + const float min_top = float(layerm->flow(frTopSolidInfill).scaled_width()); + const auto direction = layerm->region().config().top_surface_expansion_direction.value; + + ExPolygons grown; + for (const ExPolygon &island : union_ex(layerm_slices_surfaces)) { + // The top infill only exists inside the perimeters, so seed and measure from the infill + // region (the island minus the wall band), not the raw slice. A section whose only + // exposed top lies in the wall band - i.e. a layer where the top is just the walls + // themselves - has no infill here and is skipped, instead of being flooded inward by + // the expansion. Thin slivers inside the infill region are dropped by the opening too. + const ExPolygons infill_region = wall_band > 0. ? offset_ex(island, -float(wall_band)) : ExPolygons{ island }; + const ExPolygons island_top = intersection_ex(T, infill_region); + if (opening_ex(island_top, min_top).empty()) + continue; // no real top infill in this section - never expand into it + + // grow by d, then keep only the part allowed by the configured direction: inward fills + // the holes/gaps left by features (clip the growth back to the top's own filled outline, + // which leaves the outer edge fixed), outward grows the outer edge toward the walls (drop + // the growth that fell into the original holes), and inward+outward keeps both. + ExPolygons expanded = offset_ex_2(island_top, d, jt); + if (direction != TopSurfaceExpansionDirection::InwardAndOutward) { + ExPolygons outline; // the top with its holes filled (same outer edge) + outline.reserve(island_top.size()); + for (const ExPolygon &ex : island_top) + outline.emplace_back(ex.contour); + outline = union_ex(outline); + expanded = direction == TopSurfaceExpansionDirection::Inward ? + intersection_ex(expanded, outline) : // only growth into the holes + diff_ex(expanded, diff_ex(outline, island_top)); // only growth past the outer edge + } + // hold the expansion clear of the walls by the configured margin + const ExPolygons allowed = margin > 0. ? offset_ex(infill_region, -float(margin)) : infill_region; + append(grown, intersection_ex(expanded, allowed)); + } + + ExPolygons new_top = diff_ex(union_ex(T, grown), to_expolygons(bottom)); + top.clear(); + surfaces_append(top, std::move(new_top), stTop); + } + #ifdef SLIC3R_DEBUG_SLICE_PROCESSING { static int iRun = 0; diff --git a/src/slic3r/GUI/ConfigManipulation.cpp b/src/slic3r/GUI/ConfigManipulation.cpp index 46758e3e64..a41473bc3d 100644 --- a/src/slic3r/GUI/ConfigManipulation.cpp +++ b/src/slic3r/GUI/ConfigManipulation.cpp @@ -713,6 +713,13 @@ void ConfigManipulation::toggle_print_fff_options(DynamicPrintConfig *config, in toggle_field("top_layer_direction", has_top_shell); toggle_field("bottom_layer_direction", has_bottom_shell); + toggle_line("top_surface_expansion", has_top_shell); + toggle_line("top_surface_expansion_margin", has_top_shell); + bool has_top_surface_expansion = config->opt_float("top_surface_expansion") > 0; + toggle_field("top_surface_expansion_margin", has_top_surface_expansion); + toggle_line("top_surface_expansion_direction", has_top_shell); + toggle_field("top_surface_expansion_direction", has_top_surface_expansion); + for (auto el : { "infill_direction", "sparse_infill_line_width", "gap_fill_target","filter_out_gap_fill","infill_wall_overlap", "bridge_angle", "internal_bridge_angle", "relative_bridge_angle", "solid_infill_direction", "solid_infill_rotate_template", "internal_solid_infill_pattern", "internal_solid_filament_id", "top_surface_filament_id", "bottom_surface_filament_id", diff --git a/src/slic3r/GUI/GUI_Factories.cpp b/src/slic3r/GUI/GUI_Factories.cpp index ccb6dedd29..75dc44add6 100644 --- a/src/slic3r/GUI/GUI_Factories.cpp +++ b/src/slic3r/GUI/GUI_Factories.cpp @@ -132,6 +132,9 @@ std::map> SettingsFactory::PART_CATE {"infill_anchor", "", 1}, {"infill_anchor_max", "", 1}, {"top_surface_pattern", "", 1}, + {"top_surface_expansion", "", 1}, + {"top_surface_expansion_margin", "", 1}, + {"top_surface_expansion_direction", "", 1}, {"bottom_surface_pattern", "", 1}, {"internal_solid_infill_pattern", "", 1}, {"align_infill_direction_to_model", "", 1}, @@ -187,7 +190,7 @@ std::vector SettingsFactory::get_visible_options(const std::s //Quality "wall_infill_order", "ironing_type", "inner_wall_line_width", "outer_wall_line_width", "top_surface_line_width", //Shell - "wall_loops", "top_shell_layers", "bottom_shell_layers", "top_shell_thickness", "bottom_shell_thickness", + "wall_loops", "top_shell_layers", "bottom_shell_layers", "top_shell_thickness", "bottom_shell_thickness", "top_surface_expansion", "top_surface_expansion_margin", "top_surface_expansion_direction", //Infill "sparse_infill_density", "sparse_infill_pattern", "top_surface_pattern", "bottom_surface_pattern", "infill_combination", "infill_direction", "infill_wall_overlap", //speed diff --git a/src/slic3r/GUI/Tab.cpp b/src/slic3r/GUI/Tab.cpp index b3fc51c245..0e0adbbdda 100644 --- a/src/slic3r/GUI/Tab.cpp +++ b/src/slic3r/GUI/Tab.cpp @@ -2743,6 +2743,10 @@ void TabPrint::build() optgroup->append_single_option_line("top_surface_density", "strength_settings_top_bottom_shells#surface-density"); optgroup->append_single_option_line("top_surface_pattern", "strength_settings_top_bottom_shells#surface-pattern"); optgroup->append_single_option_line("top_layer_direction", "strength_settings_infill#top-direction"); + optgroup->append_single_option_line("top_surface_hole_contraction", "strength_settings_top_bottom_shells#surface-hole-contraction"); + optgroup->append_single_option_line("top_surface_expansion", "strength_settings_top_bottom_shells#surface-expansion"); + optgroup->append_single_option_line("top_surface_expansion_margin", "strength_settings_top_bottom_shells#surface-expansion-margin"); + optgroup->append_single_option_line("top_surface_expansion_direction", "strength_settings_top_bottom_shells#surface-expansion-direction"); optgroup->append_single_option_line("bottom_shell_layers", "strength_settings_top_bottom_shells#shell-layers"); optgroup->append_single_option_line("bottom_shell_thickness", "strength_settings_top_bottom_shells#shell-thickness"); optgroup->append_single_option_line("bottom_surface_density", "strength_settings_top_bottom_shells#surface-density"); From 378843a4daa0e5a21afd6bc836c0b491e461ce5d Mon Sep 17 00:00:00 2001 From: Ian Bassi Date: Wed, 8 Jul 2026 15:15:50 -0300 Subject: [PATCH 3/8] Remove unused variable (#14670) --- src/slic3r/GUI/Tab.cpp | 1 - 1 file changed, 1 deletion(-) diff --git a/src/slic3r/GUI/Tab.cpp b/src/slic3r/GUI/Tab.cpp index 0e0adbbdda..14ad4bc596 100644 --- a/src/slic3r/GUI/Tab.cpp +++ b/src/slic3r/GUI/Tab.cpp @@ -2743,7 +2743,6 @@ void TabPrint::build() optgroup->append_single_option_line("top_surface_density", "strength_settings_top_bottom_shells#surface-density"); optgroup->append_single_option_line("top_surface_pattern", "strength_settings_top_bottom_shells#surface-pattern"); optgroup->append_single_option_line("top_layer_direction", "strength_settings_infill#top-direction"); - optgroup->append_single_option_line("top_surface_hole_contraction", "strength_settings_top_bottom_shells#surface-hole-contraction"); optgroup->append_single_option_line("top_surface_expansion", "strength_settings_top_bottom_shells#surface-expansion"); optgroup->append_single_option_line("top_surface_expansion_margin", "strength_settings_top_bottom_shells#surface-expansion-margin"); optgroup->append_single_option_line("top_surface_expansion_direction", "strength_settings_top_bottom_shells#surface-expansion-direction"); From bc6ffcfb31ec226d14e987d4352cfb62b36b862b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=CF=80=C2=B2?= Date: Wed, 8 Jul 2026 21:40:19 +0300 Subject: [PATCH 4/8] Anisotropic surfaces + Separated Infills (remake) (#11682) Co-authored-by: Rodrigo Faselli <162915171+RF47@users.noreply.github.com> Co-authored-by: Ian Bassi --- src/libslic3r/Fill/Fill.cpp | 92 ++++++++++++++++++++++++++- src/libslic3r/Fill/FillBase.cpp | 5 +- src/libslic3r/Fill/FillBase.hpp | 2 + src/libslic3r/Fill/FillPlanePath.cpp | 90 +++++++++++++++----------- src/libslic3r/Model.cpp | 17 +++++ src/libslic3r/Model.hpp | 34 +++++++--- src/libslic3r/Preset.cpp | 3 + src/libslic3r/Print.cpp | 1 + src/libslic3r/PrintConfig.cpp | 48 ++++++++++++++ src/libslic3r/PrintConfig.hpp | 9 +++ src/libslic3r/PrintObject.cpp | 8 +++ src/slic3r/GUI/ConfigManipulation.cpp | 23 +++++++ src/slic3r/GUI/Plater.cpp | 3 + src/slic3r/GUI/Tab.cpp | 3 + 14 files changed, 288 insertions(+), 50 deletions(-) diff --git a/src/libslic3r/Fill/Fill.cpp b/src/libslic3r/Fill/Fill.cpp index bdb21124d4..68180cd2f7 100644 --- a/src/libslic3r/Fill/Fill.cpp +++ b/src/libslic3r/Fill/Fill.cpp @@ -272,6 +272,10 @@ struct SurfaceFillParams // For Gyroid: when true, use the parameterized "optimized" wave. bool gyroid_optimized = false; + bool anisotropic_surfaces{false}; + CenterOfSurfacePattern center_of_surface_pattern{CenterOfSurfacePattern::Each_Surface}; + bool separated_infills{false}; + bool operator<(const SurfaceFillParams &rhs) const { #define RETURN_COMPARE_NON_EQUAL(KEY) if (this->KEY < rhs.KEY) return true; if (this->KEY > rhs.KEY) return false; #define RETURN_COMPARE_NON_EQUAL_TYPED(TYPE, KEY) if (TYPE(this->KEY) < TYPE(rhs.KEY)) return true; if (TYPE(this->KEY) > TYPE(rhs.KEY)) return false; @@ -301,8 +305,12 @@ struct SurfaceFillParams RETURN_COMPARE_NON_EQUAL(lateral_lattice_angle_2); RETURN_COMPARE_NON_EQUAL(symmetric_infill_y_axis); RETURN_COMPARE_NON_EQUAL(infill_lock_depth); - RETURN_COMPARE_NON_EQUAL(skin_infill_depth); RETURN_COMPARE_NON_EQUAL(infill_overhang_angle); + RETURN_COMPARE_NON_EQUAL(skin_infill_depth); + RETURN_COMPARE_NON_EQUAL(infill_overhang_angle); RETURN_COMPARE_NON_EQUAL(gyroid_optimized); + RETURN_COMPARE_NON_EQUAL(anisotropic_surfaces); + RETURN_COMPARE_NON_EQUAL(center_of_surface_pattern); + RETURN_COMPARE_NON_EQUAL(separated_infills); return false; } @@ -329,6 +337,9 @@ struct SurfaceFillParams this->infill_lock_depth == rhs.infill_lock_depth && this->skin_infill_depth == rhs.skin_infill_depth && this->infill_overhang_angle == rhs.infill_overhang_angle && + this->anisotropic_surfaces == rhs.anisotropic_surfaces && + this->center_of_surface_pattern == rhs.center_of_surface_pattern && + this->separated_infills == rhs.separated_infills && this->gyroid_optimized == rhs.gyroid_optimized; } }; @@ -868,6 +879,9 @@ std::vector group_fills(const Layer &layer, LockRegionParam &lock_p params.lateral_lattice_angle_1 = region_config.lateral_lattice_angle_1; params.lateral_lattice_angle_2 = region_config.lateral_lattice_angle_2; params.infill_overhang_angle = region_config.infill_overhang_angle; + params.anisotropic_surfaces = region_config.anisotropic_surfaces; + params.center_of_surface_pattern = region_config.center_of_surface_pattern; + params.separated_infills = region_config.separated_infills; if (params.pattern == ipLockedZag) { params.infill_lock_depth = scale_(region_config.infill_lock_depth); params.skin_infill_depth = scale_(region_config.skin_infill_depth); @@ -1309,6 +1323,22 @@ void Layer::make_fills(FillAdaptive::Octree* adaptive_fill_octree, FillAdaptive: params.config = ®ion_config; params.pattern = surface_fill.params.pattern; + // Orca: Checking the filling of a centered surface by drawing for each model parts + bool is_top_or_bottom = params.extrusion_role == erTopSolidInfill || params.extrusion_role == erBottomSurface; + bool is_centered_infill = surface_fill.params.pattern == ipArchimedeanChords || surface_fill.params.pattern == ipOctagramSpiral; + if (is_top_or_bottom) { + params.is_anisotropic = surface_fill.params.anisotropic_surfaces; // Orca: anisotropic surfaces + params.center_of_surface_pattern = surface_fill.params.center_of_surface_pattern; // Orca: center of surface pattern + } + // Orca: Each_Model centers the pattern on each model part's bbox; Each_Surface / Each_Assembly + // fall through to the default (whole-object) bounding box below. + bool is_per_model_center = is_top_or_bottom && params.center_of_surface_pattern == CenterOfSurfacePattern::Each_Model && is_centered_infill; + bool is_separate_infill = !is_top_or_bottom && surface_fill.params.separated_infills && + ( + is_centered_infill || + params.config->solid_infill_rotate_template != "" || + params.config->sparse_infill_rotate_template != "" ); + if( surface_fill.params.pattern == ipLockedZag ) { params.locked_zag = true; params.infill_lock_depth = surface_fill.params.infill_lock_depth; @@ -1332,7 +1362,65 @@ void Layer::make_fills(FillAdaptive::Octree* adaptive_fill_octree, FillAdaptive: params.can_reverse = false; for (ExPolygon& expoly : surface_fill.expolygons) { - f->no_overlap_expolygons = intersection_ex(surface_fill.no_overlap_expolygons, ExPolygons() = {expoly}, ApplySafetyOffset::Yes); + // Orca: separate infill / per-model pattern centering. + // + // First assign this fill region to the model part whose slice at this layer overlaps it + // the most. A strict "contains" test is ambiguous for assemblies whose parts overlap (a + // region may sit inside several parts, or straddle a boundary and be inside none), so we + // pick by intersection area instead. + // + // The center must belong to an *overlap group*, not a single part: parts that + // touch/overlap form one connected physical body that shares a single center, while a + // part detached from the rest of the assembly gets its own. This holds for both + // separated infills and Each_Model surface centering (Each_Model == per connected body). + // firstLayerObjGroups() already holds these connected components, so we widen the chosen + // part's bbox to the whole group it belongs to. + if (is_per_model_center || is_separate_infill) { + double best_overlap = 0.; + ObjectID best_vol_id; + const PrintInstance* best_instance = nullptr; + for (const auto& instance : this->object()->instances()) { + for (const auto& volume : instance.print_object->firstLayerObjSlice()) { + if (f->layer_id >= volume.slices.size()) + continue; + const double overlap = area(intersection_ex(volume.slices[f->layer_id], ExPolygons{expoly})); + if (overlap > best_overlap) { + best_overlap = overlap; + best_vol_id = volume.volume_id; + best_instance = &instance; + } + } + } + if (best_instance) { + const Transform3d matrix = best_instance->model_instance->get_matrix(); + Point shift = best_instance->shift; // get_volume_bbox takes a non-const ref + auto& volumes = best_instance->model_instance->get_object()->volumes; + + // Volume ids to center on: the whole overlap group the winning part belongs to, + // falling back to just that part if it isn't part of any group. + std::vector center_ids; + for (const auto& group : best_instance->print_object->firstLayerObjGroups()) { + bool in_group = false; + for (const ObjectID& vid : group.volume_ids) + if (vid == best_vol_id) { in_group = true; break; } + if (in_group) { center_ids = group.volume_ids; break; } + } + if (center_ids.empty()) + center_ids.push_back(best_vol_id); + + BoundingBox bbox; + for (const ObjectID& vid : center_ids) + for (auto model_volume : volumes) + if (vid.id == model_volume->id().id) { + bbox.merge(model_volume->get_volume_bbox(matrix, shift, true)); + break; + } + if (bbox.defined) + f->set_bounding_box(bbox); + } + } // - End: separate infill / per-model pattern centering + + f->no_overlap_expolygons = intersection_ex(surface_fill.no_overlap_expolygons, ExPolygons() = {expoly}, ApplySafetyOffset::Yes); if (params.symmetric_infill_y_axis) { params.symmetric_y_axis = f->extended_object_bounding_box().center().x(); expoly.symmetric_y(params.symmetric_y_axis); diff --git a/src/libslic3r/Fill/FillBase.cpp b/src/libslic3r/Fill/FillBase.cpp index 1d15614ccb..cf74606dd4 100644 --- a/src/libslic3r/Fill/FillBase.cpp +++ b/src/libslic3r/Fill/FillBase.cpp @@ -165,7 +165,7 @@ void Fill::fill_surface_extrusion(const Surface* surface, const FillParams& para // ORCA: special flag for flow rate calibration auto is_flow_calib = params.extrusion_role == erTopSolidInfill && this->print_object_config->has("calib_flowrate_topinfill_special_order") && this->print_object_config->option("calib_flowrate_topinfill_special_order")->getBool(); - if (is_flow_calib) { + if (is_flow_calib || params.is_anisotropic) { // Orca: disable sorting while anisotropic surfaces eec->no_sort = true; } size_t idx = eec->entities.size(); @@ -186,7 +186,8 @@ void Fill::fill_surface_extrusion(const Surface* surface, const FillParams& para } // Orca: run gap fill - this->_create_gap_fill(surface, params, eec); + if (!(params.is_anisotropic)) // Orca: Disable gap filling while anisotropic + this->_create_gap_fill(surface, params, eec); } } diff --git a/src/libslic3r/Fill/FillBase.hpp b/src/libslic3r/Fill/FillBase.hpp index ef6a6bc804..ffeed3045b 100644 --- a/src/libslic3r/Fill/FillBase.hpp +++ b/src/libslic3r/Fill/FillBase.hpp @@ -106,6 +106,8 @@ struct FillParams bool locked_zag{false}; float infill_lock_depth{0.0}; float skin_infill_depth{0.0}; + bool is_anisotropic{false}; + CenterOfSurfacePattern center_of_surface_pattern{CenterOfSurfacePattern::Each_Surface}; }; static_assert(IsTriviallyCopyable::value, "FillParams class is not POD (and it should be - see constructor)."); diff --git a/src/libslic3r/Fill/FillPlanePath.cpp b/src/libslic3r/Fill/FillPlanePath.cpp index b4681d05f9..01f4ca00da 100644 --- a/src/libslic3r/Fill/FillPlanePath.cpp +++ b/src/libslic3r/Fill/FillPlanePath.cpp @@ -77,20 +77,24 @@ void FillPlanePath::_fill_surface_single( //FIXME Vojtech: We are not sure whether the user expects the fill patterns on visible surfaces to be aligned across all the islands of a single layer. // One may align for this->centered() to align the patterns for Archimedean Chords and Octagram Spiral patterns. - const bool align = params.density < 0.995; - + // Orca: the old implementation became obsolete when it became possible to change the density of the top and bottom surfaces + bool align = params.extrusion_role == ExtrusionRole::erInternalInfill; + BoundingBox bounding_box; BoundingBox snug_bounding_box = get_extents(expolygon).inflated(SCALED_EPSILON); // Expand the bounding box to avoid artifacts at the edges - snug_bounding_box.offset(scale_(this->spacing)*params.multiline); + snug_bounding_box.offset(scale_(this->spacing)*params.multiline); - // Rotated bounding box of the area to fill in with the pattern. - BoundingBox bounding_box = align ? - // Sparse infill needs to be aligned across layers. Align infill across layers using the object's bounding box. - this->bounding_box.rotated(-direction.first) : - // Solid infill does not need to be aligned across layers, generate the infill pattern - // around the clipping expolygon only. - snug_bounding_box; + // Sparse infill (or Internal where align == true) needs to be aligned across layers. Align infill across layers using the object's bounding box. + // Solid infill does not need to be aligned across layers, generate the infill pattern around the clipping expolygon only. + if (align) + bounding_box = this->bounding_box.rotated(-direction.first); + else if (params.center_of_surface_pattern == CenterOfSurfacePattern::Each_Surface) + bounding_box = snug_bounding_box; + else if (params.center_of_surface_pattern == CenterOfSurfacePattern::Each_Model) + bounding_box = this->bounding_box.rotated(-direction.first); + else + bounding_box = extended_object_bounding_box(); Point shift = this->centered() ? bounding_box.center() : @@ -129,35 +133,49 @@ void FillPlanePath::_fill_surface_single( polylines = intersection_pl(std::move(polylines), expolygon); if (!polylines.empty()) { Polylines chained; - if (params.dont_connect() || params.density > 0.5) { - // ORCA: special flag for flow rate calibration - auto is_flow_calib = params.extrusion_role == erTopSolidInfill && - this->print_object_config->has("calib_flowrate_topinfill_special_order") && - this->print_object_config->option("calib_flowrate_topinfill_special_order")->getBool() && - dynamic_cast(this); - if (is_flow_calib) { - // We want the spiral part to be printed inside-out - // Find the center spiral line first, by looking for the longest one - auto it = std::max_element(polylines.begin(), polylines.end(), - [](const Polyline& a, const Polyline& b) { return a.length() < b.length(); }); - Polyline center_spiral = std::move(*it); + if (!params.is_anisotropic) { // Orca: not anisotropic surface + if ((params.dont_connect() || params.density > 0.5)) { + // ORCA: special flag for flow rate calibration + auto is_flow_calib = params.extrusion_role == erTopSolidInfill && + this->print_object_config->has("calib_flowrate_topinfill_special_order") && + this->print_object_config->option("calib_flowrate_topinfill_special_order")->getBool() && + dynamic_cast(this); + if (is_flow_calib) { + // We want the spiral part to be printed inside-out + // Find the center spiral line first, by looking for the longest one + auto it = std::max_element(polylines.begin(), polylines.end(), + [](const Polyline& a, const Polyline& b) { return a.length() < b.length(); }); + Polyline center_spiral = std::move(*it); - // Ensure the spiral is printed from inside to out - if (center_spiral.first_point().squaredNorm() > center_spiral.last_point().squaredNorm()) { - center_spiral.reverse(); + // Ensure the spiral is printed from inside to out + if ((center_spiral.first_point().squaredNorm() > center_spiral.last_point().squaredNorm())) { + center_spiral.reverse(); + } + + // Chain the other polylines + polylines.erase(it); + chained = chain_polylines(std::move(polylines), nullptr); + + // Then add the center spiral back + chained.push_back(std::move(center_spiral)); + } else { + chained = chain_polylines(std::move(polylines), nullptr); } - - // Chain the other polylines - polylines.erase(it); - chained = chain_polylines(std::move(polylines)); - - // Then add the center spiral back - chained.push_back(std::move(center_spiral)); - } else { - chained = chain_polylines(std::move(polylines)); + } else + connect_infill(std::move(polylines), expolygon, chained, this->spacing, params); + } else { // Orca: anisotropic surface + const Point _center(0., 0.); + for (Polyline& segment : polylines) { // sort paths by its direction + if (segment.size() > 1) { // need at least two points to evaluate direction + if (segment.first_point().ccw(segment.points[1], _center) < 0) + segment.reverse(); + } + chained.emplace_back(std::move(segment)); } - } else - connect_infill(std::move(polylines), expolygon, chained, this->spacing, params); + std::sort(chained.begin(), chained.end(), [&_center](const Polyline& a, const Polyline& b) { // just sort polylines from center to outside + return a.distance_to(_center) < b.distance_to(_center); + }); + } // paths must be repositioned and rotated back for (Polyline& pl : chained) { pl.translate(shift.x(), shift.y()); diff --git a/src/libslic3r/Model.cpp b/src/libslic3r/Model.cpp index 1177a5227d..eb9153a661 100644 --- a/src/libslic3r/Model.cpp +++ b/src/libslic3r/Model.cpp @@ -2708,6 +2708,23 @@ const TriangleMesh& ModelVolume::get_convex_hull() const return *m_convex_hull.get(); } +// Orca: get volume bbox for separate infill +static std::mutex mtx_model; +BoundingBox ModelVolume::get_volume_bbox(const Transform3d &matrix, Point &shift, bool apply_cache = false) { + std::unique_lock l(mtx_model); // locks function here + // Orca: the cache is keyed by the instance transform/shift; a ModelVolume is shared + // across instances, so returning the cache blindly would hand back another instance's bbox. + if (m_cached_volume_bbox.defined && apply_cache + && matrix.isApprox(m_cached_volume_bbox_matrix) + && shift == m_cached_volume_bbox_shift) + return m_cached_volume_bbox; + auto hull = get_convex_hull_2d(matrix); + hull.translate(-shift); + m_cached_volume_bbox_matrix = matrix; + m_cached_volume_bbox_shift = shift; + return m_cached_volume_bbox = hull.bounding_box().polygon().bounding_box(); +} + //BBS: refine the model part names ModelVolumeType ModelVolume::type_from_string(const std::string &s) { diff --git a/src/libslic3r/Model.hpp b/src/libslic3r/Model.hpp index b15124d9ed..518fbbaa1c 100644 --- a/src/libslic3r/Model.hpp +++ b/src/libslic3r/Model.hpp @@ -920,6 +920,14 @@ public: // Extruder ID is only valid for FFF. Returns -1 for SLA or if the extruder ID is not applicable (support volumes). int extruder_id() const; + //Orca: cache clearing procedure to ensure that the shape is positioned accurately when manipulating it + void clear_cache() { + m_cached_trans_matrix = Transform3d::Identity().inverse(); // get unvelivable matrix + m_cached_volume_bbox.reset(); + m_convex_hull_2d.clear(); + m_cached_2d_polygon.clear(); + }; + bool is_splittable() const; // BBS @@ -961,39 +969,42 @@ public: // Get count of errors in the mesh int get_repaired_errors_count() const; + BoundingBox get_volume_bbox(const Transform3d &matrix, Point &shift, bool apply_cache); + void reset_volume_bbox() { m_cached_volume_bbox.reset(); }; + // Helpers for loading / storing into AMF / 3MF files. static ModelVolumeType type_from_string(const std::string &s); static std::string type_to_string(const ModelVolumeType t); const Geometry::Transformation& get_transformation() const { return m_transformation; } - void set_transformation(const Geometry::Transformation& transformation) { m_transformation = transformation; } - void set_transformation(const Transform3d& trafo) { m_transformation.set_matrix(trafo); } + void set_transformation(const Geometry::Transformation& transformation) { clear_cache(); m_transformation = transformation; } + void set_transformation(const Transform3d& trafo) { clear_cache(); m_transformation.set_matrix(trafo); } Vec3d get_offset() const { return m_transformation.get_offset(); } double get_offset(Axis axis) const { return m_transformation.get_offset(axis); } - void set_offset(const Vec3d& offset) { m_transformation.set_offset(offset); } - void set_offset(Axis axis, double offset) { m_transformation.set_offset(axis, offset); } + void set_offset(const Vec3d& offset) { clear_cache(); m_transformation.set_offset(offset); } + void set_offset(Axis axis, double offset) { clear_cache(); m_transformation.set_offset(axis, offset); } Vec3d get_rotation() const { return m_transformation.get_rotation(); } double get_rotation(Axis axis) const { return m_transformation.get_rotation(axis); } - void set_rotation(const Vec3d& rotation) { m_transformation.set_rotation(rotation); } - void set_rotation(Axis axis, double rotation) { m_transformation.set_rotation(axis, rotation); } + void set_rotation(const Vec3d& rotation) { clear_cache(); m_transformation.set_rotation(rotation); } + void set_rotation(Axis axis, double rotation) { clear_cache(); m_transformation.set_rotation(axis, rotation); } Vec3d get_scaling_factor() const { return m_transformation.get_scaling_factor(); } double get_scaling_factor(Axis axis) const { return m_transformation.get_scaling_factor(axis); } - void set_scaling_factor(const Vec3d& scaling_factor) { m_transformation.set_scaling_factor(scaling_factor); } - void set_scaling_factor(Axis axis, double scaling_factor) { m_transformation.set_scaling_factor(axis, scaling_factor); } + void set_scaling_factor(const Vec3d& scaling_factor) { clear_cache(); m_transformation.set_scaling_factor(scaling_factor); } + void set_scaling_factor(Axis axis, double scaling_factor) {clear_cache(); m_transformation.set_scaling_factor(axis, scaling_factor); } Vec3d get_mirror() const { return m_transformation.get_mirror(); } double get_mirror(Axis axis) const { return m_transformation.get_mirror(axis); } bool is_left_handed() const { return m_transformation.is_left_handed(); } - void set_mirror(const Vec3d& mirror) { m_transformation.set_mirror(mirror); } - void set_mirror(Axis axis, double mirror) { m_transformation.set_mirror(axis, mirror); } + void set_mirror(const Vec3d& mirror) { clear_cache(); m_transformation.set_mirror(mirror); } + void set_mirror(Axis axis, double mirror) { clear_cache(); m_transformation.set_mirror(axis, mirror); } void convert_from_imperial_units(); void convert_from_meters(); @@ -1048,6 +1059,9 @@ private: mutable Transform3d m_cached_trans_matrix; //BBS, used for convex_hell_2d acceleration mutable Polygon m_cached_2d_polygon; //BBS, used for convex_hell_2d acceleration Geometry::Transformation m_transformation; + mutable BoundingBox m_cached_volume_bbox; //Orca: used for separated infills + mutable Transform3d m_cached_volume_bbox_matrix{Transform3d::Identity()}; //Orca: cache key for m_cached_volume_bbox + mutable Point m_cached_volume_bbox_shift{Point(0, 0)}; //Orca: cache key for m_cached_volume_bbox //BBS: add convex_hell_2d related logic void calculate_convex_hull_2d(const Geometry::Transformation &transformation) const; diff --git a/src/libslic3r/Preset.cpp b/src/libslic3r/Preset.cpp index e045b8c3c3..b9319ec0e3 100644 --- a/src/libslic3r/Preset.cpp +++ b/src/libslic3r/Preset.cpp @@ -1063,6 +1063,9 @@ static std::vector s_Preset_print_options{ "skin_infill_density", "align_infill_direction_to_model", "extra_solid_infills", + "anisotropic_surfaces", + "center_of_surface_pattern", + "separated_infills", "minimum_sparse_infill_area", "reduce_infill_retraction", "internal_solid_infill_pattern", diff --git a/src/libslic3r/Print.cpp b/src/libslic3r/Print.cpp index 257bbd9c31..10d7b46349 100644 --- a/src/libslic3r/Print.cpp +++ b/src/libslic3r/Print.cpp @@ -209,6 +209,7 @@ bool Print::invalidate_state_by_config_options(const ConfigOptionResolver & /* n "chamber_minimal_temperature", "thumbnails", "thumbnails_format", + "anisotropic_surfaces", "center_of_surface_pattern", "separated_infills", "seam_gap", "role_based_wipe_speed", "wipe_speed", diff --git a/src/libslic3r/PrintConfig.cpp b/src/libslic3r/PrintConfig.cpp index e626d3e37f..b3f8a2a0b8 100644 --- a/src/libslic3r/PrintConfig.cpp +++ b/src/libslic3r/PrintConfig.cpp @@ -188,6 +188,12 @@ static t_config_enum_values s_keys_map_PowerLossRecoveryMode { }; CONFIG_OPTION_ENUM_DEFINE_STATIC_MAPS(PowerLossRecoveryMode) +static t_config_enum_values s_keys_map_CenterOfSurfacePattern{ + {"each_surface", int(CenterOfSurfacePattern::Each_Surface)}, + {"each_model", int(CenterOfSurfacePattern::Each_Model)}, + {"each_assembly", int(CenterOfSurfacePattern::Each_Assembly)}}; +CONFIG_OPTION_ENUM_DEFINE_STATIC_MAPS(CenterOfSurfacePattern) + static t_config_enum_values s_keys_map_FuzzySkinType { { "none", int(FuzzySkinType::None) }, { "external", int(FuzzySkinType::External) }, @@ -6869,6 +6875,48 @@ void PrintConfigDef::init_fff_params() def->min = 0; def->set_default_value(new ConfigOptionFloat(0.6)); + def = this->add("anisotropic_surfaces", coBool); + def->label = L("Anisotropic surfaces"); + def->category = L("Strength"); + def->tooltip = L("Anisotropic patterns on the top and bottom surfaces.\n" + "Co-directional printing mode will be applied. For certain patterns, omni-directional filling provides color " + "dispersion when using multi-colored or silk plastics.\n" + "This option disable the gap fill.\n" + "This option can increase a printing time."); + def->mode = comExpert; + def->set_default_value(new ConfigOptionBool(false)); + + def = this->add("separated_infills", coBool); + def->label = L("Separated infills"); + def->category = L("Strength"); + def->tooltip = L("Aligns the internal infill pattern of each part independently instead of across the whole object or assembly.\n" + "By default, aligned infill patterns share a single origin for the entire object, so the pattern of every " + "part is referenced to the same point. When enabled, each connected body is aligned on its own: parts that " + "touch or overlap are treated as one body and share an origin, while parts detached from the rest each get " + "their own.\n Useful when an assembly groups several distinct objects that should each keep a self-centered infill.\n" + "Only affects centered infill patterns (Archimedean Chords, Octagram Spiral) and patterns driven by an " + "infill rotation template."); + def->mode = comExpert; + def->set_default_value(new ConfigOptionBool(false)); + + def = this->add("center_of_surface_pattern", coEnum); + def->label = L("Center surface pattern on"); + def->category = L("Strength"); + def->tooltip = L("Chooses where the centering point of centered top/bottom surface patterns (Archimedean Chords, " + "Octagram Spiral) is placed.\n" + " - Each Surface: centers the pattern on every individual surface region, so each island is symmetric on its own.\n" + " - Each Model: centers the pattern on each connected body. Parts that touch or overlap share one center; " + "parts detached from the rest each get their own.\n" + " - Each Assembly: uses a single shared center for the whole object or assembly."); + def->enum_keys_map = &ConfigOptionEnum::get_enum_values(); + def->enum_values.push_back("each_surface"); + def->enum_values.push_back("each_model"); + def->enum_values.push_back("each_assembly"); + def->enum_labels.push_back(L("Each Surface")); + def->enum_labels.push_back(L("Each Model")); + def->enum_labels.push_back(L("Each Assembly")); + def->mode = comExpert; + def->set_default_value(new ConfigOptionEnum(CenterOfSurfacePattern::Each_Surface)); def = this->add("travel_speed", coFloats); def->label = L("Travel"); diff --git a/src/libslic3r/PrintConfig.hpp b/src/libslic3r/PrintConfig.hpp index 31e3c2cc45..f36076da45 100644 --- a/src/libslic3r/PrintConfig.hpp +++ b/src/libslic3r/PrintConfig.hpp @@ -69,6 +69,12 @@ enum class TopSurfaceExpansionDirection { Outward, }; +enum class CenterOfSurfacePattern { + Each_Surface, + Each_Model, + Each_Assembly, +}; + enum class NoiseType { Classic, Perlin, @@ -1137,6 +1143,9 @@ PRINT_CONFIG_CLASS_DEFINE( ((ConfigOptionFloat, lightning_prune_angle)) ((ConfigOptionFloat, lightning_straightening_angle)) ((ConfigOptionBool, align_infill_direction_to_model)) + ((ConfigOptionBool, anisotropic_surfaces)) + ((ConfigOptionEnum, center_of_surface_pattern)) + ((ConfigOptionBool, separated_infills)) ((ConfigOptionString, extra_solid_infills)) ((ConfigOptionEnum, fuzzy_skin)) ((ConfigOptionFloat, fuzzy_skin_thickness)) diff --git a/src/libslic3r/PrintObject.cpp b/src/libslic3r/PrintObject.cpp index 10cc92013b..d977a5419b 100644 --- a/src/libslic3r/PrintObject.cpp +++ b/src/libslic3r/PrintObject.cpp @@ -563,6 +563,11 @@ void PrintObject::prepare_infill() { if (! this->set_started(posPrepareInfill)) return; + + // Orca: clear all volume bbox caches + for (auto volume : this->model_object()->volumes) + volume->reset_volume_bbox(); + m_print->set_status(25, L("Generating infill regions")); if (m_typed_slices) { // To improve robustness of detect_surfaces_type() when reslicing (working with typed slices), see GH issue #7442. @@ -1334,6 +1339,9 @@ bool PrintObject::invalidate_state_by_config_options( || opt_key == "top_surface_line_width" || opt_key == "top_surface_density" || opt_key == "bottom_surface_density" + || opt_key == "anisotropic_surfaces" + || opt_key == "center_of_surface_pattern" + || opt_key == "separated_infills" || opt_key == "initial_layer_line_width" || opt_key == "small_area_infill_flow_compensation" || opt_key == "lateral_lattice_angle_1" diff --git a/src/slic3r/GUI/ConfigManipulation.cpp b/src/slic3r/GUI/ConfigManipulation.cpp index a41473bc3d..00e2cebc44 100644 --- a/src/slic3r/GUI/ConfigManipulation.cpp +++ b/src/slic3r/GUI/ConfigManipulation.cpp @@ -720,6 +720,29 @@ void ConfigManipulation::toggle_print_fff_options(DynamicPrintConfig *config, in toggle_line("top_surface_expansion_direction", has_top_shell); toggle_field("top_surface_expansion_direction", has_top_surface_expansion); + // Orca: Archimedean Chords and Octagram Spiral are the centered surface patterns that the + // pattern-centering, anisotropic-surface and separated-infill features act on. + auto is_centered_pattern = [](InfillPattern p) { + return p == InfillPattern::ipArchimedeanChords || p == InfillPattern::ipOctagramSpiral; + }; + bool is_top_centered = is_centered_pattern(config->option>("top_surface_pattern")->value); + bool is_bottom_centered = is_centered_pattern(config->option>("bottom_surface_pattern")->value); + bool has_centered_surface = (has_top_shell && is_top_centered) || (has_bottom_shell && is_bottom_centered); + + // Orca: center of surface pattern / anisotropic surfaces + toggle_line("center_of_surface_pattern", has_centered_surface); + toggle_line("anisotropic_surfaces", has_centered_surface); + + // Orca: separate infills + bool is_internal_infill_centered = is_centered_pattern(config->option>("sparse_infill_pattern")->value) || + config->opt_string("sparse_infill_rotate_template") != "" || + config->opt_string("solid_infill_rotate_template") != ""; + toggle_line("separated_infills", is_internal_infill_centered); + + // Orca: no need gaps + for (auto el : {"gap_fill_target", "filter_out_gap_fill"}) + toggle_field(el, !config->opt_bool("anisotropic_surfaces")); + for (auto el : { "infill_direction", "sparse_infill_line_width", "gap_fill_target","filter_out_gap_fill","infill_wall_overlap", "bridge_angle", "internal_bridge_angle", "relative_bridge_angle", "solid_infill_direction", "solid_infill_rotate_template", "internal_solid_infill_pattern", "internal_solid_filament_id", "top_surface_filament_id", "bottom_surface_filament_id", diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index a4a4b9a98e..aecea8f3b8 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -13113,6 +13113,9 @@ void adjust_settings_for_flowrate_calib(ModelObjectPtrs& objects, bool linear, i _obj->config.set_key_value("top_solid_infill_flow_ratio", new ConfigOptionFloat(1.0f)); _obj->config.set_key_value("infill_direction", new ConfigOptionFloat(45)); _obj->config.set_key_value("solid_infill_direction", new ConfigOptionFloat(135)); + _obj->config.set_key_value("anisotropic_surfaces", new ConfigOptionBool(false)); + _obj->config.set_key_value("center_of_surface_pattern", new ConfigOptionEnum(CenterOfSurfacePattern::Each_Surface)); + _obj->config.set_key_value("separated_infills", new ConfigOptionBool(false)); _obj->config.set_key_value("align_infill_direction_to_model", new ConfigOptionBool(true)); _obj->config.set_key_value("ironing_type", new ConfigOptionEnum(IroningType::NoIroning)); _obj->config.set_key_value("internal_solid_infill_speed", new ConfigOptionFloatsNullable(internal_solid_speeds)); diff --git a/src/slic3r/GUI/Tab.cpp b/src/slic3r/GUI/Tab.cpp index 14ad4bc596..0fda586a39 100644 --- a/src/slic3r/GUI/Tab.cpp +++ b/src/slic3r/GUI/Tab.cpp @@ -2751,6 +2751,8 @@ void TabPrint::build() optgroup->append_single_option_line("bottom_surface_density", "strength_settings_top_bottom_shells#surface-density"); optgroup->append_single_option_line("bottom_surface_pattern", "strength_settings_top_bottom_shells#surface-pattern"); optgroup->append_single_option_line("bottom_layer_direction", "strength_settings_infill#direction"); + optgroup->append_single_option_line("center_of_surface_pattern", "strength_settings_top_bottom_shells#center-surface-pattern-on"); + optgroup->append_single_option_line("anisotropic_surfaces", "strength_settings_top_bottom_shells#anisotropic-surfaces"); optgroup->append_single_option_line("top_bottom_infill_wall_overlap", "strength_settings_top_bottom_shells#infillwall-overlap"); optgroup = page->new_optgroup(L("Infill"), L"param_infill"); @@ -2781,6 +2783,7 @@ void TabPrint::build() optgroup->append_single_option_line("solid_infill_rotate_template", "strength_settings_infill_rotation_template_metalanguage"); optgroup->append_single_option_line("gap_fill_target", "strength_settings_infill#apply-gap-fill"); optgroup->append_single_option_line("filter_out_gap_fill", "strength_settings_infill#filter-out-tiny-gaps"); + optgroup->append_single_option_line("separated_infills", "strength_settings_infill#separated-infills"); optgroup->append_single_option_line("infill_wall_overlap", "strength_settings_infill#infill-wall-overlap"); optgroup = page->new_optgroup(L("Advanced"), L"param_advanced"); From 7d174004437c16d84ee3cde03475b647e4017465 Mon Sep 17 00:00:00 2001 From: Kris Austin Date: Wed, 8 Jul 2026 19:56:03 -0500 Subject: [PATCH 5/8] fix: dangling static lambda crashes support G-code export on repeated slices (#14677) GCode::extrude_support declared its per-path speed helper as a function-local static lambda that captures `this` by reference. The closure is built once, on the first extrude_support call, and reused for the rest of the process, so a second G-code export in the same process runs the helper against a `this` from the first export's stack frame, which has already returned. The stale `this` flows through NOZZLE_CONFIG(...) -> cur_extruder_index() -> GCodeWriter::filament(), reading a garbage current-extruder id and indexing with it. It is silent whenever the reused stack still holds a usable pointer, and an order-dependent SIGSEGV otherwise; AddressSanitizer reports it as a stack-use-after-return in GCodeWriter::filament(). It is the only static capturing lambda in libslic3r. Drop static so the closure is rebuilt each call against the live frame. Add an fff_print regression test that slices a support object twice in one process; it fails without the fix (stack-use-after-return under ASan) and passes with it. --- src/libslic3r/GCode.cpp | 3 ++- tests/fff_print/test_support_material.cpp | 11 +++++++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/src/libslic3r/GCode.cpp b/src/libslic3r/GCode.cpp index 0b62fd0d6f..8238f5be85 100644 --- a/src/libslic3r/GCode.cpp +++ b/src/libslic3r/GCode.cpp @@ -6215,7 +6215,8 @@ std::string GCode::extrude_support(const ExtrusionEntityCollection &support_fill static constexpr const char* support_transition_label = "support transition"; static constexpr const char* support_ironing_label = "support ironing"; - static const auto speed_for_path = [&](double length, ExtrusionRole role, double default_speed = -1.0) { + // Not static: it captures `this` by reference. + const auto speed_for_path = [&](double length, ExtrusionRole role, double default_speed = -1.0) { if (!is_support(role) || length > SMALL_PERIMETER_LENGTH(NOZZLE_CONFIG(small_support_perimeter_threshold))) return default_speed; diff --git a/tests/fff_print/test_support_material.cpp b/tests/fff_print/test_support_material.cpp index 24c0e9cdc5..f854939c8c 100644 --- a/tests/fff_print/test_support_material.cpp +++ b/tests/fff_print/test_support_material.cpp @@ -93,3 +93,14 @@ SCENARIO("Support layer Z honors contact distance", "[SupportMaterial]") } } } + +// extrude_support once held a `static` lambda capturing `this`, so a second export in the +// same process dereferenced a returned stack frame (ASan: stack-use-after-return). +TEST_CASE("Support G-code emission survives a second slice in the same process", "[SupportMaterial][Regression]") +{ + const std::string first = slice({ TestMesh::overhang }, { { "enable_support", 1 } }); + REQUIRE(! layers_with_role(first, "support").empty()); + + const std::string second = slice({ TestMesh::overhang }, { { "enable_support", 1 } }); + REQUIRE(! layers_with_role(second, "support").empty()); +} From 2194037d1650460826805c7fc27a3a077a8ff7ea Mon Sep 17 00:00:00 2001 From: Kris Austin Date: Wed, 8 Jul 2026 20:57:06 -0500 Subject: [PATCH 6/8] test: cover floor/ceil and the built-in function boundary in the placeholder parser (#14667) round() already had unit coverage; floor() and ceil() had none. Add the missing positive cases for both signs, plus round()'s half-away-from-zero tie-break, and one negative case asserting that a name outside the grammar's built-in function set is treated as an undefined variable and throws, rather than being passed through to a math library. --- tests/libslic3r/test_placeholder_parser.cpp | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/tests/libslic3r/test_placeholder_parser.cpp b/tests/libslic3r/test_placeholder_parser.cpp index ec72b72769..14716b0b5e 100644 --- a/tests/libslic3r/test_placeholder_parser.cpp +++ b/tests/libslic3r/test_placeholder_parser.cpp @@ -52,6 +52,11 @@ SCENARIO("Placeholder parser scripting", "[PlaceholderParser]") { SECTION("math: round(-13.4)") { REQUIRE(parser.process("{round(-13.4)}") == "-13"); } SECTION("math: round(13.6)") { REQUIRE(parser.process("{round(13.6)}") == "14"); } SECTION("math: round(-13.6)") { REQUIRE(parser.process("{round(-13.6)}") == "-14"); } + SECTION("math: round(13.5)") { REQUIRE(parser.process("{round(13.5)}") == "14"); } + SECTION("math: floor(13.9)") { REQUIRE(parser.process("{floor(13.9)}") == "13"); } + SECTION("math: floor(-13.1)") { REQUIRE(parser.process("{floor(-13.1)}") == "-14"); } + SECTION("math: ceil(13.1)") { REQUIRE(parser.process("{ceil(13.1)}") == "14"); } + SECTION("math: ceil(-13.9)") { REQUIRE(parser.process("{ceil(-13.9)}") == "-13"); } SECTION("math: digits(5, 15)") { REQUIRE(parser.process("{digits(5, 15)}") == " 5"); } SECTION("math: digits(5., 15)") { REQUIRE(parser.process("{digits(5., 15)}") == " 5"); } SECTION("math: zdigits(5, 15)") { REQUIRE(parser.process("{zdigits(5, 15)}") == "000000000000005"); } @@ -65,6 +70,8 @@ SCENARIO("Placeholder parser scripting", "[PlaceholderParser]") { SECTION("math: interpolate_table(13.84375892476, (0, 0), (20, 20))") { REQUIRE(std::stod(parser.process("{interpolate_table(13.84375892476, (0, 0), (20, 20))}")) == Catch::Approx(13.84375892476)); } SECTION("math: interpolate_table(13, (0, 0), (20, 20), (30, 20))") { REQUIRE(std::stod(parser.process("{interpolate_table(13, (0, 0), (20, 20), (30, 20))}")) == Catch::Approx(13.)); } SECTION("math: interpolate_table(25, (0, 0), (20, 20), (30, 20))") { REQUIRE(std::stod(parser.process("{interpolate_table(25, (0, 0), (20, 20), (30, 20))}")) == Catch::Approx(20.)); } + // Only the grammar's built-in functions are callable; any other name is an undefined variable and throws. + SECTION("math: a non-built-in function name throws") { REQUIRE_THROWS(parser.process("{sqrt(16)}")); } // regex_replace(subject, /pattern/, replacement): the string-transform primitive. SECTION("regex_replace: strips a file extension") { REQUIRE(parser.process("{regex_replace(\"part.stl\", /\\.[^.]*$/, \"\")}") == "part"); } From 6fda82476dbfa1e6f8f82f62189d4a1323a60852 Mon Sep 17 00:00:00 2001 From: Kris Austin Date: Thu, 9 Jul 2026 02:47:57 -0500 Subject: [PATCH 7/8] fix: out-of-bounds read computing tool-ordering max layer height (#14665) * fix: out-of-bounds read computing tool-ordering max layer height calc_max_layer_height() loops over the extruder count (nozzle_diameter) but indexes max_layer_height with the same counter, reading past the end when that array is shorter. Silent on release builds, aborts under a bounds-checked STL (_GLIBCXX_ASSERTIONS). Read via get_at(), which falls back to the first entry when the index is out of range, as Slicing.cpp already does for this option. Add a fff_print regression test slicing a two-extruder printer with a single-entry max_layer_height. * docs: clarify how max_layer_height ends up short in the regression test Normalization sizes it to the filament count under single_extruder_multi_material, not "a mismatch a profile can ship" as the earlier comment guessed. --- src/libslic3r/GCode/ToolOrdering.cpp | 3 ++- tests/fff_print/test_multifilament.cpp | 19 +++++++++++++++++++ 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/src/libslic3r/GCode/ToolOrdering.cpp b/src/libslic3r/GCode/ToolOrdering.cpp index ee03d6e959..65c887f343 100644 --- a/src/libslic3r/GCode/ToolOrdering.cpp +++ b/src/libslic3r/GCode/ToolOrdering.cpp @@ -138,7 +138,8 @@ static double calc_max_layer_height(const PrintConfig &config, double max_object { double max_layer_height = std::numeric_limits::max(); for (size_t i = 0; i < config.nozzle_diameter.values.size(); ++ i) { - double mlh = config.max_layer_height.values[i]; + // max_layer_height may be shorter than the extruder count; get_at() clamps. + double mlh = config.max_layer_height.get_at(i); if (mlh == 0.) mlh = 0.75 * config.nozzle_diameter.values[i]; max_layer_height = std::min(max_layer_height, mlh); diff --git a/tests/fff_print/test_multifilament.cpp b/tests/fff_print/test_multifilament.cpp index 12c1cae766..f33a8e1e61 100644 --- a/tests/fff_print/test_multifilament.cpp +++ b/tests/fff_print/test_multifilament.cpp @@ -85,3 +85,22 @@ TEST_CASE("Per-object wall filament override is honored", "[MultiFilament]") CHECK(tools_for_role(gcode, "perimeter") == std::set{ 0, 1 }); CHECK(tools_for_role(gcode, "infill") == std::set{ 0 }); // infill not overridden: stays on F1 } + +// max_layer_height can be shorter than the extruder count (normalization sizes it to the +// filament count under single_extruder_multi_material). calc_max_layer_height() in ToolOrdering +// indexed it per-nozzle and read past the end. Shortened directly here to isolate that read; +// the other per-extruder keys stay extruder-length so slicing reaches the code under test. +TEST_CASE("Multi-extruder slice stays in bounds with a short max_layer_height", "[MultiFilament]") +{ + DynamicPrintConfig config = multifilament_config(2); + config.set_deserialize_strict({ + { "nozzle_diameter", "0.4,0.4" }, + { "printer_extruder_id", "1,2" }, + { "printer_extruder_variant", "Direct Drive Standard,Direct Drive Standard" }, + { "extruder_printable_height", "0,0" }, + { "max_layer_height", "0.3" }, // deliberately one entry short + }); + Print print; + init_and_process_print({ cube(20) }, print, config); + REQUIRE_FALSE(print.objects().front()->layers().empty()); +} From 05ed2e4dfabaf2494b86a3a68814c2df88e07e09 Mon Sep 17 00:00:00 2001 From: SoftFever Date: Thu, 9 Jul 2026 21:57:59 +0800 Subject: [PATCH 8/8] Fixed a rare crash on app startup caused by ENOTSUP (errno 45) on Mac (#14686) fix: prevent startup crash when preset-sync directory scan hits a transient FS error On startup the user-preset sync thread scans the preset folder for orphaned .info files (scan_orphaned_info_files). It iterated the directory with a throwing boost::filesystem::directory_iterator while running on a background thread that has no exception guard. On macOS, readdir() can intermittently fail with ENOTSUP (errno 45); boost then throws filesystem_error, which -- uncaught on the sync thread -- calls std::terminate and aborts the whole application on startup. - Iterate with the error_code-based directory_iterator so a transient read failure is logged and skipped instead of thrown. The orphan scan is best-effort and re-runs on the next sync, so skipping a cycle is harmless. This mirrors the existing pattern in has_json_presets() and the plugin scan. - Wrap the entire sync-thread body in try/catch as defense-in-depth, so no future uncaught exception on that otherwise-unguarded thread can abort the app. --- src/slic3r/GUI/GUI_App.cpp | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/src/slic3r/GUI/GUI_App.cpp b/src/slic3r/GUI/GUI_App.cpp index 121acdb947..feaddf07ab 100644 --- a/src/slic3r/GUI/GUI_App.cpp +++ b/src/slic3r/GUI/GUI_App.cpp @@ -6965,6 +6965,10 @@ void GUI_App::start_sync_user_preset(bool with_progress_dlg) // finishFn tears down the progress dialog (and clears the re-entrancy guard), so it // must run on every exit path — otherwise an early bail-out would leak the modal // dialog and leave the guard stuck, blocking all later manual syncs. + // Guard the whole thread body: an uncaught exception here (e.g. a transient + // boost::filesystem error while scanning the preset folder) would otherwise + // propagate out of the thread and terminate the entire application. + try { if (!m_agent) { finishFn(false); return; } // One-time scan for orphaned .info files left over from offline deletions; queues HTTP DELETEs. @@ -7206,6 +7210,11 @@ void GUI_App::start_sync_user_preset(bool with_progress_dlg) boost::this_thread::sleep_for(boost::chrono::milliseconds(500)); } } + } catch (const std::exception& e) { + BOOST_LOG_TRIVIAL(error) << "user preset sync thread terminated by exception: " << e.what(); + } catch (...) { + BOOST_LOG_TRIVIAL(error) << "user preset sync thread terminated by unknown exception"; + } }); } @@ -8526,8 +8535,13 @@ void GUI_App::scan_orphaned_info_files() if (!fs::exists(type_dir)) continue; - // Iterate through all .info files - for (auto& entry : boost::filesystem::directory_iterator(type_dir)) { + // Iterate through all .info files. Use the error_code-based iterator so a transient + // directory-read failure (e.g. macOS readdir returning ENOTSUP) is logged and skipped + // instead of throwing an uncaught exception that would terminate the app from the + // background sync thread this runs on. + boost::system::error_code ec; + for (boost::filesystem::directory_iterator it(type_dir, ec), end; !ec && it != end; it.increment(ec)) { + const auto& entry = *it; if (entry.path().extension() != ".info") continue; @@ -8546,6 +8560,8 @@ void GUI_App::scan_orphaned_info_files() } } } + if (ec) + BOOST_LOG_TRIVIAL(warning) << "scan_orphaned_info_files: failed to scan " << type_dir.string() << ": " << ec.message(); } }