mirror of
https://git.mirrors.martin98.com/https://github.com/prusa3d/PrusaSlicer.git
synced 2025-08-14 15:25:58 +08:00
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:
parent
f253822707
commit
535cbb2567
@ -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 ¤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<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_}
|
||||
{
|
||||
|
@ -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 };
|
||||
};
|
||||
|
@ -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();
|
||||
|
@ -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();
|
||||
}
|
||||
|
@ -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);
|
||||
|
@ -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;
|
||||
|
Loading…
x
Reference in New Issue
Block a user