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/libslic3r/CMakeLists.txt b/src/libslic3r/CMakeLists.txt index 2b7ff6256d..ca0976de6a 100644 --- a/src/libslic3r/CMakeLists.txt +++ b/src/libslic3r/CMakeLists.txt @@ -303,7 +303,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.hpp MeshBoolean.cpp 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/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/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(); 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/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()) { diff --git a/src/libslic3r/TriangleSelector.cpp b/src/libslic3r/TriangleSelector.cpp index 1699786887..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 { @@ -124,24 +230,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 +265,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 +890,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 +942,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 +966,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,71 +999,80 @@ 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::Cursor::is_facet_visible(const Cursor &cursor, int facet_idx, const std::vector &face_normals) { - 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 (!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? -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 +// 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] = 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; - 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; - - // 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()) + 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::Circle::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 &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; + + // 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(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 +1113,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 +1151,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 +1211,6 @@ TriangleSelector::TriangleSelector(const TriangleMesh& mesh) reset(); } - void TriangleSelector::reset() { m_vertices.clear(); @@ -1124,17 +1231,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,54 +1794,132 @@ 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 = float(radius_world / sf(0)); + radius_sqr = float(Slic3r::sqr(radius_world / sf(0))); 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 = radius_world; + radius_sqr = Slic3r::sqr(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 +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); + if ((center - transformed_point).squaredNorm() < radius_sqr) + return is_mesh_point_not_clipped(point, clipping_plane); + + return false; +} + +// 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; - 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! -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 +1940,108 @@ 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); +} + +// 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 b9f136c2e2..09b833e825 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,146 @@ 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 = 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_); + + Transform3f trafo; + Vec3f source; + + bool uniform_scaling; + Transform3f trafo_normal; + float radius; + 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_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 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: + 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; + 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 + { + 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_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; 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 +196,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 +335,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/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>; diff --git a/src/slic3r/GUI/GUI_App.cpp b/src/slic3r/GUI/GUI_App.cpp index 60f8e21a9c..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(); @@ -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 eec7084f6a..8dad6be484 100644 --- a/src/slic3r/GUI/GUI_App.hpp +++ b/src/slic3r/GUI/GUI_App.hpp @@ -133,6 +133,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 @@ -218,6 +219,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/Gizmos/GLGizmoPainterBase.cpp b/src/slic3r/GUI/Gizmos/GLGizmoPainterBase.cpp index 821ce367af..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,50 +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; - } 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); - m_triangle_selectors[m_rr.mesh_id]->request_update_render_data(); + m_seed_fill_last_mesh_id = -1; + } + } else if (m_tool_type == ToolType::BRUSH) { + 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[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/MainFrame.cpp b/src/slic3r/GUI/MainFrame.cpp index d953626c4d..8a9702c400 100644 --- a/src/slic3r/GUI/MainFrame.cpp +++ b/src/slic3r/GUI/MainFrame.cpp @@ -319,8 +319,13 @@ 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) { + // 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; + } // update tab selection @@ -1398,7 +1403,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/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: diff --git a/src/slic3r/GUI/NotificationManager.cpp b/src/slic3r/GUI/NotificationManager.cpp index ef299bf09d..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) @@ -393,8 +363,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 +650,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 +771,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 +786,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 diff --git a/src/slic3r/GUI/NotificationManager.hpp b/src/slic3r/GUI/NotificationManager.hpp index 94048b798b..af1d836eb9 100644 --- a/src/slic3r/GUI/NotificationManager.hpp +++ b/src/slic3r/GUI/NotificationManager.hpp @@ -749,7 +749,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 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/ObjectDataViewModel.cpp b/src/slic3r/GUI/ObjectDataViewModel.cpp index 8a7cb35ad1..8e82ffbaaf 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"}, }, }; @@ -1680,6 +1680,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); @@ -1703,6 +1706,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/OptionsGroup.cpp b/src/slic3r/GUI/OptionsGroup.cpp index c4c123a484..9f00fae9cb 100644 --- a/src/slic3r/GUI/OptionsGroup.cpp +++ b/src/slic3r/GUI/OptionsGroup.cpp @@ -3,6 +3,8 @@ #include "Plater.hpp" #include "GUI_App.hpp" #include "OG_CustomCtrl.hpp" +#include "MsgDialog.hpp" +#include "format.hpp" #include #include @@ -10,6 +12,7 @@ #include #include "libslic3r/Exception.hpp" #include "libslic3r/Utils.hpp" +#include "libslic3r/AppConfig.hpp" #include "I18N.hpp" namespace Slic3r { namespace GUI { @@ -504,7 +507,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 +965,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 +1030,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/Plater.cpp b/src/slic3r/GUI/Plater.cpp index e256ea866b..d881873583 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -141,11 +141,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 @@ -239,6 +238,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) @@ -1137,6 +1137,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 }) @@ -2225,6 +2226,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(); } @@ -2744,16 +2746,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(); @@ -5664,10 +5667,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; + } } } } diff --git a/src/slic3r/GUI/Tab.cpp b/src/slic3r/GUI/Tab.cpp index 6ea044f406..d5bb67a751 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); }); @@ -2480,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); @@ -2590,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); @@ -2695,7 +2707,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 +2746,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 +2799,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 +3748,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; @@ -4204,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); @@ -4224,7 +4235,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 +4243,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 +4382,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 diff --git a/src/slic3r/Utils/OctoPrint.cpp b/src/slic3r/Utils/OctoPrint.cpp index 0aec912c1d..250b16b4ac 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" @@ -24,9 +26,16 @@ 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, 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 @@ -35,9 +44,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()) { @@ -45,8 +61,38 @@ 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 +#endif // WIN32 OctoPrint::OctoPrint(DynamicPrintConfig *config) : m_host(config->opt_string("print_host")), @@ -103,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) { - msg = boost::nowide::widen(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; @@ -131,35 +179,38 @@ 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; } 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.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" 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"), 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 @@ -225,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 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; 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: