Replaced the repeated application of Cursors (Sphere or Circle) in painting using 2D and 3D Capsules.

Previously, the Cursor (Sphere or Circle) was repeatedly applied between two mouse positions, creating brushstrokes with ripples on the edges between those mouse positions.
Now, a single capsule (3D or 2D) is applied between those mouse positions, which creates brushstrokes without these ripples.
This commit is contained in:
Lukáš Hejl 2021-11-15 12:53:51 +01:00
parent e898eda320
commit 7bb38840e1
6 changed files with 558 additions and 69 deletions

View file

@ -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<Vec3f> &face_normals) const
bool TriangleSelector::Cursor::is_facet_visible(const Cursor &cursor, int facet_idx, const std::vector<Vec3f> &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<Vertex> &vertices) const
{
std::array<Vec3f, 3> 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<Vertex> &vertices) const
bool TriangleSelector::Circle::is_edge_inside_cursor(const Triangle &tr, const std::vector<Vertex> &vertices) const
{
std::array<Vec3f, 3> 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<Vertex> &vertices) const
{
std::array<Vec3f, 3> 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<Vertex> &vertices) const
{
std::array<Vec3f, 3> 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), &centers_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

View file

@ -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<Vertex> &vertices) const;
virtual bool is_edge_inside_cursor(const Triangle &tr, const std::vector<Vertex> &vertices) const = 0;
virtual bool is_facet_visible(int facet_idx, const std::vector<Vec3f> &face_normals) const { return true; }
virtual bool is_facet_visible(int facet_idx, const std::vector<Vec3f> &face_normals) const = 0;
static bool is_facet_visible(const Cursor &cursor, int facet_idx, const std::vector<Vec3f> &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<Vertex> &vertices) const override;
bool is_pointer_in_triangle(const Vec3f &p1, const Vec3f &p2, const Vec3f &p3) const override;
static std::unique_ptr<Cursor> cursor_factory(const Vec3f &center, 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> 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<TriangleSelector::Capsule3D>(first_center, second_center, camera_pos, cursor_radius, trafo_matrix, clipping_plane);
else
return std::make_unique<TriangleSelector::Capsule2D>(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<Vertex> &vertices) const override;
bool is_facet_visible(int facet_idx, const std::vector<Vec3f> &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<Vec3f> &face_normals) const override;
bool is_edge_inside_cursor(const Triangle &tr, const std::vector<Vertex> &vertices) const override;
bool is_facet_visible(int facet_idx, const std::vector<Vec3f> &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<Vertex> &vertices) const override;
bool is_facet_visible(int facet_idx, const std::vector<Vec3f> &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<Vertex> &vertices) const override;
bool is_facet_visible(int facet_idx, const std::vector<Vec3f> &face_normals) const override
{
return TriangleSelector::Cursor::is_facet_visible(*this, facet_idx, face_normals);
}
};
std::pair<std::vector<Vec3i>, std::vector<Vec3i>> precompute_all_neighbors() const;

View file

@ -13,7 +13,8 @@
#include "libslic3r/PresetBundle.hpp"
#include "libslic3r/TriangleMesh.hpp"
#include <memory>
#include <optional>
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<GLGizmoPainterBase::ProjectedMousePosition>
// with the same mesh_idx, but all items in std::vector<GLGizmoPainterBase::ProjectedMousePosition> always have the same mesh_idx.
std::vector<std::vector<GLGizmoPainterBase::ProjectedMousePosition>> GLGizmoPainterBase::get_projected_mouse_positions(const Vec2d &mouse_position, const double resolution, const std::vector<Transform3d> &trafo_matrices) const
{
// List of mouse positions that will be used as seeds for painting.
std::vector<Vec2d> 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<ProjectedMousePosition> 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<std::vector<ProjectedMousePosition>> 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<ProjectedMousePosition> &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<ProjectedMousePosition> &hit_points) -> std::optional<Plane> {
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<double>();
const Vec3d &second_point = hit_points[third_idx - 1].mesh_hit.cast<double>();
const Vec3d &third_point = hit_points[third_idx].mesh_hit.cast<double>();
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 GramSchmidt 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<ProjectedMousePosition> &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> 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<double>();
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<ProjectedMousePosition> 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<double>(point.x());
const double y_cord = unscale<double>(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<float>());
new_hit_points.push_back({new_hit_point.cast<float>(), 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<Vec2d> 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<Transform3d> trafo_matrices;
std::vector<Transform3d> 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<std::vector<ProjectedMousePosition>> 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<ProjectedMousePosition> &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<float>();
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<TriangleSelector::Cursor> 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<TriangleSelector::Cursor> 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;
}

View file

@ -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<std::vector<ProjectedMousePosition>> get_projected_mouse_positions(const Vec2d &mouse_position, double resolution, const std::vector<Transform3d> &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,

View file

@ -304,7 +304,13 @@ Vec3f MeshRaycaster::get_closest_point(const Vec3f& point, Vec3f* normal) const
return closest_point.cast<float>();
}
int MeshRaycaster::get_closest_facet(const Vec3f &point) const
{
int facet_idx = 0;
Vec3d closest_point;
m_emesh.squared_distance(point.cast<double>(), facet_idx, closest_point);
return facet_idx;
}
} // namespace GUI
} // namespace Slic3r

View file

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