diff --git a/src/libslic3r/TriangleSelector.cpp b/src/libslic3r/TriangleSelector.cpp index a74bd2179d..2bac762f21 100644 --- a/src/libslic3r/TriangleSelector.cpp +++ b/src/libslic3r/TriangleSelector.cpp @@ -9,6 +9,112 @@ namespace Slic3r { +// Check if the line is whole inside the sphere, or it is partially inside (intersecting) the sphere. +// Inspired by Christer Ericson's Real-Time Collision Detection, pp. 177-179. +static bool test_line_inside_sphere(const Vec3f &line_a, const Vec3f &line_b, const Vec3f &sphere_p, const float sphere_radius) +{ + const float sphere_radius_sqr = Slic3r::sqr(sphere_radius); + const Vec3f line_dir = line_b - line_a; // n + const Vec3f origins_diff = line_a - sphere_p; // m + + const float m_dot_m = origins_diff.dot(origins_diff); + // Check if any of the end-points of the line is inside the sphere. + if (m_dot_m <= sphere_radius_sqr || (line_b - sphere_p).squaredNorm() <= sphere_radius_sqr) + return true; + + // Check if the infinite line is going through the sphere. + const float n_dot_n = line_dir.dot(line_dir); + const float m_dot_n = origins_diff.dot(line_dir); + + const float eq_a = n_dot_n; + const float eq_b = m_dot_n; + const float eq_c = m_dot_m - sphere_radius_sqr; + + const float discr = eq_b * eq_b - eq_a * eq_c; + // A negative discriminant corresponds to the infinite line infinite not going through the sphere. + if (discr < 0.f) + return false; + + // Check if the finite line is going through the sphere. + const float discr_sqrt = std::sqrt(discr); + const float t1 = (-eq_b - discr_sqrt) / eq_a; + if (0.f <= t1 && t1 <= 1.f) + return true; + + const float t2 = (-eq_b + discr_sqrt) / eq_a; + if (0.f <= t2 && t2 <= 1.f && discr_sqrt > 0.f) + return true; + + return false; +} + +// Check if the line is whole inside the finite cylinder, or it is partially inside (intersecting) the finite cylinder. +// Inspired by Christer Ericson's Real-Time Collision Detection, pp. 194-198. +static bool test_line_inside_cylinder(const Vec3f &line_a, const Vec3f &line_b, const Vec3f &cylinder_P, const Vec3f &cylinder_Q, const float cylinder_radius) +{ + assert(cylinder_P != cylinder_Q); + const Vec3f cylinder_dir = cylinder_Q - cylinder_P; // d + auto is_point_inside_finite_cylinder = [&cylinder_P, &cylinder_Q, &cylinder_radius, &cylinder_dir](const Vec3f &pt) { + const Vec3f first_center_diff = cylinder_P - pt; + const Vec3f second_center_diff = cylinder_Q - pt; + // First, check if the point pt is laying between planes defined by cylinder_p and cylinder_q. + // Then check if it is inside the cylinder between cylinder_p and cylinder_q. + return first_center_diff.dot(cylinder_dir) <= 0 && second_center_diff.dot(cylinder_dir) >= 0 && + (first_center_diff.cross(cylinder_dir).norm() / cylinder_dir.norm()) <= cylinder_radius; + }; + + // Check if any of the end-points of the line is inside the cylinder. + if (is_point_inside_finite_cylinder(line_a) || is_point_inside_finite_cylinder(line_b)) + return true; + + // Check if the line is going through the cylinder. + const Vec3f origins_diff = line_a - cylinder_P; // m + const Vec3f line_dir = line_b - line_a; // n + + const float m_dot_d = origins_diff.dot(cylinder_dir); + const float n_dot_d = line_dir.dot(cylinder_dir); + const float d_dot_d = cylinder_dir.dot(cylinder_dir); + + const float n_dot_n = line_dir.dot(line_dir); + const float m_dot_n = origins_diff.dot(line_dir); + const float m_dot_m = origins_diff.dot(origins_diff); + + const float eq_a = d_dot_d * n_dot_n - n_dot_d * n_dot_d; + const float eq_b = d_dot_d * m_dot_n - n_dot_d * m_dot_d; + const float eq_c = d_dot_d * (m_dot_m - Slic3r::sqr(cylinder_radius)) - m_dot_d * m_dot_d; + + const float discr = eq_b * eq_b - eq_a * eq_c; + // A negative discriminant corresponds to the infinite line not going through the infinite cylinder. + if (discr < 0.0f) + return false; + + // Check if the finite line is going through the finite cylinder. + const float discr_sqrt = std::sqrt(discr); + const float t1 = (-eq_b - discr_sqrt) / eq_a; + if (0.f <= t1 && t1 <= 1.f) + if (const float cylinder_endcap_t1 = m_dot_d + t1 * n_dot_d; 0.f <= cylinder_endcap_t1 && cylinder_endcap_t1 <= d_dot_d) + return true; + + const float t2 = (-eq_b + discr_sqrt) / eq_a; + if (0.f <= t2 && t2 <= 1.f) + if (const float cylinder_endcap_t2 = (m_dot_d + t2 * n_dot_d); 0.f <= cylinder_endcap_t2 && cylinder_endcap_t2 <= d_dot_d) + return true; + + return false; +} + +// Check if the line is whole inside the capsule, or it is partially inside (intersecting) the capsule. +static bool test_line_inside_capsule(const Vec3f &line_a, const Vec3f &line_b, const Vec3f &capsule_p, const Vec3f &capsule_q, const float capsule_radius) { + assert(capsule_p != capsule_q); + + // Check if the line intersect any of the spheres forming the capsule. + if (test_line_inside_sphere(line_a, line_b, capsule_p, capsule_radius) || test_line_inside_sphere(line_a, line_b, capsule_q, capsule_radius)) + return true; + + // Check if the line intersects the cylinder between the centers of the spheres. + return test_line_inside_cylinder(line_a, line_b, capsule_p, capsule_q, capsule_radius); +} + #ifndef NDEBUG bool TriangleSelector::verify_triangle_midpoints(const Triangle &tr) const { @@ -902,13 +1008,13 @@ bool TriangleSelector::Cursor::is_pointer_in_triangle(const Triangle &tr, const } // Determine whether this facet is potentially visible (still can be obscured). -bool TriangleSelector::Circle::is_facet_visible(int facet_idx, const std::vector &face_normals) const +bool TriangleSelector::Cursor::is_facet_visible(const Cursor &cursor, int facet_idx, const std::vector &face_normals) { assert(facet_idx < int(face_normals.size())); Vec3f n = face_normals[facet_idx]; - if (!this->uniform_scaling) - n = this->trafo_normal * n; - return n.dot(this->dir) < 0.f; + if (!cursor.uniform_scaling) + n = cursor.trafo_normal * n; + return n.dot(cursor.dir) < 0.f; } // How many vertices of a triangle are inside the circle? @@ -922,8 +1028,27 @@ int TriangleSelector::Cursor::vertices_inside(const Triangle &tr, const std::vec return inside; } +// Is any edge inside Sphere cursor? +bool TriangleSelector::Sphere::is_edge_inside_cursor(const Triangle &tr, const std::vector &vertices) const +{ + std::array pts; + for (int i = 0; i < 3; ++i) { + pts[i] = vertices[tr.verts_idxs[i]].v; + if (!this->uniform_scaling) + pts[i] = this->trafo * pts[i]; + } + + for (int side = 0; side < 3; ++side) { + const Vec3f &edge_a = pts[side]; + const Vec3f &edge_b = pts[side < 2 ? side + 1 : 0]; + if (test_line_inside_sphere(edge_a, edge_b, this->center, this->radius)) + return true; + } + return false; +} + // Is edge inside cursor? -bool TriangleSelector::SinglePointCursor::is_edge_inside_cursor(const Triangle &tr, const std::vector &vertices) const +bool TriangleSelector::Circle::is_edge_inside_cursor(const Triangle &tr, const std::vector &vertices) const { std::array pts; for (int i = 0; i < 3; ++i) { @@ -933,7 +1058,6 @@ bool TriangleSelector::SinglePointCursor::is_edge_inside_cursor(const Triangle & } const Vec3f &p = this->center; - for (int side = 0; side < 3; ++side) { const Vec3f &a = pts[side]; const Vec3f &b = pts[side < 2 ? side + 1 : 0]; @@ -1675,7 +1799,8 @@ TriangleSelector::Cursor::Cursor(const Vec3f &source_, float radius_world, const { Vec3d sf = Geometry::Transformation(trafo_).get_scaling_factor(); if (is_approx(sf(0), sf(1)) && is_approx(sf(1), sf(2))) { - radius_sqr = float(std::pow(radius_world / sf(0), 2)); + radius = float(radius_world / sf(0)); + radius_sqr = float(Slic3r::sqr(radius_world / sf(0))); uniform_scaling = true; } else { // In case that the transformation is non-uniform, all checks whether @@ -1683,7 +1808,8 @@ TriangleSelector::Cursor::Cursor(const Vec3f &source_, float radius_world, const // First transform source in world coords and remember that we did this. source = trafo * source; uniform_scaling = false; - radius_sqr = radius_world * radius_world; + radius = radius_world; + radius_sqr = Slic3r::sqr(radius_world); trafo_normal = trafo.linear().inverse().transpose(); } } @@ -1701,16 +1827,32 @@ TriangleSelector::SinglePointCursor::SinglePointCursor(const Vec3f& center_, con dir = (center - source).normalized(); } +TriangleSelector::DoublePointCursor::DoublePointCursor(const Vec3f &first_center_, const Vec3f &second_center_, const Vec3f &source_, float radius_world, const Transform3d &trafo_, const ClippingPlane &clipping_plane_) + : first_center{first_center_}, second_center{second_center_}, Cursor(source_, radius_world, trafo_, clipping_plane_) +{ + if (!uniform_scaling) { + first_center = trafo * first_center_; + second_center = trafo * second_center_; + } + + // Calculate dir, in whatever coords is appropriate. + dir = (first_center - source).normalized(); +} + +// Returns true if clipping plane is not active or if the point not clipped by clipping plane. +inline static bool is_mesh_point_not_clipped(const Vec3f &point, const TriangleSelector::ClippingPlane &clipping_plane) +{ + return !clipping_plane.is_active() || !clipping_plane.is_mesh_point_clipped(point); +} + // Is a point (in mesh coords) inside a Sphere cursor? bool TriangleSelector::Sphere::is_mesh_point_inside(const Vec3f &point) const { const Vec3f transformed_point = uniform_scaling ? point : Vec3f(trafo * point); - const bool is_point_inside = (center - point).squaredNorm() < radius_sqr; + if ((center - transformed_point).squaredNorm() < radius_sqr) + return is_mesh_point_not_clipped(point, clipping_plane); - if (is_point_inside && clipping_plane.is_active()) - return !clipping_plane.is_mesh_point_clipped(point); - - return is_point_inside; + return false; } // Is a point (in mesh coords) inside a Circle cursor? @@ -1718,12 +1860,62 @@ bool TriangleSelector::Circle::is_mesh_point_inside(const Vec3f &point) const { const Vec3f transformed_point = uniform_scaling ? point : Vec3f(trafo * point); const Vec3f diff = center - transformed_point; - const bool is_point_inside = (diff - diff.dot(dir) * dir).squaredNorm() < radius_sqr; - if (is_point_inside && clipping_plane.is_active()) - return !clipping_plane.is_mesh_point_clipped(point); + if ((diff - diff.dot(dir) * dir).squaredNorm() < radius_sqr) + return is_mesh_point_not_clipped(point, clipping_plane); - return is_point_inside; + return false; +} + +// Is a point (in mesh coords) inside a Capsule3D cursor? +bool TriangleSelector::Capsule3D::is_mesh_point_inside(const Vec3f &point) const +{ + const Vec3f transformed_point = uniform_scaling ? point : Vec3f(trafo * point); + const Vec3f first_center_diff = this->first_center - transformed_point; + const Vec3f second_center_diff = this->second_center - transformed_point; + if (first_center_diff.squaredNorm() < this->radius_sqr || second_center_diff.squaredNorm() < this->radius_sqr) + return is_mesh_point_not_clipped(point, clipping_plane); + + // First, check if the point pt is laying between planes defined by first_center and second_center. + // Then check if it is inside the cylinder between first_center and second_center. + const Vec3f centers_diff = this->second_center - this->first_center; + if (first_center_diff.dot(centers_diff) <= 0.f && second_center_diff.dot(centers_diff) >= 0.f && (first_center_diff.cross(centers_diff).norm() / centers_diff.norm()) <= this->radius) + return is_mesh_point_not_clipped(point, clipping_plane); + + return false; +} + +// Is a point (in mesh coords) inside a Capsule2D cursor? +bool TriangleSelector::Capsule2D::is_mesh_point_inside(const Vec3f &point) const +{ + const Vec3f transformed_point = uniform_scaling ? point : Vec3f(trafo * point); + const Vec3f first_center_diff = this->first_center - transformed_point; + const Vec3f first_center_diff_projected = first_center_diff - first_center_diff.dot(this->dir) * this->dir; + if (first_center_diff_projected.squaredNorm() < this->radius_sqr) + return is_mesh_point_not_clipped(point, clipping_plane); + + const Vec3f second_center_diff = this->second_center - transformed_point; + const Vec3f second_center_diff_projected = second_center_diff - second_center_diff.dot(this->dir) * this->dir; + if (second_center_diff_projected.squaredNorm() < this->radius_sqr) + return is_mesh_point_not_clipped(point, clipping_plane); + + const Vec3f centers_diff = this->second_center - this->first_center; + const Vec3f centers_diff_projected = centers_diff - centers_diff.dot(this->dir) * this->dir; + + // First, check if the point is laying between first_center and second_center. + if (first_center_diff_projected.dot(centers_diff_projected) <= 0.f && second_center_diff_projected.dot(centers_diff_projected) >= 0.f) { + // Vector in the direction of line |AD| of the rectangle that intersects the circle with the center in first_center. + const Vec3f rectangle_da_dir = centers_diff.cross(this->dir); + // Vector pointing from first_center to the point 'A' of the rectangle. + const Vec3f first_center_rectangle_a_diff = rectangle_da_dir.normalized() * this->radius; + const Vec3f rectangle_a = this->first_center - first_center_rectangle_a_diff; + const Vec3f rectangle_d = this->first_center + first_center_rectangle_a_diff; + // Now check if the point is laying inside the rectangle between circles with centers in first_center and second_center. + if ((rectangle_a - transformed_point).dot(rectangle_da_dir) <= 0.f && (rectangle_d - transformed_point).dot(rectangle_da_dir) >= 0.f) + return is_mesh_point_not_clipped(point, clipping_plane); + } + + return false; } // p1, p2, p3 are in mesh coords! @@ -1754,4 +1946,102 @@ bool TriangleSelector::SinglePointCursor::is_pointer_in_triangle(const Vec3f &p1 return is_circle_pointer_inside_triangle(p1_, p2_, p3_, center, dir, uniform_scaling, trafo); } +// p1, p2, p3 are in mesh coords! +bool TriangleSelector::DoublePointCursor::is_pointer_in_triangle(const Vec3f &p1_, const Vec3f &p2_, const Vec3f &p3_) const +{ + return is_circle_pointer_inside_triangle(p1_, p2_, p3_, first_center, dir, uniform_scaling, trafo) || + is_circle_pointer_inside_triangle(p1_, p2_, p3_, second_center, dir, uniform_scaling, trafo); +} + +bool line_plane_intersection(const Vec3f &line_a, const Vec3f &line_b, const Vec3f &plane_origin, const Vec3f &plane_normal, Vec3f &out_intersection) +{ + Vec3f line_dir = line_b - line_a; + float t_denominator = plane_normal.dot(line_dir); + if (t_denominator == 0.f) + return false; + + // Compute 'd' in plane equation by using some point (origin) on the plane + float plane_d = plane_normal.dot(plane_origin); + if (float t = (plane_d - plane_normal.dot(line_a)) / t_denominator; t >= 0.f && t <= 1.f) { + out_intersection = line_a + t * line_dir; + return true; + } + + return false; +} + +bool TriangleSelector::Capsule3D::is_edge_inside_cursor(const Triangle &tr, const std::vector &vertices) const +{ + std::array pts; + for (int i = 0; i < 3; ++i) { + pts[i] = vertices[tr.verts_idxs[i]].v; + if (!this->uniform_scaling) + pts[i] = this->trafo * pts[i]; + } + + for (int side = 0; side < 3; ++side) { + const Vec3f &edge_a = pts[side]; + const Vec3f &edge_b = pts[side < 2 ? side + 1 : 0]; + if (test_line_inside_capsule(edge_a, edge_b, this->first_center, this->second_center, this->radius)) + return true; + } + + return false; +} + +// Is edge inside cursor? +bool TriangleSelector::Capsule2D::is_edge_inside_cursor(const Triangle &tr, const std::vector &vertices) const +{ + std::array pts; + for (int i = 0; i < 3; ++i) { + pts[i] = vertices[tr.verts_idxs[i]].v; + if (!this->uniform_scaling) + pts[i] = this->trafo * pts[i]; + } + + const Vec3f centers_diff = this->second_center - this->first_center; + // Vector in the direction of line |AD| of the rectangle that intersects the circle with the center in first_center. + const Vec3f rectangle_da_dir = centers_diff.cross(this->dir); + // Vector pointing from first_center to the point 'A' of the rectangle. + const Vec3f first_center_rectangle_a_diff = rectangle_da_dir.normalized() * this->radius; + const Vec3f rectangle_a = this->first_center - first_center_rectangle_a_diff; + const Vec3f rectangle_d = this->first_center + first_center_rectangle_a_diff; + + auto edge_inside_rectangle = [&self = std::as_const(*this), ¢ers_diff](const Vec3f &edge_a, const Vec3f &edge_b, const Vec3f &plane_origin, const Vec3f &plane_normal) -> bool { + Vec3f intersection(-1.f, -1.f, -1.f); + if (line_plane_intersection(edge_a, edge_b, plane_origin, plane_normal, intersection)) { + // Now check if the intersection point is inside the rectangle. That means it is between 'first_center' and 'second_center', resp. between 'A' and 'B'. + if (self.first_center.dot(centers_diff) <= intersection.dot(centers_diff) && intersection.dot(centers_diff) <= self.second_center.dot(centers_diff)) + return true; + } + return false; + }; + + for (int side = 0; side < 3; ++side) { + const Vec3f &edge_a = pts[side]; + const Vec3f &edge_b = pts[side < 2 ? side + 1 : 0]; + const Vec3f edge_dir = edge_b - edge_a; + const Vec3f edge_dir_n = edge_dir.normalized(); + + float t1 = (this->first_center - edge_a).dot(edge_dir_n); + float t2 = (this->second_center - edge_a).dot(edge_dir_n); + Vec3f vector1 = edge_a + t1 * edge_dir_n - this->first_center; + Vec3f vector2 = edge_a + t2 * edge_dir_n - this->second_center; + + // Vectors vector1 and vector2 are 3D vector from centers to the intersections. What we want to + // measure is length of its projection onto plane perpendicular to dir. + if (float dist = vector1.squaredNorm() - std::pow(vector1.dot(this->dir), 2.f); dist < this->radius_sqr && t1 >= 0.f && t1 <= edge_dir.norm()) + return true; + + if (float dist = vector2.squaredNorm() - std::pow(vector2.dot(this->dir), 2.f); dist < this->radius_sqr && t2 >= 0.f && t2 <= edge_dir.norm()) + return true; + + // Check if the edge is passing through the rectangle between first_center and second_center. + if (edge_inside_rectangle(edge_a, edge_b, rectangle_a, (rectangle_d - rectangle_a)) || edge_inside_rectangle(edge_a, edge_b, rectangle_d, (rectangle_a - rectangle_d))) + return true; + } + + return false; +} + } // namespace Slic3r diff --git a/src/libslic3r/TriangleSelector.hpp b/src/libslic3r/TriangleSelector.hpp index 05930731cd..09b833e825 100644 --- a/src/libslic3r/TriangleSelector.hpp +++ b/src/libslic3r/TriangleSelector.hpp @@ -52,7 +52,9 @@ public: virtual bool is_pointer_in_triangle(const Vec3f &p1, const Vec3f &p2, const Vec3f &p3) const = 0; virtual int vertices_inside(const Triangle &tr, const std::vector &vertices) const; virtual bool is_edge_inside_cursor(const Triangle &tr, const std::vector &vertices) const = 0; - virtual bool is_facet_visible(int facet_idx, const std::vector &face_normals) const { return true; } + virtual bool is_facet_visible(int facet_idx, const std::vector &face_normals) const = 0; + + static bool is_facet_visible(const Cursor &cursor, int facet_idx, const std::vector &face_normals); protected: explicit Cursor(const Vec3f &source_, float radius_world, const Transform3d &trafo_, const ClippingPlane &clipping_plane_); @@ -62,6 +64,7 @@ public: bool uniform_scaling; Transform3f trafo_normal; + float radius; float radius_sqr; Vec3f dir = Vec3f(0.f, 0.f, 0.f); @@ -76,7 +79,6 @@ public: SinglePointCursor() = delete; ~SinglePointCursor() override = default; - bool is_edge_inside_cursor(const Triangle &tr, const std::vector &vertices) const override; bool is_pointer_in_triangle(const Vec3f &p1, const Vec3f &p2, const Vec3f &p3) const override; static std::unique_ptr cursor_factory(const Vec3f ¢er, const Vec3f &camera_pos, const float cursor_radius, const CursorType cursor_type, const Transform3d &trafo_matrix, const ClippingPlane &clipping_plane) @@ -94,6 +96,30 @@ public: Vec3f center; }; + class DoublePointCursor : public Cursor + { + public: + DoublePointCursor() = delete; + ~DoublePointCursor() override = default; + + bool is_pointer_in_triangle(const Vec3f &p1, const Vec3f &p2, const Vec3f &p3) const override; + + static std::unique_ptr cursor_factory(const Vec3f &first_center, const Vec3f &second_center, const Vec3f &camera_pos, const float cursor_radius, const CursorType cursor_type, const Transform3d &trafo_matrix, const ClippingPlane &clipping_plane) + { + assert(cursor_type == TriangleSelector::CursorType::CIRCLE || cursor_type == TriangleSelector::CursorType::SPHERE); + if (cursor_type == TriangleSelector::CursorType::SPHERE) + return std::make_unique(first_center, second_center, camera_pos, cursor_radius, trafo_matrix, clipping_plane); + else + return std::make_unique(first_center, second_center, camera_pos, cursor_radius, trafo_matrix, clipping_plane); + } + + protected: + explicit DoublePointCursor(const Vec3f &first_center_, const Vec3f &second_center_, const Vec3f &source_, float radius_world, const Transform3d &trafo_, const ClippingPlane &clipping_plane_); + + Vec3f first_center; + Vec3f second_center; + }; + class Sphere : public SinglePointCursor { public: @@ -103,6 +129,8 @@ public: ~Sphere() override = default; bool is_mesh_point_inside(const Vec3f &point) const override; + bool is_edge_inside_cursor(const Triangle &tr, const std::vector &vertices) const override; + bool is_facet_visible(int facet_idx, const std::vector &face_normals) const override { return true; } }; class Circle : public SinglePointCursor @@ -114,7 +142,42 @@ public: ~Circle() override = default; bool is_mesh_point_inside(const Vec3f &point) const override; - bool is_facet_visible(int facet_idx, const std::vector &face_normals) const override; + bool is_edge_inside_cursor(const Triangle &tr, const std::vector &vertices) const override; + bool is_facet_visible(int facet_idx, const std::vector &face_normals) const override + { + return TriangleSelector::Cursor::is_facet_visible(*this, facet_idx, face_normals); + } + }; + + class Capsule3D : public DoublePointCursor + { + public: + Capsule3D() = delete; + explicit Capsule3D(const Vec3f &first_center_, const Vec3f &second_center_, const Vec3f &source_, float radius_world, const Transform3d &trafo_, const ClippingPlane &clipping_plane_) + : TriangleSelector::DoublePointCursor(first_center_, second_center_, source_, radius_world, trafo_, clipping_plane_) + {} + ~Capsule3D() override = default; + + bool is_mesh_point_inside(const Vec3f &point) const override; + bool is_edge_inside_cursor(const Triangle &tr, const std::vector &vertices) const override; + bool is_facet_visible(int facet_idx, const std::vector &face_normals) const override { return true; } + }; + + class Capsule2D : public DoublePointCursor + { + public: + Capsule2D() = delete; + explicit Capsule2D(const Vec3f &first_center_, const Vec3f &second_center_, const Vec3f &source_, float radius_world, const Transform3d &trafo_, const ClippingPlane &clipping_plane_) + : TriangleSelector::DoublePointCursor(first_center_, second_center_, source_, radius_world, trafo_, clipping_plane_) + {} + ~Capsule2D() override = default; + + bool is_mesh_point_inside(const Vec3f &point) const override; + bool is_edge_inside_cursor(const Triangle &tr, const std::vector &vertices) const override; + bool is_facet_visible(int facet_idx, const std::vector &face_normals) const override + { + return TriangleSelector::Cursor::is_facet_visible(*this, facet_idx, face_normals); + } }; std::pair, std::vector> precompute_all_neighbors() const; diff --git a/src/slic3r/GUI/Gizmos/GLGizmoPainterBase.cpp b/src/slic3r/GUI/Gizmos/GLGizmoPainterBase.cpp index 1b9d996e65..e164caee6d 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoPainterBase.cpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoPainterBase.cpp @@ -13,7 +13,8 @@ #include "libslic3r/PresetBundle.hpp" #include "libslic3r/TriangleMesh.hpp" - +#include +#include namespace Slic3r::GUI { @@ -223,6 +224,126 @@ bool GLGizmoPainterBase::is_mesh_point_clipped(const Vec3d& point, const Transfo return m_c->object_clipper()->get_clipping_plane()->is_point_clipped(transformed_point); } +// Interpolate points between the previous and current mouse positions, which are then projected onto the object. +// Returned projected mouse positions are grouped by mesh_idx. It may contain multiple std::vector +// with the same mesh_idx, but all items in std::vector always have the same mesh_idx. +std::vector> GLGizmoPainterBase::get_projected_mouse_positions(const Vec2d &mouse_position, const double resolution, const std::vector &trafo_matrices) const +{ + // List of mouse positions that will be used as seeds for painting. + std::vector mouse_positions{mouse_position}; + if (m_last_mouse_click != Vec2d::Zero()) { + // In case current mouse position is far from the last one, + // add several positions from between into the list, so there + // are no gaps in the painted region. + if (size_t patches_in_between = size_t((mouse_position - m_last_mouse_click).norm() / resolution); patches_in_between > 0) { + const Vec2d diff = (m_last_mouse_click - mouse_position) / (patches_in_between + 1); + for (size_t patch_idx = 1; patch_idx <= patches_in_between; ++patch_idx) + mouse_positions.emplace_back(mouse_position + patch_idx * diff); + mouse_positions.emplace_back(m_last_mouse_click); + } + } + + const Camera &camera = wxGetApp().plater()->get_camera(); + std::vector mesh_hit_points; + mesh_hit_points.reserve(mouse_position.size()); + + // In mesh_hit_points only the last item could have mesh_id == -1, any other items mustn't. + for (const Vec2d &mp : mouse_positions) { + update_raycast_cache(mp, camera, trafo_matrices); + mesh_hit_points.push_back({m_rr.hit, m_rr.mesh_id, m_rr.facet}); + if (m_rr.mesh_id == -1) + break; + } + + // Divide mesh_hit_points into groups with the same mesh_idx. It may contain multiple groups with the same mesh_idx. + std::vector> mesh_hit_points_by_mesh; + for (size_t prev_mesh_hit_point = 0, curr_mesh_hit_point = 0; curr_mesh_hit_point < mesh_hit_points.size(); ++curr_mesh_hit_point) { + size_t next_mesh_hit_point = curr_mesh_hit_point + 1; + if (next_mesh_hit_point >= mesh_hit_points.size() || mesh_hit_points[curr_mesh_hit_point].mesh_idx != mesh_hit_points[next_mesh_hit_point].mesh_idx) { + mesh_hit_points_by_mesh.emplace_back(); + mesh_hit_points_by_mesh.back().insert(mesh_hit_points_by_mesh.back().end(), mesh_hit_points.begin() + int(prev_mesh_hit_point), mesh_hit_points.begin() + int(next_mesh_hit_point)); + prev_mesh_hit_point = next_mesh_hit_point; + } + } + + auto on_same_facet = [](std::vector &hit_points) -> bool { + for (const ProjectedMousePosition &mesh_hit_point : hit_points) + if (mesh_hit_point.facet_idx != hit_points.front().facet_idx) + return false; + return true; + }; + + struct Plane + { + Vec3d origin; + Vec3d first_axis; + Vec3d second_axis; + }; + auto find_plane = [](std::vector &hit_points) -> std::optional { + assert(hit_points.size() >= 3); + for (size_t third_idx = 2; third_idx < hit_points.size(); ++third_idx) { + const Vec3d &first_point = hit_points[third_idx - 2].mesh_hit.cast(); + const Vec3d &second_point = hit_points[third_idx - 1].mesh_hit.cast(); + const Vec3d &third_point = hit_points[third_idx].mesh_hit.cast(); + + const Vec3d first_vec = first_point - second_point; + const Vec3d second_vec = third_point - second_point; + + // If three points aren't collinear, then there exists only one plane going through all points. + if (first_vec.cross(second_vec).squaredNorm() > sqr(EPSILON)) { + const Vec3d first_axis_vec_n = first_vec.normalized(); + // Make second_vec perpendicular to first_axis_vec_n using Gram–Schmidt orthogonalization process + const Vec3d second_axis_vec_n = (second_vec - (first_vec.dot(second_vec) / first_vec.dot(first_vec)) * first_vec).normalized(); + return Plane{second_point, first_axis_vec_n, second_axis_vec_n}; + } + } + + return std::nullopt; + }; + + for(std::vector &hit_points : mesh_hit_points_by_mesh) { + assert(!hit_points.empty()); + if (hit_points.back().mesh_idx == -1) + break; + + if (hit_points.size() <= 2) + continue; + + if (on_same_facet(hit_points)) { + hit_points = {hit_points.front(), hit_points.back()}; + } else if (std::optional plane = find_plane(hit_points); plane) { + Polyline polyline; + polyline.points.reserve(hit_points.size()); + // Project hit_points into its plane to simplified them in the next step. + for (auto &hit_point : hit_points) { + const Vec3d &point = hit_point.mesh_hit.cast(); + const double x_cord = plane->first_axis.dot(point - plane->origin); + const double y_cord = plane->second_axis.dot(point - plane->origin); + polyline.points.emplace_back(scale_(x_cord), scale_(y_cord)); + } + + polyline.simplify(scale_(m_cursor_radius) / 10.); + + const int mesh_idx = hit_points.front().mesh_idx; + std::vector new_hit_points; + new_hit_points.reserve(polyline.points.size()); + // Project 2D simplified hit_points beck to 3D. + for (const Point &point : polyline.points) { + const double x_cord = unscale(point.x()); + const double y_cord = unscale(point.y()); + const Vec3d new_hit_point = plane->origin + x_cord * plane->first_axis + y_cord * plane->second_axis; + const int facet_idx = m_c->raycaster()->raycasters()[mesh_idx]->get_closest_facet(new_hit_point.cast()); + new_hit_points.push_back({new_hit_point.cast(), mesh_idx, size_t(facet_idx)}); + } + + hit_points = new_hit_points; + } else { + hit_points = {hit_points.front(), hit_points.back()}; + } + } + + return mesh_hit_points_by_mesh; +} // Following function is called from GLCanvas3D to inform the gizmo about a mouse/keyboard event. // The gizmo has an opportunity to react - if it does, it should return true so that the Canvas3D is @@ -295,28 +416,6 @@ bool GLGizmoPainterBase::gizmo_event(SLAGizmoEventType action, const Vec2d& mous const Transform3d instance_trafo = mi->get_transformation().get_matrix(); const Transform3d instance_trafo_not_translate = mi->get_transformation().get_matrix(true); - // List of mouse positions that will be used as seeds for painting. - std::vector mouse_positions{mouse_position}; - - // In case current mouse position is far from the last one, - // add several positions from between into the list, so there - // are no gaps in the painted region. - { - if (m_last_mouse_click == Vec2d::Zero()) - m_last_mouse_click = mouse_position; - // resolution describes minimal distance limit using circle radius - // as a unit (e.g., 2 would mean the patches will be touching). - double resolution = 0.7; - double diameter_px = resolution * m_cursor_radius * camera.get_zoom(); - int patches_in_between = int(((mouse_position - m_last_mouse_click).norm() - diameter_px) / diameter_px); - if (patches_in_between > 0) { - Vec2d diff = (mouse_position - m_last_mouse_click)/(patches_in_between+1); - for (int i=1; i<=patches_in_between; ++i) - mouse_positions.emplace_back(m_last_mouse_click + i*diff); - } - } - m_last_mouse_click = Vec2d::Zero(); // only actual hits should be saved - // Precalculate transformations of individual meshes. std::vector trafo_matrices; std::vector trafo_matrices_not_translate; @@ -326,51 +425,70 @@ bool GLGizmoPainterBase::gizmo_event(SLAGizmoEventType action, const Vec2d& mous trafo_matrices_not_translate.emplace_back(instance_trafo_not_translate * mv->get_matrix(true)); } - // Now "click" into all the prepared points and spill paint around them. - for (const Vec2d& mp : mouse_positions) { - update_raycast_cache(mp, camera, trafo_matrices); + std::vector> projected_mouse_positions_by_mesh = get_projected_mouse_positions(mouse_position, 1., trafo_matrices); + m_last_mouse_click = Vec2d::Zero(); // only actual hits should be saved - bool dragging_while_painting = (action == SLAGizmoEventType::Dragging && m_button_down != Button::None); + for (const std::vector &projected_mouse_positions : projected_mouse_positions_by_mesh) { + assert(!projected_mouse_positions.empty()); + const int mesh_idx = projected_mouse_positions.front().mesh_idx; + const bool dragging_while_painting = (action == SLAGizmoEventType::Dragging && m_button_down != Button::None); // The mouse button click detection is enabled when there is a valid hit. // Missing the object entirely // shall not capture the mouse. - if (m_rr.mesh_id != -1) { + if (mesh_idx != -1) if (m_button_down == Button::None) m_button_down = ((action == SLAGizmoEventType::LeftDown) ? Button::Left : Button::Right); - } - if (m_rr.mesh_id == -1) { - // In case we have no valid hit, we can return. The event will be stopped when - // dragging while painting (to prevent scene rotations and moving the object) + // In case we have no valid hit, we can return. The event will be stopped when + // dragging while painting (to prevent scene rotations and moving the object) + if (mesh_idx == -1) return dragging_while_painting; - } - const Transform3d &trafo_matrix = trafo_matrices[m_rr.mesh_id]; - const Transform3d &trafo_matrix_not_translate = trafo_matrices_not_translate[m_rr.mesh_id]; + const Transform3d &trafo_matrix = trafo_matrices[mesh_idx]; + const Transform3d &trafo_matrix_not_translate = trafo_matrices_not_translate[mesh_idx]; // Calculate direction from camera to the hit (in mesh coords): Vec3f camera_pos = (trafo_matrix.inverse() * camera.get_position()).cast(); - assert(m_rr.mesh_id < int(m_triangle_selectors.size())); + assert(mesh_idx < 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_tool_type == ToolType::BUCKET_FILL || (m_tool_type == ToolType::BRUSH && m_cursor_type == TriangleSelector::CursorType::POINTER)) { - m_triangle_selectors[m_rr.mesh_id]->seed_fill_apply_on_triangles(new_state); - 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_paint_on_overhangs_only ? m_highlight_by_angle_threshold_deg : 0.f, true); - 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, false, true); - else if (m_tool_type == ToolType::BUCKET_FILL) - m_triangle_selectors[m_rr.mesh_id]->bucket_fill_select_triangles(m_rr.hit, int(m_rr.facet), clp, true, true); + for(const ProjectedMousePosition &projected_mouse_position : projected_mouse_positions) { + assert(projected_mouse_position.mesh_idx == mesh_idx); + const Vec3f mesh_hit = projected_mouse_position.mesh_hit; + 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_paint_on_overhangs_only ? m_highlight_by_angle_threshold_deg : 0.f, true); + 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, false, true); + else if (m_tool_type == ToolType::BUCKET_FILL) + m_triangle_selectors[mesh_idx]->bucket_fill_select_triangles(mesh_hit, facet_idx, clp, true, true); - m_seed_fill_last_mesh_id = -1; + m_seed_fill_last_mesh_id = -1; + } } else if (m_tool_type == ToolType::BRUSH) { - auto cursor = TriangleSelector::SinglePointCursor::cursor_factory(m_rr.hit, camera_pos, m_cursor_radius, m_cursor_type, trafo_matrix, clp); - m_triangle_selectors[m_rr.mesh_id]->select_patch(int(m_rr.facet), std::move(cursor), new_state, trafo_matrix_not_translate, m_triangle_splitting_enabled, m_paint_on_overhangs_only ? m_highlight_by_angle_threshold_deg : 0.f); + assert(m_cursor_type == TriangleSelector::CursorType::CIRCLE || m_cursor_type == TriangleSelector::CursorType::SPHERE); + + if (projected_mouse_positions.size() == 1) { + const ProjectedMousePosition &first_position = projected_mouse_positions.front(); + std::unique_ptr cursor = TriangleSelector::SinglePointCursor::cursor_factory(first_position.mesh_hit, + camera_pos, m_cursor_radius, + m_cursor_type, trafo_matrix, clp); + m_triangle_selectors[mesh_idx]->select_patch(int(first_position.facet_idx), std::move(cursor), new_state, trafo_matrix_not_translate, + m_triangle_splitting_enabled, m_paint_on_overhangs_only ? m_highlight_by_angle_threshold_deg : 0.f); + } else { + for (auto first_position_it = projected_mouse_positions.cbegin(); first_position_it != projected_mouse_positions.cend() - 1; ++first_position_it) { + auto second_position_it = first_position_it + 1; + std::unique_ptr cursor = TriangleSelector::DoublePointCursor::cursor_factory(first_position_it->mesh_hit, second_position_it->mesh_hit, camera_pos, m_cursor_radius, m_cursor_type, trafo_matrix, clp); + m_triangle_selectors[mesh_idx]->select_patch(int(first_position_it->facet_idx), std::move(cursor), new_state, trafo_matrix_not_translate, m_triangle_splitting_enabled, m_paint_on_overhangs_only ? m_highlight_by_angle_threshold_deg : 0.f); + } + } } - m_triangle_selectors[m_rr.mesh_id]->request_update_render_data(); + m_triangle_selectors[mesh_idx]->request_update_render_data(); m_last_mouse_click = mouse_position; } diff --git a/src/slic3r/GUI/Gizmos/GLGizmoPainterBase.hpp b/src/slic3r/GUI/Gizmos/GLGizmoPainterBase.hpp index ff030f19f2..97ac8e4e98 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoPainterBase.hpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoPainterBase.hpp @@ -156,6 +156,13 @@ protected: SMART_FILL }; + struct ProjectedMousePosition + { + Vec3f mesh_hit; + int mesh_idx; + size_t facet_idx; + }; + bool m_triangle_splitting_enabled = true; ToolType m_tool_type = ToolType::BRUSH; float m_smart_fill_angle = 30.f; @@ -188,6 +195,8 @@ protected: TriangleSelector::ClippingPlane get_clipping_plane_in_volume_coordinates(const Transform3d &trafo) const; private: + std::vector> get_projected_mouse_positions(const Vec2d &mouse_position, double resolution, const std::vector &trafo_matrices) const; + bool is_mesh_point_clipped(const Vec3d& point, const Transform3d& trafo) const; void update_raycast_cache(const Vec2d& mouse_position, const Camera& camera, diff --git a/src/slic3r/GUI/MeshUtils.cpp b/src/slic3r/GUI/MeshUtils.cpp index 1e68123195..440b0ec0db 100644 --- a/src/slic3r/GUI/MeshUtils.cpp +++ b/src/slic3r/GUI/MeshUtils.cpp @@ -304,7 +304,13 @@ Vec3f MeshRaycaster::get_closest_point(const Vec3f& point, Vec3f* normal) const return closest_point.cast(); } - +int MeshRaycaster::get_closest_facet(const Vec3f &point) const +{ + int facet_idx = 0; + Vec3d closest_point; + m_emesh.squared_distance(point.cast(), facet_idx, closest_point); + return facet_idx; +} } // namespace GUI } // namespace Slic3r diff --git a/src/slic3r/GUI/MeshUtils.hpp b/src/slic3r/GUI/MeshUtils.hpp index ccdb830420..bb8a1aa618 100644 --- a/src/slic3r/GUI/MeshUtils.hpp +++ b/src/slic3r/GUI/MeshUtils.hpp @@ -151,6 +151,9 @@ public: Vec3f get_closest_point(const Vec3f& point, Vec3f* normal = nullptr) const; + // Given a point in mesh coords, the method returns the closest facet from mesh. + int get_closest_facet(const Vec3f &point) const; + Vec3f get_triangle_normal(size_t facet_idx) const; private: