OrcaSlicer/src/slic3r/GUI/Gizmos/GLGizmoFdmSupports.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

902 lines
36 KiB
C++

#include "GLGizmoFdmSupports.hpp"
#include "libslic3r/Model.hpp"
//BBS
#include "libslic3r/Layer.hpp"
#include "libslic3r/Thread.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/format.hpp"
#include "slic3r/GUI/GUI.hpp"
#include "slic3r/Utils/UndoRedo.hpp"
#include <GL/glew.h>
#include <boost/log/trivial.hpp>
namespace Slic3r::GUI {
GLGizmoFdmSupports::GLGizmoFdmSupports(GLCanvas3D& parent, const std::string& icon_filename, unsigned int sprite_id)
: GLGizmoPainterBase(parent, icon_filename, sprite_id), m_current_tool(ImGui::CircleButtonIcon)
{
m_tool_type = ToolType::BRUSH;
m_cursor_type = TriangleSelector::CursorType::CIRCLE;
}
void GLGizmoFdmSupports::on_shutdown()
{
//BBS
//wait the thread
if (m_thread.joinable()) {
Print *print = m_print_instance.print_object->print();
if (print) {
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << "cancel the print";
print->cancel();
}
//join the thread
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << "try to join thread for 2000 ms";
auto ret = m_thread.try_join_for(boost::chrono::milliseconds(2000));
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << "join thread returns "<<ret;
}
m_print_instance.print_object = NULL;
m_print_instance.model_instance = NULL;
m_highlight_by_angle_threshold_deg = 0.f;
m_parent.use_slope(false);
m_parent.toggle_model_objects_visibility(true);
}
//BBS: add on_open
void GLGizmoFdmSupports::on_opening()
{
m_angle_threshold_deg = 40;
m_parent.set_slope_normal_angle(90.f - m_angle_threshold_deg);
if (! m_parent.is_using_slope()) {
m_parent.use_slope(true);
m_parent.set_as_dirty();
}
m_print_instance.print_object = NULL;
m_print_instance.model_instance = NULL;
m_edit_state = state_idle;
m_volume_ready = false;
m_volume_valid = false;
}
std::string GLGizmoFdmSupports::on_get_name() const
{
return _u8L("Support Painting");
}
bool GLGizmoFdmSupports::on_init()
{
// BBS
m_shortcut_key = WXK_CONTROL_L;
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("Pen size");
m_desc["enforce_caption"] = _L("Left mouse button");
m_desc["enforce"] = _L("Enforce supports");
m_desc["block_caption"] = _L("Right mouse button");
m_desc["block"] = _L("Block supports");
m_desc["remove_caption"] = _L("Shift + Left mouse button");
m_desc["remove"] = _L("Erase");
m_desc["remove_all"] = _L("Erase all painting");
m_desc["highlight_by_angle"] = _L("Highlight overhang areas");
m_desc["gap_fill"] = _L("Gap fill");
m_desc["perform"] = _L("Perform");
m_desc["gap_area_caption"] = _L("Ctrl + Mouse wheel");
m_desc["gap_area"] = _L("Gap area");
m_desc["tool_type"] = _L("Tool type");
m_desc["smart_fill_angle_caption"] = _L("Ctrl + Mouse wheel");
m_desc["smart_fill_angle"] = _L("Smart fill angle");
m_desc["on_overhangs_only"] = _L("On overhangs only");
memset(&m_print_instance, 0, sizeof(m_print_instance));
return true;
}
void GLGizmoFdmSupports::render_painter_gizmo()
{
const Selection& selection = m_parent.get_selection();
glsafe(::glEnable(GL_BLEND));
glsafe(::glEnable(GL_DEPTH_TEST));
render_triangles(selection);
//BBS: draw support volumes
if (m_volume_ready && m_support_volume && (m_edit_state != state_generating))
{
// TODO: FIXME
m_support_volume->set_render_color({0.f, 0.7f, 0.f, 0.7f});
m_support_volume->render();
}
m_c->object_clipper()->render_cut();
m_c->instances_hider()->render_cut();
render_cursor();
glsafe(::glDisable(GL_BLEND));
}
// BBS
bool GLGizmoFdmSupports::on_key_down_select_tool_type(int keyCode) {
switch (keyCode)
{
case 'F':
m_current_tool = ImGui::FillButtonIcon;
break;
case 'S':
m_current_tool = ImGui::SphereButtonIcon;
break;
case 'C':
m_current_tool = ImGui::CircleButtonIcon;
break;
case 'G':
m_current_tool = ImGui::GapFillIcon;
break;
default:
return false;
break;
}
return true;
}
void GLGizmoFdmSupports::on_set_state()
{
GLGizmoPainterBase::on_set_state();
if (get_state() == On) {
m_support_threshold_angle = -1;
}
else if (get_state() == Off) {
ModelObject* mo = m_c->selection_info()->model_object();
if (mo) Slic3r::save_object_mesh(*mo);
}
}
void GLGizmoFdmSupports::on_render_input_window(float x, float y, float bottom_limit)
{
init_print_instance();
if (! m_c->selection_info()->model_object())
return;
// BBS
wchar_t old_tool = m_current_tool;
int support_threshold_angle = get_selection_support_threshold_angle();
// when support painting tool is on, reset highlight threshold angle
if (m_support_threshold_angle == -1) {
m_highlight_by_angle_threshold_deg = support_threshold_angle;
m_parent.set_slope_normal_angle(90.f - m_highlight_by_angle_threshold_deg);
}
m_support_threshold_angle = support_threshold_angle;
const float approx_height = m_imgui->scaled(23.f);
y = std::min(y, bottom_limit - approx_height);
GizmoImguiSetNextWIndowPos(x, y, ImGuiCond_Always, 0.0f, 0.0f);
//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 = m_imgui->calc_text_size(m_desc.at("clipping_of_view")).x + m_imgui->scaled(1.5f);
const float cursor_slider_left = m_imgui->calc_text_size(m_desc.at("cursor_size")).x + m_imgui->scaled(1.5f);
const float gap_fill_slider_left = m_imgui->calc_text_size(m_desc.at("gap_fill")).x + m_imgui->scaled(1.5f);
const float highlight_slider_left = m_imgui->calc_text_size(m_desc.at("highlight_by_angle")).x + m_imgui->scaled(1.5f);
const float reset_button_slider_left = m_imgui->calc_text_size(m_desc.at("reset_direction")).x + m_imgui->scaled(1.5f) + ImGui::GetStyle().FramePadding.x * 2;
const float on_overhangs_only_width = m_imgui->calc_text_size(m_desc["on_overhangs_only"]).x + m_imgui->scaled(1.5f);
const float remove_btn_width = m_imgui->calc_text_size(m_desc.at("remove_all")).x + m_imgui->scaled(1.5f);
const float filter_btn_width = m_imgui->calc_text_size(m_desc.at("perform")).x + m_imgui->scaled(1.5f);
const float gap_area_txt_width = m_imgui->calc_text_size(m_desc.at("gap_area")).x + m_imgui->scaled(1.5f);
const float smart_fill_angle_txt_width = m_imgui->calc_text_size(m_desc.at("smart_fill_angle")).x + m_imgui->scaled(1.5f);
const float buttons_width = remove_btn_width + filter_btn_width + m_imgui->scaled(1.5f);
const float empty_button_width = m_imgui->calc_button_size("").x;
const float tips_width = m_imgui->calc_text_size(_L("Auto support threshold angle: ") + " 90 ").x + m_imgui->scaled(1.5f);
const float minimal_slider_width = m_imgui->scaled(4.f);
float caption_max = 0.f;
float total_text_max = 0.f;
for (const auto &t : std::array<std::string, 5>{"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);
}
total_text_max += caption_max + m_imgui->scaled(1.f);
caption_max += m_imgui->scaled(1.f);
const float sliders_left_width = std::max(gap_area_txt_width, std::max(smart_fill_angle_txt_width, std::max(reset_button_slider_left, std::max(std::max(cursor_slider_left, clipping_slider_left), std::max(highlight_slider_left, gap_fill_slider_left)))));
const float slider_icon_width = m_imgui->get_slider_icon_size().x;
const float max_tooltip_width = ImGui::GetFontSize() * 20.0f;
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;
float drag_pos_times = 0.7;
ImGui::AlignTextToFramePadding();
m_imgui->text(m_desc.at("tool_type"));
std::array<wchar_t, 4> tool_ids = { ImGui::CircleButtonIcon, ImGui::SphereButtonIcon, ImGui::FillButtonIcon, ImGui::GapFillIcon };
std::array<wchar_t, 4> icons;
if (m_is_dark_mode)
icons = { ImGui::CircleButtonDarkIcon, ImGui::SphereButtonDarkIcon, ImGui::FillButtonDarkIcon, ImGui::GapFillDarkIcon };
else
icons = { ImGui::CircleButtonIcon, ImGui::SphereButtonIcon, ImGui::FillButtonIcon, ImGui::GapFillIcon };
std::array<wxString, 4> tool_tips = { _L("Circle"), _L("Sphere"), _L("Fill"), _L("Gap Fill") };
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);
}
}
m_imgui->bbl_checkbox(m_desc["on_overhangs_only"], m_paint_on_overhangs_only);
if (ImGui::IsItemHovered())
m_imgui->tooltip(format_wxstr(_L("Allows painting only on facets selected by: \"%1%\""), m_desc["highlight_by_angle"]), max_tooltip_width);
ImGui::Separator();
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;
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");
} 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");
} else if (m_current_tool == ImGui::FillButtonIcon) {
m_cursor_type = TriangleSelector::CursorType::POINTER;
m_tool_type = ToolType::SMART_FILL;
ImGui::AlignTextToFramePadding();
m_imgui->text(m_desc.at("smart_fill_angle"));
std::string format_str = std::string("%.f") + I18N::translate_utf8("", "Face angle threshold, placed after the number with no whitespace in between.");
ImGui::SameLine(sliders_left_width);
ImGui::PushItemWidth(sliders_width);
if (m_imgui->bbl_slider_float_style("##smart_fill_angle", &m_smart_fill_angle, SmartFillAngleMin, SmartFillAngleMax, format_str.data(), 1.0f, true))
for (auto& triangle_selector : m_triangle_selectors) {
triangle_selector->seed_fill_unselect_all_triangles();
triangle_selector->request_update_render_data();
}
ImGui::SameLine(drag_left_width);
ImGui::PushItemWidth(1.5 * slider_icon_width);
ImGui::BBLDragFloat("##smart_fill_angle_input", &m_smart_fill_angle, 0.05f, 0.0f, 0.0f, "%.2f");
} else if (m_current_tool == ImGui::GapFillIcon) {
m_tool_type = ToolType::GAP_FILL;
m_cursor_type = TriangleSelector::CursorType::POINTER;
ImGui::AlignTextToFramePadding();
m_imgui->text(m_desc["gap_area"]);
ImGui::SameLine(sliders_left_width);
ImGui::PushItemWidth(sliders_width);
std::string format_str = std::string("%.2f") + I18N::translate_utf8("", "Triangle patch area threshold,""triangle patch will be merged to neighbor if its area is less than threshold");
m_imgui->bbl_slider_float_style("##gap_area", &TriangleSelectorPatch::gap_area, TriangleSelectorPatch::GapAreaMin, TriangleSelectorPatch::GapAreaMax, format_str.data(), 1.0f, true);
ImGui::SameLine(drag_left_width);
ImGui::PushItemWidth(1.5 * slider_icon_width);
ImGui::BBLDragFloat("##gap_area_input", &TriangleSelectorPatch::gap_area, 0.05f, 0.0f, 0.0f, "%.2f");
}
float position_before_text_y = ImGui::GetCursorPos().y;
ImGui::AlignTextToFramePadding();
m_imgui->text(m_desc["highlight_by_angle"]);
ImGui::AlignTextToFramePadding();
float position_after_text_y = ImGui::GetCursorPos().y;
ImGui::SameLine(sliders_left_width);
float slider_height = m_imgui->get_slider_float_height();
// Makes slider to be aligned to bottom of the multi-line text.
float slider_start_position_y = std::max(position_before_text_y, position_after_text_y - slider_height);
ImGui::SetCursorPosY(slider_start_position_y);
std::string format_str = std::string("%.f");
ImGui::PushItemWidth(sliders_width);
wxString tooltip = _L("Highlight faces according to overhang angle.");
if (m_imgui->bbl_slider_float_style("##angle_threshold_deg", &m_highlight_by_angle_threshold_deg, 0.f, 90.f, format_str.data(), 1.0f, true, tooltip)) {
m_parent.set_slope_normal_angle(90.f - m_highlight_by_angle_threshold_deg);
if (!m_parent.is_using_slope()) {
m_parent.use_slope(true);
m_parent.set_as_dirty();
}
}
if (ImGui::IsItemHovered()) {
ImGui::BeginTooltip();
if (m_support_threshold_angle != 0) {
wxString str_tooltip = (_L("Auto support threshold angle: ") + std::to_string(m_support_threshold_angle));
m_imgui->text_colored(ImGuiWrapper::COL_WINDOW_BG, str_tooltip);
} else {
wxString s_tooltip = (_L("No auto support"));
m_imgui->text_colored(ImGuiWrapper::COL_WINDOW_BG, s_tooltip);
}
ImGui::EndTooltip();
}
ImGui::SameLine(drag_left_width);
ImGui::PushItemWidth(1.5 * slider_icon_width);
ImGui::BBLDragFloat("##angle_threshold_deg_input", &m_highlight_by_angle_threshold_deg, 0.05f, 0.0f, 0.0f, "%.2f");
if (m_current_tool != ImGui::GapFillIcon) {
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 b_bbl_slider_float = 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_drag_input = ImGui::BBLDragFloat("##clp_dist_input", &clp_dist, 0.05f, 0.0f, 0.0f, "%.2f");
if (b_bbl_slider_float || b_drag_input) m_c->object_clipper()->set_position_by_ratio(clp_dist, true);
}
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();
// Perform button is for gap fill
if (m_current_tool == ImGui::GapFillIcon) {
if (m_imgui->button(m_desc.at("perform"))) {
Plater::TakeSnapshot snapshot(wxGetApp().plater(), "Reset selection", UndoRedo::SnapshotType::GizmoAction);
for (int i = 0; i < m_triangle_selectors.size(); i++) {
TriangleSelectorPatch* ts_mm = dynamic_cast<TriangleSelectorPatch*>(m_triangle_selectors[i].get());
ts_mm->update_selector_triangles();
ts_mm->request_update_render_data(true);
}
update_model_object();
m_parent.set_as_dirty();
}
}
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();
}
void GLGizmoFdmSupports::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 GLGizmoFdmSupports::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 + 15.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) {
// BBS
m_imgui->text_colored(ImGuiWrapper::COL_ACTIVE, caption);
ImGui::SameLine(caption_max);
m_imgui->text_colored(ImGuiWrapper::COL_WINDOW_BG, text);
};
std::vector<std::string> tip_items;
switch (m_tool_type) {
case ToolType::BRUSH:
tip_items = {"enforce", "block", "remove", "cursor_size", "clipping_of_view"};
break;
case ToolType::BUCKET_FILL:
break;
case ToolType::SMART_FILL:
tip_items = {"enforce", "block", "remove", "smart_fill_angle", "clipping_of_view"};
break;
case ToolType::GAP_FILL:
tip_items = {"gap_area"};
break;
default:
break;
}
for (const auto &t : tip_items) draw_text_with_caption(m_desc.at(t + "_caption") + ": ", m_desc.at(t));
ImGui::EndTooltip();
}
ImGui::PopStyleVar(2);
}
// BBS
int GLGizmoFdmSupports::get_selection_support_threshold_angle()
{
auto sel_info = m_c->selection_info();
if (sel_info == nullptr)
return -1;
const DynamicPrintConfig& obj_cfg = sel_info->model_object()->config.get();
const DynamicPrintConfig& glb_cfg = wxGetApp().preset_bundle->prints.get_edited_preset().config;
bool enable_support = obj_cfg.option("enable_support") ? obj_cfg.opt_bool("enable_support") : glb_cfg.opt_bool("enable_support");
SupportType support_type = obj_cfg.option("support_type") ? obj_cfg.opt_enum<SupportType>("support_type") : glb_cfg.opt_enum<SupportType>("support_type");
int support_threshold_angle = obj_cfg.option("support_threshold_angle") ? obj_cfg.opt_int("support_threshold_angle") : glb_cfg.opt_int("support_threshold_angle");
bool auto_support = enable_support && is_auto(support_type);
return auto_support ? support_threshold_angle : 0;
}
void GLGizmoFdmSupports::select_facets_by_angle(float threshold_deg, bool block)
{
float threshold = (float(M_PI)/180.f)*threshold_deg;
const Selection& selection = m_parent.get_selection();
const ModelObject* mo = m_c->selection_info()->model_object();
const ModelInstance* mi = mo->instances[selection.get_instance_idx()];
int mesh_id = -1;
for (const ModelVolume* mv : mo->volumes) {
if (! mv->is_model_part())
continue;
++mesh_id;
const Transform3d trafo_matrix = mi->get_matrix_no_offset() * mv->get_matrix_no_offset();
Vec3f down = (trafo_matrix.inverse() * (-Vec3d::UnitZ())).cast<float>().normalized();
Vec3f limit = (trafo_matrix.inverse() * Vec3d(std::sin(threshold), 0, -std::cos(threshold))).cast<float>().normalized();
float dot_limit = limit.dot(down);
// Now calculate dot product of vert_direction and facets' normals.
int idx = 0;
const indexed_triangle_set &its = mv->mesh().its;
for (const stl_triangle_vertex_indices &face : its.indices) {
if (its_face_normal(its, face).dot(down) > dot_limit) {
m_triangle_selectors[mesh_id]->set_facet(idx, block ? EnforcerBlockerType::BLOCKER : EnforcerBlockerType::ENFORCER);
m_triangle_selectors.back()->request_update_render_data();
}
++ idx;
}
}
Plater::TakeSnapshot snapshot(wxGetApp().plater(), block ? "Block supports by angle"
: "Add supports by angle");
update_model_object();
m_parent.set_as_dirty();
}
//BBS: remove const
void GLGizmoFdmSupports::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->supported_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: invalid volume_support status
invalid_support_volumes(true);
}
//BBS
void GLGizmoFdmSupports::update_from_model_object(bool first_update)
{
wxBusyCursor wait;
const ModelObject* mo = m_c->selection_info()->model_object();
m_triangle_selectors.clear();
//BBS: add timestamp logic
m_volume_timestamps.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->supported_facets.get_data(), false);
m_triangle_selectors.back()->request_update_render_data();
//BBS: add timestamp logic
m_volume_timestamps.emplace_back(mv->supported_facets.timestamp());
}
//BBS: invalid volume_support status
invalid_support_volumes(true);
}
PainterGizmoType GLGizmoFdmSupports::get_painter_type() const
{
return PainterGizmoType::FDM_SUPPORTS;
}
wxString GLGizmoFdmSupports::handle_snapshot_action_name(bool shift_down, GLGizmoPainterBase::Button button_down) const
{
// BBS remove _L()
wxString action_name;
if (shift_down)
action_name = ("Unselect all");
else {
if (button_down == Button::Left)
action_name = ("Enforce supports");
else
action_name = ("Block supports");
}
return action_name;
}
//BBS
void GLGizmoFdmSupports::init_print_instance()
{
const PrintObject* print_object = NULL;
PrintInstance print_instance = { 0 };
const Print *print = m_parent.fff_print();
if (!m_c->selection_info() || (m_print_instance.print_object))
{
//no selection or already got a print instance before
return;
}
const ModelObject* model_object = m_c->selection_info()->model_object();
int instance_index = m_c->selection_info()->get_active_instance();
const ModelInstance* model_instance = model_object->instances[instance_index];
//check the print
if (!print)
{
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ",print invalid\n";
return;
}
for (const PrintObject* object : print->objects())
{
if (object->model_object()->id() == model_object->id())
{
BOOST_LOG_TRIVIAL(trace) << __FUNCTION__ << ",found a PrintObject, id is" << model_object->id().id;
print_object = object;
break;
}
}
//check the pring object
if (!print_object)
{
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ",can not find a PrintObject\n";
return;
}
//find the print instance
for (const PrintInstance &instance : print_object->instances())
{
if (instance.model_instance->id() == model_instance->id())
{
BOOST_LOG_TRIVIAL(trace) << __FUNCTION__ << ",found a PrintInstance, id is" << model_instance->id().id;
m_print_instance = instance;
break;
}
}
//check the pring object
if (!m_print_instance.print_object)
{
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ",can not find a PrintInstance\n";
return;
}
const PrintObjectConfig& config = m_print_instance.print_object->config();
m_angle_threshold_deg = config.support_angle;
m_is_tree_support = config.enable_support.value && is_tree(config.support_type.value);
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ",get support_angle "<< m_angle_threshold_deg<<", is_tree "<<m_is_tree_support;
return;
}
void GLGizmoFdmSupports::invalid_support_volumes(bool invalid_step)
{
std::unique_lock<std::mutex> lck(m_mutex);
m_volume_valid = false;
if ((invalid_step) && (m_edit_state == state_generating) && m_print_instance.print_object)
{
Print *print = m_print_instance.print_object->print();
if (print) {
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << "cancel the print";
print->cancel();
}
}
m_edit_state = state_idle;
lck.unlock();
return;
}
bool GLGizmoFdmSupports::need_regenerate_support_volumes()
{
if (!m_support_volume)
return true;
const ModelObject* mo = m_c->selection_info()->model_object();
if (m_object_id != m_print_instance.print_object->id().id)
{
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ",object_id changed from " << m_object_id << " to " << m_print_instance.print_object->id().id << ", need to regenerate";
return true;
}
int volume_id = -1;
for (const ModelVolume* mv : mo->volumes) {
if (! mv->is_model_part())
continue;
++volume_id;
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ",volume_id "<<volume_id<<", record_timestamp "<< m_volume_timestamps[volume_id]
<<", current_timestamp "<<mv->supported_facets.timestamp();
if (m_volume_timestamps[volume_id] != mv->supported_facets.timestamp())
{
return true;
}
}
return false;
}
void GLGizmoFdmSupports::update_support_volumes()
{
//PrintInstance m_print_instance = get_current_print_instance();
if ((!m_print_instance.print_object))
{
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ",invalid param, m_volume_ready="<< m_volume_ready;
return;
}
if (m_volume_valid || !need_regenerate_support_volumes())
{
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ",no need to regenerate support volume, return directly";
std::unique_lock<std::mutex> lck(m_mutex);
m_volume_ready = true;
m_volume_valid = true;
m_edit_state = state_ready;
lck.unlock();
return;
}
//generate_support_preview in async mode
std::unique_lock<std::mutex> lck(m_mutex);
m_volume_ready = false;
//destroy previous support volume
if (m_support_volume)
{
delete m_support_volume;
m_support_volume = NULL;
}
lck.unlock();
if (m_thread.joinable()) {
//join the thread in ui thread
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << "try to join thread for 100 ms";
auto ret = m_thread.try_join_for(boost::chrono::milliseconds(100));
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << "join thread returns "<<ret;
}
m_cancel = false;
m_thread = create_thread([this]{this->run_thread();});
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ",created thread to generate support volumes";
return;
}
void GLGizmoFdmSupports::run_thread()
{
try {
Print *print = m_print_instance.print_object->print();
print->restart();
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ",before generate_support_preview";
m_print_instance.print_object->generate_support_preview();
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ",after generate_support_preview";
if (m_cancel)
{
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ", cancelled";
goto _finished;
}
std::unique_lock<std::mutex> lck(m_mutex);
m_support_volume = new GLVolume(0.5f, 0.5f, 0.5f, 0.5f);
//m_support_volume->is_support_part = true;
m_support_volume->force_native_color = true;
m_support_volume->set_render_color();
lck.unlock();
auto record_timestamp = [this]()
{
const ModelObject* mo = m_c->selection_info()->model_object();
int volume_id = -1;
for (const ModelVolume* mv : mo->volumes) {
if (!mv->is_model_part())
continue;
++volume_id;
m_volume_timestamps[volume_id] = mv->supported_facets.timestamp();
}
};
if (m_cancel)
{
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ", cancelled";
goto _finished;
}
if (!m_print_instance.print_object->support_layers().size())
{
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ",no support layer found, update status to 100%\n";
print->set_status(100, L("Support Generated"));
goto _finished;
}
GLModel::Geometry init_data;
init_data.format = { GLModel::Geometry::EPrimitiveType::Triangles, GLModel::Geometry::EVertexLayout::P3N3 };
for (const SupportLayer *support_layer : m_print_instance.print_object->support_layers())
{
for (const ExtrusionEntity *extrusion_entity : support_layer->support_fills.entities)
{
_3DScene::extrusionentity_to_verts(extrusion_entity, float(support_layer->print_z), m_print_instance.shift, init_data);
}
}
m_support_volume->model.init_from(std::move(init_data));
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ", finished extrusionentity_to_verts, update status to 100%";
print->set_status(100, L("Support Generated"));
record_timestamp();
}
catch (...) {
BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << ",exception catched, mostly cancelling from gui!";
//wxTheApp->OnUnhandledException();
}
_finished:
std::unique_lock<std::mutex> lck(m_mutex);
if (m_edit_state == state_generating)
m_edit_state = state_ready;
lck.unlock();
m_parent.set_as_dirty();
m_parent.post_event(SimpleEvent(wxEVT_PAINT));
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ", finished all";
return;
}
void GLGizmoFdmSupports::generate_support_volume()
{
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ",before finalize_geometry";
std::unique_lock<std::mutex> lck(m_mutex);
m_volume_ready = true;
m_volume_valid = true;
m_object_id = m_print_instance.print_object->id().id;
lck.unlock();
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ",finished finalize_geometry";
}
} // namespace Slic3r::GUI