enh: Remap filament (#12016)

# Description

This Pr improve the Remap filament feature.
It now recognizes the filaments used in the object and makes them available only for remapping.
You can also see a small preview, showing which color will be changed to which.

Before PR:
![before](https://github.com/user-attachments/assets/e13f3622-2420-478e-a22c-9d7cdc12b24c)


After PR:
![after](https://github.com/user-attachments/assets/38d94230-f5bd-45f2-b9cd-c5bdff5ee801)
This commit is contained in:
tome9111991
2026-01-26 08:21:18 +01:00
committed by GitHub
parent e81e7b9a23
commit 8abf0f761d
4 changed files with 197 additions and 46 deletions

View File

@@ -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<EnforcerBlockerType> TriangleSelector::extract_used_facet_states(const TriangleSplittingData &data)
{
std::vector<EnforcerBlockerType> out;
for (size_t i = 0; i < data.used_states.size(); ++i) {
if (data.used_states[i])
out.push_back(static_cast<EnforcerBlockerType>(i));
}
return out;
}
} // namespace Slic3r

View File

@@ -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<EnforcerBlockerType> 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();
}
}

View File

@@ -114,8 +114,10 @@ protected:
bool m_detect_geometry_edge = true;
// Filament remap feature
bool m_show_remap_panel = false;
std::vector<size_t> m_extruder_remap; // index → target extruder index
bool m_show_filament_remap_ui = false;
// ORCA: Cache used filaments to filter UI
std::set<size_t> 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.

22
task.md
View File

@@ -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<ConfigOptionFloats>("nozzle_diameter")->values.size();
extruder_variant_count = config.option<ConfigOptionStrings>("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<ConfigOptionFloats>("nozzle_diameter")->values.size();
extruder_variant_count = config.option<ConfigOptionStrings>("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);
}