diff --git a/src/libslic3r/CMakeLists.txt b/src/libslic3r/CMakeLists.txt index 28865149f..f9bef903b 100644 --- a/src/libslic3r/CMakeLists.txt +++ b/src/libslic3r/CMakeLists.txt @@ -178,6 +178,8 @@ add_library(libslic3r STATIC PrintRegion.cpp PNGReadWrite.hpp PNGReadWrite.cpp + QuadricEdgeCollapse.cpp + QuadricEdgeCollapse.hpp Semver.cpp ShortestPath.cpp ShortestPath.hpp diff --git a/src/libslic3r/MutablePriorityQueue.hpp b/src/libslic3r/MutablePriorityQueue.hpp index 68cd01859..cc1cae68c 100644 --- a/src/libslic3r/MutablePriorityQueue.hpp +++ b/src/libslic3r/MutablePriorityQueue.hpp @@ -354,7 +354,7 @@ inline void MutableSkipHeapPriorityQueue::max()); + m_index_setter(m_heap[1], std::numeric_limits::max()); } // Zero'th element is padding, thus non-empty queue must have at least two elements. if (m_heap.size() > 2) { diff --git a/src/libslic3r/QuadricEdgeCollapse.cpp b/src/libslic3r/QuadricEdgeCollapse.cpp new file mode 100644 index 000000000..a66dbc192 --- /dev/null +++ b/src/libslic3r/QuadricEdgeCollapse.cpp @@ -0,0 +1,654 @@ +#include "QuadricEdgeCollapse.hpp" +#include +#include "MutablePriorityQueue.hpp" +#include "SimplifyMeshImpl.hpp" +#include + +using namespace Slic3r; + +// only private namespace not neccessary be in hpp +namespace QuadricEdgeCollapse { + using Vertices = std::vector; + using Triangle = stl_triangle_vertex_indices; + using Indices = std::vector; + using SymMat = SimplifyMesh::implementation::SymetricMatrix; + + // smallest error caused by edges, identify smallest edge in triangle + struct Error + { + float value = -1.; // identifying of smallest edge is stored inside of TriangleInfo + uint32_t triangle_index = 0; + Error(float value, uint32_t triangle_index) + : value(value) + , triangle_index(triangle_index) + {} + Error() = default; + }; + using Errors = std::vector; + + // merge information together - faster access during processing + struct TriangleInfo { + Vec3f n; // normalized normal - used for check when fliped + + // range(0 .. 2), + unsigned char min_index = 0; // identify edge for minimal Error -> lightweight Error structure + + TriangleInfo() = default; + bool is_deleted() const { return n.x() > 2.f; } + void set_deleted() { n.x() = 3.f; } + }; + using TriangleInfos = std::vector; + struct VertexInfo { + SymMat q; // sum quadric of surround triangles + uint32_t start = 0, count = 0; // vertex neighbor triangles + VertexInfo() = default; + bool is_deleted() const { return count == 0; } + }; + using VertexInfos = std::vector; + struct EdgeInfo { + uint32_t t_index=0; // triangle index + unsigned char edge = 0; // 0 or 1 or 2 + EdgeInfo() = default; + }; + using EdgeInfos = std::vector; + + // DTO for change neighbors + struct CopyEdgeInfo { + uint32_t start; + uint32_t count; + uint32_t move; + CopyEdgeInfo(uint32_t start, uint32_t count, uint32_t move) + : start(start), count(count), move(move) + {} + }; + using CopyEdgeInfos = std::vector; + + Vec3d create_normal(const Triangle &triangle, const Vertices &vertices); + std::array create_vertices(uint32_t id_v1, uint32_t id_v2, const Vertices &vertices); + std::array vertices_error(const SymMat &q, const std::array &vertices); + double calculate_determinant(const SymMat &q); + double calculate_error(uint32_t id_v1, uint32_t id_v2, const SymMat & q, const Vertices &vertices); + Vec3f calculate_vertex(uint32_t id_v1, uint32_t id_v2, const SymMat & q, const Vertices &vertices); + Vec3d calculate_vertex(double det, const SymMat &q); + // calculate error for vertex and quadrics, triangle quadrics and triangle vertex give zero, only pozitive number + double vertex_error(const SymMat &q, const Vec3d &vertex); + SymMat create_quadric(const Triangle &t, const Vec3d& n, const Vertices &vertices); + std::tuple init(const indexed_triangle_set &its); + std::optional find_triangle_index1(uint32_t vi, const VertexInfo& v_info, + uint32_t ti, const EdgeInfos& e_infos, const Indices& indices); + bool is_flipped(const Vec3f &new_vertex, uint32_t ti0, uint32_t ti1, const VertexInfo& v_info, + const TriangleInfos &t_infos, const EdgeInfos &e_infos, const indexed_triangle_set &its); + + // find edge with smallest error in triangle + Vec3d calculate_3errors(const Triangle &t, const Vertices &vertices, const VertexInfos &v_infos); + Error calculate_error(uint32_t ti, const Triangle& t,const Vertices &vertices, const VertexInfos& v_infos, unsigned char& min_index); + void remove_triangle(EdgeInfos &e_infos, VertexInfo &v_info, uint32_t ti); + void change_neighbors(EdgeInfos &e_infos, VertexInfos &v_infos, uint32_t ti0, uint32_t ti1, + uint32_t vi0, uint32_t vi1, uint32_t vi_top0, + const Triangle &t1, CopyEdgeInfos& infos, EdgeInfos &e_infos1); + void compact(const VertexInfos &v_infos, const TriangleInfos &t_infos, const EdgeInfos &e_infos, indexed_triangle_set &its); +} // namespace QuadricEdgeCollapse + +using namespace QuadricEdgeCollapse; + +void Slic3r::its_quadric_edge_collapse( + indexed_triangle_set & its, + uint32_t triangle_count, + float * max_error, + std::function throw_on_cancel, + std::function statusfn) +{ + // constants --> may be move to config + const int status_init_size = 10; // in percents + const int check_cancel_period = 16; // how many edge to reduce before call throw_on_cancel + + // check input + if (triangle_count >= its.indices.size()) return; + float maximal_error = (max_error == nullptr)? std::numeric_limits::max() : *max_error; + if (maximal_error <= 0.f) return; + if (throw_on_cancel == nullptr) throw_on_cancel = []() {}; + if (statusfn == nullptr) statusfn = [](int) {}; + + TriangleInfos t_infos; // only normals with information about deleted triangle + VertexInfos v_infos; + EdgeInfos e_infos; + Errors errors; + std::tie(t_infos, v_infos, e_infos, errors) = init(its); + throw_on_cancel(); + statusfn(status_init_size); + + // convert from triangle index to mutable priority queue index + std::vector ti_2_mpqi(its.indices.size(), {0}); + auto setter = [&ti_2_mpqi](const Error &e, size_t index) { ti_2_mpqi[e.triangle_index] = index; }; + auto less = [](const Error &e1, const Error &e2) -> bool { return e1.value < e2.value; }; + auto mpq = make_miniheap_mutable_priority_queue(std::move(setter), std::move(less)); + //MutablePriorityQueue mpq(std::move(setter), std::move(less)); + mpq.reserve(its.indices.size()); + for (Error &error :errors) mpq.push(error); + + const size_t max_triangle_count_for_one_vertex = 50; + CopyEdgeInfos ceis; + ceis.reserve(max_triangle_count_for_one_vertex); + EdgeInfos e_infos_swap; + e_infos_swap.reserve(max_triangle_count_for_one_vertex); + std::vector changed_triangle_indices; + changed_triangle_indices.reserve(2 * max_triangle_count_for_one_vertex); + + uint32_t actual_triangle_count = its.indices.size(); + uint32_t count_triangle_to_reduce = actual_triangle_count - triangle_count; + auto increase_status = [&]() { + double reduced = (actual_triangle_count - triangle_count) / + (double) count_triangle_to_reduce; + double status = status_init_size + (100 - status_init_size) * + (1. - reduced); + statusfn(static_cast(std::round(status))); + }; + // modulo for update status + uint32_t status_mod = std::max(uint32_t(16), count_triangle_to_reduce / 100); + + uint32_t iteration_number = 0; + float last_collapsed_error = 0.f; + while (actual_triangle_count > triangle_count && !mpq.empty()) { + ++iteration_number; + if (iteration_number % status_mod == 0) increase_status(); + if (iteration_number % check_cancel_period == 0) throw_on_cancel(); + + // triangle index 0 + Error e = mpq.top(); // copy + if (e.value >= maximal_error) break; // Too big error + mpq.pop(); + uint32_t ti0 = e.triangle_index; + TriangleInfo &t_info0 = t_infos[ti0]; + if (t_info0.is_deleted()) continue; + assert(t_info0.min_index < 3); + + const Triangle &t0 = its.indices[ti0]; + uint32_t vi0 = t0[t_info0.min_index]; + uint32_t vi1 = t0[(t_info0.min_index+1) %3]; + // Need by move of neighbor edge infos in function: change_neighbors + if (vi0 > vi1) std::swap(vi0, vi1); + VertexInfo &v_info0 = v_infos[vi0]; + VertexInfo &v_info1 = v_infos[vi1]; + assert(!v_info0.is_deleted() && !v_info1.is_deleted()); + + // new vertex position + SymMat q(v_info0.q); + q += v_info1.q; + Vec3f new_vertex0 = calculate_vertex(vi0, vi1, q, its.vertices); + // set of triangle indices that change quadric + auto ti1_opt = (v_info0.count < v_info1.count)? + find_triangle_index1(vi1, v_info0, ti0, e_infos, its.indices) : + find_triangle_index1(vi0, v_info1, ti0, e_infos, its.indices) ; + if (!ti1_opt.has_value() || // edge has only one triangle + is_flipped(new_vertex0, ti0, *ti1_opt, v_info0, t_infos, e_infos, its) || + is_flipped(new_vertex0, ti0, *ti1_opt, v_info1, t_infos, e_infos, its)) { + // try other triangle's edge + Vec3d errors = calculate_3errors(t0, its.vertices, v_infos); + Vec3i ord = (errors[0] < errors[1]) ? + ((errors[0] < errors[2])? + ((errors[1] < errors[2]) ? Vec3i(0, 1, 2) : Vec3i(0, 2, 1)) : + Vec3i(2, 0, 1)): + ((errors[1] < errors[2])? + ((errors[0] < errors[2]) ? Vec3i(1, 0, 2) : Vec3i(1, 2, 0)) : + Vec3i(2, 1, 0)); + if (t_info0.min_index == ord[0]) { + t_info0.min_index = ord[1]; + e.value = errors[t_info0.min_index]; + } else if (t_info0.min_index == ord[1]) { + t_info0.min_index = ord[2]; + e.value = errors[t_info0.min_index]; + } else { + // error is changed when surround edge is reduced + t_info0.min_index = 3; // bad index -> invalidate + e.value = maximal_error; + } + // IMPROVE: check mpq top if it is ti1 with same edge + mpq.push(e); + continue; + } + uint32_t ti1 = *ti1_opt; + last_collapsed_error = e.value; + changed_triangle_indices.clear(); + changed_triangle_indices.reserve(v_info0.count + v_info1.count - 4); + + // for each vertex0 triangles + uint32_t v_info0_end = v_info0.start + v_info0.count; + for (uint32_t di = v_info0.start; di < v_info0_end; ++di) { + assert(di < e_infos.size()); + uint32_t ti = e_infos[di].t_index; + if (ti == ti0) continue; // ti0 will be deleted + if (ti == ti1) continue; // ti1 will be deleted + changed_triangle_indices.emplace_back(ti); + } + + // for each vertex1 triangles + uint32_t v_info1_end = v_info1.start + v_info1.count; + for (uint32_t di = v_info1.start; di < v_info1_end; ++di) { + assert(di < e_infos.size()); + EdgeInfo &e_info = e_infos[di]; + uint32_t ti = e_info.t_index; + if (ti == ti0) continue; // ti0 will be deleted + if (ti == ti1) continue; // ti1 will be deleted + Triangle &t = its.indices[ti]; + t[e_info.edge] = vi0; // change index + changed_triangle_indices.emplace_back(ti); + } + v_info0.q = q; + + // fix neighbors + + // vertex index of triangle 0 which is not vi0 nor vi1 + uint32_t vi_top0 = t0[(t_info0.min_index + 2) % 3]; + const Triangle &t1 = its.indices[ti1]; + change_neighbors(e_infos, v_infos, ti0, ti1, vi0, vi1, + vi_top0, t1, ceis, e_infos_swap); + + // Change vertex + its.vertices[vi0] = new_vertex0; + + // fix errors - must be after set neighbors - v_infos + mpq.remove(ti_2_mpqi[ti1]); + for (uint32_t ti : changed_triangle_indices) { + size_t priority_queue_index = ti_2_mpqi[ti]; + TriangleInfo& t_info = t_infos[ti]; + t_info.n = create_normal(its.indices[ti], its.vertices).cast(); // recalc normals + mpq[priority_queue_index] = calculate_error(ti, its.indices[ti], its.vertices, v_infos, t_info.min_index); + mpq.update(priority_queue_index); + } + + // set triangle(0 + 1) indices as deleted + TriangleInfo &t_info1 = t_infos[ti1]; + t_info0.set_deleted(); + t_info1.set_deleted(); + // triangle counter decrementation + actual_triangle_count-=2; + } + + // compact triangle + compact(v_infos, t_infos, e_infos, its); + if (max_error != nullptr) *max_error = last_collapsed_error; +} + +Vec3d QuadricEdgeCollapse::create_normal(const Triangle &triangle, + const Vertices &vertices) +{ + Vec3d v0 = vertices[triangle[0]].cast(); + Vec3d v1 = vertices[triangle[1]].cast(); + Vec3d v2 = vertices[triangle[2]].cast(); + // n = triangle normal + Vec3d n = (v1 - v0).cross(v2 - v0); + n.normalize(); + return n; +} + +double QuadricEdgeCollapse::calculate_determinant(const SymMat &q) +{ + return q.det(0, 1, 2, 1, 4, 5, 2, 5, 7); +} + +Vec3d QuadricEdgeCollapse::calculate_vertex(double det, const SymMat &q) { + double det_1 = -1 / det; + double det_x = q.det(1, 2, 3, 4, 5, 6, 5, 7, 8); // vx = A41/det(q_delta) + double det_y = q.det(0, 2, 3, 1, 5, 6, 2, 7, 8); // vy = A42/det(q_delta) + double det_z = q.det(0, 1, 3, 1, 4, 6, 2, 5, 8); // vz = A43/det(q_delta) + return Vec3d(det_1 * det_x, -det_1 * det_y, det_1 * det_z); +} + +std::array QuadricEdgeCollapse::create_vertices(uint32_t id_v1, uint32_t id_v2, const Vertices &vertices) +{ + Vec3d v0 = vertices[id_v1].cast(); + Vec3d v1 = vertices[id_v2].cast(); + Vec3d vm = (v0 + v1) / 2.; + return {v0, v1, vm}; +} + +std::array QuadricEdgeCollapse::vertices_error( + const SymMat &q, const std::array &vertices) +{ + return { + vertex_error(q, vertices[0]), + vertex_error(q, vertices[1]), + vertex_error(q, vertices[2])}; +} + +double QuadricEdgeCollapse::calculate_error(uint32_t id_v1, + uint32_t id_v2, + const SymMat & q, + const Vertices &vertices) +{ + double det = calculate_determinant(q); + if (std::abs(det) < std::numeric_limits::epsilon()) { + // can't divide by zero + auto verts = create_vertices(id_v1, id_v2, vertices); + auto errors = vertices_error(q, verts); + return *std::min_element(std::begin(errors), std::end(errors)); + } + Vec3d vertex = calculate_vertex(det, q); + return vertex_error(q, vertex); +} + +// similar as calculate error but focus on new vertex without calculation of error +Vec3f QuadricEdgeCollapse::calculate_vertex(uint32_t id_v1, + uint32_t id_v2, + const SymMat & q, + const Vertices &vertices) +{ + double det = calculate_determinant(q); + if (std::abs(det) < std::numeric_limits::epsilon()) { + // can't divide by zero + auto verts = create_vertices(id_v1, id_v2, vertices); + auto errors = vertices_error(q, verts); + auto mit = std::min_element(std::begin(errors), std::end(errors)); + return verts[mit - std::begin(errors)].cast(); + } + return calculate_vertex(det, q).cast(); +} + +double QuadricEdgeCollapse::vertex_error(const SymMat &q, const Vec3d &vertex) +{ + const double &x = vertex.x(), &y = vertex.y(), &z = vertex.z(); + return q[0] * x * x + 2 * q[1] * x * y + 2 * q[2] * x * z + + 2 * q[3] * x + q[4] * y * y + 2 * q[5] * y * z + + 2 * q[6] * y + q[7] * z * z + 2 * q[8] * z + q[9]; +} + +SymMat QuadricEdgeCollapse::create_quadric(const Triangle &t, + const Vec3d & n, + const Vertices &vertices) +{ + Vec3d v0 = vertices[t[0]].cast(); + return SymMat(n.x(), n.y(), n.z(), -n.dot(v0)); +} + +std::tuple +QuadricEdgeCollapse::init(const indexed_triangle_set &its) +{ + TriangleInfos t_infos(its.indices.size()); + VertexInfos v_infos(its.vertices.size()); + { + std::vector triangle_quadrics(its.indices.size()); + // calculate normals + tbb::parallel_for(tbb::blocked_range(0, its.indices.size()), + [&](const tbb::blocked_range &range) { + for (size_t i = range.begin(); i < range.end(); ++i) { + const Triangle &t = its.indices[i]; + TriangleInfo & t_info = t_infos[i]; + Vec3d normal = create_normal(t, its.vertices); + t_info.n = normal.cast(); + triangle_quadrics[i] = create_quadric(t, normal, its.vertices); + } + }); // END parallel for + + // sum quadrics + for (size_t i = 0; i < its.indices.size(); i++) { + const Triangle &t = its.indices[i]; + const SymMat & q = triangle_quadrics[i]; + for (size_t e = 0; e < 3; e++) { + VertexInfo &v_info = v_infos[t[e]]; + v_info.q += q; + ++v_info.count; // triangle count + } + } + } // remove triangle quadrics + + // set offseted starts + uint32_t triangle_start = 0; + for (VertexInfo &v_info : v_infos) { + v_info.start = triangle_start; + triangle_start += v_info.count; + // set filled vertex to zero + v_info.count = 0; + } + assert(its.indices.size() * 3 == triangle_start); + + // calc error + Errors errors(its.indices.size()); + tbb::parallel_for(tbb::blocked_range(0, its.indices.size()), + [&](const tbb::blocked_range &range) { + for (size_t i = range.begin(); i < range.end(); ++i) { + const Triangle &t = its.indices[i]; + TriangleInfo & t_info = t_infos[i]; + errors[i] = calculate_error(i, t, its.vertices, v_infos, t_info.min_index); + } + }); // END parallel for + + // create reference + EdgeInfos e_infos(its.indices.size() * 3); + for (size_t i = 0; i < its.indices.size(); i++) { + const Triangle &t = its.indices[i]; + for (size_t j = 0; j < 3; ++j) { + VertexInfo &v_info = v_infos[t[j]]; + size_t ei = v_info.start + v_info.count; + assert(ei < e_infos.size()); + EdgeInfo &e_info = e_infos[ei]; + e_info.t_index = i; + e_info.edge = j; + ++v_info.count; + } + } + return {t_infos, v_infos, e_infos, errors}; +} + +std::optional QuadricEdgeCollapse::find_triangle_index1(uint32_t vi, + const VertexInfo &v_info, + uint32_t ti0, + const EdgeInfos & e_infos, + const Indices & indices) +{ + coord_t vi_coord = static_cast(vi); + uint32_t end = v_info.start + v_info.count; + for (uint32_t ei = v_info.start; ei < end; ++ei) { + const EdgeInfo &e_info = e_infos[ei]; + if (e_info.t_index == ti0) continue; + const Triangle& t = indices[e_info.t_index]; + if (t[(e_info.edge + 1) % 3] == vi_coord || + t[(e_info.edge + 2) % 3] == vi_coord) + return e_info.t_index; + } + // triangle0 is on border and do NOT have twin edge + return {}; +} + +bool QuadricEdgeCollapse::is_flipped(const Vec3f & new_vertex, + uint32_t ti0, + uint32_t ti1, + const VertexInfo & v_info, + const TriangleInfos & t_infos, + const EdgeInfos & e_infos, + const indexed_triangle_set &its) +{ + static const float thr_pos = 1.0f - std::numeric_limits::epsilon(); + static const float thr_neg = -thr_pos; + static const float dot_thr = 0.2f; // Value from simplify mesh cca 80 DEG + + // for each vertex triangles + size_t v_info_end = v_info.start + v_info.count; + for (size_t ei = v_info.start; ei < v_info_end; ++ei) { + assert(ei < e_infos.size()); + const EdgeInfo &e_info = e_infos[ei]; + if (e_info.t_index == ti0) continue; // ti0 will be deleted + if (e_info.t_index == ti1) continue; // ti1 will be deleted + const Triangle &t = its.indices[e_info.t_index]; + const Vec3f &normal = t_infos[e_info.t_index].n; + const Vec3f &vf = its.vertices[t[(e_info.edge + 1) % 3]]; + const Vec3f &vs = its.vertices[t[(e_info.edge + 2) % 3]]; + + Vec3f d1 = vf - new_vertex; + d1.normalize(); + Vec3f d2 = vs - new_vertex; + d2.normalize(); + + float dot = d1.dot(d2); + if (dot > thr_pos || dot < thr_neg) return true; + // IMPROVE: propagate new normal + Vec3f n = d1.cross(d2); + n.normalize(); + if(n.dot(normal) < dot_thr) return true; + } + return false; +} + +Vec3d QuadricEdgeCollapse::calculate_3errors(const Triangle & t, + const Vertices & vertices, + const VertexInfos &v_infos) +{ + Vec3d error; + for (size_t j = 0; j < 3; ++j) { + size_t j2 = (j == 2) ? 0 : (j + 1); + uint32_t vi0 = t[j]; + uint32_t vi1 = t[j2]; + SymMat q(v_infos[vi0].q); // copy + q += v_infos[vi1].q; + error[j] = calculate_error(vi0, vi1, q, vertices); + } + return error; +} + +Error QuadricEdgeCollapse::calculate_error(uint32_t ti, + const Triangle & t, + const Vertices & vertices, + const VertexInfos &v_infos, + unsigned char & min_index) +{ + Vec3d error = calculate_3errors(t, vertices, v_infos); + // select min error + min_index = (error[0] < error[1]) ? ((error[0] < error[2]) ? 0 : 2) : + ((error[1] < error[2]) ? 1 : 2); + return Error(static_cast(error[min_index]), ti); +} + +void QuadricEdgeCollapse::remove_triangle(EdgeInfos & e_infos, + VertexInfo &v_info, + uint32_t ti) +{ + auto e_info = e_infos.begin() + v_info.start; + auto e_info_end = e_info + v_info.count - 1; + for (; e_info != e_info_end; ++e_info) { + if (e_info->t_index == ti) { + *e_info = *e_info_end; + --v_info.count; + return; + } + } + assert(e_info_end->t_index == ti); + // last triangle is ti + --v_info.count; +} + +void QuadricEdgeCollapse::change_neighbors(EdgeInfos & e_infos, + VertexInfos & v_infos, + uint32_t ti0, + uint32_t ti1, + uint32_t vi0, + uint32_t vi1, + uint32_t vi_top0, + const Triangle &t1, + CopyEdgeInfos& infos, + EdgeInfos & e_infos1) +{ + // have to copy Edge info from higher vertex index into smaller + assert(vi0 < vi1); + + // vertex index of triangle 1 which is not vi0 nor vi1 + uint32_t vi_top1 = t1[0]; + if (vi_top1 == vi0 || vi_top1 == vi1) { + vi_top1 = t1[1]; + if (vi_top1 == vi0 || vi_top1 == vi1) vi_top1 = t1[2]; + } + + remove_triangle(e_infos, v_infos[vi_top0], ti0); + remove_triangle(e_infos, v_infos[vi_top1], ti1); + + VertexInfo &v_info0 = v_infos[vi0]; + VertexInfo &v_info1 = v_infos[vi1]; + + uint32_t new_triangle_count = v_info0.count + v_info1.count - 4; + remove_triangle(e_infos, v_info0, ti0); + remove_triangle(e_infos, v_info0, ti1); + + // copy second's edge infos out of e_infos, to free size + e_infos1.clear(); + e_infos1.reserve(v_info1.count - 2); + uint32_t v_info_s_end = v_info1.start + v_info1.count; + for (uint32_t ei = v_info1.start; ei < v_info_s_end; ++ei) { + const EdgeInfo &e_info = e_infos[ei]; + if (e_info.t_index == ti0) continue; + if (e_info.t_index == ti1) continue; + e_infos1.emplace_back(e_info); + } + v_info1.count = 0; + + uint32_t need = (new_triangle_count < v_info0.count)? 0: + (new_triangle_count - v_info0.count); + + uint32_t act_vi = vi0 + 1; + VertexInfo *act_v_info = &v_infos[act_vi]; + uint32_t act_start = act_v_info->start; + uint32_t last_end = v_info0.start + v_info0.count; + + infos.clear(); + infos.reserve(need); + + while (true) { + uint32_t save = act_start - last_end; + if (save > 0) { + if (save >= need) break; + need -= save; + infos.emplace_back(act_v_info->start, act_v_info->count, need); + } else { + infos.back().count += act_v_info->count; + } + last_end = act_v_info->start + act_v_info->count; + act_v_info->start += need; + ++act_vi; + if (act_vi < v_infos.size()) { + act_v_info = &v_infos[act_vi]; + act_start = act_v_info->start; + } else + act_start = e_infos.size(); // fix for edge between last two triangles + } + + // copy by c_infos + for (uint32_t i = infos.size(); i > 0; --i) { + const CopyEdgeInfo &c_info = infos[i - 1]; + for (uint32_t ei = c_info.start + c_info.count - 1; ei >= c_info.start; --ei) + e_infos[ei + c_info.move] = e_infos[ei]; // copy + } + + // copy triangle from first info into second + for (uint32_t ei_s = 0; ei_s < e_infos1.size(); ++ei_s) { + uint32_t ei_f = v_info0.start + v_info0.count; + e_infos[ei_f] = e_infos1[ei_s]; // copy + ++v_info0.count; + } +} + +void QuadricEdgeCollapse::compact(const VertexInfos & v_infos, + const TriangleInfos & t_infos, + const EdgeInfos & e_infos, + indexed_triangle_set &its) +{ + uint32_t vi_new = 0; + for (uint32_t vi = 0; vi < v_infos.size(); ++vi) { + const VertexInfo &v_info = v_infos[vi]; + if (v_info.is_deleted()) continue; // deleted + uint32_t e_info_end = v_info.start + v_info.count; + for (uint32_t ei = v_info.start; ei < e_info_end; ++ei) { + const EdgeInfo &e_info = e_infos[ei]; + // change vertex index + its.indices[e_info.t_index][e_info.edge] = vi_new; + } + // compact vertices + its.vertices[vi_new++] = its.vertices[vi]; + } + // remove vertices tail + its.vertices.erase(its.vertices.begin() + vi_new, its.vertices.end()); + + uint32_t ti_new = 0; + for (uint32_t ti = 0; ti < t_infos.size(); ti++) { + const TriangleInfo &t_info = t_infos[ti]; + if (t_info.is_deleted()) continue; + its.indices[ti_new++] = its.indices[ti]; + } + its.indices.erase(its.indices.begin() + ti_new, its.indices.end()); +} diff --git a/src/libslic3r/QuadricEdgeCollapse.hpp b/src/libslic3r/QuadricEdgeCollapse.hpp new file mode 100644 index 000000000..c7330bfc5 --- /dev/null +++ b/src/libslic3r/QuadricEdgeCollapse.hpp @@ -0,0 +1,28 @@ +// paper: https://people.eecs.berkeley.edu/~jrs/meshpapers/GarlandHeckbert2.pdf +// sum up: https://users.csc.calpoly.edu/~zwood/teaching/csc570/final06/jseeba/ +// inspiration: https://github.com/sp4cerat/Fast-Quadric-Mesh-Simplification + +#include +#include +#include "TriangleMesh.hpp" + +namespace Slic3r { + +/// +/// Simplify mesh by Quadric metric +/// +/// IN/OUT triangle mesh to be simplified. +/// Wanted triangle count. +/// Maximal Quadric for reduce. +/// When nullptr then max float is used +/// Output: Last used ErrorValue to collapse edge +/// Could stop process of calculation. +/// Give a feed back to user about progress. Values 1 - 100 +void its_quadric_edge_collapse( + indexed_triangle_set & its, + uint32_t triangle_count = 0, + float * max_error = nullptr, + std::function throw_on_cancel = nullptr, + std::function statusfn = nullptr); + +} // namespace Slic3r diff --git a/src/libslic3r/SimplifyMeshImpl.hpp b/src/libslic3r/SimplifyMeshImpl.hpp index 4b6b0f5cb..d143b6d87 100644 --- a/src/libslic3r/SimplifyMeshImpl.hpp +++ b/src/libslic3r/SimplifyMeshImpl.hpp @@ -107,7 +107,7 @@ public: // Determinant T det(int a11, int a12, int a13, int a21, int a22, int a23, - int a31, int a32, int a33) + int a31, int a32, int a33) const { T det = m[a11] * m[a22] * m[a33] + m[a13] * m[a21] * m[a32] + m[a12] * m[a23] * m[a31] - m[a13] * m[a22] * m[a31] - @@ -121,7 +121,7 @@ public: for (size_t i = 0; i < N; ++i) m[i] += n[i]; return *this; } - + SymetricMatrix operator+(const SymetricMatrix& n) { SymetricMatrix self = *this; diff --git a/src/slic3r/CMakeLists.txt b/src/slic3r/CMakeLists.txt index d21f55d15..3d89025cc 100644 --- a/src/slic3r/CMakeLists.txt +++ b/src/slic3r/CMakeLists.txt @@ -57,6 +57,8 @@ set(SLIC3R_GUI_SOURCES GUI/Gizmos/GLGizmoPainterBase.hpp GUI/Gizmos/GLGizmoSeam.cpp GUI/Gizmos/GLGizmoSeam.hpp + GUI/Gizmos/GLGizmoSimplify.cpp + GUI/Gizmos/GLGizmoSimplify.hpp GUI/Gizmos/GLGizmoMmuSegmentation.cpp GUI/Gizmos/GLGizmoMmuSegmentation.hpp GUI/GLSelectionRectangle.cpp diff --git a/src/slic3r/GUI/GUI_Factories.cpp b/src/slic3r/GUI/GUI_Factories.cpp index db3241e35..92ba427b5 100644 --- a/src/slic3r/GUI/GUI_Factories.cpp +++ b/src/slic3r/GUI/GUI_Factories.cpp @@ -657,6 +657,15 @@ wxMenuItem* MenuFactory::append_menu_item_fix_through_netfabb(wxMenu* menu) wxMenuItem* menu_item = append_menu_item(menu, wxID_ANY, _L("Fix through the Netfabb"), "", [](wxCommandEvent&) { obj_list()->fix_through_netfabb(); }, "", menu, []() {return plater()->can_fix_through_netfabb(); }, plater()); + + return menu_item; +} + +wxMenuItem* MenuFactory::append_menu_item_simplify(wxMenu* menu) +{ + wxMenuItem* menu_item = append_menu_item(menu, wxID_ANY, _L("Simplify model"), "", + [](wxCommandEvent&) { obj_list()->simplify(); }, "", menu, + []() {return plater()->can_simplify(); }, plater()); menu->AppendSeparator(); return menu_item; @@ -874,6 +883,7 @@ void MenuFactory::create_common_object_menu(wxMenu* menu) append_menu_item_scale_selection_to_fit_print_volume(menu); append_menu_item_fix_through_netfabb(menu); + append_menu_item_simplify(menu); append_menu_items_mirror(menu); } @@ -923,6 +933,7 @@ void MenuFactory::create_part_menu() append_menu_item_replace_with_stl(menu); append_menu_item_export_stl(menu); append_menu_item_fix_through_netfabb(menu); + append_menu_item_simplify(menu); append_menu_items_mirror(menu); append_menu_item(menu, wxID_ANY, _L("Split"), _L("Split the selected object into individual parts"), diff --git a/src/slic3r/GUI/GUI_Factories.hpp b/src/slic3r/GUI/GUI_Factories.hpp index f47837d73..0c478a97b 100644 --- a/src/slic3r/GUI/GUI_Factories.hpp +++ b/src/slic3r/GUI/GUI_Factories.hpp @@ -92,6 +92,7 @@ private: wxMenuItem* append_menu_item_printable(wxMenu* menu); void append_menu_items_osx(wxMenu* menu); wxMenuItem* append_menu_item_fix_through_netfabb(wxMenu* menu); + wxMenuItem* append_menu_item_simplify(wxMenu* menu); void append_menu_item_export_stl(wxMenu* menu); void append_menu_item_reload_from_disk(wxMenu* menu); void append_menu_item_replace_with_stl(wxMenu* menu); diff --git a/src/slic3r/GUI/GUI_ObjectList.cpp b/src/slic3r/GUI/GUI_ObjectList.cpp index b7027404e..0eb535ee8 100644 --- a/src/slic3r/GUI/GUI_ObjectList.cpp +++ b/src/slic3r/GUI/GUI_ObjectList.cpp @@ -3955,6 +3955,12 @@ void ObjectList::fix_through_netfabb() update_item_error_icon(obj_idx, vol_idx); } +void ObjectList::simplify() +{ + GLGizmosManager& gizmos_mgr = wxGetApp().plater()->canvas3D()->get_gizmos_manager(); + gizmos_mgr.open_gizmo(GLGizmosManager::EType::Simplify); +} + void ObjectList::update_item_error_icon(const int obj_idx, const int vol_idx) const { const wxDataViewItem item = vol_idx <0 ? m_objects_model->GetItemById(obj_idx) : @@ -3972,6 +3978,8 @@ void ObjectList::update_item_error_icon(const int obj_idx, const int vol_idx) co // unmark fixed item only m_objects_model->DeleteWarningIcon(item); } + else + m_objects_model->AddWarningIcon(item); } void ObjectList::msw_rescale() diff --git a/src/slic3r/GUI/GUI_ObjectList.hpp b/src/slic3r/GUI/GUI_ObjectList.hpp index a57947044..0f1bcd601 100644 --- a/src/slic3r/GUI/GUI_ObjectList.hpp +++ b/src/slic3r/GUI/GUI_ObjectList.hpp @@ -357,6 +357,7 @@ public: void split_instances(); void rename_item(); void fix_through_netfabb(); + void simplify(); void update_item_error_icon(const int obj_idx, int vol_idx) const ; void copy_layers_to_clipboard(); diff --git a/src/slic3r/GUI/Gizmos/GLGizmoMmuSegmentation.cpp b/src/slic3r/GUI/Gizmos/GLGizmoMmuSegmentation.cpp index e61fc2b9f..ec48c511f 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoMmuSegmentation.cpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoMmuSegmentation.cpp @@ -51,6 +51,11 @@ bool GLGizmoMmuSegmentation::on_is_selectable() const && wxGetApp().get_mode() != comSimple && wxGetApp().extruders_edited_cnt() > 1); } +bool GLGizmoMmuSegmentation::on_is_activable() const +{ + return GLGizmoPainterBase::on_is_activable() && wxGetApp().extruders_edited_cnt() > 1; +} + static std::vector> get_extruders_colors() { unsigned char rgb_color[3] = {}; diff --git a/src/slic3r/GUI/Gizmos/GLGizmoMmuSegmentation.hpp b/src/slic3r/GUI/Gizmos/GLGizmoMmuSegmentation.hpp index e890ca031..b1b19bfac 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoMmuSegmentation.hpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoMmuSegmentation.hpp @@ -109,6 +109,7 @@ protected: std::string on_get_name() const override; bool on_is_selectable() const override; + bool on_is_activable() const override; wxString handle_snapshot_action_name(bool shift_down, Button button_down) const override; diff --git a/src/slic3r/GUI/Gizmos/GLGizmoPainterBase.cpp b/src/slic3r/GUI/Gizmos/GLGizmoPainterBase.cpp index 1045ac9e6..2e8c5a4a7 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoPainterBase.cpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoPainterBase.cpp @@ -520,7 +520,7 @@ bool GLGizmoPainterBase::on_is_activable() const const Selection& selection = m_parent.get_selection(); if (wxGetApp().preset_bundle->printers.get_edited_preset().printer_technology() != ptFFF - || !selection.is_single_full_instance()) + || !selection.is_single_full_instance() || wxGetApp().get_mode() == comSimple) return false; // Check that none of the selected volumes is outside. Only SLA auxiliaries (supports) are allowed outside. diff --git a/src/slic3r/GUI/Gizmos/GLGizmoSimplify.cpp b/src/slic3r/GUI/Gizmos/GLGizmoSimplify.cpp new file mode 100644 index 000000000..694eeadd4 --- /dev/null +++ b/src/slic3r/GUI/Gizmos/GLGizmoSimplify.cpp @@ -0,0 +1,322 @@ +// Include GLGizmoBase.hpp before I18N.hpp as it includes some libigl code, which overrides our localization "L" macro. +#include "GLGizmoSimplify.hpp" +#include "slic3r/GUI/GLCanvas3D.hpp" +#include "slic3r/GUI/GUI_App.hpp" +#include "slic3r/GUI/Plater.hpp" +#include "slic3r/GUI/GUI_ObjectManipulation.hpp" +#include "slic3r/GUI/GUI_ObjectList.hpp" +#include "libslic3r/AppConfig.hpp" +#include "libslic3r/Model.hpp" +#include "libslic3r/QuadricEdgeCollapse.hpp" + +#include +#include + +namespace Slic3r::GUI { + +GLGizmoSimplify::GLGizmoSimplify(GLCanvas3D & parent, + const std::string &icon_filename, + unsigned int sprite_id) + : GLGizmoBase(parent, icon_filename, -1) + , state(State::settings) + , is_valid_result(false) + , progress(0) + , volume(nullptr) + , obj_index(0) + , need_reload(false) +{} + +GLGizmoSimplify::~GLGizmoSimplify() { + state = State::canceling; + if (worker.joinable()) worker.join(); +} + +bool GLGizmoSimplify::on_init() +{ + //m_grabbers.emplace_back(); + //m_shortcut_key = WXK_CONTROL_C; + return true; +} + + +std::string GLGizmoSimplify::on_get_name() const +{ + return (_L("Simplify")).ToUTF8().data(); +} + +void GLGizmoSimplify::on_render() {} +void GLGizmoSimplify::on_render_for_picking() {} + +void GLGizmoSimplify::on_render_input_window(float x, float y, float bottom_limit) +{ + const int min_triangle_count = 4; // tetrahedron + const int max_char_in_name = 25; + create_gui_cfg(); + + int flag = ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoResize | + ImGuiWindowFlags_NoCollapse; + m_imgui->begin(on_get_name(), flag); + + const Selection &selection = m_parent.get_selection(); + int object_idx = selection.get_object_idx(); + ModelObject *obj = wxGetApp().plater()->model().objects[object_idx]; + ModelVolume *act_volume = obj->volumes.front(); + + // Check selection of new volume + // Do not reselect object when processing + if (act_volume != volume && state == State::settings) { + obj_index = object_idx; // to remember correct object + volume = act_volume; + original_its = {}; + const TriangleMesh &tm = volume->mesh(); + c.wanted_percent = 50.; // default value + c.update_percent(tm.its.indices.size()); + is_valid_result = false; + // set window position + ImVec2 pos = ImGui::GetMousePos(); + pos.x -= gui_cfg->window_offset; + pos.y -= gui_cfg->window_offset; + ImGui::SetWindowPos(pos, ImGuiCond_Always); + } + + size_t triangle_count = volume->mesh().its.indices.size(); + // already reduced mesh + if (original_its.has_value()) + triangle_count = original_its->indices.size(); + + m_imgui->text_colored(ImGuiWrapper::COL_ORANGE_LIGHT, _L("Mesh name") + ":"); + ImGui::SameLine(gui_cfg->top_left_width); + std::string name = volume->name; + if (name.length() > max_char_in_name) + name = name.substr(0, max_char_in_name-3) + "..."; + m_imgui->text(name); + m_imgui->text_colored(ImGuiWrapper::COL_ORANGE_LIGHT, _L("Triangles") + ":"); + ImGui::SameLine(gui_cfg->top_left_width); + m_imgui->text(std::to_string(triangle_count)); + + ImGui::Separator(); + + ImGui::Text(_L("Limit by triangles").c_str()); + ImGui::SameLine(gui_cfg->bottom_left_width); + // First initialization + fix triangle count + if (m_imgui->checkbox("##UseCount", c.use_count)) { + if (!c.use_count) c.use_error = true; + is_valid_result = false; + } + + m_imgui->disabled_begin(!c.use_count); + ImGui::Text(_L("Triangle count").c_str()); + ImGui::SameLine(gui_cfg->bottom_left_width); + int wanted_count = c.wanted_count; + ImGui::SetNextItemWidth(gui_cfg->input_width); + if (ImGui::SliderInt("##triangle_count", &wanted_count, min_triangle_count, triangle_count, "%d")) { + c.wanted_count = static_cast(wanted_count); + if (c.wanted_count < min_triangle_count) + c.wanted_count = min_triangle_count; + if (c.wanted_count > triangle_count) + c.wanted_count = triangle_count; + c.update_count(triangle_count); + is_valid_result = false; + } + ImGui::Text(_L("Ratio").c_str()); + ImGui::SameLine(gui_cfg->bottom_left_width); + ImGui::SetNextItemWidth(gui_cfg->input_small_width); + const char * precision = (c.wanted_percent > 10)? "%.0f": ((c.wanted_percent > 1)? "%.1f":"%.2f"); + float step = (c.wanted_percent > 10)? 1.f: ((c.wanted_percent > 1)? 0.1f : 0.01f); + if (ImGui::InputFloat("%", &c.wanted_percent, step, 10*step, precision)) { + if (c.wanted_percent > 100.f) c.wanted_percent = 100.f; + c.update_percent(triangle_count); + if (c.wanted_count < min_triangle_count) { + c.wanted_count = min_triangle_count; + c.update_count(triangle_count); + } + is_valid_result = false; + } + m_imgui->disabled_end(); // use_count + + ImGui::NewLine(); + ImGui::Text(_L("Limit by error").c_str()); + ImGui::SameLine(gui_cfg->bottom_left_width); + if (m_imgui->checkbox("##UseError", c.use_error)) { + if (!c.use_error) c.use_count = true; + is_valid_result = false; + } + + m_imgui->disabled_begin(!c.use_error); + ImGui::Text(_L("Max. error").c_str()); + ImGui::SameLine(gui_cfg->bottom_left_width); + ImGui::SetNextItemWidth(gui_cfg->input_small_width); + if (ImGui::InputFloat("##maxError", &c.max_error, 0.01f, .1f, "%.2f")) { + if (c.max_error < 0.f) c.max_error = 0.f; + is_valid_result = false; + } + m_imgui->disabled_end(); // use_error + + if (state == State::settings) { + if (m_imgui->button(_L("Cancel"))) { + if (original_its.has_value()) { + set_its(*original_its); + state = State::close_on_end; + } else { + close(); + } + } + ImGui::SameLine(gui_cfg->bottom_left_width); + if (m_imgui->button(_L("Preview"))) { + state = State::simplifying; + // simplify but not aply on mesh + process(); + } + ImGui::SameLine(); + if (m_imgui->button(_L("Apply"))) { + if (!is_valid_result) { + state = State::close_on_end; + process(); + } else { + // use preview and close + close(); + } + } + } else { + m_imgui->disabled_begin(state == State::canceling); + if (m_imgui->button(_L("Cancel"))) state = State::canceling; + m_imgui->disabled_end(); + + ImGui::SameLine(gui_cfg->bottom_left_width); + // draw progress bar + char buf[32]; + sprintf(buf, L("Process %d / 100"), progress); + ImGui::ProgressBar(progress / 100., ImVec2(gui_cfg->input_width, 0.f), buf); + } + m_imgui->end(); + + if (need_reload) { + need_reload = false; + + // Reload visualization of mesh - change VBO, FBO on GPU + m_parent.reload_scene(true); // deactivate gizmo?? + GLGizmosManager &gizmos_mgr = m_parent.get_gizmos_manager(); + gizmos_mgr.open_gizmo(GLGizmosManager::EType::Simplify); + + if (state == State::close_on_end) { + // fix hollowing, sla support points, modifiers, ... + auto plater = wxGetApp().plater(); + plater->changed_mesh(obj_index); // deactivate gizmo?? + // changed_mesh cause close(); + //close(); + } + + // change from simplifying | aply + state = State::settings; + + // Fix warning icon in object list + wxGetApp().obj_list()->update_item_error_icon(obj_index, -1); + } +} + +void GLGizmoSimplify::close() { + volume = nullptr; + + // close gizmo == open it again + GLGizmosManager &gizmos_mgr = m_parent.get_gizmos_manager(); + gizmos_mgr.open_gizmo(GLGizmosManager::EType::Simplify); +} + + +void GLGizmoSimplify::process() +{ + class SimplifyCanceledException : public std::exception { + public: + const char* what() const throw() { return L("Model simplification has been canceled"); } + }; + + if (!original_its.has_value()) + original_its = volume->mesh().its; // copy + + auto plater = wxGetApp().plater(); + plater->take_snapshot(_L("Simplify ") + volume->name); + plater->clear_before_change_mesh(obj_index); + progress = 0; + if (worker.joinable()) worker.join(); + worker = std::thread([&]() { + // store original triangles + uint32_t triangle_count = (c.use_count) ? c.wanted_count : 0; + float max_error = (c.use_error) ? c.max_error : std::numeric_limits::max(); + + std::function throw_on_cancel = [&]() { + if (state == State::canceling) { + throw SimplifyCanceledException(); + } + }; + std::function statusfn = [&](int percent) { + progress = percent; + m_parent.schedule_extra_frame(0); + }; + + indexed_triangle_set collapsed; + if (last_error.has_value()) { + // is chance to continue with last reduction + const indexed_triangle_set &its = volume->mesh().its; + uint32_t last_triangle_count = static_cast(its.indices.size()); + if ((!c.use_count || triangle_count <= last_triangle_count) && + (!c.use_error || c.max_error <= *last_error)) { + collapsed = its; // small copy + } else { + collapsed = *original_its; // copy + } + } else { + collapsed = *original_its; // copy + } + + try { + its_quadric_edge_collapse(collapsed, triangle_count, &max_error, throw_on_cancel, statusfn); + set_its(collapsed); + is_valid_result = true; + last_error = max_error; + } catch (SimplifyCanceledException &) { + state = State::settings; + } + // need to render last status fn + // without sleep it freezes until mouse move + std::this_thread::sleep_for(std::chrono::milliseconds(50)); + m_parent.schedule_extra_frame(0); + }); +} + +void GLGizmoSimplify::set_its(indexed_triangle_set &its) { + auto tm = std::make_unique(its); + tm->repair(); + volume->set_mesh(std::move(tm)); + volume->set_new_unique_id(); + volume->get_object()->invalidate_bounding_box(); + need_reload = true; +} + +bool GLGizmoSimplify::on_is_activable() const +{ + return !m_parent.get_selection().is_empty(); +} + +void GLGizmoSimplify::create_gui_cfg() { + if (gui_cfg.has_value()) return; + + int space_size = m_imgui->calc_text_size(":MM").x; + GuiCfg cfg; + cfg.top_left_width = std::max(m_imgui->calc_text_size(_L("Mesh name")).x, + m_imgui->calc_text_size(_L("Triangles")).x) + + space_size; + + cfg.bottom_left_width = + std::max( + std::max(m_imgui->calc_text_size(_L("Limit by triangles")).x, + std::max(m_imgui->calc_text_size(_L("Triangle count")).x, + m_imgui->calc_text_size(_L("Ratio")).x)), + std::max(m_imgui->calc_text_size(_L("Limit by error")).x, + m_imgui->calc_text_size(_L("Max. error")).x)) + space_size; + cfg.input_width = cfg.bottom_left_width; + cfg.input_small_width = cfg.input_width * 0.8; + cfg.window_offset = cfg.input_width; + gui_cfg = cfg; +} + +} // namespace Slic3r::GUI diff --git a/src/slic3r/GUI/Gizmos/GLGizmoSimplify.hpp b/src/slic3r/GUI/Gizmos/GLGizmoSimplify.hpp new file mode 100644 index 000000000..bd3637360 --- /dev/null +++ b/src/slic3r/GUI/Gizmos/GLGizmoSimplify.hpp @@ -0,0 +1,89 @@ +#ifndef slic3r_GLGizmoSimplify_hpp_ +#define slic3r_GLGizmoSimplify_hpp_ + +#include "GLGizmoBase.hpp" +#include "libslic3r/Model.hpp" +#include +#include + +namespace Slic3r { +namespace GUI { + +class GLGizmoSimplify : public GLGizmoBase +{ + enum class State { + settings, + simplifying, // start processing + canceling, // canceled + successfull, // successful simplified + close_on_end + } state; + + bool is_valid_result; // differ what to do in apply + int progress; + + ModelVolume *volume; + size_t obj_index; + std::optional original_its; + + std::optional last_error; // for use previous reduction + + bool need_reload; // after simplify, glReload must be on main thread + std::thread worker; +public: + GLGizmoSimplify(GLCanvas3D& parent, const std::string& icon_filename, unsigned int sprite_id); + virtual ~GLGizmoSimplify(); +protected: + virtual bool on_init() override; + virtual std::string on_get_name() const override; + virtual void on_render() override; + virtual void on_render_for_picking() override; + virtual void on_render_input_window(float x, float y, float bottom_limit) override; + virtual bool on_is_activable() const override; + virtual bool on_is_selectable() const override { return false; } + +private: + void close(); + void process(); + void set_its(indexed_triangle_set &its); + struct Configuration + { + bool use_count = true; + // minimal triangle count + float wanted_percent = 50.f; + uint32_t wanted_count = 0; // initialize by percents + + bool use_error = false; + // maximal quadric error + float max_error = 1.; + + void update_count(size_t triangle_count) + { + wanted_percent = (float) wanted_count / triangle_count * 100.f; + } + void update_percent(size_t triangle_count) + { + wanted_count = static_cast( + std::round(triangle_count * wanted_percent / 100.f)); + } + } c; + + // This configs holds GUI layout size given by translated texts. + // etc. When language changes, GUI is recreated and this class constructed again, + // so the change takes effect. (info by GLGizmoFdmSupports.hpp) + struct GuiCfg + { + int top_left_width = 100; + int bottom_left_width = 100; + int input_width = 100; + int input_small_width = 80; + int window_offset = 100; + }; + std::optional gui_cfg; + void create_gui_cfg(); +}; + +} // namespace GUI +} // namespace Slic3r + +#endif // slic3r_GLGizmoSimplify_hpp_ diff --git a/src/slic3r/GUI/Gizmos/GLGizmosManager.cpp b/src/slic3r/GUI/Gizmos/GLGizmosManager.cpp index dbfeaaa80..eda95a2a5 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmosManager.cpp +++ b/src/slic3r/GUI/Gizmos/GLGizmosManager.cpp @@ -20,6 +20,7 @@ #include "slic3r/GUI/Gizmos/GLGizmoHollow.hpp" #include "slic3r/GUI/Gizmos/GLGizmoSeam.hpp" #include "slic3r/GUI/Gizmos/GLGizmoMmuSegmentation.hpp" +#include "slic3r/GUI/Gizmos/GLGizmoSimplify.hpp" #include "libslic3r/Model.hpp" #include "libslic3r/PresetBundle.hpp" @@ -50,14 +51,7 @@ std::vector GLGizmosManager::get_selectable_idxs() const return out; } -std::vector GLGizmosManager::get_activable_idxs() const -{ - std::vector out; - for (size_t i=0; iis_activable()) - out.push_back(i); - return out; -} + size_t GLGizmosManager::get_gizmo_idx_from_mouse(const Vec2d& mouse_pos) const { @@ -110,6 +104,7 @@ bool GLGizmosManager::init() m_gizmos.emplace_back(new GLGizmoFdmSupports(m_parent, "fdm_supports.svg", 7)); m_gizmos.emplace_back(new GLGizmoSeam(m_parent, "seam.svg", 8)); m_gizmos.emplace_back(new GLGizmoMmuSegmentation(m_parent, "fdm_supports.svg", 9)); + m_gizmos.emplace_back(new GLGizmoSimplify(m_parent, "cut.svg", 10)); m_common_gizmos_data.reset(new CommonGizmosDataPool(&m_parent)); @@ -169,7 +164,7 @@ void GLGizmosManager::refresh_on_off_state() return; if (m_current != Undefined - && (! m_gizmos[m_current]->is_activable() || ! m_gizmos[m_current]->is_selectable())) { + && ! m_gizmos[m_current]->is_activable()) { activate_gizmo(Undefined); update_data(); } @@ -187,7 +182,7 @@ void GLGizmosManager::reset_all_states() bool GLGizmosManager::open_gizmo(EType type) { int idx = int(type); - if (m_gizmos[idx]->is_selectable() && m_gizmos[idx]->is_activable()) { + if (m_gizmos[idx]->is_activable()) { activate_gizmo(m_current == idx ? Undefined : (EType)idx); update_data(); return true; @@ -304,7 +299,7 @@ bool GLGizmosManager::handle_shortcut(int key) auto it = std::find_if(m_gizmos.begin(), m_gizmos.end(), [key](const std::unique_ptr& gizmo) { int gizmo_key = gizmo->get_shortcut_key(); - return gizmo->is_selectable() + return gizmo->is_activable() && ((gizmo_key == key - 64) || (gizmo_key == key - 96)); }); @@ -1077,6 +1072,7 @@ void GLGizmosManager::do_render_overlay() const float u_offset = 1.0f / (float)tex_width; float v_offset = 1.0f / (float)tex_height; + float current_y = FLT_MAX; for (size_t idx : selectable_idxs) { GLGizmoBase* gizmo = m_gizmos[idx].get(); @@ -1090,12 +1086,18 @@ void GLGizmosManager::do_render_overlay() const float u_right = u_left + du - u_offset; GLTexture::render_sub_texture(icons_texture_id, zoomed_top_x, zoomed_top_x + zoomed_icons_size, zoomed_top_y - zoomed_icons_size, zoomed_top_y, { { u_left, v_bottom }, { u_right, v_bottom }, { u_right, v_top }, { u_left, v_top } }); - if (idx == m_current) { - float toolbar_top = cnv_h - wxGetApp().plater()->get_view_toolbar().get_height(); - gizmo->render_input_window(width, 0.5f * cnv_h - zoomed_top_y * zoom, toolbar_top); + if (idx == m_current || current_y == FLT_MAX) { + // The FLT_MAX trick is here so that even non-selectable but activable + // gizmos are passed some meaningful value. + current_y = 0.5f * cnv_h - zoomed_top_y * zoom; } zoomed_top_y -= zoomed_stride_y; } + + if (m_current != Undefined) { + float toolbar_top = cnv_h - wxGetApp().plater()->get_view_toolbar().get_height(); + m_gizmos[m_current]->render_input_window(width, current_y, toolbar_top); + } } float GLGizmosManager::get_scaled_total_height() const diff --git a/src/slic3r/GUI/Gizmos/GLGizmosManager.hpp b/src/slic3r/GUI/Gizmos/GLGizmosManager.hpp index 64780a2bc..cdfb3f6ff 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmosManager.hpp +++ b/src/slic3r/GUI/Gizmos/GLGizmosManager.hpp @@ -69,6 +69,7 @@ public: FdmSupports, Seam, MmuSegmentation, + Simplify, Undefined }; @@ -101,7 +102,6 @@ private: std::pair m_highlight; // bool true = higlightedShown, false = highlightedHidden std::vector get_selectable_idxs() const; - std::vector get_activable_idxs() const; size_t get_gizmo_idx_from_mouse(const Vec2d& mouse_pos) const; void activate_gizmo(EType type); diff --git a/src/slic3r/GUI/ObjectDataViewModel.cpp b/src/slic3r/GUI/ObjectDataViewModel.cpp index 9f48bcc3c..8f65e4f90 100644 --- a/src/slic3r/GUI/ObjectDataViewModel.cpp +++ b/src/slic3r/GUI/ObjectDataViewModel.cpp @@ -1697,6 +1697,24 @@ wxBitmap ObjectDataViewModel::GetVolumeIcon(const Slic3r::ModelVolumeType vol_ty return *bmp; } +void ObjectDataViewModel::AddWarningIcon(const wxDataViewItem& item) +{ + if (!item.IsOk()) + return; + ObjectDataViewModelNode *node = static_cast(item.GetID()); + + if (node->GetType() & itObject) { + node->SetBitmap(m_warning_bmp); + return; + } + + if (node->GetType() & itVolume) { + node->SetBitmap(GetVolumeIcon(node->GetVolumeType(), true)); + node->GetParent()->SetBitmap(m_warning_bmp); + return; + } +} + void ObjectDataViewModel::DeleteWarningIcon(const wxDataViewItem& item, const bool unmark_object/* = false*/) { if (!item.IsOk()) diff --git a/src/slic3r/GUI/ObjectDataViewModel.hpp b/src/slic3r/GUI/ObjectDataViewModel.hpp index 1cf10faf5..664cf7ff5 100644 --- a/src/slic3r/GUI/ObjectDataViewModel.hpp +++ b/src/slic3r/GUI/ObjectDataViewModel.hpp @@ -376,6 +376,7 @@ public: wxBitmap GetVolumeIcon(const Slic3r::ModelVolumeType vol_type, const bool is_marked = false); + void AddWarningIcon(const wxDataViewItem& item); void DeleteWarningIcon(const wxDataViewItem& item, const bool unmark_object = false); t_layer_height_range GetLayerRangeByItem(const wxDataViewItem& item) const; diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index 4da29afdf..652524c5c 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -1771,6 +1771,7 @@ struct Plater::priv bool can_arrange() const; bool can_layers_editing() const; bool can_fix_through_netfabb() const; + bool can_simplify() const; bool can_set_instance_to_object() const; bool can_mirror() const; bool can_reload_from_disk() const; @@ -3495,44 +3496,10 @@ void Plater::priv::fix_through_netfabb(const int obj_idx, const int vol_idx/* = // size_t snapshot_time = undo_redo_stack().active_snapshot_time(); Plater::TakeSnapshot snapshot(q, _L("Fix through NetFabb")); + q->clear_before_change_mesh(obj_idx); ModelObject* mo = model.objects[obj_idx]; - - // If there are custom supports/seams/mmu segmentation, remove them. Fixed mesh - // may be different and they would make no sense. - bool paint_removed = false; - for (ModelVolume* mv : mo->volumes) { - paint_removed |= ! mv->supported_facets.empty() || ! mv->seam_facets.empty() || ! mv->mmu_segmentation_facets.empty(); - mv->supported_facets.clear(); - mv->seam_facets.clear(); - mv->mmu_segmentation_facets.clear(); - } - if (paint_removed) { - // snapshot_time is captured by copy so the lambda knows where to undo/redo to. - notification_manager->push_notification( - NotificationType::CustomSupportsAndSeamRemovedAfterRepair, - NotificationManager::NotificationLevel::RegularNotification, - _u8L("Custom supports and seams were removed after repairing the mesh.")); -// _u8L("Undo the repair"), -// [this, snapshot_time](wxEvtHandler*){ -// // Make sure the snapshot is still available and that -// // we are in the main stack and not in a gizmo-stack. -// if (undo_redo_stack().has_undo_snapshot(snapshot_time) -// && q->canvas3D()->get_gizmos_manager().get_current() == nullptr) -// undo_redo_to(snapshot_time); -// else -// notification_manager->push_notification( -// NotificationType::CustomSupportsAndSeamRemovedAfterRepair, -// NotificationManager::NotificationLevel::RegularNotification, -// _u8L("Cannot undo to before the mesh repair!")); -// return true; -// }); - } - fix_model_by_win10_sdk_gui(*mo, vol_idx); - sla::reproject_points_and_holes(mo); - this->update(); - this->object_list_changed(); - this->schedule_background_process(); + q->changed_mesh(obj_idx); } void Plater::priv::set_current_panel(wxPanel* panel) @@ -4304,6 +4271,12 @@ bool Plater::priv::can_fix_through_netfabb() const return model.objects[obj_idx]->get_mesh_errors_count() > 0; } + +bool Plater::priv::can_simplify() const +{ + return true; +} + bool Plater::priv::can_increase_instances() const { if (m_ui_jobs.is_any_running() @@ -6135,6 +6108,51 @@ bool Plater::set_printer_technology(PrinterTechnology printer_technology) return ret; } +void Plater::clear_before_change_mesh(int obj_idx) +{ + ModelObject* mo = model().objects[obj_idx]; + + // If there are custom supports/seams/mmu segmentation, remove them. Fixed mesh + // may be different and they would make no sense. + bool paint_removed = false; + for (ModelVolume* mv : mo->volumes) { + paint_removed |= ! mv->supported_facets.empty() || ! mv->seam_facets.empty() || ! mv->mmu_segmentation_facets.empty(); + mv->supported_facets.clear(); + mv->seam_facets.clear(); + mv->mmu_segmentation_facets.clear(); + } + if (paint_removed) { + // snapshot_time is captured by copy so the lambda knows where to undo/redo to. + get_notification_manager()->push_notification( + NotificationType::CustomSupportsAndSeamRemovedAfterRepair, + NotificationManager::NotificationLevel::RegularNotification, + _u8L("Custom supports and seams were removed after repairing the mesh.")); +// _u8L("Undo the repair"), +// [this, snapshot_time](wxEvtHandler*){ +// // Make sure the snapshot is still available and that +// // we are in the main stack and not in a gizmo-stack. +// if (undo_redo_stack().has_undo_snapshot(snapshot_time) +// && q->canvas3D()->get_gizmos_manager().get_current() == nullptr) +// undo_redo_to(snapshot_time); +// else +// notification_manager->push_notification( +// NotificationType::CustomSupportsAndSeamRemovedAfterRepair, +// NotificationManager::NotificationLevel::RegularNotification, +// _u8L("Cannot undo to before the mesh repair!")); +// return true; +// }); + } +} + +void Plater::changed_mesh(int obj_idx) +{ + ModelObject* mo = model().objects[obj_idx]; + sla::reproject_points_and_holes(mo); + update(); + p->object_list_changed(); + p->schedule_background_process(); +} + void Plater::changed_object(int obj_idx) { if (obj_idx < 0) @@ -6402,6 +6420,7 @@ bool Plater::can_increase_instances() const { return p->can_increase_instances() bool Plater::can_decrease_instances() const { return p->can_decrease_instances(); } bool Plater::can_set_instance_to_object() const { return p->can_set_instance_to_object(); } bool Plater::can_fix_through_netfabb() const { return p->can_fix_through_netfabb(); } +bool Plater::can_simplify() const { return p->can_simplify(); } bool Plater::can_split_to_objects() const { return p->can_split_to_objects(); } bool Plater::can_split_to_volumes() const { return p->can_split_to_volumes(); } bool Plater::can_arrange() const { return p->can_arrange(); } diff --git a/src/slic3r/GUI/Plater.hpp b/src/slic3r/GUI/Plater.hpp index 9426cd112..2b1a83ba2 100644 --- a/src/slic3r/GUI/Plater.hpp +++ b/src/slic3r/GUI/Plater.hpp @@ -226,6 +226,10 @@ public: void reslice_SLA_supports(const ModelObject &object, bool postpone_error_messages = false); void reslice_SLA_hollowing(const ModelObject &object, bool postpone_error_messages = false); void reslice_SLA_until_step(SLAPrintObjectStep step, const ModelObject &object, bool postpone_error_messages = false); + + void clear_before_change_mesh(int obj_idx); + void changed_mesh(int obj_idx); + void changed_object(int obj_idx); void changed_objects(const std::vector& object_idxs); void schedule_background_process(bool schedule = true); @@ -306,6 +310,7 @@ public: bool can_decrease_instances() const; bool can_set_instance_to_object() const; bool can_fix_through_netfabb() const; + bool can_simplify() const; bool can_split_to_objects() const; bool can_split_to_volumes() const; bool can_arrange() const; diff --git a/tests/libslic3r/test_indexed_triangle_set.cpp b/tests/libslic3r/test_indexed_triangle_set.cpp index 4c1128345..b640d8410 100644 --- a/tests/libslic3r/test_indexed_triangle_set.cpp +++ b/tests/libslic3r/test_indexed_triangle_set.cpp @@ -4,6 +4,8 @@ #include "libslic3r/TriangleMesh.hpp" +using namespace Slic3r; + TEST_CASE("Split empty mesh", "[its_split][its]") { using namespace Slic3r; @@ -100,3 +102,148 @@ TEST_CASE("Split two watertight meshes", "[its_split][its]") { debug_write_obj(res, "parts_watertight"); } +#include +static float triangle_area(const Vec3f &v0, const Vec3f &v1, const Vec3f &v2) +{ + Vec3f ab = v1 - v0; + Vec3f ac = v2 - v0; + return ab.cross(ac).norm() / 2.f; +} + +static float triangle_area(const Vec3crd &triangle_inices, const std::vector &vertices) +{ + return triangle_area(vertices[triangle_inices[0]], + vertices[triangle_inices[1]], + vertices[triangle_inices[2]]); +} + +static std::mt19937 create_random_generator() { + std::random_device rd; + std::mt19937 gen(rd()); + return gen; +} + +std::vector its_sample_surface(const indexed_triangle_set &its, + double sample_per_mm2, + std::mt19937 random_generator = create_random_generator()) +{ + std::vector samples; + std::uniform_real_distribution rand01(0.f, 1.f); + for (const auto &triangle_indices : its.indices) { + float area = triangle_area(triangle_indices, its.vertices); + float countf; + float fractional = std::modf(area * sample_per_mm2, &countf); + int count = static_cast(countf); + + float generate = rand01(random_generator); + if (generate < fractional) ++count; + if (count == 0) continue; + + const Vec3f &v0 = its.vertices[triangle_indices[0]]; + const Vec3f &v1 = its.vertices[triangle_indices[1]]; + const Vec3f &v2 = its.vertices[triangle_indices[2]]; + for (int c = 0; c < count; c++) { + // barycentric coordinate + Vec3f b; + b[0] = rand01(random_generator); + b[1] = rand01(random_generator); + if ((b[0] + b[1]) > 1.f) { + b[0] = 1.f - b[0]; + b[1] = 1.f - b[1]; + } + b[2] = 1.f - b[0] - b[1]; + Vec3f pos; + for (int i = 0; i < 3; i++) { + pos[i] = b[0] * v0[i] + b[1] * v1[i] + b[2] * v2[i]; + } + samples.push_back(pos); + } + } + return samples; +} + + +#include "libslic3r/AABBTreeIndirect.hpp" + +// return Average abs distance to original +float compare(const indexed_triangle_set &original, + const indexed_triangle_set &simplified, + double sample_per_mm2) +{ + // create ABBTree + auto tree = AABBTreeIndirect::build_aabb_tree_over_indexed_triangle_set( + original.vertices, original.indices); + + unsigned int init = 0; + std::mt19937 rnd(init); + auto samples = its_sample_surface(simplified, sample_per_mm2, rnd); + + float sumDistance = 0; + for (const Vec3f &sample : samples) { + size_t hit_idx; + Vec3f hit_point; + float distance2 = AABBTreeIndirect::squared_distance_to_indexed_triangle_set( + original.vertices, original.indices, tree, sample, hit_idx, + hit_point); + sumDistance += sqrt(distance2); + } + return sumDistance / samples.size(); +} + +TEST_CASE("Reduce one edge by Quadric Edge Collapse", "[its]") +{ + indexed_triangle_set its; + its.vertices = {Vec3f(-1.f, 0.f, 0.f), Vec3f(0.f, 1.f, 0.f), + Vec3f(1.f, 0.f, 0.f), Vec3f(0.f, 0.f, 1.f), + // vertex to be removed + Vec3f(0.9f, .1f, -.1f)}; + its.indices = {Vec3i(1, 0, 3), Vec3i(2, 1, 3), Vec3i(0, 2, 3), + Vec3i(0, 1, 4), Vec3i(1, 2, 4), Vec3i(2, 0, 4)}; + // edge to remove is between vertices 2 and 4 on trinagles 4 and 5 + + indexed_triangle_set its_ = its; // copy + // its_write_obj(its, "tetrhedron_in.obj"); + uint32_t wanted_count = its.indices.size() - 1; + its_quadric_edge_collapse(its, wanted_count); + // its_write_obj(its, "tetrhedron_out.obj"); + CHECK(its.indices.size() == 4); + CHECK(its.vertices.size() == 4); + + for (size_t i = 0; i < 3; i++) { + CHECK(its.indices[i] == its_.indices[i]); + } + + for (size_t i = 0; i < 4; i++) { + if (i == 2) continue; + CHECK(its.vertices[i] == its_.vertices[i]); + } + + const Vec3f &v = its.vertices[2]; // new vertex + const Vec3f &v2 = its_.vertices[2]; // moved vertex + const Vec3f &v4 = its_.vertices[4]; // removed vertex + for (size_t i = 0; i < 3; i++) { + bool is_between = (v[i] < v4[i] && v[i] > v2[i]) || + (v[i] > v4[i] && v[i] < v2[i]); + CHECK(is_between); + } + float avg_distance = compare(its_, its, 10); + CHECK(avg_distance < 8e-3f); +} + +#include "test_utils.hpp" +TEST_CASE("Simplify mesh by Quadric edge collapse to 5%", "[its]") +{ + TriangleMesh mesh = load_model("frog_legs.obj"); + double original_volume = its_volume(mesh.its); + uint32_t wanted_count = mesh.its.indices.size() * 0.05; + REQUIRE_FALSE(mesh.empty()); + indexed_triangle_set its = mesh.its; // copy + float max_error = std::numeric_limits::max(); + its_quadric_edge_collapse(its, wanted_count, &max_error); + //its_write_obj(its, "frog_legs_qec.obj"); + CHECK(its.indices.size() <= wanted_count); + double volume = its_volume(its); + CHECK(fabs(original_volume - volume) < 33.); + float avg_distance = compare(mesh.its, its, 10); + CHECK(avg_distance < 0.022f); // 0.02022 | 0.0199614074 +} \ No newline at end of file diff --git a/tests/libslic3r/test_mutable_priority_queue.cpp b/tests/libslic3r/test_mutable_priority_queue.cpp index 37b244432..40ed5a158 100644 --- a/tests/libslic3r/test_mutable_priority_queue.cpp +++ b/tests/libslic3r/test_mutable_priority_queue.cpp @@ -339,3 +339,104 @@ TEST_CASE("Mutable priority queue - reshedule first", "[MutableSkipHeapPriorityQ } } } + +TEST_CASE("Mutable priority queue - first pop", "[MutableSkipHeapPriorityQueue]") +{ + struct MyValue{ + int id; + float val; + }; + size_t count = 50000; + std::vector idxs(count, {0}); + std::vector dels(count, false); + auto q = make_miniheap_mutable_priority_queue( + [&](MyValue &v, size_t idx) { + idxs[v.id] = idx; + }, + [](MyValue &l, MyValue &r) { return l.val < r.val; }); + q.reserve(count); + for (size_t id = 0; id < count; id++) { + MyValue mv; + mv.id = id; + mv.val = rand(); + q.push(mv); + } + MyValue it = q.top(); // copy + q.pop(); + bool valid = (it.id != 0) && (idxs[0] < 3 * count); + CHECK(valid); +} + +TEST_CASE("Mutable priority queue complex", "[MutableSkipHeapPriorityQueue]") +{ + struct MyValue { + size_t id; + float val; + }; + size_t count = 5000; + std::vector idxs(count, {0}); + std::vector dels(count, false); + auto q = make_miniheap_mutable_priority_queue( + [&](MyValue &v, size_t idx) { idxs[v.id] = idx; }, + [](MyValue &l, MyValue &r) { return l.val < r.val; }); + q.reserve(count); + + auto rand_val = [&]()->float { return (rand() % 53) / 10.f; }; + size_t ord = 0; + for (size_t id = 0; id < count; id++) { + MyValue mv; + mv.id = ord++; + mv.val = rand_val(); + q.push(mv); + } + auto check = [&]()->bool{ + for (size_t i = 0; i < idxs.size(); ++i) { + if (dels[i]) continue; + size_t qid = idxs[i]; + if (qid > 3*count) { + return false; + } + MyValue &mv = q[qid]; + if (mv.id != i) { + return false; // ERROR + } + } + return true; + }; + + CHECK(check()); // initial check + + auto get_valid_id = [&]()->int { + int id = 0; + do { + id = rand() % count; + } while (dels[id]); + return id; + }; + for (size_t i = 0; i < 100; i++) { + MyValue it = q.top(); // copy + q.pop(); + dels[it.id] = true; + CHECK(check()); + if (i % 20 == 0) { + it.val = rand_val(); + q.push(it); + dels[it.id] = false; + CHECK(check()); + continue; + } + + int id = get_valid_id(); + q.remove(idxs[id]); + dels[id] = true; + CHECK(check()); + for (size_t j = 0; j < 5; j++) { + int id = get_valid_id(); + size_t qid = idxs[id]; + MyValue &mv = q[qid]; + mv.val = rand_val(); + q.update(qid); + CHECK(check()); + } + } +}