From eea3e32d0dfdb7e6bc6687725fea848d8dc6c4ab Mon Sep 17 00:00:00 2001 From: Oleksandra Yushchenko Date: Wed, 20 Sep 2023 11:06:28 +0800 Subject: [PATCH] NEW:add PartSelection class and some apis for cut tool Jira:STUDIO-4227 most of code from PrusaSlcer,thanks for PrusaSlcer and YuSanka commit 1aa8d8ea99a56a622d234f71be8d312e1ebe4735 Author: YuSanka Date: Fri Jun 23 16:53:29 2023 +0200 WIP: Cut with Tongue and Groove * Implemented preview rendering of groove ... Change-Id: Id5db8742db50aa10f9b5ebb057ba70f92fc22aeb --- src/slic3r/GUI/Gizmos/GLGizmoAdvancedCut.cpp | 212 ++++++++++++++++++- src/slic3r/GUI/Gizmos/GLGizmoAdvancedCut.hpp | 54 ++++- src/slic3r/GUI/Gizmos/GLGizmosCommon.cpp | 12 ++ src/slic3r/GUI/Gizmos/GLGizmosCommon.hpp | 1 + src/slic3r/GUI/MeshUtils.cpp | 58 ++++- src/slic3r/GUI/MeshUtils.hpp | 5 + 6 files changed, 339 insertions(+), 3 deletions(-) diff --git a/src/slic3r/GUI/Gizmos/GLGizmoAdvancedCut.cpp b/src/slic3r/GUI/Gizmos/GLGizmoAdvancedCut.cpp index 0d846897f..381277f91 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoAdvancedCut.cpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoAdvancedCut.cpp @@ -10,7 +10,7 @@ #include #include - +#include "GLGizmosCommon.hpp" #include "slic3r/GUI/GUI_App.hpp" #include "slic3r/GUI/Plater.hpp" #include "libslic3r/AppConfig.hpp" @@ -57,6 +57,13 @@ static const ColorRGBA CONNECTOR_DEF_COLOR = {1.0f, 1.0f, 1.0f, 0.5f}; static const ColorRGBA CONNECTOR_ERR_COLOR = {1.0f, 0.3f, 0.3f, 0.5f}; static const ColorRGBA HOVERED_ERR_COLOR = {1.0f, 0.3f, 0.3f, 1.0f}; +static const ColorRGBA CUT_PLANE_DEF_COLOR = {0.9f, 0.9f, 0.9f, 0.5f}; +static const ColorRGBA CUT_PLANE_ERR_COLOR = {1.0f, 0.8f, 0.8f, 0.5f}; + +static const ColorRGBA UPPER_PART_COLOR = CYAN(); +static const ColorRGBA LOWER_PART_COLOR = MAGENTA(); +static const ColorRGBA MODIFIER_COLOR = {0.75f, 0.75f, 0.75f, 0.5f}; + static Vec3d rotate_vec3d_around_vec3d_with_rotate_matrix( const Vec3d& rotate_point, const Vec3d& origin_point, @@ -1931,5 +1938,208 @@ bool GLGizmoAdvancedCut::process_cut_line(SLAGizmoEventType action, const Vec2d return false; } +PartSelection::PartSelection( + const ModelObject *mo, const Transform3d &cut_matrix, int instance_idx_in, const Vec3d ¢er, const Vec3d &normal, const CommonGizmosDataObjects::ObjectClipper &oc) + : m_instance_idx(instance_idx_in) +{ + Cut cut(mo, instance_idx_in, cut_matrix); + add_object(cut.perform_with_plane().front()); + + const ModelVolumePtrs &volumes = model_object()->volumes; + + // split to parts + for (int id = int(volumes.size()) - 1; id >= 0; id--) + if (volumes[id]->is_splittable()) volumes[id]->split(1); + + const Vec3d inst_offset = model_object()->instances[m_instance_idx]->get_offset(); + int i = 0; + m_cut_parts.resize(volumes.size()); + for (const ModelVolume *volume : volumes) { + assert(volume != nullptr); + m_cut_parts[i].is_up_part = false; + if (m_cut_parts[i].raycaster) { delete m_cut_parts[i].raycaster; } + m_cut_parts[i].raycaster = new MeshRaycaster(volume->mesh()); + m_cut_parts[i].glmodel.reset(); + m_cut_parts[i].glmodel.init_from(volume->mesh_ptr()->its); + m_cut_parts[i].trans = Geometry::translation_transform(inst_offset) * model_object()->volumes[i]->get_matrix(); + // Now check whether this part is below or above the plane. + Transform3d tr = (model_object()->instances[m_instance_idx]->get_matrix() * volume->get_matrix()).inverse(); + Vec3f pos = (tr * center).cast(); + Vec3f norm = (tr.linear().inverse().transpose() * normal).cast(); + + for (const Vec3f &v : volume->mesh().its.vertices) { + double p = (v - pos).dot(norm); + if (std::abs(p) > EPSILON) { + m_cut_parts[i].is_up_part = p > 0.; + break; + } + } + i++; + } + + // Now go through the contours and create a map from contours to parts. + m_contour_points.clear(); + m_contour_to_parts.clear(); + m_debug_pts = std::vector>(m_cut_parts.size(), std::vector()); + if (std::vector pts = oc.point_per_contour(); !pts.empty()) { + m_contour_to_parts.resize(pts.size()); + + for (size_t pt_idx = 0; pt_idx < pts.size(); ++pt_idx) { + const Vec3d &pt = pts[pt_idx]; + const Vec3d dir = (center - pt).dot(normal) * normal; + m_contour_points.emplace_back(dir + pt); // the result is in world coordinates. + + // Now, cast a ray from every contour point and see which volumes of the ones above + // the plane are hit from the inside. + for (size_t part_id = 0; part_id < m_cut_parts.size(); ++part_id) { + const sla::IndexedMesh &aabb = m_cut_parts[part_id].raycaster->get_aabb_mesh(); + const Transform3d & tr = (Geometry::translation_transform(model_object()->instances[m_instance_idx]->get_offset()) * + Geometry::translation_transform(model_object()->volumes[part_id]->get_offset())) + .inverse(); + for (double d : {-1., 1.}) { + const Vec3d dir_mesh = d * tr.linear().inverse().transpose() * normal; + const Vec3d src = tr * (m_contour_points[pt_idx] + d * 0.01 * normal); + auto hit = aabb.query_ray_hit(src, dir_mesh); + + m_debug_pts[part_id].emplace_back(src); + + if (hit.is_inside()) { + // This part belongs to this point. + if (d == 1.) + m_contour_to_parts[pt_idx].first.emplace_back(part_id); + else + m_contour_to_parts[pt_idx].second.emplace_back(part_id); + } + } + } + } + } + + m_valid = true; +} + +// In CutMode::cutTongueAndGroove we use PartSelection just for rendering +PartSelection::PartSelection(const ModelObject *object, int instance_idx_in) : m_instance_idx(instance_idx_in) +{ + add_object(object); + const ModelVolumePtrs &volumes = model_object()->volumes; + const Vec3d inst_offset = model_object()->instances[m_instance_idx]->get_offset(); + int i = 0; + m_cut_parts.resize(volumes.size()); + for (const ModelVolume *volume : volumes) { + assert(volume != nullptr); + if (m_cut_parts[i].raycaster) { delete m_cut_parts[i].raycaster; } + m_cut_parts[i].raycaster = new MeshRaycaster(volume->mesh()); + m_cut_parts[i].glmodel.reset(); + m_cut_parts[i].glmodel.init_from(volume->mesh_ptr()->its); + m_cut_parts[i].trans = Geometry::translation_transform(inst_offset) * model_object()->volumes[i]->get_matrix(); + m_cut_parts[i].is_up_part = volume->is_from_upper(); + i++; + } + + m_valid = true; +} + +void PartSelection::part_render(const Vec3d *normal) +{ + if (!valid()) + return; + + const Camera &camera = wxGetApp().plater()->get_camera(); + const bool is_looking_forward = normal && camera.get_dir_forward().dot(*normal) < 0.05; + + glEnable(GL_DEPTH_TEST); + for (size_t id = 0; id < m_cut_parts.size(); ++id) { // m_parts.size() test + if (normal && ((is_looking_forward && m_cut_parts[id].is_up_part) || (!is_looking_forward && !m_cut_parts[id].is_up_part))) + continue; + GLGizmoAdvancedCut::render_glmodel(m_cut_parts[id].glmodel, m_cut_parts[id].is_up_part ? UPPER_PART_COLOR : LOWER_PART_COLOR, m_cut_parts[id].trans); + } +} + +void PartSelection::add_object(const ModelObject *object) +{ + m_model = Model(); + m_model.add_object(*object); + + const double sla_shift_z = wxGetApp().plater()->canvas3D()->get_selection().get_first_volume()->get_sla_shift_z(); + if (!is_approx(sla_shift_z, 0.)) { + Vec3d inst_offset = model_object()->instances[m_instance_idx]->get_offset(); + inst_offset[Z] += sla_shift_z; + model_object()->instances[m_instance_idx]->set_offset(inst_offset); + } +} + +bool PartSelection::is_one_object() const +{ + // In theory, the implementation could be just this: + // return m_contour_to_parts.size() == m_ignored_contours.size(); + // However, this would require that the part-contour correspondence works + // flawlessly. Because it is currently not always so for self-intersecting + // objects, let's better check the parts itself: + if (m_cut_parts.size() < 2) return true; + return std::all_of(m_cut_parts.begin(), m_cut_parts.end(), [this](const PartPara &part) { return part.is_up_part == m_cut_parts.front().is_up_part; }); +} + +std::vector PartSelection::get_cut_parts() +{ + std::vector parts; + for (const auto &part : m_cut_parts) parts.push_back({part.is_up_part, false}); + return parts; +} + +void PartSelection::toggle_selection(const Vec2d &mouse_pos) +{ + const Camera &camera = wxGetApp().plater()->get_camera(); + const Vec3d & camera_pos = camera.get_position(); + + Vec3f pos; + Vec3f normal; + + std::vector> hits_id_and_sqdist; + + for (size_t id = 0; id < m_cut_parts.size(); ++id) { + // const Vec3d volume_offset = model_object()->volumes[id]->get_offset(); + Transform3d tr = Geometry::translation_transform(model_object()->instances[m_instance_idx]->get_offset()) * + Geometry::translation_transform(model_object()->volumes[id]->get_offset()); + if (m_cut_parts[id].raycaster->unproject_on_mesh(mouse_pos, tr, camera, pos, normal)) { + hits_id_and_sqdist.emplace_back(id, (camera_pos - tr * (pos.cast())).squaredNorm()); + } + } + if (!hits_id_and_sqdist.empty()) { + size_t id = std::min_element(hits_id_and_sqdist.begin(), hits_id_and_sqdist.end(), [](const std::pair &a, const std::pair &b) { + return a.second < b.second; + })->first; + toggle_selection(id); + } +} + +void PartSelection::toggle_selection(int id) +{ + if (id >= 0) { + m_cut_parts[id].is_up_part = !m_cut_parts[id].is_up_part; + + // And now recalculate the contours which should be ignored. + /* m_ignored_contours.clear(); + size_t cont_id = 0; + for (const auto &[parts_above, parts_below] : m_contour_to_parts) { + for (size_t upper : parts_above) { + bool upper_sel = m_cut_parts[upper].is_up_part; + if (std::find_if(parts_below.begin(), parts_below.end(), [this, &upper_sel](const size_t &i) { + return m_cut_parts[i].is_up_part == upper_sel; }) != + parts_below.end()) { + m_ignored_contours.emplace_back(cont_id); + break; + } + } + ++cont_id; + }*/ + } +} + +void PartSelection::turn_over_selection() +{ + for (PartPara &part : m_cut_parts) part.is_up_part = !part.is_up_part; +} + } // namespace GUI } // namespace Slic3r diff --git a/src/slic3r/GUI/Gizmos/GLGizmoAdvancedCut.hpp b/src/slic3r/GUI/Gizmos/GLGizmoAdvancedCut.hpp index 21c70498a..ba6282ac7 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoAdvancedCut.hpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoAdvancedCut.hpp @@ -4,7 +4,8 @@ #include "GLGizmoBase.hpp" #include "GLGizmoRotate.hpp" #include "libslic3r/Model.hpp" - +#include "libslic3r/CutUtils.hpp" +#include "slic3r/GUI/MeshUtils.hpp" namespace Slic3r { enum class CutConnectorType : int; class ModelVolume; @@ -13,6 +14,57 @@ struct CutConnectorAttributes; namespace GUI { enum class SLAGizmoEventType : unsigned char; +namespace CommonGizmosDataObjects { +class ObjectClipper; +} +class PartSelection +{ +public: + PartSelection() = default; + PartSelection( + const ModelObject *mo, const Transform3d &cut_matrix, int instance_idx, const Vec3d ¢er, const Vec3d &normal, const CommonGizmosDataObjects::ObjectClipper &oc); + PartSelection(const ModelObject *mo, int instance_idx_in); + ~PartSelection() + { + m_model.clear_objects(); + for (size_t i = 0; i < m_cut_parts.size(); i++) { + if (m_cut_parts[i].raycaster) { delete m_cut_parts[i].raycaster; } + } + } + + struct PartPara + { + GLModel glmodel; + MeshRaycaster* raycaster; + bool is_up_part; + Transform3d trans; + }; + void part_render(const Vec3d *normal); + void toggle_selection(const Vec2d &mouse_pos); + void toggle_selection(int id); + void turn_over_selection(); + ModelObject* model_object() { return m_model.objects.front(); } + bool valid() const { return m_valid; } + bool is_one_object() const; + + const std::vector *get_ignored_contours_ptr() const { return (valid() ? &m_ignored_contours : nullptr); } + + std::vector get_cut_parts(); + std::vector &get_parts() { return m_cut_parts; } + +private: + Model m_model; + int m_instance_idx; + std::vector m_cut_parts; + bool m_valid = false; + std::vector, std::vector>> m_contour_to_parts; // for each contour, there is a vector of parts above and a vector of parts below + std::vector m_ignored_contours; // contour that should not be rendered (the parts on both sides will both be parts of the same object) + + std::vector m_contour_points; // Debugging + std::vector> m_debug_pts; // Debugging + void add_object(const ModelObject *object); +}; + class GLGizmoAdvancedCut : public GLGizmoRotate3D { struct Rotate_data { diff --git a/src/slic3r/GUI/Gizmos/GLGizmosCommon.cpp b/src/slic3r/GUI/Gizmos/GLGizmosCommon.cpp index f57c92749..ae7ea219c 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmosCommon.cpp +++ b/src/slic3r/GUI/Gizmos/GLGizmosCommon.cpp @@ -488,6 +488,18 @@ void ObjectClipper::set_range_and_pos(const Vec3d &cpl_normal, double cpl_offset get_pool()->get_canvas()->set_as_dirty(); } +std::vector CommonGizmosDataObjects::ObjectClipper::point_per_contour() const +{ + std::vector pts; + + for (const auto &clipper : m_clippers) { + const std::vector pts_clipper = clipper->point_per_contour(); + pts.insert(pts.end(), pts_clipper.begin(), pts_clipper.end()); + ; + } + return pts; +} + bool ObjectClipper::is_projection_inside_cut(const Vec3d &point) const { return m_clp_ratio != 0. && std::any_of(m_clippers.begin(), m_clippers.end(), [point](const auto &cl) { diff --git a/src/slic3r/GUI/Gizmos/GLGizmosCommon.hpp b/src/slic3r/GUI/Gizmos/GLGizmosCommon.hpp index f4211dfb5..1c97f82cb 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmosCommon.hpp +++ b/src/slic3r/GUI/Gizmos/GLGizmosCommon.hpp @@ -268,6 +268,7 @@ public: void set_range_and_pos(const Vec3d &cpl_normal, double cpl_offset, double pos); + std::vector point_per_contour() const; bool is_projection_inside_cut(const Vec3d &point_in) const; bool has_valid_contour() const; diff --git a/src/slic3r/GUI/MeshUtils.cpp b/src/slic3r/GUI/MeshUtils.cpp index cd9a617f4..619d0f0cd 100644 --- a/src/slic3r/GUI/MeshUtils.cpp +++ b/src/slic3r/GUI/MeshUtils.cpp @@ -95,6 +95,47 @@ bool MeshClipper::has_valid_contour() const return m_result && std::any_of(m_result->cut_islands.begin(), m_result->cut_islands.end(), [](const CutIsland &isl) { return !isl.expoly.empty(); }); } +std::vector MeshClipper::point_per_contour() const { + assert(m_result); + std::vector out; + if (m_result == std::nullopt) { + return out; + } + for (const CutIsland &isl : m_result->cut_islands) { + assert(isl.expoly.contour.size() > 2); + // Now return a point lying inside the contour but not in a hole. + // We do this by taking a point lying close to the edge, repeating + // this several times for different edges and distances from them. + // (We prefer point not extremely close to the border. + bool done = false; + Vec2d p; + size_t i = 1; + while (i < isl.expoly.contour.size()) { + const Vec2d &a = unscale(isl.expoly.contour.points[i - 1]); + const Vec2d &b = unscale(isl.expoly.contour.points[i]); + Vec2d n = (b - a).normalized(); + std::swap(n.x(), n.y()); + n.x() = -1 * n.x(); + double f = 10.; + while (f > 0.05) { + p = (0.5 * (b + a)) + f * n; + if (isl.expoly.contains(Point::new_scale(p))) { + done = true; + break; + } + f = f / 10.; + } + if (done) break; + i += std::max(size_t(2), isl.expoly.contour.size() / 5); + } + // If the above failed, just return the centroid, regardless of whether + // it is inside the contour or in a hole (we must return something). + Vec2d c = done ? p : unscale(isl.expoly.contour.centroid()); + out.emplace_back(m_result->trafo * Vec3d(c.x(), c.y(), 0.)); + } + return out; +} + void MeshClipper::recalculate_triangles() { const Transform3f& instance_matrix_no_translation_no_scaling = m_trafo.get_matrix(true,false,true).cast(); @@ -280,7 +321,22 @@ bool MeshRaycaster::unproject_on_mesh(const Vec2d& mouse_pos, const Transform3d& } -std::vector MeshRaycaster::get_unobscured_idxs(const Geometry::Transformation& trafo, const Camera& camera, const std::vector& points, +bool MeshRaycaster::intersects_line(Vec3d point, Vec3d direction, const Transform3d &trafo) const +{ + Transform3d trafo_inv = trafo.inverse(); + Vec3d to = trafo_inv * (point + direction); + point = trafo_inv * point; + direction = (to - point).normalized(); + + std::vector hits = m_emesh.query_ray_hits(point, direction); + std::vector neg_hits = m_emesh.query_ray_hits(point, -direction); + + return !hits.empty() || !neg_hits.empty(); +} + +std::vector MeshRaycaster::get_unobscured_idxs(const Geometry::Transformation &trafo, + const Camera & camera, + const std::vector & points, const ClippingPlane* clipping_plane) const { std::vector out; diff --git a/src/slic3r/GUI/MeshUtils.hpp b/src/slic3r/GUI/MeshUtils.hpp index 123b8a785..c55a74e98 100644 --- a/src/slic3r/GUI/MeshUtils.hpp +++ b/src/slic3r/GUI/MeshUtils.hpp @@ -97,6 +97,7 @@ public: bool is_projection_inside_cut(const Vec3d &point) const; bool has_valid_contour() const; + std::vector point_per_contour() const; private: void recalculate_triangles(); @@ -154,6 +155,10 @@ public: bool sinking_limit = true ) const; + const sla::IndexedMesh &get_aabb_mesh() const { return m_emesh; } + // Given a point and direction in world coords, returns whether the respective line + // intersects the mesh if it is transformed into world by trafo. + bool intersects_line(Vec3d point, Vec3d direction, const Transform3d &trafo) const; // Given a vector of points in woorld coordinates, this returns vector // of indices of points that are visible (i.e. not cut by clipping plane // or obscured by part of the mesh.