SPE-2003: Improve the smart fill in the multi-material painting gizmo to automatically select tiny triangles even if they exceed the angle limit.

This commit is contained in:
Lukáš Hejl 2024-03-19 20:59:41 +01:00 committed by Lukas Matena
parent f253822707
commit 535cbb2567
6 changed files with 160 additions and 16 deletions

View File

@ -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> &&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<float>();
// Facets that need to be checked for gap filling.
std::vector<int> 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<int> &gap_fill_candidate_facets, const float seed_fill_gap_area) {
std::vector<bool> 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<int> facet_queue;
std::vector<int> gap_facets;
facet_queue.push(starting_facet_idx);
while (!facet_queue.empty()) {
const int current_facet_idx = facet_queue.front();
const Triangle &current_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<Vec3i> &neighbors_out, std::vector<Vec3i> &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<float>()}, clipping_plane{clipping_plane_}
{

View File

@ -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<int> &touching_subtriangles_out) const;
void append_touching_edges(int itriangle, int vertexi, int vertexj, std::vector<Vec2i> &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<Vec2i> &edges_out) const;
bool is_any_neighbor_selected_by_seed_fill(const Triangle &triangle);
void seed_fill_fill_gaps(const std::vector<int> &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 };
};

View File

@ -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();

View File

@ -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();
}

View File

@ -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);

View File

@ -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;