From 13ae3a1c90781b555db06cb3811b9f1705be9d99 Mon Sep 17 00:00:00 2001 From: maddavo <1432875+maddavo@users.noreply.github.com> Date: Sun, 2 Aug 2026 12:54:16 +1000 Subject: [PATCH] Add outer-only mouse ears and align ear radius controls (#15015) Improve mouse ear brim controls --- src/libslic3r/Brim.cpp | 16 +-- src/libslic3r/Preset.cpp | 2 +- src/libslic3r/PrintConfig.cpp | 7 + src/libslic3r/PrintConfig.hpp | 1 + src/libslic3r/PrintObject.cpp | 1 + src/slic3r/GUI/ConfigManipulation.cpp | 21 ++- src/slic3r/GUI/ConfigManipulation.hpp | 7 +- src/slic3r/GUI/Gizmos/GLGizmoBrimEars.cpp | 70 +++++----- src/slic3r/GUI/Gizmos/GLGizmoBrimEars.hpp | 4 +- src/slic3r/GUI/OptionsGroup.cpp | 2 + src/slic3r/GUI/OptionsGroup.hpp | 9 ++ src/slic3r/GUI/Tab.cpp | 14 +- src/slic3r/GUI/Tab.hpp | 1 + tests/fff_print/test_skirt_brim.cpp | 150 ++++++++++++++++++++++ 14 files changed, 252 insertions(+), 53 deletions(-) diff --git a/src/libslic3r/Brim.cpp b/src/libslic3r/Brim.cpp index b22c9c323e..9cee5a0e4b 100644 --- a/src/libslic3r/Brim.cpp +++ b/src/libslic3r/Brim.cpp @@ -349,7 +349,7 @@ static ExPolygons make_brim_ears_auto(const ExPolygons& obj_expoly, coord_t size return mouse_ears_ex; } -static ExPolygons make_brim_ears(const PrintObject* object, const double& flowWidth, float brim_offset, Flow &flow, bool is_outer_brim) +static ExPolygons make_brim_ears(const PrintObject* object) { ExPolygons mouse_ears_ex; BrimPoints brim_ear_points = object->model_object()->brim_points; @@ -373,12 +373,7 @@ static ExPolygons make_brim_ears(const PrintObject* object, const double& flowWi Vec3f world_pos = pt.transform(trsf.get_matrix()); if ( world_pos.z() > 0) continue; Polygon point_round; - float brim_width = floor(scale_(pt.head_front_radius) / flowWidth / 2) * flowWidth * 2; - if (is_outer_brim) { - double flowWidthScale = flowWidth / SCALING_FACTOR; - brim_width = floor(brim_width / flowWidthScale / 2) * flowWidthScale * 2; - } - coord_t size_ear = (brim_width - brim_offset - flow.scaled_spacing()); + const coord_t size_ear = scale_(pt.head_front_radius); for (size_t i = 0; i < POLY_SIDE_COUNT; i++) { double angle = (2.0 * PI * i) / POLY_SIDE_COUNT; point_round.points.emplace_back(size_ear * cos(angle), size_ear * sin(angle)); @@ -452,7 +447,8 @@ static ExPolygons outer_inner_brim_area(const Print& print, bool has_brim_auto = object->config().brim_type == btAutoBrim; const bool use_auto_brim_ears = object->config().brim_type == btEar; const bool use_brim_ears = object->config().brim_type == btPainted; - const bool has_inner_brim = brim_type == btInnerOnly || brim_type == btOuterAndInner || use_auto_brim_ears || use_brim_ears; + const bool use_inner_brim_ears = (use_auto_brim_ears || use_brim_ears) && !object->config().brim_ears_outer_only.value; + const bool has_inner_brim = brim_type == btInnerOnly || brim_type == btOuterAndInner || use_inner_brim_ears; const bool has_outer_brim = brim_type == btOuterOnly || brim_type == btOuterAndInner || brim_type == btAutoBrim || use_auto_brim_ears || use_brim_ears; coord_t ear_detection_length = scale_(object->config().brim_ears_detection_length.value); coordf_t brim_ears_max_angle = object->config().brim_ears_max_angle.value; @@ -531,7 +527,7 @@ static ExPolygons outer_inner_brim_area(const Print& print, auto innerExpoly = offset_ex(ex_poly.contour, brim_offset, jtRound, SCALED_RESOLUTION); ExPolygons outerExpoly; if (use_brim_ears) { - outerExpoly = make_brim_ears(object, flowWidth, brim_offset, flow, true); + outerExpoly = make_brim_ears(object); //outerExpoly = offset_ex(outerExpoly, brim_width_mod, jtRound, SCALED_RESOLUTION); } else if (use_auto_brim_ears) { coord_t size_ear = (brim_width_mod - brim_offset - flow.scaled_spacing()); @@ -545,7 +541,7 @@ static ExPolygons outer_inner_brim_area(const Print& print, ExPolygons outerExpoly; auto innerExpoly = offset_ex(ex_poly_holes_reversed, -brim_width - brim_offset); if (use_brim_ears) { - outerExpoly = make_brim_ears(object, flowWidth, brim_offset, flow, false); + outerExpoly = make_brim_ears(object); } else if (use_auto_brim_ears) { coord_t size_ear = (brim_width - brim_offset - flow.scaled_spacing()); outerExpoly = make_brim_ears_auto(offset_ex(ex_poly_holes_reversed, -brim_offset), size_ear, ear_detection_length, brim_ears_max_angle, false); diff --git a/src/libslic3r/Preset.cpp b/src/libslic3r/Preset.cpp index 3bd277d44a..2821bef0af 100644 --- a/src/libslic3r/Preset.cpp +++ b/src/libslic3r/Preset.cpp @@ -1091,7 +1091,7 @@ static std::vector s_Preset_print_options{ "top_surface_speed", "support_speed", "support_object_xy_distance", "support_object_first_layer_gap", "support_interface_speed", "bridge_speed", "internal_bridge_speed", "gap_infill_speed", "travel_speed", "travel_speed_z", "initial_layer_speed", "outer_wall_acceleration", "initial_layer_acceleration", "top_surface_acceleration", "default_acceleration", "skirt_type", "skirt_loops", "skirt_speed","min_skirt_length", "skirt_distance", "skirt_start_angle", "skirt_height","single_loop_draft_shield", "draft_shield", - "brim_width", "brim_object_gap", "brim_flow_ratio", "brim_use_efc_outline", "combine_brims", "brim_type", "brim_ears_max_angle", "brim_ears_detection_length", "enable_support", "support_type", "support_threshold_angle", "support_threshold_overlap","enforce_support_layers", + "brim_width", "brim_object_gap", "brim_flow_ratio", "brim_use_efc_outline", "combine_brims", "brim_type", "brim_ears_max_angle", "brim_ears_detection_length", "brim_ears_outer_only", "enable_support", "support_type", "support_threshold_angle", "support_threshold_overlap","enforce_support_layers", "raft_layers", "raft_first_layer_density", "raft_first_layer_expansion", "raft_contact_distance", "raft_expansion", "support_base_pattern", "support_base_pattern_spacing", "support_expansion", "support_style", // BBS diff --git a/src/libslic3r/PrintConfig.cpp b/src/libslic3r/PrintConfig.cpp index f7222b864f..cc991f91cc 100644 --- a/src/libslic3r/PrintConfig.cpp +++ b/src/libslic3r/PrintConfig.cpp @@ -1937,6 +1937,13 @@ void PrintConfigDef::init_fff_params() def->mode = comAdvanced; def->set_default_value(new ConfigOptionFloat(1)); + def = this->add("brim_ears_outer_only", coBool); + def->label = L("Brim ears outer only"); + def->category = L("Support"); + def->tooltip = L("Generate mouse ears only on the outer contour of the model, excluding holes and enclosed sections."); + def->mode = comAdvanced; + def->set_default_value(new ConfigOptionBool(false)); + def = this->add("compatible_printers", coStrings); def->label = L("Select printers"); def->mode = comAdvanced; diff --git a/src/libslic3r/PrintConfig.hpp b/src/libslic3r/PrintConfig.hpp index 1082c43491..90aa1adb3d 100644 --- a/src/libslic3r/PrintConfig.hpp +++ b/src/libslic3r/PrintConfig.hpp @@ -1082,6 +1082,7 @@ PRINT_CONFIG_CLASS_DEFINE( ((ConfigOptionFloat, brim_width)) ((ConfigOptionFloat, brim_ears_detection_length)) ((ConfigOptionFloat, brim_ears_max_angle)) + ((ConfigOptionBool, brim_ears_outer_only)) ((ConfigOptionFloat, skirt_start_angle)) ((ConfigOptionBool, bridge_no_support)) ((ConfigOptionFloat, elefant_foot_compensation)) diff --git a/src/libslic3r/PrintObject.cpp b/src/libslic3r/PrintObject.cpp index 9356f937ab..b2a92f11a6 100644 --- a/src/libslic3r/PrintObject.cpp +++ b/src/libslic3r/PrintObject.cpp @@ -1175,6 +1175,7 @@ bool PrintObject::invalidate_state_by_config_options( || opt_key == "brim_type" || opt_key == "brim_ears_max_angle" || opt_key == "brim_ears_detection_length" + || opt_key == "brim_ears_outer_only" // BBS: brim generation depends on printing speed || opt_key == "outer_wall_speed" || opt_key == "small_perimeter_speed" diff --git a/src/slic3r/GUI/ConfigManipulation.cpp b/src/slic3r/GUI/ConfigManipulation.cpp index 53a3575a57..e46faa803a 100644 --- a/src/slic3r/GUI/ConfigManipulation.cpp +++ b/src/slic3r/GUI/ConfigManipulation.cpp @@ -70,6 +70,12 @@ void ConfigManipulation::toggle_line(const std::string& opt_key, const bool togg cb_toggle_line(opt_key, toggle, opt_index); } +void ConfigManipulation::set_option_label(const std::string& opt_key, const wxString& label, int opt_index) +{ + if (cb_set_option_label) + cb_set_option_label(opt_key, label, opt_index); +} + void ConfigManipulation::check_nozzle_recommended_temperature_range(DynamicPrintConfig *config) { if (is_msg_dlg_already_exist) return; @@ -808,14 +814,19 @@ void ConfigManipulation::toggle_print_fff_options(DynamicPrintConfig *config, in toggle_field("outer_wall_filament_id", have_perimeters || have_brim); toggle_field("inner_wall_filament_id", have_perimeters || have_brim); - bool have_brim_ear = (config->opt_enum("brim_type") == btEar); + const BrimType brim_type = config->opt_enum("brim_type"); + const bool have_auto_brim_ear = brim_type == btEar; + const bool have_painted_brim_ear = brim_type == btPainted; + set_option_label("brim_width", have_auto_brim_ear ? _L("Brim ear radius") : _L("Brim width")); const auto brim_width = config->opt_float("brim_width"); - // disable brim_ears_max_angle and brim_ears_detection_length if brim_width is 0 + // Automatic brim ear settings require a non-zero brim width. toggle_field("brim_ears_max_angle", brim_width > 0.0f); toggle_field("brim_ears_detection_length", brim_width > 0.0f); - // hide brim_ears_max_angle and brim_ears_detection_length if brim_ear is not selected - toggle_line("brim_ears_max_angle", have_brim_ear); - toggle_line("brim_ears_detection_length", have_brim_ear); + // Painted ears carry their own radius and do not depend on brim_width. + toggle_field("brim_ears_outer_only", have_painted_brim_ear || brim_width > 0.0f); + toggle_line("brim_ears_max_angle", have_auto_brim_ear); + toggle_line("brim_ears_detection_length", have_auto_brim_ear); + toggle_line("brim_ears_outer_only", have_auto_brim_ear || have_painted_brim_ear); // Hide Elephant foot compensation layers if elefant_foot_compensation is not enabled toggle_line("elefant_foot_compensation_layers", config->opt_float("elefant_foot_compensation") > 0 || config->option("elefant_foot_layers_density")->get_abs_value(1.0f) < 1.0f); diff --git a/src/slic3r/GUI/ConfigManipulation.hpp b/src/slic3r/GUI/ConfigManipulation.hpp index 0ad1fb0b7c..d191ef2c4f 100644 --- a/src/slic3r/GUI/ConfigManipulation.hpp +++ b/src/slic3r/GUI/ConfigManipulation.hpp @@ -29,6 +29,7 @@ class ConfigManipulation std::function load_config = nullptr; std::function cb_toggle_field = nullptr; std::function cb_toggle_line = nullptr; + std::function cb_set_option_label = nullptr; // callback to propagation of changed value, if needed std::function cb_value_change = nullptr; //BBS: change local config to const DynamicPrintConfig @@ -45,10 +46,12 @@ public: std::function cb_value_change, //BBS: change local config to DynamicPrintConfig const DynamicPrintConfig* local_config = nullptr, - wxWindow* msg_dlg_parent = nullptr) : + wxWindow* msg_dlg_parent = nullptr, + std::function cb_set_option_label = nullptr) : load_config(load_config), cb_toggle_field(cb_toggle_field), cb_toggle_line(cb_toggle_line), + cb_set_option_label(cb_set_option_label), cb_value_change(cb_value_change), m_msg_dlg_parent(msg_dlg_parent), local_config(local_config) {} @@ -58,6 +61,7 @@ public: load_config = nullptr; cb_toggle_field = nullptr; cb_toggle_line = nullptr; + cb_set_option_label = nullptr; cb_value_change = nullptr; } @@ -67,6 +71,7 @@ public: t_config_option_keys const &applying_keys() const; void toggle_field(const std::string& field_key, const bool toggle, int opt_index = -1); void toggle_line(const std::string& field_key, const bool toggle, int opt_index = -1); + void set_option_label(const std::string& field_key, const wxString& label, int opt_index = -1); // FFF print void update_print_fff_config(DynamicPrintConfig* config, const bool is_global_config = false, const bool is_plate_config = false); diff --git a/src/slic3r/GUI/Gizmos/GLGizmoBrimEars.cpp b/src/slic3r/GUI/Gizmos/GLGizmoBrimEars.cpp index 45aaf37a86..709e7b5b21 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoBrimEars.cpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoBrimEars.cpp @@ -15,6 +15,8 @@ static const ColorRGBA DEF_COLOR = {0.7f, 0.7f, 0.7f, 1.f}; static const ColorRGBA SELECTED_COLOR = {0.0f, 0.5f, 0.5f, 1.0f}; static const ColorRGBA ERR_COLOR = {1.0f, 0.3f, 0.3f, 0.5f}; static const ColorRGBA HOVER_COLOR = {0.7f, 0.7f, 0.7f, 0.5f}; +static constexpr float BRIM_EAR_RADIUS_MIN = 0.1f; +static constexpr float BRIM_EAR_RADIUS_MAX = 100.f; static ModelVolume *get_model_volume(const Selection &selection, Model &model) { @@ -41,14 +43,14 @@ GLGizmoBrimEars::GLGizmoBrimEars(GLCanvas3D &parent, const std::string &icon_fil bool GLGizmoBrimEars::on_init() { - m_new_point_head_diameter = get_brim_default_radius(); + m_new_point_head_radius = get_brim_default_radius(); m_shortcut_key = WXK_CONTROL_E; const wxString ctrl = GUI::shortkey_ctrl_prefix(); const wxString alt = GUI::shortkey_alt_prefix(); - m_desc["head_diameter"] = _L("Head diameter"); + m_desc["brim_ear_radius"] = _L("Brim ear radius"); m_desc["max_angle"] = _L("Max angle"); m_desc["detection_radius"] = _L("Detection radius"); m_desc["remove"] = _L("Remove"); @@ -62,7 +64,7 @@ bool GLGizmoBrimEars::on_init() m_shortcuts = { {_L("Left mouse button"), _L("Add or Select")}, {_L("Right mouse button"), _L("Remove")}, - {ctrl + _L("Mouse wheel"), m_desc["head_diameter"]}, + {ctrl + _L("Mouse wheel"), m_desc["brim_ear_radius"]}, {alt + _L("Mouse wheel"), m_desc["section_view"]}, }; @@ -358,7 +360,7 @@ bool GLGizmoBrimEars::gizmo_event(SLAGizmoEventType action, const Vec2d &mouse_p Transform3d inverse_trsf = volume->get_instance_transformation().get_matrix_no_offset().inverse(); std::pair pos_and_normal; if (unproject_on_mesh2(mouse_position, pos_and_normal)) { - render_hover_point = CacheEntry(BrimPoint(pos_and_normal.first, m_new_point_head_diameter / 2.f), false, (inverse_trsf * m_world_normal).cast(), true); + render_hover_point = CacheEntry(BrimPoint(pos_and_normal.first, m_new_point_head_radius), false, (inverse_trsf * m_world_normal).cast(), true); } else { render_hover_point.reset(); } @@ -397,7 +399,7 @@ bool GLGizmoBrimEars::gizmo_event(SLAGizmoEventType action, const Vec2d &mouse_p Vec3d object_pos = trsf.inverse() * world_pos; // brim ear always face up Plater::TakeSnapshot snapshot(wxGetApp().plater(), "Add brim ear"); - add_point_to_cache(object_pos.cast(), m_new_point_head_diameter / 2.f, false, (inverse_trsf * m_world_normal).cast()); + add_point_to_cache(object_pos.cast(), m_new_point_head_radius, false, (inverse_trsf * m_world_normal).cast()); m_parent.set_as_dirty(); m_wait_for_up_event = true; find_single(); @@ -490,9 +492,9 @@ bool GLGizmoBrimEars::gizmo_event(SLAGizmoEventType action, const Vec2d &mouse_p // mouse wheel up if (action == SLAGizmoEventType::MouseWheelUp) { if (control_down) { - float initial_value = m_new_point_head_diameter; + float initial_value = m_new_point_head_radius; begin_radius_change(initial_value); - m_new_point_head_diameter = std::min(20., initial_value + 0.1); + m_new_point_head_radius = std::min(BRIM_EAR_RADIUS_MAX, initial_value + 0.1f); update_cache_radius(); return true; } @@ -502,9 +504,9 @@ bool GLGizmoBrimEars::gizmo_event(SLAGizmoEventType action, const Vec2d &mouse_p if (action == SLAGizmoEventType::MouseWheelDown) { if (control_down) { - float initial_value = m_new_point_head_diameter; + float initial_value = m_new_point_head_radius; begin_radius_change(initial_value); - m_new_point_head_diameter = std::max(5., initial_value - 0.1); + m_new_point_head_radius = std::max(BRIM_EAR_RADIUS_MIN, initial_value - 0.1f); update_cache_radius(); return true; } @@ -597,18 +599,18 @@ std::vector GLGizmoBrimEars::get_config_options(const std: void GLGizmoBrimEars::begin_radius_change(float initial_value) { - if (m_old_point_head_diameter == 0.f) - m_old_point_head_diameter = initial_value; + if (m_old_point_head_radius == 0.f) + m_old_point_head_radius = initial_value; } void GLGizmoBrimEars::update_cache_radius() { if (render_hover_point) - render_hover_point->brim_point.head_front_radius = m_new_point_head_diameter / 2.f; + render_hover_point->brim_point.head_front_radius = m_new_point_head_radius; for (auto &cache_entry : m_editing_cache) if (cache_entry.selected) { - cache_entry.brim_point.head_front_radius = m_new_point_head_diameter / 2.f; + cache_entry.brim_point.head_front_radius = m_new_point_head_radius; find_single(); update_model_object(); } @@ -617,18 +619,18 @@ void GLGizmoBrimEars::update_cache_radius() void GLGizmoBrimEars::apply_radius_change() { - if (m_old_point_head_diameter == 0.f) return; + if (m_old_point_head_radius == 0.f) return; // momentarily restore the old value to take snapshot for (auto& cache_entry : m_editing_cache) if (cache_entry.selected) - cache_entry.brim_point.head_front_radius = m_old_point_head_diameter / 2.f; - float backup = m_new_point_head_diameter; - m_new_point_head_diameter = m_old_point_head_diameter; - Plater::TakeSnapshot snapshot(wxGetApp().plater(), "Change point head diameter"); - m_new_point_head_diameter = backup; + cache_entry.brim_point.head_front_radius = m_old_point_head_radius; + float backup = m_new_point_head_radius; + m_new_point_head_radius = m_old_point_head_radius; + Plater::TakeSnapshot snapshot(wxGetApp().plater(), "Change brim ear radius"); + m_new_point_head_radius = backup; update_cache_radius(); - m_old_point_head_diameter = 0.f; + m_old_point_head_radius = 0.f; } void GLGizmoBrimEars::on_render_input_window(float x, float y, float bottom_limit) @@ -653,7 +655,7 @@ 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["head_diameter"], m_desc["max_angle"], m_desc["detection_radius"], m_desc["clipping_of_view"], + 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"]}; float widest_text = m_imgui->find_widest_text(text_list); float caption_size = widest_text + space_size + ImGui::GetStyle().WindowPadding.x; @@ -680,11 +682,11 @@ void GLGizmoBrimEars::on_render_input_window(float x, float y, float bottom_limi // - keep updating the head radius during sliding so it is continuosly refreshed in 3D scene // - take correct undo/redo snapshot after the user is done with moving the slider ImGui::AlignTextToFramePadding(); - float initial_value = m_new_point_head_diameter; - m_imgui->text(m_desc["head_diameter"]); + float initial_value = m_new_point_head_radius; + m_imgui->text(m_desc["brim_ear_radius"]); ImGui::SameLine(caption_size); ImGui::PushItemWidth(slider_width); - m_imgui->bbl_slider_float_style("##head_diameter", &m_new_point_head_diameter, 5, 20, "%.1f", 1.0f, true); + m_imgui->bbl_slider_float_style("##brim_ear_radius", &m_new_point_head_radius, BRIM_EAR_RADIUS_MIN, BRIM_EAR_RADIUS_MAX, "%.1f", 1.0f, true); if (m_imgui->get_last_slider_status().clicked) { begin_radius_change(initial_value); } @@ -695,7 +697,7 @@ void GLGizmoBrimEars::on_render_input_window(float x, float y, float bottom_limi } ImGui::SameLine(drag_left_width); ImGui::PushItemWidth(1.5 * slider_icon_width); - ImGui::BBLDragFloat("##head_diameter_input", &m_new_point_head_diameter, 0.05f, 0.0f, 0.0f, "%.1f"); + ImGui::BBLDragFloat("##brim_ear_radius_input", &m_new_point_head_radius, 0.05f, BRIM_EAR_RADIUS_MIN, BRIM_EAR_RADIUS_MAX, "%.1f"); ImGui::Separator(); @@ -910,9 +912,9 @@ void GLGizmoBrimEars::on_stop_dragging() m_point_before_drag = CacheEntry(); } -void GLGizmoBrimEars::on_load(cereal::BinaryInputArchive &ar) { ar(m_new_point_head_diameter, m_editing_cache, m_selection_empty); } +void GLGizmoBrimEars::on_load(cereal::BinaryInputArchive &ar) { ar(m_new_point_head_radius, m_editing_cache, m_selection_empty); } -void GLGizmoBrimEars::on_save(cereal::BinaryOutputArchive &ar) const { ar(m_new_point_head_diameter, m_editing_cache, m_selection_empty); } +void GLGizmoBrimEars::on_save(cereal::BinaryOutputArchive &ar) const { ar(m_new_point_head_radius, m_editing_cache, m_selection_empty); } void GLGizmoBrimEars::select_point(int i) { @@ -920,11 +922,11 @@ void GLGizmoBrimEars::select_point(int i) for (auto &point_and_selection : m_editing_cache) point_and_selection.selected = (i == AllPoints); m_selection_empty = (i == NoPoints); - if (i == AllPoints) m_new_point_head_diameter = m_editing_cache[0].brim_point.head_front_radius * 2.f; + if (i == AllPoints) m_new_point_head_radius = m_editing_cache[0].brim_point.head_front_radius; } else { m_editing_cache[i].selected = true; m_selection_empty = false; - m_new_point_head_diameter = m_editing_cache[i].brim_point.head_front_radius * 2.f; + m_new_point_head_radius = m_editing_cache[i].brim_point.head_front_radius; } } @@ -1011,8 +1013,7 @@ void GLGizmoBrimEars::auto_generate() auto add_point = [this, &trsf, &normal](const Point &p) { Vec3d world_pos = {float(p.x() * SCALING_FACTOR), float(p.y() * SCALING_FACTOR), -0.0001}; Vec3d object_pos = trsf.inverse() * world_pos; - // m_editing_cache.emplace_back(BrimPoint(object_pos.cast(), m_new_point_head_diameter / 2), false, normal); - add_point_to_cache(object_pos.cast(), m_new_point_head_diameter / 2, false, normal); + add_point_to_cache(object_pos.cast(), m_new_point_head_radius, false, normal); }; for (const ExPolygon &ex_poly : m_first_layer) { Polygon out_poly = ex_poly.contour; @@ -1158,8 +1159,11 @@ void GLGizmoBrimEars::reset_all_pick() { std::mapprinters.get_edited_preset().config.option("nozzle_diameter")->get_at(0); - const DynamicPrintConfig &pring_cfg = wxGetApp().preset_bundle->prints.get_edited_preset().config; - return pring_cfg.get_abs_value("initial_layer_line_width", nozzle_diameter) * 16.0f; + const DynamicPrintConfig &print_cfg = wxGetApp().preset_bundle->prints.get_edited_preset().config; + return std::clamp( + float(print_cfg.get_abs_value("initial_layer_line_width", nozzle_diameter) * 8.0), + BRIM_EAR_RADIUS_MIN, + BRIM_EAR_RADIUS_MAX); } ExPolygon GLGizmoBrimEars::make_polygon(BrimPoint point, const Geometry::Transformation &trsf) diff --git a/src/slic3r/GUI/Gizmos/GLGizmoBrimEars.hpp b/src/slic3r/GUI/Gizmos/GLGizmoBrimEars.hpp index 8b1ff2ca62..4e531e6acc 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoBrimEars.hpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoBrimEars.hpp @@ -98,12 +98,12 @@ private: void render_points(const Selection& selection); - float m_new_point_head_diameter; // Size of a new point. + float m_new_point_head_radius; // Radius of a new point. float m_max_angle = 125.f; float m_detection_radius = 1.f; double m_detection_radius_max = .0f; CacheEntry m_point_before_drag; // undo/redo - so we know what state was edited - float m_old_point_head_diameter = 0.; // the same + float m_old_point_head_radius = 0.; // the same mutable std::vector m_editing_cache; // a support point and whether it is currently selectedchanges or undo/redo std::map m_single_brim; ObjectID m_old_mo_id; diff --git a/src/slic3r/GUI/OptionsGroup.cpp b/src/slic3r/GUI/OptionsGroup.cpp index 0f6cdba602..9fb4483883 100644 --- a/src/slic3r/GUI/OptionsGroup.cpp +++ b/src/slic3r/GUI/OptionsGroup.cpp @@ -386,6 +386,7 @@ void OptionsGroup::activate_line(Line& line) } if (label != nullptr && line.label_tooltip != "") label->SetToolTip(line.label_tooltip); + line.label_widget = label; } } @@ -574,6 +575,7 @@ void OptionsGroup::clear(bool destroy_custom_ctrl) for (Line& line : m_lines) { if (line.near_label_widget_win) line.near_label_widget_win = nullptr; + line.label_widget = nullptr; if (line.widget_sizer) { line.widget_sizer->Clear(true); diff --git a/src/slic3r/GUI/OptionsGroup.hpp b/src/slic3r/GUI/OptionsGroup.hpp index 5e1f55dfd8..c808545145 100644 --- a/src/slic3r/GUI/OptionsGroup.hpp +++ b/src/slic3r/GUI/OptionsGroup.hpp @@ -62,6 +62,7 @@ public: widget_t widget {nullptr}; std::function near_label_widget{ nullptr }; wxWindow* near_label_widget_win {nullptr}; + wxStaticText* label_widget {nullptr}; wxSizer* widget_sizer {nullptr}; wxSizer* extra_widget_sizer {nullptr}; //BBS: export the extra colume widget @@ -81,6 +82,14 @@ public: label(_(label)), label_tooltip(_(tooltip)) {} Line() : m_is_separator(true) {} + void set_label(const wxString& new_label) { + label = new_label; + if (label_widget != nullptr) { + label_widget->SetLabel(label + (label.IsEmpty() ? "" : ": ")); + label_widget->Refresh(); + } + } + bool is_separator() const { return m_is_separator; } bool has_only_option(const std::string& opt_key) const { return m_options.size() == 1 && m_options[0].opt_id == opt_key; } diff --git a/src/slic3r/GUI/Tab.cpp b/src/slic3r/GUI/Tab.cpp index 01458b368a..c8fa524ca5 100644 --- a/src/slic3r/GUI/Tab.cpp +++ b/src/slic3r/GUI/Tab.cpp @@ -1738,6 +1738,13 @@ void Tab::toggle_line(const std::string &opt_key, bool toggle, int opt_index) if (line) line->toggle_visible = toggle; }; +void Tab::set_option_label(const std::string &opt_key, const wxString &label, int opt_index) +{ + if (!m_active_page) return; + Line *line = m_active_page->get_line(opt_key, opt_index); + if (line) line->set_label(label); +} + // To be called by custom widgets, load a value into a config, // update the preset selection boxes (the dirty flags) // If value is saved before calling this function, put saved_value = true, @@ -3070,6 +3077,7 @@ void TabPrint::build() optgroup->append_single_option_line("combine_brims", "others_settings_brim#combine-brims"); optgroup->append_single_option_line("brim_ears_max_angle", "others_settings_brim#ear-max-angle"); optgroup->append_single_option_line("brim_ears_detection_length", "others_settings_brim#ear-detection-radius"); + optgroup->append_single_option_line("brim_ears_outer_only"); optgroup = page->new_optgroup(L("Special mode"), L"param_special"); optgroup->append_single_option_line("slicing_mode", "others_settings_special_mode#slicing-mode"); @@ -8935,11 +8943,15 @@ ConfigManipulation Tab::get_config_manipulation() return toggle_line(opt_key, toggle, opt_index >= 0 ? opt_index + 256 : opt_index); }; + auto cb_set_option_label = [this](const t_config_option_key &opt_key, const wxString &label, int opt_index) { + return set_option_label(opt_key, label, opt_index >= 0 ? opt_index + 256 : opt_index); + }; + auto cb_value_change = [this](const std::string& opt_key, const boost::any& value) { return on_value_change(opt_key, value); }; - return ConfigManipulation(load_config, cb_toggle_field, cb_toggle_line, cb_value_change, nullptr, this); + return ConfigManipulation(load_config, cb_toggle_field, cb_toggle_line, cb_value_change, nullptr, this, cb_set_option_label); } diff --git a/src/slic3r/GUI/Tab.hpp b/src/slic3r/GUI/Tab.hpp index 9be9bc17f8..7187aff467 100644 --- a/src/slic3r/GUI/Tab.hpp +++ b/src/slic3r/GUI/Tab.hpp @@ -402,6 +402,7 @@ public: Field* get_field(const t_config_option_key &opt_key, Page** selected_page, int opt_index = -1); void toggle_option(const std::string &opt_key, bool toggle, int opt_index = -1); void toggle_line(const std::string &opt_key, bool toggle, int opt_index = -1); // BBS: hide some line + void set_option_label(const std::string &opt_key, const wxString &label, int opt_index = -1); wxSizer* description_line_widget(wxWindow* parent, ogStaticText** StaticText, wxString text = wxEmptyString); bool current_preset_is_dirty() const; bool saved_preset_is_dirty() const; diff --git a/tests/fff_print/test_skirt_brim.cpp b/tests/fff_print/test_skirt_brim.cpp index 3f63d3de5f..17a79a7828 100644 --- a/tests/fff_print/test_skirt_brim.cpp +++ b/tests/fff_print/test_skirt_brim.cpp @@ -4,6 +4,7 @@ #include "libslic3r/Config.hpp" #include "libslic3r/Geometry.hpp" #include "libslic3r/Geometry/ConvexHull.hpp" +#include "libslic3r/Layer.hpp" #include @@ -32,6 +33,30 @@ static size_t brim_loop_count(Print &print) return n; } +static bool brim_enters_first_layer_hole(Print &print) +{ + const PrintObject *object = print.get_object(0); + Polygons holes; + for (const ExPolygon &slice : object->layers().front()->lslices) + holes.insert(holes.end(), slice.holes.begin(), slice.holes.end()); + + const Vec3d plate_origin = print.get_plate_origin(); + Point shift = object->instances().front().shift_without_plate_offset(); + shift += Point(scaled(plate_origin.x()), scaled(plate_origin.y())); + for (Polygon &hole : holes) + hole.translate(shift); + + for (const auto &kv : print.get_brimMap()) { + Polylines brim_paths; + kv.second.collect_polylines(brim_paths); + for (const Polyline &path : brim_paths) + for (const Point &point : path.points) + if (contains(holes, point, false)) + return true; + } + return false; +} + // The span is skirt_height layers, or every layer when a draft shield is on (forced even at // height 0); per-object skirts are rejected in By object printing (no room between objects). TEST_CASE("Skirt is emitted once per layer it spans", "[SkirtBrim]") @@ -225,6 +250,131 @@ TEST_CASE("Brim ears appear only at corners within the max angle", "[SkirtBrim]" } } +TEST_CASE("Outer-only brim ears stay out of model holes", "[SkirtBrim]") +{ + const bool outer_only = GENERATE(false, true); + DYNAMIC_SECTION("brim_ears_outer_only=" << outer_only) { + Print print; + init_and_process_print({ TestMesh::cube_with_concave_hole }, print, { + { "skirt_loops", 0 }, + { "brim_type", "brim_ears" }, + { "brim_width", 2 }, + { "brim_ears_max_angle", 125 }, + { "brim_ears_detection_length", 0 }, + { "brim_ears_outer_only", outer_only }, + { "initial_layer_line_width", 0.5 }, + }); + + REQUIRE(brim_loop_count(print) > 0); + CHECK(brim_enters_first_layer_hole(print) != outer_only); + } +} + +TEST_CASE("Painted brim ear radius controls sliced size", "[SkirtBrim]") +{ + constexpr double ear_radius = 10.0; + + DynamicPrintConfig config = DynamicPrintConfig::full_print_config(); + config.set_deserialize_strict({ + { "skirt_loops", 0 }, + { "brim_type", "painted" }, + { "brim_width", 15 }, + { "brim_object_gap", 0.1 }, + { "brim_ears_outer_only", true }, + { "initial_layer_line_width", 0.5 }, + }); + + Print print; + Model model; + init_print({ cube(20) }, print, model, config); + print.process(); + + const PrintObject *object = print.get_object(0); + REQUIRE(!object->layers().front()->lslices.empty()); + const Point ear_center = object->layers().front()->lslices.front().contour.points.front(); + + Transform3d model_transform = model.objects.front()->instances.front()->get_transformation().get_matrix_no_offset(); + const Point ¢er_offset = object->center_offset(); + model_transform = model_transform.pretranslate( + Vec3d(-unscale(center_offset.x()), -unscale(center_offset.y()), 0)); + Vec3d model_pos = model_transform.inverse() * + Vec3d(unscale(ear_center.x()), unscale(ear_center.y()), 0); + model_pos.z() = model.objects.front()->raw_mesh_bounding_box().min.z() - 0.0001; + model.objects.front()->brim_points = { + BrimPoint(model_pos.cast(), float(ear_radius)), + }; + + print.apply(model, config); + print.process(); + + const Vec3d plate_origin = print.get_plate_origin(); + Point path_center = ear_center + object->instances().front().shift_without_plate_offset(); + path_center += Point(scaled(plate_origin.x()), scaled(plate_origin.y())); + + double max_path_radius = 0.0; + for (const auto &kv : print.get_brimMap()) { + Polylines brim_paths; + kv.second.collect_polylines(brim_paths); + for (const Polyline &path : brim_paths) + for (const Point &point : path.points) + max_path_radius = std::max(max_path_radius, unscale((point - path_center).cast().norm())); + } + + REQUIRE(max_path_radius > 0.0); + INFO("Outermost painted-ear path radius: " << max_path_radius << " mm"); + CHECK(max_path_radius > ear_radius - 0.5); + CHECK(max_path_radius < ear_radius); +} + +TEST_CASE("Outer-only painted brim ears stay out of model holes", "[SkirtBrim]") +{ + DynamicPrintConfig config = DynamicPrintConfig::full_print_config(); + config.set_deserialize_strict({ + { "skirt_loops", 0 }, + { "brim_type", "painted" }, + { "brim_ears_outer_only", true }, + { "initial_layer_line_width", 0.5 }, + }); + + Print print; + Model model; + init_print({ TestMesh::cube_with_concave_hole }, print, model, config); + + // Slice once to obtain exact outer and inner contour points in print + // coordinates, then express them in the model coordinates painted ears store. + print.process(); + const PrintObject *object = print.get_object(0); + REQUIRE(!object->layers().front()->lslices.empty()); + REQUIRE(!object->layers().front()->lslices.front().holes.empty()); + + Transform3d model_transform = model.objects.front()->instances.front()->get_transformation().get_matrix_no_offset(); + const Point ¢er_offset = object->center_offset(); + model_transform = model_transform.pretranslate( + Vec3d(-unscale(center_offset.x()), -unscale(center_offset.y()), 0)); + const double bottom_z = model.objects.front()->raw_mesh_bounding_box().min.z() - 0.0001; + auto painted_point = [&model_transform, bottom_z](const Point &point) { + Vec3d model_pos = model_transform.inverse() * + Vec3d(unscale(point.x()), unscale(point.y()), 0); + model_pos.z() = bottom_z; + return BrimPoint(model_pos.cast(), 3.f); + }; + + const ExPolygon &first_slice = object->layers().front()->lslices.front(); + Polygon inner_contour = first_slice.holes.front(); + inner_contour.reverse(); + const Points inner_ear_points = inner_contour.concave_points(55. * PI / 180.); + REQUIRE(!inner_ear_points.empty()); + model.objects.front()->brim_points = { + painted_point(first_slice.contour.points.front()), + painted_point(inner_ear_points.front()), + }; + print.apply(model, config); + print.process(); + + REQUIRE(brim_loop_count(print) > 0); + CHECK_FALSE(brim_enters_first_layer_hole(print)); +} + SCENARIO("Skirt has the configured number of loops", "[SkirtBrim]") { GIVEN("20mm cube and default config") { WHEN("skirt_loops is set to 2") {