From fc089fd2c54b31de4b5a260c1f573b04eda2e4df Mon Sep 17 00:00:00 2001 From: YuSanka Date: Thu, 2 Dec 2021 11:24:21 +0100 Subject: [PATCH 01/21] Code refactoring for https://github.com/prusa3d/PrusaSlicer/commit/d88ef826cd9be79ef55d735dda5a938a085da3f2 reload_scene() call were followed by update(). It synchronizes back-end with front-end and then it calls reload_scene() again. However in SLA mode reload_scene() expects the back-end to be synchronized with front-end, thus we get asserts that we all have ignored for a long time. So, we call ObjectList::update_info_items() after the call of update() where reload_scene() is already called and GLCanvas3D::is_object_sinking() will return correct value --- src/slic3r/GUI/Plater.cpp | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index e61015388c..baa6fd4cef 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -2736,16 +2736,17 @@ std::vector Plater::priv::load_model_objects(const ModelObjectPtrs& mode _L("Object too large?")); } - // Now ObjectList uses GLCanvas3D::is_object_sinkin() to show/hide "Sinking" InfoItem, - // so 3D-scene should be updated before object additing to the ObjectList - this->view3D->reload_scene(false, (unsigned int)UpdateParams::FORCE_FULL_SCREEN_REFRESH); - notification_manager->close_notification_of_type(NotificationType::UpdatedItemsInfo); for (const size_t idx : obj_idxs) { wxGetApp().obj_list()->add_object_to_list(idx); } update(); + // Update InfoItems in ObjectList after update() to use of a correct value of the GLCanvas3D::is_sinking(), + // which is updated after a view3D->reload_scene(false, flags & (unsigned int)UpdateParams::FORCE_FULL_SCREEN_REFRESH) call + for (const size_t idx : obj_idxs) + wxGetApp().obj_list()->update_info_items(idx); + object_list_changed(); this->schedule_background_process(); From e898eda320c9eb4fedeb72d3c42e1afd3a841810 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Luk=C3=A1=C5=A1=20Hejl?= Date: Mon, 15 Nov 2021 11:39:47 +0100 Subject: [PATCH 02/21] Refactoring of Cursors in TriangleSelector as preparation for upcoming improvements of painting. --- src/libslic3r/TriangleSelector.cpp | 167 +++++++++---------- src/libslic3r/TriangleSelector.hpp | 126 ++++++++++---- src/slic3r/GUI/Gizmos/GLGizmoPainterBase.cpp | 9 +- 3 files changed, 175 insertions(+), 127 deletions(-) diff --git a/src/libslic3r/TriangleSelector.cpp b/src/libslic3r/TriangleSelector.cpp index 1699786887..a74bd2179d 100644 --- a/src/libslic3r/TriangleSelector.cpp +++ b/src/libslic3r/TriangleSelector.cpp @@ -124,24 +124,20 @@ int TriangleSelector::select_unsplit_triangle(const Vec3f &hit, int facet_idx) c return this->select_unsplit_triangle(hit, facet_idx, neighbors); } -void TriangleSelector::select_patch(const Vec3f& hit, int facet_start, - const Vec3f& source, float radius, - CursorType cursor_type, EnforcerBlockerType new_state, - const Transform3d& trafo, const Transform3d& trafo_no_translate, - bool triangle_splitting, const ClippingPlane &clp, float highlight_by_angle_deg) +void TriangleSelector::select_patch(int facet_start, std::unique_ptr &&cursor, EnforcerBlockerType new_state, const Transform3d& trafo_no_translate, bool triangle_splitting, float highlight_by_angle_deg) { assert(facet_start < m_orig_size_indices); // Save current cursor center, squared radius and camera direction, so we don't // have to pass it around. - m_cursor = Cursor(hit, source, radius, cursor_type, trafo, clp); + m_cursor = std::move(cursor); // In case user changed cursor size since last time, update triangle edge limit. // It is necessary to compare the internal radius in m_cursor! radius is in // world coords and does not change after scaling. - if (m_old_cursor_radius_sqr != m_cursor.radius_sqr) { - set_edge_limit(std::sqrt(m_cursor.radius_sqr) / 5.f); - m_old_cursor_radius_sqr = m_cursor.radius_sqr; + if (m_old_cursor_radius_sqr != m_cursor->radius_sqr) { + set_edge_limit(std::sqrt(m_cursor->radius_sqr) / 5.f); + m_old_cursor_radius_sqr = m_cursor->radius_sqr; } const float highlight_angle_limit = cos(Geometry::deg2rad(highlight_by_angle_deg)); @@ -163,7 +159,7 @@ void TriangleSelector::select_patch(const Vec3f& hit, int facet_start, if (select_triangle(facet, new_state, triangle_splitting)) { // add neighboring facets to list to be processed later for (int neighbor_idx : m_neighbors[facet]) - if (neighbor_idx >= 0 && (m_cursor.type == SPHERE || faces_camera(neighbor_idx))) + if (neighbor_idx >= 0 && m_cursor->is_facet_visible(neighbor_idx, m_face_normals)) facets_to_check.push_back(neighbor_idx); } } @@ -788,11 +784,11 @@ bool TriangleSelector::select_triangle_recursive(int facet_idx, const Vec3i &nei assert(this->verify_triangle_neighbors(*tr, neighbors)); - int num_of_inside_vertices = vertices_inside(facet_idx); + int num_of_inside_vertices = m_cursor->vertices_inside(*tr, m_vertices); if (num_of_inside_vertices == 0 - && ! is_pointer_in_triangle(facet_idx) - && ! is_edge_inside_cursor(facet_idx)) + && ! m_cursor->is_pointer_in_triangle(*tr, m_vertices) + && ! m_cursor->is_edge_inside_cursor(*tr, m_vertices)) return false; if (num_of_inside_vertices == 3) { @@ -840,7 +836,7 @@ void TriangleSelector::set_facet(int facet_idx, EnforcerBlockerType state) } // called by select_patch()->select_triangle()...select_triangle() -// to decide which sides of the traingle to split and to actually split it calling set_division() and perform_split(). +// to decide which sides of the triangle to split and to actually split it calling set_division() and perform_split(). void TriangleSelector::split_triangle(int facet_idx, const Vec3i &neighbors) { if (m_triangles[facet_idx].is_split()) { @@ -864,9 +860,9 @@ void TriangleSelector::split_triangle(int facet_idx, const Vec3i &neighbors) // In case the object is non-uniformly scaled, transform the // points to world coords. - if (! m_cursor.uniform_scaling) { + if (! m_cursor->uniform_scaling) { for (size_t i=0; itrafo * (*pts[i]); pts[i] = &pts_transformed[i]; } } @@ -897,72 +893,63 @@ void TriangleSelector::split_triangle(int facet_idx, const Vec3i &neighbors) perform_split(facet_idx, neighbors, old_type); } - - // Is pointer in a triangle? -bool TriangleSelector::is_pointer_in_triangle(int facet_idx) const -{ - const Vec3f& p1 = m_vertices[m_triangles[facet_idx].verts_idxs[0]].v; - const Vec3f& p2 = m_vertices[m_triangles[facet_idx].verts_idxs[1]].v; - const Vec3f& p3 = m_vertices[m_triangles[facet_idx].verts_idxs[2]].v; - return m_cursor.is_pointer_in_triangle(p1, p2, p3); +bool TriangleSelector::Cursor::is_pointer_in_triangle(const Triangle &tr, const std::vector &vertices) const { + const Vec3f& p1 = vertices[tr.verts_idxs[0]].v; + const Vec3f& p2 = vertices[tr.verts_idxs[1]].v; + const Vec3f& p3 = vertices[tr.verts_idxs[2]].v; + return this->is_pointer_in_triangle(p1, p2, p3); } - - // Determine whether this facet is potentially visible (still can be obscured). -bool TriangleSelector::faces_camera(int facet) const +bool TriangleSelector::Circle::is_facet_visible(int facet_idx, const std::vector &face_normals) const { - assert(facet < m_orig_size_indices); - Vec3f n = m_face_normals[facet]; - if (! m_cursor.uniform_scaling) - n = m_cursor.trafo_normal * n; - return n.dot(m_cursor.dir) < 0.; + 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; } - // How many vertices of a triangle are inside the circle? -int TriangleSelector::vertices_inside(int facet_idx) const +int TriangleSelector::Cursor::vertices_inside(const Triangle &tr, const std::vector &vertices) const { int inside = 0; - for (size_t i=0; i<3; ++i) { - if (m_cursor.is_mesh_point_inside(m_vertices[m_triangles[facet_idx].verts_idxs[i]].v)) + for (size_t i = 0; i < 3; ++i) + if (this->is_mesh_point_inside(vertices[tr.verts_idxs[i]].v)) ++inside; - } + return inside; } - // Is edge inside cursor? -bool TriangleSelector::is_edge_inside_cursor(int facet_idx) const +bool TriangleSelector::SinglePointCursor::is_edge_inside_cursor(const Triangle &tr, const std::vector &vertices) const { std::array pts; - for (int i=0; i<3; ++i) { - pts[i] = m_vertices[m_triangles[facet_idx].verts_idxs[i]].v; - if (! m_cursor.uniform_scaling) - pts[i] = m_cursor.trafo * pts[i]; + 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& p = m_cursor.center; + 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]; - Vec3f s = (b-a).normalized(); - float t = (p-a).dot(s); - Vec3f vector = a+t*s - p; + const Vec3f &a = pts[side]; + const Vec3f &b = pts[side < 2 ? side + 1 : 0]; + Vec3f s = (b - a).normalized(); + float t = (p - a).dot(s); + Vec3f vector = a + t * s - p; // vector is 3D vector from center to the intersection. What we want to // measure is length of its projection onto plane perpendicular to dir. - float dist_sqr = vector.squaredNorm() - std::pow(vector.dot(m_cursor.dir), 2.f); - if (dist_sqr < m_cursor.radius_sqr && t>=0.f && t<=(b-a).norm()) + float dist_sqr = vector.squaredNorm() - std::pow(vector.dot(this->dir), 2.f); + if (dist_sqr < this->radius_sqr && t >= 0.f && t <= (b - a).norm()) return true; } return false; } - - // Recursively remove all subtriangles. void TriangleSelector::undivide_triangle(int facet_idx) { @@ -1002,7 +989,6 @@ void TriangleSelector::undivide_triangle(int facet_idx) } } - void TriangleSelector::remove_useless_children(int facet_idx) { // Check that all children are leafs of the same type. If not, try to @@ -1041,8 +1027,6 @@ void TriangleSelector::remove_useless_children(int facet_idx) tr.set_state(first_child_type); } - - void TriangleSelector::garbage_collect() { // First make a map from old to new triangle indices. @@ -1103,7 +1087,6 @@ TriangleSelector::TriangleSelector(const TriangleMesh& mesh) reset(); } - void TriangleSelector::reset() { m_vertices.clear(); @@ -1124,17 +1107,11 @@ void TriangleSelector::reset() } - - - - void TriangleSelector::set_edge_limit(float edge_limit) { m_edge_limit_sqr = std::pow(edge_limit, 2.f); } - - int TriangleSelector::push_triangle(int a, int b, int c, int source_triangle, const EnforcerBlockerType state) { for (int i : {a, b, c}) { @@ -1693,42 +1670,55 @@ void TriangleSelector::seed_fill_apply_on_triangles(EnforcerBlockerType new_stat } } -TriangleSelector::Cursor::Cursor( - const Vec3f& center_, const Vec3f& source_, float radius_world, - CursorType type_, const Transform3d& trafo_, const ClippingPlane &clipping_plane_) - : center{center_}, - source{source_}, - type{type_}, - trafo{trafo_.cast()}, - clipping_plane(clipping_plane_) +TriangleSelector::Cursor::Cursor(const Vec3f &source_, float radius_world, const Transform3d &trafo_, const ClippingPlane &clipping_plane_) + : source{source_}, trafo{trafo_.cast()}, clipping_plane{clipping_plane_} { 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_sqr = float(std::pow(radius_world / sf(0), 2)); uniform_scaling = true; - } - else { + } else { // In case that the transformation is non-uniform, all checks whether // something is inside the cursor should be done in world coords. - // First transform center, source and dir in world coords and remember - // that we did this. - center = trafo * center; - source = trafo * source; + // First transform source in world coords and remember that we did this. + source = trafo * source; uniform_scaling = false; - radius_sqr = radius_world * radius_world; - trafo_normal = trafo.linear().inverse().transpose(); + radius_sqr = radius_world * radius_world; + trafo_normal = trafo.linear().inverse().transpose(); } +} + +TriangleSelector::SinglePointCursor::SinglePointCursor(const Vec3f& center_, const Vec3f& source_, float radius_world, const Transform3d& trafo_, const ClippingPlane &clipping_plane_) + : center{center_}, Cursor(source_, radius_world, trafo_, clipping_plane_) +{ + // In case that the transformation is non-uniform, all checks whether + // something is inside the cursor should be done in world coords. + // Because of the center is transformed. + if (!uniform_scaling) + center = trafo * center; // Calculate dir, in whatever coords is appropriate. dir = (center - source).normalized(); } -// Is a point (in mesh coords) inside a cursor? -bool TriangleSelector::Cursor::is_mesh_point_inside(const Vec3f &point) const +// 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 (is_point_inside && clipping_plane.is_active()) + return !clipping_plane.is_mesh_point_clipped(point); + + return is_point_inside; +} + +// Is a point (in mesh coords) inside a Circle cursor? +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 = (type == CIRCLE ? (diff - diff.dot(dir) * dir).squaredNorm() : diff.squaredNorm()) < radius_sqr; + 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); @@ -1737,10 +1727,7 @@ bool TriangleSelector::Cursor::is_mesh_point_inside(const Vec3f &point) const } // p1, p2, p3 are in mesh coords! -bool TriangleSelector::Cursor::is_pointer_in_triangle(const Vec3f& p1_, - const Vec3f& p2_, - const Vec3f& p3_) const -{ +static bool is_circle_pointer_inside_triangle(const Vec3f &p1_, const Vec3f &p2_, const Vec3f &p3_, const Vec3f ¢er, const Vec3f &dir, const bool uniform_scaling, const Transform3f &trafo) { const Vec3f& q1 = center + dir; const Vec3f& q2 = center - dir; @@ -1761,4 +1748,10 @@ bool TriangleSelector::Cursor::is_pointer_in_triangle(const Vec3f& p1_, return signed_volume_sign(q1,q2,p2,p3) == pos && signed_volume_sign(q1,q2,p3,p1) == pos; } +// p1, p2, p3 are in mesh coords! +bool TriangleSelector::SinglePointCursor::is_pointer_in_triangle(const Vec3f &p1_, const Vec3f &p2_, const Vec3f &p3_) const +{ + return is_circle_pointer_inside_triangle(p1_, p2_, p3_, center, dir, uniform_scaling, trafo); +} + } // namespace Slic3r diff --git a/src/libslic3r/TriangleSelector.hpp b/src/libslic3r/TriangleSelector.hpp index b9f136c2e2..05930731cd 100644 --- a/src/libslic3r/TriangleSelector.hpp +++ b/src/libslic3r/TriangleSelector.hpp @@ -15,7 +15,12 @@ enum class EnforcerBlockerType : int8_t; // Following class holds information about selected triangles. It also has power // to recursively subdivide the triangles and make the selection finer. -class TriangleSelector { +class TriangleSelector +{ +protected: + class Triangle; + struct Vertex; + public: enum CursorType { CIRCLE, @@ -35,6 +40,83 @@ public: bool is_mesh_point_clipped(const Vec3f &point) const { return normal.dot(point) - offset > 0.f; } }; + class Cursor + { + public: + Cursor() = delete; + virtual ~Cursor() = default; + + bool is_pointer_in_triangle(const Triangle &tr, const std::vector &vertices) const; + + virtual bool is_mesh_point_inside(const Vec3f &point) const = 0; + 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; } + + protected: + explicit Cursor(const Vec3f &source_, float radius_world, const Transform3d &trafo_, const ClippingPlane &clipping_plane_); + + Transform3f trafo; + Vec3f source; + + bool uniform_scaling; + Transform3f trafo_normal; + float radius_sqr; + Vec3f dir = Vec3f(0.f, 0.f, 0.f); + + ClippingPlane clipping_plane; // Clipping plane to limit painting to not clipped facets only + + friend TriangleSelector; + }; + + class SinglePointCursor : public Cursor + { + 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) + { + assert(cursor_type == TriangleSelector::CursorType::CIRCLE || cursor_type == TriangleSelector::CursorType::SPHERE); + if (cursor_type == TriangleSelector::CursorType::SPHERE) + return std::make_unique(center, camera_pos, cursor_radius, trafo_matrix, clipping_plane); + else + return std::make_unique(center, camera_pos, cursor_radius, trafo_matrix, clipping_plane); + } + + protected: + explicit SinglePointCursor(const Vec3f ¢er_, const Vec3f &source_, float radius_world, const Transform3d &trafo_, const ClippingPlane &clipping_plane_); + + Vec3f center; + }; + + class Sphere : public SinglePointCursor + { + public: + Sphere() = delete; + explicit Sphere(const Vec3f ¢er_, const Vec3f &source_, float radius_world, const Transform3d &trafo_, const ClippingPlane &clipping_plane_) + : SinglePointCursor(center_, source_, radius_world, trafo_, clipping_plane_){}; + ~Sphere() override = default; + + bool is_mesh_point_inside(const Vec3f &point) const override; + }; + + class Circle : public SinglePointCursor + { + public: + Circle() = delete; + explicit Circle(const Vec3f ¢er_, const Vec3f &source_, float radius_world, const Transform3d &trafo_, const ClippingPlane &clipping_plane_) + : SinglePointCursor(center_, source_, radius_world, trafo_, clipping_plane_){}; + ~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; + }; + std::pair, std::vector> precompute_all_neighbors() const; void precompute_all_neighbors_recursive(int facet_idx, const Vec3i &neighbors, const Vec3i &neighbors_propagated, std::vector &neighbors_out, std::vector &neighbors_normal_out) const; @@ -51,17 +133,12 @@ public: [[nodiscard]] int select_unsplit_triangle(const Vec3f &hit, int facet_idx, const Vec3i &neighbors) const; // Select all triangles fully inside the circle, subdivide where needed. - void select_patch(const Vec3f &hit, // point where to start - int facet_start, // facet of the original mesh (unsplit) that the hit point belongs to - const Vec3f &source, // camera position (mesh coords) - float radius, // radius of the cursor - CursorType type, // current type of cursor - EnforcerBlockerType new_state, // enforcer or blocker? - const Transform3d &trafo, // matrix to get from mesh to world - const Transform3d &trafo_no_translate, // matrix to get from mesh to world without translation - bool triangle_splitting, // If triangles will be split base on the cursor or not - const ClippingPlane &clp, // Clipping plane to limit painting to not clipped facets only - float highlight_by_angle_deg = 0.f); // The maximal angle of overhang. If it is set to a non-zero value, it is possible to paint only the triangles of overhang defined by this angle in degrees. + void select_patch(int facet_start, // facet of the original mesh (unsplit) that the hit point belongs to + std::unique_ptr &&cursor, // Cursor containing information about the point where to start, camera position (mesh coords), matrix to get from mesh to world, and its shape and type. + EnforcerBlockerType new_state, // enforcer or blocker? + const Transform3d &trafo_no_translate, // matrix to get from mesh to world without translation + bool triangle_splitting, // If triangles will be split base on the cursor or not + float highlight_by_angle_deg = 0.f); // The maximal angle of overhang. If it is set to a non-zero value, it is possible to paint only the triangles of overhang defined by this angle in degrees. void seed_fill_select_triangles(const Vec3f &hit, // point where to start int facet_start, // facet of the original mesh (unsplit) that the hit point belongs to @@ -195,39 +272,16 @@ protected: int m_orig_size_vertices = 0; int m_orig_size_indices = 0; - // Cache for cursor position, radius and direction. - struct Cursor { - Cursor() = default; - Cursor(const Vec3f& center_, const Vec3f& source_, float radius_world, - CursorType type_, const Transform3d& trafo_, const ClippingPlane &clipping_plane_); - bool is_mesh_point_inside(const Vec3f &pt) const; - bool is_pointer_in_triangle(const Vec3f& p1, const Vec3f& p2, const Vec3f& p3) const; - - Vec3f center; - Vec3f source; - Vec3f dir; - float radius_sqr; - CursorType type; - Transform3f trafo; - Transform3f trafo_normal; - bool uniform_scaling; - ClippingPlane clipping_plane; - }; - - Cursor m_cursor; + std::unique_ptr m_cursor; float m_old_cursor_radius_sqr; // Private functions: private: bool select_triangle(int facet_idx, EnforcerBlockerType type, bool triangle_splitting); bool select_triangle_recursive(int facet_idx, const Vec3i &neighbors, EnforcerBlockerType type, bool triangle_splitting); - int vertices_inside(int facet_idx) const; - bool faces_camera(int facet) const; void undivide_triangle(int facet_idx); void split_triangle(int facet_idx, const Vec3i &neighbors); void remove_useless_children(int facet_idx); // No hidden meaning. Triangles are meant. - bool is_pointer_in_triangle(int facet_idx) const; - bool is_edge_inside_cursor(int facet_idx) const; bool is_facet_clipped(int facet_idx, const ClippingPlane &clp) const; int push_triangle(int a, int b, int c, int source_triangle, EnforcerBlockerType state = EnforcerBlockerType{0}); void perform_split(int facet_idx, const Vec3i &neighbors, EnforcerBlockerType old_state); diff --git a/src/slic3r/GUI/Gizmos/GLGizmoPainterBase.cpp b/src/slic3r/GUI/Gizmos/GLGizmoPainterBase.cpp index 821ce367af..1b9d996e65 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoPainterBase.cpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoPainterBase.cpp @@ -365,10 +365,11 @@ bool GLGizmoPainterBase::gizmo_event(SLAGizmoEventType action, const Vec2d& mous m_triangle_selectors[m_rr.mesh_id]->bucket_fill_select_triangles(m_rr.hit, int(m_rr.facet), clp, true, true); m_seed_fill_last_mesh_id = -1; - } else if (m_tool_type == ToolType::BRUSH) - m_triangle_selectors[m_rr.mesh_id]->select_patch(m_rr.hit, int(m_rr.facet), camera_pos, m_cursor_radius, m_cursor_type, - new_state, trafo_matrix, trafo_matrix_not_translate, m_triangle_splitting_enabled, clp, - m_paint_on_overhangs_only ? m_highlight_by_angle_threshold_deg : 0.f); + } 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); + } + m_triangle_selectors[m_rr.mesh_id]->request_update_render_data(); m_last_mouse_click = mouse_position; } From 7bb38840e18ecc435ad3acca07828dee526499f8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Luk=C3=A1=C5=A1=20Hejl?= Date: Mon, 15 Nov 2021 12:53:51 +0100 Subject: [PATCH 03/21] 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. --- src/libslic3r/TriangleSelector.cpp | 324 ++++++++++++++++++- src/libslic3r/TriangleSelector.hpp | 69 +++- src/slic3r/GUI/Gizmos/GLGizmoPainterBase.cpp | 214 +++++++++--- src/slic3r/GUI/Gizmos/GLGizmoPainterBase.hpp | 9 + src/slic3r/GUI/MeshUtils.cpp | 8 +- src/slic3r/GUI/MeshUtils.hpp | 3 + 6 files changed, 558 insertions(+), 69 deletions(-) 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: From b062a0f2817b6eb6ff0bef558298f2c659200811 Mon Sep 17 00:00:00 2001 From: enricoturri1966 Date: Thu, 2 Dec 2021 13:42:26 +0100 Subject: [PATCH 04/21] Fixed build when tech ENABLE_SMOOTH_NORMALS is enabled --- src/slic3r/GUI/3DScene.cpp | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/src/slic3r/GUI/3DScene.cpp b/src/slic3r/GUI/3DScene.cpp index 6aa12431c4..94b1f31569 100644 --- a/src/slic3r/GUI/3DScene.cpp +++ b/src/slic3r/GUI/3DScene.cpp @@ -72,15 +72,10 @@ namespace Slic3r { #if ENABLE_SMOOTH_NORMALS static void smooth_normals_corner(TriangleMesh& mesh, std::vector& normals) { - mesh.repair(); - using MapMatrixXfUnaligned = Eigen::Map>; using MapMatrixXiUnaligned = Eigen::Map>; - std::vector face_normals(mesh.stl.stats.number_of_facets); - for (uint32_t i = 0; i < mesh.stl.stats.number_of_facets; ++i) { - face_normals[i] = mesh.stl.facet_start[i].normal; - } + std::vector face_normals = its_face_normals(mesh.its); Eigen::MatrixXd vertices = MapMatrixXfUnaligned(mesh.its.vertices.front().data(), Eigen::Index(mesh.its.vertices.size()), 3).cast(); @@ -102,8 +97,6 @@ static void smooth_normals_corner(TriangleMesh& mesh, std::vector& n static void smooth_normals_vertex(TriangleMesh& mesh, std::vector& normals) { - mesh.repair(); - using MapMatrixXfUnaligned = Eigen::Map>; using MapMatrixXiUnaligned = Eigen::Map>; From d60bbc382df2c86cf1d4a8ad5c7a87b10943bce5 Mon Sep 17 00:00:00 2001 From: David Kocik Date: Mon, 22 Nov 2021 21:35:42 +0100 Subject: [PATCH 05/21] few asserts in notifications manager --- src/slic3r/GUI/NotificationManager.cpp | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/slic3r/GUI/NotificationManager.cpp b/src/slic3r/GUI/NotificationManager.cpp index ef299bf09d..1af7b81dd7 100644 --- a/src/slic3r/GUI/NotificationManager.cpp +++ b/src/slic3r/GUI/NotificationManager.cpp @@ -393,8 +393,7 @@ void NotificationManager::PopNotification::render_text(ImGuiWrapper& imgui, cons std::string line; for (size_t i = 0; i < (m_multiline ? m_endlines.size() : std::min(m_endlines.size(), (size_t)2)); i++) { - if (m_endlines[i] > m_text1.size()) - break; + assert(m_endlines.size() > i && m_text1.size() >= m_endlines[i]); line.clear(); ImGui::SetCursorPosX(x_offset); ImGui::SetCursorPosY(starting_y + i * shift_y); @@ -681,6 +680,7 @@ void NotificationManager::ExportFinishedNotification::render_text(ImGuiWrapper& float starting_y = m_line_height / 2;//10; float shift_y = m_line_height;// -m_line_height / 20; for (size_t i = 0; i < m_lines_count; i++) { + assert(m_text1.size() >= m_endlines[i]); if (m_text1.size() >= m_endlines[i]) { std::string line = m_text1.substr(last_end, m_endlines[i] - last_end); last_end = m_endlines[i]; @@ -801,6 +801,7 @@ void NotificationManager::ProgressBarNotification::render_text(ImGuiWrapper& img // hypertext is not rendered at all. If it is needed, it needs to be added here. // m_endlines should have endline for each line and then for hypertext thus m_endlines[1] should always be in m_text1 if (m_multiline) { + assert(m_text1.size() >= m_endlines[0] || m_text1.size() >= m_endlines[1]); if(m_endlines[0] > m_text1.size() || m_endlines[1] > m_text1.size()) return; // two lines text (what doesnt fit, wont show), one line bar @@ -815,6 +816,7 @@ void NotificationManager::ProgressBarNotification::render_text(ImGuiWrapper& img render_cancel_button(imgui, win_size_x, win_size_y, win_pos_x, win_pos_y); render_bar(imgui, win_size_x, win_size_y, win_pos_x, win_pos_y); } else { + assert(m_text1.size() >= m_endlines[0]); if (m_endlines[0] > m_text1.size()) return; //one line text, one line bar From 15104cb78723b8a92f28756371d438aeee6341d6 Mon Sep 17 00:00:00 2001 From: David Kocik Date: Thu, 2 Dec 2021 13:57:49 +0100 Subject: [PATCH 06/21] Load basic notifications later so the translations are correct. --- src/slic3r/GUI/NotificationManager.cpp | 30 ------------------------ src/slic3r/GUI/NotificationManager.hpp | 32 +++++++++++++++++++++++++- 2 files changed, 31 insertions(+), 31 deletions(-) diff --git a/src/slic3r/GUI/NotificationManager.cpp b/src/slic3r/GUI/NotificationManager.cpp index 1af7b81dd7..66c22cb9b7 100644 --- a/src/slic3r/GUI/NotificationManager.cpp +++ b/src/slic3r/GUI/NotificationManager.cpp @@ -33,36 +33,6 @@ wxDEFINE_EVENT(EVT_EJECT_DRIVE_NOTIFICAION_CLICKED, EjectDriveNotificationClicke wxDEFINE_EVENT(EVT_EXPORT_GCODE_NOTIFICAION_CLICKED, ExportGcodeNotificationClickedEvent); wxDEFINE_EVENT(EVT_PRESET_UPDATE_AVAILABLE_CLICKED, PresetUpdateAvailableClickedEvent); -const NotificationManager::NotificationData NotificationManager::basic_notifications[] = { - {NotificationType::Mouse3dDisconnected, NotificationLevel::RegularNotificationLevel, 10, _u8L("3D Mouse disconnected.") }, - {NotificationType::PresetUpdateAvailable, NotificationLevel::ImportantNotificationLevel, 20, _u8L("Configuration update is available."), _u8L("See more."), - [](wxEvtHandler* evnthndlr) { - if (evnthndlr != nullptr) - wxPostEvent(evnthndlr, PresetUpdateAvailableClickedEvent(EVT_PRESET_UPDATE_AVAILABLE_CLICKED)); - return true; - } - }, - {NotificationType::EmptyColorChangeCode, NotificationLevel::PrintInfoNotificationLevel, 10, - _u8L("You have just added a G-code for color change, but its value is empty.\n" - "To export the G-code correctly, check the \"Color Change G-code\" in \"Printer Settings > Custom G-code\"") }, - {NotificationType::EmptyAutoColorChange, NotificationLevel::PrintInfoNotificationLevel, 10, - _u8L("No color change event was added to the print. The print does not look like a sign.") }, - {NotificationType::DesktopIntegrationSuccess, NotificationLevel::RegularNotificationLevel, 10, - _u8L("Desktop integration was successful.") }, - {NotificationType::DesktopIntegrationFail, NotificationLevel::WarningNotificationLevel, 10, - _u8L("Desktop integration failed.") }, - {NotificationType::UndoDesktopIntegrationSuccess, NotificationLevel::RegularNotificationLevel, 10, - _u8L("Undo desktop integration was successful.") }, - {NotificationType::UndoDesktopIntegrationFail, NotificationLevel::WarningNotificationLevel, 10, - _u8L("Undo desktop integration failed.") }, - {NotificationType::ExportOngoing, NotificationLevel::RegularNotificationLevel, 0, _u8L("Exporting.") }, - //{NotificationType::NewAppAvailable, NotificationLevel::ImportantNotificationLevel, 20, _u8L("New version is available."), _u8L("See Releases page."), [](wxEvtHandler* evnthndlr) { - // wxGetApp().open_browser_with_warning_dialog("https://github.com/prusa3d/PrusaSlicer/releases"); return true; }}, - //{NotificationType::NewAppAvailable, NotificationLevel::ImportantNotificationLevel, 20, _u8L("New vesion of PrusaSlicer is available.", _u8L("Download page.") }, - //{NotificationType::LoadingFailed, NotificationLevel::RegularNotificationLevel, 20, _u8L("Loading of model has Failed") }, - //{NotificationType::DeviceEjected, NotificationLevel::RegularNotificationLevel, 10, _u8L("Removable device has been safely ejected")} // if we want changeble text (like here name of device), we need to do it as CustomNotification -}; - namespace { /* // not used? ImFont* add_default_font(float pixel_size) diff --git a/src/slic3r/GUI/NotificationManager.hpp b/src/slic3r/GUI/NotificationManager.hpp index 0030937d77..9265cb55e4 100644 --- a/src/slic3r/GUI/NotificationManager.hpp +++ b/src/slic3r/GUI/NotificationManager.hpp @@ -747,7 +747,37 @@ private: NotificationType::SimplifySuggestion }; //prepared (basic) notifications - static const NotificationData basic_notifications[]; + // non-static so its not loaded too early. If static, the translations wont load correctly. + const std::vector basic_notifications = { + {NotificationType::Mouse3dDisconnected, NotificationLevel::RegularNotificationLevel, 10, _u8L("3D Mouse disconnected.") }, + {NotificationType::PresetUpdateAvailable, NotificationLevel::ImportantNotificationLevel, 20, _u8L("Configuration update is available."), _u8L("See more."), + [](wxEvtHandler* evnthndlr) { + if (evnthndlr != nullptr) + wxPostEvent(evnthndlr, PresetUpdateAvailableClickedEvent(EVT_PRESET_UPDATE_AVAILABLE_CLICKED)); + return true; + } + }, + {NotificationType::EmptyColorChangeCode, NotificationLevel::PrintInfoNotificationLevel, 10, + _u8L("You have just added a G-code for color change, but its value is empty.\n" + "To export the G-code correctly, check the \"Color Change G-code\" in \"Printer Settings > Custom G-code\"") }, + {NotificationType::EmptyAutoColorChange, NotificationLevel::PrintInfoNotificationLevel, 10, + _u8L("No color change event was added to the print. The print does not look like a sign.") }, + {NotificationType::DesktopIntegrationSuccess, NotificationLevel::RegularNotificationLevel, 10, + _u8L("Desktop integration was successful.") }, + {NotificationType::DesktopIntegrationFail, NotificationLevel::WarningNotificationLevel, 10, + _u8L("Desktop integration failed.") }, + {NotificationType::UndoDesktopIntegrationSuccess, NotificationLevel::RegularNotificationLevel, 10, + _u8L("Undo desktop integration was successful.") }, + {NotificationType::UndoDesktopIntegrationFail, NotificationLevel::WarningNotificationLevel, 10, + _u8L("Undo desktop integration failed.") }, + {NotificationType::ExportOngoing, NotificationLevel::RegularNotificationLevel, 0, _u8L("Exporting.") }, + //{NotificationType::NewAppAvailable, NotificationLevel::ImportantNotificationLevel, 20, _u8L("New version is available."), _u8L("See Releases page."), [](wxEvtHandler* evnthndlr) { + // wxGetApp().open_browser_with_warning_dialog("https://github.com/prusa3d/PrusaSlicer/releases"); return true; }}, + //{NotificationType::NewAppAvailable, NotificationLevel::ImportantNotificationLevel, 20, _u8L("New vesion of PrusaSlicer is available.", _u8L("Download page.") }, + //{NotificationType::LoadingFailed, NotificationLevel::RegularNotificationLevel, 20, _u8L("Loading of model has Failed") }, + //{NotificationType::DeviceEjected, NotificationLevel::RegularNotificationLevel, 10, _u8L("Removable device has been safely ejected")} // if we want changeble text (like here name of device), we need to do it as CustomNotification + }; + }; }//namespace GUI From efbf64fdeaded93e011271c04fddf507a6a0372a Mon Sep 17 00:00:00 2001 From: YuSanka Date: Thu, 2 Dec 2021 15:14:39 +0100 Subject: [PATCH 07/21] Added description line for the "Post-processing scripts". ogStaticText id extended for SetPathEnd() function. It allows to use description line like a hyperlink --- src/slic3r/GUI/GUI_App.cpp | 1 + src/slic3r/GUI/GUI_App.hpp | 2 + src/slic3r/GUI/OG_CustomCtrl.cpp | 47 ++---------------- src/slic3r/GUI/OptionsGroup.cpp | 84 +++++++++++++++++++++++++++++++- src/slic3r/GUI/OptionsGroup.hpp | 21 +++++--- src/slic3r/GUI/Tab.cpp | 65 ++++++++++++++---------- src/slic3r/GUI/Tab.hpp | 3 +- 7 files changed, 146 insertions(+), 77 deletions(-) diff --git a/src/slic3r/GUI/GUI_App.cpp b/src/slic3r/GUI/GUI_App.cpp index 60f8e21a9c..7aff9a6cad 100644 --- a/src/slic3r/GUI/GUI_App.cpp +++ b/src/slic3r/GUI/GUI_App.cpp @@ -1487,6 +1487,7 @@ void GUI_App::update_fonts(const MainFrame *main_frame) m_normal_font = main_frame->normal_font(); m_small_font = m_normal_font; m_bold_font = main_frame->normal_font().Bold(); + m_link_font = m_bold_font.Underlined(); m_em_unit = main_frame->em_unit(); m_code_font.SetPointSize(m_normal_font.GetPointSize()); } diff --git a/src/slic3r/GUI/GUI_App.hpp b/src/slic3r/GUI/GUI_App.hpp index 9e8e913f69..82feb32827 100644 --- a/src/slic3r/GUI/GUI_App.hpp +++ b/src/slic3r/GUI/GUI_App.hpp @@ -132,6 +132,7 @@ private: wxFont m_bold_font; wxFont m_normal_font; wxFont m_code_font; + wxFont m_link_font; int m_em_unit; // width of a "m"-symbol in pixels for current system font // Note: for 100% Scale m_em_unit = 10 -> it's a good enough coefficient for a size setting of controls @@ -217,6 +218,7 @@ public: const wxFont& bold_font() { return m_bold_font; } const wxFont& normal_font() { return m_normal_font; } const wxFont& code_font() { return m_code_font; } + const wxFont& link_font() { return m_link_font; } int em_unit() const { return m_em_unit; } bool tabs_as_menu() const; wxSize get_min_size() const; diff --git a/src/slic3r/GUI/OG_CustomCtrl.cpp b/src/slic3r/GUI/OG_CustomCtrl.cpp index 5304e83e13..e9153c70f4 100644 --- a/src/slic3r/GUI/OG_CustomCtrl.cpp +++ b/src/slic3r/GUI/OG_CustomCtrl.cpp @@ -28,17 +28,6 @@ static wxSize get_bitmap_size(const wxBitmap& bmp) #endif } -static wxString get_url(const wxString& path_end, bool get_default = false) -{ - if (path_end.IsEmpty()) - return wxEmptyString; - - wxString language = wxGetApp().app_config->get("translation_language"); - wxString lang_marker = language.IsEmpty() ? "en" : language.BeforeFirst('_'); - - return wxString("https://help.prusa3d.com/") + lang_marker + "/article/" + path_end; -} - OG_CustomCtrl::OG_CustomCtrl( wxWindow* parent, OptionsGroup* og, const wxPoint& pos /* = wxDefaultPosition*/, @@ -264,7 +253,7 @@ void OG_CustomCtrl::OnMotion(wxMouseEvent& event) line.is_focused = is_point_in_rect(pos, line.rect_label); if (line.is_focused) { if (!suppress_hyperlinks && !line.og_line.label_path.empty()) - tooltip = get_url(line.og_line.label_path) +"\n\n"; + tooltip = OptionsGroup::get_url(line.og_line.label_path) +"\n\n"; tooltip += line.og_line.label_tooltip; break; } @@ -577,7 +566,7 @@ void OG_CustomCtrl::CtrlLine::render(wxDC& dc, wxCoord v_pos) bool is_url_string = false; if (ctrl->opt_group->label_width != 0 && !label.IsEmpty()) { const wxColour* text_clr = (option_set.size() == 1 && field ? field->label_color() : og_line.full_Label_color); - is_url_string = !suppress_hyperlinks && !og_line.label_path.IsEmpty(); + is_url_string = !suppress_hyperlinks && !og_line.label_path.empty(); h_pos = draw_text(dc, wxPoint(h_pos, v_pos), label + ":", text_clr, ctrl->opt_group->label_width * ctrl->m_em_unit, is_url_string); } @@ -619,7 +608,7 @@ void OG_CustomCtrl::CtrlLine::render(wxDC& dc, wxCoord v_pos) if (is_url_string) is_url_string = false; else if(opt == option_set.front()) - is_url_string = !suppress_hyperlinks && !og_line.label_path.IsEmpty(); + is_url_string = !suppress_hyperlinks && !og_line.label_path.empty(); h_pos = draw_text(dc, wxPoint(h_pos, v_pos), label, field ? field->label_color() : nullptr, ctrl->opt_group->sublabel_width * ctrl->m_em_unit, is_url_string); } @@ -766,36 +755,10 @@ wxCoord OG_CustomCtrl::CtrlLine::draw_act_bmps(wxDC& dc, wxPoint pos, const wxBi bool OG_CustomCtrl::CtrlLine::launch_browser() const { - if (!is_focused || og_line.label_path.IsEmpty()) + if (!is_focused || og_line.label_path.empty()) return false; - bool launch = true; - - if (get_app_config()->get("suppress_hyperlinks").empty()) { - RichMessageDialog dialog(nullptr, _L("Open hyperlink in default browser?"), _L("PrusaSlicer: Open hyperlink"), wxYES_NO); - dialog.ShowCheckBox(_L("Remember my choice")); - int answer = dialog.ShowModal(); - - if (dialog.IsCheckBoxChecked()) { - wxString preferences_item = _L("Suppress to open hyperlink in browser"); - wxString msg = - _L("PrusaSlicer will remember your choice.") + "\n\n" + - _L("You will not be asked about it again on label hovering.") + "\n\n" + - format_wxstr(_L("Visit \"Preferences\" and check \"%1%\"\nto changes your choice."), preferences_item); - - MessageDialog msg_dlg(nullptr, msg, _L("PrusaSlicer: Don't ask me again"), wxOK | wxCANCEL | wxICON_INFORMATION); - if (msg_dlg.ShowModal() == wxID_CANCEL) - return false; - - get_app_config()->set("suppress_hyperlinks", dialog.IsCheckBoxChecked() ? (answer == wxID_NO ? "1" : "0") : ""); - } - - launch = answer == wxID_YES; - } - if (launch) - launch = get_app_config()->get("suppress_hyperlinks") != "1"; - - return launch && wxLaunchDefaultBrowser(get_url(og_line.label_path)); + return OptionsGroup::launch_browser(og_line.label_path); } } // GUI diff --git a/src/slic3r/GUI/OptionsGroup.cpp b/src/slic3r/GUI/OptionsGroup.cpp index c4c123a484..4fe0e186eb 100644 --- a/src/slic3r/GUI/OptionsGroup.cpp +++ b/src/slic3r/GUI/OptionsGroup.cpp @@ -3,6 +3,7 @@ #include "Plater.hpp" #include "GUI_App.hpp" #include "OG_CustomCtrl.hpp" +#include "MsgDialog.hpp" #include #include @@ -10,6 +11,7 @@ #include #include "libslic3r/Exception.hpp" #include "libslic3r/Utils.hpp" +#include "libslic3r/AppConfig.hpp" #include "I18N.hpp" namespace Slic3r { namespace GUI { @@ -504,7 +506,7 @@ void OptionsGroup::clear(bool destroy_custom_ctrl) m_fields.clear(); } -Line OptionsGroup::create_single_option_line(const Option& option, const wxString& path/* = wxEmptyString*/) const { +Line OptionsGroup::create_single_option_line(const Option& option, const std::string& path/* = std::string()*/) const { // Line retval{ _(option.opt.label), _(option.opt.tooltip) }; wxString tooltip = _(option.opt.tooltip); edit_tooltip(tooltip); @@ -962,6 +964,54 @@ void ConfigOptionsGroup::change_opt_value(const t_config_option_key& opt_key, co m_modelconfig->touch(); } +wxString OptionsGroup::get_url(const std::string& path_end) +{ + if (path_end.empty()) + return wxEmptyString; + + wxString language = get_app_config()->get("translation_language"); + wxString lang_marker = language.IsEmpty() ? "en" : language.BeforeFirst('_'); + + return wxString("https://help.prusa3d.com/") + lang_marker + wxString("/article/" + path_end); +} + +bool OptionsGroup::launch_browser(const std::string& path_end) +{ + bool launch = true; + + if (get_app_config()->get("suppress_hyperlinks").empty()) { + RichMessageDialog dialog(nullptr, _L("Open hyperlink in default browser?"), _L("PrusaSlicer: Open hyperlink"), wxYES_NO); + dialog.ShowCheckBox(_L("Remember my choice")); + int answer = dialog.ShowModal(); + + if (dialog.IsCheckBoxChecked()) { + wxString preferences_item = _L("Suppress to open hyperlink in browser"); + wxString msg = + _L("PrusaSlicer will remember your choice.") + "\n\n" + + _L("You will not be asked about it again on label hovering.") + "\n\n" + + format_wxstr(_L("Visit \"Preferences\" and check \"%1%\"\nto changes your choice."), preferences_item); + + MessageDialog msg_dlg(nullptr, msg, _L("PrusaSlicer: Don't ask me again"), wxOK | wxCANCEL | wxICON_INFORMATION); + if (msg_dlg.ShowModal() == wxID_CANCEL) + return false; + + get_app_config()->set("suppress_hyperlinks", dialog.IsCheckBoxChecked() ? (answer == wxID_NO ? "1" : "0") : ""); + } + + launch = answer == wxID_YES; + } + if (launch) + launch = get_app_config()->get("suppress_hyperlinks") != "1"; + + return launch && wxLaunchDefaultBrowser(OptionsGroup::get_url(path_end)); +} + + + +//------------------------------------------------------------------------------------------- +// ogStaticText +//------------------------------------------------------------------------------------------- + ogStaticText::ogStaticText(wxWindow* parent, const wxString& text) : wxStaticText(parent, wxID_ANY, text, wxDefaultPosition, wxDefaultSize) { @@ -979,5 +1029,37 @@ void ogStaticText::SetText(const wxString& value, bool wrap/* = true*/) GetParent()->Layout(); } +void ogStaticText::SetPathEnd(const std::string& link) +{ + if (get_app_config()->get("suppress_hyperlinks") != "1") + SetToolTip(OptionsGroup::get_url(link)); + + Bind(wxEVT_LEFT_DOWN, [this](wxMouseEvent& event) { + if (HasCapture()) + return; + this->CaptureMouse(); + event.Skip(); + } ); + Bind(wxEVT_LEFT_UP, [link, this](wxMouseEvent& event) { + if (!HasCapture()) + return; + ReleaseMouse(); + OptionsGroup::launch_browser(link); + event.Skip(); + } ); + Bind(wxEVT_ENTER_WINDOW, [this](wxMouseEvent& event) { FocusText(true) ; event.Skip(); }); + Bind(wxEVT_LEAVE_WINDOW, [this](wxMouseEvent& event) { FocusText(false); event.Skip(); }); +} + +void ogStaticText::FocusText(bool focus) +{ + if (get_app_config()->get("suppress_hyperlinks") == "1") + return; + + SetFont(focus ? Slic3r::GUI::wxGetApp().link_font() : + Slic3r::GUI::wxGetApp().normal_font()); + Refresh(); +} + } // GUI } // Slic3r diff --git a/src/slic3r/GUI/OptionsGroup.hpp b/src/slic3r/GUI/OptionsGroup.hpp index 597527aefe..6647740dd0 100644 --- a/src/slic3r/GUI/OptionsGroup.hpp +++ b/src/slic3r/GUI/OptionsGroup.hpp @@ -53,7 +53,7 @@ class Line { public: wxString label; wxString label_tooltip; - wxString label_path; + std::string label_path; size_t full_width {0}; wxColour* full_Label_color {nullptr}; @@ -133,8 +133,8 @@ public: // delete all controls from the option group void clear(bool destroy_custom_ctrl = false); - Line create_single_option_line(const Option& option, const wxString& path = wxEmptyString) const; - void append_single_option_line(const Option& option, const wxString& path = wxEmptyString) { append_line(create_single_option_line(option, path)); } + Line create_single_option_line(const Option& option, const std::string& path = std::string()) const; + void append_single_option_line(const Option& option, const std::string& path = std::string()) { append_line(create_single_option_line(option, path)); } void append_separator(); // return a non-owning pointer reference @@ -219,6 +219,10 @@ protected: virtual void on_change_OG(const t_config_option_key& opt_id, const boost::any& value); virtual void back_to_initial_value(const std::string& opt_key) {} virtual void back_to_sys_value(const std::string& opt_key) {} + +public: + static wxString get_url(const std::string& path_end); + static bool launch_browser(const std::string& path_end); }; class ConfigOptionsGroup: public OptionsGroup { @@ -239,17 +243,17 @@ public: void set_config_category_and_type(const wxString &category, int type) { m_config_category = category; m_config_type = type; } void set_config(DynamicPrintConfig* config) { m_config = config; m_modelconfig = nullptr; } Option get_option(const std::string& opt_key, int opt_index = -1); - Line create_single_option_line(const std::string& title, const wxString& path = wxEmptyString, int idx = -1) /*const*/{ + Line create_single_option_line(const std::string& title, const std::string& path = std::string(), int idx = -1) /*const*/{ Option option = get_option(title, idx); return OptionsGroup::create_single_option_line(option, path); } - Line create_single_option_line(const Option& option, const wxString& path = wxEmptyString) const { + Line create_single_option_line(const Option& option, const std::string& path = std::string()) const { return OptionsGroup::create_single_option_line(option, path); } - void append_single_option_line(const Option& option, const wxString& path = wxEmptyString) { + void append_single_option_line(const Option& option, const std::string& path = std::string()) { OptionsGroup::append_single_option_line(option, path); } - void append_single_option_line(const std::string title, const wxString& path = wxEmptyString, int idx = -1) + void append_single_option_line(const std::string title, const std::string& path = std::string(), int idx = -1) { Option option = get_option(title, idx); append_single_option_line(option, path); @@ -298,6 +302,9 @@ public: ~ogStaticText() {} void SetText(const wxString& value, bool wrap = true); + // Set special path end. It will be used to generation of the hyperlink on info page + void SetPathEnd(const std::string& link); + void FocusText(bool focus); }; }} diff --git a/src/slic3r/GUI/Tab.cpp b/src/slic3r/GUI/Tab.cpp index 6ea044f406..518017db98 100644 --- a/src/slic3r/GUI/Tab.cpp +++ b/src/slic3r/GUI/Tab.cpp @@ -1432,7 +1432,7 @@ void TabPrint::build() load_initial_data(); auto page = add_options_page(L("Layers and perimeters"), "layers"); - wxString category_path = "layers-and-perimeters_1748#"; + std::string category_path = "layers-and-perimeters_1748#"; auto optgroup = page->new_optgroup(L("Layer height")); optgroup->append_single_option_line("layer_height", category_path + "layer-height"); optgroup->append_single_option_line("first_layer_height", category_path + "first-layer-height"); @@ -1673,6 +1673,12 @@ void TabPrint::build() optgroup->append_single_option_line(option); optgroup = page->new_optgroup(L("Post-processing scripts"), 0); + line = { "", "" }; + line.full_width = 1; + line.widget = [this](wxWindow* parent) { + return description_line_widget(parent, &m_post_process_explanation); + }; + optgroup->append_line(line); option = optgroup->get_option("post_process"); option.opt.full_width = true; option.opt.height = 5;//50; @@ -1688,7 +1694,7 @@ void TabPrint::build() page = add_options_page(L("Dependencies"), "wrench.png"); optgroup = page->new_optgroup(L("Profile dependencies")); - create_line_with_widget(optgroup.get(), "compatible_printers", wxEmptyString, [this](wxWindow* parent) { + create_line_with_widget(optgroup.get(), "compatible_printers", "", [this](wxWindow* parent) { return compatible_widget_create(parent, m_compatible_printers); }); @@ -1721,6 +1727,12 @@ void TabPrint::update_description_lines() m_top_bottom_shell_thickness_explanation->SetText( from_u8(PresetHints::top_bottom_shell_thickness_explanation(*m_preset_bundle))); } + + if (m_active_page && m_active_page->title() == "Output options" && m_post_process_explanation) { + m_post_process_explanation->SetText( + _u8L("Post processing scripts shall modify G-code file in place.")); + m_post_process_explanation->SetPathEnd("post-processing-scripts_283913"); + } } void TabPrint::toggle_options() @@ -1774,6 +1786,7 @@ void TabPrint::clear_pages() m_recommended_thin_wall_thickness_description_line = nullptr; m_top_bottom_shell_thickness_explanation = nullptr; + m_post_process_explanation = nullptr; } bool Tab::validate_custom_gcode(const wxString& title, const std::string& gcode) @@ -1938,7 +1951,7 @@ void TabFilament::build() optgroup->append_line(line); page = add_options_page(L("Cooling"), "cooling"); - wxString category_path = "cooling_127569#"; + std::string category_path = "cooling_127569#"; optgroup = page->new_optgroup(L("Enable")); optgroup->append_single_option_line("fan_always_on"); optgroup->append_single_option_line("cooling"); @@ -1999,7 +2012,7 @@ void TabFilament::build() optgroup->append_single_option_line("filament_cooling_initial_speed"); optgroup->append_single_option_line("filament_cooling_final_speed"); - create_line_with_widget(optgroup.get(), "filament_ramming_parameters", wxEmptyString, [this](wxWindow* parent) { + create_line_with_widget(optgroup.get(), "filament_ramming_parameters", "", [this](wxWindow* parent) { auto ramming_dialog_btn = new wxButton(parent, wxID_ANY, _(L("Ramming settings"))+dots, wxDefaultPosition, wxDefaultSize, wxBU_EXACTFIT); wxGetApp().UpdateDarkUI(ramming_dialog_btn); ramming_dialog_btn->SetFont(Slic3r::GUI::wxGetApp().normal_font()); @@ -2055,7 +2068,7 @@ void TabFilament::build() page = add_options_page(L("Dependencies"), "wrench.png"); optgroup = page->new_optgroup(L("Profile dependencies")); - create_line_with_widget(optgroup.get(), "compatible_printers", wxEmptyString, [this](wxWindow* parent) { + create_line_with_widget(optgroup.get(), "compatible_printers", "", [this](wxWindow* parent) { return compatible_widget_create(parent, m_compatible_printers); }); @@ -2063,7 +2076,7 @@ void TabFilament::build() option.opt.full_width = true; optgroup->append_single_option_line(option); - create_line_with_widget(optgroup.get(), "compatible_prints", wxEmptyString, [this](wxWindow* parent) { + create_line_with_widget(optgroup.get(), "compatible_prints", "", [this](wxWindow* parent) { return compatible_widget_create(parent, m_compatible_prints); }); @@ -2695,7 +2708,7 @@ void TabPrinter::build_unregular_pages(bool from_initial_build/* = false*/) m_pages.insert(m_pages.begin() + n_before_extruders + extruder_idx, page); auto optgroup = page->new_optgroup(L("Size")); - optgroup->append_single_option_line("nozzle_diameter", wxEmptyString, extruder_idx); + optgroup->append_single_option_line("nozzle_diameter", "", extruder_idx); optgroup->m_on_change = [this, extruder_idx](const t_config_option_key& opt_key, boost::any value) { @@ -2734,32 +2747,32 @@ void TabPrinter::build_unregular_pages(bool from_initial_build/* = false*/) }; optgroup = page->new_optgroup(L("Layer height limits")); - optgroup->append_single_option_line("min_layer_height", wxEmptyString, extruder_idx); - optgroup->append_single_option_line("max_layer_height", wxEmptyString, extruder_idx); + optgroup->append_single_option_line("min_layer_height", "", extruder_idx); + optgroup->append_single_option_line("max_layer_height", "", extruder_idx); optgroup = page->new_optgroup(L("Position (for multi-extruder printers)")); - optgroup->append_single_option_line("extruder_offset", wxEmptyString, extruder_idx); + optgroup->append_single_option_line("extruder_offset", "", extruder_idx); optgroup = page->new_optgroup(L("Retraction")); - optgroup->append_single_option_line("retract_length", wxEmptyString, extruder_idx); - optgroup->append_single_option_line("retract_lift", wxEmptyString, extruder_idx); + optgroup->append_single_option_line("retract_length", "", extruder_idx); + optgroup->append_single_option_line("retract_lift", "", extruder_idx); Line line = { L("Only lift Z"), "" }; line.append_option(optgroup->get_option("retract_lift_above", extruder_idx)); line.append_option(optgroup->get_option("retract_lift_below", extruder_idx)); optgroup->append_line(line); - optgroup->append_single_option_line("retract_speed", wxEmptyString, extruder_idx); - optgroup->append_single_option_line("deretract_speed", wxEmptyString, extruder_idx); - optgroup->append_single_option_line("retract_restart_extra", wxEmptyString, extruder_idx); - optgroup->append_single_option_line("retract_before_travel", wxEmptyString, extruder_idx); - optgroup->append_single_option_line("retract_layer_change", wxEmptyString, extruder_idx); - optgroup->append_single_option_line("wipe", wxEmptyString, extruder_idx); - optgroup->append_single_option_line("retract_before_wipe", wxEmptyString, extruder_idx); + optgroup->append_single_option_line("retract_speed", "", extruder_idx); + optgroup->append_single_option_line("deretract_speed", "", extruder_idx); + optgroup->append_single_option_line("retract_restart_extra", "", extruder_idx); + optgroup->append_single_option_line("retract_before_travel", "", extruder_idx); + optgroup->append_single_option_line("retract_layer_change", "", extruder_idx); + optgroup->append_single_option_line("wipe", "", extruder_idx); + optgroup->append_single_option_line("retract_before_wipe", "", extruder_idx); optgroup = page->new_optgroup(L("Retraction when tool is disabled (advanced settings for multi-extruder setups)")); - optgroup->append_single_option_line("retract_length_toolchange", wxEmptyString, extruder_idx); - optgroup->append_single_option_line("retract_restart_extra_toolchange", wxEmptyString, extruder_idx); + optgroup->append_single_option_line("retract_length_toolchange", "", extruder_idx); + optgroup->append_single_option_line("retract_restart_extra_toolchange", "", extruder_idx); optgroup = page->new_optgroup(L("Preview")); @@ -2787,7 +2800,7 @@ void TabPrinter::build_unregular_pages(bool from_initial_build/* = false*/) return sizer; }; - line = optgroup->create_single_option_line("extruder_colour", wxEmptyString, extruder_idx); + line = optgroup->create_single_option_line("extruder_colour", "", extruder_idx); line.append_widget(reset_to_filament_color); optgroup->append_line(line); } @@ -3736,7 +3749,7 @@ void Tab::update_ui_from_settings() } } -void Tab::create_line_with_widget(ConfigOptionsGroup* optgroup, const std::string& opt_key, const wxString& path, widget_t widget) +void Tab::create_line_with_widget(ConfigOptionsGroup* optgroup, const std::string& opt_key, const std::string& path, widget_t widget) { Line line = optgroup->create_single_option_line(opt_key); line.widget = widget; @@ -4224,7 +4237,7 @@ void TabSLAMaterial::build() page = add_options_page(L("Dependencies"), "wrench.png"); optgroup = page->new_optgroup(L("Profile dependencies")); - create_line_with_widget(optgroup.get(), "compatible_printers", wxEmptyString, [this](wxWindow* parent) { + create_line_with_widget(optgroup.get(), "compatible_printers", "", [this](wxWindow* parent) { return compatible_widget_create(parent, m_compatible_printers); }); @@ -4232,7 +4245,7 @@ void TabSLAMaterial::build() option.opt.full_width = true; optgroup->append_single_option_line(option); - create_line_with_widget(optgroup.get(), "compatible_prints", wxEmptyString, [this](wxWindow* parent) { + create_line_with_widget(optgroup.get(), "compatible_prints", "", [this](wxWindow* parent) { return compatible_widget_create(parent, m_compatible_prints); }); @@ -4371,7 +4384,7 @@ void TabSLAPrint::build() page = add_options_page(L("Dependencies"), "wrench"); optgroup = page->new_optgroup(L("Profile dependencies")); - create_line_with_widget(optgroup.get(), "compatible_printers", wxEmptyString, [this](wxWindow* parent) { + create_line_with_widget(optgroup.get(), "compatible_printers", "", [this](wxWindow* parent) { return compatible_widget_create(parent, m_compatible_printers); }); diff --git a/src/slic3r/GUI/Tab.hpp b/src/slic3r/GUI/Tab.hpp index 8a87c60c54..38e142591f 100644 --- a/src/slic3r/GUI/Tab.hpp +++ b/src/slic3r/GUI/Tab.hpp @@ -351,7 +351,7 @@ public: bool validate_custom_gcodes_was_shown{ false }; protected: - void create_line_with_widget(ConfigOptionsGroup* optgroup, const std::string& opt_key, const wxString& path, widget_t widget); + void create_line_with_widget(ConfigOptionsGroup* optgroup, const std::string& opt_key, const std::string& path, widget_t widget); wxSizer* compatible_widget_create(wxWindow* parent, PresetDependencies &deps); void compatible_widget_reload(PresetDependencies &deps); void load_key_value(const std::string& opt_key, const boost::any& value, bool saved_value = false); @@ -387,6 +387,7 @@ public: private: ogStaticText* m_recommended_thin_wall_thickness_description_line = nullptr; ogStaticText* m_top_bottom_shell_thickness_explanation = nullptr; + ogStaticText* m_post_process_explanation = nullptr; }; class TabFilament : public Tab From 71a9ded1c010f2c58b0d61e046e56a624f54a792 Mon Sep 17 00:00:00 2001 From: YuSanka Date: Thu, 2 Dec 2021 16:11:52 +0100 Subject: [PATCH 08/21] ObjectList: Fixed update of the icons for InfoItems, when color mode was changed Sidebar:ObjectInfo: Fixed update of the icon, when color mode was changed + Added new icons for "Sinking" and "ShapeGallery" --- resources/icons/fdm_supports_.svg | 19 ++++++++++++++ resources/icons/mmu_segmentation_.svg | 28 +++++++++++++++++++++ resources/icons/seam_.svg | 35 ++++++++++++++++++++++++++ resources/icons/shape_gallery.svg | 17 +++++++++++++ resources/icons/sinking.svg | 18 +++++++++++++ src/slic3r/GUI/MainFrame.cpp | 2 +- src/slic3r/GUI/ObjectDataViewModel.cpp | 13 +++++++--- src/slic3r/GUI/Plater.cpp | 2 ++ 8 files changed, 129 insertions(+), 5 deletions(-) create mode 100644 resources/icons/fdm_supports_.svg create mode 100644 resources/icons/mmu_segmentation_.svg create mode 100644 resources/icons/seam_.svg create mode 100644 resources/icons/shape_gallery.svg create mode 100644 resources/icons/sinking.svg diff --git a/resources/icons/fdm_supports_.svg b/resources/icons/fdm_supports_.svg new file mode 100644 index 0000000000..3efd9c184c --- /dev/null +++ b/resources/icons/fdm_supports_.svg @@ -0,0 +1,19 @@ + + + + + + + + + + diff --git a/resources/icons/mmu_segmentation_.svg b/resources/icons/mmu_segmentation_.svg new file mode 100644 index 0000000000..12d31fc266 --- /dev/null +++ b/resources/icons/mmu_segmentation_.svg @@ -0,0 +1,28 @@ + + + + + + diff --git a/resources/icons/seam_.svg b/resources/icons/seam_.svg new file mode 100644 index 0000000000..f909eee447 --- /dev/null +++ b/resources/icons/seam_.svg @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/icons/shape_gallery.svg b/resources/icons/shape_gallery.svg new file mode 100644 index 0000000000..a0b6fccf56 --- /dev/null +++ b/resources/icons/shape_gallery.svg @@ -0,0 +1,17 @@ + + + + + + + + + + diff --git a/resources/icons/sinking.svg b/resources/icons/sinking.svg new file mode 100644 index 0000000000..462b17120f --- /dev/null +++ b/resources/icons/sinking.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + diff --git a/src/slic3r/GUI/MainFrame.cpp b/src/slic3r/GUI/MainFrame.cpp index d953626c4d..7c3c99dbc0 100644 --- a/src/slic3r/GUI/MainFrame.cpp +++ b/src/slic3r/GUI/MainFrame.cpp @@ -1398,7 +1398,7 @@ void MainFrame::init_menubar_as_editor() if (!input_files.IsEmpty()) m_plater->sidebar().obj_list()->load_shape_object_from_gallery(input_files); } - }, "cog", nullptr, []() {return true; }, this); + }, "shape_gallery", nullptr, []() {return true; }, this); windowMenu->AppendSeparator(); append_menu_item(windowMenu, wxID_ANY, _L("Print &Host Upload Queue") + "\tCtrl+J", _L("Display the Print Host Upload Queue window"), diff --git a/src/slic3r/GUI/ObjectDataViewModel.cpp b/src/slic3r/GUI/ObjectDataViewModel.cpp index ed4b477b81..496cdcfc79 100644 --- a/src/slic3r/GUI/ObjectDataViewModel.cpp +++ b/src/slic3r/GUI/ObjectDataViewModel.cpp @@ -46,10 +46,10 @@ struct InfoItemAtributes { const std::map INFO_ITEMS{ // info_item Type info_item Name info_item BitmapName - { InfoItemType::CustomSupports, {L("Paint-on supports"), "fdm_supports" }, }, - { InfoItemType::CustomSeam, {L("Paint-on seam"), "seam" }, }, - { InfoItemType::MmuSegmentation, {L("Multimaterial painting"), "mmu_segmentation"}, }, - { InfoItemType::Sinking, {L("Sinking"), "support_blocker"}, }, + { InfoItemType::CustomSupports, {L("Paint-on supports"), "fdm_supports_" }, }, + { InfoItemType::CustomSeam, {L("Paint-on seam"), "seam_" }, }, + { InfoItemType::MmuSegmentation, {L("Multimaterial painting"), "mmu_segmentation_"}, }, + { InfoItemType::Sinking, {L("Sinking"), "sinking"}, }, { InfoItemType::VariableLayerHeight, {L("Variable layer height"), "layers"}, }, }; @@ -1682,6 +1682,9 @@ void ObjectDataViewModel::Rescale() m_warning_bmp = create_scaled_bitmap(WarningIcon); m_warning_manifold_bmp = create_scaled_bitmap(WarningManifoldIcon); + for (auto item : INFO_ITEMS) + m_info_bmps[item.first] = create_scaled_bitmap(item.second.bmp_name); + wxDataViewItemArray all_items; GetAllChildren(wxDataViewItem(0), all_items); @@ -1705,6 +1708,8 @@ void ObjectDataViewModel::Rescale() node->m_bmp = create_scaled_bitmap(LayerRootIcon); case itLayer: node->m_bmp = create_scaled_bitmap(LayerIcon); + case itInfo: + node->m_bmp = m_info_bmps.at(node->m_info_item_type); break; default: break; } diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index baa6fd4cef..197de43eeb 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -236,6 +236,7 @@ void ObjectInfo::show_sizer(bool show) void ObjectInfo::msw_rescale() { manifold_warning_icon->SetBitmap(create_scaled_bitmap(m_warning_icon_name)); + info_icon->SetBitmap(create_scaled_bitmap("info")); } void ObjectInfo::update_warning_icon(const std::string& warning_icon_name) @@ -1134,6 +1135,7 @@ void Sidebar::sys_color_changed() for (wxWindow* win : std::vector{ this, p->sliced_info->GetStaticBox(), p->object_info->GetStaticBox(), p->btn_reslice, p->btn_export_gcode }) wxGetApp().UpdateDarkUI(win); + p->object_info->msw_rescale(); for (wxWindow* win : std::vector{ p->scrolled, p->presets_panel }) wxGetApp().UpdateAllStaticTextDarkUI(win); for (wxWindow* btn : std::vector{ p->btn_reslice, p->btn_export_gcode }) From a840f8020f2dd87b65a051fe9453ee82766504a5 Mon Sep 17 00:00:00 2001 From: David Kocik Date: Thu, 2 Dec 2021 16:31:19 +0100 Subject: [PATCH 09/21] Substitution of host during ip resolve: correct handling of ipv6 --- src/slic3r/Utils/OctoPrint.cpp | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/src/slic3r/Utils/OctoPrint.cpp b/src/slic3r/Utils/OctoPrint.cpp index 0aec912c1d..6aa753404a 100644 --- a/src/slic3r/Utils/OctoPrint.cpp +++ b/src/slic3r/Utils/OctoPrint.cpp @@ -35,9 +35,16 @@ std::string substitute_host(const std::string& orig_addr, const std::string sub_ // userinfo size_t at = orig_addr.find("@"); host_start = (at != std::string::npos && at > host_start ? at + 1 : host_start); - // end of host, could be port, subpath (could be query or fragment?) - size_t host_end = orig_addr.find_first_of(":/?#", host_start); - host_end = (host_end == std::string::npos ? orig_addr.length() : host_end); + // end of host, could be port(:), subpath(/) (could be query(?) or fragment(#)?) + // or it will be ']' if address is ipv6 ) + size_t potencial_host_end = orig_addr.find_first_of(":/", host_start); + // if there are more ':' it must be ipv6 + if (potencial_host_end != std::string::npos && orig_addr[potencial_host_end] == ':' && orig_addr.rfind(':') != potencial_host_end) { + size_t ipv6_end = orig_addr.find(']', host_start); + // DK: Uncomment and replace orig_addr.length() if we want to allow subpath after ipv6 without [] parentheses. + potencial_host_end = (ipv6_end != std::string::npos ? ipv6_end + 1 : orig_addr.length()); //orig_addr.find('/', host_start)); + } + size_t host_end = (potencial_host_end != std::string::npos ? potencial_host_end : orig_addr.length()); // now host_start and host_end should mark where to put resolved addr // check host_start. if its nonsense, lets just use original addr (or resolved addr?) if (host_start >= orig_addr.length()) { From 21e5481a58cf5af2110e3fd2a812177afebe4614 Mon Sep 17 00:00:00 2001 From: Vojtech Bubnik Date: Thu, 2 Dec 2021 16:40:18 +0100 Subject: [PATCH 10/21] Fix of fan control for raft layers. Fixes Fan starts at first layer, even though disabled for first layer. #7232 This is a regression due to cooling refactoring, which cooled support layers independently from object layers. The bug here was that all the raft layers were cooled together with the first object layer. --- src/libslic3r/GCode.cpp | 14 ++++++++++---- src/libslic3r/SupportMaterial.cpp | 2 +- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/src/libslic3r/GCode.cpp b/src/libslic3r/GCode.cpp index 4ce078136d..cddb506c26 100644 --- a/src/libslic3r/GCode.cpp +++ b/src/libslic3r/GCode.cpp @@ -1998,13 +1998,19 @@ GCode::LayerResult GCode::process_layer( // Either printing all copies of all objects, or just a single copy of a single object. assert(single_object_instance_idx == size_t(-1) || layers.size() == 1); + // First object, support and raft layer, if available. const Layer *object_layer = nullptr; const SupportLayer *support_layer = nullptr; + const SupportLayer *raft_layer = nullptr; for (const LayerToPrint &l : layers) { - if (l.object_layer != nullptr && object_layer == nullptr) + if (l.object_layer && ! object_layer) object_layer = l.object_layer; - if (l.support_layer != nullptr && support_layer == nullptr) - support_layer = l.support_layer; + if (l.support_layer) { + if (! support_layer) + support_layer = l.support_layer; + if (! raft_layer && support_layer->id() < support_layer->object()->slicing_parameters().raft_layers()) + raft_layer = support_layer; + } } const Layer &layer = (object_layer != nullptr) ? *object_layer : *support_layer; GCode::LayerResult result { {}, layer.id(), false, last_layer }; @@ -2406,7 +2412,7 @@ GCode::LayerResult GCode::process_layer( log_memory_info(); result.gcode = std::move(gcode); - result.cooling_buffer_flush = object_layer || last_layer; + result.cooling_buffer_flush = object_layer || raft_layer || last_layer; return result; } diff --git a/src/libslic3r/SupportMaterial.cpp b/src/libslic3r/SupportMaterial.cpp index 9eb83eea1b..647d4bce80 100644 --- a/src/libslic3r/SupportMaterial.cpp +++ b/src/libslic3r/SupportMaterial.cpp @@ -1480,7 +1480,7 @@ static inline std::tuple detect_overhangs( overhang_polygons = to_polygons(layer.lslices); #endif // Expand for better stability. - contact_polygons = expand(overhang_polygons, scaled(object_config.raft_expansion.value)); + contact_polygons = object_config.raft_expansion.value > 0 ? expand(overhang_polygons, scaled(object_config.raft_expansion.value)) : overhang_polygons; } else if (! layer.regions().empty()) { From 7272b2b083448859ec63798326f7b6feead0d071 Mon Sep 17 00:00:00 2001 From: YuSanka Date: Thu, 2 Dec 2021 17:05:39 +0100 Subject: [PATCH 11/21] Fix for #7207 - Display of object labels does not match with the "View"-menu, if "Complete individual objects" is used --- src/slic3r/GUI/MainFrame.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/slic3r/GUI/MainFrame.cpp b/src/slic3r/GUI/MainFrame.cpp index 7c3c99dbc0..cca14e5703 100644 --- a/src/slic3r/GUI/MainFrame.cpp +++ b/src/slic3r/GUI/MainFrame.cpp @@ -319,8 +319,10 @@ static void add_tabs_as_menu(wxMenuBar* bar, MainFrame* main_frame, wxWindow* ba bar_parent->Bind(wxEVT_MENU_OPEN, [main_frame, bar, is_mainframe_menu](wxMenuEvent& event) { wxMenu* const menu = event.GetMenu(); - if (!menu || menu->GetMenuItemCount() > 0) + if (!menu || menu->GetMenuItemCount() > 0) { + event.Skip(); // it is verry important to next pricessing of the wxEVT_UPDATE_UI by this menu return; + } // update tab selection From 26a6cb21296cf8472639f26714f2ac5e6690ee34 Mon Sep 17 00:00:00 2001 From: Vojtech Bubnik Date: Thu, 2 Dec 2021 18:18:26 +0100 Subject: [PATCH 12/21] Fixed ironing over areas with modifier meshes: 1) Areas inside modifier meshes were ironed multiple times. 2) Ironing areas were not properly merged. Layer::lslices were not always properly merged with modifier meshes applied, which lead to the ironed surface being split and not fully ironed, as there were artificial gaps created between regions as if they were covered by perimeters (we don't iron over perimeters). --- src/libslic3r/Fill/Fill.cpp | 20 +++++++++++++------- src/libslic3r/Layer.cpp | 2 +- 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/src/libslic3r/Fill/Fill.cpp b/src/libslic3r/Fill/Fill.cpp index 726ba17a44..a3e4aee311 100644 --- a/src/libslic3r/Fill/Fill.cpp +++ b/src/libslic3r/Fill/Fill.cpp @@ -539,7 +539,7 @@ void Layer::make_ironing() fill_params.density = 1.; fill_params.monotonic = true; - for (size_t i = 0; i < by_extruder.size(); ++ i) { + for (size_t i = 0; i < by_extruder.size();) { // Find span of regions equivalent to the ironing operation. IroningParams &ironing_params = by_extruder[i]; size_t j = i; @@ -589,14 +589,17 @@ void Layer::make_ironing() polygons_append(infills, surface.expolygon); } } + + if (! infills.empty() || j > i + 1) { + // Ironing over more than a single region or over solid internal infill. + if (! infills.empty()) + // For IroningType::AllSolid only: + // Add solid infill areas for layers, that contain some non-ironable infil (sparse infill, bridge infill). + append(polys, std::move(infills)); + polys = union_safety_offset(polys); + } // Trim the top surfaces with half the nozzle diameter. ironing_areas = intersection_ex(polys, offset(this->lslices, - float(scale_(0.5 * nozzle_dmr)))); - if (! infills.empty()) { - // For IroningType::AllSolid only: - // Add solid infill areas for layers, that contain some non-ironable infil (sparse infill, bridge infill). - append(infills, to_polygons(std::move(ironing_areas))); - ironing_areas = union_safety_offset_ex(infills); - } } // Create the filler object. @@ -626,6 +629,9 @@ void Layer::make_ironing() flow_mm3_per_mm, extrusion_width, float(extrusion_height)); } } + + // Regions up to j were processed. + i = j; } } diff --git a/src/libslic3r/Layer.cpp b/src/libslic3r/Layer.cpp index 39228516c0..5c661ed68b 100644 --- a/src/libslic3r/Layer.cpp +++ b/src/libslic3r/Layer.cpp @@ -45,7 +45,7 @@ void Layer::make_slices() Polygons slices_p; for (LayerRegion *layerm : m_regions) polygons_append(slices_p, to_polygons(layerm->slices.surfaces)); - slices = union_ex(slices_p); + slices = union_safety_offset_ex(slices_p); } this->lslices.clear(); From f88d678a4a8957377f0145749e594d67225527cf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Luk=C3=A1=C5=A1=20Hejl?= Date: Thu, 2 Dec 2021 21:04:55 +0100 Subject: [PATCH 13/21] Added a missing include (GCC11.1 without PCH). --- src/slic3r/GUI/OptionsGroup.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/src/slic3r/GUI/OptionsGroup.cpp b/src/slic3r/GUI/OptionsGroup.cpp index 4fe0e186eb..9f00fae9cb 100644 --- a/src/slic3r/GUI/OptionsGroup.cpp +++ b/src/slic3r/GUI/OptionsGroup.cpp @@ -4,6 +4,7 @@ #include "GUI_App.hpp" #include "OG_CustomCtrl.hpp" #include "MsgDialog.hpp" +#include "format.hpp" #include #include From f4dfbb69e2e4397ff7369fd1709ecf6da17aab83 Mon Sep 17 00:00:00 2001 From: Vojtech Bubnik Date: Fri, 3 Dec 2021 08:02:16 +0100 Subject: [PATCH 14/21] Replaced the code to substitute host address part in URL with libcurl library calls. This solution should be more robust than a homebrew URL parser solution. --- src/slic3r/GUI/Plater.cpp | 1 + src/slic3r/Utils/OctoPrint.cpp | 63 +++++++++++++++++++++++++-------- src/slic3r/Utils/TCPConsole.hpp | 2 ++ 3 files changed, 51 insertions(+), 15 deletions(-) diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index 197de43eeb..c2dfcb24cc 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -2219,6 +2219,7 @@ Plater::priv::~priv() { if (config != nullptr) delete config; + // Saves the database of visited (already shown) hints into hints.ini. notification_manager->deactivate_loaded_hints(); } diff --git a/src/slic3r/Utils/OctoPrint.cpp b/src/slic3r/Utils/OctoPrint.cpp index 6aa753404a..9842d7d89a 100644 --- a/src/slic3r/Utils/OctoPrint.cpp +++ b/src/slic3r/Utils/OctoPrint.cpp @@ -5,13 +5,15 @@ #include #include #include -#include #include #include #include +#include + #include +#include "slic3r/GUI/GUI.hpp" #include "slic3r/GUI/I18N.hpp" #include "slic3r/GUI/GUI.hpp" #include "Http.hpp" @@ -25,8 +27,13 @@ namespace pt = boost::property_tree; namespace Slic3r { namespace { -std::string substitute_host(const std::string& orig_addr, const std::string sub_addr) +std::string substitute_host(const std::string& orig_addr, std::string sub_addr) { + // put ipv6 into [] brackets + if (sub_addr.find(':') != std::string::npos && sub_addr.at(0) != '[') + sub_addr = "[" + sub_addr + "]"; + +#if 0 //URI = scheme ":"["//"[userinfo "@"] host [":" port]] path["?" query]["#" fragment] std::string final_addr = orig_addr; // http @@ -52,6 +59,35 @@ std::string substitute_host(const std::string& orig_addr, const std::string sub_ } final_addr.replace(host_start, host_end - host_start, sub_addr); return final_addr; +#else + // Using the new CURL API for handling URL. https://everything.curl.dev/libcurl/url + // If anything fails, return the input unchanged. + std::string out = orig_addr; + CURLU *hurl = curl_url(); + if (hurl) { + // Parse the input URL. + CURLUcode rc = curl_url_set(hurl, CURLUPART_URL, orig_addr.c_str(), 0); + if (rc == CURLUE_OK) { + // Replace the address. + rc = curl_url_set(hurl, CURLUPART_HOST, sub_addr.c_str(), 0); + if (rc == CURLUE_OK) { + // Extract a string fromt the CURL URL handle. + char *url; + rc = curl_url_get(hurl, CURLUPART_URL, &url, 0); + if (rc == CURLUE_OK) { + out = url; + curl_free(url); + } else + BOOST_LOG_TRIVIAL(error) << "OctoPrint substitute_host: failed to extract the URL after substitution"; + } else + BOOST_LOG_TRIVIAL(error) << "OctoPrint substitute_host: failed to substitute host " << sub_addr << " in URL " << orig_addr; + } else + BOOST_LOG_TRIVIAL(error) << "OctoPrint substitute_host: failed to parse URL " << orig_addr; + curl_url_cleanup(hurl); + } else + BOOST_LOG_TRIVIAL(error) << "OctoPrint substitute_host: failed to allocate curl_url"; + return out; +#endif } } //namespace @@ -110,7 +146,7 @@ bool OctoPrint::test(wxString &msg) const #ifdef WIN32 .ssl_revoke_best_effort(m_ssl_revoke_best_effort) .on_ip_resolve([&](std::string address) { - msg = boost::nowide::widen(address); + msg = GUI::from_u8(address); }) #endif .perform_sync(); @@ -138,9 +174,11 @@ bool OctoPrint::upload(PrintHostUpload upload_data, ProgressFn prorgess_fn, Erro const auto upload_filename = upload_data.upload_path.filename(); const auto upload_parent_path = upload_data.upload_path.parent_path(); - wxString test_msg; - if (! test(test_msg)) { - error_fn(std::move(test_msg)); + // If test fails, test_msg_or_host_ip contains the error message. + // Otherwise on Windows it contains the resolved IP address of the host. + wxString test_msg_or_host_ip; + if (! test(test_msg_or_host_ip)) { + error_fn(std::move(test_msg_or_host_ip)); return false; } @@ -149,23 +187,18 @@ bool OctoPrint::upload(PrintHostUpload upload_data, ProgressFn prorgess_fn, Erro bool allow_ip_resolve = GUI::get_app_config()->get("allow_ip_resolve") == "1"; - if (m_host.find("https://") == 0 || test_msg.empty() || !allow_ip_resolve) { + if (m_host.find("https://") == 0 || test_msg_or_host_ip.empty() || !allow_ip_resolve) { // If https is entered we assume signed ceritificate is being used // IP resolving will not happen - it could resolve into address not being specified in cert url = make_url("api/files/local"); } else { // Curl uses easy_getinfo to get ip address of last successful transaction. // If it got the address use it instead of the stored in "host" variable. - // This new address returns in "test_msg" variable. + // This new address returns in "test_msg_or_host_ip" variable. // Solves troubles of uploades failing with name address. - std::string resolved_addr = boost::nowide::narrow(test_msg); - // put ipv6 into [] brackets - if (resolved_addr.find(':') != std::string::npos && resolved_addr.at(0) != '[') - resolved_addr = "[" + resolved_addr + "]"; // in original address (m_host) replace host for resolved ip - std::string final_addr = substitute_host(m_host, resolved_addr); - BOOST_LOG_TRIVIAL(debug) << "Upload address after ip resolve: " << final_addr; - url = make_url("api/files/local", final_addr); + url = substitute_host(make_url("api/files/local", m_host), GUI::into_u8(test_msg_or_host_ip)); + BOOST_LOG_TRIVIAL(info) << "Upload address after ip resolve: " << url; } BOOST_LOG_TRIVIAL(info) << boost::format("%1%: Uploading file %2% at %3%, filename: %4%, path: %5%, print: %6%") diff --git a/src/slic3r/Utils/TCPConsole.hpp b/src/slic3r/Utils/TCPConsole.hpp index 7c0e1d2901..d353634e87 100644 --- a/src/slic3r/Utils/TCPConsole.hpp +++ b/src/slic3r/Utils/TCPConsole.hpp @@ -13,6 +13,8 @@ namespace Utils { using boost::asio::ip::tcp; +// Generic command / response TCP telnet like console class. +// Used by the MKS host to send G-code commands to test connection ("M105") and to start printing ("M23 filename", "M24"). class TCPConsole { public: From 4d1ce89c2261ad3d9e72e22c5749839dbd1300ec Mon Sep 17 00:00:00 2001 From: Vojtech Bubnik Date: Fri, 3 Dec 2021 08:12:47 +0100 Subject: [PATCH 15/21] Follow-up to f4dfbb69e2e4397ff7369fd1709ecf6da17aab83 Now that libcurl is used for URL host substitution, we want to make sure that the new code compiles on Windows only because that is where we need to do the URL host substitution due to Windows 10/11 mDNS resolve issues and because we have a control on the libcurl version statically linked on Windows, so we are sure the URL API is available. --- src/slic3r/Utils/OctoPrint.cpp | 21 ++++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/src/slic3r/Utils/OctoPrint.cpp b/src/slic3r/Utils/OctoPrint.cpp index 9842d7d89a..43e6145f50 100644 --- a/src/slic3r/Utils/OctoPrint.cpp +++ b/src/slic3r/Utils/OctoPrint.cpp @@ -26,6 +26,8 @@ namespace pt = boost::property_tree; namespace Slic3r { +#ifdef WIN32 +// Workaround for Windows 10/11 mDNS resolve issue, where two mDNS resolves in succession fail. namespace { std::string substitute_host(const std::string& orig_addr, std::string sub_addr) { @@ -90,6 +92,7 @@ std::string substitute_host(const std::string& orig_addr, std::string sub_addr) #endif } } //namespace +#endif // WIN32 OctoPrint::OctoPrint(DynamicPrintConfig *config) : m_host(config->opt_string("print_host")), @@ -146,9 +149,11 @@ bool OctoPrint::test(wxString &msg) const #ifdef WIN32 .ssl_revoke_best_effort(m_ssl_revoke_best_effort) .on_ip_resolve([&](std::string address) { + // Workaround for Windows 10/11 mDNS resolve issue, where two mDNS resolves in succession fail. + // Remember resolved address to be reused at successive REST API call. msg = GUI::from_u8(address); }) -#endif +#endif // WIN32 .perform_sync(); return res; @@ -185,13 +190,18 @@ bool OctoPrint::upload(PrintHostUpload upload_data, ProgressFn prorgess_fn, Erro std::string url; bool res = true; - bool allow_ip_resolve = GUI::get_app_config()->get("allow_ip_resolve") == "1"; - - if (m_host.find("https://") == 0 || test_msg_or_host_ip.empty() || !allow_ip_resolve) { +#ifdef WIN32 + // Workaround for Windows 10/11 mDNS resolve issue, where two mDNS resolves in succession fail. + if (m_host.find("https://") == 0 || test_msg_or_host_ip.empty() || GUI::get_app_config()->get("allow_ip_resolve") != "1") +#endif // _WIN32 + { // If https is entered we assume signed ceritificate is being used // IP resolving will not happen - it could resolve into address not being specified in cert url = make_url("api/files/local"); - } else { + } +#ifdef WIN32 + else { + // Workaround for Windows 10/11 mDNS resolve issue, where two mDNS resolves in succession fail. // Curl uses easy_getinfo to get ip address of last successful transaction. // If it got the address use it instead of the stored in "host" variable. // This new address returns in "test_msg_or_host_ip" variable. @@ -200,6 +210,7 @@ bool OctoPrint::upload(PrintHostUpload upload_data, ProgressFn prorgess_fn, Erro url = substitute_host(make_url("api/files/local", m_host), GUI::into_u8(test_msg_or_host_ip)); BOOST_LOG_TRIVIAL(info) << "Upload address after ip resolve: " << url; } +#endif // _WIN32 BOOST_LOG_TRIVIAL(info) << boost::format("%1%: Uploading file %2% at %3%, filename: %4%, path: %5%, print: %6%") % name From 45d9e6bddd52164aaa12c503e5323de307ad8215 Mon Sep 17 00:00:00 2001 From: Vojtech Bubnik Date: Fri, 3 Dec 2021 09:05:14 +0100 Subject: [PATCH 16/21] Follow-up to 7828964f8ca1c5ca029ef1ec5544769696e4eb2a Fixed no way of leaving the "export G-code" dialog loop. Improved the error message by explaining that some characters are not allowed by a FAT file system. --- src/slic3r/GUI/Plater.cpp | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index c2dfcb24cc..c368161f2b 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -138,11 +138,10 @@ bool Plater::has_illegal_filename_characters(const std::string& name) void Plater::show_illegal_characters_warning(wxWindow* parent) { - show_error(parent, _L("The supplied name is not valid;") + "\n" + + show_error(parent, _L("The provided name is not valid;") + "\n" + _L("the following characters are not allowed:") + " <>:/\\|?*\""); } - // Sidebar widgets // struct InfoBox : public wxStaticBox @@ -5656,10 +5655,15 @@ void Plater::export_gcode(bool prefer_removable) if (dlg.ShowModal() == wxID_OK) { output_path = into_path(dlg.GetPath()); while (has_illegal_filename_characters(output_path.filename().string())) { - show_illegal_characters_warning(this); + show_error(this, _L("The provided file name is not valid.") + "\n" + + _L("The following characters are not allowed by a FAT file system:") + " <>:/\\|?*\""); dlg.SetFilename(from_path(output_path.filename())); if (dlg.ShowModal() == wxID_OK) output_path = into_path(dlg.GetPath()); + else { + output_path.clear(); + break; + } } } } From 50da39d30b3787f59ea58607444c115c60833bd9 Mon Sep 17 00:00:00 2001 From: Vojtech Bubnik Date: Fri, 3 Dec 2021 09:26:44 +0100 Subject: [PATCH 17/21] Windows specific: Only start the 3rd party updater application if enabled in PrusaSlicer preferences. --- src/slic3r/GUI/GUI_App.cpp | 4 ++-- src/slic3r/Utils/PresetUpdater.cpp | 5 +++++ src/slic3r/Utils/PresetUpdater.hpp | 3 +++ 3 files changed, 10 insertions(+), 2 deletions(-) diff --git a/src/slic3r/GUI/GUI_App.cpp b/src/slic3r/GUI/GUI_App.cpp index 7aff9a6cad..c7654fcd65 100644 --- a/src/slic3r/GUI/GUI_App.cpp +++ b/src/slic3r/GUI/GUI_App.cpp @@ -779,8 +779,8 @@ void GUI_App::post_init() show_send_system_info_dialog_if_needed(); } #ifdef _WIN32 - // Run external updater on Windows. - if (! run_updater_win()) + // Run external updater on Windows if version check is enabled. + if (this->preset_updater->version_check_enabled() && ! run_updater_win()) // "prusaslicer-updater.exe" was not started, run our own update check. #endif // _WIN32 this->preset_updater->slic3r_update_notify(); diff --git a/src/slic3r/Utils/PresetUpdater.cpp b/src/slic3r/Utils/PresetUpdater.cpp index f93864c2e6..2b458df537 100644 --- a/src/slic3r/Utils/PresetUpdater.cpp +++ b/src/slic3r/Utils/PresetUpdater.cpp @@ -954,4 +954,9 @@ void PresetUpdater::on_update_notification_confirm() } } +bool PresetUpdater::version_check_enabled() const +{ + return p->enabled_version_check; +} + } diff --git a/src/slic3r/Utils/PresetUpdater.hpp b/src/slic3r/Utils/PresetUpdater.hpp index 1313c3df83..97d85a4eae 100644 --- a/src/slic3r/Utils/PresetUpdater.hpp +++ b/src/slic3r/Utils/PresetUpdater.hpp @@ -57,6 +57,9 @@ public: bool install_bundles_rsrc(std::vector bundles, bool snapshot = true) const; void on_update_notification_confirm(); + + bool version_check_enabled() const; + private: struct priv; std::unique_ptr p; From def5bd6797666d7d962051629e17fdf6e0c07d71 Mon Sep 17 00:00:00 2001 From: YuSanka Date: Fri, 3 Dec 2021 09:38:50 +0100 Subject: [PATCH 18/21] Follow-up to https://github.com/prusa3d/PrusaSlicer/commit/7272b2b083448859ec63798326f7b6feead0d071 Comment is extended --- src/slic3r/GUI/MainFrame.cpp | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/slic3r/GUI/MainFrame.cpp b/src/slic3r/GUI/MainFrame.cpp index cca14e5703..8a9702c400 100644 --- a/src/slic3r/GUI/MainFrame.cpp +++ b/src/slic3r/GUI/MainFrame.cpp @@ -320,7 +320,10 @@ static void add_tabs_as_menu(wxMenuBar* bar, MainFrame* main_frame, wxWindow* ba bar_parent->Bind(wxEVT_MENU_OPEN, [main_frame, bar, is_mainframe_menu](wxMenuEvent& event) { wxMenu* const menu = event.GetMenu(); if (!menu || menu->GetMenuItemCount() > 0) { - event.Skip(); // it is verry important to next pricessing of the wxEVT_UPDATE_UI by this menu + // If we are here it means that we open regular menu and not a tab used as a menu + event.Skip(); // event.Skip() is verry important to next processing of the wxEVT_UPDATE_UI by this menu items. + // If wxEVT_MENU_OPEN will not be pocessed in next event queue then MenuItems of this menu will never caught wxEVT_UPDATE_UI + // and, as a result, "check/radio value" will not be updated return; } From 7837070d29c0f40a968f2460d82c95b2e4cbd108 Mon Sep 17 00:00:00 2001 From: Vojtech Bubnik Date: Fri, 3 Dec 2021 09:39:50 +0100 Subject: [PATCH 19/21] Follow-up to c6de3e84eb42969c78f4fdb64f7e413d43674f6b Fixed typos in option labels. --- src/libslic3r/PrintConfig.cpp | 4 ++-- src/slic3r/GUI/Tab.cpp | 8 +++----- 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/src/libslic3r/PrintConfig.cpp b/src/libslic3r/PrintConfig.cpp index 9a4db03f6f..011539aa4e 100644 --- a/src/libslic3r/PrintConfig.cpp +++ b/src/libslic3r/PrintConfig.cpp @@ -3188,7 +3188,7 @@ void PrintConfigDef::init_sla_params() def = this->add("relative_correction_y", coFloat); def->label = L("Printer scaling correction in Y axis"); - def->full_label = L("Printer scaling X axis correction"); + def->full_label = L("Printer scaling Y axis correction"); def->tooltip = L("Printer scaling correction in Y axis"); def->min = 0; def->mode = comExpert; @@ -3196,7 +3196,7 @@ void PrintConfigDef::init_sla_params() def = this->add("relative_correction_z", coFloat); def->label = L("Printer scaling correction in Z axis"); - def->full_label = L("Printer scaling X axis correction"); + def->full_label = L("Printer scaling Z axis correction"); def->tooltip = L("Printer scaling correction in Z axis"); def->min = 0; def->mode = comExpert; diff --git a/src/slic3r/GUI/Tab.cpp b/src/slic3r/GUI/Tab.cpp index 518017db98..d5bb67a751 100644 --- a/src/slic3r/GUI/Tab.cpp +++ b/src/slic3r/GUI/Tab.cpp @@ -2493,8 +2493,7 @@ void TabPrinter::build_sla() optgroup = page->new_optgroup(L("Corrections")); line = Line{ m_config->def()->get("relative_correction")->full_label, "" }; - std::vector axes{ "X", "Y", "Z" }; - for (auto& axis : axes) { + for (auto& axis : { "X", "Y", "Z" }) { auto opt = optgroup->get_option(std::string("relative_correction_") + char(std::tolower(axis[0]))); opt.opt.label = axis; line.append_option(opt); @@ -2603,7 +2602,7 @@ PageShp TabPrinter::build_kinematics_page() optgroup->append_line(line); } - std::vector axes{ "x", "y", "z", "e" }; + const std::vector axes{ "x", "y", "z", "e" }; optgroup = page->new_optgroup(L("Maximum feedrates")); for (const std::string &axis : axes) { append_option_line(optgroup, "machine_max_feedrate_" + axis); @@ -4217,8 +4216,7 @@ void TabSLAMaterial::build() optgroup = page->new_optgroup(L("Corrections")); auto line = Line{ m_config->def()->get("material_correction")->full_label, "" }; - std::vector axes{ "X", "Y", "Z" }; - for (auto& axis : axes) { + for (auto& axis : { "X", "Y", "Z" }) { auto opt = optgroup->get_option(std::string("material_correction_") + char(std::tolower(axis[0]))); opt.opt.label = axis; line.append_option(opt); From 08e3e60a5f6ae014e7d6019cd951403f04f333f2 Mon Sep 17 00:00:00 2001 From: tamasmeszaros Date: Fri, 3 Dec 2021 09:46:07 +0100 Subject: [PATCH 20/21] Fix CGAL build with major version > 4 on Linux fixes #7341 CGAL upstream CMake config will lock in the major version if installed as a distro package and will not provide any version info if compiled and installed from upstream. As of this commit, PrusaSlicer can be built with CGAL 4.13.2, and 5.0 but there is no universal way to specify the minimum version that would work with static dependencies and linux packages. sorry #4912 --- src/libslic3r/CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libslic3r/CMakeLists.txt b/src/libslic3r/CMakeLists.txt index 6f5b264200..deaa0b9241 100644 --- a/src/libslic3r/CMakeLists.txt +++ b/src/libslic3r/CMakeLists.txt @@ -298,7 +298,7 @@ set(CGAL_DO_NOT_WARN_ABOUT_CMAKE_BUILD_TYPE ON CACHE BOOL "" FORCE) cmake_policy(PUSH) cmake_policy(SET CMP0011 NEW) -find_package(CGAL 4.13 REQUIRED) +find_package(CGAL REQUIRED) cmake_policy(POP) add_library(libslic3r_cgal STATIC MeshBoolean.cpp MeshBoolean.hpp TryCatchSignal.hpp From 60b8a8245c15701d1da4441ecc8c396d2b262b91 Mon Sep 17 00:00:00 2001 From: David Kocik Date: Fri, 3 Dec 2021 09:47:51 +0100 Subject: [PATCH 21/21] Deleted unused function in Octoprint --- src/slic3r/Utils/OctoPrint.cpp | 17 +---------------- src/slic3r/Utils/OctoPrint.hpp | 1 - 2 files changed, 1 insertion(+), 17 deletions(-) diff --git a/src/slic3r/Utils/OctoPrint.cpp b/src/slic3r/Utils/OctoPrint.cpp index 43e6145f50..250b16b4ac 100644 --- a/src/slic3r/Utils/OctoPrint.cpp +++ b/src/slic3r/Utils/OctoPrint.cpp @@ -207,7 +207,7 @@ bool OctoPrint::upload(PrintHostUpload upload_data, ProgressFn prorgess_fn, Erro // This new address returns in "test_msg_or_host_ip" variable. // Solves troubles of uploades failing with name address. // in original address (m_host) replace host for resolved ip - url = substitute_host(make_url("api/files/local", m_host), GUI::into_u8(test_msg_or_host_ip)); + url = substitute_host(make_url("api/files/local"), GUI::into_u8(test_msg_or_host_ip)); BOOST_LOG_TRIVIAL(info) << "Upload address after ip resolve: " << url; } #endif // _WIN32 @@ -276,21 +276,6 @@ std::string OctoPrint::make_url(const std::string &path) const } } -std::string OctoPrint::make_url(const std::string& path, const std::string& addr) const -{ - std::string hst = addr.empty() ? m_host : addr; - if (hst.find("http://") == 0 || hst.find("https://") == 0) { - if (hst.back() == '/') { - return (boost::format("%1%%2%") % hst % path).str(); - } - else { - return (boost::format("%1%/%2%") % hst % path).str(); - } - } else { - return (boost::format("http://%1%/%2%") % hst % path).str(); - } -} - SL1Host::SL1Host(DynamicPrintConfig *config) : OctoPrint(config), m_authorization_type(dynamic_cast*>(config->option("printhost_authorization_type"))->value), diff --git a/src/slic3r/Utils/OctoPrint.hpp b/src/slic3r/Utils/OctoPrint.hpp index 7945cfdb1a..262efe9ff5 100644 --- a/src/slic3r/Utils/OctoPrint.hpp +++ b/src/slic3r/Utils/OctoPrint.hpp @@ -44,7 +44,6 @@ private: virtual void set_auth(Http &http) const; std::string make_url(const std::string &path) const; - std::string make_url(const std::string& path, const std::string& addr) const; }; class SL1Host: public OctoPrint