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 <yusanka@gmail.com> Date: Fri Jun 23 16:53:29 2023 +0200 WIP: Cut with Tongue and Groove * Implemented preview rendering of groove ... Change-Id: Id5db8742db50aa10f9b5ebb057ba70f92fc22aeb
This commit is contained in:
parent
101ca69402
commit
eea3e32d0d
|
@ -10,7 +10,7 @@
|
|||
#include <wx/sizer.h>
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
#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<float>();
|
||||
Vec3f norm = (tr.linear().inverse().transpose() * normal).cast<float>();
|
||||
|
||||
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<std::vector<Vec3d>>(m_cut_parts.size(), std::vector<Vec3d>());
|
||||
if (std::vector<Vec3d> 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<Cut::Part> PartSelection::get_cut_parts()
|
||||
{
|
||||
std::vector<Cut::Part> 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<std::pair<size_t, double>> 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<double>())).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<size_t, double> &a, const std::pair<size_t, double> &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
|
||||
|
|
|
@ -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<size_t> *get_ignored_contours_ptr() const { return (valid() ? &m_ignored_contours : nullptr); }
|
||||
|
||||
std::vector<Cut::Part> get_cut_parts();
|
||||
std::vector<PartPara> &get_parts() { return m_cut_parts; }
|
||||
|
||||
private:
|
||||
Model m_model;
|
||||
int m_instance_idx;
|
||||
std::vector<PartPara> m_cut_parts;
|
||||
bool m_valid = false;
|
||||
std::vector<std::pair<std::vector<size_t>, std::vector<size_t>>> m_contour_to_parts; // for each contour, there is a vector of parts above and a vector of parts below
|
||||
std::vector<size_t> m_ignored_contours; // contour that should not be rendered (the parts on both sides will both be parts of the same object)
|
||||
|
||||
std::vector<Vec3d> m_contour_points; // Debugging
|
||||
std::vector<std::vector<Vec3d>> m_debug_pts; // Debugging
|
||||
void add_object(const ModelObject *object);
|
||||
};
|
||||
|
||||
class GLGizmoAdvancedCut : public GLGizmoRotate3D
|
||||
{
|
||||
struct Rotate_data {
|
||||
|
|
|
@ -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<Vec3d> CommonGizmosDataObjects::ObjectClipper::point_per_contour() const
|
||||
{
|
||||
std::vector<Vec3d> pts;
|
||||
|
||||
for (const auto &clipper : m_clippers) {
|
||||
const std::vector<Vec3d> 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) {
|
||||
|
|
|
@ -268,6 +268,7 @@ public:
|
|||
|
||||
void set_range_and_pos(const Vec3d &cpl_normal, double cpl_offset, double pos);
|
||||
|
||||
std::vector<Vec3d> point_per_contour() const;
|
||||
bool is_projection_inside_cut(const Vec3d &point_in) const;
|
||||
bool has_valid_contour() const;
|
||||
|
||||
|
|
|
@ -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<Vec3d> MeshClipper::point_per_contour() const {
|
||||
assert(m_result);
|
||||
std::vector<Vec3d> 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<float>();
|
||||
|
@ -280,7 +321,22 @@ bool MeshRaycaster::unproject_on_mesh(const Vec2d& mouse_pos, const Transform3d&
|
|||
}
|
||||
|
||||
|
||||
std::vector<unsigned> MeshRaycaster::get_unobscured_idxs(const Geometry::Transformation& trafo, const Camera& camera, const std::vector<Vec3f>& 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<sla::IndexedMesh::hit_result> hits = m_emesh.query_ray_hits(point, direction);
|
||||
std::vector<sla::IndexedMesh::hit_result> neg_hits = m_emesh.query_ray_hits(point, -direction);
|
||||
|
||||
return !hits.empty() || !neg_hits.empty();
|
||||
}
|
||||
|
||||
std::vector<unsigned> MeshRaycaster::get_unobscured_idxs(const Geometry::Transformation &trafo,
|
||||
const Camera & camera,
|
||||
const std::vector<Vec3f> & points,
|
||||
const ClippingPlane* clipping_plane) const
|
||||
{
|
||||
std::vector<unsigned> out;
|
||||
|
|
|
@ -97,6 +97,7 @@ public:
|
|||
bool is_projection_inside_cut(const Vec3d &point) const;
|
||||
bool has_valid_contour() const;
|
||||
|
||||
std::vector<Vec3d> 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.
|
||||
|
|
Loading…
Reference in New Issue