diff --git a/src/libslic3r/TriangleSelector.cpp b/src/libslic3r/TriangleSelector.cpp index ad292023bb..17c107f980 100644 --- a/src/libslic3r/TriangleSelector.cpp +++ b/src/libslic3r/TriangleSelector.cpp @@ -241,7 +241,7 @@ int TriangleSelector::select_unsplit_triangle(const Vec3f &hit, int facet_idx) c void TriangleSelector::select_patch(int facet_start, std::unique_ptr &&cursor, TriangleStateType new_state, const Transform3d& trafo_no_translate, bool triangle_splitting, float highlight_by_angle_deg) { - assert(facet_start < m_orig_size_indices); + assert(this->is_original_triangle(facet_start)); // Save current cursor center, squared radius and camera direction, so we don't // have to pass it around. @@ -286,11 +286,25 @@ bool TriangleSelector::is_facet_clipped(int facet_idx, const ClippingPlane &clp) return false; } +bool TriangleSelector::is_any_neighbor_selected_by_seed_fill(const Triangle &triangle) { + size_t triangle_idx = &triangle - m_triangles.data(); + assert(triangle_idx < m_triangles.size()); + + for (int neighbor_idx: m_neighbors[triangle_idx]) { + assert(neighbor_idx >= -1); + + if (neighbor_idx >= 0 && m_triangles[neighbor_idx].is_selected_by_seed_fill()) + return true; + } + + return false; +} + void TriangleSelector::seed_fill_select_triangles(const Vec3f &hit, int facet_start, const Transform3d& trafo_no_translate, - const ClippingPlane &clp, float seed_fill_angle, + const ClippingPlane &clp, float seed_fill_angle, float seed_fill_gap_area, float highlight_by_angle_deg, ForceReselection force_reselection) { - assert(facet_start < m_orig_size_indices); + assert(this->is_original_triangle(facet_start)); // Recompute seed fill only if the cursor is pointing on facet unselected by seed fill or a clipping plane is active. if (int start_facet_idx = select_unsplit_triangle(hit, facet_start); start_facet_idx >= 0 && m_triangles[start_facet_idx].is_selected_by_seed_fill() && force_reselection == ForceReselection::NO && !clp.is_active()) @@ -306,6 +320,9 @@ void TriangleSelector::seed_fill_select_triangles(const Vec3f &hit, int facet_st const float highlight_angle_limit = cos(Geometry::deg2rad(highlight_by_angle_deg)); Vec3f vec_down = (trafo_no_translate.inverse() * -Vec3d::UnitZ()).normalized().cast(); + // Facets that need to be checked for gap filling. + std::vector gap_fill_candidate_facets; + // Depth-first traversal of neighbors of the face hit by the ray thrown from the mouse cursor. while (!facet_queue.empty()) { int current_facet = facet_queue.front(); @@ -317,14 +334,15 @@ void TriangleSelector::seed_fill_select_triangles(const Vec3f &hit, int facet_st for (int split_triangle_idx = 0; split_triangle_idx <= m_triangles[current_facet].number_of_split_sides(); ++split_triangle_idx) { assert(split_triangle_idx < int(m_triangles[current_facet].children.size())); assert(m_triangles[current_facet].children[split_triangle_idx] < int(m_triangles.size())); - if (int child = m_triangles[current_facet].children[split_triangle_idx]; !visited[child]) + if (int child = m_triangles[current_facet].children[split_triangle_idx]; !visited[child]) { // Child triangle shares normal with its parent. Select it. facet_queue.push(child); + } } } else m_triangles[current_facet].select_by_seed_fill(); - if (current_facet < m_orig_size_indices) + if (this->is_original_triangle(current_facet)) { // Propagate over the original triangles. for (int neighbor_idx : m_neighbors[current_facet]) { assert(neighbor_idx >= -1); @@ -332,13 +350,102 @@ void TriangleSelector::seed_fill_select_triangles(const Vec3f &hit, int facet_st // Check if neighbour_facet_idx is satisfies angle in seed_fill_angle and append it to facet_queue if it do. const Vec3f &n1 = m_face_normals[m_triangles[neighbor_idx].source_triangle]; const Vec3f &n2 = m_face_normals[m_triangles[current_facet].source_triangle]; - if (std::clamp(n1.dot(n2), 0.f, 1.f) >= facet_angle_limit) + if (std::clamp(n1.dot(n2), 0.f, 1.f) >= facet_angle_limit) { facet_queue.push(neighbor_idx); + } else if (seed_fill_gap_area > 0. && get_triangle_area(m_triangles[neighbor_idx]) <= seed_fill_gap_area) { + gap_fill_candidate_facets.emplace_back(neighbor_idx); + } } } + } } + visited[current_facet] = true; } + + seed_fill_fill_gaps(gap_fill_candidate_facets, seed_fill_gap_area); +} + +void TriangleSelector::seed_fill_fill_gaps(const std::vector &gap_fill_candidate_facets, const float seed_fill_gap_area) { + std::vector visited(m_triangles.size(), false); + + for (const int starting_facet_idx: gap_fill_candidate_facets) { + const Triangle &starting_facet = m_triangles[starting_facet_idx]; + + // If starting_facet_idx was visited from any facet, then we can skip it. + if (visited[starting_facet_idx]) + continue; + + // In the way how gap_fill_candidate_facets is filled, neither of the following two conditions should ever be met. + // But both of those conditions are here to allow more general usage of this method. + if (starting_facet.is_selected_by_seed_fill() || starting_facet.is_split()) { + // Already selected by seed fill or split facet, so no additional actions are required. + visited[starting_facet_idx] = true; + continue; + } else if (!is_any_neighbor_selected_by_seed_fill(starting_facet)) { + // No neighbor triangles are selected by seed fill, so we will skip them for now. + continue; + } + + // Now we have a triangle that has at least one neighbor selected by seed fill. + // So we start depth-first (it doesn't need to be depth-first) traversal of neighbors to check + // if the total area of unselected triangles by seed fill meets the threshold. + double total_gap_area = 0.; + std::queue facet_queue; + std::vector gap_facets; + + facet_queue.push(starting_facet_idx); + while (!facet_queue.empty()) { + const int current_facet_idx = facet_queue.front(); + const Triangle ¤t_facet = m_triangles[current_facet_idx]; + facet_queue.pop(); + + if (visited[current_facet_idx]) + continue; + + if (this->is_original_triangle(current_facet_idx)) + total_gap_area += get_triangle_area(current_facet); + + // We exceed maximum gap area. + if (total_gap_area > seed_fill_gap_area) { + // It is necessary to set every facet inside gap_facets unvisited. + // Otherwise, we incorrectly select facets that are in a gap that is bigger + // than seed_fill_gap_area. + for (const int gap_facet_idx : gap_facets) + visited[gap_facet_idx] = false; + + gap_facets.clear(); + break; + } + + if (current_facet.is_split()) { + for (int split_triangle_idx = 0; split_triangle_idx <= current_facet.number_of_split_sides(); ++split_triangle_idx) { + assert(split_triangle_idx < int(current_facet.children.size())); + assert(current_facet.children[split_triangle_idx] < int(m_triangles.size())); + if (int child = current_facet.children[split_triangle_idx]; !visited[child]) + facet_queue.push(child); + } + } else if (total_gap_area < seed_fill_gap_area) { + gap_facets.emplace_back(current_facet_idx); + } + + if (this->is_original_triangle(current_facet_idx)) { + // Propagate over the original triangles. + for (int neighbor_idx: m_neighbors[current_facet_idx]) { + assert(neighbor_idx >= -1); + if (neighbor_idx >= 0 && !visited[neighbor_idx] && !m_triangles[neighbor_idx].is_selected_by_seed_fill()) + facet_queue.push(neighbor_idx); + } + } + + visited[current_facet_idx] = true; + } + + for (int to_seed_idx : gap_facets) + m_triangles[to_seed_idx].select_by_seed_fill(); + + gap_facets.clear(); + } } void TriangleSelector::precompute_all_neighbors_recursive(const int facet_idx, const Vec3i &neighbors, const Vec3i &neighbors_propagated, std::vector &neighbors_out, std::vector &neighbors_propagated_out) const @@ -913,7 +1020,7 @@ bool TriangleSelector::select_triangle_recursive(int facet_idx, const Vec3i &nei } void TriangleSelector::set_facet(int facet_idx, TriangleStateType state) { - assert(facet_idx < m_orig_size_indices); + assert(this->is_original_triangle(facet_idx)); undivide_triangle(facet_idx); assert(! m_triangles[facet_idx].is_split()); m_triangles[facet_idx].set_state(state); @@ -1866,6 +1973,13 @@ void TriangleSelector::seed_fill_apply_on_triangles(TriangleStateType new_state) } } +double TriangleSelector::get_triangle_area(const Triangle &triangle) const { + const stl_vertex &v0 = m_vertices[triangle.verts_idxs[0]].v; + const stl_vertex &v1 = m_vertices[triangle.verts_idxs[1]].v; + const stl_vertex &v2 = m_vertices[triangle.verts_idxs[2]].v; + return (v1 - v0).cross(v2 - v0).norm() / 2.; +} + TriangleSelector::Cursor::Cursor(const Vec3f &source_, float radius_world, const Transform3d &trafo_, const ClippingPlane &clipping_plane_) : source{source_}, trafo{trafo_.cast()}, clipping_plane{clipping_plane_} { diff --git a/src/libslic3r/TriangleSelector.hpp b/src/libslic3r/TriangleSelector.hpp index 2605d28a52..7ad9f2cca5 100644 --- a/src/libslic3r/TriangleSelector.hpp +++ b/src/libslic3r/TriangleSelector.hpp @@ -320,6 +320,7 @@ public: const Transform3d &trafo_no_translate, // matrix to get from mesh to world without translation const ClippingPlane &clp, // Clipping plane to limit painting to not clipped facets only float seed_fill_angle, // the maximal angle between two facets to be painted by the same color + float seed_fill_gap_area, // The maximal area that will be automatically selected when the surrounding triangles have already been selected. float highlight_by_angle_deg = 0.f, // The maximal angle of overhang. If it is set to a non-zero value, it is possible to paint only the triangles of overhang defined by this angle in degrees. ForceReselection force_reselection = ForceReselection::NO); // force reselection of the triangle mesh even in cases that mouse is pointing on the selected triangle @@ -381,6 +382,9 @@ public: // The operation may merge split triangles if they are being assigned the same color. void seed_fill_apply_on_triangles(TriangleStateType new_state); + // Compute total area of the triangle. + double get_triangle_area(const Triangle &triangle) const; + protected: // Triangle and info about how it's split. class Triangle { @@ -498,6 +502,9 @@ private: void append_touching_subtriangles(int itriangle, int vertexi, int vertexj, std::vector &touching_subtriangles_out) const; void append_touching_edges(int itriangle, int vertexi, int vertexj, std::vector &touching_edges_out) const; + // Check if the triangle index is the original triangle from mesh, or it was additionally created by splitting. + bool is_original_triangle(int triangle_idx) const { return triangle_idx < m_orig_size_indices; } + #ifndef NDEBUG bool verify_triangle_neighbors(const Triangle& tr, const Vec3i& neighbors) const; bool verify_triangle_midpoints(const Triangle& tr) const; @@ -516,6 +523,11 @@ private: void get_seed_fill_contour_recursive(int facet_idx, const Vec3i &neighbors, const Vec3i &neighbors_propagated, std::vector &edges_out) const; + bool is_any_neighbor_selected_by_seed_fill(const Triangle &triangle); + + void seed_fill_fill_gaps(const std::vector &gap_fill_candidate_facets, // Facet of the original mesh (unsplit), which needs to be checked if the surrounding gap can be filled (selected). + float seed_fill_gap_area); // The maximal area that will be automatically selected when the surrounding triangles have already been selected. + int m_free_triangles_head { -1 }; int m_free_vertices_head { -1 }; }; diff --git a/src/slic3r/GUI/Gizmos/GLGizmoFdmSupports.cpp b/src/slic3r/GUI/Gizmos/GLGizmoFdmSupports.cpp index 4b61965454..dde015484d 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoFdmSupports.cpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoFdmSupports.cpp @@ -301,11 +301,12 @@ void GLGizmoFdmSupports::on_render_input_window(float x, float y, float bottom_l ImGui::SameLine(sliders_left_width); ImGui::PushItemWidth(window_width - sliders_left_width - slider_icon_width); - if (m_imgui->slider_float("##smart_fill_angle", &m_smart_fill_angle, SmartFillAngleMin, SmartFillAngleMax, format_str.data(), 1.0f, true, _L("Alt + Mouse wheel"))) - for (auto &triangle_selector : m_triangle_selectors) { + if (m_imgui->slider_float("##smart_fill_angle", &m_smart_fill_angle, SmartFillAngleMin, SmartFillAngleMax, format_str.data(), 1.0f, true, _L("Alt + Mouse wheel"))) { + for (auto &triangle_selector: m_triangle_selectors) { triangle_selector->seed_fill_unselect_all_triangles(); triangle_selector->request_update_render_data(); } + } } ImGui::Separator(); diff --git a/src/slic3r/GUI/Gizmos/GLGizmoMmuSegmentation.cpp b/src/slic3r/GUI/Gizmos/GLGizmoMmuSegmentation.cpp index adb89913e8..fc0ddbb420 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoMmuSegmentation.cpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoMmuSegmentation.cpp @@ -121,6 +121,7 @@ bool GLGizmoMmuSegmentation::on_init() m_desc["tool_bucket_fill"] = _u8L("Bucket fill"); m_desc["smart_fill_angle"] = _u8L("Smart fill angle"); + m_desc["smart_fill_gap_area"] = _u8L("Smart fill gap"); m_desc["split_triangles"] = _u8L("Split triangles"); init_extruders_data(); @@ -452,15 +453,27 @@ void GLGizmoMmuSegmentation::on_render_input_window(float x, float y, float bott } else if(m_tool_type == ToolType::SMART_FILL) { ImGui::AlignTextToFramePadding(); ImGuiPureWrap::text(m_desc["smart_fill_angle"] + ":"); - std::string format_str = std::string("%.f") + I18N::translate_utf8("°", "Degree sign to use in the respective slider in MMU gizmo," - "placed after the number with no whitespace in between."); + std::string format_str_angle = std::string("%.f") + I18N::translate_utf8("°", "Degree sign to use in the respective slider in MMU gizmo," + "placed after the number with no whitespace in between."); ImGui::SameLine(sliders_left_width); ImGui::PushItemWidth(window_width - sliders_left_width - slider_icon_width); - if (m_imgui->slider_float("##smart_fill_angle", &m_smart_fill_angle, SmartFillAngleMin, SmartFillAngleMax, format_str.data(), 1.0f, true, _L("Alt + Mouse wheel"))) - for (auto &triangle_selector : m_triangle_selectors) { + if (m_imgui->slider_float("##smart_fill_angle", &m_smart_fill_angle, SmartFillAngleMin, SmartFillAngleMax, format_str_angle.data(), 1.0f, true, _L("Alt + Mouse wheel"))) { + for (auto &triangle_selector: m_triangle_selectors) { triangle_selector->seed_fill_unselect_all_triangles(); triangle_selector->request_update_render_data(); } + } + + ImGui::AlignTextToFramePadding(); + ImGuiPureWrap::text(m_desc["smart_fill_gap_area"] + ":"); + ImGui::SameLine(sliders_left_width); + ImGui::PushItemWidth(window_width - sliders_left_width - slider_icon_width); + if (m_imgui->slider_float("##smart_fill_gap_area", &m_smart_fill_gap_area, SmartFillGapAreaMin, SmartFillGapAreaMax, "%.2f", 1.0f, true)) { + for (auto &triangle_selector: m_triangle_selectors) { + triangle_selector->seed_fill_unselect_all_triangles(); + triangle_selector->request_update_render_data(); + } + } ImGui::Separator(); } diff --git a/src/slic3r/GUI/Gizmos/GLGizmoPainterBase.cpp b/src/slic3r/GUI/Gizmos/GLGizmoPainterBase.cpp index c130973eec..d2b4c50d39 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoPainterBase.cpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoPainterBase.cpp @@ -480,7 +480,7 @@ bool GLGizmoPainterBase::gizmo_event(SLAGizmoEventType action, const Vec2d& mous const Transform3d trafo_matrix_not_translate = mi->get_transformation().get_matrix_no_offset() * mo->volumes[m_rr.mesh_id]->get_matrix_no_offset(); const Transform3d trafo_matrix = mi->get_transformation().get_matrix() * mo->volumes[m_rr.mesh_id]->get_matrix(); m_triangle_selectors[m_rr.mesh_id]->seed_fill_select_triangles(m_rr.hit, int(m_rr.facet), trafo_matrix_not_translate, this->get_clipping_plane_in_volume_coordinates(trafo_matrix), m_smart_fill_angle, - m_paint_on_overhangs_only ? m_highlight_by_angle_threshold_deg : 0.f, TriangleSelector::ForceReselection::YES); + m_smart_fill_gap_area, m_paint_on_overhangs_only ? m_highlight_by_angle_threshold_deg : 0.f, TriangleSelector::ForceReselection::YES); m_triangle_selectors[m_rr.mesh_id]->request_update_render_data(); m_seed_fill_last_mesh_id = m_rr.mesh_id; } @@ -562,7 +562,7 @@ bool GLGizmoPainterBase::gizmo_event(SLAGizmoEventType action, const Vec2d& mous const int facet_idx = int(projected_mouse_position.facet_idx); m_triangle_selectors[mesh_idx]->seed_fill_apply_on_triangles(new_state); if (m_tool_type == ToolType::SMART_FILL) { - m_triangle_selectors[mesh_idx]->seed_fill_select_triangles(mesh_hit, facet_idx, trafo_matrix_not_translate, clp, m_smart_fill_angle, + m_triangle_selectors[mesh_idx]->seed_fill_select_triangles(mesh_hit, facet_idx, trafo_matrix_not_translate, clp, m_smart_fill_angle, m_smart_fill_gap_area, (m_paint_on_overhangs_only ? m_highlight_by_angle_threshold_deg : 0.f), TriangleSelector::ForceReselection::YES); } else if (m_tool_type == ToolType::BRUSH && m_cursor_type == TriangleSelector::CursorType::POINTER) { m_triangle_selectors[mesh_idx]->bucket_fill_select_triangles(mesh_hit, facet_idx, clp, TriangleSelector::BucketFillPropagate::NO, TriangleSelector::ForceReselection::YES); @@ -647,7 +647,7 @@ bool GLGizmoPainterBase::gizmo_event(SLAGizmoEventType action, const Vec2d& mous assert(m_rr.mesh_id < int(m_triangle_selectors.size())); const TriangleSelector::ClippingPlane &clp = this->get_clipping_plane_in_volume_coordinates(trafo_matrix); if (m_tool_type == ToolType::SMART_FILL) - m_triangle_selectors[m_rr.mesh_id]->seed_fill_select_triangles(m_rr.hit, int(m_rr.facet), trafo_matrix_not_translate, clp, m_smart_fill_angle, + m_triangle_selectors[m_rr.mesh_id]->seed_fill_select_triangles(m_rr.hit, int(m_rr.facet), trafo_matrix_not_translate, clp, m_smart_fill_angle, m_smart_fill_gap_area, m_paint_on_overhangs_only ? m_highlight_by_angle_threshold_deg : 0.f); else if (m_tool_type == ToolType::BRUSH && m_cursor_type == TriangleSelector::CursorType::POINTER) m_triangle_selectors[m_rr.mesh_id]->bucket_fill_select_triangles(m_rr.hit, int(m_rr.facet), clp, TriangleSelector::BucketFillPropagate::NO); diff --git a/src/slic3r/GUI/Gizmos/GLGizmoPainterBase.hpp b/src/slic3r/GUI/Gizmos/GLGizmoPainterBase.hpp index 2f209ca327..6da818f88c 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoPainterBase.hpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoPainterBase.hpp @@ -150,6 +150,7 @@ protected: bool m_triangle_splitting_enabled = true; ToolType m_tool_type = ToolType::BRUSH; float m_smart_fill_angle = 30.f; + float m_smart_fill_gap_area = 0.02f; bool m_paint_on_overhangs_only = false; float m_highlight_by_angle_threshold_deg = 0.f; @@ -162,6 +163,9 @@ protected: static constexpr float SmartFillAngleMax = 90.f; static constexpr float SmartFillAngleStep = 1.f; + static constexpr float SmartFillGapAreaMin = 0.0f; + static constexpr float SmartFillGapAreaMax = 1.f; + // It stores the value of the previous mesh_id to which the seed fill was applied. // It is used to detect when the mouse has moved from one volume to another one. int m_seed_fill_last_mesh_id = -1;