diff --git a/src/libslic3r/TriangleSelector.cpp b/src/libslic3r/TriangleSelector.cpp index 6792f1aece..45766a984e 100644 --- a/src/libslic3r/TriangleSelector.cpp +++ b/src/libslic3r/TriangleSelector.cpp @@ -3,6 +3,8 @@ #include #include +#include +#include #ifndef NDEBUG // #define EXPENSIVE_DEBUG_CHECKS @@ -1256,6 +1258,22 @@ void TriangleSelector::garbage_collect() m_free_vertices_head = -1; } +void TriangleSelector::remap_triangle_state(const EnforcerBlockerStateMap& state_map) +{ + if (m_triangles.empty()) + return; + + tbb::parallel_for(tbb::blocked_range(0, m_triangles.size()), [this, &state_map](const tbb::blocked_range& range) { + for (size_t i = range.begin(); i != range.end(); ++i) { + Triangle& tr = m_triangles[i]; + if (tr.valid()) { + const auto current_state = static_cast(tr.get_state()); + tr.set_state(state_map[current_state]); + } + } + }); +} + TriangleSelector::TriangleSelector(const TriangleMesh& mesh, float edge_limit) : m_mesh{mesh}, m_neighbors(its_face_neighbors(mesh.its)), m_face_normals(its_face_normals(mesh.its)), m_edge_limit(edge_limit) { diff --git a/src/libslic3r/TriangleSelector.hpp b/src/libslic3r/TriangleSelector.hpp index 464804c276..fb606facdd 100644 --- a/src/libslic3r/TriangleSelector.hpp +++ b/src/libslic3r/TriangleSelector.hpp @@ -37,6 +37,9 @@ enum class EnforcerBlockerType : int8_t { ExtruderMax = Extruder16 }; +// Type alias for the state mapping array to improve code readability +using EnforcerBlockerStateMap = std::array; + // Following class holds information about selected triangles. It also has power // to recursively subdivide the triangles and make the selection finer. class TriangleSelector @@ -344,6 +347,10 @@ public: // Remove all unnecessary data. void garbage_collect(); + // Orca: remap the state of triangles according to the state_map + void remap_triangle_state(const EnforcerBlockerStateMap& state_map); + + // Store the division trees in compact form (a long stream of bits for each triangle of the original mesh). // First vector contains pairs of (triangle index, first bit in the second vector). TriangleSplittingData serialize() const; @@ -416,10 +423,17 @@ protected: // or index of a vertex shared by the two split edges (for number_of_splits == 2). // For number_of_splits == 3, special_side_idx is always zero. char special_side_idx { 0 }; - EnforcerBlockerType state; bool m_selected_by_seed_fill : 1; // Is this triangle valid or marked to be removed? bool m_valid : 1; + + // Orca: + // IMPORTANT: `state` is intentionally placed after all other small members + // to prevent compilers from packing it in a way that would create + // data races during parallel processing. A write to `state` could + // otherwise become a non-atomic read-modify-write on a memory word + // that also contains other (bit-field) members, causing race conditions. + EnforcerBlockerType state; }; struct Vertex { diff --git a/src/slic3r/GUI/Gizmos/GLGizmoMmuSegmentation.cpp b/src/slic3r/GUI/Gizmos/GLGizmoMmuSegmentation.cpp index cc43025df2..4e216bd81c 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoMmuSegmentation.cpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoMmuSegmentation.cpp @@ -89,8 +89,13 @@ static std::vector get_extruder_id_for_volumes(const ModelObject &model_obj void GLGizmoMmuSegmentation::init_extruders_data() { - m_extruders_colors = get_extruders_colors(); + m_extruders_colors = get_extruders_colors(); m_selected_extruder_idx = 0; + + // keep remap table consistent with current extruder count + m_extruder_remap.resize(m_extruders_colors.size()); + for (size_t i = 0; i < m_extruder_remap.size(); ++i) + m_extruder_remap[i] = i; } bool GLGizmoMmuSegmentation::on_init() @@ -137,6 +142,11 @@ bool GLGizmoMmuSegmentation::on_init() m_desc["toggle_wireframe_caption"] = _L("Alt + Shift + Enter"); m_desc["toggle_wireframe"] = _L("Toggle Wireframe"); + // Filament remapping descriptions + m_desc["perform_remap"] = _L("Remap filaments"); + m_desc["remap"] = _L("Remap"); + m_desc["cancel_remap"] = _L("Cancel"); + init_extruders_data(); return true; @@ -349,7 +359,8 @@ void GLGizmoMmuSegmentation::on_render_input_window(float x, float y, float bott const float remove_btn_width = m_imgui->calc_text_size(m_desc.at("remove_all")).x + m_imgui->scaled(1.f); const float filter_btn_width = m_imgui->calc_text_size(m_desc.at("perform")).x + m_imgui->scaled(1.f); - const float buttons_width = remove_btn_width + filter_btn_width + m_imgui->scaled(1.f); + const float remap_btn_width = m_imgui->calc_text_size(m_desc.at("perform_remap")).x + m_imgui->scaled(1.f); + 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); @@ -437,9 +448,9 @@ void GLGizmoMmuSegmentation::on_render_input_window(float x, float y, float bott ImGui::SameLine(button_offset + (button_size.x - label_size.x) / 2.f); ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, {10.0,15.0}); if (gray * 255.f < 80.f) - ImGui::TextColored(ImVec4(1.0f, 1.0f, 1.0f, 1.0f), item_text.c_str()); + ImGui::TextColored(ImVec4(1.0f, 1.0f, 1.0f, 1.0f), "%s", item_text.c_str()); else - ImGui::TextColored(ImVec4(0.0f, 0.0f, 0.0f, 1.0f), item_text.c_str()); + ImGui::TextColored(ImVec4(0.0f, 0.0f, 0.0f, 1.0f), "%s", item_text.c_str()); ImGui::PopStyleVar(); } @@ -674,6 +685,25 @@ void GLGizmoMmuSegmentation::on_render_input_window(float x, float y, float bott } ImGui::Separator(); + + + if (m_imgui->button(m_desc.at("perform_remap"))) { + m_show_filament_remap_ui = !m_show_filament_remap_ui; + if (m_show_filament_remap_ui) { + // reset remap to identity on opening + m_extruder_remap.resize(m_extruders_colors.size()); + for (size_t i = 0; i < m_extruder_remap.size(); ++i) + m_extruder_remap[i] = i; + } + } + + // Render filament swap UI if enabled + if (m_show_filament_remap_ui) { + ImGui::Separator(); + render_filament_remap_ui(window_width, max_tooltip_width); + } + ImGui::Separator(); + ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(6.0f, 10.0f)); float get_cur_y = ImGui::GetContentRegionMax().y + ImGui::GetFrameHeight() + y; show_tooltip_information(caption_max, x, get_cur_y); @@ -928,4 +958,205 @@ void GLMmSegmentationGizmo3DScene::finalize_triangle_indices() } } +void GLGizmoMmuSegmentation::render_filament_remap_ui(float window_width, float max_tooltip_width) +{ + 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 ImVec2 button_size(max_label_size.x + m_imgui->scaled(0.5f), 0.f); + + 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(); + std::string btn_id = "##remap_src_" + std::to_string(src); + + ImGuiColorEditFlags flags = ImGuiColorEditFlags_NoAlpha | ImGuiColorEditFlags_NoInputs | + ImGuiColorEditFlags_NoLabel | ImGuiColorEditFlags_NoPicker | + ImGuiColorEditFlags_NoTooltip; + if (m_selected_extruder_idx != src) flags |= ImGuiColorEditFlags_NoBorder; + + #ifdef __APPLE__ + ImGui::PushStyleColor(ImGuiCol_FrameBg, ImGuiWrapper::COL_ORCA); + ImGui::PushStyleVar(ImGuiStyleVar_FrameBorderSize, 0.0f); + ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 3.0); + bool clicked = ImGui::ColorButton(btn_id.c_str(), col_vec, flags, button_size); + ImGui::PopStyleVar(2); + ImGui::PopStyleColor(1); + #else + ImGui::PushStyleColor(ImGuiCol_FrameBg, ImGuiWrapper::COL_ORCA); + ImGui::PushStyleVar(ImGuiStyleVar_FrameBorderSize, 0.0); + ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 2.0); + bool clicked = ImGui::ColorButton(btn_id.c_str(), col_vec, flags, button_size); + ImGui::PopStyleVar(2); + ImGui::PopStyleColor(1); + #endif + + // overlay destination number with proper contrast calculation + std::string dst_txt = std::to_string(m_extruder_remap[src] + 1); + float gray = 0.299f * dst_col.r() + 0.587f * dst_col.g() + 0.114f * dst_col.b(); + ImVec2 txt_sz = ImGui::CalcTextSize(dst_txt.c_str()); + ImVec2 pos = ImGui::GetItemRectMin(); + ImVec2 size = ImGui::GetItemRectSize(); + + if (gray * 255.f < 80.f) + ImGui::GetWindowDrawList()->AddText( + ImVec2(pos.x + (size.x - txt_sz.x) * 0.5f, pos.y + (size.y - txt_sz.y) * 0.5f), + IM_COL32(255,255,255,255), dst_txt.c_str()); + else + ImGui::GetWindowDrawList()->AddText( + ImVec2(pos.x + (size.x - txt_sz.x) * 0.5f, pos.y + (size.y - txt_sz.y) * 0.5f), + IM_COL32(0,0,0,255), dst_txt.c_str()); + + // popup with possible destinations + std::string pop_id = "popup_" + std::to_string(src); + if (clicked) { + // Calculate popup position centered below the current button + ImVec2 button_pos = ImGui::GetItemRectMin(); + ImVec2 button_size = ImGui::GetItemRectSize(); + ImVec2 popup_pos(button_pos.x + button_size.x * 0.5f, button_pos.y + button_size.y); + + // Set popup styling BEFORE opening popup + ImGui::SetNextWindowPos(popup_pos, ImGuiCond_Appearing, ImVec2(0.5f, -0.1f)); + ImGui::SetNextWindowBgAlpha(1.0f); // Ensure full opacity + ImGui::OpenPopup(pop_id.c_str()); + } + + // Apply popup styling before BeginPopup using standard Orca colors + ImGui::PushStyleVar(ImGuiStyleVar_PopupRounding, 4.0f); + ImGui::PushStyleVar(ImGuiStyleVar_PopupBorderSize, 1.0f); + ImGui::PushStyleColor(ImGuiCol_PopupBg, m_is_dark_mode ? ImGuiWrapper::COL_WINDOW_BG_DARK : ImGuiWrapper::COL_WINDOW_BG); + 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())) { + + 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(); + std::string dst_btn = "##dst_" + std::to_string(src) + "_" + std::to_string(dst); + + // Apply same styling to destination buttons + ImGuiColorEditFlags dst_flags = ImGuiColorEditFlags_NoAlpha | ImGuiColorEditFlags_NoInputs | + ImGuiColorEditFlags_NoLabel | ImGuiColorEditFlags_NoPicker | + ImGuiColorEditFlags_NoTooltip; + // Show border for currently selected destination filament + if (m_extruder_remap[src] != dst) dst_flags |= ImGuiColorEditFlags_NoBorder; + + #ifdef __APPLE__ + ImGui::PushStyleColor(ImGuiCol_FrameBg, ImGuiWrapper::COL_ORCA); + ImGui::PushStyleVar(ImGuiStyleVar_FrameBorderSize, 0.0f); + ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 3.0); + bool dst_clicked = ImGui::ColorButton(dst_btn.c_str(), dst_vec, dst_flags, button_size); + ImGui::PopStyleVar(2); + ImGui::PopStyleColor(1); + #else + ImGui::PushStyleColor(ImGuiCol_FrameBg, ImGuiWrapper::COL_ORCA); + ImGui::PushStyleVar(ImGuiStyleVar_FrameBorderSize, 0.0); + ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 2.0); + bool dst_clicked = ImGui::ColorButton(dst_btn.c_str(), dst_vec, dst_flags, button_size); + ImGui::PopStyleVar(2); + ImGui::PopStyleColor(1); + #endif + + // overlay destination number on popup buttons + std::string dst_num_txt = std::to_string(dst + 1); + float dst_gray = 0.299f * dst_col_popup.r() + 0.587f * dst_col_popup.g() + 0.114f * dst_col_popup.b(); + ImVec2 dst_txt_sz = ImGui::CalcTextSize(dst_num_txt.c_str()); + ImVec2 dst_pos = ImGui::GetItemRectMin(); + ImVec2 dst_size = ImGui::GetItemRectSize(); + + if (dst_gray * 255.f < 80.f) + ImGui::GetWindowDrawList()->AddText( + ImVec2(dst_pos.x + (dst_size.x - dst_txt_sz.x) * 0.5f, dst_pos.y + (dst_size.y - dst_txt_sz.y) * 0.5f), + IM_COL32(255,255,255,255), dst_num_txt.c_str()); + else + ImGui::GetWindowDrawList()->AddText( + ImVec2(dst_pos.x + (dst_size.x - dst_txt_sz.x) * 0.5f, dst_pos.y + (dst_size.y - dst_txt_sz.y) * 0.5f), + IM_COL32(0,0,0,255), dst_num_txt.c_str()); + + if (dst_clicked) + { + m_extruder_remap[src] = dst; + // update the source button color immediately + ImGui::CloseCurrentPopup(); + } + } + ImGui::EndPopup(); + } + + // Clean up popup styling (always pop, whether popup was open or not) + ImGui::PopStyleColor(2); // PopupBg and Border + ImGui::PopStyleVar(2); // PopupRounding and PopupBorderSize + } + + ImGui::Dummy(ImVec2(0.0f, ImGui::GetFontSize() * 0.3f)); + + if (m_imgui->button(m_desc.at("remap"))) { + remap_filament_assignments(); + m_show_filament_remap_ui = false; + } + + ImGui::SameLine(); + if (m_imgui->button(m_desc.at("cancel_remap"))) + m_show_filament_remap_ui = false; +} + +void GLGizmoMmuSegmentation::remap_filament_assignments() +{ + if (m_extruder_remap.empty()) + return; + + constexpr size_t MAX_EBT = (size_t)EnforcerBlockerType::ExtruderMax; + EnforcerBlockerStateMap state_map; + + // identity mapping by default + for (size_t i = 0; i <= MAX_EBT; ++i) + state_map[i] = static_cast(i); + + size_t n_extr = std::min(m_extruder_remap.size(), MAX_EBT); + const int start_extruder = (int) EnforcerBlockerType::Extruder1; + bool any_change = false; + for (size_t src = 0; src < n_extr; ++src) { + size_t dst = m_extruder_remap[src]; + if (dst != src) { + state_map[src+start_extruder] = static_cast(dst+start_extruder); + if (src == 0) + state_map[0] = static_cast(dst + start_extruder); + + any_change = true; + } + } + if (!any_change) + return; + + Plater::TakeSnapshot snapshot(wxGetApp().plater(), + "Remap filament assignments", + UndoRedo::SnapshotType::GizmoAction); + + bool updated = false; + int idx = -1; + ModelObject* mo = m_c->selection_info()->model_object(); + if (!mo) return; + + for (ModelVolume* mv : mo->volumes) { + if (!mv->is_model_part()) continue; + ++idx; + TriangleSelectorGUI* ts = m_triangle_selectors[idx].get(); + if (!ts) continue; + ts->remap_triangle_state(state_map); + ts->request_update_render_data(true); + updated = true; + } + + if (updated) { + wxGetApp().plater()->get_notification_manager()->push_notification( + _L("Filament remapping finished.").ToStdString()); + update_model_object(); + m_parent.set_as_dirty(); + } +} + } // namespace Slic3r diff --git a/src/slic3r/GUI/Gizmos/GLGizmoMmuSegmentation.hpp b/src/slic3r/GUI/Gizmos/GLGizmoMmuSegmentation.hpp index c6c33ec90f..00017f2a93 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoMmuSegmentation.hpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoMmuSegmentation.hpp @@ -111,6 +111,10 @@ protected: // BBS wchar_t m_current_tool = 0; bool m_detect_geometry_edge = true; + + // Filament remap feature + std::vector m_extruder_remap; // index → target extruder index + bool m_show_filament_remap_ui = false; static const constexpr float CursorRadiusMin = 0.1f; // cannot be zero @@ -132,6 +136,10 @@ private: // BBS void update_triangle_selectors_colors(); void init_extruders_data(); + + // Filament remapping methods + void remap_filament_assignments(); + void render_filament_remap_ui(float window_width, float max_tooltip_width); // This map holds all translated description texts, so they can be easily referenced during layout calculations // etc. When language changes, GUI is recreated and this class constructed again, so the change takes effect.