diff --git a/src/libslic3r/TriangleSelector.cpp b/src/libslic3r/TriangleSelector.cpp index f09d540189..a6d19f505c 100644 --- a/src/libslic3r/TriangleSelector.cpp +++ b/src/libslic3r/TriangleSelector.cpp @@ -2197,4 +2197,15 @@ bool TriangleSelector::Capsule2D::is_edge_inside_cursor(const Triangle &tr, cons return false; } +// ORCA: Helper to extract used states from serialized data +std::vector TriangleSelector::extract_used_facet_states(const TriangleSplittingData &data) +{ + std::vector out; + for (size_t i = 0; i < data.used_states.size(); ++i) { + if (data.used_states[i]) + out.push_back(static_cast(i)); + } + return out; +} + } // namespace Slic3r diff --git a/src/slic3r/GUI/Gizmos/GLGizmoMmuSegmentation.cpp b/src/slic3r/GUI/Gizmos/GLGizmoMmuSegmentation.cpp index d8bc90fdd6..b719bd7f54 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoMmuSegmentation.cpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoMmuSegmentation.cpp @@ -679,23 +679,60 @@ void GLGizmoMmuSegmentation::on_render_input_window(float x, float y, float bott } ImGui::Separator(); + ImGui::Dummy(ImVec2(0.0f, ImGui::GetFontSize() * 0.5f)); - + // ORCA: Remap filaments section (Border only, Title in border). + // Styled as a panel for visual grouping. 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; + m_show_remap_panel = !m_show_remap_panel; + } + + if (m_show_remap_panel) + { + ImDrawList* draw_list = ImGui::GetWindowDrawList(); + std::string title = into_u8(m_desc.at("perform_remap")); + float available_width = ImGui::GetContentRegionAvail().x; + + // ORCA: Draw Background filled (consistent with Filaments section) + // Use static to remember height from previous frame so we can draw it behind. + static float remap_panel_high = 40.0f; + ImVec2 p_bg_min = ImGui::GetCursorScreenPos(); + // Adjust background position: slight negative offset to align with padding, width fills available + // height from static variable. + draw_list->AddRectFilled({p_bg_min.x - 10.0f, p_bg_min.y - 7.0f}, {p_bg_min.x + available_width + ImGui::GetFrameHeight(), p_bg_min.y + remap_panel_high}, ImGui::GetColorU32(ImGuiCol_FrameBgActive, 1.0f), 5.0f); + + float start_y = ImGui::GetCursorPos().y; + + // ORCA: Title as simple text - Removed as per request (redundant with button) + // m_imgui->text(title); + + ImGui::BeginGroup(); + // ORCA: Reduce vertical spacing within this group + ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(m_imgui->scaled(0.4f), m_imgui->scaled(0.2f))); + + render_filament_remap_ui(window_width, max_tooltip_width); + + ImGui::PopStyleVar(); + ImGui::EndGroup(); + + // ORCA: Update height for next frame fill + remap_panel_high = ImGui::GetCursorPos().y - start_y; + + // ORCA: Add Remap and Cancel buttons (outside the panel) + ImGui::Dummy(ImVec2(0.0f, ImGui::GetFontSize() * 0.2f)); + if (m_imgui->button(m_desc.at("remap"))) { + this->remap_filament_assignments(); + // Reset mapping to identity after apply + for (size_t i = 0; i < m_extruder_remap.size(); ++i) m_extruder_remap[i] = i; + } + ImGui::SameLine(); + if (m_imgui->button(m_desc.at("cancel_remap"))) { + // Reset mapping to identity + 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::Dummy(ImVec2(0.0f, ImGui::GetFontSize() * 0.5f)); ImGui::Separator(); ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(6.0f, 10.0f)); @@ -763,6 +800,9 @@ void GLGizmoMmuSegmentation::update_model_object() wxGetApp().obj_list()->update_info_items(obj_idx); wxGetApp().plater()->get_partplate_list().notify_instance_update(obj_idx, 0); m_parent.post_event(SimpleEvent(EVT_GLCANVAS_SCHEDULE_BACKGROUND_PROCESS)); + + // ORCA: Refresh cache + this->update_used_filaments(); } } @@ -825,6 +865,9 @@ void GLGizmoMmuSegmentation::update_from_model_object(bool first_update) this->init_extruders_data(); this->init_model_triangle_selectors(); + + // ORCA: Refresh cache when model changes + this->update_used_filaments(); } void GLGizmoMmuSegmentation::tool_changed(wchar_t old_tool, wchar_t new_tool) @@ -1010,6 +1053,35 @@ void GLMmSegmentationGizmo3DScene::finalize_triangle_indices() } } +// ORCA: Update the cache of used filaments (both base volume extruders and painted triangles) +void GLGizmoMmuSegmentation::update_used_filaments() +{ + m_used_filaments.clear(); + + // Add base extruder IDs from volumes (unpainted areas) + for (int ext_id : m_volumes_extruder_idxs) { + // ext_id is 1-based (1 = Extruder 1), 0 = Default (usually maps to first available or object default) + // Here we assume 0 maps to index 0 (Extruder 1) for simplicity in display, + // or we should check logic in init_model_triangle_selectors where it does: + // int extruder_idx = (mv->extruder_id() > 0) ? mv->extruder_id() - 1 : 0; + int idx = (ext_id > 0) ? ext_id - 1 : 0; + if (idx >= 0 && idx < m_extruders_colors.size()) + m_used_filaments.insert((size_t)idx); + } + + // Add painted states + for (const auto& selector : m_triangle_selectors) { + if (!selector) continue; + TriangleSelector::TriangleSplittingData data = selector->serialize(); + std::vector states = TriangleSelector::extract_used_facet_states(data); + for (EnforcerBlockerType s : states) { + int idx = (int)s - (int)EnforcerBlockerType::Extruder1; + if (idx >= 0 && idx < m_extruders_colors.size()) + m_used_filaments.insert((size_t)idx); + } + } +} + 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()); @@ -1017,18 +1089,34 @@ void GLGizmoMmuSegmentation::render_filament_remap_ui(float window_width, float 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) { + int displayed_count = 0; + const int max_per_line = 8; + + // ORCA: Use m_used_filaments to show only relevant source filaments + for (size_t src : m_used_filaments) { + if (src >= n_extr) continue; + 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); + + // ORCA: Button now shows the SOURCE color (per maintainer request) + // This keeps the UI stable until "Remap" is clicked. + ImVec4 col_vec = ImGuiWrapper::to_ImVec4(src_col); - if (src) ImGui::SameLine(); + if (displayed_count > 0 && (displayed_count % max_per_line != 0)) + ImGui::SameLine(); + std::string btn_id = "##remap_src_" + std::to_string(src); + std::string pop_id = "popup_" + 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; + + // ORCA: Show border ONLY if the popup is open (visual feedback for active selection) + // Decoupled from m_selected_extruder_idx to prevent unwanted selection highlights. + if (!ImGui::IsPopupOpen(pop_id.c_str())) + flags |= ImGuiColorEditFlags_NoBorder; #ifdef __APPLE__ ImGui::PushStyleColor(ImGuiCol_FrameBg, ImGuiWrapper::COL_ORCA); @@ -1047,8 +1135,9 @@ void GLGizmoMmuSegmentation::render_filament_remap_ui(float window_width, float #endif // overlay destination number with proper contrast calculation + // ORCA: Text still shows DESTINATION index, but contrast is against SOURCE color now. 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(); + float gray = 0.299f * src_col.r() + 0.587f * src_col.g() + 0.114f * src_col.b(); ImVec2 txt_sz = ImGui::CalcTextSize(dst_txt.c_str()); ImVec2 pos = ImGui::GetItemRectMin(); ImVec2 size = ImGui::GetItemRectSize(); @@ -1062,8 +1151,35 @@ void GLGizmoMmuSegmentation::render_filament_remap_ui(float window_width, float 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()); + // ORCA: Show NEW color as a small triangle in the corner if remapped + if (src != m_extruder_remap[src]) { + float s = m_imgui->scaled(0.55f); + float offset = m_imgui->scaled(0.15f); // Inset to avoid rounded corner clipping + ImVec2 p = ImVec2(pos.x + offset, pos.y + offset); + + // Contrast outline: White for dark backgrounds, Black for light backgrounds + // Use dst_col (new color) for outline contrast check? Or src_col? + // Usually outline is around the triangle (dst_col). + float dst_gray = 0.299f * dst_col.r() + 0.587f * dst_col.g() + 0.114f * dst_col.b(); + ImU32 outline_col = (dst_gray * 255.f < 80.f) ? IM_COL32(255, 255, 255, 180) : IM_COL32(0, 0, 0, 180); + + ImDrawList* draw_list = ImGui::GetWindowDrawList(); + draw_list->AddTriangleFilled( + p, + ImVec2(p.x + s, p.y), + ImVec2(p.x, p.y + s), + ImGuiWrapper::to_ImU32(dst_col)); + + // ORCA: Add a thin outline for better contrast when colors are similar + draw_list->AddTriangle( + p, + ImVec2(p.x + s, p.y), + ImVec2(p.x, p.y + s), + outline_col, + 0.5f); + } + // 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(); @@ -1079,15 +1195,19 @@ void GLGizmoMmuSegmentation::render_filament_remap_ui(float window_width, float // 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); + // ORCA: Use FrameBgActive for consistency and to ensure visibility of white filaments + ImGui::PushStyleColor(ImGuiCol_PopupBg, ImGui::GetStyleColorVec4(ImGuiCol_FrameBgActive)); 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())) { + m_imgui->text(_L("To:")); + 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 > 0 && (dst % max_per_line != 0)) + ImGui::SameLine(); std::string dst_btn = "##dst_" + std::to_string(src) + "_" + std::to_string(dst); // Apply same styling to destination buttons @@ -1142,18 +1262,9 @@ void GLGizmoMmuSegmentation::render_filament_remap_ui(float window_width, float // Clean up popup styling (always pop, whether popup was open or not) ImGui::PopStyleColor(2); // PopupBg and Border ImGui::PopStyleVar(2); // PopupRounding and PopupBorderSize + + displayed_count++; } - - 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() @@ -1193,21 +1304,46 @@ void GLGizmoMmuSegmentation::remap_filament_assignments() ModelObject* mo = m_c->selection_info()->model_object(); if (!mo) return; + bool volume_extruder_changed = false; + for (ModelVolume* mv : mo->volumes) { if (!mv->is_model_part()) continue; ++idx; TriangleSelectorGUI* ts = m_triangle_selectors[idx].get(); if (!ts) continue; + + // Remap painted triangles ts->remap_triangle_state(state_map); ts->request_update_render_data(true); + + // ORCA: Remap base volume extruder as well if selected + int current_ext_id = mv->extruder_id(); + int current_idx = (current_ext_id > 0) ? current_ext_id - 1 : 0; + + if (current_idx >= 0 && current_idx < m_extruder_remap.size()) { + size_t dest_idx = m_extruder_remap[current_idx]; + if (dest_idx != current_idx) { + mv->config.set("extruder", (int)dest_idx + 1); + if (idx < m_volumes_extruder_idxs.size()) + m_volumes_extruder_idxs[idx] = (int)dest_idx + 1; + volume_extruder_changed = true; + } + } + updated = true; } if (updated) { - wxGetApp().plater()->get_notification_manager()->push_notification( - _L("Filament remapping finished.").ToStdString()); + // ORCA: Update renderer colors if base volume extruder changed + if (volume_extruder_changed) + this->update_triangle_selectors_colors(); + + // ORCA: Removed "Filament remapping finished" notification to reduce UI noise. update_model_object(); m_parent.set_as_dirty(); + + // ORCA: Refresh used filaments cache + this->update_used_filaments(); } } diff --git a/src/slic3r/GUI/Gizmos/GLGizmoMmuSegmentation.hpp b/src/slic3r/GUI/Gizmos/GLGizmoMmuSegmentation.hpp index 2ab524e379..f9bcfdee12 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoMmuSegmentation.hpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoMmuSegmentation.hpp @@ -114,8 +114,10 @@ protected: bool m_detect_geometry_edge = true; // Filament remap feature + bool m_show_remap_panel = false; std::vector m_extruder_remap; // index → target extruder index - bool m_show_filament_remap_ui = false; + // ORCA: Cache used filaments to filter UI + std::set m_used_filaments; // Set of used filament indices (cached) static const constexpr float CursorRadiusMin = 0.1f; // cannot be zero @@ -141,6 +143,8 @@ private: // Filament remapping methods void remap_filament_assignments(); void render_filament_remap_ui(float window_width, float max_tooltip_width); + // ORCA: Helper to update the cache of used filaments + void update_used_filaments(); // 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. diff --git a/task.md b/task.md index 4067c4d613..10fc9ac724 100644 --- a/task.md +++ b/task.md @@ -1,12 +1,12 @@ -Analyze the bug that it failed to load project(3mf) from old version. -It failed pass below check in PresetBundle::load_config_file_config function, hence throw error. - if (config.option("extruder_variant_list")) { - //3mf support multiple extruder logic - size_t extruder_count = config.option("nozzle_diameter")->values.size(); - extruder_variant_count = config.option("filament_extruder_variant", true)->size(); - if ((extruder_variant_count != filament_self_indice.size()) - || (extruder_variant_count < num_filaments)) { - assert(false); - BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << boost::format(": invalid config file %1%, can not find suitable filament_extruder_variant or filament_self_index") % name_or_path; - throw Slic3r::RuntimeError(std::string("Invalid configuration file: ") + name_or_path); +Analyze the bug that it failed to load project(3mf) from old version. +It failed pass below check in PresetBundle::load_config_file_config function, hence throw error. + if (config.option("extruder_variant_list")) { + //3mf support multiple extruder logic + size_t extruder_count = config.option("nozzle_diameter")->values.size(); + extruder_variant_count = config.option("filament_extruder_variant", true)->size(); + if ((extruder_variant_count != filament_self_indice.size()) + || (extruder_variant_count < num_filaments)) { + assert(false); + BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << boost::format(": invalid config file %1%, can not find suitable filament_extruder_variant or filament_self_index") % name_or_path; + throw Slic3r::RuntimeError(std::string("Invalid configuration file: ") + name_or_path); } \ No newline at end of file