diff --git a/src/libslic3r/TriangleSelector.cpp b/src/libslic3r/TriangleSelector.cpp index b12b0bbc51..ee165f5247 100644 --- a/src/libslic3r/TriangleSelector.cpp +++ b/src/libslic3r/TriangleSelector.cpp @@ -1448,7 +1448,12 @@ void TriangleSelector::get_facets(std::vector& facets_per_ { facets_per_type.clear(); - for (int type = (int)EnforcerBlockerType::NONE; type <= (int)EnforcerBlockerType::ExtruderMax; type++) { + int max_state = int(EnforcerBlockerType::NONE); + for (const Triangle &tr : m_triangles) + if (tr.valid() && !tr.is_split()) + max_state = std::max(max_state, int(tr.get_state())); + + for (int type = int(EnforcerBlockerType::NONE); type <= max_state; ++type) { facets_per_type.emplace_back(); indexed_triangle_set& its = facets_per_type.back(); std::vector vertex_map(m_vertices.size(), -1); @@ -1642,9 +1647,10 @@ void TriangleSelector::get_seed_fill_contour_recursive(const int facet_idx, cons TriangleSelector::TriangleSplittingData TriangleSelector::serialize() const { // Each original triangle of the mesh is assigned a number encoding its state - // or how it is split. Each triangle is encoded by 4 bits (xxyy) or 8 bits (zzzzxxyy): + // or how it is split. Each triangle is encoded by 4 bits (xxyy) or by + // 4 bits plus one or more extension nibbles: // leaf triangle: xx = EnforcerBlockerType (Only values 0, 1, and 2. Value 3 is used as an indicator for additional 4 bits.), yy = 0 - // leaf triangle: xx = 0b11, yy = 0b00, zzzz = EnforcerBlockerType (subtracted by 3) + // leaf triangle: xx = 0b11, yy = 0b00, zzzz... = EnforcerBlockerType (subtracted by 3) in base-15 chunks // non-leaf: xx = special side, yy = number of split sides // These are bitwise appended and formed into one 64-bit integer. @@ -1687,14 +1693,17 @@ TriangleSelector::TriangleSplittingData TriangleSelector::serialize() const { data.used_states[n] = true; if (n >= 3) { - assert(n <= 16); - if (n <= 16) { - // Store "11" plus 4 bits of (n-3). - data.bitstream.insert(data.bitstream.end(), { true, true }); - n -= 3; + // Store "11" plus one or more 4-bit chunks of (n - 3), where + // 0b1111 indicates that another chunk follows. + data.bitstream.insert(data.bitstream.end(), { true, true }); + n -= 3; + while (n >= 15) { for (size_t bit_idx = 0; bit_idx < 4; ++bit_idx) - data.bitstream.push_back(n & (uint64_t(0b0001) << bit_idx)); + data.bitstream.push_back(uint64_t(0b1111) & (uint64_t(0b0001) << bit_idx)); + n -= 15; } + for (size_t bit_idx = 0; bit_idx < 4; ++bit_idx) + data.bitstream.push_back(n & (uint64_t(0b0001) << bit_idx)); } else { // Simple case, compatible with PrusaSlicer 2.3.1 and older for storing paint on supports and seams. // Store 2 bits of n. @@ -1890,7 +1899,16 @@ void TriangleSelector::TriangleSplittingData::update_used_states(const size_t bi if (const bool is_split = (code & 0b11) != 0; is_split) continue; - const uint8_t facet_state = (code & 0b1100) == 0b1100 ? read_next_nibble() + 3 : code >> 2; + size_t facet_state = code >> 2; + if ((code & 0b1100) == 0b1100) { + size_t extension_count = 0; + size_t next_code = read_next_nibble(); + while (next_code == 0b1111) { + ++extension_count; + next_code = read_next_nibble(); + } + facet_state = next_code + 15 * extension_count + 3; + } assert(facet_state < this->used_states.size()); if (facet_state >= this->used_states.size()) continue; @@ -1920,9 +1938,19 @@ bool TriangleSelector::has_facets(const TriangleSplittingData &data, const Enfor auto num_children_or_state = [&next_nibble]() -> int { int code = next_nibble(); int num_of_split_sides = code & 0b11; - return num_of_split_sides == 0 ? - ((code & 0b1100) == 0b1100 ? next_nibble() + 3 : code >> 2) : - - num_of_split_sides - 1; + if (num_of_split_sides != 0) + return - num_of_split_sides - 1; + + if ((code & 0b1100) != 0b1100) + return code >> 2; + + int extension_count = 0; + int next_code = next_nibble(); + while (next_code == 0b1111) { + ++extension_count; + next_code = next_nibble(); + } + return next_code + 15 * extension_count + 3; }; int state = num_children_or_state(); diff --git a/src/libslic3r/TriangleSelector.hpp b/src/libslic3r/TriangleSelector.hpp index 37f4b6c3b1..e433b566e3 100644 --- a/src/libslic3r/TriangleSelector.hpp +++ b/src/libslic3r/TriangleSelector.hpp @@ -10,14 +10,15 @@ namespace Slic3r { -enum class EnforcerBlockerType : int8_t { +enum class EnforcerBlockerType : int16_t { // Maximum is 3. The value is serialized in TriangleSelector into 2 bits. NONE = 0, ENFORCER = 1, BLOCKER = 2, // For the fuzzy skin, we use just two values (NONE and FUZZY_SKIN). FUZZY_SKIN = ENFORCER, - // Maximum is 15. The value is serialized in TriangleSelector into 6 bits using a 2 bit prefix code. + // Extruder states are serialized using a 2-bit prefix plus one or more 4-bit nibbles. + // This allows more than 16 painted states while keeping backward compatibility. Extruder1 = ENFORCER, Extruder2 = BLOCKER, Extruder3, @@ -34,7 +35,7 @@ enum class EnforcerBlockerType : int8_t { Extruder14, Extruder15, Extruder16, - ExtruderMax = Extruder16 + ExtruderMax = 255 }; // Type alias for the state mapping array to improve code readability diff --git a/src/slic3r/GUI/Gizmos/GLGizmoMmuSegmentation.cpp b/src/slic3r/GUI/Gizmos/GLGizmoMmuSegmentation.cpp index fe9bcd61e7..1a8902544f 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoMmuSegmentation.cpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoMmuSegmentation.cpp @@ -33,7 +33,7 @@ static inline void show_notification_extruders_limit_exceeded() void GLGizmoMmuSegmentation::on_opening() { - if (wxGetApp().filaments_cnt() > int(GLGizmoMmuSegmentation::EXTRUDERS_LIMIT)) + if (get_extruders_colors().size() > GLGizmoMmuSegmentation::EXTRUDERS_LIMIT) show_notification_extruders_limit_exceeded(); } @@ -89,10 +89,11 @@ static std::vector get_extruder_id_for_volumes(const ModelObject &model_obj return extruders_idx; } -void GLGizmoMmuSegmentation::init_extruders_data() +void GLGizmoMmuSegmentation::init_extruders_data(const std::vector &extruder_colors) { - m_extruders_colors = get_extruders_colors(); - m_selected_extruder_idx = 0; + const size_t old_selected = m_selected_extruder_idx; + m_extruders_colors = extruder_colors; + m_selected_extruder_idx = m_extruders_colors.empty() ? 0 : std::min(old_selected, m_extruders_colors.size() - 1); // keep remap table consistent with current extruder count m_extruder_remap.resize(m_extruders_colors.size()); @@ -100,6 +101,11 @@ void GLGizmoMmuSegmentation::init_extruders_data() m_extruder_remap[i] = i; } +void GLGizmoMmuSegmentation::init_extruders_data() +{ + init_extruders_data(get_extruders_colors()); +} + bool GLGizmoMmuSegmentation::on_init() { // BBS @@ -188,18 +194,20 @@ void GLGizmoMmuSegmentation::data_changed(bool is_serializing) return; ModelObject* model_object = m_c->selection_info()->model_object(); - int prev_extruders_count = int(m_extruders_colors.size()); - if (prev_extruders_count != wxGetApp().filaments_cnt()) { - if (wxGetApp().filaments_cnt() > int(GLGizmoMmuSegmentation::EXTRUDERS_LIMIT)) + const std::vector current_extruder_colors = get_extruders_colors(); + const int prev_extruders_count = int(m_extruders_colors.size()); + const int current_extruders_count = int(current_extruder_colors.size()); + if (prev_extruders_count != current_extruders_count) { + if (current_extruder_colors.size() > GLGizmoMmuSegmentation::EXTRUDERS_LIMIT) show_notification_extruders_limit_exceeded(); - this->init_extruders_data(); + this->init_extruders_data(current_extruder_colors); // Reinitialize triangle selectors because of change of extruder count need also change the size of GLIndexedVertexArray - if (prev_extruders_count != wxGetApp().filaments_cnt()) + if (prev_extruders_count != current_extruders_count) this->init_model_triangle_selectors(); } - else if (get_extruders_colors() != m_extruders_colors) { - this->init_extruders_data(); + else if (current_extruder_colors != m_extruders_colors) { + this->init_extruders_data(current_extruder_colors); this->update_triangle_selectors_colors(); } else if (model_object != nullptr && get_extruder_id_for_volumes(*model_object) != m_volumes_extruder_idxs) { @@ -251,14 +259,14 @@ static void render_extruders_combo(const std::string& label, size_t& selection_idx) { assert(!extruders_colors.empty()); - assert(extruders_colors.size() == extruders_colors.size()); + assert(extruders.size() == extruders_colors.size()); size_t selection_out = selection_idx; // It is necessary to use BeginGroup(). Otherwise, when using SameLine() is called, then other items will be drawn inside the combobox. ImGui::BeginGroup(); ImVec2 combo_pos = ImGui::GetCursorScreenPos(); if (ImGui::BeginCombo(label.c_str(), "")) { - for (size_t extruder_idx = 0; extruder_idx < std::min(extruders.size(), GLGizmoMmuSegmentation::EXTRUDERS_LIMIT); ++extruder_idx) { + for (size_t extruder_idx = 0; extruder_idx < extruders.size(); ++extruder_idx) { ImGui::PushID(int(extruder_idx)); ImVec2 start_position = ImGui::GetCursorScreenPos(); @@ -371,6 +379,9 @@ void GLGizmoMmuSegmentation::on_render_input_window(float x, float y, float bott const float buttons_width = remove_btn_width + filter_btn_width + remap_btn_width + m_imgui->scaled(2.f); const float minimal_slider_width = m_imgui->scaled(4.f); const float color_button_width = m_imgui->calc_text_size(std::string_view{""}).x + m_imgui->scaled(1.75f); + const size_t total_filament_count = m_extruders_colors.size(); + const std::string max_filament_label = std::to_string(std::max(total_filament_count, 1)); + const ImVec2 max_filament_label_size = ImGui::CalcTextSize(max_filament_label.c_str(), NULL, true); float caption_max = 0.f; float total_text_max = 0.f; @@ -390,7 +401,7 @@ void GLGizmoMmuSegmentation::on_render_input_window(float x, float y, float bott float window_width = minimal_slider_width + sliders_left_width + slider_icon_width; const int max_filament_items_per_line = 8; const float empty_button_width = m_imgui->calc_button_size("").x; - const float filament_item_width = empty_button_width + m_imgui->scaled(1.5f); + const float filament_item_width = std::max(empty_button_width, max_filament_label_size.x + m_imgui->scaled(1.4f)) + m_imgui->scaled(1.5f); window_width = std::max(window_width, total_text_max); window_width = std::max(window_width, buttons_width); @@ -410,17 +421,15 @@ void GLGizmoMmuSegmentation::on_render_input_window(float x, float y, float bott m_imgui->text(m_desc.at("filaments")); float start_pos_x = ImGui::GetCursorPos().x; - const ImVec2 max_label_size = ImGui::CalcTextSize("99", NULL, true); - const float item_spacing = m_imgui->scaled(0.8f); - size_t n_extruder_colors = std::min((size_t)EnforcerBlockerType::ExtruderMax, m_extruders_colors.size()); - for (int extruder_idx = 0; extruder_idx < n_extruder_colors; extruder_idx++) { + size_t n_extruder_colors = std::min(GLGizmoMmuSegmentation::EXTRUDERS_LIMIT, m_extruders_colors.size()); + for (size_t extruder_idx = 0; extruder_idx < n_extruder_colors; ++extruder_idx) { const ColorRGBA &extruder_color = m_extruders_colors[extruder_idx]; ImVec4 color_vec = ImGuiWrapper::to_ImVec4(extruder_color); std::string color_label = std::string("##extruder color ") + std::to_string(extruder_idx); std::string item_text = std::to_string(extruder_idx + 1); const ImVec2 label_size = ImGui::CalcTextSize(item_text.c_str(), NULL, true); - const ImVec2 button_size(max_label_size.x + m_imgui->scaled(0.5f),0.f); + const ImVec2 button_size(max_filament_label_size.x + m_imgui->scaled(0.5f), 0.f); float button_offset = start_pos_x; if (extruder_idx % max_filament_items_per_line != 0) { @@ -449,7 +458,12 @@ void GLGizmoMmuSegmentation::on_render_input_window(float x, float y, float bott color_button_high = ImGui::GetCursorPos().y - color_button - 2.0; if (color_picked) { m_selected_extruder_idx = extruder_idx; } - if (extruder_idx < 16 && ImGui::IsItemHovered()) m_imgui->tooltip(_L("Shortcut Key ") + std::to_string(extruder_idx + 1), max_tooltip_width); + if (ImGui::IsItemHovered()) { + if (extruder_idx < 9) + m_imgui->tooltip(_L("Shortcut Key ") + std::to_string(extruder_idx + 1), max_tooltip_width); + else + m_imgui->tooltip(wxString::Format(_L("Filament %d"), int(extruder_idx + 1)), max_tooltip_width); + } // draw filament id float gray = 0.299 * extruder_color.r() + 0.587 * extruder_color.g() + 0.114 * extruder_color.b(); @@ -465,6 +479,21 @@ void GLGizmoMmuSegmentation::on_render_input_window(float x, float y, float bott //ImGui::NewLine(); ImGui::Dummy(ImVec2(0.0f, ImGui::GetFontSize() * 0.1)); + if (n_extruder_colors > 0) { + int selected_filament = int(m_selected_extruder_idx) + 1; + ImGui::AlignTextToFramePadding(); + m_imgui->text(_L("Selected filament")); + ImGui::SameLine(); + ImGui::PushItemWidth(m_imgui->scaled(4.5f)); + if (ImGui::InputInt("##selected_filament", &selected_filament, 1, 10, ImGuiInputTextFlags_CharsDecimal)) { + selected_filament = std::clamp(selected_filament, 1, int(n_extruder_colors)); + m_selected_extruder_idx = size_t(selected_filament - 1); + } + ImGui::SameLine(); + m_imgui->text(wxString::Format(_L("/ %d"), int(n_extruder_colors))); + ImGui::Dummy(ImVec2(0.0f, ImGui::GetFontSize() * 0.1)); + } + m_imgui->text(m_desc.at("tool_type")); std::array tool_ids; @@ -857,9 +886,10 @@ void GLGizmoMmuSegmentation::update_from_model_object(bool first_update) // Extruder colors need to be reloaded before calling init_model_triangle_selectors to render painted triangles // using colors from loaded 3MF and not from printer profile in Slicer. + const std::vector current_extruder_colors = get_extruders_colors(); if (int prev_extruders_count = int(m_extruders_colors.size()); - prev_extruders_count != wxGetApp().filaments_cnt() || get_extruders_colors() != m_extruders_colors) - this->init_extruders_data(); + prev_extruders_count != int(current_extruder_colors.size()) || current_extruder_colors != m_extruders_colors) + this->init_extruders_data(current_extruder_colors); this->init_model_triangle_selectors(); } @@ -907,7 +937,7 @@ wxString GLGizmoMmuSegmentation::handle_snapshot_action_name(bool shift_down, GL if (shift_down) action_name = _L("Remove painted color"); else { - action_name = GUI::format(_L("Painted using: Filament %1%"), m_selected_extruder_idx); + action_name = GUI::format(_L("Painted using: Filament %1%"), m_selected_extruder_idx + 1); } return action_name; } @@ -993,15 +1023,20 @@ void GLGizmoMmuSegmentation::render_filament_remap_ui(float window_width, float { size_t n_extr = std::min((size_t)EnforcerBlockerType::ExtruderMax, m_extruders_colors.size()); - const ImVec2 max_label_size = ImGui::CalcTextSize("99", NULL, true); + const std::string max_label = std::to_string(std::max(n_extr, 1)); + const ImVec2 max_label_size = ImGui::CalcTextSize(max_label.c_str(), NULL, true); const ImVec2 button_size(max_label_size.x + m_imgui->scaled(0.5f), 0.f); + const int max_items_per_line = 8; + const float item_width = button_size.x + m_imgui->scaled(1.5f); + const float start_pos_x = ImGui::GetCursorPosX(); for (int src = 0; src < (int)n_extr; ++src) { - const ColorRGBA &src_col = m_extruders_colors[src]; // keep for text contrast const ColorRGBA &dst_col = m_extruders_colors[m_extruder_remap[src]]; ImVec4 col_vec = ImGuiWrapper::to_ImVec4(dst_col); - if (src) ImGui::SameLine(); + if (src % max_items_per_line != 0) { + ImGui::SameLine(start_pos_x + item_width * (src % max_items_per_line)); + } std::string btn_id = "##remap_src_" + std::to_string(src); ImGuiColorEditFlags flags = ImGuiColorEditFlags_NoAlpha | ImGuiColorEditFlags_NoInputs | @@ -1062,11 +1097,13 @@ void GLGizmoMmuSegmentation::render_filament_remap_ui(float window_width, float ImGui::PushStyleColor(ImGuiCol_Border, m_is_dark_mode ? ImVec4(0.5f, 0.5f, 0.5f, 1.0f) : ImVec4(0.6f, 0.6f, 0.6f, 1.0f)); if (ImGui::BeginPopup(pop_id.c_str())) { + const float popup_start_pos_x = ImGui::GetCursorPosX(); for (int dst = 0; dst < (int)n_extr; ++dst) { const ColorRGBA &dst_col_popup = m_extruders_colors[dst]; ImVec4 dst_vec = ImGuiWrapper::to_ImVec4(dst_col_popup); - if (dst) ImGui::SameLine(); + if (dst % max_items_per_line != 0) + ImGui::SameLine(popup_start_pos_x + item_width * (dst % max_items_per_line)); std::string dst_btn = "##dst_" + std::to_string(src) + "_" + std::to_string(dst); // Apply same styling to destination buttons diff --git a/src/slic3r/GUI/Gizmos/GLGizmoMmuSegmentation.hpp b/src/slic3r/GUI/Gizmos/GLGizmoMmuSegmentation.hpp index 00017f2a93..7fea5d2518 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoMmuSegmentation.hpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoMmuSegmentation.hpp @@ -71,11 +71,8 @@ public: void data_changed(bool is_serializing) override; - // TriangleSelector::serialization/deserialization has a limit to store 19 different states. - // EXTRUDER_LIMIT + 1 states are used to storing the painting because also uncolored triangles are stored. - // When increasing EXTRUDER_LIMIT, it needs to ensure that TriangleSelector::serialization/deserialization - // will be also extended to support additional states, requiring at least one state to remain free out of 19 states. - static const constexpr size_t EXTRUDERS_LIMIT = 16; + // Keep this in sync with the shared triangle-selector state range. + static const constexpr size_t EXTRUDERS_LIMIT = static_cast(EnforcerBlockerType::ExtruderMax); const float get_cursor_radius_min() const override { return CursorRadiusMin; } @@ -136,6 +133,7 @@ private: // BBS void update_triangle_selectors_colors(); void init_extruders_data(); + void init_extruders_data(const std::vector &extruder_colors); // Filament remapping methods void remap_filament_assignments(); diff --git a/src/slic3r/GUI/Gizmos/GLGizmoPainterBase.cpp b/src/slic3r/GUI/Gizmos/GLGizmoPainterBase.cpp index 319f3a57cf..a26b251911 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoPainterBase.cpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoPainterBase.cpp @@ -1283,11 +1283,15 @@ void TriangleSelectorPatch::render(ImGuiWrapper* imgui, const Transform3d& matri ColorRGBA color; if (patch.is_fragment() && !patch.neighbor_types.empty()) { size_t color_idx = (size_t)*patch.neighbor_types.begin(); + if (color_idx >= m_ebt_colors.size()) + continue; color = m_ebt_colors[color_idx]; color.a(0.85); } else { size_t color_idx = (size_t)patch.type; + if (color_idx >= m_ebt_colors.size()) + continue; color = m_ebt_colors[color_idx]; } //to make black not too hard too see @@ -1303,7 +1307,7 @@ void TriangleSelectorPatch::render(ImGuiWrapper* imgui, const Transform3d& matri void TriangleSelectorPatch::update_triangles_per_type() { //BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(", enter"); - m_triangle_patches.resize((int)EnforcerBlockerType::ExtruderMax + 1); + m_triangle_patches.resize(m_ebt_colors.size()); for (int i = 0; i < m_triangle_patches.size(); i++) { auto& patch = m_triangle_patches[i]; patch.type = (EnforcerBlockerType)i; @@ -1317,6 +1321,8 @@ void TriangleSelectorPatch::update_triangles_per_type() continue; int state = (int)triangle.get_state(); + if (state < 0 || state >= int(m_triangle_patches.size())) + continue; auto& patch = m_triangle_patches[state]; //patch.triangle_indices.insert(patch.triangle_indices.end(), triangle.verts_idxs.begin(), triangle.verts_idxs.end()); for (int i = 0; i < 3; ++i) { diff --git a/tests/libslic3r/CMakeLists.txt b/tests/libslic3r/CMakeLists.txt index 38052e4b6b..33485d352a 100644 --- a/tests/libslic3r/CMakeLists.txt +++ b/tests/libslic3r/CMakeLists.txt @@ -11,6 +11,7 @@ add_executable(${_TEST_NAME}_tests test_elephant_foot_compensation.cpp test_geometry.cpp test_mixed_filament.cpp + test_triangle_selector.cpp test_local_z_order_optimizer.cpp test_placeholder_parser.cpp test_polygon.cpp diff --git a/tests/libslic3r/test_triangle_selector.cpp b/tests/libslic3r/test_triangle_selector.cpp new file mode 100644 index 0000000000..dc95ee5300 --- /dev/null +++ b/tests/libslic3r/test_triangle_selector.cpp @@ -0,0 +1,39 @@ +#include + +#include "libslic3r/TriangleMesh.hpp" +#include "libslic3r/TriangleSelector.hpp" + +using namespace Slic3r; + +TEST_CASE("Triangle selector round-trips painted states above sixteen", "[TriangleSelector][MMUPaint]") +{ + indexed_triangle_set its; + its.vertices = { + Vec3f(0.f, 0.f, 0.f), + Vec3f(1.f, 0.f, 0.f), + Vec3f(0.f, 1.f, 0.f), + }; + its.indices = { + stl_triangle_vertex_indices(0, 1, 2), + }; + + TriangleMesh mesh(its); + TriangleSelector selector(mesh); + + constexpr int painted_state = 120; + selector.set_facet(0, static_cast(painted_state)); + + auto data = selector.serialize(); + REQUIRE_FALSE(data.triangles_to_split.empty()); + REQUIRE(data.used_states.size() > painted_state); + CHECK(data.used_states[painted_state]); + + data.reset_used_states(); + data.update_used_states(size_t(data.triangles_to_split.front().bitstream_start_idx)); + CHECK(data.used_states[painted_state]); + CHECK(TriangleSelector::has_facets(data, static_cast(painted_state))); + + TriangleSelector restored(mesh); + restored.deserialize(data, true, static_cast(painted_state)); + CHECK(restored.has_facets(static_cast(painted_state))); +}