From d25ceeeb7a2e6476326744699724a69bb6ef1bee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Luk=C3=A1=C5=A1=20Hejl?= Date: Thu, 28 Mar 2024 23:11:32 +0100 Subject: [PATCH] SPE-2003: Add an option to limit selection by the bucket fill using the angle between triangles. Also, it adds to the bucket fill the automatic selection of tiny triangles even if they exceed the angle limit. --- src/libslic3r/TriangleSelector.cpp | 133 +++++++++++++++--- src/libslic3r/TriangleSelector.hpp | 11 ++ .../GUI/Gizmos/GLGizmoMmuSegmentation.cpp | 10 +- src/slic3r/GUI/Gizmos/GLGizmoPainterBase.cpp | 33 +++-- 4 files changed, 150 insertions(+), 37 deletions(-) diff --git a/src/libslic3r/TriangleSelector.cpp b/src/libslic3r/TriangleSelector.cpp index 105eeb5b82..3c4cd62838 100644 --- a/src/libslic3r/TriangleSelector.cpp +++ b/src/libslic3r/TriangleSelector.cpp @@ -316,7 +316,7 @@ void TriangleSelector::seed_fill_select_triangles(const Vec3f &hit, int facet_st std::queue facet_queue; facet_queue.push(facet_start); - const double facet_angle_limit = cos(Geometry::deg2rad(seed_fill_angle)) - EPSILON; + const float facet_angle_limit = cos(Geometry::deg2rad(seed_fill_angle)) - EPSILON; const float highlight_angle_limit = cos(Geometry::deg2rad(highlight_by_angle_deg)); Vec3f vec_down = (trafo_no_translate.inverse() * -Vec3d::UnitZ()).normalized().cast(); @@ -552,9 +552,29 @@ void TriangleSelector::append_touching_edges(int itriangle, int vertexi, int ver process_subtriangle(touching.second, Partition::Second); } +// Returns all triangles that are touching the given facet. +std::vector TriangleSelector::get_all_touching_triangles(int facet_idx, const Vec3i &neighbors, const Vec3i &neighbors_propagated) const { + assert(facet_idx != -1 && facet_idx < int(m_triangles.size())); + assert(this->verify_triangle_neighbors(m_triangles[facet_idx], neighbors)); + + const Vec3i vertices = { m_triangles[facet_idx].verts_idxs[0], m_triangles[facet_idx].verts_idxs[1], m_triangles[facet_idx].verts_idxs[2] }; + + std::vector touching_triangles; + append_touching_subtriangles(neighbors(0), vertices(1), vertices(0), touching_triangles); + append_touching_subtriangles(neighbors(1), vertices(2), vertices(1), touching_triangles); + append_touching_subtriangles(neighbors(2), vertices(0), vertices(2), touching_triangles); + + for (int neighbor_idx: neighbors_propagated) { + if (neighbor_idx != -1 && !m_triangles[neighbor_idx].is_split()) + touching_triangles.emplace_back(neighbor_idx); + } + + return touching_triangles; +} + void TriangleSelector::bucket_fill_select_triangles(const Vec3f &hit, int facet_start, const ClippingPlane &clp, - BucketFillPropagate propagate, - ForceReselection force_reselection) { + float bucket_fill_angle, float bucket_fill_gap_area, + BucketFillPropagate propagate, ForceReselection force_reselection) { int start_facet_idx = select_unsplit_triangle(hit, facet_start); assert(start_facet_idx != -1); // Recompute bucket fill only if the cursor is pointing on facet unselected by bucket fill or a clipping plane is active. @@ -570,26 +590,15 @@ void TriangleSelector::bucket_fill_select_triangles(const Vec3f &hit, int facet_ return; } - auto get_all_touching_triangles = [this](int facet_idx, const Vec3i &neighbors, const Vec3i &neighbors_propagated) -> std::vector { - assert(facet_idx != -1 && facet_idx < int(m_triangles.size())); - assert(this->verify_triangle_neighbors(m_triangles[facet_idx], neighbors)); - std::vector touching_triangles; - Vec3i vertices = {m_triangles[facet_idx].verts_idxs[0], m_triangles[facet_idx].verts_idxs[1], m_triangles[facet_idx].verts_idxs[2]}; - append_touching_subtriangles(neighbors(0), vertices(1), vertices(0), touching_triangles); - append_touching_subtriangles(neighbors(1), vertices(2), vertices(1), touching_triangles); - append_touching_subtriangles(neighbors(2), vertices(0), vertices(2), touching_triangles); - - for (int neighbor_idx : neighbors_propagated) - if (neighbor_idx != -1 && !m_triangles[neighbor_idx].is_split()) - touching_triangles.emplace_back(neighbor_idx); - - return touching_triangles; - }; + const float facet_angle_limit = std::cos(Geometry::deg2rad(bucket_fill_angle)) - EPSILON; auto [neighbors, neighbors_propagated] = this->precompute_all_neighbors(); std::vector visited(m_triangles.size(), false); std::queue facet_queue; + // Facets that need to be checked for gap filling. + std::vector gap_fill_candidate_facets; + facet_queue.push(start_facet_idx); while (!facet_queue.empty()) { int current_facet = facet_queue.front(); @@ -599,18 +608,98 @@ void TriangleSelector::bucket_fill_select_triangles(const Vec3f &hit, int facet_ if (!visited[current_facet]) { m_triangles[current_facet].select_by_seed_fill(); - std::vector touching_triangles = get_all_touching_triangles(current_facet, neighbors[current_facet], neighbors_propagated[current_facet]); - for(const int tr_idx : touching_triangles) { + std::vector touching_triangles = this->get_all_touching_triangles(current_facet, neighbors[current_facet], neighbors_propagated[current_facet]); + for (const int tr_idx: touching_triangles) { if (tr_idx < 0 || visited[tr_idx] || m_triangles[tr_idx].get_state() != start_facet_state || is_facet_clipped(tr_idx, clp)) continue; - assert(!m_triangles[tr_idx].is_split()); - facet_queue.push(tr_idx); + // 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[tr_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) { + assert(!m_triangles[tr_idx].is_split()); + facet_queue.push(tr_idx); + } else if (bucket_fill_gap_area > 0. && get_triangle_area(m_triangles[tr_idx]) <= bucket_fill_gap_area) { + gap_fill_candidate_facets.emplace_back(tr_idx); + } } } visited[current_facet] = true; } + + bucket_fill_fill_gaps(gap_fill_candidate_facets, bucket_fill_gap_area, start_facet_state, neighbors, neighbors_propagated); +} + +void TriangleSelector::bucket_fill_fill_gaps(const std::vector &gap_fill_candidate_facets, const float bucket_fill_gap_area, + const TriangleStateType start_facet_state, const std::vector &neighbors, + const std::vector &neighbors_propagated) { + 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; + } + + // In the way how bucket_fill_select_triangles() is implemented, all gap_fill_candidate_facets + // have 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; + + total_gap_area += get_triangle_area(current_facet); + + // We exceed maximum gap area. + if (total_gap_area > bucket_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 bucket_fill_gap_area. + for (const int gap_facet_idx : gap_facets) + visited[gap_facet_idx] = false; + + gap_facets.clear(); + break; + } + + gap_facets.emplace_back(current_facet_idx); + + std::vector touching_triangles = this->get_all_touching_triangles(current_facet_idx, neighbors[current_facet_idx], neighbors_propagated[current_facet_idx]); + for (const int tr_idx: touching_triangles) { + if (tr_idx < 0 || visited[tr_idx] || m_triangles[tr_idx].get_state() != start_facet_state || m_triangles[tr_idx].is_selected_by_seed_fill()) + continue; + + facet_queue.push(tr_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(); + } } // Selects either the whole triangle (discarding any children it had), or divides diff --git a/src/libslic3r/TriangleSelector.hpp b/src/libslic3r/TriangleSelector.hpp index afafc2ad72..e0849fd13b 100644 --- a/src/libslic3r/TriangleSelector.hpp +++ b/src/libslic3r/TriangleSelector.hpp @@ -348,6 +348,8 @@ public: void bucket_fill_select_triangles(const Vec3f &hit, // point where to start int facet_start, // facet of the original mesh (unsplit) that the hit point belongs to const ClippingPlane &clp, // Clipping plane to limit painting to not clipped facets only + float bucket_fill_angle, // the maximal angle between two facets to be painted by the same color + float bucket_fill_gap_area, // The maximal area that will be automatically selected when the surrounding triangles have already been selected. BucketFillPropagate propagate, // if bucket fill is propagated to neighbor faces or if it fills the only facet of the modified mesh that the hit point belongs to. ForceReselection force_reselection = ForceReselection::NO); // force reselection of the triangle mesh even in cases that mouse is pointing on the selected triangle @@ -523,6 +525,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; + // Returns all triangles that are touching the given facet. + std::vector get_all_touching_triangles(int facet_idx, const Vec3i &neighbors, const Vec3i &neighbors_propagated) 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; } @@ -549,6 +554,12 @@ private: 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. + void bucket_fill_fill_gaps(const std::vector &gap_fill_candidate_facets, // Facet of the mesh (unsplit), which needs to be checked if the surrounding gap can be filled (selected). + float bucket_fill_gap_area, // The maximal area that will be automatically selected when the surrounding triangles have already been selected. + TriangleStateType start_facet_state, // The state of the starting facet that determines which neighbors to consider. + const std::vector &neighbors, + const std::vector &neighbors_propagate); + int m_free_triangles_head { -1 }; int m_free_vertices_head { -1 }; }; diff --git a/src/slic3r/GUI/Gizmos/GLGizmoMmuSegmentation.cpp b/src/slic3r/GUI/Gizmos/GLGizmoMmuSegmentation.cpp index ba3283f2e0..a2465bfafa 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoMmuSegmentation.cpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoMmuSegmentation.cpp @@ -123,6 +123,10 @@ bool GLGizmoMmuSegmentation::on_init() m_desc["smart_fill_angle"] = _u8L("Smart fill angle"); m_desc["smart_fill_gap_area"] = _u8L("Smart fill gap"); + + m_desc["bucket_fill_angle"] = _u8L("Bucket fill angle"); + m_desc["bucket_fill_gap_area"] = _u8L("Bucket fill gap"); + m_desc["split_triangles"] = _u8L("Split triangles"); m_desc["height_range_z_range"] = _u8L("Height range"); @@ -475,9 +479,9 @@ void GLGizmoMmuSegmentation::on_render_input_window(float x, float y, float bott m_imgui->disabled_end(); ImGui::Separator(); - } else if (m_tool_type == ToolType::SMART_FILL) { + } else if (m_tool_type == ToolType::SMART_FILL || m_tool_type == ToolType::BUCKET_FILL) { ImGui::AlignTextToFramePadding(); - ImGuiPureWrap::text(m_desc["smart_fill_angle"] + ":"); + ImGuiPureWrap::text((m_tool_type == ToolType::SMART_FILL ? m_desc["smart_fill_angle"] : m_desc["bucket_fill_angle"]) + ":"); 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); @@ -490,7 +494,7 @@ void GLGizmoMmuSegmentation::on_render_input_window(float x, float y, float bott } ImGui::AlignTextToFramePadding(); - ImGuiPureWrap::text(m_desc["smart_fill_gap_area"] + ":"); + ImGuiPureWrap::text((m_tool_type == ToolType::SMART_FILL ? m_desc["smart_fill_gap_area"] : m_desc["bucket_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)) { diff --git a/src/slic3r/GUI/Gizmos/GLGizmoPainterBase.cpp b/src/slic3r/GUI/Gizmos/GLGizmoPainterBase.cpp index 8199bf60ac..36c309c3f3 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoPainterBase.cpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoPainterBase.cpp @@ -540,18 +540,27 @@ bool GLGizmoPainterBase::gizmo_event(SLAGizmoEventType action, const Vec2d& mous : std::min(m_cursor_radius + this->get_cursor_radius_step(), this->get_cursor_radius_max()); m_parent.set_as_dirty(); return true; - } else if (m_tool_type == ToolType::SMART_FILL) { + } else if (m_tool_type == ToolType::SMART_FILL || m_tool_type == ToolType::BUCKET_FILL) { m_smart_fill_angle = action == SLAGizmoEventType::MouseWheelDown ? std::max(m_smart_fill_angle - SmartFillAngleStep, SmartFillAngleMin) : std::min(m_smart_fill_angle + SmartFillAngleStep, SmartFillAngleMax); m_parent.set_as_dirty(); if (m_rr.mesh_id != -1) { - 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()]; - 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_smart_fill_gap_area, m_paint_on_overhangs_only ? m_highlight_by_angle_threshold_deg : 0.f, TriangleSelector::ForceReselection::YES); + 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()]; + const Transform3d trafo_matrix = mi->get_transformation().get_matrix() * mo->volumes[m_rr.mesh_id]->get_matrix(); + const TriangleSelector::ClippingPlane &clipping_plane = this->get_clipping_plane_in_volume_coordinates(trafo_matrix); + + if (m_tool_type == ToolType::SMART_FILL) { + const Transform3d trafo_matrix_not_translate = mi->get_transformation().get_matrix_no_offset() * mo->volumes[m_rr.mesh_id]->get_matrix_no_offset(); + m_triangle_selectors[m_rr.mesh_id]->seed_fill_select_triangles(m_rr.hit, int(m_rr.facet), trafo_matrix_not_translate, clipping_plane, 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 { + assert(m_tool_type == ToolType::BUCKET_FILL); + m_triangle_selectors[m_rr.mesh_id]->bucket_fill_select_triangles(m_rr.hit, int(m_rr.facet), clipping_plane, m_smart_fill_angle, m_smart_fill_gap_area, + TriangleSelector::BucketFillPropagate::YES, TriangleSelector::ForceReselection::YES); + } + m_triangle_selectors[m_rr.mesh_id]->request_update_render_data(); m_seed_fill_last_mesh_id = m_rr.mesh_id; } @@ -641,9 +650,9 @@ bool GLGizmoPainterBase::gizmo_event(SLAGizmoEventType action, const Vec2d& mous 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); + m_triangle_selectors[mesh_idx]->bucket_fill_select_triangles(mesh_hit, facet_idx, clp, m_smart_fill_angle, m_smart_fill_gap_area, TriangleSelector::BucketFillPropagate::NO, TriangleSelector::ForceReselection::YES); } else if (m_tool_type == ToolType::BUCKET_FILL) { - m_triangle_selectors[mesh_idx]->bucket_fill_select_triangles(mesh_hit, facet_idx, clp, TriangleSelector::BucketFillPropagate::YES, TriangleSelector::ForceReselection::YES); + m_triangle_selectors[mesh_idx]->bucket_fill_select_triangles(mesh_hit, facet_idx, clp, m_smart_fill_angle, m_smart_fill_gap_area, TriangleSelector::BucketFillPropagate::YES, TriangleSelector::ForceReselection::YES); } m_seed_fill_last_mesh_id = -1; @@ -736,9 +745,9 @@ bool GLGizmoPainterBase::gizmo_event(SLAGizmoEventType action, const Vec2d& mous 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); + m_triangle_selectors[m_rr.mesh_id]->bucket_fill_select_triangles(m_rr.hit, int(m_rr.facet), clp, m_smart_fill_angle, m_smart_fill_gap_area, TriangleSelector::BucketFillPropagate::NO); else if (m_tool_type == ToolType::BUCKET_FILL) - m_triangle_selectors[m_rr.mesh_id]->bucket_fill_select_triangles(m_rr.hit, int(m_rr.facet), clp, TriangleSelector::BucketFillPropagate::YES); + m_triangle_selectors[m_rr.mesh_id]->bucket_fill_select_triangles(m_rr.hit, int(m_rr.facet), clp, m_smart_fill_angle, m_smart_fill_gap_area, TriangleSelector::BucketFillPropagate::YES); m_triangle_selectors[m_rr.mesh_id]->request_update_render_data(); m_seed_fill_last_mesh_id = m_rr.mesh_id; return true;