Remap filament for pre-colored models (#10303)

* Add a new feature to allow users to remap filament for a pre-painted model.

* Fix the color issues to support the theme

* clean up code

* Fix broken freetype-2.12.1.tar.gz link
This commit is contained in:
SoftFever
2025-08-03 13:28:03 +08:00
committed by GitHub
parent e5ea789b89
commit f27a40d29b
4 changed files with 276 additions and 5 deletions

View File

@@ -3,6 +3,8 @@
#include <boost/container/small_vector.hpp>
#include <boost/log/trivial.hpp>
#include <cstddef>
#include <tbb/parallel_for.h>
#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<size_t>(0, m_triangles.size()), [this, &state_map](const tbb::blocked_range<size_t>& 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<size_t>(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)
{

View File

@@ -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<EnforcerBlockerType, (size_t)EnforcerBlockerType::ExtruderMax + 1>;
// 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 {

View File

@@ -89,8 +89,13 @@ static std::vector<int> 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<EnforcerBlockerType>(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<EnforcerBlockerType>(dst+start_extruder);
if (src == 0)
state_map[0] = static_cast<EnforcerBlockerType>(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

View File

@@ -111,6 +111,10 @@ protected:
// BBS
wchar_t m_current_tool = 0;
bool m_detect_geometry_edge = true;
// Filament remap feature
std::vector<size_t> 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.