Files
OrcaSlicer/src/slic3r/GUI/Gizmos/GLGizmoSeam.cpp
Noisyfox 50e64d5961 Add fuzzy skin painting (#9979)
* SPE-2486: Refactor function apply_mm_segmentation() to prepare support for fuzzy skin painting.

(cherry picked from commit 2c06c81159f7aadd6ac20c7a7583c8f4959a5601)

* SPE-2585: Fix empty layers when multi-material painting and modifiers are used.

(cherry picked from commit 4b3da02ec26d43bfad91897cb34779fb21419e3e)

* Update project structure to match Prusa

* SPE-2486: Add a new gizmo for fuzzy skin painting.

(cherry picked from commit 886faac74ebe6978b828f51be62d26176e2900e5)

* Fix render

* Remove duplicated painting gizmo `render_triangles` code

* SPE-2486: Extend multi-material segmentation to allow segmentation of any painted faces.

(cherry picked from commit 519f5eea8e3be0d7c2cd5d030323ff264727e3d0)

---------

Co-authored-by: Lukáš Hejl <hejl.lukas@gmail.com>

* SPE-2486: Implement segmentation of layers based on fuzzy skin painting.

(cherry picked from commit 800b742b950438c5ed8323693074b6171300131c)

* SPE-2486: Separate fuzzy skin implementation into the separate file.

(cherry picked from commit efd95c1c66dc09fca7695fb82405056c687c2291)

* Move more fuzzy code to separate file

* Don't hide fuzzy skin option, so it can be applied to paint on fuzzy

* Fix build

* Add option group for fuzzy skin

* Update icon color

* Fix reset painting

* Update UI style

* Store fuzzy painting in bbs_3mf

* Add missing fuzzy paint code

* SPE-2486: Limit the depth of the painted fuzzy skin regions to make regions cover just external perimeters.

This reduces the possibility of artifacts that could happen during regions merging.

(cherry picked from commit fa2663f02647f80b239da4f45d92ef66f5ce048a)

* Update icons

---------

Co-authored-by: yw4z <ywsyildiz@gmail.com>

* Make the region compatible check a separate function

* Only warn about multi-material if it's truly multi-perimeters

* Improve gizmo UI & tooltips

---------

Co-authored-by: Lukáš Hejl <hejl.lukas@gmail.com>
Co-authored-by: yw4z <ywsyildiz@gmail.com>
2025-07-18 16:01:25 +08:00

391 lines
14 KiB
C++

#include "GLGizmoSeam.hpp"
#include "libslic3r/Model.hpp"
//#include "slic3r/GUI/3DScene.hpp"
#include "slic3r/GUI/GLCanvas3D.hpp"
#include "slic3r/GUI/GUI_App.hpp"
#include "slic3r/GUI/ImGuiWrapper.hpp"
#include "slic3r/GUI/Plater.hpp"
#include "slic3r/GUI/GUI_ObjectList.hpp"
#include "slic3r/GUI/GUI.hpp"
#include "slic3r/Utils/UndoRedo.hpp"
#include <GL/glew.h>
namespace Slic3r::GUI {
void GLGizmoSeam::on_shutdown()
{
m_parent.toggle_model_objects_visibility(true);
}
bool GLGizmoSeam::on_init()
{
m_shortcut_key = WXK_CONTROL_P;
m_desc["clipping_of_view_caption"] = _L("Alt + Mouse wheel");
m_desc["clipping_of_view"] = _L("Section view");
m_desc["reset_direction"] = _L("Reset direction");
m_desc["cursor_size_caption"] = _L("Ctrl + Mouse wheel");
m_desc["cursor_size"] = _L("Brush size");
m_desc["cursor_type"] = _L("Brush shape");
m_desc["enforce_caption"] = _L("Left mouse button");
m_desc["enforce"] = _L("Enforce seam");
m_desc["block_caption"] = _L("Right mouse button");
m_desc["block"] = _L("Block seam");
m_desc["remove_caption"] = _L("Shift + Left mouse button");
m_desc["remove"] = _L("Erase");
m_desc["remove_all"] = _L("Erase all painting");
m_desc["circle"] = _L("Circle");
m_desc["sphere"] = _L("Sphere");
return true;
}
GLGizmoSeam::GLGizmoSeam(GLCanvas3D& parent, const std::string& icon_filename, unsigned int sprite_id)
: GLGizmoPainterBase(parent, icon_filename, sprite_id), m_current_tool(ImGui::CircleButtonIcon)
{
}
std::string GLGizmoSeam::on_get_name() const
{
return _u8L("Seam painting");
}
void GLGizmoSeam::render_painter_gizmo()
{
const Selection& selection = m_parent.get_selection();
glsafe(::glEnable(GL_BLEND));
glsafe(::glEnable(GL_DEPTH_TEST));
render_triangles(selection);
m_c->object_clipper()->render_cut();
m_c->instances_hider()->render_cut();
render_cursor();
glsafe(::glDisable(GL_BLEND));
}
// BBS
bool GLGizmoSeam::on_key_down_select_tool_type(int keyCode) {
switch (keyCode)
{
case 'S':
m_current_tool = ImGui::SphereButtonIcon;
break;
case 'C':
m_current_tool = ImGui::CircleButtonIcon;
break;
default:
return false;
break;
}
return true;
}
void GLGizmoSeam::show_tooltip_information(float caption_max, float x, float y)
{
ImTextureID normal_id = m_parent.get_gizmos_manager().get_icon_texture_id(GLGizmosManager::MENU_ICON_NAME::IC_TOOLBAR_TOOLTIP);
ImTextureID hover_id = m_parent.get_gizmos_manager().get_icon_texture_id(GLGizmosManager::MENU_ICON_NAME::IC_TOOLBAR_TOOLTIP_HOVER);
caption_max += m_imgui->calc_text_size(std::string_view{": "}).x + 35.f;
float scale = m_parent.get_scale();
ImVec2 button_size = ImVec2(25 * scale, 25 * scale); // ORCA: Use exact resolution will prevent blur on icon
ImGui::PushStyleVar(ImGuiStyleVar_FrameBorderSize, 0.0f);
ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, {0, 0}); // ORCA: Dont add padding
ImGui::ImageButton3(normal_id, hover_id, button_size);
if (ImGui::IsItemHovered()) {
ImGui::BeginTooltip2(ImVec2(x, y));
auto draw_text_with_caption = [this, &caption_max](const wxString &caption, const wxString &text) {
m_imgui->text_colored(ImGuiWrapper::COL_ACTIVE, caption);
ImGui::SameLine(caption_max);
m_imgui->text_colored(ImGuiWrapper::COL_WINDOW_BG, text);
};
for (const auto &t : std::array<std::string, 5>{"enforce", "block", "remove", "cursor_size", "clipping_of_view"}) draw_text_with_caption(m_desc.at(t + "_caption") + ": ", m_desc.at(t));
ImGui::EndTooltip();
}
ImGui::PopStyleVar(2);
}
void GLGizmoSeam::tool_changed(wchar_t old_tool, wchar_t new_tool)
{
if ((old_tool == ImGui::GapFillIcon && new_tool == ImGui::GapFillIcon) ||
(old_tool != ImGui::GapFillIcon && new_tool != ImGui::GapFillIcon))
return;
for (auto& selector_ptr : m_triangle_selectors) {
TriangleSelectorPatch* tsp = dynamic_cast<TriangleSelectorPatch*>(selector_ptr.get());
tsp->set_filter_state(new_tool == ImGui::GapFillIcon);
}
}
void GLGizmoSeam::on_render_input_window(float x, float y, float bottom_limit)
{
if (! m_c->selection_info()->model_object())
return;
const float approx_height = m_imgui->scaled(12.5f);
y = std::min(y, bottom_limit - approx_height);
//BBS: GUI refactor: move gizmo to the right
#if BBS_TOOLBAR_ON_TOP
GizmoImguiSetNextWIndowPos(x, y, ImGuiCond_Always, 0.0f, 0.0f);
#else
GizmoImguiSetNextWIndowPos(x, y, ImGuiCond_Always, 1.0f, 0.0f);
#endif
//m_imgui->set_next_window_pos(x, y, ImGuiCond_Always);
wchar_t old_tool = m_current_tool;
//BBS
ImGuiWrapper::push_toolbar_style(m_parent.get_scale());
GizmoImguiBegin(get_name(), ImGuiWindowFlags_NoMove | ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoTitleBar);
// First calculate width of all the texts that are could possibly be shown. We will decide set the dialog width based on that:
const float space_size = m_imgui->get_style_scaling() * 8;
const float clipping_slider_left = std::max(m_imgui->calc_text_size(m_desc.at("clipping_of_view")).x,
m_imgui->calc_text_size(m_desc.at("reset_direction")).x + ImGui::GetStyle().FramePadding.x * 2)
+ m_imgui->scaled(1.5f);
const float cursor_size_slider_left = m_imgui->calc_text_size(m_desc.at("cursor_size")).x + m_imgui->scaled(1.f);
const float empty_button_width = m_imgui->calc_button_size("").x;
float caption_max = 0.f;
float total_text_max = 0.f;
for (const auto &t : std::array<std::string, 6>{"enforce", "block", "remove", "cursor_size", "clipping_of_view"}) {
caption_max = std::max(caption_max, m_imgui->calc_text_size(m_desc[t + "_caption"]).x);
total_text_max = std::max(total_text_max, m_imgui->calc_text_size(m_desc[t]).x);
}
const float sliders_left_width = std::max(cursor_size_slider_left, clipping_slider_left);
const float slider_icon_width = m_imgui->get_slider_icon_size().x;
const float sliders_width = m_imgui->scaled(7.0f);
const float drag_left_width = ImGui::GetStyle().WindowPadding.x + sliders_left_width + sliders_width - space_size;
const float max_tooltip_width = ImGui::GetFontSize() * 20.0f;
ImGui::AlignTextToFramePadding();
m_imgui->text(m_desc.at("cursor_type"));
std::array<wchar_t, 2> tool_ids = { ImGui::CircleButtonIcon, ImGui::SphereButtonIcon };
std::array<wchar_t, 2> icons;
if (m_is_dark_mode)
icons = { ImGui::CircleButtonDarkIcon, ImGui::SphereButtonDarkIcon};
else
icons = { ImGui::CircleButtonIcon, ImGui::SphereButtonIcon };
std::array<wxString, 2> tool_tips = { _L("Circle"), _L("Sphere")};
for (int i = 0; i < tool_ids.size(); i++) {
std::string str_label = std::string("##");
std::wstring btn_name = icons[i] + boost::nowide::widen(str_label);
if (i != 0) ImGui::SameLine((empty_button_width + m_imgui->scaled(1.75f)) * i + m_imgui->scaled(1.3f));
ImGui::PushStyleVar(ImGuiStyleVar_FrameBorderSize, 0.0);
ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.f, 0.f, 0.f, 0.f)); // ORCA Removes button background on dark mode
ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.f, 1.f, 1.f, 1.f)); // ORCA: Fixes icon rendered without colors while using Light theme
if (m_current_tool == tool_ids[i]) {
ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.f, 0.59f, 0.53f, 0.25f)); // ORCA use orca color for selected tool / brush
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.f, 0.59f, 0.53f, 0.25f)); // ORCA use orca color for selected tool / brush
ImGui::PushStyleColor(ImGuiCol_ButtonActive, ImVec4(0.f, 0.59f, 0.53f, 0.30f)); // ORCA use orca color for selected tool / brush
ImGui::PushStyleColor(ImGuiCol_Border, ImGuiWrapper::COL_ORCA); // ORCA use orca color for border on selected tool / brush
ImGui::PushStyleVar(ImGuiStyleVar_FrameBorderSize, 1.0);
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 1.0);
}
bool btn_clicked = ImGui::Button(into_u8(btn_name).c_str());
if (m_current_tool == tool_ids[i])
{
ImGui::PopStyleColor(4);
ImGui::PopStyleVar(2);
}
ImGui::PopStyleColor(2);
ImGui::PopStyleVar(1);
if (btn_clicked && m_current_tool != tool_ids[i]) {
m_current_tool = tool_ids[i];
for (auto& triangle_selector : m_triangle_selectors) {
triangle_selector->seed_fill_unselect_all_triangles();
triangle_selector->request_update_render_data();
}
}
if (ImGui::IsItemHovered()) {
m_imgui->tooltip(tool_tips[i], max_tooltip_width);
}
}
if (m_current_tool != old_tool)
this->tool_changed(old_tool, m_current_tool);
ImGui::Dummy(ImVec2(0.0f, ImGui::GetFontSize() * 0.1));
if (m_current_tool == ImGui::CircleButtonIcon) {
m_cursor_type = TriangleSelector::CursorType::CIRCLE;
m_tool_type = ToolType::BRUSH;
} else if (m_current_tool == ImGui::SphereButtonIcon) {
m_cursor_type = TriangleSelector::CursorType::SPHERE;
m_tool_type = ToolType::BRUSH;
}
ImGui::AlignTextToFramePadding();
m_imgui->text(m_desc.at("cursor_size"));
ImGui::SameLine(sliders_left_width);
ImGui::PushItemWidth(sliders_width);
m_imgui->bbl_slider_float_style("##cursor_radius", &m_cursor_radius, CursorRadiusMin, CursorRadiusMax, "%.2f", 1.0f, true);
ImGui::SameLine(drag_left_width);
ImGui::PushItemWidth(1.5 * slider_icon_width);
ImGui::BBLDragFloat("##cursor_radius_input", &m_cursor_radius, 0.05f, 0.0f, 0.0f, "%.2f");
ImGui::Separator();
if (m_c->object_clipper()->get_position() == 0.f) {
ImGui::AlignTextToFramePadding();
m_imgui->text(m_desc.at("clipping_of_view"));
}
else {
if (m_imgui->button(m_desc.at("reset_direction"))) {
wxGetApp().CallAfter([this](){
m_c->object_clipper()->set_position_by_ratio(-1., false);
});
}
}
auto clp_dist = float(m_c->object_clipper()->get_position());
ImGui::SameLine(sliders_left_width);
ImGui::PushItemWidth(sliders_width);
bool slider_clp_dist = m_imgui->bbl_slider_float_style("##clp_dist", &clp_dist, 0.f, 1.f, "%.2f", 1.0f, true);
ImGui::SameLine(drag_left_width);
ImGui::PushItemWidth(1.5 * slider_icon_width);
bool b_clp_dist_input = ImGui::BBLDragFloat("##clp_dist_input", &clp_dist, 0.05f, 0.0f, 0.0f, "%.2f");
if (slider_clp_dist || b_clp_dist_input) { m_c->object_clipper()->set_position_by_ratio(clp_dist, true); }
ImGui::Separator();
m_imgui->bbl_checkbox(_L("Vertical"), m_vertical_only);
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);
float f_scale =m_parent.get_gizmos_manager().get_layout_scale();
ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(6.0f, 4.0f * f_scale));
ImGui::SameLine();
if (m_imgui->button(m_desc.at("remove_all"))) {
Plater::TakeSnapshot snapshot(wxGetApp().plater(), "Reset selection", UndoRedo::SnapshotType::GizmoAction);
ModelObject *mo = m_c->selection_info()->model_object();
int idx = -1;
for (ModelVolume *mv : mo->volumes)
if (mv->is_model_part()) {
++idx;
m_triangle_selectors[idx]->reset();
m_triangle_selectors[idx]->request_update_render_data(true);
}
update_model_object();
m_parent.set_as_dirty();
}
ImGui::PopStyleVar(2);
GizmoImguiEnd();
//BBS
ImGuiWrapper::pop_toolbar_style();
}
// BBS
void GLGizmoSeam::on_set_state()
{
GLGizmoPainterBase::on_set_state();
if (get_state() == Off) {
ModelObject* mo = m_c->selection_info()->model_object();
if (mo) Slic3r::save_object_mesh(*mo);
}
}
//BBS: remove const
void GLGizmoSeam::update_model_object()
{
bool updated = false;
ModelObject* mo = m_c->selection_info()->model_object();
int idx = -1;
for (ModelVolume* mv : mo->volumes) {
if (! mv->is_model_part())
continue;
++idx;
updated |= mv->seam_facets.set(*m_triangle_selectors[idx].get());
}
if (updated) {
const ModelObjectPtrs& mos = wxGetApp().model().objects;
wxGetApp().obj_list()->update_info_items(std::find(mos.begin(), mos.end(), mo) - mos.begin());
m_parent.post_event(SimpleEvent(EVT_GLCANVAS_SCHEDULE_BACKGROUND_PROCESS));
}
}
//BBS: add logic to distinguish the first_time_update and later_update
void GLGizmoSeam::update_from_model_object(bool first_update)
{
wxBusyCursor wait;
const ModelObject* mo = m_c->selection_info()->model_object();
m_triangle_selectors.clear();
int volume_id = -1;
std::vector<ColorRGBA> ebt_colors;
ebt_colors.push_back(GLVolume::NEUTRAL_COLOR);
ebt_colors.push_back(TriangleSelectorGUI::enforcers_color);
ebt_colors.push_back(TriangleSelectorGUI::blockers_color);
for (const ModelVolume* mv : mo->volumes) {
if (! mv->is_model_part())
continue;
++volume_id;
// This mesh does not account for the possible Z up SLA offset.
const TriangleMesh* mesh = &mv->mesh();
m_triangle_selectors.emplace_back(std::make_unique<TriangleSelectorPatch>(*mesh, ebt_colors));
// Reset of TriangleSelector is done inside TriangleSelectorGUI's constructor, so we don't need it to perform it again in deserialize().
m_triangle_selectors.back()->deserialize(mv->seam_facets.get_data(), false);
m_triangle_selectors.back()->request_update_render_data();
}
}
PainterGizmoType GLGizmoSeam::get_painter_type() const
{
return PainterGizmoType::SEAM;
}
wxString GLGizmoSeam::handle_snapshot_action_name(bool shift_down, GLGizmoPainterBase::Button button_down) const
{
wxString action_name;
if (shift_down)
action_name = _L("Remove selection");
else {
if (button_down == Button::Left)
action_name = _L("Enforce seam");
else
action_name = _L("Block seam");
}
return action_name;
}
} // namespace Slic3r::GUI